Merge branch 'feature/reverse-ens-tx-list' into develop
This commit is contained in:
commit
689723655d
|
@ -12,6 +12,7 @@ import ResultHeader from "./search/ResultHeader";
|
||||||
import PendingResults from "./search/PendingResults";
|
import PendingResults from "./search/PendingResults";
|
||||||
import TransactionItem from "./search/TransactionItem";
|
import TransactionItem from "./search/TransactionItem";
|
||||||
import { SearchController } from "./search/search";
|
import { SearchController } from "./search/search";
|
||||||
|
import { useENSCache } from "./useReverseCache";
|
||||||
import { useFeeToggler } from "./search/useFeeToggler";
|
import { useFeeToggler } from "./search/useFeeToggler";
|
||||||
import { provider } from "./ethersconfig";
|
import { provider } from "./ethersconfig";
|
||||||
|
|
||||||
|
@ -41,8 +42,20 @@ const AddressTransactions: React.FC = () => {
|
||||||
// If it looks like it is an ENS name, try to resolve it
|
// If it looks like it is an ENS name, try to resolve it
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ethers.utils.isAddress(params.addressOrName)) {
|
if (ethers.utils.isAddress(params.addressOrName)) {
|
||||||
|
setENS(false);
|
||||||
|
setError(false);
|
||||||
|
|
||||||
// Normalize to checksummed address
|
// Normalize to checksummed address
|
||||||
setChecksummedAddress(ethers.utils.getAddress(params.addressOrName));
|
const _checksummedAddress = ethers.utils.getAddress(params.addressOrName);
|
||||||
|
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);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,27 +63,16 @@ const AddressTransactions: React.FC = () => {
|
||||||
const resolvedAddress = await provider.resolveName(params.addressOrName);
|
const resolvedAddress = await provider.resolveName(params.addressOrName);
|
||||||
if (resolvedAddress !== null) {
|
if (resolvedAddress !== null) {
|
||||||
setENS(true);
|
setENS(true);
|
||||||
setChecksummedAddress(resolvedAddress);
|
|
||||||
setError(false);
|
setError(false);
|
||||||
|
setChecksummedAddress(resolvedAddress);
|
||||||
} else {
|
} else {
|
||||||
|
setENS(false);
|
||||||
setError(true);
|
setError(true);
|
||||||
|
setChecksummedAddress(undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
resolveName();
|
resolveName();
|
||||||
}, [params.addressOrName]);
|
}, [params.addressOrName, history, params.direction, location.search]);
|
||||||
|
|
||||||
// Request came with a non-checksummed address; fix the URL
|
|
||||||
if (
|
|
||||||
!isENS &&
|
|
||||||
checksummedAddress &&
|
|
||||||
params.addressOrName !== checksummedAddress
|
|
||||||
) {
|
|
||||||
history.replace(
|
|
||||||
`/address/${checksummedAddress}${
|
|
||||||
params.direction ? "/" + params.direction : ""
|
|
||||||
}${location.search}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [controller, setController] = useState<SearchController>();
|
const [controller, setController] = useState<SearchController>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -128,6 +130,7 @@ const AddressTransactions: React.FC = () => {
|
||||||
}, [checksummedAddress, params.direction, hash, controller]);
|
}, [checksummedAddress, params.direction, hash, controller]);
|
||||||
|
|
||||||
const page = useMemo(() => controller?.getPage(), [controller]);
|
const page = useMemo(() => controller?.getPage(), [controller]);
|
||||||
|
const reverseCache = useENSCache(page);
|
||||||
|
|
||||||
document.title = `Address ${params.addressOrName} | Otterscan`;
|
document.title = `Address ${params.addressOrName} | Otterscan`;
|
||||||
|
|
||||||
|
@ -189,6 +192,7 @@ const AddressTransactions: React.FC = () => {
|
||||||
<TransactionItem
|
<TransactionItem
|
||||||
key={tx.hash}
|
key={tx.hash}
|
||||||
tx={tx}
|
tx={tx}
|
||||||
|
ensCache={reverseCache}
|
||||||
selectedAddress={checksummedAddress}
|
selectedAddress={checksummedAddress}
|
||||||
feeDisplay={feeDisplay}
|
feeDisplay={feeDisplay}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -14,6 +14,7 @@ import BlockLink from "./components/BlockLink";
|
||||||
import { ProcessedTransaction } from "./types";
|
import { ProcessedTransaction } from "./types";
|
||||||
import { PAGE_SIZE } from "./params";
|
import { PAGE_SIZE } from "./params";
|
||||||
import { useFeeToggler } from "./search/useFeeToggler";
|
import { useFeeToggler } from "./search/useFeeToggler";
|
||||||
|
import { useENSCache } from "./useReverseCache";
|
||||||
|
|
||||||
type BlockParams = {
|
type BlockParams = {
|
||||||
blockNumber: string;
|
blockNumber: string;
|
||||||
|
@ -80,6 +81,8 @@ const BlockTransactions: React.FC = () => {
|
||||||
}, [txs, pageNumber]);
|
}, [txs, pageNumber]);
|
||||||
const total = useMemo(() => txs?.length ?? 0, [txs]);
|
const total = useMemo(() => txs?.length ?? 0, [txs]);
|
||||||
|
|
||||||
|
const reverseCache = useENSCache(page);
|
||||||
|
|
||||||
document.title = `Block #${blockNumber} Txns | Otterscan`;
|
document.title = `Block #${blockNumber} Txns | Otterscan`;
|
||||||
|
|
||||||
const [feeDisplay, feeDisplayToggler] = useFeeToggler();
|
const [feeDisplay, feeDisplayToggler] = useFeeToggler();
|
||||||
|
@ -112,7 +115,12 @@ const BlockTransactions: React.FC = () => {
|
||||||
{page ? (
|
{page ? (
|
||||||
<>
|
<>
|
||||||
{page.map((tx) => (
|
{page.map((tx) => (
|
||||||
<TransactionItem key={tx.hash} tx={tx} feeDisplay={feeDisplay} />
|
<TransactionItem
|
||||||
|
key={tx.hash}
|
||||||
|
tx={tx}
|
||||||
|
ensCache={reverseCache}
|
||||||
|
feeDisplay={feeDisplay}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
<div className="flex justify-between items-baseline py-3">
|
<div className="flex justify-between items-baseline py-3">
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
const Address: React.FC = ({ children }) => (
|
type AddressProps = {
|
||||||
<span className="font-address text-gray-400 truncate">
|
address: string;
|
||||||
<p className="truncate">{children}</p>
|
};
|
||||||
|
|
||||||
|
const Address: React.FC<AddressProps> = ({ address }) => (
|
||||||
|
<span className="font-address text-gray-400 truncate" title={address}>
|
||||||
|
<p className="truncate">{address}</p>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Address;
|
export default React.memo(Address);
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import React from "react";
|
||||||
|
import Address from "./Address";
|
||||||
|
import AddressLink from "./AddressLink";
|
||||||
|
import ENSName from "./ENSName";
|
||||||
|
import ENSNameLink from "./ENSNameLink";
|
||||||
|
|
||||||
|
type AddressOrENSNameProps = {
|
||||||
|
address: string;
|
||||||
|
ensName?: string;
|
||||||
|
selectedAddress?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddressOrENSName: React.FC<AddressOrENSNameProps> = ({
|
||||||
|
address,
|
||||||
|
ensName,
|
||||||
|
selectedAddress,
|
||||||
|
}) => {
|
||||||
|
return address === selectedAddress ? (
|
||||||
|
<>
|
||||||
|
{ensName ? (
|
||||||
|
<ENSName name={ensName} address={address} />
|
||||||
|
) : (
|
||||||
|
<Address address={address} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{ensName ? (
|
||||||
|
<ENSNameLink name={ensName} address={address} />
|
||||||
|
) : (
|
||||||
|
<AddressLink address={address} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(AddressOrENSName);
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React from "react";
|
||||||
|
import ENSLogo from "./ensLogo.svg";
|
||||||
|
|
||||||
|
type ENSNameProps = {
|
||||||
|
name: string;
|
||||||
|
address: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ENSName: React.FC<ENSNameProps> = ({ name, address }) => (
|
||||||
|
<div
|
||||||
|
className="flex items-baseline space-x-1 font-sans text-gray-700 truncate"
|
||||||
|
title={`${name}: ${address}`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="self-center filter grayscale"
|
||||||
|
src={ENSLogo}
|
||||||
|
alt="ENS Logo"
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
/>
|
||||||
|
<p className="truncate">{name}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default React.memo(ENSName);
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React from "react";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
import ENSLogo from "./ensLogo.svg";
|
||||||
|
|
||||||
|
type ENSNameLinkProps = {
|
||||||
|
name: string;
|
||||||
|
address: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ENSNameLink: React.FC<ENSNameLinkProps> = ({ name, address }) => (
|
||||||
|
<NavLink
|
||||||
|
className="flex items-baseline space-x-1 font-sans text-link-blue hover:text-link-blue-hover truncate"
|
||||||
|
to={`/address/${name}`}
|
||||||
|
title={`${name}: ${address}`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="self-center"
|
||||||
|
src={ENSLogo}
|
||||||
|
alt="ENS Logo"
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
/>
|
||||||
|
<p className="truncate">{name}</p>
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default React.memo(ENSNameLink);
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 72.52 80.95"><defs><style>.cls-3{fill:#a0a8d4}</style><linearGradient id="linear-gradient" x1="41.95" y1="2.57" x2="12.57" y2="34.42" gradientUnits="userSpaceOnUse"><stop offset=".58" stop-color="#a0a8d4"/><stop offset=".73" stop-color="#8791c7"/><stop offset=".91" stop-color="#6470b4"/></linearGradient><linearGradient id="linear-gradient-2" x1="42.57" y1="81.66" x2="71.96" y2="49.81" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-3" x1="42.26" y1="1.24" x2="42.26" y2="82.84" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#513eff"/><stop offset=".18" stop-color="#5157ff"/><stop offset=".57" stop-color="#5298ff"/><stop offset="1" stop-color="#52e5ff"/></linearGradient></defs><g style="isolation:isolate"><g id="Layer_1" data-name="Layer 1"><path d="M15.28 34.39c.8 1.71 2.78 5.09 2.78 5.09L40.95 1.64l-22.34 15.6a9.75 9.75 0 0 0-3.18 3.5 16.19 16.19 0 0 0-.15 13.65z" transform="translate(-6 -1.64)" fill="url(#linear-gradient)"/><path class="cls-3" d="M6.21 46.85a25.47 25.47 0 0 0 10 18.51l24.71 17.23s-15.46-22.28-28.5-44.45a22.39 22.39 0 0 1-2.62-7.56 12.1 12.1 0 0 1 0-3.63c-.34.63-1 1.92-1 1.92a29.35 29.35 0 0 0-2.67 8.55 52.28 52.28 0 0 0 .08 9.43z" transform="translate(-6 -1.64)"/><path d="M69.25 49.84c-.8-1.71-2.78-5.09-2.78-5.09L43.58 82.59 65.92 67a9.75 9.75 0 0 0 3.18-3.5 16.19 16.19 0 0 0 .15-13.66z" transform="translate(-6 -1.64)" fill="url(#linear-gradient-2)"/><path class="cls-3" d="M78.32 37.38a25.47 25.47 0 0 0-10-18.51L43.61 1.64s15.45 22.28 28.5 44.45a22.39 22.39 0 0 1 2.61 7.56 12.1 12.1 0 0 1 0 3.63c.34-.63 1-1.92 1-1.92a29.35 29.35 0 0 0 2.67-8.55 52.28 52.28 0 0 0-.07-9.43z" transform="translate(-6 -1.64)"/><path d="M15.43 20.74a9.75 9.75 0 0 1 3.18-3.5l22.34-15.6-22.89 37.85s-2-3.38-2.78-5.09a16.19 16.19 0 0 1 .15-13.66zM6.21 46.85a25.47 25.47 0 0 0 10 18.51l24.71 17.23s-15.46-22.28-28.5-44.45a22.39 22.39 0 0 1-2.62-7.56 12.1 12.1 0 0 1 0-3.63c-.34.63-1 1.92-1 1.92a29.35 29.35 0 0 0-2.67 8.55 52.28 52.28 0 0 0 .08 9.43zm63 3c-.8-1.71-2.78-5.09-2.78-5.09L43.58 82.59 65.92 67a9.75 9.75 0 0 0 3.18-3.5 16.19 16.19 0 0 0 .15-13.66zm9.07-12.46a25.47 25.47 0 0 0-10-18.51L43.61 1.64s15.45 22.28 28.5 44.45a22.39 22.39 0 0 1 2.61 7.56 12.1 12.1 0 0 1 0 3.63c.34-.63 1-1.92 1-1.92a29.35 29.35 0 0 0 2.67-8.55 52.28 52.28 0 0 0-.07-9.43z" transform="translate(-6 -1.64)" style="mix-blend-mode:color" fill="url(#linear-gradient-3)"/></g></g></svg>
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -4,25 +4,26 @@ import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
import MethodName from "../components/MethodName";
|
import MethodName from "../components/MethodName";
|
||||||
import BlockLink from "../components/BlockLink";
|
import BlockLink from "../components/BlockLink";
|
||||||
import TransactionLink from "../components/TransactionLink";
|
import TransactionLink from "../components/TransactionLink";
|
||||||
import Address from "../components/Address";
|
import AddressOrENSName from "../components/AddressOrENSName";
|
||||||
import AddressLink from "../components/AddressLink";
|
|
||||||
import TimestampAge from "../components/TimestampAge";
|
import TimestampAge from "../components/TimestampAge";
|
||||||
import TransactionDirection, {
|
import TransactionDirection, {
|
||||||
Direction,
|
Direction,
|
||||||
} from "../components/TransactionDirection";
|
} from "../components/TransactionDirection";
|
||||||
import TransactionValue from "../components/TransactionValue";
|
import TransactionValue from "../components/TransactionValue";
|
||||||
import { ProcessedTransaction } from "../types";
|
import { ENSReverseCache, ProcessedTransaction } from "../types";
|
||||||
import { FeeDisplay } from "./useFeeToggler";
|
import { FeeDisplay } from "./useFeeToggler";
|
||||||
import { formatValue } from "../components/formatter";
|
import { formatValue } from "../components/formatter";
|
||||||
|
|
||||||
type TransactionItemProps = {
|
type TransactionItemProps = {
|
||||||
tx: ProcessedTransaction;
|
tx: ProcessedTransaction;
|
||||||
|
ensCache?: ENSReverseCache;
|
||||||
selectedAddress?: string;
|
selectedAddress?: string;
|
||||||
feeDisplay: FeeDisplay;
|
feeDisplay: FeeDisplay;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TransactionItem: React.FC<TransactionItemProps> = ({
|
const TransactionItem: React.FC<TransactionItemProps> = ({
|
||||||
tx,
|
tx,
|
||||||
|
ensCache,
|
||||||
selectedAddress,
|
selectedAddress,
|
||||||
feeDisplay,
|
feeDisplay,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -39,6 +40,9 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ensFrom = ensCache && tx.from && ensCache[tx.from];
|
||||||
|
const ensTo = ensCache && tx.to && ensCache[tx.to];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-12 gap-x-1 items-baseline text-sm border-t border-gray-200 hover:bg-gray-100 px-2 py-3">
|
<div className="grid grid-cols-12 gap-x-1 items-baseline text-sm border-t border-gray-200 hover:bg-gray-100 px-2 py-3">
|
||||||
<div className="col-span-2 flex space-x-1 items-baseline">
|
<div className="col-span-2 flex space-x-1 items-baseline">
|
||||||
|
@ -58,24 +62,26 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
|
||||||
<TimestampAge timestamp={tx.timestamp} />
|
<TimestampAge timestamp={tx.timestamp} />
|
||||||
<span className="col-span-2 flex justify-between items-baseline space-x-2 pr-2">
|
<span className="col-span-2 flex justify-between items-baseline space-x-2 pr-2">
|
||||||
<span className="truncate" title={tx.from}>
|
<span className="truncate" title={tx.from}>
|
||||||
{tx.from &&
|
{tx.from && (
|
||||||
(tx.from === selectedAddress ? (
|
<AddressOrENSName
|
||||||
<Address>{tx.from}</Address>
|
address={tx.from}
|
||||||
) : (
|
ensName={ensFrom}
|
||||||
<AddressLink address={tx.from} />
|
selectedAddress={selectedAddress}
|
||||||
))}
|
/>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<TransactionDirection direction={direction} />
|
<TransactionDirection direction={direction} />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="col-span-2 truncate" title={tx.to}>
|
<span className="col-span-2 truncate" title={tx.to}>
|
||||||
{tx.to &&
|
{tx.to && (
|
||||||
(tx.to === selectedAddress ? (
|
<AddressOrENSName
|
||||||
<Address>{tx.to}</Address>
|
address={tx.to}
|
||||||
) : (
|
ensName={ensTo}
|
||||||
<AddressLink address={tx.to} />
|
selectedAddress={selectedAddress}
|
||||||
))}
|
/>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="col-span-2 truncate">
|
<span className="col-span-2 truncate">
|
||||||
<TransactionValue value={tx.value} />
|
<TransactionValue value={tx.value} />
|
||||||
|
|
|
@ -19,3 +19,7 @@ export type TransactionChunk = {
|
||||||
firstPage: boolean;
|
firstPage: boolean;
|
||||||
lastPage: boolean;
|
lastPage: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ENSReverseCache = {
|
||||||
|
[address: string]: string;
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { ENSReverseCache, ProcessedTransaction } from "./types";
|
||||||
|
import { provider } from "./ethersconfig";
|
||||||
|
|
||||||
|
export const useENSCache = (page?: ProcessedTransaction[]) => {
|
||||||
|
const [reverseCache, setReverseCache] = useState<ENSReverseCache>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!page) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addrSet = new Set<string>();
|
||||||
|
for (const tx of page) {
|
||||||
|
if (tx.from) {
|
||||||
|
addrSet.add(tx.from);
|
||||||
|
}
|
||||||
|
if (tx.to) {
|
||||||
|
addrSet.add(tx.to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const addresses = Array.from(addrSet);
|
||||||
|
|
||||||
|
const reverseResolve = async () => {
|
||||||
|
const solvers: Promise<string>[] = [];
|
||||||
|
for (const a of addresses) {
|
||||||
|
solvers.push(provider.lookupAddress(a));
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(solvers);
|
||||||
|
const cache: ENSReverseCache = {};
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
if (results[i] === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cache[addresses[i]] = results[i];
|
||||||
|
}
|
||||||
|
setReverseCache(cache);
|
||||||
|
};
|
||||||
|
reverseResolve();
|
||||||
|
}, [page]);
|
||||||
|
|
||||||
|
return reverseCache;
|
||||||
|
};
|
|
@ -1,11 +1,7 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
@ -20,7 +16,5 @@
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src"]
|
||||||
"src"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue