Merge branch 'feature/tx-by-nonce' into develop
This commit is contained in:
commit
e45ea7db12
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,9 +1,12 @@
|
||||
[submodule "4bytes"]
|
||||
path = 4bytes
|
||||
url = https://github.com/ethereum-lists/4bytes.git
|
||||
ignore = dirty
|
||||
[submodule "trustwallet"]
|
||||
path = trustwallet
|
||||
url = https://github.com/trustwallet/assets.git
|
||||
ignore = dirty
|
||||
[submodule "topic0"]
|
||||
path = topic0
|
||||
url = https://github.com/wmitsuda/topic0.git
|
||||
ignore = dirty
|
||||
|
@ -13,6 +13,7 @@ import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
|
||||
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons/faQuestionCircle";
|
||||
import StandardFrame from "./StandardFrame";
|
||||
import StandardSubtitle from "./StandardSubtitle";
|
||||
import AddressOrENSNameNotFound from "./components/AddressOrENSNameNotFound";
|
||||
import Copy from "./components/Copy";
|
||||
import NavTab from "./components/NavTab";
|
||||
import SourcifyLogo from "./sourcify/SourcifyLogo";
|
||||
@ -20,12 +21,19 @@ import AddressTransactionResults from "./address/AddressTransactionResults";
|
||||
import Contracts from "./address/Contracts";
|
||||
import { RuntimeContext } from "./useRuntime";
|
||||
import { useAppConfigContext } from "./useAppConfig";
|
||||
import { useAddressOrENSFromURL } from "./useResolvedAddresses";
|
||||
import { useAddressOrENS } from "./useResolvedAddresses";
|
||||
import { useMultipleMetadata } from "./sourcify/useSourcify";
|
||||
import { ChecksummedAddress } from "./types";
|
||||
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 { addressOrName, direction } = useParams();
|
||||
if (addressOrName === undefined) {
|
||||
@ -45,7 +53,7 @@ const AddressTransactions: React.FC = () => {
|
||||
},
|
||||
[navigate, direction, searchParams]
|
||||
);
|
||||
const [checksummedAddress, isENS, error] = useAddressOrENSFromURL(
|
||||
const [checksummedAddress, isENS, error] = useAddressOrENS(
|
||||
addressOrName,
|
||||
urlFixer
|
||||
);
|
||||
@ -78,12 +86,21 @@ const AddressTransactions: React.FC = () => {
|
||||
? metadatas[checksummedAddress]
|
||||
: undefined;
|
||||
|
||||
// Search address by nonce === transaction @ nonce
|
||||
const rawNonce = searchParams.get("nonce");
|
||||
if (rawNonce !== null) {
|
||||
return (
|
||||
<AddressTransactionByNonce
|
||||
checksummedAddress={checksummedAddress}
|
||||
rawNonce={rawNonce}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StandardFrame>
|
||||
{error ? (
|
||||
<span className="text-base">
|
||||
"{addressOrName}" is not an ETH address or ENS name.
|
||||
</span>
|
||||
<AddressOrENSNameNotFound addressOrENSName={addressOrName} />
|
||||
) : (
|
||||
checksummedAddress && (
|
||||
<>
|
||||
@ -175,4 +192,4 @@ const AddressTransactions: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AddressTransactions;
|
||||
export default Address;
|
106
src/AddressTransactionByNonce.tsx
Normal file
106
src/AddressTransactionByNonce.tsx
Normal 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;
|
16
src/App.tsx
16
src/App.tsx
@ -17,11 +17,9 @@ const BlockTransactions = React.lazy(
|
||||
/* webpackChunkName: "blocktxs", webpackPrefetch: true */ "./BlockTransactions"
|
||||
)
|
||||
);
|
||||
const AddressTransactions = React.lazy(
|
||||
const Address = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "address", webpackPrefetch: true */ "./AddressTransactions"
|
||||
)
|
||||
import(/* webpackChunkName: "address", webpackPrefetch: true */ "./Address")
|
||||
);
|
||||
const Transaction = React.lazy(
|
||||
() =>
|
||||
@ -33,6 +31,12 @@ const London = React.lazy(
|
||||
/* webpackChunkName: "london", webpackPrefetch: true */ "./special/london/London"
|
||||
)
|
||||
);
|
||||
const PageNotFound = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "notfound", webpackPrefetch: true */ "./PageNotFound"
|
||||
)
|
||||
);
|
||||
|
||||
const App = () => {
|
||||
const runtime = useRuntime();
|
||||
@ -61,9 +65,9 @@ const App = () => {
|
||||
<Route path="tx/:txhash/*" element={<Transaction />} />
|
||||
<Route
|
||||
path="address/:addressOrName/*"
|
||||
element={<AddressTransactions />}
|
||||
element={<Address />}
|
||||
/>
|
||||
<Route path="*" element={<Home />} />
|
||||
<Route path="*" element={<PageNotFound />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
|
16
src/PageNotFound.tsx
Normal file
16
src/PageNotFound.tsx
Normal 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;
|
@ -1,129 +1,13 @@
|
||||
import React, { useMemo, useContext } from "react";
|
||||
import { useParams, Routes, Route } 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 { 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"
|
||||
)
|
||||
);
|
||||
import React from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import TransactionPageContent from "./TransactionPageContent";
|
||||
|
||||
const Transaction: React.FC = () => {
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
const { txhash } = useParams();
|
||||
if (txhash === undefined) {
|
||||
throw new Error("txhash couldn't be undefined here");
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
return <TransactionPageContent txHash={txhash} />;
|
||||
};
|
||||
|
||||
export default Transaction;
|
||||
|
131
src/TransactionPageContent.tsx
Normal file
131
src/TransactionPageContent.tsx
Normal 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;
|
@ -1,11 +1,12 @@
|
||||
import React, { useContext } from "react";
|
||||
import PlainAddress from "./PlainAddress";
|
||||
import { resolverRendererRegistry } from "../api/address-resolver";
|
||||
import { useResolvedAddress } from "../useResolvedAddresses";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import PlainAddress from "./PlainAddress";
|
||||
import { ChecksummedAddress } from "../types";
|
||||
|
||||
type AddressOrENSNameProps = {
|
||||
address: string;
|
||||
address: ChecksummedAddress;
|
||||
selectedAddress?: string;
|
||||
dontOverrideColors?: boolean;
|
||||
};
|
||||
|
25
src/components/AddressOrENSNameInvalidNonce.tsx
Normal file
25
src/components/AddressOrENSNameInvalidNonce.tsx
Normal 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);
|
24
src/components/AddressOrENSNameNoTx.tsx
Normal file
24
src/components/AddressOrENSNameNoTx.tsx
Normal 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);
|
22
src/components/AddressOrENSNameNotFound.tsx
Normal file
22
src/components/AddressOrENSNameNotFound.tsx
Normal 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);
|
@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { commify } from "@ethersproject/units";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp";
|
||||
|
||||
@ -14,7 +15,7 @@ const Nonce: React.FC<NonceProps> = ({ value }) => (
|
||||
<span className="text-green-400">
|
||||
<FontAwesomeIcon icon={faArrowUp} size="1x" />
|
||||
</span>
|
||||
<span className="text-green-600">{value}</span>
|
||||
<span className="text-green-600">{commify(value)}</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { transactionURL } from "../url";
|
||||
|
||||
type TransactionLinkProps = {
|
||||
txHash: string;
|
||||
@ -8,7 +9,7 @@ type TransactionLinkProps = {
|
||||
const TransactionLink: React.FC<TransactionLinkProps> = ({ txHash }) => (
|
||||
<NavLink
|
||||
className="text-link-blue hover:text-link-blue-hover font-hash"
|
||||
to={`/tx/${txHash}`}
|
||||
to={transactionURL(txHash)}
|
||||
>
|
||||
<p className="truncate">{txHash}</p>
|
||||
</NavLink>
|
||||
|
@ -1,3 +1,3 @@
|
||||
export const MIN_API_LEVEL = 5;
|
||||
export const MIN_API_LEVEL = 6;
|
||||
|
||||
export const PAGE_SIZE = 25;
|
||||
|
@ -206,17 +206,36 @@ export class SearchController {
|
||||
}
|
||||
}
|
||||
|
||||
const doSearch = (q: string, navigate: NavigateFunction) => {
|
||||
if (isAddress(q)) {
|
||||
navigate(`/address/${q}`, { replace: true });
|
||||
const doSearch = async (q: string, navigate: NavigateFunction) => {
|
||||
// Cleanup
|
||||
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;
|
||||
}
|
||||
|
||||
// Tx hash?
|
||||
if (isHexString(q, 32)) {
|
||||
navigate(`/tx/${q}`, { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Block number?
|
||||
const blockNumber = parseInt(q);
|
||||
if (!isNaN(blockNumber)) {
|
||||
navigate(`/block/${blockNumber}`, { replace: true });
|
||||
@ -224,7 +243,12 @@ const doSearch = (q: string, navigate: NavigateFunction) => {
|
||||
}
|
||||
|
||||
// Assume it is an ENS name
|
||||
navigate(`/address/${q}`);
|
||||
navigate(
|
||||
`/address/${maybeAddress}${
|
||||
maybeIndex !== "" ? `?nonce=${maybeIndex}` : ""
|
||||
}`,
|
||||
{ replace: true }
|
||||
);
|
||||
};
|
||||
|
||||
export const useGenericSearch = (): [
|
||||
|
@ -15,6 +15,7 @@ import BlockConfirmations from "../components/BlockConfirmations";
|
||||
import TransactionAddress from "../components/TransactionAddress";
|
||||
import Copy from "../components/Copy";
|
||||
import Nonce from "../components/Nonce";
|
||||
import NavNonce from "./NavNonce";
|
||||
import Timestamp from "../components/Timestamp";
|
||||
import InternalTransactionOperation from "../components/InternalTransactionOperation";
|
||||
import MethodName from "../components/MethodName";
|
||||
@ -253,6 +254,7 @@ const Details: React.FC<DetailsProps> = ({
|
||||
</div>
|
||||
<div className="flex items-baseline pl-3">
|
||||
<Nonce value={txData.nonce} />
|
||||
<NavNonce sender={txData.from} nonce={txData.nonce} />
|
||||
</div>
|
||||
</div>
|
||||
</InfoRow>
|
||||
|
36
src/transaction/NavButton.tsx
Normal file
36
src/transaction/NavButton.tsx
Normal 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;
|
66
src/transaction/NavNonce.tsx
Normal file
66
src/transaction/NavNonce.tsx
Normal 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);
|
@ -18,6 +18,11 @@ export const blockURL = (blockNum: BlockTag) => `/block/${blockNum}`;
|
||||
|
||||
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 {
|
||||
// Resolve trusted IPNS for root IPFS
|
||||
IPFS_IPNS,
|
||||
|
@ -6,6 +6,7 @@ import { Contract } from "@ethersproject/contracts";
|
||||
import { defaultAbiCoder } from "@ethersproject/abi";
|
||||
import { BigNumber } from "@ethersproject/bignumber";
|
||||
import { arrayify, hexDataSlice, isHexString } from "@ethersproject/bytes";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
import { getInternalOperations } from "./nodeFunctions";
|
||||
import {
|
||||
TokenMetas,
|
||||
@ -190,98 +191,107 @@ export const useTxData = (
|
||||
}
|
||||
|
||||
const readTxData = async () => {
|
||||
const [_response, _receipt] = await Promise.all([
|
||||
provider.getTransaction(txhash),
|
||||
provider.getTransactionReceipt(txhash),
|
||||
]);
|
||||
if (_response === null) {
|
||||
try {
|
||||
const [_response, _receipt] = await Promise.all([
|
||||
provider.getTransaction(txhash),
|
||||
provider.getTransactionReceipt(txhash),
|
||||
]);
|
||||
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);
|
||||
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();
|
||||
}, [provider, txhash]);
|
||||
|
||||
@ -502,3 +512,91 @@ export const useTransactionError = (
|
||||
|
||||
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;
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ import { SelectedResolvedName } from "./api/address-resolver/CompositeAddressRes
|
||||
import { RuntimeContext } from "./useRuntime";
|
||||
import { ChecksummedAddress } from "./types";
|
||||
|
||||
export const useAddressOrENSFromURL = (
|
||||
export const useAddressOrENS = (
|
||||
addressOrName: string,
|
||||
urlFixer: (address: ChecksummedAddress) => void
|
||||
): [
|
||||
|
Loading…
Reference in New Issue
Block a user