Merge branch 'feature/reverse-ens-tx-list' into develop

This commit is contained in:
Willian Mitsuda 2021-07-03 02:00:08 -03:00
commit 689723655d
11 changed files with 198 additions and 44 deletions

View File

@ -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}
/> />

View File

@ -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">

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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} />

View File

@ -19,3 +19,7 @@ export type TransactionChunk = {
firstPage: boolean; firstPage: boolean;
lastPage: boolean; lastPage: boolean;
}; };
export type ENSReverseCache = {
[address: string]: string;
};

44
src/useReverseCache.ts Normal file
View File

@ -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;
};

View File

@ -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"
]
} }