Merge branch 'feature/swr-everything-3' into develop
This commit is contained in:
commit
0acac32fc9
|
@ -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];
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 && (
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in New Issue