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
WORKDIR /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.
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.
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.

14
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

@ -2,22 +2,28 @@ import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCaretRight } from "@fortawesome/free-solid-svg-icons";
import AddressHighlighter from "./components/AddressHighlighter";
import AddressOrENSName from "./components/AddressOrENSName";
import AddressLink from "./components/AddressLink";
import TokenLogo from "./components/TokenLogo";
import ValueHighlighter from "./components/ValueHighlighter";
import DecoratedAddressLink from "./components/DecoratedAddressLink";
import FormattedBalance from "./components/FormattedBalance";
import { TokenMetas, TokenTransfer } from "./types";
import {
AddressContext,
TokenMetas,
TokenTransfer,
TransactionData,
} from "./types";
type TokenTransferItemProps = {
t: TokenTransfer;
txData: TransactionData;
tokenMetas: TokenMetas;
};
const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
t,
txData,
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">
<FontAwesomeIcon icon={faCaretRight} size="1x" />
</span>
@ -25,40 +31,46 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
<div className="flex space-x-1">
<span className="font-bold">From</span>
<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>
</div>
<div className="flex space-x-1">
<span className="font-bold">To</span>
<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>
</div>
<div className="col-span-3 flex space-x-1">
<span className="font-bold">For</span>
<span>
<FormattedBalance
value={t.value}
decimals={tokenMetas[t.token].decimals}
<ValueHighlighter value={t.value}>
<FormattedBalance
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>
<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>
</AddressHighlighter>
</div>
</div>
</div>

View File

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

View File

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

View File

@ -4,16 +4,23 @@ import { NavLink } from "react-router-dom";
type AddressLinkProps = {
address: string;
text?: string;
dontOverrideColors?: boolean;
};
const AddressLink: React.FC<AddressLinkProps> = ({ address, text }) => (
const AddressLink: React.FC<AddressLinkProps> = ({
address,
text,
dontOverrideColors,
}) => (
<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}`}
>
<p className="truncate" title={text ?? address}>
<span className="truncate" title={text ?? address}>
{text ?? address}
</p>
</span>
</NavLink>
);

View File

@ -1,6 +1,4 @@
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCoins } from "@fortawesome/free-solid-svg-icons";
import Address from "./Address";
import AddressLink from "./AddressLink";
import ENSName from "./ENSName";
@ -10,21 +8,18 @@ type AddressOrENSNameProps = {
address: string;
ensName?: string;
selectedAddress?: string;
minerAddress?: string;
text?: string;
dontOverrideColors?: boolean;
};
const AddressOrENSName: React.FC<AddressOrENSNameProps> = ({
address,
ensName,
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 ? (
<>
{ensName ? (
@ -36,13 +31,21 @@ const AddressOrENSName: React.FC<AddressOrENSNameProps> = ({
) : (
<>
{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);

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}
height={12}
/>
<p className="truncate">{name}</p>
<span className="truncate">{name}</span>
</div>
);

View File

@ -5,11 +5,18 @@ import ENSLogo from "./ensLogo.svg";
type ENSNameLinkProps = {
name: string;
address: string;
dontOverrideColors?: boolean;
};
const ENSNameLink: React.FC<ENSNameLinkProps> = ({ name, address }) => (
const ENSNameLink: React.FC<ENSNameLinkProps> = ({
name,
address,
dontOverrideColors,
}) => (
<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}`}
title={`${name}: ${address}`}
>
@ -20,7 +27,7 @@ const ENSNameLink: React.FC<ENSNameLinkProps> = ({ name, address }) => (
width={12}
height={12}
/>
<p className="truncate">{name}</p>
<span className="truncate">{name}</span>
</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 { ethers } from "ethers";
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 AddressLink from "./AddressLink";
import { TransactionData, Transfer } from "../types";
import DecoratedAddressLink from "./DecoratedAddressLink";
import { TransactionData, InternalOperation } from "../types";
type InternalTransferProps = {
txData: TransactionData;
transfer: Transfer;
internalOp: InternalOperation;
};
const InternalTransfer: React.FC<InternalTransferProps> = ({
txData,
transfer,
internalOp,
}) => {
const fromMiner =
txData.miner !== undefined && transfer.from === txData.miner;
const toMiner = txData.miner !== undefined && transfer.to === txData.miner;
txData.miner !== undefined && internalOp.from === txData.miner;
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" /> TRANSFER
</span>
<span>{ethers.utils.formatEther(transfer.value)} Ether</span>
<span>{ethers.utils.formatEther(internalOp.value)} Ether</span>
<div className="flex items-baseline">
<span className="text-gray-500">From</span>
<AddressHighlighter address={transfer.from}>
<AddressHighlighter address={internalOp.from}>
<div
className={`flex items-baseline space-x-1 ${
fromMiner ? "rounded px-2 py-1 bg-yellow-100" : ""
}`}
>
{fromMiner && (
<span className="text-yellow-400" title="Miner address">
<FontAwesomeIcon icon={faCoins} size="1x" />
</span>
)}
<AddressLink address={transfer.from} />
<DecoratedAddressLink
address={internalOp.from}
miner={fromMiner}
txFrom={internalOp.from === txData.from}
txTo={internalOp.from === txData.to}
/>
</div>
</AddressHighlighter>
</div>
<div className="flex items-baseline">
<span className="text-gray-500">To</span>
<AddressHighlighter address={transfer.to}>
<AddressHighlighter address={internalOp.to}>
<div
className={`flex items-baseline space-x-1 ${
toMiner ? "rounded px-2 py-1 bg-yellow-100" : ""
}`}
>
{toMiner && (
<span className="text-yellow-400" title="Miner address">
<FontAwesomeIcon icon={faCoins} size="1x" />
</span>
)}
<AddressLink address={transfer.to} />
<DecoratedAddressLink
address={internalOp.to}
miner={toMiner}
txFrom={internalOp.to === txData.from}
txTo={internalOp.to === txData.to}
/>
</div>
</AddressHighlighter>
</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;

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import React from "react";
import ContentFrame from "../ContentFrame";
import AddressLink from "../components/AddressLink";
import DecoratedAddressLink from "../components/DecoratedAddressLink";
import { TransactionData } from "../types";
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="font-bold text-right">Address</div>
<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>
{l.topics.map((t, i) => (
@ -45,6 +50,7 @@ const Logs: React.FC<LogsProps> = ({ txData }) => (
<textarea
className="w-full h-20 bg-gray-50 font-mono focus:outline-none border rounded p-2"
value={l.data}
readOnly
/>
</div>
</div>

View File

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