diff --git a/src/PriceBox.tsx b/src/PriceBox.tsx index 60b0903..30ec31e 100644 --- a/src/PriceBox.tsx +++ b/src/PriceBox.tsx @@ -1,17 +1,16 @@ -import React, { useState, useEffect, useMemo, useContext } from "react"; -import { Contract } from "@ethersproject/contracts"; +import React, { useMemo, useContext } from "react"; import { commify, formatUnits } from "@ethersproject/units"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faGasPump } from "@fortawesome/free-solid-svg-icons/faGasPump"; -import AggregatorV3Interface from "@chainlink/contracts/abi/v0.8/AggregatorV3Interface.json"; import { RuntimeContext } from "./useRuntime"; import { formatValue } from "./components/formatter"; import { useLatestBlockHeader } from "./useLatestBlock"; import { useChainInfo } from "./useChainInfo"; +import { useETHUSDRawOracle, useFastGasRawOracle } from "./usePriceOracle"; +// TODO: encapsulate this magic number const ETH_FEED_DECIMALS = 8; -// TODO: reduce duplication with useETHUSDOracle const PriceBox: React.FC = () => { const { provider } = useContext(RuntimeContext); const { @@ -22,37 +21,8 @@ const PriceBox: React.FC = () => { const maybeOutdated: boolean = latestBlock !== undefined && Date.now() / 1000 - latestBlock.timestamp > 3600; - const ethFeed = useMemo( - () => - provider && - new Contract("eth-usd.data.eth", AggregatorV3Interface, provider), - [provider] - ); - const gasFeed = useMemo( - () => - provider && - new Contract("fast-gas-gwei.data.eth", AggregatorV3Interface, provider), - [provider] - ); - - const [latestPriceData, setLatestPriceData] = useState(); - const [latestGasData, setLatestGasData] = useState(); - useEffect(() => { - if (!ethFeed || !gasFeed) { - return; - } - - const readData = async () => { - const [priceData, gasData] = await Promise.all([ - ethFeed.latestRoundData(), - await gasFeed.latestRoundData(), - ]); - setLatestPriceData(priceData); - setLatestGasData(gasData); - }; - readData(); - }, [ethFeed, gasFeed]); + const latestPriceData = useETHUSDRawOracle(provider, "latest"); const [latestPrice, latestPriceTimestamp] = useMemo(() => { if (!latestPriceData) { return [undefined, undefined]; @@ -65,6 +35,7 @@ const PriceBox: React.FC = () => { return [formattedPrice, timestamp]; }, [latestPriceData]); + const latestGasData = useFastGasRawOracle(provider, "latest"); const [latestGasPrice, latestGasPriceTimestamp] = useMemo(() => { if (!latestGasData) { return [undefined, undefined]; diff --git a/src/TokenTransferItem.tsx b/src/TokenTransferItem.tsx index 0284a58..5fba3ca 100644 --- a/src/TokenTransferItem.tsx +++ b/src/TokenTransferItem.tsx @@ -6,24 +6,21 @@ import TransactionAddress from "./components/TransactionAddress"; import ValueHighlighter from "./components/ValueHighlighter"; import FormattedBalance from "./components/FormattedBalance"; import USDAmount from "./components/USDAmount"; -import { AddressContext, TokenMeta, TokenTransfer } from "./types"; import { RuntimeContext } from "./useRuntime"; import { useBlockNumberContext } from "./useBlockTagContext"; +import { useTokenMetadata } from "./useErigonHooks"; import { useTokenUSDOracle } from "./usePriceOracle"; +import { AddressContext, TokenTransfer } from "./types"; type TokenTransferItemProps = { t: TokenTransfer; - tokenMeta?: TokenMeta | null | undefined; }; -// TODO: handle partial -const TokenTransferItem: React.FC = ({ - t, - tokenMeta, -}) => { +const TokenTransferItem: React.FC = ({ t }) => { const { provider } = useContext(RuntimeContext); const blockNumber = useBlockNumberContext(); const [quote, decimals] = useTokenUSDOracle(provider, blockNumber, t.token); + const tokenMeta = useTokenMetadata(provider, t.token); return (
diff --git a/src/Transaction.tsx b/src/Transaction.tsx index 905b5d8..4c2596a 100644 --- a/src/Transaction.tsx +++ b/src/Transaction.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React, { useContext, useEffect } from "react"; import { useParams, Route, Routes } from "react-router-dom"; import { Tab } from "@headlessui/react"; import StandardFrame from "./StandardFrame"; @@ -25,6 +25,12 @@ const Transaction: React.FC = () => { const txData = useTxData(provider, txHash); const selectionCtx = useSelection(); + useEffect(() => { + if (txData) { + document.title = `Transaction ${txData.transactionHash} | Otterscan`; + } + }, [txData]); + return ( @@ -46,8 +52,7 @@ const Transaction: React.FC = () => { {txData.confirmedData?.blockNumber !== undefined && ( Logs - {txData && - ` (${txData.confirmedData?.logs?.length ?? 0})`} + {` (${txData.confirmedData?.logs?.length ?? 0})`} )} Trace diff --git a/src/api/address-resolver/ERCTokenResolver.ts b/src/api/address-resolver/ERCTokenResolver.ts index 49301bd..18ee2b6 100644 --- a/src/api/address-resolver/ERCTokenResolver.ts +++ b/src/api/address-resolver/ERCTokenResolver.ts @@ -1,18 +1,18 @@ import { BaseProvider } from "@ethersproject/providers"; import { Contract } from "@ethersproject/contracts"; -import { Interface } from "@ethersproject/abi"; +import { AddressZero } from "@ethersproject/constants"; import { IAddressResolver } from "./address-resolver"; import erc20 from "../../erc20.json"; import { TokenMeta } from "../../types"; -const erc20Interface = new Interface(erc20); +const ERC20_PROTOTYPE = new Contract(AddressZero, erc20); export class ERCTokenResolver implements IAddressResolver { async resolveAddress( provider: BaseProvider, address: string ): Promise { - const erc20Contract = new Contract(address, erc20Interface, provider); + const erc20Contract = ERC20_PROTOTYPE.connect(provider).attach(address); try { const name = (await erc20Contract.name()) as string; if (!name.trim()) { diff --git a/src/api/address-resolver/UniswapV1Resolver.ts b/src/api/address-resolver/UniswapV1Resolver.ts index a8cb2e0..f3c33a5 100644 --- a/src/api/address-resolver/UniswapV1Resolver.ts +++ b/src/api/address-resolver/UniswapV1Resolver.ts @@ -12,6 +12,11 @@ const UNISWAP_V1_FACTORY_ABI = [ const NULL_ADDRESS = "0x0000000000000000000000000000000000000000"; +const UNISWAP_V1_FACTORY_PROTOTYPE = new Contract( + UNISWAP_V1_FACTORY, + UNISWAP_V1_FACTORY_ABI +); + export type UniswapV1TokenMeta = { address: ChecksummedAddress; } & TokenMeta; @@ -28,11 +33,7 @@ export class UniswapV1Resolver implements IAddressResolver { provider: BaseProvider, address: string ): Promise { - const factoryContract = new Contract( - UNISWAP_V1_FACTORY, - UNISWAP_V1_FACTORY_ABI, - provider - ); + const factoryContract = UNISWAP_V1_FACTORY_PROTOTYPE.connect(provider); try { // First, probe the getToken() function; if it responds with an UniswapV1 exchange diff --git a/src/api/address-resolver/UniswapV2Resolver.ts b/src/api/address-resolver/UniswapV2Resolver.ts index 761237c..e493ff0 100644 --- a/src/api/address-resolver/UniswapV2Resolver.ts +++ b/src/api/address-resolver/UniswapV2Resolver.ts @@ -1,5 +1,6 @@ import { BaseProvider } from "@ethersproject/providers"; import { Contract } from "@ethersproject/contracts"; +import { AddressZero } from "@ethersproject/constants"; import { IAddressResolver } from "./address-resolver"; import { ChecksummedAddress, TokenMeta } from "../../types"; import { ERCTokenResolver } from "./ERCTokenResolver"; @@ -16,6 +17,16 @@ const UNISWAP_V2_PAIR_ABI = [ "function token1() external view returns (address)", ]; +const UNISWAP_V2_FACTORY_PROTOTYPE = new Contract( + UNISWAP_V2_FACTORY, + UNISWAP_V2_FACTORY_ABI +); + +const UNISWAP_V2_PAIR_PROTOTYPE = new Contract( + AddressZero, + UNISWAP_V2_PAIR_ABI +); + export type UniswapV2TokenMeta = { address: ChecksummedAddress; } & TokenMeta; @@ -33,12 +44,9 @@ export class UniswapV2Resolver implements IAddressResolver { provider: BaseProvider, address: string ): Promise { - const pairContract = new Contract(address, UNISWAP_V2_PAIR_ABI, provider); - const factoryContract = new Contract( - UNISWAP_V2_FACTORY, - UNISWAP_V2_FACTORY_ABI, - provider - ); + const pairContract = + UNISWAP_V2_PAIR_PROTOTYPE.connect(provider).attach(address); + const factoryContract = UNISWAP_V2_FACTORY_PROTOTYPE.connect(provider); try { // First, probe the factory() function; if it responds with UniswapV2 factory diff --git a/src/api/address-resolver/UniswapV3Resolver.ts b/src/api/address-resolver/UniswapV3Resolver.ts index 835e823..c93d8df 100644 --- a/src/api/address-resolver/UniswapV3Resolver.ts +++ b/src/api/address-resolver/UniswapV3Resolver.ts @@ -1,5 +1,6 @@ import { BaseProvider } from "@ethersproject/providers"; import { Contract } from "@ethersproject/contracts"; +import { AddressZero } from "@ethersproject/constants"; import { IAddressResolver } from "./address-resolver"; import { ChecksummedAddress, TokenMeta } from "../../types"; import { ERCTokenResolver } from "./ERCTokenResolver"; @@ -17,6 +18,16 @@ const UNISWAP_V3_PAIR_ABI = [ "function fee() external view returns (uint24)", ]; +const UNISWAP_V3_FACTORY_PROTOTYPE = new Contract( + UNISWAP_V3_FACTORY, + UNISWAP_V3_FACTORY_ABI +); + +const UNISWAP_V3_PAIR_PROTOTYPE = new Contract( + AddressZero, + UNISWAP_V3_PAIR_ABI +); + export type UniswapV3TokenMeta = { address: ChecksummedAddress; } & TokenMeta; @@ -35,12 +46,9 @@ export class UniswapV3Resolver implements IAddressResolver { provider: BaseProvider, address: string ): Promise { - const poolContract = new Contract(address, UNISWAP_V3_PAIR_ABI, provider); - const factoryContract = new Contract( - UNISWAP_V3_FACTORY, - UNISWAP_V3_FACTORY_ABI, - provider - ); + const poolContract = + UNISWAP_V3_PAIR_PROTOTYPE.connect(provider).attach(address); + const factoryContract = UNISWAP_V3_FACTORY_PROTOTYPE.connect(provider); try { // First, probe the factory() function; if it responds with UniswapV2 factory diff --git a/src/components/InternalSelfDestruct.tsx b/src/components/InternalSelfDestruct.tsx index f87233f..d3cd33a 100644 --- a/src/components/InternalSelfDestruct.tsx +++ b/src/components/InternalSelfDestruct.tsx @@ -1,10 +1,12 @@ -import React from "react"; +import React, { useContext } from "react"; import { formatEther } from "@ethersproject/units"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight"; import AddressHighlighter from "./AddressHighlighter"; import DecoratedAddressLink from "./DecoratedAddressLink"; import TransactionAddress from "./TransactionAddress"; +import { RuntimeContext } from "../useRuntime"; +import { useBlockDataFromTransaction } from "../useErigonHooks"; import { useChainInfo } from "../useChainInfo"; import { TransactionData, InternalOperation } from "../types"; @@ -17,12 +19,12 @@ const InternalSelfDestruct: React.FC = ({ txData, internalOp, }) => { + const { provider } = useContext(RuntimeContext); + const block = useBlockDataFromTransaction(provider, txData); const { nativeCurrency: { symbol }, } = useChainInfo(); - const toMiner = - txData.confirmedData?.miner !== undefined && - internalOp.to === txData.confirmedData.miner; + const toMiner = block?.miner !== undefined && internalOp.to === block.miner; return ( <> diff --git a/src/components/InternalTransfer.tsx b/src/components/InternalTransfer.tsx index 0faeb77..e1de103 100644 --- a/src/components/InternalTransfer.tsx +++ b/src/components/InternalTransfer.tsx @@ -8,7 +8,7 @@ import AddressHighlighter from "./AddressHighlighter"; import DecoratedAddressLink from "./DecoratedAddressLink"; import USDAmount from "./USDAmount"; import { RuntimeContext } from "../useRuntime"; -import { useHasCode } from "../useErigonHooks"; +import { useBlockDataFromTransaction, useHasCode } from "../useErigonHooks"; import { useChainInfo } from "../useChainInfo"; import { useETHUSDOracle } from "../usePriceOracle"; import { TransactionData, InternalOperation } from "../types"; @@ -22,17 +22,16 @@ const InternalTransfer: React.FC = ({ txData, internalOp, }) => { + const { provider } = useContext(RuntimeContext); + const block = useBlockDataFromTransaction(provider, txData); + const { nativeCurrency: { symbol, decimals }, } = useChainInfo(); const fromMiner = - txData.confirmedData?.miner !== undefined && - internalOp.from === txData.confirmedData.miner; - const toMiner = - txData.confirmedData?.miner !== undefined && - internalOp.to === txData.confirmedData.miner; + block?.miner !== undefined && internalOp.from === block.miner; + const toMiner = block?.miner !== undefined && internalOp.to === block.miner; - const { provider } = useContext(RuntimeContext); const blockETHUSDPrice = useETHUSDOracle( provider, txData.confirmedData?.blockNumber diff --git a/src/components/TransactionAddress.tsx b/src/components/TransactionAddress.tsx index 823bd50..6045cf5 100644 --- a/src/components/TransactionAddress.tsx +++ b/src/components/TransactionAddress.tsx @@ -4,7 +4,7 @@ import DecoratedAddressLink from "./DecoratedAddressLink"; import { useSelectedTransaction } from "../useSelectedTransaction"; import { useBlockNumberContext } from "../useBlockTagContext"; import { RuntimeContext } from "../useRuntime"; -import { useHasCode } from "../useErigonHooks"; +import { useBlockDataFromTransaction, useHasCode } from "../useErigonHooks"; import { AddressContext, ChecksummedAddress } from "../types"; type TransactionAddressProps = { @@ -23,6 +23,8 @@ const TransactionAddress: React.FC = ({ const creation = address === txData?.confirmedData?.createdContractAddress; const { provider } = useContext(RuntimeContext); + const block = useBlockDataFromTransaction(provider, txData); + const blockNumber = useBlockNumberContext(); const toHasCode = useHasCode( provider, @@ -39,7 +41,7 @@ const TransactionAddress: React.FC = ({ { - const rawTransfers = await provider.send("ots_getInternalOperations", [ - txHash, - ]); - - const _transfers: InternalOperation[] = []; - for (const t of rawTransfers) { - _transfers.push({ - type: t.type, - from: getAddress(t.from), - to: getAddress(t.to), - value: t.value, - }); - } - return _transfers; -}; diff --git a/src/search/TransactionItem.tsx b/src/search/TransactionItem.tsx index 94e4820..c0f6d39 100644 --- a/src/search/TransactionItem.tsx +++ b/src/search/TransactionItem.tsx @@ -16,7 +16,7 @@ import TransactionItemFiatFee from "./TransactionItemFiatFee"; import { ProcessedTransaction } from "../types"; import { FeeDisplay } from "./useFeeToggler"; import { RuntimeContext } from "../useRuntime"; -import { useHasCode } from "../useErigonHooks"; +import { useHasCode, useSendsToMiner } from "../useErigonHooks"; import { formatValue } from "../components/formatter"; type TransactionItemProps = { @@ -36,6 +36,7 @@ const TransactionItem: React.FC = ({ tx.to ?? undefined, tx.blockNumber - 1 ); + const [sendsToMiner] = useSendsToMiner(provider, tx.hash, tx.miner); let direction: Direction | undefined; if (selectedAddress) { @@ -53,7 +54,7 @@ const TransactionItem: React.FC = ({ } } - const flash = tx.gasPrice.isZero() && tx.internalMinerInteraction; + const flash = tx.gasPrice.isZero() && sendsToMiner; return (
= ({ diff --git a/src/transaction/Details.tsx b/src/transaction/Details.tsx index 62844ff..8801e7e 100644 --- a/src/transaction/Details.tsx +++ b/src/transaction/Details.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useMemo, useState } from "react"; +import React, { useContext, useState } from "react"; import { Tab } from "@headlessui/react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle"; @@ -43,7 +43,12 @@ import { useTransactionDescription as useSourcifyTransactionDescription, } from "../sourcify/useSourcify"; import { RuntimeContext } from "../useRuntime"; -import { useInternalOperations, useTransactionError } from "../useErigonHooks"; +import { + useBlockDataFromTransaction, + useSendsToMiner, + useTokenTransfers, + useTransactionError, +} from "../useErigonHooks"; import { useChainInfo } from "../useChainInfo"; import { useETHUSDOracle } from "../usePriceOracle"; @@ -53,10 +58,10 @@ type DetailsProps = { const Details: React.FC = ({ txData }) => { const { provider } = useContext(RuntimeContext); + const block = useBlockDataFromTransaction(provider, txData); const hasEIP1559 = - txData.confirmedData?.blockBaseFeePerGas !== undefined && - txData.confirmedData?.blockBaseFeePerGas !== null; + block?.baseFeePerGas !== undefined && block?.baseFeePerGas !== null; const fourBytes = txData.to !== null ? extract4Bytes(txData.data) ?? "0x" : "0x"; @@ -67,19 +72,13 @@ const Details: React.FC = ({ txData }) => { txData.value ); - const internalOps = useInternalOperations(provider, txData); - const sendsEthToMiner = useMemo(() => { - if (!txData || !internalOps) { - return false; - } + const [sendsEthToMiner, internalOps] = useSendsToMiner( + provider, + txData.confirmedData ? txData.transactionHash : undefined, + block?.miner + ); - for (const t of internalOps) { - if (t.to === txData.confirmedData?.miner) { - return true; - } - } - return false; - }, [txData, internalOps]); + const tokenTransfers = useTokenTransfers(txData); const metadata = useSourcifyMetadata(txData?.to, provider?.network.chainId); @@ -224,22 +223,24 @@ const Details: React.FC = ({ txData }) => { confirmations={txData.confirmedData.confirmations} />
-
- - -
+ {block && ( +
+ + +
+ )}
- + {block && } )} @@ -290,14 +291,10 @@ const Details: React.FC = ({ txData }) => { )} - {txData.tokenTransfers.length > 0 && ( - - {txData.tokenTransfers.map((t, i) => ( - + {tokenTransfers && tokenTransfers.length > 0 && ( + + {tokenTransfers.map((t, i) => ( + ))} )} @@ -372,18 +369,10 @@ const Details: React.FC = ({ txData }) => { )} - {txData.confirmedData && hasEIP1559 && ( + {block && hasEIP1559 && ( - {" "} - Gwei ( - {" "} - wei) + Gwei ( + wei) )} {txData.confirmedData && ( diff --git a/src/transaction/RewardSplit.tsx b/src/transaction/RewardSplit.tsx index 56dd0db..520ea0e 100644 --- a/src/transaction/RewardSplit.tsx +++ b/src/transaction/RewardSplit.tsx @@ -1,24 +1,30 @@ -import React from "react"; +import React, { useContext } from "react"; +import { BigNumber } from "@ethersproject/bignumber"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn"; import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins"; import FormattedBalance from "../components/FormattedBalance"; import PercentageGauge from "../components/PercentageGauge"; -import { TransactionData } from "../types"; +import { RuntimeContext } from "../useRuntime"; +import { useBlockDataFromTransaction } from "../useErigonHooks"; import { useChainInfo } from "../useChainInfo"; +import { TransactionData } from "../types"; type RewardSplitProps = { txData: TransactionData; }; const RewardSplit: React.FC = ({ txData }) => { + const { provider } = useContext(RuntimeContext); + const block = useBlockDataFromTransaction(provider, txData); + const { nativeCurrency: { symbol }, } = useChainInfo(); const paidFees = txData.gasPrice.mul(txData.confirmedData!.gasUsed); - const burntFees = txData.confirmedData!.blockBaseFeePerGas!.mul( - txData.confirmedData!.gasUsed - ); + const burntFees = block + ? block.baseFeePerGas!.mul(txData.confirmedData!.gasUsed) + : BigNumber.from(0); const minerReward = paidFees.sub(burntFees); const burntPerc = diff --git a/src/types.ts b/src/types.ts index adedc86..1379192 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,7 +18,6 @@ export type ProcessedTransaction = { from?: string; to: string | null; createdContractAddress?: string; - internalMinerInteraction?: boolean; value: BigNumber; fee: BigNumber; gasPrice: BigNumber; @@ -37,8 +36,6 @@ export type TransactionData = { from: string; to?: string; value: BigNumber; - tokenTransfers: TokenTransfer[]; - tokenMetas: TokenMetas; type: number; maxFeePerGas?: BigNumber | undefined; maxPriorityFeePerGas?: BigNumber | undefined; @@ -53,11 +50,7 @@ export type ConfirmedTransactionData = { status: boolean; blockNumber: number; transactionIndex: number; - blockBaseFeePerGas?: BigNumber | undefined | null; - blockTransactionCount: number; confirmations: number; - timestamp: number; - miner: string; createdContractAddress?: string; fee: BigNumber; gasUsed: BigNumber; diff --git a/src/use4Bytes.ts b/src/use4Bytes.ts index ddf8e2b..b61b076 100644 --- a/src/use4Bytes.ts +++ b/src/use4Bytes.ts @@ -53,23 +53,29 @@ const fourBytesFetcher = const fourBytes = key.slice(2); const signatureURL = fourBytesURL(assetsURLPrefix, fourBytes); - const res = await fetch(signatureURL); - if (!res.ok) { - console.warn(`Signature does not exist in 4bytes DB: ${fourBytes}`); + try { + const res = await fetch(signatureURL); + if (!res.ok) { + console.warn(`Signature does not exist in 4bytes DB: ${fourBytes}`); + return null; + } + + // Get only the first occurrence, for now ignore alternative param names + const sigs = await res.text(); + const sig = sigs.split(";")[0]; + const cut = sig.indexOf("("); + const method = sig.slice(0, cut); + + const entry: FourBytesEntry = { + name: method, + signature: sig, + }; + return entry; + } catch (err) { + // Network error or something wrong with URL config; + // silence and don't try it again return null; } - - // Get only the first occurrence, for now ignore alternative param names - const sigs = await res.text(); - const sig = sigs.split(";")[0]; - const cut = sig.indexOf("("); - const method = sig.slice(0, cut); - - const entry: FourBytesEntry = { - name: method, - signature: sig, - }; - return entry; }; /** diff --git a/src/useErigonHooks.ts b/src/useErigonHooks.ts index f2b798c..50663ce 100644 --- a/src/useErigonHooks.ts +++ b/src/useErigonHooks.ts @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { Block, BlockWithTransactions, @@ -10,17 +10,17 @@ import { Contract } from "@ethersproject/contracts"; import { defaultAbiCoder } from "@ethersproject/abi"; import { BigNumber } from "@ethersproject/bignumber"; import { arrayify, hexDataSlice, isHexString } from "@ethersproject/bytes"; +import { AddressZero } from "@ethersproject/constants"; import useSWR from "swr"; import useSWRImmutable from "swr/immutable"; -import { getInternalOperations } from "./nodeFunctions"; import { - TokenMetas, TokenTransfer, TransactionData, InternalOperation, ProcessedTransaction, OperationType, ChecksummedAddress, + TokenMeta, } from "./types"; import erc20 from "./erc20.json"; @@ -141,27 +141,6 @@ export const useBlockTransactions = ( .reverse(); setTxs(rawTxs); setTotalTxs(result.fullblock.transactionCount); - - const checkTouchMinerAddr = await Promise.all( - rawTxs.map(async (res) => { - const ops = await getInternalOperations(provider, res.hash); - return ( - ops.findIndex( - (op) => - op.type === OperationType.TRANSFER && - res.miner !== undefined && - res.miner === getAddress(op.to) - ) !== -1 - ); - }) - ); - const processedTxs = rawTxs.map( - (r, i): ProcessedTransaction => ({ - ...r, - internalMinerInteraction: checkTouchMinerAddr[i], - }) - ); - setTxs(processedTxs); }; readBlock(); }, [provider, blockNumber, pageNumber, pageSize]); @@ -169,23 +148,40 @@ export const useBlockTransactions = ( return [totalTxs, txs]; }; +const blockDataFetcher = async ( + provider: JsonRpcProvider, + blockNumberOrHash: string +) => { + return await readBlock(provider, blockNumberOrHash); +}; + +// TODO: some callers may use only block headers? export const useBlockData = ( provider: JsonRpcProvider | undefined, - blockNumberOrHash: string + blockNumberOrHash: string | undefined ): ExtendedBlock | null | undefined => { - const [block, setBlock] = useState(); - useEffect(() => { - if (!provider) { - return undefined; - } - - const _readBlock = async () => { - const extBlock = await readBlock(provider, blockNumberOrHash); - setBlock(extBlock); - }; - _readBlock(); - }, [provider, blockNumberOrHash]); + const { data, error } = useSWRImmutable( + provider !== undefined && blockNumberOrHash !== undefined + ? [provider, blockNumberOrHash] + : null, + blockDataFetcher + ); + if (error) { + return undefined; + } + return data; +}; +export const useBlockDataFromTransaction = ( + provider: JsonRpcProvider | undefined, + txData: TransactionData | null | undefined +): ExtendedBlock | null | undefined => { + const block = useBlockData( + provider, + txData?.confirmedData + ? txData.confirmedData.blockNumber.toString() + : undefined + ); return block; }; @@ -211,66 +207,11 @@ export const useTxData = ( return; } - let _block: ExtendedBlock | null | 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, @@ -285,11 +226,7 @@ export const useTxData = ( 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, @@ -308,33 +245,75 @@ export const useTxData = ( return txData; }; +export const useTokenTransfers = ( + txData: TransactionData +): TokenTransfer[] | undefined => { + const transfers = useMemo(() => { + if (!txData.confirmedData) { + return undefined; + } + + return txData.confirmedData.logs + .filter((l) => l.topics.length === 3 && l.topics[0] === TRANSFER_TOPIC) + .map((l) => ({ + token: l.address, + from: getAddress(hexDataSlice(arrayify(l.topics[1]), 12)), + to: getAddress(hexDataSlice(arrayify(l.topics[2]), 12)), + value: BigNumber.from(l.data), + })); + }, [txData]); + + return transfers; +}; + export const useInternalOperations = ( provider: JsonRpcProvider | undefined, - txData: TransactionData | undefined | null + txHash: string | undefined ): InternalOperation[] | undefined => { - const [intTransfers, setIntTransfers] = useState(); + const { data, error } = useSWRImmutable( + provider !== undefined && txHash !== undefined + ? ["ots_getInternalOperations", txHash] + : null, + providerFetcher(provider) + ); - useEffect(() => { - const traceTransfers = async () => { - if (!provider || !txData || !txData.confirmedData) { - return; - } + const _transfers = useMemo(() => { + if (provider === undefined || error || data === undefined) { + return undefined; + } - const _transfers = await getInternalOperations( - provider, - txData.transactionHash - ); - for (const t of _transfers) { - t.from = provider.formatter.address(t.from); - t.to = provider.formatter.address(t.to); - t.value = provider.formatter.bigNumber(t.value); - } - setIntTransfers(_transfers); - }; - traceTransfers(); - }, [provider, txData]); + const _t: InternalOperation[] = []; + for (const t of data) { + _t.push({ + type: t.type, + from: provider.formatter.address(getAddress(t.from)), + to: provider.formatter.address(getAddress(t.to)), + value: provider.formatter.bigNumber(t.value), + }); + } + return _t; + }, [provider, data]); + return _transfers; +}; - return intTransfers; +export const useSendsToMiner = ( + provider: JsonRpcProvider | undefined, + txHash: string | undefined, + miner: string | undefined +): [boolean, InternalOperation[]] | [undefined, undefined] => { + const ops = useInternalOperations(provider, txHash); + if (ops === undefined) { + return [undefined, undefined]; + } + + const send = + ops.findIndex( + (op) => + op.type === OperationType.TRANSFER && + miner !== undefined && + miner === getAddress(op.to) + ) !== -1; + return [send, ops]; }; export type TraceEntry = { @@ -665,3 +644,61 @@ export const useHasCode = ( } return data as boolean | undefined; }; + +const ERC20_PROTOTYPE = new Contract(AddressZero, erc20); + +const tokenMetadataFetcher = + (provider: JsonRpcProvider | undefined) => + async ( + _: "tokenmeta", + address: ChecksummedAddress + ): Promise => { + if (provider === undefined) { + return null; + } + + const erc20Contract = ERC20_PROTOTYPE.connect(provider).attach(address); + try { + const name = (await erc20Contract.name()) as string; + if (!name.trim()) { + return null; + } + + const [symbol, decimals] = (await Promise.all([ + erc20Contract.symbol(), + erc20Contract.decimals(), + ])) as [string, number]; + + // Prevent faulty tokens with empty name/symbol + if (!symbol.trim()) { + return null; + } + + return { + name, + symbol, + decimals, + }; + } catch (err) { + // Ignore on purpose; this indicates the probe failed and the address + // is not a token + return null; + } + }; + +export const useTokenMetadata = ( + provider: JsonRpcProvider | undefined, + address: ChecksummedAddress | undefined +): TokenMeta | null | undefined => { + const fetcher = tokenMetadataFetcher(provider); + const { data, error } = useSWRImmutable( + provider !== undefined && address !== undefined + ? ["tokenmeta", address] + : null, + fetcher + ); + if (error) { + return undefined; + } + return data; +}; diff --git a/src/usePriceOracle.ts b/src/usePriceOracle.ts index 783d76e..309aa9e 100644 --- a/src/usePriceOracle.ts +++ b/src/usePriceOracle.ts @@ -1,6 +1,7 @@ import { JsonRpcProvider, BlockTag } from "@ethersproject/providers"; import { Contract } from "@ethersproject/contracts"; import { BigNumber } from "@ethersproject/bignumber"; +import { AddressZero } from "@ethersproject/constants"; import AggregatorV3Interface from "@chainlink/contracts/abi/v0.8/AggregatorV3Interface.json"; import FeedRegistryInterface from "@chainlink/contracts/abi/v0.8/FeedRegistryInterface.json"; import { Fetcher } from "swr"; @@ -26,11 +27,20 @@ const feedRegistryFetcherKey = ( return [tokenAddress, blockTag]; }; +const FEED_REGISTRY_MAINNET_PROTOTYPE = new Contract( + FEED_REGISTRY_MAINNET, + FeedRegistryInterface +); + const feedRegistryFetcher = ( provider: JsonRpcProvider | undefined ): Fetcher => async (tokenAddress, blockTag) => { + if (provider === undefined) { + return [undefined, undefined]; + } + // It work works on ethereum mainnet and kovan, see: // https://docs.chain.link/docs/feed-registry/ if (provider!.network.chainId !== 1) { @@ -38,11 +48,7 @@ const feedRegistryFetcher = } // Let SWR handle error - const feedRegistry = new Contract( - FEED_REGISTRY_MAINNET, - FeedRegistryInterface, - provider - ); + const feedRegistry = FEED_REGISTRY_MAINNET_PROTOTYPE.connect(provider); const priceData = await feedRegistry.latestRoundData(tokenAddress, USD, { blockTag, }); @@ -76,23 +82,39 @@ const ethUSDFetcherKey = (blockTag: BlockTag | undefined) => { return ["ethusd", blockTag]; }; +const ETH_USD_FEED_PROTOTYPE = new Contract(AddressZero, AggregatorV3Interface); + const ethUSDFetcher = ( provider: JsonRpcProvider | undefined - ): Fetcher => + ): Fetcher => async (_, blockTag) => { if (provider?.network.chainId !== 1) { return undefined; } - const c = new Contract("eth-usd.data.eth", AggregatorV3Interface, provider); + + const c = + ETH_USD_FEED_PROTOTYPE.connect(provider).attach("eth-usd.data.eth"); const priceData = await c.latestRoundData({ blockTag }); - return BigNumber.from(priceData.answer); + return priceData; }; export const useETHUSDOracle = ( provider: JsonRpcProvider | undefined, blockTag: BlockTag | undefined ): BigNumber | undefined => { + const fetcher = ethUSDFetcher(provider); + const { data, error } = useSWRImmutable(ethUSDFetcherKey(blockTag), fetcher); + if (error) { + return undefined; + } + return data !== undefined ? BigNumber.from(data.answer) : undefined; +}; + +export const useETHUSDRawOracle = ( + provider: JsonRpcProvider | undefined, + blockTag: BlockTag | undefined +): any | undefined => { const fetcher = ethUSDFetcher(provider); const { data, error } = useSWRImmutable(ethUSDFetcherKey(blockTag), fetcher); if (error) { @@ -100,3 +122,42 @@ export const useETHUSDOracle = ( } return data; }; + +const fastGasFetcherKey = (blockTag: BlockTag | undefined) => { + if (blockTag === undefined) { + return null; + } + return ["gasgwei", blockTag]; +}; + +const FAST_GAS_FEED_PROTOTYPE = new Contract( + AddressZero, + AggregatorV3Interface +); + +const fastGasFetcher = + ( + provider: JsonRpcProvider | undefined + ): Fetcher => + async (_, blockTag) => { + if (provider?.network.chainId !== 1) { + return undefined; + } + const c = FAST_GAS_FEED_PROTOTYPE.connect(provider).attach( + "fast-gas-gwei.data.eth" + ); + const priceData = await c.latestRoundData({ blockTag }); + return priceData; + }; + +export const useFastGasRawOracle = ( + provider: JsonRpcProvider | undefined, + blockTag: BlockTag | undefined +): any | undefined => { + const fetcher = fastGasFetcher(provider); + const { data, error } = useSWRImmutable(fastGasFetcherKey(blockTag), fetcher); + if (error) { + return undefined; + } + return data; +};