Merge branch 'release/v2021.07.04-otterscan'

This commit is contained in:
Willian Mitsuda 2021-07-22 19:18:41 -03:00
commit aa51bf5561
31 changed files with 659 additions and 160 deletions

79
.github/workflows/docker-publish.yaml vendored Normal file
View File

@ -0,0 +1,79 @@
name: Publish Docker image
on:
push:
branches:
- "develop"
tags:
- "*"
env:
IMAGE_NAME: ${{ github.repository }}
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v2.3.4
with:
submodules: recursive
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v1.5.1
with:
driver: docker-container
driver-opts: |
image=moby/buildkit:master
- name: Docker Login
uses: docker/login-action@v1.10.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker Login
uses: docker/login-action@v1.10.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Docker Metadata action
id: meta
uses: docker/metadata-action@v3.4.1
with:
images: |
otterscan/otterscan
ghcr.io/${{ env.IMAGE_NAME }}
- name: Build and push Docker images
uses: docker/build-push-action@v2.6.1
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
# Temp fix
# https://github.com/docker/build-push-action/issues/252
# https://github.com/moby/buildkit/issues/1896
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v14.17.3

View File

@ -1,4 +1,4 @@
FROM node:12.22.3-alpine AS builder FROM node:14.17.3-alpine3.14 AS builder
RUN npm i -g npm@7.19.1 RUN npm i -g npm@7.19.1
WORKDIR /otterscan-build WORKDIR /otterscan-build
COPY ["package.json", "package-lock.json", "/otterscan-build/"] COPY ["package.json", "package-lock.json", "/otterscan-build/"]

View File

