import React, { useState, useEffect, useCallback } from "react"; import { Route, Switch, useParams } from "react-router-dom"; import { BigNumber, ethers } from "ethers"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheckCircle, faTimesCircle, faAngleRight, faCaretRight, } from "@fortawesome/free-solid-svg-icons"; import { provider } from "./ethersconfig"; import StandardFrame from "./StandardFrame"; import StandardSubtitle from "./StandardSubtitle"; import Tab from "./components/Tab"; import ContentFrame from "./ContentFrame"; import BlockLink from "./components/BlockLink"; import AddressLink from "./components/AddressLink"; import Copy from "./components/Copy"; import Timestamp from "./components/Timestamp"; import TokenLogo from "./components/TokenLogo"; import GasValue from "./components/GasValue"; import FormattedBalance from "./components/FormattedBalance"; import erc20 from "./erc20.json"; const USE_OTS = true; const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; type TransactionParams = { txhash: string; }; type TransactionData = { transactionHash: string; status: boolean; blockNumber: number; transactionIndex: number; confirmations: number; timestamp: number; from: string; to: string; value: BigNumber; tokenTransfers: TokenTransfer[]; tokenMetas: TokenMetas; fee: BigNumber; gasPrice: BigNumber; gasLimit: BigNumber; gasUsed: BigNumber; gasUsedPerc: number; nonce: number; data: string; logs: ethers.providers.Log[]; }; type From = { current: string; depth: number; }; type Transfer = { from: string; to: string; value: BigNumber; }; type TokenTransfer = { token: string; from: string; to: string; value: BigNumber; }; type TokenMeta = { name: string; symbol: string; decimals: number; }; type TokenMetas = { [tokenAddress: string]: TokenMeta; }; const Transaction: React.FC = () => { const params = useParams(); const { txhash } = params; const [txData, setTxData] = useState(); useEffect(() => { const readBlock = async () => { const [_response, _receipt] = await Promise.all([ provider.getTransaction(txhash), provider.getTransactionReceipt(txhash), ]); const _block = await provider.getBlock(_receipt.blockNumber); document.title = `Transaction ${_response.hash} | Otterscan`; // Extract token transfers const tokenTransfers: TokenTransfer[] = []; 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: ethers.utils.hexDataSlice( ethers.utils.arrayify(l.topics[1]), 12 ), to: ethers.utils.hexDataSlice(ethers.utils.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]) { continue; } const erc20Contract = new ethers.Contract(t.token, erc20, provider); const [name, symbol, decimals] = await Promise.all([ erc20Contract.name(), erc20Contract.symbol(), erc20Contract.decimals(), ]); tokenMetas[t.token] = { name, symbol, decimals, }; } setTxData({ transactionHash: _receipt.transactionHash, status: _receipt.status === 1, blockNumber: _receipt.blockNumber, transactionIndex: _receipt.transactionIndex, confirmations: _receipt.confirmations, timestamp: _block.timestamp, from: _receipt.from, to: _receipt.to, value: _response.value, tokenTransfers, tokenMetas, fee: _response.gasPrice!.mul(_receipt.gasUsed), gasPrice: _response.gasPrice!, gasLimit: _response.gasLimit, gasUsed: _receipt.gasUsed, gasUsedPerc: _receipt.gasUsed.toNumber() / _response.gasLimit.toNumber(), nonce: _response.nonce, data: _response.data, logs: _receipt.logs, }); }; readBlock(); }, [txhash]); const [transfers, setTransfers] = useState(); const traceTransfersUsingDebugTrace = async () => { const r = await provider.send("debug_traceTransaction", [ txData?.transactionHash, { disableStorage: true, disableMemory: true }, ]); const fromStack: From[] = [ { current: txData!.to, depth: 0, }, ]; const _transfers: Transfer[] = []; for (const l of r.structLogs) { if (l.op !== "CALL") { if (parseInt(l.depth) === fromStack[fromStack.length - 1].depth) { fromStack.pop(); } continue; } const { stack } = l; const addr = stack[stack.length - 2].slice(24); const value = BigNumber.from("0x" + stack[stack.length - 3]); if (!value.isZero()) { const t: Transfer = { from: ethers.utils.getAddress( fromStack[fromStack.length - 1].current ), to: ethers.utils.getAddress(addr), value, }; _transfers.push(t); } fromStack.push({ current: addr, depth: parseInt(l.depth), }); } setTransfers(_transfers); }; const traceTransfersUsingOtsTrace = useCallback(async () => { if (!txData) { return; } const r = await provider.send("ots_getTransactionTransfers", [ txData.transactionHash, ]); const _transfers: Transfer[] = []; for (const t of r) { _transfers.push({ from: t.from, to: t.to, value: t.value, }); } setTransfers(_transfers); }, [txData]); useEffect(() => { if (USE_OTS) { traceTransfersUsingOtsTrace(); } }, [traceTransfersUsingOtsTrace]); return ( Transaction Details {txData && ( <>
Overview Logs{txData && ` (${txData.logs.length})`}
{txData.transactionHash}
{txData.status ? ( Success ) : ( Fail )}
{txData.confirmations} Block Confirmations
{transfers ? (
{transfers.map((t, i) => (
{" "} TRANSFER {ethers.utils.formatEther(t.value)} Ether From To
))}
) : ( !USE_OTS && ( ) )}
{txData.tokenTransfers.length > 0 && (
{txData.tokenTransfers && txData.tokenTransfers.map((t, i) => (
From To For {txData.tokenMetas[t.token] ? ( <>
) : ( )}
))}
)} {ethers.utils.formatEther(txData.value)} Ether Ether Ether ( {" "} Gwei) N/A ( {(txData.gasUsedPerc * 100).toFixed(2)}%) {txData.nonce} {txData.transactionIndex}