Merge branch 'feature/swr-everything-3' into develop

This commit is contained in:
Willian Mitsuda 2022-08-24 04:48:24 -03:00
commit 0acac32fc9
No known key found for this signature in database
19 changed files with 367 additions and 303 deletions

View File

@ -1,17 +1,16 @@
import React, { useState, useEffect, useMemo, useContext } from "react"; import React, { useMemo, useContext } from "react";
import { Contract } from "@ethersproject/contracts";
import { commify, formatUnits } from "@ethersproject/units"; import { commify, formatUnits } from "@ethersproject/units";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGasPump } from "@fortawesome/free-solid-svg-icons/faGasPump"; import { faGasPump } from "@fortawesome/free-solid-svg-icons/faGasPump";
import AggregatorV3Interface from "@chainlink/contracts/abi/v0.8/AggregatorV3Interface.json";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { formatValue } from "./components/formatter"; import { formatValue } from "./components/formatter";
import { useLatestBlockHeader } from "./useLatestBlock"; import { useLatestBlockHeader } from "./useLatestBlock";
import { useChainInfo } from "./useChainInfo"; import { useChainInfo } from "./useChainInfo";
import { useETHUSDRawOracle, useFastGasRawOracle } from "./usePriceOracle";
// TODO: encapsulate this magic number
const ETH_FEED_DECIMALS = 8; const ETH_FEED_DECIMALS = 8;
// TODO: reduce duplication with useETHUSDOracle
const PriceBox: React.FC = () => { const PriceBox: React.FC = () => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const { const {
@ -22,37 +21,8 @@ const PriceBox: React.FC = () => {
const maybeOutdated: boolean = const maybeOutdated: boolean =
latestBlock !== undefined && latestBlock !== undefined &&
Date.now() / 1000 - latestBlock.timestamp > 3600; 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<any>();
const [latestGasData, setLatestGasData] = useState<any>();
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(() => { const [latestPrice, latestPriceTimestamp] = useMemo(() => {
if (!latestPriceData) { if (!latestPriceData) {
return [undefined, undefined]; return [undefined, undefined];
@ -65,6 +35,7 @@ const PriceBox: React.FC = () => {
return [formattedPrice, timestamp]; return [formattedPrice, timestamp];
}, [latestPriceData]); }, [latestPriceData]);
const latestGasData = useFastGasRawOracle(provider, "latest");
const [latestGasPrice, latestGasPriceTimestamp] = useMemo(() => { const [latestGasPrice, latestGasPriceTimestamp] = useMemo(() => {
if (!latestGasData) { if (!latestGasData) {
return [undefined, undefined]; return [undefined, undefined];

View File

@ -6,24 +6,21 @@ import TransactionAddress from "./components/TransactionAddress";
import ValueHighlighter from "./components/ValueHighlighter"; import ValueHighlighter from "./components/ValueHighlighter";
import FormattedBalance from "./components/FormattedBalance"; import FormattedBalance from "./components/FormattedBalance";
import USDAmount from "./components/USDAmount"; import USDAmount from "./components/USDAmount";
import { AddressContext, TokenMeta, TokenTransfer } from "./types";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { useBlockNumberContext } from "./useBlockTagContext"; import { useBlockNumberContext } from "./useBlockTagContext";
import { useTokenMetadata } from "./useErigonHooks";
import { useTokenUSDOracle } from "./usePriceOracle"; import { useTokenUSDOracle } from "./usePriceOracle";
import { AddressContext, TokenTransfer } from "./types";
type TokenTransferItemProps = { type TokenTransferItemProps = {
t: TokenTransfer; t: TokenTransfer;
tokenMeta?: TokenMeta | null | undefined;
}; };
// TODO: handle partial const TokenTransferItem: React.FC<TokenTransferItemProps> = ({ t }) => {
const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
t,
tokenMeta,
}) => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const blockNumber = useBlockNumberContext(); const blockNumber = useBlockNumberContext();
const [quote, decimals] = useTokenUSDOracle(provider, blockNumber, t.token); const [quote, decimals] = useTokenUSDOracle(provider, blockNumber, t.token);
const tokenMeta = useTokenMetadata(provider, t.token);
return ( return (
<div className="flex items-baseline space-x-2 px-2 py-1 truncate hover:bg-gray-100"> <div className="flex items-baseline space-x-2 px-2 py-1 truncate hover:bg-gray-100">

View File

@ -1,4 +1,4 @@
import React, { useContext } from "react"; import React, { useContext, useEffect } from "react";
import { useParams, Route, Routes } from "react-router-dom"; import { useParams, Route, Routes } from "react-router-dom";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import StandardFrame from "./StandardFrame"; import StandardFrame from "./StandardFrame";
@ -25,6 +25,12 @@ const Transaction: React.FC = () => {
const txData = useTxData(provider, txHash); const txData = useTxData(provider, txHash);
const selectionCtx = useSelection(); const selectionCtx = useSelection();
useEffect(() => {
if (txData) {
document.title = `Transaction ${txData.transactionHash} | Otterscan`;
}
}, [txData]);
return ( return (
<SelectedTransactionContext.Provider value={txData}> <SelectedTransactionContext.Provider value={txData}>
<BlockNumberContext.Provider value={txData?.confirmedData?.blockNumber}> <BlockNumberContext.Provider value={txData?.confirmedData?.blockNumber}>
@ -46,8 +52,7 @@ const Transaction: React.FC = () => {
{txData.confirmedData?.blockNumber !== undefined && ( {txData.confirmedData?.blockNumber !== undefined && (
<NavTab href="logs"> <NavTab href="logs">
Logs Logs
{txData && {` (${txData.confirmedData?.logs?.length ?? 0})`}
` (${txData.confirmedData?.logs?.length ?? 0})`}
</NavTab> </NavTab>
)} )}
<NavTab href="trace">Trace</NavTab> <NavTab href="trace">Trace</NavTab>

View File

@ -1,18 +1,18 @@
import { BaseProvider } from "@ethersproject/providers"; import { BaseProvider } from "@ethersproject/providers";
import { Contract } from "@ethersproject/contracts"; import { Contract } from "@ethersproject/contracts";
import { Interface } from "@ethersproject/abi"; import { AddressZero } from "@ethersproject/constants";
import { IAddressResolver } from "./address-resolver"; import { IAddressResolver } from "./address-resolver";
import erc20 from "../../erc20.json"; import erc20 from "../../erc20.json";
import { TokenMeta } from "../../types"; import { TokenMeta } from "../../types";
const erc20Interface = new Interface(erc20); const ERC20_PROTOTYPE = new Contract(AddressZero, erc20);
export class ERCTokenResolver implements IAddressResolver<TokenMeta> { export class ERCTokenResolver implements IAddressResolver<TokenMeta> {
async resolveAddress( async resolveAddress(
provider: BaseProvider, provider: BaseProvider,
address: string address: string
): Promise<TokenMeta | undefined> { ): Promise<TokenMeta | undefined> {
const erc20Contract = new Contract(address, erc20Interface, provider); const erc20Contract = ERC20_PROTOTYPE.connect(provider).attach(address);
try { try {
const name = (await erc20Contract.name()) as string; const name = (await erc20Contract.name()) as string;
if (!name.trim()) { if (!name.trim()) {

View File

@ -12,6 +12,11 @@ const UNISWAP_V1_FACTORY_ABI = [
const NULL_ADDRESS = "0x0000000000000000000000000000000000000000"; const NULL_ADDRESS = "0x0000000000000000000000000000000000000000";
const UNISWAP_V1_FACTORY_PROTOTYPE = new Contract(
UNISWAP_V1_FACTORY,
UNISWAP_V1_FACTORY_ABI
);
export type UniswapV1TokenMeta = { export type UniswapV1TokenMeta = {
address: ChecksummedAddress; address: ChecksummedAddress;
} & TokenMeta; } & TokenMeta;
@ -28,11 +33,7 @@ export class UniswapV1Resolver implements IAddressResolver<UniswapV1PairMeta> {
provider: BaseProvider, provider: BaseProvider,
address: string address: string
): Promise<UniswapV1PairMeta | undefined> { ): Promise<UniswapV1PairMeta | undefined> {
const factoryContract = new Contract( const factoryContract = UNISWAP_V1_FACTORY_PROTOTYPE.connect(provider);
UNISWAP_V1_FACTORY,
UNISWAP_V1_FACTORY_ABI,
provider
);
try { try {
// First, probe the getToken() function; if it responds with an UniswapV1 exchange // First, probe the getToken() function; if it responds with an UniswapV1 exchange

View File

@ -1,5 +1,6 @@
import { BaseProvider } from "@ethersproject/providers"; import { BaseProvider } from "@ethersproject/providers";
import { Contract } from "@ethersproject/contracts"; import { Contract } from "@ethersproject/contracts";
import { AddressZero } from "@ethersproject/constants";
import { IAddressResolver } from "./address-resolver"; import { IAddressResolver } from "./address-resolver";
import { ChecksummedAddress, TokenMeta } from "../../types"; import { ChecksummedAddress, TokenMeta } from "../../types";
import { ERCTokenResolver } from "./ERCTokenResolver"; import { ERCTokenResolver } from "./ERCTokenResolver";
@ -16,6 +17,16 @@ const UNISWAP_V2_PAIR_ABI = [
"function token1() external view returns (address)", "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 = { export type UniswapV2TokenMeta = {
address: ChecksummedAddress; address: ChecksummedAddress;
} & TokenMeta; } & TokenMeta;
@ -33,12 +44,9 @@ export class UniswapV2Resolver implements IAddressResolver<UniswapV2PairMeta> {
provider: BaseProvider, provider: BaseProvider,
address: string address: string
): Promise<UniswapV2PairMeta | undefined> { ): Promise<UniswapV2PairMeta | undefined> {
const pairContract = new Contract(address, UNISWAP_V2_PAIR_ABI, provider); const pairContract =
const factoryContract = new Contract( UNISWAP_V2_PAIR_PROTOTYPE.connect(provider).attach(address);
UNISWAP_V2_FACTORY, const factoryContract = UNISWAP_V2_FACTORY_PROTOTYPE.connect(provider);
UNISWAP_V2_FACTORY_ABI,
provider
);
try { try {
// First, probe the factory() function; if it responds with UniswapV2 factory // First, probe the factory() function; if it responds with UniswapV2 factory

View File

@ -1,5 +1,6 @@
import { BaseProvider } from "@ethersproject/providers"; import { BaseProvider } from "@ethersproject/providers";
import { Contract } from "@ethersproject/contracts"; import { Contract } from "@ethersproject/contracts";
import { AddressZero } from "@ethersproject/constants";
import { IAddressResolver } from "./address-resolver"; import { IAddressResolver } from "./address-resolver";
import { ChecksummedAddress, TokenMeta } from "../../types"; import { ChecksummedAddress, TokenMeta } from "../../types";
import { ERCTokenResolver } from "./ERCTokenResolver"; import { ERCTokenResolver } from "./ERCTokenResolver";
@ -17,6 +18,16 @@ const UNISWAP_V3_PAIR_ABI = [
"function fee() external view returns (uint24)", "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 = { export type UniswapV3TokenMeta = {
address: ChecksummedAddress; address: ChecksummedAddress;
} & TokenMeta; } & TokenMeta;
@ -35,12 +46,9 @@ export class UniswapV3Resolver implements IAddressResolver<UniswapV3PairMeta> {
provider: BaseProvider, provider: BaseProvider,
address: string address: string
): Promise<UniswapV3PairMeta | undefined> { ): Promise<UniswapV3PairMeta | undefined> {
const poolContract = new Contract(address, UNISWAP_V3_PAIR_ABI, provider); const poolContract =
const factoryContract = new Contract( UNISWAP_V3_PAIR_PROTOTYPE.connect(provider).attach(address);
UNISWAP_V3_FACTORY, const factoryContract = UNISWAP_V3_FACTORY_PROTOTYPE.connect(provider);
UNISWAP_V3_FACTORY_ABI,
provider
);
try { try {
// First, probe the factory() function; if it responds with UniswapV2 factory // First, probe the factory() function; if it responds with UniswapV2 factory

View File

@ -1,10 +1,12 @@
import React from "react"; import React, { useContext } from "react";
import { formatEther } from "@ethersproject/units"; import { formatEther } from "@ethersproject/units";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight"; import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight";
import AddressHighlighter from "./AddressHighlighter"; import AddressHighlighter from "./AddressHighlighter";
import DecoratedAddressLink from "./DecoratedAddressLink"; import DecoratedAddressLink from "./DecoratedAddressLink";
import TransactionAddress from "./TransactionAddress"; import TransactionAddress from "./TransactionAddress";
import { RuntimeContext } from "../useRuntime";
import { useBlockDataFromTransaction } from "../useErigonHooks";
import { useChainInfo } from "../useChainInfo"; import { useChainInfo } from "../useChainInfo";
import { TransactionData, InternalOperation } from "../types"; import { TransactionData, InternalOperation } from "../types";
@ -17,12 +19,12 @@ const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({
txData, txData,
internalOp, internalOp,
}) => { }) => {
const { provider } = useContext(RuntimeContext);
const block = useBlockDataFromTransaction(provider, txData);
const { const {
nativeCurrency: { symbol }, nativeCurrency: { symbol },
} = useChainInfo(); } = useChainInfo();
const toMiner = const toMiner = block?.miner !== undefined && internalOp.to === block.miner;
txData.confirmedData?.miner !== undefined &&
internalOp.to === txData.confirmedData.miner;
return ( return (
<> <>

View File

@ -8,7 +8,7 @@ import AddressHighlighter from "./AddressHighlighter";
import DecoratedAddressLink from "./DecoratedAddressLink"; import DecoratedAddressLink from "./DecoratedAddressLink";
import USDAmount from "./USDAmount"; import USDAmount from "./USDAmount";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { useHasCode } from "../useErigonHooks"; import { useBlockDataFromTransaction, useHasCode } from "../useErigonHooks";
import { useChainInfo } from "../useChainInfo"; import { useChainInfo } from "../useChainInfo";
import { useETHUSDOracle } from "../usePriceOracle"; import { useETHUSDOracle } from "../usePriceOracle";
import { TransactionData, InternalOperation } from "../types"; import { TransactionData, InternalOperation } from "../types";
@ -22,17 +22,16 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({
txData, txData,
internalOp, internalOp,
}) => { }) => {
const { provider } = useContext(RuntimeContext);
const block = useBlockDataFromTransaction(provider, txData);
const { const {
nativeCurrency: { symbol, decimals }, nativeCurrency: { symbol, decimals },
} = useChainInfo(); } = useChainInfo();
const fromMiner = const fromMiner =
txData.confirmedData?.miner !== undefined && block?.miner !== undefined && internalOp.from === block.miner;
internalOp.from === txData.confirmedData.miner; const toMiner = block?.miner !== undefined && internalOp.to === block.miner;
const toMiner =
txData.confirmedData?.miner !== undefined &&
internalOp.to === txData.confirmedData.miner;
const { provider } = useContext(RuntimeContext);
const blockETHUSDPrice = useETHUSDOracle( const blockETHUSDPrice = useETHUSDOracle(
provider, provider,
txData.confirmedData?.blockNumber txData.confirmedData?.blockNumber

View File

@ -4,7 +4,7 @@ import DecoratedAddressLink from "./DecoratedAddressLink";
import { useSelectedTransaction } from "../useSelectedTransaction"; import { useSelectedTransaction } from "../useSelectedTransaction";
import { useBlockNumberContext } from "../useBlockTagContext"; import { useBlockNumberContext } from "../useBlockTagContext";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { useHasCode } from "../useErigonHooks"; import { useBlockDataFromTransaction, useHasCode } from "../useErigonHooks";
import { AddressContext, ChecksummedAddress } from "../types"; import { AddressContext, ChecksummedAddress } from "../types";
type TransactionAddressProps = { type TransactionAddressProps = {
@ -23,6 +23,8 @@ const TransactionAddress: React.FC<TransactionAddressProps> = ({
const creation = address === txData?.confirmedData?.createdContractAddress; const creation = address === txData?.confirmedData?.createdContractAddress;
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const block = useBlockDataFromTransaction(provider, txData);
const blockNumber = useBlockNumberContext(); const blockNumber = useBlockNumberContext();
const toHasCode = useHasCode( const toHasCode = useHasCode(
provider, provider,
@ -39,7 +41,7 @@ const TransactionAddress: React.FC<TransactionAddressProps> = ({
<DecoratedAddressLink <DecoratedAddressLink
address={address} address={address}
addressCtx={addressCtx} addressCtx={addressCtx}
miner={address === txData?.confirmedData?.miner} miner={address === block?.miner}
txFrom={address === txData?.from} txFrom={address === txData?.from}
txTo={address === txData?.to || creation} txTo={address === txData?.to || creation}
creation={creation} creation={creation}

View File

@ -11,6 +11,7 @@ export enum Direction {
} }
export enum Flags { export enum Flags {
// Means the transaction internal sends ETH to the miner, e.g. flashbots
MINER, MINER,
} }

View File

@ -1,23 +0,0 @@
import { JsonRpcProvider } from "@ethersproject/providers";
import { getAddress } from "@ethersproject/address";
import { InternalOperation } from "./types";
export const getInternalOperations = async (
provider: JsonRpcProvider,
txHash: string
) => {
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;
};

View File

@ -16,7 +16,7 @@ import TransactionItemFiatFee from "./TransactionItemFiatFee";
import { ProcessedTransaction } from "../types"; import { ProcessedTransaction } from "../types";
import { FeeDisplay } from "./useFeeToggler"; import { FeeDisplay } from "./useFeeToggler";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { useHasCode } from "../useErigonHooks"; import { useHasCode, useSendsToMiner } from "../useErigonHooks";
import { formatValue } from "../components/formatter"; import { formatValue } from "../components/formatter";
type TransactionItemProps = { type TransactionItemProps = {
@ -36,6 +36,7 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
tx.to ?? undefined, tx.to ?? undefined,
tx.blockNumber - 1 tx.blockNumber - 1
); );
const [sendsToMiner] = useSendsToMiner(provider, tx.hash, tx.miner);
let direction: Direction | undefined; let direction: Direction | undefined;
if (selectedAddress) { if (selectedAddress) {
@ -53,7 +54,7 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
} }
} }
const flash = tx.gasPrice.isZero() && tx.internalMinerInteraction; const flash = tx.gasPrice.isZero() && sendsToMiner;
return ( return (
<div <div
@ -91,7 +92,7 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
<span> <span>
<TransactionDirection <TransactionDirection
direction={direction} direction={direction}
flags={tx.internalMinerInteraction ? Flags.MINER : undefined} flags={sendsToMiner ? Flags.MINER : undefined}
/> />
</span> </span>
</span> </span>

View File

@ -1,4 +1,4 @@
import React, { useContext, useMemo, useState } from "react"; import React, { useContext, useState } from "react";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle"; import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
@ -43,7 +43,12 @@ import {
useTransactionDescription as useSourcifyTransactionDescription, useTransactionDescription as useSourcifyTransactionDescription,
} from "../sourcify/useSourcify"; } from "../sourcify/useSourcify";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { useInternalOperations, useTransactionError } from "../useErigonHooks"; import {
useBlockDataFromTransaction,
useSendsToMiner,
useTokenTransfers,
useTransactionError,
} from "../useErigonHooks";
import { useChainInfo } from "../useChainInfo"; import { useChainInfo } from "../useChainInfo";
import { useETHUSDOracle } from "../usePriceOracle"; import { useETHUSDOracle } from "../usePriceOracle";
@ -53,10 +58,10 @@ type DetailsProps = {
const Details: React.FC<DetailsProps> = ({ txData }) => { const Details: React.FC<DetailsProps> = ({ txData }) => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const block = useBlockDataFromTransaction(provider, txData);
const hasEIP1559 = const hasEIP1559 =
txData.confirmedData?.blockBaseFeePerGas !== undefined && block?.baseFeePerGas !== undefined && block?.baseFeePerGas !== null;
txData.confirmedData?.blockBaseFeePerGas !== null;
const fourBytes = const fourBytes =
txData.to !== null ? extract4Bytes(txData.data) ?? "0x" : "0x"; txData.to !== null ? extract4Bytes(txData.data) ?? "0x" : "0x";
@ -67,19 +72,13 @@ const Details: React.FC<DetailsProps> = ({ txData }) => {
txData.value txData.value
); );
const internalOps = useInternalOperations(provider, txData); const [sendsEthToMiner, internalOps] = useSendsToMiner(
const sendsEthToMiner = useMemo(() => { provider,
if (!txData || !internalOps) { txData.confirmedData ? txData.transactionHash : undefined,
return false; block?.miner
} );
for (const t of internalOps) { const tokenTransfers = useTokenTransfers(txData);
if (t.to === txData.confirmedData?.miner) {
return true;
}
}
return false;
}, [txData, internalOps]);
const metadata = useSourcifyMetadata(txData?.to, provider?.network.chainId); const metadata = useSourcifyMetadata(txData?.to, provider?.network.chainId);
@ -224,22 +223,24 @@ const Details: React.FC<DetailsProps> = ({ txData }) => {
confirmations={txData.confirmedData.confirmations} confirmations={txData.confirmedData.confirmations}
/> />
</div> </div>
{block && (
<div className="flex space-x-2 items-baseline pl-3"> <div className="flex space-x-2 items-baseline pl-3">
<RelativePosition <RelativePosition
pos={txData.confirmedData.transactionIndex} pos={txData.confirmedData.transactionIndex}
total={txData.confirmedData.blockTransactionCount - 1} total={block.transactionCount - 1}
/> />
<PercentagePosition <PercentagePosition
perc={ perc={
txData.confirmedData.transactionIndex / txData.confirmedData.transactionIndex /
(txData.confirmedData.blockTransactionCount - 1) (block.transactionCount - 1)
} }
/> />
</div> </div>
)}
</div> </div>
</InfoRow> </InfoRow>
<InfoRow title="Timestamp"> <InfoRow title="Timestamp">
<Timestamp value={txData.confirmedData.timestamp} /> {block && <Timestamp value={block.timestamp} />}
</InfoRow> </InfoRow>
</> </>
)} )}
@ -290,14 +291,10 @@ const Details: React.FC<DetailsProps> = ({ txData }) => {
<MethodName data={txData.data} /> <MethodName data={txData.data} />
</InfoRow> </InfoRow>
)} )}
{txData.tokenTransfers.length > 0 && ( {tokenTransfers && tokenTransfers.length > 0 && (
<InfoRow title={`Tokens Transferred (${txData.tokenTransfers.length})`}> <InfoRow title={`Tokens Transferred (${tokenTransfers.length})`}>
{txData.tokenTransfers.map((t, i) => ( {tokenTransfers.map((t, i) => (
<TokenTransferItem <TokenTransferItem key={i} t={t} />
key={i}
t={t}
tokenMeta={txData.tokenMetas[t.token]}
/>
))} ))}
</InfoRow> </InfoRow>
)} )}
@ -372,18 +369,10 @@ const Details: React.FC<DetailsProps> = ({ txData }) => {
</div> </div>
</InfoRow> </InfoRow>
)} )}
{txData.confirmedData && hasEIP1559 && ( {block && hasEIP1559 && (
<InfoRow title="Block Base Fee"> <InfoRow title="Block Base Fee">
<FormattedBalance <FormattedBalance value={block.baseFeePerGas!} decimals={9} /> Gwei (
value={txData.confirmedData.blockBaseFeePerGas!} <FormattedBalance value={block.baseFeePerGas!} decimals={0} /> wei)
decimals={9}
/>{" "}
Gwei (
<FormattedBalance
value={txData.confirmedData.blockBaseFeePerGas!}
decimals={0}
/>{" "}
wei)
</InfoRow> </InfoRow>
)} )}
{txData.confirmedData && ( {txData.confirmedData && (

View File

@ -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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn"; import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn";
import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins"; import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins";
import FormattedBalance from "../components/FormattedBalance"; import FormattedBalance from "../components/FormattedBalance";
import PercentageGauge from "../components/PercentageGauge"; import PercentageGauge from "../components/PercentageGauge";
import { TransactionData } from "../types"; import { RuntimeContext } from "../useRuntime";
import { useBlockDataFromTransaction } from "../useErigonHooks";
import { useChainInfo } from "../useChainInfo"; import { useChainInfo } from "../useChainInfo";
import { TransactionData } from "../types";
type RewardSplitProps = { type RewardSplitProps = {
txData: TransactionData; txData: TransactionData;
}; };
const RewardSplit: React.FC<RewardSplitProps> = ({ txData }) => { const RewardSplit: React.FC<RewardSplitProps> = ({ txData }) => {
const { provider } = useContext(RuntimeContext);
const block = useBlockDataFromTransaction(provider, txData);
const { const {
nativeCurrency: { symbol }, nativeCurrency: { symbol },
} = useChainInfo(); } = useChainInfo();
const paidFees = txData.gasPrice.mul(txData.confirmedData!.gasUsed); const paidFees = txData.gasPrice.mul(txData.confirmedData!.gasUsed);
const burntFees = txData.confirmedData!.blockBaseFeePerGas!.mul( const burntFees = block
txData.confirmedData!.gasUsed ? block.baseFeePerGas!.mul(txData.confirmedData!.gasUsed)
); : BigNumber.from(0);
const minerReward = paidFees.sub(burntFees); const minerReward = paidFees.sub(burntFees);
const burntPerc = const burntPerc =

View File

@ -18,7 +18,6 @@ export type ProcessedTransaction = {
from?: string; from?: string;
to: string | null; to: string | null;
createdContractAddress?: string; createdContractAddress?: string;
internalMinerInteraction?: boolean;
value: BigNumber; value: BigNumber;
fee: BigNumber; fee: BigNumber;
gasPrice: BigNumber; gasPrice: BigNumber;
@ -37,8 +36,6 @@ export type TransactionData = {
from: string; from: string;
to?: string; to?: string;
value: BigNumber; value: BigNumber;
tokenTransfers: TokenTransfer[];
tokenMetas: TokenMetas;
type: number; type: number;
maxFeePerGas?: BigNumber | undefined; maxFeePerGas?: BigNumber | undefined;
maxPriorityFeePerGas?: BigNumber | undefined; maxPriorityFeePerGas?: BigNumber | undefined;
@ -53,11 +50,7 @@ export type ConfirmedTransactionData = {
status: boolean; status: boolean;
blockNumber: number; blockNumber: number;
transactionIndex: number; transactionIndex: number;
blockBaseFeePerGas?: BigNumber | undefined | null;
blockTransactionCount: number;
confirmations: number; confirmations: number;
timestamp: number;
miner: string;
createdContractAddress?: string; createdContractAddress?: string;
fee: BigNumber; fee: BigNumber;
gasUsed: BigNumber; gasUsed: BigNumber;

View File

@ -53,6 +53,7 @@ const fourBytesFetcher =
const fourBytes = key.slice(2); const fourBytes = key.slice(2);
const signatureURL = fourBytesURL(assetsURLPrefix, fourBytes); const signatureURL = fourBytesURL(assetsURLPrefix, fourBytes);
try {
const res = await fetch(signatureURL); const res = await fetch(signatureURL);
if (!res.ok) { if (!res.ok) {
console.warn(`Signature does not exist in 4bytes DB: ${fourBytes}`); console.warn(`Signature does not exist in 4bytes DB: ${fourBytes}`);
@ -70,6 +71,11 @@ const fourBytesFetcher =
signature: sig, signature: sig,
}; };
return entry; return entry;
} catch (err) {
// Network error or something wrong with URL config;
// silence and don't try it again
return null;
}
}; };
/** /**

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useMemo } from "react";
import { import {
Block, Block,
BlockWithTransactions, BlockWithTransactions,
@ -10,17 +10,17 @@ import { Contract } from "@ethersproject/contracts";
import { defaultAbiCoder } from "@ethersproject/abi"; import { defaultAbiCoder } from "@ethersproject/abi";
import { BigNumber } from "@ethersproject/bignumber"; import { BigNumber } from "@ethersproject/bignumber";
import { arrayify, hexDataSlice, isHexString } from "@ethersproject/bytes"; import { arrayify, hexDataSlice, isHexString } from "@ethersproject/bytes";
import { AddressZero } from "@ethersproject/constants";
import useSWR from "swr"; import useSWR from "swr";
import useSWRImmutable from "swr/immutable"; import useSWRImmutable from "swr/immutable";
import { getInternalOperations } from "./nodeFunctions";
import { import {
TokenMetas,
TokenTransfer, TokenTransfer,
TransactionData, TransactionData,
InternalOperation, InternalOperation,
ProcessedTransaction, ProcessedTransaction,
OperationType, OperationType,
ChecksummedAddress, ChecksummedAddress,
TokenMeta,
} from "./types"; } from "./types";
import erc20 from "./erc20.json"; import erc20 from "./erc20.json";
@ -141,27 +141,6 @@ export const useBlockTransactions = (
.reverse(); .reverse();
setTxs(rawTxs); setTxs(rawTxs);
setTotalTxs(result.fullblock.transactionCount); 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(); readBlock();
}, [provider, blockNumber, pageNumber, pageSize]); }, [provider, blockNumber, pageNumber, pageSize]);
@ -169,23 +148,40 @@ export const useBlockTransactions = (
return [totalTxs, txs]; 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 = ( export const useBlockData = (
provider: JsonRpcProvider | undefined, provider: JsonRpcProvider | undefined,
blockNumberOrHash: string blockNumberOrHash: string | undefined
): ExtendedBlock | null | undefined => { ): ExtendedBlock | null | undefined => {
const [block, setBlock] = useState<ExtendedBlock | null | undefined>(); const { data, error } = useSWRImmutable(
useEffect(() => { provider !== undefined && blockNumberOrHash !== undefined
if (!provider) { ? [provider, blockNumberOrHash]
: null,
blockDataFetcher
);
if (error) {
return undefined; return undefined;
} }
return data;
};
const _readBlock = async () => { export const useBlockDataFromTransaction = (
const extBlock = await readBlock(provider, blockNumberOrHash); provider: JsonRpcProvider | undefined,
setBlock(extBlock); txData: TransactionData | null | undefined
}; ): ExtendedBlock | null | undefined => {
_readBlock(); const block = useBlockData(
}, [provider, blockNumberOrHash]); provider,
txData?.confirmedData
? txData.confirmedData.blockNumber.toString()
: undefined
);
return block; return block;
}; };
@ -211,66 +207,11 @@ export const useTxData = (
return; 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({ setTxData({
transactionHash: _response.hash, transactionHash: _response.hash,
from: _response.from, from: _response.from,
to: _response.to, to: _response.to,
value: _response.value, value: _response.value,
tokenTransfers,
tokenMetas,
type: _response.type ?? 0, type: _response.type ?? 0,
maxFeePerGas: _response.maxFeePerGas, maxFeePerGas: _response.maxFeePerGas,
maxPriorityFeePerGas: _response.maxPriorityFeePerGas, maxPriorityFeePerGas: _response.maxPriorityFeePerGas,
@ -285,11 +226,7 @@ export const useTxData = (
status: _receipt.status === 1, status: _receipt.status === 1,
blockNumber: _receipt.blockNumber, blockNumber: _receipt.blockNumber,
transactionIndex: _receipt.transactionIndex, transactionIndex: _receipt.transactionIndex,
blockBaseFeePerGas: _block!.baseFeePerGas,
blockTransactionCount: _block!.transactionCount,
confirmations: _receipt.confirmations, confirmations: _receipt.confirmations,
timestamp: _block!.timestamp,
miner: _block!.miner,
createdContractAddress: _receipt.contractAddress, createdContractAddress: _receipt.contractAddress,
fee: _response.gasPrice!.mul(_receipt.gasUsed), fee: _response.gasPrice!.mul(_receipt.gasUsed),
gasUsed: _receipt.gasUsed, gasUsed: _receipt.gasUsed,
@ -308,33 +245,75 @@ export const useTxData = (
return txData; 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 = ( export const useInternalOperations = (
provider: JsonRpcProvider | undefined, provider: JsonRpcProvider | undefined,
txData: TransactionData | undefined | null txHash: string | undefined
): InternalOperation[] | undefined => { ): InternalOperation[] | undefined => {
const [intTransfers, setIntTransfers] = useState<InternalOperation[]>(); const { data, error } = useSWRImmutable(
provider !== undefined && txHash !== undefined
useEffect(() => { ? ["ots_getInternalOperations", txHash]
const traceTransfers = async () => { : null,
if (!provider || !txData || !txData.confirmedData) { providerFetcher(provider)
return;
}
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]);
return intTransfers; const _transfers = useMemo(() => {
if (provider === undefined || error || data === undefined) {
return undefined;
}
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;
};
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 = { export type TraceEntry = {
@ -665,3 +644,61 @@ export const useHasCode = (
} }
return data as boolean | undefined; return data as boolean | undefined;
}; };
const ERC20_PROTOTYPE = new Contract(AddressZero, erc20);
const tokenMetadataFetcher =
(provider: JsonRpcProvider | undefined) =>
async (
_: "tokenmeta",
address: ChecksummedAddress
): Promise<TokenMeta | null> => {
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;
};

View File

@ -1,6 +1,7 @@
import { JsonRpcProvider, BlockTag } from "@ethersproject/providers"; import { JsonRpcProvider, BlockTag } from "@ethersproject/providers";
import { Contract } from "@ethersproject/contracts"; import { Contract } from "@ethersproject/contracts";
import { BigNumber } from "@ethersproject/bignumber"; import { BigNumber } from "@ethersproject/bignumber";
import { AddressZero } from "@ethersproject/constants";
import AggregatorV3Interface from "@chainlink/contracts/abi/v0.8/AggregatorV3Interface.json"; import AggregatorV3Interface from "@chainlink/contracts/abi/v0.8/AggregatorV3Interface.json";
import FeedRegistryInterface from "@chainlink/contracts/abi/v0.8/FeedRegistryInterface.json"; import FeedRegistryInterface from "@chainlink/contracts/abi/v0.8/FeedRegistryInterface.json";
import { Fetcher } from "swr"; import { Fetcher } from "swr";
@ -26,11 +27,20 @@ const feedRegistryFetcherKey = (
return [tokenAddress, blockTag]; return [tokenAddress, blockTag];
}; };
const FEED_REGISTRY_MAINNET_PROTOTYPE = new Contract(
FEED_REGISTRY_MAINNET,
FeedRegistryInterface
);
const feedRegistryFetcher = const feedRegistryFetcher =
( (
provider: JsonRpcProvider | undefined provider: JsonRpcProvider | undefined
): Fetcher<FeedRegistryFetcherData, FeedRegistryFetcherKey> => ): Fetcher<FeedRegistryFetcherData, FeedRegistryFetcherKey> =>
async (tokenAddress, blockTag) => { async (tokenAddress, blockTag) => {
if (provider === undefined) {
return [undefined, undefined];
}
// It work works on ethereum mainnet and kovan, see: // It work works on ethereum mainnet and kovan, see:
// https://docs.chain.link/docs/feed-registry/ // https://docs.chain.link/docs/feed-registry/
if (provider!.network.chainId !== 1) { if (provider!.network.chainId !== 1) {
@ -38,11 +48,7 @@ const feedRegistryFetcher =
} }
// Let SWR handle error // Let SWR handle error
const feedRegistry = new Contract( const feedRegistry = FEED_REGISTRY_MAINNET_PROTOTYPE.connect(provider);
FEED_REGISTRY_MAINNET,
FeedRegistryInterface,
provider
);
const priceData = await feedRegistry.latestRoundData(tokenAddress, USD, { const priceData = await feedRegistry.latestRoundData(tokenAddress, USD, {
blockTag, blockTag,
}); });
@ -76,23 +82,39 @@ const ethUSDFetcherKey = (blockTag: BlockTag | undefined) => {
return ["ethusd", blockTag]; return ["ethusd", blockTag];
}; };
const ETH_USD_FEED_PROTOTYPE = new Contract(AddressZero, AggregatorV3Interface);
const ethUSDFetcher = const ethUSDFetcher =
( (
provider: JsonRpcProvider | undefined provider: JsonRpcProvider | undefined
): Fetcher<BigNumber | undefined, ["ethusd", BlockTag | undefined]> => ): Fetcher<any | undefined, ["ethusd", BlockTag | undefined]> =>
async (_, blockTag) => { async (_, blockTag) => {
if (provider?.network.chainId !== 1) { if (provider?.network.chainId !== 1) {
return undefined; 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 }); const priceData = await c.latestRoundData({ blockTag });
return BigNumber.from(priceData.answer); return priceData;
}; };
export const useETHUSDOracle = ( export const useETHUSDOracle = (
provider: JsonRpcProvider | undefined, provider: JsonRpcProvider | undefined,
blockTag: BlockTag | undefined blockTag: BlockTag | undefined
): BigNumber | 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 fetcher = ethUSDFetcher(provider);
const { data, error } = useSWRImmutable(ethUSDFetcherKey(blockTag), fetcher); const { data, error } = useSWRImmutable(ethUSDFetcherKey(blockTag), fetcher);
if (error) { if (error) {
@ -100,3 +122,42 @@ export const useETHUSDOracle = (
} }
return data; 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<any | undefined, ["gasgwei", BlockTag | undefined]> =>
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;
};