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 { 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<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(() => {
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];

View File

@ -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<TokenTransferItemProps> = ({
t,
tokenMeta,
}) => {
const TokenTransferItem: React.FC<TokenTransferItemProps> = ({ t }) => {
const { provider } = useContext(RuntimeContext);
const blockNumber = useBlockNumberContext();
const [quote, decimals] = useTokenUSDOracle(provider, blockNumber, t.token);
const tokenMeta = useTokenMetadata(provider, t.token);
return (
<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 { 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 (
<SelectedTransactionContext.Provider value={txData}>
<BlockNumberContext.Provider value={txData?.confirmedData?.blockNumber}>
@ -46,8 +52,7 @@ const Transaction: React.FC = () => {
{txData.confirmedData?.blockNumber !== undefined && (
<NavTab href="logs">
Logs
{txData &&
` (${txData.confirmedData?.logs?.length ?? 0})`}
{` (${txData.confirmedData?.logs?.length ?? 0})`}
</NavTab>
)}
<NavTab href="trace">Trace</NavTab>

View File

@ -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<TokenMeta> {
async resolveAddress(
provider: BaseProvider,
address: string
): Promise<TokenMeta | undefined> {
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()) {

View File

@ -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<UniswapV1PairMeta> {
provider: BaseProvider,
address: string
): Promise<UniswapV1PairMeta | undefined> {
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

View File

@ -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<UniswapV2PairMeta> {
provider: BaseProvider,
address: string
): Promise<UniswapV2PairMeta | undefined> {
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

View File

@ -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<UniswapV3PairMeta> {
provider: BaseProvider,
address: string
): Promise<UniswapV3PairMeta | undefined> {
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

View File

@ -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<InternalSelfDestructProps> = ({
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 (
<>

View File

@ -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<InternalTransferProps> = ({
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

View File

@ -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<TransactionAddressProps> = ({
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<TransactionAddressProps> = ({
<DecoratedAddressLink
address={address}
addressCtx={addressCtx}
miner={address === txData?.confirmedData?.miner}
miner={address === block?.miner}
txFrom={address === txData?.from}
txTo={address === txData?.to || creation}
creation={creation}

View File

@ -11,6 +11,7 @@ export enum Direction {
}
export enum Flags {
// Means the transaction internal sends ETH to the miner, e.g. flashbots
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 { 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<TransactionItemProps> = ({
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<TransactionItemProps> = ({
}
}
const flash = tx.gasPrice.isZero() && tx.internalMinerInteraction;
const flash = tx.gasPrice.isZero() && sendsToMiner;
return (
<div
@ -91,7 +92,7 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
<span>
<TransactionDirection
direction={direction}
flags={tx.internalMinerInteraction ? Flags.MINER : undefined}
flags={sendsToMiner ? Flags.MINER : undefined}
/>
</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 { 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<DetailsProps> = ({ 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<DetailsProps> = ({ 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<DetailsProps> = ({ txData }) => {
confirmations={txData.confirmedData.confirmations}
/>
</div>
<div className="flex space-x-2 items-baseline pl-3">
<RelativePosition
pos={txData.confirmedData.transactionIndex}
total={txData.confirmedData.blockTransactionCount - 1}
/>
<PercentagePosition
perc={
txData.confirmedData.transactionIndex /
(txData.confirmedData.blockTransactionCount - 1)
}
/>
</div>
{block && (
<div className="flex space-x-2 items-baseline pl-3">
<RelativePosition
pos={txData.confirmedData.transactionIndex}
total={block.transactionCount - 1}
/>
<PercentagePosition
perc={
txData.confirmedData.transactionIndex /
(block.transactionCount - 1)
}
/>
</div>
)}
</div>
</InfoRow>
<InfoRow title="Timestamp">
<Timestamp value={txData.confirmedData.timestamp} />
{block && <Timestamp value={block.timestamp} />}
</InfoRow>
</>
)}
@ -290,14 +291,10 @@ const Details: React.FC<DetailsProps> = ({ txData }) => {
<MethodName data={txData.data} />
</InfoRow>
)}
{txData.tokenTransfers.length > 0 && (
<InfoRow title={`Tokens Transferred (${txData.tokenTransfers.length})`}>
{txData.tokenTransfers.map((t, i) => (
<TokenTransferItem
key={i}
t={t}
tokenMeta={txData.tokenMetas[t.token]}
/>
{tokenTransfers && tokenTransfers.length > 0 && (
<InfoRow title={`Tokens Transferred (${tokenTransfers.length})`}>
{tokenTransfers.map((t, i) => (
<TokenTransferItem key={i} t={t} />
))}
</InfoRow>
)}
@ -372,18 +369,10 @@ const Details: React.FC<DetailsProps> = ({ txData }) => {
</div>
</InfoRow>
)}
{txData.confirmedData && hasEIP1559 && (
{block && hasEIP1559 && (
<InfoRow title="Block Base Fee">
<FormattedBalance
value={txData.confirmedData.blockBaseFeePerGas!}
decimals={9}
/>{" "}
Gwei (
<FormattedBalance
value={txData.confirmedData.blockBaseFeePerGas!}
decimals={0}
/>{" "}
wei)
<FormattedBalance value={block.baseFeePerGas!} decimals={9} /> Gwei (
<FormattedBalance value={block.baseFeePerGas!} decimals={0} /> wei)
</InfoRow>
)}
{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 { 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<RewardSplitProps> = ({ 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 =

View File

@ -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;

View File

@ -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;
};
/**

View File

@ -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<ExtendedBlock | null | undefined>();
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<InternalOperation[]>();
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<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 { 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<FeedRegistryFetcherData, FeedRegistryFetcherKey> =>
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<BigNumber | undefined, ["ethusd", BlockTag | undefined]> =>
): Fetcher<any | undefined, ["ethusd", BlockTag | undefined]> =>
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<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;
};