Merge branch 'feature/tx-by-nonce' into develop

This commit is contained in:
Willian Mitsuda 2022-02-01 19:15:51 -03:00
commit e45ea7db12
21 changed files with 698 additions and 232 deletions

3
.gitmodules vendored
View File

@ -1,9 +1,12 @@
[submodule "4bytes"] [submodule "4bytes"]
path = 4bytes path = 4bytes
url = https://github.com/ethereum-lists/4bytes.git url = https://github.com/ethereum-lists/4bytes.git
ignore = dirty
[submodule "trustwallet"] [submodule "trustwallet"]
path = trustwallet path = trustwallet
url = https://github.com/trustwallet/assets.git url = https://github.com/trustwallet/assets.git
ignore = dirty
[submodule "topic0"] [submodule "topic0"]
path = topic0 path = topic0
url = https://github.com/wmitsuda/topic0.git url = https://github.com/wmitsuda/topic0.git
ignore = dirty

View File

@ -13,6 +13,7 @@ import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons/faQuestionCircle"; import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons/faQuestionCircle";
import StandardFrame from "./StandardFrame"; import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle"; import StandardSubtitle from "./StandardSubtitle";
import AddressOrENSNameNotFound from "./components/AddressOrENSNameNotFound";
import Copy from "./components/Copy"; import Copy from "./components/Copy";
import NavTab from "./components/NavTab"; import NavTab from "./components/NavTab";
import SourcifyLogo from "./sourcify/SourcifyLogo"; import SourcifyLogo from "./sourcify/SourcifyLogo";
@ -20,12 +21,19 @@ import AddressTransactionResults from "./address/AddressTransactionResults";
import Contracts from "./address/Contracts"; import Contracts from "./address/Contracts";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { useAppConfigContext } from "./useAppConfig"; import { useAppConfigContext } from "./useAppConfig";
import { useAddressOrENSFromURL } from "./useResolvedAddresses"; import { useAddressOrENS } from "./useResolvedAddresses";
import { useMultipleMetadata } from "./sourcify/useSourcify"; import { useMultipleMetadata } from "./sourcify/useSourcify";
import { ChecksummedAddress } from "./types"; import { ChecksummedAddress } from "./types";
import { useAddressesWithCode } from "./useErigonHooks"; import { useAddressesWithCode } from "./useErigonHooks";
const AddressTransactions: React.FC = () => { const AddressTransactionByNonce = React.lazy(
() =>
import(
/* webpackChunkName: "addresstxbynonce", webpackPrefetch: true */ "./AddressTransactionByNonce"
)
);
const Address: React.FC = () => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const { addressOrName, direction } = useParams(); const { addressOrName, direction } = useParams();
if (addressOrName === undefined) { if (addressOrName === undefined) {
@ -45,7 +53,7 @@ const AddressTransactions: React.FC = () => {
}, },
[navigate, direction, searchParams] [navigate, direction, searchParams]
); );
const [checksummedAddress, isENS, error] = useAddressOrENSFromURL( const [checksummedAddress, isENS, error] = useAddressOrENS(
addressOrName, addressOrName,
urlFixer urlFixer
); );
@ -78,12 +86,21 @@ const AddressTransactions: React.FC = () => {
? metadatas[checksummedAddress] ? metadatas[checksummedAddress]
: undefined; : undefined;
// Search address by nonce === transaction @ nonce
const rawNonce = searchParams.get("nonce");
if (rawNonce !== null) {
return (
<AddressTransactionByNonce
checksummedAddress={checksummedAddress}
rawNonce={rawNonce}
/>
);
}
return ( return (
<StandardFrame> <StandardFrame>
{error ? ( {error ? (
<span className="text-base"> <AddressOrENSNameNotFound addressOrENSName={addressOrName} />
"{addressOrName}" is not an ETH address or ENS name.
</span>
) : ( ) : (
checksummedAddress && ( checksummedAddress && (
<> <>
@ -175,4 +192,4 @@ const AddressTransactions: React.FC = () => {
); );
}; };
export default AddressTransactions; export default Address;

View File

@ -0,0 +1,106 @@
import React, { useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import StandardFrame from "./StandardFrame";
import AddressOrENSNameInvalidNonce from "./components/AddressOrENSNameInvalidNonce";
import AddressOrENSNameNoTx from "./components/AddressOrENSNameNoTx";
import { ChecksummedAddress } from "./types";
import { transactionURL } from "./url";
import { useTransactionBySenderAndNonce } from "./useErigonHooks";
import { RuntimeContext } from "./useRuntime";
type AddressTransactionByNonceProps = {
checksummedAddress: ChecksummedAddress | undefined;
rawNonce: string;
};
const AddressTransactionByNonce: React.FC<AddressTransactionByNonceProps> = ({
checksummedAddress,
rawNonce,
}) => {
const { provider } = useContext(RuntimeContext);
// Calculate txCount ONLY when asked for latest nonce
const [txCount, setTxCount] = useState<number | undefined>();
useEffect(() => {
if (!provider || !checksummedAddress || rawNonce !== "latest") {
setTxCount(undefined);
return;
}
const readTxCount = async () => {
const count = await provider.getTransactionCount(checksummedAddress);
setTxCount(count);
};
readTxCount();
}, [provider, checksummedAddress, rawNonce]);
// Determine desired nonce from parse int query param or txCount - 1 nonce
// in case of latest
let nonce: number | undefined;
if (rawNonce === "latest") {
if (txCount !== undefined) {
nonce = txCount - 1;
}
} else {
nonce = parseInt(rawNonce, 10);
if (nonce < 0) {
nonce = NaN;
}
}
// Given all base params are determined, get the corresponding tx
const txHash = useTransactionBySenderAndNonce(
provider,
checksummedAddress,
nonce !== undefined && isNaN(nonce) ? undefined : nonce
);
const navigate = useNavigate();
// Loading...
if (
checksummedAddress === undefined ||
nonce === undefined ||
txHash === undefined
) {
return <StandardFrame />;
}
// Address hasn't made the first outbound tx yet
if (nonce < 0) {
return (
<StandardFrame>
<AddressOrENSNameNoTx addressOrENSName={checksummedAddress} />
</StandardFrame>
);
}
// Garbage nonce
if (isNaN(nonce)) {
return (
<StandardFrame>
<AddressOrENSNameInvalidNonce
addressOrENSName={checksummedAddress}
nonce={rawNonce}
/>
</StandardFrame>
);
}
// Valid nonce, but no tx found
if (txHash === null) {
return (
<StandardFrame>
<AddressOrENSNameInvalidNonce
addressOrENSName={checksummedAddress}
nonce={nonce.toString()}
/>
</StandardFrame>
);
}
// Success; replace and render filler
navigate(transactionURL(txHash), { replace: true });
return <StandardFrame />;
};
export default AddressTransactionByNonce;

View File

@ -17,11 +17,9 @@ const BlockTransactions = React.lazy(
/* webpackChunkName: "blocktxs", webpackPrefetch: true */ "./BlockTransactions" /* webpackChunkName: "blocktxs", webpackPrefetch: true */ "./BlockTransactions"
) )
); );
const AddressTransactions = React.lazy( const Address = React.lazy(
() => () =>
import( import(/* webpackChunkName: "address", webpackPrefetch: true */ "./Address")
/* webpackChunkName: "address", webpackPrefetch: true */ "./AddressTransactions"
)
); );
const Transaction = React.lazy( const Transaction = React.lazy(
() => () =>
@ -33,6 +31,12 @@ const London = React.lazy(
/* webpackChunkName: "london", webpackPrefetch: true */ "./special/london/London" /* webpackChunkName: "london", webpackPrefetch: true */ "./special/london/London"
) )
); );
const PageNotFound = React.lazy(
() =>
import(
/* webpackChunkName: "notfound", webpackPrefetch: true */ "./PageNotFound"
)
);
const App = () => { const App = () => {
const runtime = useRuntime(); const runtime = useRuntime();
@ -61,9 +65,9 @@ const App = () => {
<Route path="tx/:txhash/*" element={<Transaction />} /> <Route path="tx/:txhash/*" element={<Transaction />} />
<Route <Route
path="address/:addressOrName/*" path="address/:addressOrName/*"
element={<AddressTransactions />} element={<Address />}
/> />
<Route path="*" element={<Home />} /> <Route path="*" element={<PageNotFound />} />
</Route> </Route>
</Routes> </Routes>
</Router> </Router>

16
src/PageNotFound.tsx Normal file
View File

@ -0,0 +1,16 @@
import React from "react";
import { NavLink } from "react-router-dom";
import StandardFrame from "./StandardFrame";
const PageNotFound: React.FC = () => (
<StandardFrame>
<div className="border h-full m-auto flex flex-col justify-center items-center space-y-10">
<span className="text-4xl">Page not found!</span>
<NavLink className="text-link-blue hover:text-link-blue-hover" to="/">
Click here to go to home
</NavLink>
</div>
</StandardFrame>
);
export default PageNotFound;

View File

@ -1,129 +1,13 @@
import React, { useMemo, useContext } from "react"; import React from "react";
import { useParams, Routes, Route } from "react-router-dom"; import { useParams } from "react-router-dom";
import { Tab } from "@headlessui/react"; import TransactionPageContent from "./TransactionPageContent";
import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle";
import ContentFrame from "./ContentFrame";
import NavTab from "./components/NavTab";
import { RuntimeContext } from "./useRuntime";
import { SelectionContext, useSelection } from "./useSelection";
import { useInternalOperations, useTxData } from "./useErigonHooks";
import { useETHUSDOracle } from "./usePriceOracle";
import { useAppConfigContext } from "./useAppConfig";
import { useSourcify, useTransactionDescription } from "./sourcify/useSourcify";
import { SelectedTransactionContext } from "./useSelectedTransaction";
const Details = React.lazy(
() =>
import(
/* webpackChunkName: "txdetails", webpackPrefetch: true */
"./transaction/Details"
)
);
const Logs = React.lazy(
() =>
import(
/* webpackChunkName: "txlogs", webpackPrefetch: true */ "./transaction/Logs"
)
);
const Trace = React.lazy(
() =>
import(
/* webpackChunkName: "txtrace", webpackPrefetch: true */ "./transaction/Trace"
)
);
const Transaction: React.FC = () => { const Transaction: React.FC = () => {
const { provider } = useContext(RuntimeContext);
const { txhash } = useParams(); const { txhash } = useParams();
if (txhash === undefined) { if (txhash === undefined) {
throw new Error("txhash couldn't be undefined here"); throw new Error("txhash couldn't be undefined here");
} }
return <TransactionPageContent txHash={txhash} />;
const txData = useTxData(provider, txhash);
const internalOps = useInternalOperations(provider, txData);
const sendsEthToMiner = useMemo(() => {
if (!txData || !internalOps) {
return false;
}
for (const t of internalOps) {
if (t.to === txData.confirmedData?.miner) {
return true;
}
}
return false;
}, [txData, internalOps]);
const selectionCtx = useSelection();
const blockETHUSDPrice = useETHUSDOracle(
provider,
txData?.confirmedData?.blockNumber
);
const { sourcifySource } = useAppConfigContext();
const metadata = useSourcify(
txData?.to,
provider?.network.chainId,
sourcifySource
);
const txDesc = useTransactionDescription(metadata, txData);
return (
<SelectedTransactionContext.Provider value={txData}>
<StandardFrame>
<StandardSubtitle>Transaction Details</StandardSubtitle>
{txData === null && (
<ContentFrame>
<div className="py-4 text-sm">
Transaction <span className="font-hash">{txhash}</span> not found.
</div>
</ContentFrame>
)}
{txData && (
<SelectionContext.Provider value={selectionCtx}>
<Tab.Group>
<Tab.List className="flex space-x-2 border-l border-r border-t rounded-t-lg bg-white">
<NavTab href=".">Overview</NavTab>
{txData.confirmedData?.blockNumber !== undefined && (
<NavTab href="logs">
Logs
{txData && ` (${txData.confirmedData?.logs?.length ?? 0})`}
</NavTab>
)}
<NavTab href="trace">Trace</NavTab>
</Tab.List>
</Tab.Group>
<React.Suspense fallback={null}>
<Routes>
<Route
index
element={
<Details
txData={txData}
txDesc={txDesc}
toMetadata={metadata}
userDoc={metadata?.output.userdoc}
devDoc={metadata?.output.devdoc}
internalOps={internalOps}
sendsEthToMiner={sendsEthToMiner}
ethUSDPrice={blockETHUSDPrice}
/>
}
/>
<Route
path="logs"
element={<Logs txData={txData} metadata={metadata} />}
/>
<Route path="trace" element={<Trace txData={txData} />} />
</Routes>
</React.Suspense>
</SelectionContext.Provider>
)}
</StandardFrame>
</SelectedTransactionContext.Provider>
);
}; };
export default Transaction; export default Transaction;

View File

@ -0,0 +1,131 @@
import React, { useContext, useMemo } from "react";
import { Route, Routes } from "react-router-dom";
import { Tab } from "@headlessui/react";
import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle";
import ContentFrame from "./ContentFrame";
import NavTab from "./components/NavTab";
import { RuntimeContext } from "./useRuntime";
import { useInternalOperations, useTxData } from "./useErigonHooks";
import { SelectionContext, useSelection } from "./useSelection";
import { SelectedTransactionContext } from "./useSelectedTransaction";
import { useETHUSDOracle } from "./usePriceOracle";
import { useAppConfigContext } from "./useAppConfig";
import { useSourcify, useTransactionDescription } from "./sourcify/useSourcify";
const Details = React.lazy(
() =>
import(
/* webpackChunkName: "txdetails", webpackPrefetch: true */
"./transaction/Details"
)
);
const Logs = React.lazy(
() =>
import(
/* webpackChunkName: "txlogs", webpackPrefetch: true */ "./transaction/Logs"
)
);
const Trace = React.lazy(
() =>
import(
/* webpackChunkName: "txtrace", webpackPrefetch: true */ "./transaction/Trace"
)
);
type TransactionPageContentProps = {
txHash: string;
};
const TransactionPageContent: React.FC<TransactionPageContentProps> = ({
txHash,
}) => {
const { provider } = useContext(RuntimeContext);
const txData = useTxData(provider, txHash);
const internalOps = useInternalOperations(provider, txData);
const sendsEthToMiner = useMemo(() => {
if (!txData || !internalOps) {
return false;
}
for (const t of internalOps) {
if (t.to === txData.confirmedData?.miner) {
return true;
}
}
return false;
}, [txData, internalOps]);
const selectionCtx = useSelection();
const blockETHUSDPrice = useETHUSDOracle(
provider,
txData?.confirmedData?.blockNumber
);
const { sourcifySource } = useAppConfigContext();
const metadata = useSourcify(
txData?.to,
provider?.network.chainId,
sourcifySource
);
const txDesc = useTransactionDescription(metadata, txData);
return (
<SelectedTransactionContext.Provider value={txData}>
<StandardFrame>
<StandardSubtitle>Transaction Details</StandardSubtitle>
{txData === null && (
<ContentFrame>
<div className="py-4 text-sm">
Transaction <span className="font-hash">{txHash}</span> not found.
</div>
</ContentFrame>
)}
{txData && (
<SelectionContext.Provider value={selectionCtx}>
<Tab.Group>
<Tab.List className="flex space-x-2 border-l border-r border-t rounded-t-lg bg-white">
<NavTab href=".">Overview</NavTab>
{txData.confirmedData?.blockNumber !== undefined && (
<NavTab href="logs">
Logs
{txData && ` (${txData.confirmedData?.logs?.length ?? 0})`}
</NavTab>
)}
<NavTab href="trace">Trace</NavTab>
</Tab.List>
</Tab.Group>
<React.Suspense fallback={null}>
<Routes>
<Route
index
element={
<Details
txData={txData}
txDesc={txDesc}
toMetadata={metadata}
userDoc={metadata?.output.userdoc}
devDoc={metadata?.output.devdoc}
internalOps={internalOps}
sendsEthToMiner={sendsEthToMiner}
ethUSDPrice={blockETHUSDPrice}
/>
}
/>
<Route
path="logs"
element={<Logs txData={txData} metadata={metadata} />}
/>
<Route path="trace" element={<Trace txData={txData} />} />
</Routes>
</React.Suspense>
</SelectionContext.Provider>
)}
</StandardFrame>
</SelectedTransactionContext.Provider>
);
};
export default TransactionPageContent;

View File

@ -1,11 +1,12 @@
import React, { useContext } from "react"; import React, { useContext } from "react";
import PlainAddress from "./PlainAddress";
import { resolverRendererRegistry } from "../api/address-resolver"; import { resolverRendererRegistry } from "../api/address-resolver";
import { useResolvedAddress } from "../useResolvedAddresses"; import { useResolvedAddress } from "../useResolvedAddresses";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import PlainAddress from "./PlainAddress"; import { ChecksummedAddress } from "../types";
type AddressOrENSNameProps = { type AddressOrENSNameProps = {
address: string; address: ChecksummedAddress;
selectedAddress?: string; selectedAddress?: string;
dontOverrideColors?: boolean; dontOverrideColors?: boolean;
}; };

View File

@ -0,0 +1,25 @@
import React from "react";
import StandardSubtitle from "../StandardSubtitle";
import ContentFrame from "../ContentFrame";
import AddressOrENSName from "./AddressOrENSName";
type AddressOrENSNameInvalidNonceProps = {
addressOrENSName: string;
nonce: string;
};
const AddressOrENSNameInvalidNonce: React.FC<
AddressOrENSNameInvalidNonceProps
> = ({ addressOrENSName, nonce }) => (
<>
<StandardSubtitle>Transaction Details</StandardSubtitle>
<ContentFrame>
<div className="flex py-4 text-sm">
<AddressOrENSName address={addressOrENSName} />
<span>: no transaction found for nonce="{nonce}".</span>
</div>
</ContentFrame>
</>
);
export default React.memo(AddressOrENSNameInvalidNonce);

View File

@ -0,0 +1,24 @@
import React from "react";
import StandardSubtitle from "../StandardSubtitle";
import ContentFrame from "../ContentFrame";
import AddressOrENSName from "./AddressOrENSName";
type AddressOrENSNameNoTxProps = {
addressOrENSName: string;
};
const AddressOrENSNameNoTx: React.FC<AddressOrENSNameNoTxProps> = ({
addressOrENSName,
}) => (
<>
<StandardSubtitle>Transaction Details</StandardSubtitle>
<ContentFrame>
<div className="flex py-4 text-sm">
<AddressOrENSName address={addressOrENSName} />
<span>: no outbound transactions found.</span>
</div>
</ContentFrame>
</>
);
export default React.memo(AddressOrENSNameNoTx);

View File

@ -0,0 +1,22 @@
import React from "react";
import StandardSubtitle from "../StandardSubtitle";
import ContentFrame from "../ContentFrame";
type AddressOrENSNameNotFoundProps = {
addressOrENSName: string;
};
const AddressOrENSNameNotFound: React.FC<AddressOrENSNameNotFoundProps> = ({
addressOrENSName,
}) => (
<>
<StandardSubtitle>Transaction Details</StandardSubtitle>
<ContentFrame>
<div className="py-4 text-sm">
"{addressOrENSName}" is not an ETH address or ENS name.
</div>
</ContentFrame>
</>
);
export default React.memo(AddressOrENSNameNotFound);

View File

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { commify } from "@ethersproject/units";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp"; import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp";
@ -14,7 +15,7 @@ const Nonce: React.FC<NonceProps> = ({ value }) => (
<span className="text-green-400"> <span className="text-green-400">
<FontAwesomeIcon icon={faArrowUp} size="1x" /> <FontAwesomeIcon icon={faArrowUp} size="1x" />
</span> </span>
<span className="text-green-600">{value}</span> <span className="text-green-600">{commify(value)}</span>
</span> </span>
); );

View File

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { transactionURL } from "../url";
type TransactionLinkProps = { type TransactionLinkProps = {
txHash: string; txHash: string;
@ -8,7 +9,7 @@ type TransactionLinkProps = {
const TransactionLink: React.FC<TransactionLinkProps> = ({ txHash }) => ( const TransactionLink: React.FC<TransactionLinkProps> = ({ txHash }) => (
<NavLink <NavLink
className="text-link-blue hover:text-link-blue-hover font-hash" className="text-link-blue hover:text-link-blue-hover font-hash"
to={`/tx/${txHash}`} to={transactionURL(txHash)}
> >
<p className="truncate">{txHash}</p> <p className="truncate">{txHash}</p>
</NavLink> </NavLink>

View File

@ -1,3 +1,3 @@
export const MIN_API_LEVEL = 5; export const MIN_API_LEVEL = 6;
export const PAGE_SIZE = 25; export const PAGE_SIZE = 25;

View File

@ -206,17 +206,36 @@ export class SearchController {
} }
} }
const doSearch = (q: string, navigate: NavigateFunction) => { const doSearch = async (q: string, navigate: NavigateFunction) => {
if (isAddress(q)) { // Cleanup
navigate(`/address/${q}`, { replace: true }); q = q.trim();
let maybeAddress = q;
let maybeIndex = "";
const sepIndex = q.lastIndexOf(":");
if (sepIndex !== -1) {
maybeAddress = q.substring(0, sepIndex);
maybeIndex = q.substring(sepIndex + 1);
}
// Plain address?
if (isAddress(maybeAddress)) {
navigate(
`/address/${maybeAddress}${
maybeIndex !== "" ? `?nonce=${maybeIndex}` : ""
}`,
{ replace: true }
);
return; return;
} }
// Tx hash?
if (isHexString(q, 32)) { if (isHexString(q, 32)) {
navigate(`/tx/${q}`, { replace: true }); navigate(`/tx/${q}`, { replace: true });
return; return;
} }
// Block number?
const blockNumber = parseInt(q); const blockNumber = parseInt(q);
if (!isNaN(blockNumber)) { if (!isNaN(blockNumber)) {
navigate(`/block/${blockNumber}`, { replace: true }); navigate(`/block/${blockNumber}`, { replace: true });
@ -224,7 +243,12 @@ const doSearch = (q: string, navigate: NavigateFunction) => {
} }
// Assume it is an ENS name // Assume it is an ENS name
navigate(`/address/${q}`); navigate(
`/address/${maybeAddress}${
maybeIndex !== "" ? `?nonce=${maybeIndex}` : ""
}`,
{ replace: true }
);
}; };
export const useGenericSearch = (): [ export const useGenericSearch = (): [

View File

@ -15,6 +15,7 @@ import BlockConfirmations from "../components/BlockConfirmations";
import TransactionAddress from "../components/TransactionAddress"; import TransactionAddress from "../components/TransactionAddress";
import Copy from "../components/Copy"; import Copy from "../components/Copy";
import Nonce from "../components/Nonce"; import Nonce from "../components/Nonce";
import NavNonce from "./NavNonce";
import Timestamp from "../components/Timestamp"; import Timestamp from "../components/Timestamp";
import InternalTransactionOperation from "../components/InternalTransactionOperation"; import InternalTransactionOperation from "../components/InternalTransactionOperation";
import MethodName from "../components/MethodName"; import MethodName from "../components/MethodName";
@ -253,6 +254,7 @@ const Details: React.FC<DetailsProps> = ({
</div> </div>
<div className="flex items-baseline pl-3"> <div className="flex items-baseline pl-3">
<Nonce value={txData.nonce} /> <Nonce value={txData.nonce} />
<NavNonce sender={txData.from} nonce={txData.nonce} />
</div> </div>
</div> </div>
</InfoRow> </InfoRow>

View File

@ -0,0 +1,36 @@
import { NavLink } from "react-router-dom";
import { ChecksummedAddress } from "../types";
import { addressByNonceURL } from "../url";
// TODO: extract common component with block/NavButton
type NavButtonProps = {
sender: ChecksummedAddress;
nonce: number;
disabled?: boolean;
};
const NavButton: React.FC<NavButtonProps> = ({
sender,
nonce,
disabled,
children,
}) => {
if (disabled) {
return (
<span className="bg-link-blue bg-opacity-10 text-gray-300 rounded px-2 py-1 text-xs">
{children}
</span>
);
}
return (
<NavLink
className="bg-link-blue bg-opacity-10 text-link-blue hover:bg-opacity-100 hover:text-white rounded px-2 py-1 text-xs"
to={addressByNonceURL(sender, nonce)}
>
{children}
</NavLink>
);
};
export default NavButton;

View File

@ -0,0 +1,66 @@
import React, { useContext, useEffect } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons/faChevronLeft";
import { faChevronRight } from "@fortawesome/free-solid-svg-icons/faChevronRight";
import NavButton from "./NavButton";
import { ChecksummedAddress } from "../types";
import { RuntimeContext } from "../useRuntime";
import {
prefetchTransactionBySenderAndNonce,
useTransactionCount,
} from "../useErigonHooks";
import { useSWRConfig } from "swr";
type NavNonceProps = {
sender: ChecksummedAddress;
nonce: number;
};
const NavNonce: React.FC<NavNonceProps> = ({ sender, nonce }) => {
const { provider } = useContext(RuntimeContext);
const count = useTransactionCount(provider, sender);
// Prefetch
const swrConfig = useSWRConfig();
useEffect(() => {
if (!provider || !sender || nonce === undefined || count === undefined) {
return;
}
prefetchTransactionBySenderAndNonce(swrConfig, provider, sender, nonce - 1);
prefetchTransactionBySenderAndNonce(swrConfig, provider, sender, nonce + 1);
if (count > 0) {
prefetchTransactionBySenderAndNonce(
swrConfig,
provider,
sender,
count - 1
);
}
}, [swrConfig, provider, sender, nonce, count]);
return (
<div className="pl-2 self-center flex space-x-1">
<NavButton sender={sender} nonce={nonce - 1} disabled={nonce === 0}>
<FontAwesomeIcon icon={faChevronLeft} />
</NavButton>
<NavButton
sender={sender}
nonce={nonce + 1}
disabled={count === undefined || nonce >= count - 1}
>
<FontAwesomeIcon icon={faChevronRight} />
</NavButton>
<NavButton
sender={sender}
nonce={count !== undefined ? count - 1 : -1}
disabled={count === undefined || nonce >= count - 1}
>
<FontAwesomeIcon icon={faChevronRight} />
<FontAwesomeIcon icon={faChevronRight} />
</NavButton>
</div>
);
};
export default React.memo(NavNonce);

View File

@ -18,6 +18,11 @@ export const blockURL = (blockNum: BlockTag) => `/block/${blockNum}`;
export const blockTxsURL = (blockNum: BlockTag) => `/block/${blockNum}/txs`; export const blockTxsURL = (blockNum: BlockTag) => `/block/${blockNum}/txs`;
export const transactionURL = (txHash: string) => `/tx/${txHash}`;
export const addressByNonceURL = (address: ChecksummedAddress, nonce: number) =>
`/address/${address}?nonce=${nonce}`;
export enum SourcifySource { export enum SourcifySource {
// Resolve trusted IPNS for root IPFS // Resolve trusted IPNS for root IPFS
IPFS_IPNS, IPFS_IPNS,

View File

@ -6,6 +6,7 @@ import { Contract } from "@ethersproject/contracts";
import { defaultAbiCoder } from "@ethersproject/abi"; import { defaultAbiCoder } from "@ethersproject/abi";
import { BigNumber } from "@ethersproject/bignumber"; import { BigNumber } from "@ethersproject/bignumber";
import { arrayify, hexDataSlice, isHexString } from "@ethersproject/bytes"; import { arrayify, hexDataSlice, isHexString } from "@ethersproject/bytes";
import useSWR, { useSWRConfig } from "swr";
import { getInternalOperations } from "./nodeFunctions"; import { getInternalOperations } from "./nodeFunctions";
import { import {
TokenMetas, TokenMetas,
@ -190,98 +191,107 @@ export const useTxData = (
} }
const readTxData = async () => { const readTxData = async () => {
const [_response, _receipt] = await Promise.all([ try {
provider.getTransaction(txhash), const [_response, _receipt] = await Promise.all([
provider.getTransactionReceipt(txhash), provider.getTransaction(txhash),
]); provider.getTransactionReceipt(txhash),
if (_response === null) { ]);
if (_response === null) {
setTxData(null);
return;
}
let _block: ExtendedBlock | undefined;
if (_response.blockNumber) {
_block = await readBlock(provider, _response.blockNumber.toString());
}
document.title = `Transaction ${_response.hash} | Otterscan`;
// Extract token transfers
const tokenTransfers: TokenTransfer[] = [];
if (_receipt) {
for (const l of _receipt.logs) {
if (l.topics.length !== 3) {
continue;
}
if (l.topics[0] !== TRANSFER_TOPIC) {
continue;
}
tokenTransfers.push({
token: l.address,
from: getAddress(hexDataSlice(arrayify(l.topics[1]), 12)),
to: getAddress(hexDataSlice(arrayify(l.topics[2]), 12)),
value: BigNumber.from(l.data),
});
}
}
// Extract token meta
const tokenMetas: TokenMetas = {};
for (const t of tokenTransfers) {
if (tokenMetas[t.token] !== undefined) {
continue;
}
const erc20Contract = new Contract(t.token, erc20, provider);
try {
const [name, symbol, decimals] = await Promise.all([
erc20Contract.name(),
erc20Contract.symbol(),
erc20Contract.decimals(),
]);
tokenMetas[t.token] = {
name,
symbol,
decimals,
};
} catch (err) {
tokenMetas[t.token] = null;
console.warn(
`Couldn't get token ${t.token} metadata; ignoring`,
err
);
}
}
setTxData({
transactionHash: _response.hash,
from: _response.from,
to: _response.to,
value: _response.value,
tokenTransfers,
tokenMetas,
type: _response.type ?? 0,
maxFeePerGas: _response.maxFeePerGas,
maxPriorityFeePerGas: _response.maxPriorityFeePerGas,
gasPrice: _response.gasPrice!,
gasLimit: _response.gasLimit,
nonce: _response.nonce,
data: _response.data,
confirmedData:
_receipt === null
? undefined
: {
status: _receipt.status === 1,
blockNumber: _receipt.blockNumber,
transactionIndex: _receipt.transactionIndex,
blockBaseFeePerGas: _block!.baseFeePerGas,
blockTransactionCount: _block!.transactionCount,
confirmations: _receipt.confirmations,
timestamp: _block!.timestamp,
miner: _block!.miner,
createdContractAddress: _receipt.contractAddress,
fee: _response.gasPrice!.mul(_receipt.gasUsed),
gasUsed: _receipt.gasUsed,
logs: _receipt.logs,
},
});
} catch (err) {
console.error(err);
setTxData(null); setTxData(null);
return;
} }
let _block: ExtendedBlock | undefined;
if (_response.blockNumber) {
_block = await readBlock(provider, _response.blockNumber.toString());
}
document.title = `Transaction ${_response.hash} | Otterscan`;
// Extract token transfers
const tokenTransfers: TokenTransfer[] = [];
if (_receipt) {
for (const l of _receipt.logs) {
if (l.topics.length !== 3) {
continue;
}
if (l.topics[0] !== TRANSFER_TOPIC) {
continue;
}
tokenTransfers.push({
token: l.address,
from: getAddress(hexDataSlice(arrayify(l.topics[1]), 12)),
to: getAddress(hexDataSlice(arrayify(l.topics[2]), 12)),
value: BigNumber.from(l.data),
});
}
}
// Extract token meta
const tokenMetas: TokenMetas = {};
for (const t of tokenTransfers) {
if (tokenMetas[t.token] !== undefined) {
continue;
}
const erc20Contract = new Contract(t.token, erc20, provider);
try {
const [name, symbol, decimals] = await Promise.all([
erc20Contract.name(),
erc20Contract.symbol(),
erc20Contract.decimals(),
]);
tokenMetas[t.token] = {
name,
symbol,
decimals,
};
} catch (err) {
tokenMetas[t.token] = null;
console.warn(`Couldn't get token ${t.token} metadata; ignoring`, err);
}
}
setTxData({
transactionHash: _response.hash,
from: _response.from,
to: _response.to,
value: _response.value,
tokenTransfers,
tokenMetas,
type: _response.type ?? 0,
maxFeePerGas: _response.maxFeePerGas,
maxPriorityFeePerGas: _response.maxPriorityFeePerGas,
gasPrice: _response.gasPrice!,
gasLimit: _response.gasLimit,
nonce: _response.nonce,
data: _response.data,
confirmedData:
_receipt === null
? undefined
: {
status: _receipt.status === 1,
blockNumber: _receipt.blockNumber,
transactionIndex: _receipt.transactionIndex,
blockBaseFeePerGas: _block!.baseFeePerGas,
blockTransactionCount: _block!.transactionCount,
confirmations: _receipt.confirmations,
timestamp: _block!.timestamp,
miner: _block!.miner,
createdContractAddress: _receipt.contractAddress,
fee: _response.gasPrice!.mul(_receipt.gasUsed),
gasUsed: _receipt.gasUsed,
logs: _receipt.logs,
},
});
}; };
readTxData(); readTxData();
}, [provider, txhash]); }, [provider, txhash]);
@ -502,3 +512,91 @@ export const useTransactionError = (
return [errorMsg, data, isCustomError]; return [errorMsg, data, isCustomError];
}; };
export const useTransactionCount = (
provider: JsonRpcProvider | undefined,
sender: ChecksummedAddress | undefined
): number | undefined => {
const { data, error } = useSWR(
provider && sender ? { provider, sender } : null,
async ({ provider, sender }): Promise<number | undefined> =>
provider.getTransactionCount(sender)
);
if (error) {
return undefined;
}
return data;
};
type TransactionBySenderAndNonceKey = {
network: number;
sender: ChecksummedAddress;
nonce: number;
};
const getTransactionBySenderAndNonceFetcher =
(provider: JsonRpcProvider) =>
async ({
network,
sender,
nonce,
}: TransactionBySenderAndNonceKey): Promise<string | null | undefined> => {
if (nonce < 0) {
return undefined;
}
const result = (await provider.send("ots_getTransactionBySenderAndNonce", [
sender,
nonce,
])) as string;
// Empty or success
return result;
};
export const prefetchTransactionBySenderAndNonce = (
{ mutate }: ReturnType<typeof useSWRConfig>,
provider: JsonRpcProvider,
sender: ChecksummedAddress,
nonce: number
) => {
const key: TransactionBySenderAndNonceKey = {
network: provider.network.chainId,
sender,
nonce,
};
mutate(key, (curr: any) => {
if (curr) {
return curr;
}
return getTransactionBySenderAndNonceFetcher(provider)(key);
});
// }
};
export const useTransactionBySenderAndNonce = (
provider: JsonRpcProvider | undefined,
sender: ChecksummedAddress | undefined,
nonce: number | undefined
): string | null | undefined => {
const { data, error } = useSWR<
string | null | undefined,
any,
TransactionBySenderAndNonceKey | null
>(
provider && sender && nonce !== undefined
? {
network: provider.network.chainId,
sender,
nonce,
}
: null,
getTransactionBySenderAndNonceFetcher(provider!)
);
if (error) {
return undefined;
}
return data;
};

View File

@ -7,7 +7,7 @@ import { SelectedResolvedName } from "./api/address-resolver/CompositeAddressRes
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { ChecksummedAddress } from "./types"; import { ChecksummedAddress } from "./types";
export const useAddressOrENSFromURL = ( export const useAddressOrENS = (
addressOrName: string, addressOrName: string,
urlFixer: (address: ChecksummedAddress) => void urlFixer: (address: ChecksummedAddress) => void
): [ ): [