@ -160,3 +160,17 @@ This project intends to keep following their progress and mantaining compatibili
Erigon itself is alpha, so I consider this software is also in alpha state, however it is pretty usable. Erigon itself is alpha, so I consider this software is also in alpha state, however it is pretty usable.
Also there is room for many improvements that are not possible in the current centralized, closed source block explorer offerings and the author of this software would like to have. Also there is room for many improvements that are not possible in the current centralized, closed source block explorer offerings and the author of this software would like to have.
## Getting in touch
### Erigon Discord server
Otterscan has a community channel under the "ecosystem" section of [Erigon's Discord](https://github.com/ledgerwatch/erigon#erigon-discord-server).
### Twitter
Follow the creator on Twitter for updates ([@wmitsuda](https://twitter.com/wmitsuda)).
### Donation address
If you like this project, feel free to send donations to `otterscan.eth`

View File

@ -40,7 +40,7 @@ First, a brief explanation about the app:
These instructions are subjected to changes in future for the sake of simplification. These instructions are subjected to changes in future for the sake of simplification.
Make sure you have a working node 12/npm installation. Make sure you have a working node 14/npm 7 installation.
By default, it assumes your Erigon `rpcdaemon` processs is serving requests at `http://localhost:8545`. You can customize this URL by changing the `public/config.json` file. By default, it assumes your Erigon `rpcdaemon` processs is serving requests at `http://localhost:8545`. You can customize this URL by changing the `public/config.json` file.

14
package-lock.json generated
View File

@ -23,7 +23,7 @@
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
"@types/node": "^12.20.16", "@types/node": "^14.17.5",
"@types/react": "^17.0.14", "@types/react": "^17.0.14",
"@types/react-blockies": "^1.4.1", "@types/react-blockies": "^1.4.1",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
@ -3031,9 +3031,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "12.20.16", "version": "14.17.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.16.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.5.tgz",
"integrity": "sha512-6CLxw83vQf6DKqXxMPwl8qpF8I7THFZuIwLt4TnNsumxkp1VsRZWT8txQxncT/Rl2UojTsFzWgDG4FRMwafrlA==" "integrity": "sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA=="
}, },
"node_modules/@types/normalize-package-data": { "node_modules/@types/normalize-package-data": {
"version": "2.4.0", "version": "2.4.0",
@ -21040,9 +21040,9 @@
"version": "3.0.3" "version": "3.0.3"
}, },
"@types/node": { "@types/node": {
"version": "12.20.16", "version": "14.17.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.16.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.5.tgz",
"integrity": "sha512-6CLxw83vQf6DKqXxMPwl8qpF8I7THFZuIwLt4TnNsumxkp1VsRZWT8txQxncT/Rl2UojTsFzWgDG4FRMwafrlA==" "integrity": "sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA=="
}, },
"@types/normalize-package-data": { "@types/normalize-package-data": {
"version": "2.4.0" "version": "2.4.0"

View File

@ -19,7 +19,7 @@
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
"@types/node": "^12.20.16", "@types/node": "^14.17.5",
"@types/react": "^17.0.14", "@types/react": "^17.0.14",
"@types/react-blockies": "^1.4.1", "@types/react-blockies": "^1.4.1",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",

View File

@ -13,7 +13,7 @@ import NavButton from "./components/NavButton";
import Timestamp from "./components/Timestamp"; import Timestamp from "./components/Timestamp";
import GasValue from "./components/GasValue"; import GasValue from "./components/GasValue";
import BlockLink from "./components/BlockLink"; import BlockLink from "./components/BlockLink";
import AddressOrENSName from "./components/AddressOrENSName"; import DecoratedAddressLink from "./components/DecoratedAddressLink";
import TransactionValue from "./components/TransactionValue"; import TransactionValue from "./components/TransactionValue";
import HexValue from "./components/HexValue"; import HexValue from "./components/HexValue";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
@ -159,10 +159,7 @@ const Block: React.FC = () => {
in this block in this block
</InfoRow> </InfoRow>
<InfoRow title="Mined by"> <InfoRow title="Mined by">
<AddressOrENSName <DecoratedAddressLink address={block.miner} miner />
address={block.miner}
minerAddress={block.miner}
/>
</InfoRow> </InfoRow>
<InfoRow title="Block Reward"> <InfoRow title="Block Reward">
<TransactionValue value={block.blockReward.add(block.feeReward)} /> <TransactionValue value={block.blockReward.add(block.feeReward)} />

View File

@ -5,7 +5,11 @@ import queryString from "query-string";
import StandardFrame from "./StandardFrame"; import StandardFrame from "./StandardFrame";
import BlockTransactionHeader from "./BlockTransactionHeader"; import BlockTransactionHeader from "./BlockTransactionHeader";
import BlockTransactionResults from "./BlockTransactionResults"; import BlockTransactionResults from "./BlockTransactionResults";
import { ProcessedTransaction } from "./types"; import {
InternalOperation,
OperationType,
ProcessedTransaction,
} from "./types";
import { PAGE_SIZE } from "./params"; import { PAGE_SIZE } from "./params";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
@ -57,6 +61,7 @@ const BlockTransactions: React.FC = () => {
hash: t.hash, hash: t.hash,
from: t.from, from: t.from,
to: t.to, to: t.to,
createdContractAddress: _receipts[i].contractAddress,
value: t.value, value: t.value,
fee: provider.formatter fee: provider.formatter
.bigNumber(_receipts[i].gasUsed) .bigNumber(_receipts[i].gasUsed)
@ -71,14 +76,18 @@ const BlockTransactions: React.FC = () => {
const internalChecks = await Promise.all( const internalChecks = await Promise.all(
responses.map(async (res) => { responses.map(async (res) => {
const r = await provider.send("ots_getTransactionTransfers", [ const r: InternalOperation[] = await provider.send(
res.hash, "ots_getInternalOperations",
]); [res.hash]
for (const t of r) { );
for (const op of r) {
if (op.type !== OperationType.TRANSFER) {
continue;
}
if ( if (
res.miner && res.miner &&
(res.miner === ethers.utils.getAddress(t.from) || (res.miner === ethers.utils.getAddress(op.from) ||
res.miner === ethers.utils.getAddress(t.to)) res.miner === ethers.utils.getAddress(op.to))
) { ) {
return true; return true;
} }

View File

@ -2,22 +2,28 @@ import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCaretRight } from "@fortawesome/free-solid-svg-icons"; import { faCaretRight } from "@fortawesome/free-solid-svg-icons";
import AddressHighlighter from "./components/AddressHighlighter"; import AddressHighlighter from "./components/AddressHighlighter";
import AddressOrENSName from "./components/AddressOrENSName"; import ValueHighlighter from "./components/ValueHighlighter";
import AddressLink from "./components/AddressLink"; import DecoratedAddressLink from "./components/DecoratedAddressLink";
import TokenLogo from "./components/TokenLogo";
import FormattedBalance from "./components/FormattedBalance"; import FormattedBalance from "./components/FormattedBalance";
import { TokenMetas, TokenTransfer } from "./types"; import {
AddressContext,
TokenMetas,
TokenTransfer,
TransactionData,
} from "./types";
type TokenTransferItemProps = { type TokenTransferItemProps = {
t: TokenTransfer; t: TokenTransfer;
txData: TransactionData;
tokenMetas: TokenMetas; tokenMetas: TokenMetas;
}; };
const TokenTransferItem: React.FC<TokenTransferItemProps> = ({ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
t, t,
txData,
tokenMetas, tokenMetas,
}) => ( }) => (
<div className="flex items-baseline space-x-2 truncate"> <div className="flex items-baseline space-x-2 px-2 py-1 truncate hover:bg-gray-100">
<span className="text-gray-500"> <span className="text-gray-500">
<FontAwesomeIcon icon={faCaretRight} size="1x" /> <FontAwesomeIcon icon={faCaretRight} size="1x" />
</span> </span>
@ -25,40 +31,46 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">From</span> <span className="font-bold">From</span>
<AddressHighlighter address={t.from}> <AddressHighlighter address={t.from}>
<AddressOrENSName address={t.from} /> <DecoratedAddressLink
address={t.from}
addressCtx={AddressContext.FROM}
txFrom={t.from === txData.from}
txTo={t.from === txData.to}
/>
</AddressHighlighter> </AddressHighlighter>
</div> </div>
<div className="flex space-x-1"> <div className="flex space-x-1">
<span className="font-bold">To</span> <span className="font-bold">To</span>
<AddressHighlighter address={t.to}> <AddressHighlighter address={t.to}>
<AddressOrENSName address={t.to} /> <DecoratedAddressLink
address={t.to}
addressCtx={AddressContext.TO}
txFrom={t.to === txData.from}
txTo={t.to === txData.to}
/>
</AddressHighlighter> </AddressHighlighter>
</div> </div>
<div className="col-span-3 flex space-x-1"> <div className="col-span-3 flex space-x-1">
<span className="font-bold">For</span> <span className="font-bold">For</span>
<span> <span>
<FormattedBalance <ValueHighlighter value={t.value}>
value={t.value} <FormattedBalance
decimals={tokenMetas[t.token].decimals} value={t.value}
decimals={tokenMetas[t.token].decimals}
/>
</ValueHighlighter>
</span>
<AddressHighlighter address={t.token}>
<DecoratedAddressLink
address={t.token}
text={
tokenMetas[t.token]
? `${tokenMetas[t.token].name} (${tokenMetas[t.token].symbol})`
: ""
}
tokenMeta={tokenMetas[t.token]}
/> />
</span> </AddressHighlighter>
<span className="flex space-x-1 items-baseline truncate">
{tokenMetas[t.token] ? (
<>
<div className="self-center">
<TokenLogo address={t.token} name={tokenMetas[t.token].name} />
</div>
<AddressLink
address={t.token}
text={`${tokenMetas[t.token].name} (${
tokenMetas[t.token].symbol
})`}
/>
</>
) : (
<AddressOrENSName address={t.token} />
)}
</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,10 +1,4 @@
import React, { import React, { useState, useEffect, useMemo, useContext } from "react";
useState,
useEffect,
useCallback,
useMemo,
useContext,
} from "react";
import { Route, Switch, useParams } from "react-router-dom"; import { Route, Switch, useParams } from "react-router-dom";
import { BigNumber, ethers } from "ethers"; import { BigNumber, ethers } from "ethers";
import StandardFrame from "./StandardFrame"; import StandardFrame from "./StandardFrame";
@ -13,9 +7,10 @@ import Tab from "./components/Tab";
import Details from "./transaction/Details"; import Details from "./transaction/Details";
import Logs from "./transaction/Logs"; import Logs from "./transaction/Logs";
import erc20 from "./erc20.json"; import erc20 from "./erc20.json";
import { TokenMetas, TokenTransfer, TransactionData, Transfer } from "./types"; import { TokenMetas, TokenTransfer, TransactionData } from "./types";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { SelectionContext, useSelection } from "./useSelection"; import { SelectionContext, useSelection } from "./useSelection";
import { useInternalOperations } from "./useErigonHooks";
const TRANSFER_TOPIC = const TRANSFER_TOPIC =
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
@ -93,6 +88,7 @@ const Transaction: React.FC = () => {
miner: _block.miner, miner: _block.miner,
from: _receipt.from, from: _receipt.from,
to: _receipt.to, to: _receipt.to,
createdContractAddress: _receipt.contractAddress,
value: _response.value, value: _response.value,
tokenTransfers, tokenTransfers,
tokenMetas, tokenMetas,
@ -110,42 +106,19 @@ const Transaction: React.FC = () => {
readBlock(); readBlock();
}, [provider, txhash]); }, [provider, txhash]);
const [transfers, setTransfers] = useState<Transfer[]>(); const internalOps = useInternalOperations(provider, txData);
const sendsEthToMiner = useMemo(() => { const sendsEthToMiner = useMemo(() => {
if (!txData || !transfers) { if (!txData || !internalOps) {
return false; return false;
} }
for (const t of transfers) { for (const t of internalOps) {
if (t.to === txData.miner) { if (t.to === txData.miner) {
return true; return true;
} }
} }
return false; return false;
}, [txData, transfers]); }, [txData, internalOps]);
const traceTransfers = useCallback(async () => {
if (!provider || !txData) {
return;
}
const r = await provider.send("ots_getTransactionTransfers", [
txData.transactionHash,
]);
const _transfers: Transfer[] = [];
for (const t of r) {
_transfers.push({
from: ethers.utils.getAddress(t.from),
to: ethers.utils.getAddress(t.to),
value: t.value,
});
}
setTransfers(_transfers);
}, [provider, txData]);
useEffect(() => {
traceTransfers();
}, [traceTransfers]);
const selectionCtx = useSelection(); const selectionCtx = useSelection();
@ -164,7 +137,7 @@ const Transaction: React.FC = () => {
<Route path="/tx/:txhash/" exact> <Route path="/tx/:txhash/" exact>
<Details <Details
txData={txData} txData={txData}
transfers={transfers} internalOps={internalOps}
sendsEthToMiner={sendsEthToMiner} sendsEthToMiner={sendsEthToMiner}
/> />
</Route> </Route>

View File

@ -6,7 +6,7 @@ type AddressProps = {
const Address: React.FC<AddressProps> = ({ address }) => ( const Address: React.FC<AddressProps> = ({ address }) => (
<span className="font-address text-gray-400 truncate" title={address}> <span className="font-address text-gray-400 truncate" title={address}>
<p className="truncate">{address}</p> <span className="truncate">{address}</span>
</span> </span>
); );

View File

@ -4,16 +4,23 @@ import { NavLink } from "react-router-dom";
type AddressLinkProps = { type AddressLinkProps = {
address: string; address: string;
text?: string; text?: string;
dontOverrideColors?: boolean;
}; };
const AddressLink: React.FC<AddressLinkProps> = ({ address, text }) => ( const AddressLink: React.FC<AddressLinkProps> = ({
address,
text,
dontOverrideColors,
}) => (
<NavLink <NavLink
className="text-link-blue hover:text-link-blue-hover font-address truncate" className={`${
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
} font-address truncate`}
to={`/address/${address}`} to={`/address/${address}`}
> >
<p className="truncate" title={text ?? address}> <span className="truncate" title={text ?? address}>
{text ?? address} {text ?? address}
</p> </span>
</NavLink> </NavLink>
); );

View File

@ -1,6 +1,4 @@
import React from "react"; import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCoins } from "@fortawesome/free-solid-svg-icons";
import Address from "./Address"; import Address from "./Address";
import AddressLink from "./AddressLink"; import AddressLink from "./AddressLink";
import ENSName from "./ENSName"; import ENSName from "./ENSName";
@ -10,21 +8,18 @@ type AddressOrENSNameProps = {
address: string; address: string;
ensName?: string; ensName?: string;
selectedAddress?: string; selectedAddress?: string;
minerAddress?: string; text?: string;
dontOverrideColors?: boolean;
}; };
const AddressOrENSName: React.FC<AddressOrENSNameProps> = ({ const AddressOrENSName: React.FC<AddressOrENSNameProps> = ({
address, address,
ensName, ensName,
selectedAddress, selectedAddress,
minerAddress, text,
dontOverrideColors,
}) => ( }) => (
<div className="flex items-baseline space-x-1 truncate"> <>
{minerAddress !== undefined && minerAddress === address && (
<span className="text-yellow-400" title="Miner address">
<FontAwesomeIcon icon={faCoins} size="1x" />
</span>
)}
{address === selectedAddress ? ( {address === selectedAddress ? (
<> <>
{ensName ? ( {ensName ? (
@ -36,13 +31,21 @@ const AddressOrENSName: React.FC<AddressOrENSNameProps> = ({
) : ( ) : (
<> <>
{ensName ? ( {ensName ? (
<ENSNameLink name={ensName} address={address} /> <ENSNameLink
name={ensName}
address={address}
dontOverrideColors={dontOverrideColors}
/>
) : ( ) : (
<AddressLink address={address} /> <AddressLink
address={address}
text={text}
dontOverrideColors={dontOverrideColors}
/>
)} )}
</> </>
)} )}
</div> </>
); );
export default React.memo(AddressOrENSName); export default React.memo(AddressOrENSName);

View File

@ -0,0 +1,93 @@
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faStar,
faBomb,
faMoneyBillAlt,
faBurn,
faCoins,
} from "@fortawesome/free-solid-svg-icons";
import TokenLogo from "./TokenLogo";
import AddressOrENSName from "./AddressOrENSName";
import { AddressContext, TokenMeta, ZERO_ADDRESS } from "../types";
type DecoratedAddressLinkProps = {
address: string;
ensName?: string;
selectedAddress?: string;
text?: string;
addressCtx?: AddressContext;
creation?: boolean;
miner?: boolean;
selfDestruct?: boolean;
txFrom?: boolean;
txTo?: boolean;
tokenMeta?: TokenMeta;
};
const DecoratedAddresssLink: React.FC<DecoratedAddressLinkProps> = ({
address,
ensName,
selectedAddress,
text,
addressCtx,
creation,
miner,
selfDestruct,
txFrom,
txTo,
tokenMeta,
}) => {
const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS;
const burn = addressCtx === AddressContext.TO && address === ZERO_ADDRESS;
return (
<div
className={`flex items-baseline space-x-1 ${txFrom ? "bg-red-50" : ""} ${
txTo ? "bg-green-50" : ""
} ${mint ? "italic text-green-500 hover:text-green-700" : ""} ${
burn ? "line-through text-orange-500 hover:text-orange-700" : ""
} ${selfDestruct ? "line-through opacity-70 hover:opacity-100" : ""}`}
>
{creation && (
<span className="text-yellow-300" title="Contract creation">
<FontAwesomeIcon icon={faStar} size="1x" />
</span>
)}
{selfDestruct && (
<span className="text-red-800" title="Self destruct">
<FontAwesomeIcon icon={faBomb} size="1x" />
</span>
)}
{mint && (
<span className="text-green-500" title="Mint address">
<FontAwesomeIcon icon={faMoneyBillAlt} size="1x" />
</span>
)}
{burn && (
<span className="text-orange-500" title="Burn address">
<FontAwesomeIcon icon={faBurn} size="1x" />
</span>
)}
{miner && (
<span className="text-yellow-400" title="Miner address">
<FontAwesomeIcon icon={faCoins} size="1x" />
</span>
)}
{tokenMeta && (
<div className="self-center">
<TokenLogo address={address} name={tokenMeta.name} />
</div>
)}
<AddressOrENSName
address={address}
ensName={ensName}
selectedAddress={selectedAddress}
text={text}
dontOverrideColors={mint || burn}
/>
</div>
);
};
export default React.memo(DecoratedAddresssLink);

View File

@ -18,7 +18,7 @@ const ENSName: React.FC<ENSNameProps> = ({ name, address }) => (
width={12} width={12}
height={12} height={12}
/> />
<p className="truncate">{name}</p> <span className="truncate">{name}</span>
</div> </div>
); );

View File

@ -5,11 +5,18 @@ import ENSLogo from "./ensLogo.svg";
type ENSNameLinkProps = { type ENSNameLinkProps = {
name: string; name: string;
address: string; address: string;
dontOverrideColors?: boolean;
}; };
const ENSNameLink: React.FC<ENSNameLinkProps> = ({ name, address }) => ( const ENSNameLink: React.FC<ENSNameLinkProps> = ({
name,
address,
dontOverrideColors,
}) => (
<NavLink <NavLink
className="flex items-baseline space-x-1 font-sans text-link-blue hover:text-link-blue-hover truncate" className={`flex items-baseline space-x-1 font-sans ${
dontOverrideColors ? "" : "text-link-blue hover:text-link-blue-hover"
} truncate`}
to={`/address/${name}`} to={`/address/${name}`}
title={`${name}: ${address}`} title={`${name}: ${address}`}
> >
@ -20,7 +27,7 @@ const ENSNameLink: React.FC<ENSNameLinkProps> = ({ name, address }) => (
width={12} width={12}
height={12} height={12}
/> />
<p className="truncate">{name}</p> <span className="truncate">{name}</span>
</NavLink> </NavLink>
); );

View File

@ -0,0 +1,45 @@
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleRight } from "@fortawesome/free-solid-svg-icons";
import AddressHighlighter from "./AddressHighlighter";
import DecoratedAddressLink from "./DecoratedAddressLink";
import { TransactionData, InternalOperation } from "../types";
type InternalCreateProps = {
txData: TransactionData;
internalOp: InternalOperation;
};
const InternalCreate: React.FC<InternalCreateProps> = ({
txData,
internalOp,
}) => {
return (
<>
<div className="flex items-baseline space-x-1 text-xs">
<span className="text-gray-500">
<FontAwesomeIcon icon={faAngleRight} size="1x" /> CREATE
</span>
<span>Contract</span>
<div className="flex items-baseline">
<AddressHighlighter address={internalOp.to}>
<DecoratedAddressLink address={internalOp.to} creation />
</AddressHighlighter>
</div>
<span className="flex items-baseline text-gray-400">
(Creator:{" "}
<AddressHighlighter address={internalOp.from}>
<DecoratedAddressLink
address={internalOp.from}
txFrom={internalOp.from === txData.from}
txTo={internalOp.from === txData.to}
/>
</AddressHighlighter>
)
</span>
</div>
</>
);
};
export default React.memo(InternalCreate);

View File

@ -0,0 +1,70 @@
import React, { useContext } from "react";
import { ethers } from "ethers";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleRight } from "@fortawesome/free-solid-svg-icons";
import AddressHighlighter from "./AddressHighlighter";
import DecoratedAddressLink from "./DecoratedAddressLink";
import { RuntimeContext } from "../useRuntime";
import { TransactionData, InternalOperation } from "../types";
const CHI_ADDRESS = "0x0000000000004946c0e9F43F4Dee607b0eF1fA1c";
const GST2_ADDRESS = "0x0000000000b3F879cb30FE243b4Dfee438691c04";
type InternalSelfDestructProps = {
txData: TransactionData;
internalOp: InternalOperation;
};
const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({
txData,
internalOp,
}) => {
const { provider } = useContext(RuntimeContext);
const network = provider?.network;
const toMiner = txData.miner !== undefined && internalOp.to === txData.miner;
return (
<>
<div className="flex items-baseline space-x-1 text-xs">
<span className="text-gray-500">
<FontAwesomeIcon icon={faAngleRight} size="1x" /> SELF DESTRUCT
</span>
<span>Contract</span>
<div className="flex items-baseline">
<AddressHighlighter address={internalOp.from}>
<DecoratedAddressLink address={internalOp.from} selfDestruct />
</AddressHighlighter>
</div>
{network?.chainId === 1 && internalOp.to === CHI_ADDRESS && (
<span className="text-gray-400">(Chi Gastoken)</span>
)}
{network?.chainId === 1 && internalOp.to === GST2_ADDRESS && (
<span className="text-gray-400">(GST2 Gastoken)</span>
)}
</div>
{!internalOp.value.isZero() && (
<div className="ml-5 flex items-baseline space-x-1 text-xs">
<span className="text-gray-500">
<FontAwesomeIcon icon={faAngleRight} size="1x" /> TRANSFER
</span>
<span>{ethers.utils.formatEther(internalOp.value)} Ether</span>
<div className="flex items-baseline">
<span className="text-gray-500">To</span>
<AddressHighlighter address={internalOp.to}>
<div
className={`flex items-baseline space-x-1 ${
toMiner ? "rounded px-2 py-1 bg-yellow-100" : ""
}`}
>
<DecoratedAddressLink address={internalOp.to} miner={toMiner} />
</div>
</AddressHighlighter>
</div>
</div>
)}
</>
);
};
export default React.memo(InternalSelfDestruct);

View File

@ -0,0 +1,28 @@
import React from "react";
import InternalTransfer from "./InternalTransfer";
import InternalSelfDestruct from "./InternalSelfDestruct";
import InternalCreate from "./InternalCreate";
import { TransactionData, InternalOperation, OperationType } from "../types";
type InternalTransactionOperationProps = {
txData: TransactionData;
internalOp: InternalOperation;
};
const InternalTransactionOperation: React.FC<InternalTransactionOperationProps> =
({ txData, internalOp }) => (
<>
{internalOp.type === OperationType.TRANSFER && (
<InternalTransfer txData={txData} internalOp={internalOp} />
)}
{internalOp.type === OperationType.SELF_DESTRUCT && (
<InternalSelfDestruct txData={txData} internalOp={internalOp} />
)}
{(internalOp.type === OperationType.CREATE ||
internalOp.type === OperationType.CREATE2) && (
<InternalCreate txData={txData} internalOp={internalOp} />
)}
</>
);
export default React.memo(InternalTransactionOperation);

View File

@ -1,61 +1,61 @@
import React from "react"; import React from "react";
import { ethers } from "ethers"; import { ethers } from "ethers";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleRight, faCoins } from "@fortawesome/free-solid-svg-icons"; import { faAngleRight } from "@fortawesome/free-solid-svg-icons";
import AddressHighlighter from "./AddressHighlighter"; import AddressHighlighter from "./AddressHighlighter";
import AddressLink from "./AddressLink"; import DecoratedAddressLink from "./DecoratedAddressLink";
import { TransactionData, Transfer } from "../types"; import { TransactionData, InternalOperation } from "../types";
type InternalTransferProps = { type InternalTransferProps = {
txData: TransactionData; txData: TransactionData;
transfer: Transfer; internalOp: InternalOperation;
}; };
const InternalTransfer: React.FC<InternalTransferProps> = ({ const InternalTransfer: React.FC<InternalTransferProps> = ({
txData, txData,
transfer, internalOp,
}) => { }) => {
const fromMiner = const fromMiner =
txData.miner !== undefined && transfer.from === txData.miner; txData.miner !== undefined && internalOp.from === txData.miner;
const toMiner = txData.miner !== undefined && transfer.to === txData.miner; const toMiner = txData.miner !== undefined && internalOp.to === txData.miner;
return ( return (
<div className="flex items-baseline space-x-1 text-xs"> <div className="flex items-baseline space-x-1 text-xs">
<span className="text-gray-500"> <span className="text-gray-500">
<FontAwesomeIcon icon={faAngleRight} size="1x" /> TRANSFER <FontAwesomeIcon icon={faAngleRight} size="1x" /> TRANSFER
</span> </span>
<span>{ethers.utils.formatEther(transfer.value)} Ether</span> <span>{ethers.utils.formatEther(internalOp.value)} Ether</span>
<div className="flex items-baseline"> <div className="flex items-baseline">
<span className="text-gray-500">From</span> <span className="text-gray-500">From</span>
<AddressHighlighter address={transfer.from}> <AddressHighlighter address={internalOp.from}>
<div <div
className={`flex items-baseline space-x-1 ${ className={`flex items-baseline space-x-1 ${
fromMiner ? "rounded px-2 py-1 bg-yellow-100" : "" fromMiner ? "rounded px-2 py-1 bg-yellow-100" : ""
}`} }`}
> >
{fromMiner && ( <DecoratedAddressLink
<span className="text-yellow-400" title="Miner address"> address={internalOp.from}
<FontAwesomeIcon icon={faCoins} size="1x" /> miner={fromMiner}
</span> txFrom={internalOp.from === txData.from}
)} txTo={internalOp.from === txData.to}
<AddressLink address={transfer.from} /> />
</div> </div>
</AddressHighlighter> </AddressHighlighter>
</div> </div>
<div className="flex items-baseline"> <div className="flex items-baseline">
<span className="text-gray-500">To</span> <span className="text-gray-500">To</span>
<AddressHighlighter address={transfer.to}> <AddressHighlighter address={internalOp.to}>
<div <div
className={`flex items-baseline space-x-1 ${ className={`flex items-baseline space-x-1 ${
toMiner ? "rounded px-2 py-1 bg-yellow-100" : "" toMiner ? "rounded px-2 py-1 bg-yellow-100" : ""
}`} }`}
> >
{toMiner && ( <DecoratedAddressLink
<span className="text-yellow-400" title="Miner address"> address={internalOp.to}
<FontAwesomeIcon icon={faCoins} size="1x" /> miner={toMiner}
</span> txFrom={internalOp.to === txData.from}
)} txTo={internalOp.to === txData.to}
<AddressLink address={transfer.to} /> />
</div> </div>
</AddressHighlighter> </AddressHighlighter>
</div> </div>

View File

@ -0,0 +1,38 @@
import React from "react";
import { BigNumber } from "ethers";
import { useSelectionContext } from "../useSelection";
type ValueHighlighterProps = React.PropsWithChildren<{
value: BigNumber;
}>;
const ValueHighlighter: React.FC<ValueHighlighterProps> = ({
value,
children,
}) => {
const [selection, setSelection] = useSelectionContext();
const select = () => {
setSelection({ type: "value", content: value.toString() });
};
const deselect = () => {
setSelection(null);
};
return (
<div
className={`border border-dashed rounded hover:bg-transparent hover:border-transparent px-1 truncate ${
selection !== null &&
selection.type === "value" &&
selection.content === value.toString()
? "border-orange-400 bg-yellow-100"
: "border-transparent"
}`}
onMouseEnter={select}
onMouseLeave={deselect}
>
{children}
</div>
);
};
export default React.memo(ValueHighlighter);

22
src/nodeFunctions.ts Normal file
View File

@ -0,0 +1,22 @@
import { ethers } from "ethers";
import { TransactionData, InternalOperation } from "./types";
export const getInternalOperations = async (
provider: ethers.providers.JsonRpcProvider,
txData: TransactionData
) => {
const rawTransfers = await provider.send("ots_getInternalOperations", [
txData.transactionHash,
]);
const _transfers: InternalOperation[] = [];
for (const t of rawTransfers) {
_transfers.push({
type: t.type,
from: ethers.utils.getAddress(t.from),
to: ethers.utils.getAddress(t.to),
value: t.value,
});
}
return _transfers;
};

View File

@ -1,3 +1,3 @@
export const MIN_API_LEVEL = 1; export const MIN_API_LEVEL = 2;
export const PAGE_SIZE = 25; export const PAGE_SIZE = 25;

View File

@ -4,7 +4,7 @@ import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
import MethodName from "../components/MethodName"; import MethodName from "../components/MethodName";
import BlockLink from "../components/BlockLink"; import BlockLink from "../components/BlockLink";
import TransactionLink from "../components/TransactionLink"; import TransactionLink from "../components/TransactionLink";
import AddressOrENSName from "../components/AddressOrENSName"; import DecoratedAddressLink from "../components/DecoratedAddressLink";
import TimestampAge from "../components/TimestampAge"; import TimestampAge from "../components/TimestampAge";
import AddressHighlighter from "../components/AddressHighlighter"; import AddressHighlighter from "../components/AddressHighlighter";
import TransactionDirection, { import TransactionDirection, {
@ -35,7 +35,10 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
direction = Direction.SELF; direction = Direction.SELF;
} else if (tx.from === selectedAddress) { } else if (tx.from === selectedAddress) {
direction = Direction.OUT; direction = Direction.OUT;
} else if (tx.to === selectedAddress) { } else if (
tx.to === selectedAddress ||
tx.createdContractAddress === selectedAddress
) {
direction = Direction.IN; direction = Direction.IN;
} else { } else {
direction = Direction.INTERNAL; direction = Direction.INTERNAL;
@ -44,6 +47,10 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
const ensFrom = ensCache && tx.from && ensCache[tx.from]; const ensFrom = ensCache && tx.from && ensCache[tx.from];
const ensTo = ensCache && tx.to && ensCache[tx.to]; const ensTo = ensCache && tx.to && ensCache[tx.to];
const ensCreated =
ensCache &&
tx.createdContractAddress &&
ensCache[tx.createdContractAddress];
const flash = tx.gasPrice.isZero() && tx.internalMinerInteraction; const flash = tx.gasPrice.isZero() && tx.internalMinerInteraction;
return ( return (
@ -71,11 +78,11 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
<span className="truncate"> <span className="truncate">
{tx.from && ( {tx.from && (
<AddressHighlighter address={tx.from}> <AddressHighlighter address={tx.from}>
<AddressOrENSName <DecoratedAddressLink
address={tx.from} address={tx.from}
ensName={ensFrom} ensName={ensFrom}
selectedAddress={selectedAddress} selectedAddress={selectedAddress}
minerAddress={tx.miner} miner={tx.miner === tx.from}
/> />
</AddressHighlighter> </AddressHighlighter>
)} )}
@ -89,13 +96,22 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
</span> </span>
<span className="col-span-2 flex items-baseline" title={tx.to}> <span className="col-span-2 flex items-baseline" title={tx.to}>
<span className="truncate"> <span className="truncate">
{tx.to && ( {tx.to ? (
<AddressHighlighter address={tx.to}> <AddressHighlighter address={tx.to}>
<AddressOrENSName <DecoratedAddressLink
address={tx.to} address={tx.to}
ensName={ensTo} ensName={ensTo}
selectedAddress={selectedAddress} selectedAddress={selectedAddress}
minerAddress={tx.miner} miner={tx.miner === tx.to}
/>
</AddressHighlighter>
) : (
<AddressHighlighter address={tx.createdContractAddress!}>
<DecoratedAddressLink
address={tx.createdContractAddress!}
ensName={ensCreated}
selectedAddress={selectedAddress}
creation
/> />
</AddressHighlighter> </AddressHighlighter>
)} )}

View File

@ -45,6 +45,7 @@ export class SearchController {
hash: t.hash, hash: t.hash,
from: t.from, from: t.from,
to: t.to, to: t.to,
createdContractAddress: _receipt.contractAddress,
value: t.value, value: t.value,
fee: _receipt.gasUsed.mul(t.gasPrice!), fee: _receipt.gasUsed.mul(t.gasPrice!),
gasPrice: t.gasPrice!, gasPrice: t.gasPrice!,

View File

@ -9,25 +9,25 @@ import ContentFrame from "../ContentFrame";
import InfoRow from "../components/InfoRow"; import InfoRow from "../components/InfoRow";
import BlockLink from "../components/BlockLink"; import BlockLink from "../components/BlockLink";
import AddressHighlighter from "../components/AddressHighlighter"; import AddressHighlighter from "../components/AddressHighlighter";
import AddressOrENSName from "../components/AddressOrENSName"; import DecoratedAddressLink from "../components/DecoratedAddressLink";
import Copy from "../components/Copy"; import Copy from "../components/Copy";
import Timestamp from "../components/Timestamp"; import Timestamp from "../components/Timestamp";
import InternalTransfer from "../components/InternalTransfer"; import InternalTransactionOperation from "../components/InternalTransactionOperation";
import MethodName from "../components/MethodName"; import MethodName from "../components/MethodName";
import GasValue from "../components/GasValue"; import GasValue from "../components/GasValue";
import FormattedBalance from "../components/FormattedBalance"; import FormattedBalance from "../components/FormattedBalance";
import TokenTransferItem from "../TokenTransferItem"; import TokenTransferItem from "../TokenTransferItem";
import { TransactionData, Transfer } from "../types"; import { TransactionData, InternalOperation } from "../types";
type DetailsProps = { type DetailsProps = {
txData: TransactionData; txData: TransactionData;
transfers?: Transfer[]; internalOps?: InternalOperation[];
sendsEthToMiner: boolean; sendsEthToMiner: boolean;
}; };
const Details: React.FC<DetailsProps> = ({ const Details: React.FC<DetailsProps> = ({
txData, txData,
transfers, internalOps,
sendsEthToMiner, sendsEthToMiner,
}) => ( }) => (
<ContentFrame tabs> <ContentFrame tabs>
@ -64,22 +64,47 @@ const Details: React.FC<DetailsProps> = ({
<InfoRow title="From"> <InfoRow title="From">
<div className="flex items-baseline space-x-2 -ml-1"> <div className="flex items-baseline space-x-2 -ml-1">
<AddressHighlighter address={txData.from}> <AddressHighlighter address={txData.from}>
<AddressOrENSName address={txData.from} minerAddress={txData.miner} /> <DecoratedAddressLink
address={txData.from}
miner={txData.from === txData.miner}
txFrom
/>
</AddressHighlighter> </AddressHighlighter>
<Copy value={txData.from} /> <Copy value={txData.from} />
</div> </div>
</InfoRow> </InfoRow>
<InfoRow title="Interacted With (To)"> <InfoRow title={txData.to ? "Interacted With (To)" : "Contract Created"}>
<div className="flex items-baseline space-x-2 -ml-1"> {txData.to ? (
<AddressHighlighter address={txData.to}> <div className="flex items-baseline space-x-2 -ml-1">
<AddressOrENSName address={txData.to} minerAddress={txData.miner} /> <AddressHighlighter address={txData.to}>
</AddressHighlighter> <DecoratedAddressLink
<Copy value={txData.to} /> address={txData.to}
</div> miner={txData.to === txData.miner}
{transfers && ( txTo
/>
</AddressHighlighter>
<Copy value={txData.to} />
</div>
) : (
<div className="flex items-baseline space-x-2 -ml-1">
<AddressHighlighter address={txData.createdContractAddress!}>
<DecoratedAddressLink
address={txData.createdContractAddress!}
creation
txTo
/>
</AddressHighlighter>
<Copy value={txData.createdContractAddress!} />
</div>
)}
{internalOps && (
<div className="mt-2 space-y-1"> <div className="mt-2 space-y-1">
{transfers.map((t, i) => ( {internalOps.map((op, i) => (
<InternalTransfer key={i} txData={txData} transfer={t} /> <InternalTransactionOperation
key={i}
txData={txData}
internalOp={op}
/>
))} ))}
</div> </div>
)} )}
@ -89,9 +114,14 @@ const Details: React.FC<DetailsProps> = ({
</InfoRow> </InfoRow>
{txData.tokenTransfers.length > 0 && ( {txData.tokenTransfers.length > 0 && (
<InfoRow title={`Tokens Transferred (${txData.tokenTransfers.length})`}> <InfoRow title={`Tokens Transferred (${txData.tokenTransfers.length})`}>
<div className="space-y-2"> <div>
{txData.tokenTransfers.map((t, i) => ( {txData.tokenTransfers.map((t, i) => (
<TokenTransferItem key={i} t={t} tokenMetas={txData.tokenMetas} /> <TokenTransferItem
key={i}
t={t}
txData={txData}
tokenMetas={txData.tokenMetas}
/>
))} ))}
</div> </div>
</InfoRow> </InfoRow>

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import ContentFrame from "../ContentFrame"; import ContentFrame from "../ContentFrame";
import AddressLink from "../components/AddressLink"; import DecoratedAddressLink from "../components/DecoratedAddressLink";
import { TransactionData } from "../types"; import { TransactionData } from "../types";
type LogsProps = { type LogsProps = {
@ -22,7 +22,12 @@ const Logs: React.FC<LogsProps> = ({ txData }) => (
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="font-bold text-right">Address</div> <div className="font-bold text-right">Address</div>
<div className="col-span-11"> <div className="col-span-11">
<AddressLink address={l.address} /> <DecoratedAddressLink
address={l.address}
miner={l.address === txData.miner}
txFrom={l.address === txData.from}
txTo={l.address === txData.to}
/>
</div> </div>
</div> </div>
{l.topics.map((t, i) => ( {l.topics.map((t, i) => (
@ -45,6 +50,7 @@ const Logs: React.FC<LogsProps> = ({ txData }) => (
<textarea <textarea
className="w-full h-20 bg-gray-50 font-mono focus:outline-none border rounded p-2" className="w-full h-20 bg-gray-50 font-mono focus:outline-none border rounded p-2"
value={l.data} value={l.data}
readOnly
/> />
</div> </div>
</div> </div>

View File

@ -16,6 +16,7 @@ export type ProcessedTransaction = {
hash: string; hash: string;
from?: string; from?: string;
to?: string; to?: string;
createdContractAddress?: string;
internalMinerInteraction?: boolean; internalMinerInteraction?: boolean;
value: BigNumber; value: BigNumber;
fee: BigNumber; fee: BigNumber;
@ -44,6 +45,7 @@ export type TransactionData = {
miner?: string; miner?: string;
from: string; from: string;
to: string; to: string;
createdContractAddress?: string;
value: BigNumber; value: BigNumber;
tokenTransfers: TokenTransfer[]; tokenTransfers: TokenTransfer[];
tokenMetas: TokenMetas; tokenMetas: TokenMetas;
@ -57,12 +59,28 @@ export type TransactionData = {
logs: ethers.providers.Log[]; logs: ethers.providers.Log[];
}; };
// The VOID...
export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
export enum AddressContext {
FROM,
TO,
}
export type From = { export type From = {
current: string; current: string;
depth: number; depth: number;
}; };
export type Transfer = { export enum OperationType {
TRANSFER = 0,
SELF_DESTRUCT = 1,
CREATE = 2,
CREATE2 = 3,
}
export type InternalOperation = {
type: OperationType;
from: string; from: string;
to: string; to: string;
value: BigNumber; value: BigNumber;

30
src/useErigonHooks.ts Normal file
View File

@ -0,0 +1,30 @@
import { ethers } from "ethers";
import { useState, useEffect } from "react";
import { getInternalOperations } from "./nodeFunctions";
import { TransactionData, InternalOperation } from "./types";
export const useInternalOperations = (
provider: ethers.providers.JsonRpcProvider | undefined,
txData: TransactionData | undefined
): InternalOperation[] | undefined => {
const [intTransfers, setIntTransfers] = useState<InternalOperation[]>();
useEffect(() => {
const traceTransfers = async () => {
if (!provider || !txData) {
return;
}
const _transfers = await getInternalOperations(provider, txData);
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;
};

View File

@ -1,7 +1,7 @@
import React, { useState, useContext } from "react"; import React, { useState, useContext } from "react";
export type Selection = { export type Selection = {
type: string; type: "address" | "value";
content: string; content: string;
}; };