diff --git a/.gitmodules b/.gitmodules index 3ee2444..e55ad32 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/src/AddressTransactions.tsx b/src/Address.tsx similarity index 88% rename from src/AddressTransactions.tsx rename to src/Address.tsx index d7222c0..3b9b62d 100644 --- a/src/AddressTransactions.tsx +++ b/src/Address.tsx @@ -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 ( + + ); + } + return ( {error ? ( - - "{addressOrName}" is not an ETH address or ENS name. - + ) : ( checksummedAddress && ( <> @@ -175,4 +192,4 @@ const AddressTransactions: React.FC = () => { ); }; -export default AddressTransactions; +export default Address; diff --git a/src/AddressTransactionByNonce.tsx b/src/AddressTransactionByNonce.tsx new file mode 100644 index 0000000..e804f84 --- /dev/null +++ b/src/AddressTransactionByNonce.tsx @@ -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 = ({ + checksummedAddress, + rawNonce, +}) => { + const { provider } = useContext(RuntimeContext); + + // Calculate txCount ONLY when asked for latest nonce + const [txCount, setTxCount] = useState(); + 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 ; + } + + // Address hasn't made the first outbound tx yet + if (nonce < 0) { + return ( + + + + ); + } + + // Garbage nonce + if (isNaN(nonce)) { + return ( + + + + ); + } + + // Valid nonce, but no tx found + if (txHash === null) { + return ( + + + + ); + } + + // Success; replace and render filler + navigate(transactionURL(txHash), { replace: true }); + return ; +}; + +export default AddressTransactionByNonce; diff --git a/src/App.tsx b/src/App.tsx index f31fff6..62d3b2c 100644 --- a/src/App.tsx +++ b/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 = () => { } /> } + element={
} /> - } /> + } /> diff --git a/src/PageNotFound.tsx b/src/PageNotFound.tsx new file mode 100644 index 0000000..690f0a0 --- /dev/null +++ b/src/PageNotFound.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { NavLink } from "react-router-dom"; +import StandardFrame from "./StandardFrame"; + +const PageNotFound: React.FC = () => ( + +
+ Page not found! + + Click here to go to home + +
+
+); + +export default PageNotFound; diff --git a/src/Transaction.tsx b/src/Transaction.tsx index 22873d0..94b2e43 100644 --- a/src/Transaction.tsx +++ b/src/Transaction.tsx @@ -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 ( - - - Transaction Details - {txData === null && ( - -
- Transaction {txhash} not found. -
-
- )} - {txData && ( - - - - Overview - {txData.confirmedData?.blockNumber !== undefined && ( - - Logs - {txData && ` (${txData.confirmedData?.logs?.length ?? 0})`} - - )} - Trace - - - - - - } - /> - } - /> - } /> - - - - )} -
-
- ); + return ; }; export default Transaction; diff --git a/src/TransactionPageContent.tsx b/src/TransactionPageContent.tsx new file mode 100644 index 0000000..377c345 --- /dev/null +++ b/src/TransactionPageContent.tsx @@ -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 = ({ + 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 ( + + + Transaction Details + {txData === null && ( + +
+ Transaction {txHash} not found. +
+
+ )} + {txData && ( + + + + Overview + {txData.confirmedData?.blockNumber !== undefined && ( + + Logs + {txData && ` (${txData.confirmedData?.logs?.length ?? 0})`} + + )} + Trace + + + + + + } + /> + } + /> + } /> + + + + )} +
+
+ ); +}; + +export default TransactionPageContent; diff --git a/src/components/AddressOrENSName.tsx b/src/components/AddressOrENSName.tsx index 9f43662..b884c48 100644 --- a/src/components/AddressOrENSName.tsx +++ b/src/components/AddressOrENSName.tsx @@ -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; }; diff --git a/src/components/AddressOrENSNameInvalidNonce.tsx b/src/components/AddressOrENSNameInvalidNonce.tsx new file mode 100644 index 0000000..a92496a --- /dev/null +++ b/src/components/AddressOrENSNameInvalidNonce.tsx @@ -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 }) => ( + <> + Transaction Details + +
+ + : no transaction found for nonce="{nonce}". +
+
+ +); + +export default React.memo(AddressOrENSNameInvalidNonce); diff --git a/src/components/AddressOrENSNameNoTx.tsx b/src/components/AddressOrENSNameNoTx.tsx new file mode 100644 index 0000000..c4f6178 --- /dev/null +++ b/src/components/AddressOrENSNameNoTx.tsx @@ -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 = ({ + addressOrENSName, +}) => ( + <> + Transaction Details + +
+ + : no outbound transactions found. +
+
+ +); + +export default React.memo(AddressOrENSNameNoTx); diff --git a/src/components/AddressOrENSNameNotFound.tsx b/src/components/AddressOrENSNameNotFound.tsx new file mode 100644 index 0000000..437aa6c --- /dev/null +++ b/src/components/AddressOrENSNameNotFound.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import StandardSubtitle from "../StandardSubtitle"; +import ContentFrame from "../ContentFrame"; + +type AddressOrENSNameNotFoundProps = { + addressOrENSName: string; +}; + +const AddressOrENSNameNotFound: React.FC = ({ + addressOrENSName, +}) => ( + <> + Transaction Details + +
+ "{addressOrENSName}" is not an ETH address or ENS name. +
+
+ +); + +export default React.memo(AddressOrENSNameNotFound); diff --git a/src/components/Nonce.tsx b/src/components/Nonce.tsx index f03eaad..2a59258 100644 --- a/src/components/Nonce.tsx +++ b/src/components/Nonce.tsx @@ -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 = ({ value }) => ( - {value} + {commify(value)} ); diff --git a/src/components/TransactionLink.tsx b/src/components/TransactionLink.tsx index 8db23f3..06fde64 100644 --- a/src/components/TransactionLink.tsx +++ b/src/components/TransactionLink.tsx @@ -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 = ({ txHash }) => (

{txHash}

diff --git a/src/params.ts b/src/params.ts index d1464c6..b1ef13b 100644 --- a/src/params.ts +++ b/src/params.ts @@ -1,3 +1,3 @@ -export const MIN_API_LEVEL = 5; +export const MIN_API_LEVEL = 6; export const PAGE_SIZE = 25; diff --git a/src/search/search.ts b/src/search/search.ts index 57a5958..95ebdce 100644 --- a/src/search/search.ts +++ b/src/search/search.ts @@ -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 = (): [ diff --git a/src/transaction/Details.tsx b/src/transaction/Details.tsx index 695a686..261e651 100644 --- a/src/transaction/Details.tsx +++ b/src/transaction/Details.tsx @@ -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 = ({
+
diff --git a/src/transaction/NavButton.tsx b/src/transaction/NavButton.tsx new file mode 100644 index 0000000..cad49d2 --- /dev/null +++ b/src/transaction/NavButton.tsx @@ -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 = ({ + sender, + nonce, + disabled, + children, +}) => { + if (disabled) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +}; + +export default NavButton; diff --git a/src/transaction/NavNonce.tsx b/src/transaction/NavNonce.tsx new file mode 100644 index 0000000..9efd47f --- /dev/null +++ b/src/transaction/NavNonce.tsx @@ -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 = ({ 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 ( +
+ + + + = count - 1} + > + + + = count - 1} + > + + + +
+ ); +}; + +export default React.memo(NavNonce); diff --git a/src/url.ts b/src/url.ts index 471c9d8..cf44ab6 100644 --- a/src/url.ts +++ b/src/url.ts @@ -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, diff --git a/src/useErigonHooks.ts b/src/useErigonHooks.ts index e63a718..45378f5 100644 --- a/src/useErigonHooks.ts +++ b/src/useErigonHooks.ts @@ -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 => + 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 => { + 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, + 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; +}; diff --git a/src/useResolvedAddresses.ts b/src/useResolvedAddresses.ts index adc3819..056fa98 100644 --- a/src/useResolvedAddresses.ts +++ b/src/useResolvedAddresses.ts @@ -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 ): [