Merge branch 'release/v2021.07.05-2-otterscan'

This commit is contained in:
Willian Mitsuda 2021-08-05 00:02:30 -03:00
commit 5a2fabf14f
18 changed files with 689 additions and 326 deletions

View File

@ -86,6 +86,7 @@ We intend to release a compatible rebased version containing our changes every w
``` ```
git fetch --all git fetch --all
git fetch otterscan --tags
git checkout <version-tag-otterscan> git checkout <version-tag-otterscan>
``` ```
@ -161,6 +162,12 @@ Erigon itself is alpha, so I consider this software is also in alpha state, howe
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.
## Licensing
This software itself is MIT licensed and redistributes MIT-compatible dependencies.
The Otterscan patches for Erigon are LGPL-3 as required by Geth (since Erigon is a Geth derivative) and kept in a separate repository.
## Getting in touch ## Getting in touch
### Erigon Discord server ### Erigon Discord server

118
package-lock.json generated
View File

@ -18,14 +18,14 @@
"@fortawesome/free-brands-svg-icons": "^5.15.3", "@fortawesome/free-brands-svg-icons": "^5.15.3",
"@fortawesome/free-regular-svg-icons": "^5.15.3", "@fortawesome/free-regular-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3", "@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/react-fontawesome": "^0.1.14", "@fortawesome/react-fontawesome": "^0.1.15",
"@headlessui/react": "^1.4.0", "@headlessui/react": "^1.4.0",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@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": "^14.17.5", "@types/node": "^14.17.5",
"@types/react": "^17.0.14", "@types/react": "^17.0.15",
"@types/react-blockies": "^1.4.1", "@types/react-blockies": "^1.4.1",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.1.8", "@types/react-router-dom": "^5.1.8",
@ -1354,9 +1354,9 @@
} }
}, },
"node_modules/@ethersproject/abstract-signer": { "node_modules/@ethersproject/abstract-signer": {
"version": "5.4.0", "version": "5.4.1",
"resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.4.0.tgz", "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.4.1.tgz",
"integrity": "sha512-AieQAzt05HJZS2bMofpuxMEp81AHufA5D6M4ScKwtolj041nrfIbIi8ciNW7+F59VYxXq+V4c3d568Q6l2m8ew==", "integrity": "sha512-SkkFL5HVq1k4/25dM+NWP9MILgohJCgGv5xT5AcRruGz4ILpfHeBtO/y6j+Z3UN/PAjDeb4P7E51Yh8wcGNLGA==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@ -1435,9 +1435,9 @@
} }
}, },
"node_modules/@ethersproject/bignumber": { "node_modules/@ethersproject/bignumber": {
"version": "5.4.0", "version": "5.4.1",
"resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.4.0.tgz", "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.4.1.tgz",
"integrity": "sha512-OXUu9f9hO3vGRIPxU40cignXZVaYyfx6j9NNMjebKdnaCL3anCLSSy8/b8d03vY6dh7duCC0kW72GEC4tZer2w==", "integrity": "sha512-fJhdxqoQNuDOk6epfM7yD6J8Pol4NUCy1vkaGAkuujZm0+lNow//MKu1hLhRiYV4BsOHyBv5/lsTjF+7hWwhJg==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@ -2069,9 +2069,9 @@
} }
}, },
"node_modules/@fortawesome/react-fontawesome": { "node_modules/@fortawesome/react-fontawesome": {
"version": "0.1.14", "version": "0.1.15",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.14.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.15.tgz",
"integrity": "sha512-4wqNb0gRLVaBm/h+lGe8UfPPivcbuJ6ecI4hIgW0LjI7kzpYB9FkN0L9apbVzg+lsBdcTf0AlBtODjcSX5mmKA==", "integrity": "sha512-/HFHdcoLESxxMkqZAcZ6RXDJ69pVApwdwRos/B2kiMWxDSAX2dFK8Er2/+rG+RsrzWB/dsAyjefLmemgmfE18g==",
"dependencies": { "dependencies": {
"prop-types": "^15.7.2" "prop-types": "^15.7.2"
}, },
@ -3062,9 +3062,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "17.0.14", "version": "17.0.15",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.14.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.15.tgz",
"integrity": "sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==", "integrity": "sha512-uTKHDK9STXFHLaKv6IMnwp52fm0hwU+N89w/p9grdUqcFA6WuqDyPhaWopbNyE1k/VhgzmHl8pu1L4wITtmlLw==",
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",
@ -8037,6 +8037,48 @@
"@ethersproject/wordlists": "5.4.0" "@ethersproject/wordlists": "5.4.0"
} }
}, },
"node_modules/ethers/node_modules/@ethersproject/abstract-signer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.4.0.tgz",
"integrity": "sha512-AieQAzt05HJZS2bMofpuxMEp81AHufA5D6M4ScKwtolj041nrfIbIi8ciNW7+F59VYxXq+V4c3d568Q6l2m8ew==",
"funding": [
{
"type": "individual",
"url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
},
{
"type": "individual",
"url": "https://www.buymeacoffee.com/ricmoo"
}
],
"dependencies": {
"@ethersproject/abstract-provider": "^5.4.0",
"@ethersproject/bignumber": "^5.4.0",
"@ethersproject/bytes": "^5.4.0",
"@ethersproject/logger": "^5.4.0",
"@ethersproject/properties": "^5.4.0"
}
},
"node_modules/ethers/node_modules/@ethersproject/bignumber": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.4.0.tgz",
"integrity": "sha512-OXUu9f9hO3vGRIPxU40cignXZVaYyfx6j9NNMjebKdnaCL3anCLSSy8/b8d03vY6dh7duCC0kW72GEC4tZer2w==",
"funding": [
{
"type": "individual",
"url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
},
{
"type": "individual",
"url": "https://www.buymeacoffee.com/ricmoo"
}
],
"dependencies": {
"@ethersproject/bytes": "^5.4.0",
"@ethersproject/logger": "^5.4.0",
"bn.js": "^4.11.9"
}
},
"node_modules/eventemitter3": { "node_modules/eventemitter3": {
"version": "4.0.7", "version": "4.0.7",
"license": "MIT" "license": "MIT"
@ -20042,9 +20084,9 @@
} }
}, },
"@ethersproject/abstract-signer": { "@ethersproject/abstract-signer": {
"version": "5.4.0", "version": "5.4.1",
"resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.4.0.tgz", "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.4.1.tgz",
"integrity": "sha512-AieQAzt05HJZS2bMofpuxMEp81AHufA5D6M4ScKwtolj041nrfIbIi8ciNW7+F59VYxXq+V4c3d568Q6l2m8ew==", "integrity": "sha512-SkkFL5HVq1k4/25dM+NWP9MILgohJCgGv5xT5AcRruGz4ILpfHeBtO/y6j+Z3UN/PAjDeb4P7E51Yh8wcGNLGA==",
"requires": { "requires": {
"@ethersproject/abstract-provider": "^5.4.0", "@ethersproject/abstract-provider": "^5.4.0",
"@ethersproject/bignumber": "^5.4.0", "@ethersproject/bignumber": "^5.4.0",
@ -20083,9 +20125,9 @@
} }
}, },
"@ethersproject/bignumber": { "@ethersproject/bignumber": {
"version": "5.4.0", "version": "5.4.1",
"resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.4.0.tgz", "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.4.1.tgz",
"integrity": "sha512-OXUu9f9hO3vGRIPxU40cignXZVaYyfx6j9NNMjebKdnaCL3anCLSSy8/b8d03vY6dh7duCC0kW72GEC4tZer2w==", "integrity": "sha512-fJhdxqoQNuDOk6epfM7yD6J8Pol4NUCy1vkaGAkuujZm0+lNow//MKu1hLhRiYV4BsOHyBv5/lsTjF+7hWwhJg==",
"requires": { "requires": {
"@ethersproject/bytes": "^5.4.0", "@ethersproject/bytes": "^5.4.0",
"@ethersproject/logger": "^5.4.0", "@ethersproject/logger": "^5.4.0",
@ -20445,9 +20487,9 @@
} }
}, },
"@fortawesome/react-fontawesome": { "@fortawesome/react-fontawesome": {
"version": "0.1.14", "version": "0.1.15",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.14.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.15.tgz",
"integrity": "sha512-4wqNb0gRLVaBm/h+lGe8UfPPivcbuJ6ecI4hIgW0LjI7kzpYB9FkN0L9apbVzg+lsBdcTf0AlBtODjcSX5mmKA==", "integrity": "sha512-/HFHdcoLESxxMkqZAcZ6RXDJ69pVApwdwRos/B2kiMWxDSAX2dFK8Er2/+rG+RsrzWB/dsAyjefLmemgmfE18g==",
"requires": { "requires": {
"prop-types": "^15.7.2" "prop-types": "^15.7.2"
} }
@ -21087,9 +21129,9 @@
"version": "1.5.4" "version": "1.5.4"
}, },
"@types/react": { "@types/react": {
"version": "17.0.14", "version": "17.0.15",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.14.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.15.tgz",
"integrity": "sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==", "integrity": "sha512-uTKHDK9STXFHLaKv6IMnwp52fm0hwU+N89w/p9grdUqcFA6WuqDyPhaWopbNyE1k/VhgzmHl8pu1L4wITtmlLw==",
"requires": { "requires": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",
@ -24471,6 +24513,30 @@
"@ethersproject/wallet": "5.4.0", "@ethersproject/wallet": "5.4.0",
"@ethersproject/web": "5.4.0", "@ethersproject/web": "5.4.0",
"@ethersproject/wordlists": "5.4.0" "@ethersproject/wordlists": "5.4.0"
},
"dependencies": {
"@ethersproject/abstract-signer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.4.0.tgz",
"integrity": "sha512-AieQAzt05HJZS2bMofpuxMEp81AHufA5D6M4ScKwtolj041nrfIbIi8ciNW7+F59VYxXq+V4c3d568Q6l2m8ew==",
"requires": {
"@ethersproject/abstract-provider": "^5.4.0",
"@ethersproject/bignumber": "^5.4.0",
"@ethersproject/bytes": "^5.4.0",
"@ethersproject/logger": "^5.4.0",
"@ethersproject/properties": "^5.4.0"
}
},
"@ethersproject/bignumber": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.4.0.tgz",
"integrity": "sha512-OXUu9f9hO3vGRIPxU40cignXZVaYyfx6j9NNMjebKdnaCL3anCLSSy8/b8d03vY6dh7duCC0kW72GEC4tZer2w==",
"requires": {
"@ethersproject/bytes": "^5.4.0",
"@ethersproject/logger": "^5.4.0",
"bn.js": "^4.11.9"
}
}
} }
}, },
"eventemitter3": { "eventemitter3": {

View File

@ -14,14 +14,14 @@
"@fortawesome/free-brands-svg-icons": "^5.15.3", "@fortawesome/free-brands-svg-icons": "^5.15.3",
"@fortawesome/free-regular-svg-icons": "^5.15.3", "@fortawesome/free-regular-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3", "@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/react-fontawesome": "^0.1.14", "@fortawesome/react-fontawesome": "^0.1.15",
"@headlessui/react": "^1.4.0", "@headlessui/react": "^1.4.0",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@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": "^14.17.5", "@types/node": "^14.17.5",
"@types/react": "^17.0.14", "@types/react": "^17.0.15",
"@types/react-blockies": "^1.4.1", "@types/react-blockies": "^1.4.1",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.1.8", "@types/react-router-dom": "^5.1.8",

View File

@ -83,7 +83,7 @@ const Block: React.FC = () => {
className="bg-link-blue bg-opacity-10 text-link-blue hover:bg-opacity-100 hover:text-white rounded-lg px-2 py-1 text-xs" className="bg-link-blue bg-opacity-10 text-link-blue hover:bg-opacity-100 hover:text-white rounded-lg px-2 py-1 text-xs"
to={blockTxsURL(block.number)} to={blockTxsURL(block.number)}
> >
{block.transactions.length} transactions {block.transactionCount} transactions
</NavLink>{" "} </NavLink>{" "}
in this block in this block
</InfoRow> </InfoRow>

View File

@ -1,17 +1,13 @@
import React, { useEffect, useState, useMemo, useContext } from "react"; import React, { useMemo, useContext } from "react";
import { useParams, useLocation } from "react-router"; import { useParams, useLocation } from "react-router";
import { ethers } from "ethers"; import { ethers } from "ethers";
import queryString from "query-string"; import queryString from "query-string";
import StandardFrame from "./StandardFrame"; import StandardFrame from "./StandardFrame";
import BlockTransactionHeader from "./block/BlockTransactionHeader"; import BlockTransactionHeader from "./block/BlockTransactionHeader";
import BlockTransactionResults from "./block/BlockTransactionResults"; import BlockTransactionResults from "./block/BlockTransactionResults";
import {
InternalOperation,
OperationType,
ProcessedTransaction,
} from "./types";
import { PAGE_SIZE } from "./params"; import { PAGE_SIZE } from "./params";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { useBlockTransactions } from "./useErigonHooks";
type BlockParams = { type BlockParams = {
blockNumber: string; blockNumber: string;
@ -38,87 +34,12 @@ const BlockTransactions: React.FC = () => {
[params.blockNumber] [params.blockNumber]
); );
const [txs, setTxs] = useState<ProcessedTransaction[]>(); const [totalTxs, txs] = useBlockTransactions(
useEffect(() => { provider,
if (!provider) { blockNumber.toNumber(),
return; pageNumber - 1,
} PAGE_SIZE
const readBlock = async () => {
const [_block, _receipts] = await Promise.all([
provider.getBlockWithTransactions(blockNumber.toNumber()),
provider.send("eth_getBlockReceipts", [blockNumber.toNumber()]),
]);
document.title = `Block #${_block.number} Transactions | Otterscan`;
const responses = _block.transactions
.map((t, i): ProcessedTransaction => {
return {
blockNumber: blockNumber.toNumber(),
timestamp: _block.timestamp,
miner: _block.miner,
idx: i,
hash: t.hash,
from: t.from,
to: t.to,
createdContractAddress: _receipts[i].contractAddress,
value: t.value,
fee:
t.type !== 2
? provider.formatter
.bigNumber(_receipts[i].gasUsed)
.mul(t.gasPrice!)
: provider.formatter
.bigNumber(_receipts[i].gasUsed)
.mul(t.maxPriorityFeePerGas!.add(_block.baseFeePerGas!)),
gasPrice:
t.type !== 2
? t.gasPrice!
: t.maxPriorityFeePerGas!.add(_block.baseFeePerGas!),
data: t.data,
status: provider.formatter.number(_receipts[i].status),
};
})
.reverse();
setTxs(responses);
const internalChecks = await Promise.all(
responses.map(async (res) => {
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(op.from) ||
res.miner === ethers.utils.getAddress(op.to))
) {
return true;
}
}
return false;
})
);
const processedResponses = responses.map((r, i): ProcessedTransaction => {
return { ...r, internalMinerInteraction: internalChecks[i] };
});
setTxs(processedResponses);
};
readBlock();
}, [provider, blockNumber]);
const page = useMemo(() => {
if (!txs) {
return undefined;
}
const pageStart = (pageNumber - 1) * PAGE_SIZE;
return txs.slice(pageStart, pageStart + PAGE_SIZE);
}, [txs, pageNumber]);
const total = useMemo(() => txs?.length ?? 0, [txs]);
document.title = `Block #${blockNumber} Txns | Otterscan`; document.title = `Block #${blockNumber} Txns | Otterscan`;
@ -126,8 +47,8 @@ const BlockTransactions: React.FC = () => {
<StandardFrame> <StandardFrame>
<BlockTransactionHeader blockTag={blockNumber.toNumber()} /> <BlockTransactionHeader blockTag={blockNumber.toNumber()} />
<BlockTransactionResults <BlockTransactionResults
page={page} page={txs}
total={total} total={totalTxs ?? 0}
pageNumber={pageNumber} pageNumber={pageNumber}
/> />
</StandardFrame> </StandardFrame>

View File

@ -1,19 +1,13 @@
import React, { useState, useEffect, useMemo, useContext } from "react"; import React, { 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 StandardFrame from "./StandardFrame"; import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle"; import StandardSubtitle from "./StandardSubtitle";
import Tab from "./components/Tab"; 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 { 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"; import { useInternalOperations, useTxData } from "./useErigonHooks";
const TRANSFER_TOPIC =
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
type TransactionParams = { type TransactionParams = {
txhash: string; txhash: string;
@ -24,89 +18,7 @@ const Transaction: React.FC = () => {
const params = useParams<TransactionParams>(); const params = useParams<TransactionParams>();
const { txhash } = params; const { txhash } = params;
const [txData, setTxData] = useState<TransactionData>(); const txData = useTxData(provider, txhash);
useEffect(() => {
if (!provider) {
return;
}
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.getAddress(
ethers.utils.hexDataSlice(ethers.utils.arrayify(l.topics[1]), 12)
),
to: ethers.utils.getAddress(
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,
miner: _block.miner,
from: _receipt.from,
to: _receipt.to,
createdContractAddress: _receipt.contractAddress,
value: _response.value,
tokenTransfers,
tokenMetas,
type: _response.type ?? 0,
fee: _response.gasPrice!.mul(_receipt.gasUsed),
blockBaseFeePerGas: _block.baseFeePerGas,
maxFeePerGas: _response.maxFeePerGas,
maxPriorityFeePerGas: _response.maxPriorityFeePerGas,
gasPrice: _response.gasPrice!,
gasUsed: _receipt.gasUsed,
gasLimit: _response.gasLimit,
nonce: _response.nonce,
data: _response.data,
logs: _receipt.logs,
});
};
readBlock();
}, [provider, txhash]);
const internalOps = useInternalOperations(provider, txData); const internalOps = useInternalOperations(provider, txData);
const sendsEthToMiner = useMemo(() => { const sendsEthToMiner = useMemo(() => {

21
src/components/Nonce.tsx Normal file
View File

@ -0,0 +1,21 @@
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowUp } from "@fortawesome/free-solid-svg-icons";
type NonceProps = {
value: number;
};
const Nonce: React.FC<NonceProps> = ({ value }) => (
<span
className="flex items-baseline space-x-2 rounded-lg px-2 py-1 bg-green-50 text-xs"
title="Nonce"
>
<span className="text-green-400">
<FontAwesomeIcon icon={faArrowUp} size="1x" />
</span>
<span className="text-green-600">{value}</span>
</span>
);
export default React.memo(Nonce);

View File

@ -0,0 +1,23 @@
import React from "react";
type PercentagePositionProps = {
perc: number;
};
const PercentagePosition: React.FC<PercentagePositionProps> = ({ perc }) => (
<div className="self-center w-40">
<div className="w-full h-5 relative rounded border border-orange-200">
<div className="absolute w-full h-1/2 border-b"></div>
<div className="absolute top-1/4 w-full h-1/2 border-l-2 border-r-2 border-gray-300"></div>
<div className="absolute top-1/4 w-1/2 h-1/2 border-r-2 border-gray-300"></div>
<div className="absolute top-1/4 w-1/4 h-1/2 border-r-2 border-gray-300"></div>
<div className="absolute top-1/4 w-3/4 h-1/2 border-r-2 border-gray-300"></div>
<div
className="absolute h-full border-r-4 border-black"
style={{ width: `${perc * 100}%` }}
></div>
</div>
</div>
);
export default React.memo(PercentagePosition);

View File

@ -0,0 +1,15 @@
import React from "react";
type RelativePositionProps = {
pos: React.ReactNode;
total: React.ReactNode;
};
const RelativePosition: React.FC<RelativePositionProps> = ({ pos, total }) => (
<span className="text-xs">
{pos}
<span className="text-gray-600 text-sm"> / {total}</span>
</span>
);
export default React.memo(RelativePosition);

View File

@ -1,12 +1,12 @@
import { ethers } from "ethers"; import { ethers } from "ethers";
import { TransactionData, InternalOperation } from "./types"; import { InternalOperation } from "./types";
export const getInternalOperations = async ( export const getInternalOperations = async (
provider: ethers.providers.JsonRpcProvider, provider: ethers.providers.JsonRpcProvider,
txData: TransactionData txHash: string
) => { ) => {
const rawTransfers = await provider.send("ots_getInternalOperations", [ const rawTransfers = await provider.send("ots_getInternalOperations", [
txData.transactionHash, txHash,
]); ]);
const _transfers: InternalOperation[] = []; const _transfers: InternalOperation[] = [];

View File

@ -0,0 +1,33 @@
import React, { useState } from "react";
import { Transition } from "@headlessui/react";
type BlipProps = {
value: number;
};
const Blip: React.FC<BlipProps> = ({ value }) => {
const [show, setShow] = useState<boolean>(true);
return (
<Transition
show
appear
enter="transition transform ease-in duration-1000 translate-x-full pl-3"
enterFrom="opacity-100 translate-y-0"
enterTo="opacity-0 -translate-y-5"
afterEnter={() => setShow(false)}
>
{show && value !== 0 && (
<div
className={`absolute bottom-0 font-bold ${
value > 0 ? "text-green-500" : "text-red-500"
} text-3xl`}
>
{value > 0 ? `+${value}` : `-${value}`}
</div>
)}
</Transition>
);
};
export default React.memo(Blip);

View File

@ -1,17 +1,19 @@
import { ethers } from "ethers";
import React from "react"; import React from "react";
import { ethers } from "ethers";
import BlockLink from "../../components/BlockLink"; import BlockLink from "../../components/BlockLink";
import TimestampAge from "../../components/TimestampAge"; import TimestampAge from "../../components/TimestampAge";
import { ExtendedBlock } from "../../useErigonHooks"; import { ExtendedBlock } from "../../useErigonHooks";
import Blip from "./Blip";
const ELASTICITY_MULTIPLIER = 2; const ELASTICITY_MULTIPLIER = 2;
type BlockRowProps = { type BlockRowProps = {
now: number; now: number;
block: ExtendedBlock; block: ExtendedBlock;
baseFeeDelta: number;
}; };
const BlockRow: React.FC<BlockRowProps> = ({ now, block }) => { const BlockRow: React.FC<BlockRowProps> = ({ now, block, baseFeeDelta }) => {
const gasTarget = block.gasLimit.div(ELASTICITY_MULTIPLIER); const gasTarget = block.gasLimit.div(ELASTICITY_MULTIPLIER);
const burntFees = const burntFees =
block?.baseFeePerGas && block.baseFeePerGas.mul(block.gasUsed); block?.baseFeePerGas && block.baseFeePerGas.mul(block.gasUsed);
@ -19,7 +21,7 @@ const BlockRow: React.FC<BlockRowProps> = ({ now, block }) => {
const totalReward = block.blockReward.add(netFeeReward ?? 0); const totalReward = block.blockReward.add(netFeeReward ?? 0);
return ( return (
<div className="grid grid-cols-8 px-3 py-2 hover:bg-gray-100"> <div className="grid grid-cols-8 gap-x-2 px-3 py-2 hover:bg-gray-100">
<div> <div>
<BlockLink blockTag={block.number} /> <BlockLink blockTag={block.number} />
</div> </div>
@ -37,7 +39,12 @@ const BlockRow: React.FC<BlockRowProps> = ({ now, block }) => {
<div className="text-right text-gray-400"> <div className="text-right text-gray-400">
{ethers.utils.commify(gasTarget.toString())} {ethers.utils.commify(gasTarget.toString())}
</div> </div>
<div className="text-right">{block.baseFeePerGas?.toString()} wei</div> <div className="text-right">
<div className="relative">
<span>{block.baseFeePerGas?.toString()} wei</span>
<Blip value={baseFeeDelta} />
</div>
</div>
<div className="text-right col-span-2"> <div className="text-right col-span-2">
{ethers.utils.commify(ethers.utils.formatEther(totalReward))} Ether {ethers.utils.commify(ethers.utils.formatEther(totalReward))} Ether
</div> </div>

View File

@ -7,7 +7,6 @@ import React, {
} from "react"; } from "react";
import { ethers } from "ethers"; import { ethers } from "ethers";
import { Line } from "react-chartjs-2"; import { Line } from "react-chartjs-2";
import { ChartData, ChartOptions } from "chart.js";
import { Transition } from "@headlessui/react"; import { Transition } from "@headlessui/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
@ -20,40 +19,17 @@ import {
import BlockRow from "./BlockRow"; import BlockRow from "./BlockRow";
import { ExtendedBlock, readBlock } from "../../useErigonHooks"; import { ExtendedBlock, readBlock } from "../../useErigonHooks";
import { RuntimeContext } from "../../useRuntime"; import { RuntimeContext } from "../../useRuntime";
import {
burntFeesChartOptions,
burntFeesChartData,
gasChartOptions,
gasChartData,
} from "./chart";
const MAX_BLOCK_HISTORY = 20; const MAX_BLOCK_HISTORY = 20;
const PREV_BLOCK_COUNT = 15; const PREV_BLOCK_COUNT = 15;
const options: ChartOptions = {
animation: false,
plugins: {
legend: {
display: false,
},
},
scales: {
x: {
ticks: {
callback: function (v) {
// @ts-ignore
return ethers.utils.commify(this.getLabelForValue(v));
},
},
},
y: {
beginAtZero: true,
title: {
display: true,
text: "Burnt fees",
},
ticks: {
callback: (v) => `${v} Gwei`,
},
},
},
};
type BlocksProps = { type BlocksProps = {
latestBlock: ethers.providers.Block; latestBlock: ethers.providers.Block;
targetBlockNumber: number; targetBlockNumber: number;
@ -61,8 +37,9 @@ type BlocksProps = {
const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => { const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const [blocks, setBlock] = useState<ExtendedBlock[]>([]); const [blocks, setBlocks] = useState<ExtendedBlock[]>([]);
const [now, setNow] = useState<number>(Date.now()); const [now, setNow] = useState<number>(Date.now());
const [toggleChart, setToggleChart] = useState<boolean>(true);
const addBlock = useCallback( const addBlock = useCallback(
async (blockNumber: number) => { async (blockNumber: number) => {
@ -77,7 +54,7 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
const extBlock = await readBlock(provider, blockNumber.toString()); const extBlock = await readBlock(provider, blockNumber.toString());
setNow(Date.now()); setNow(Date.now());
setBlock((_blocks) => { setBlocks((_blocks) => {
if (_blocks.length > 0 && blockNumber === _blocks[0].number) { if (_blocks.length > 0 && blockNumber === _blocks[0].number) {
return _blocks; return _blocks;
} }
@ -100,23 +77,11 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
addBlock(latestBlock.number); addBlock(latestBlock.number);
}, [addBlock, latestBlock]); }, [addBlock, latestBlock]);
const data: ChartData = useMemo(() => { const data = useMemo(
return { () => (toggleChart ? gasChartData(blocks) : burntFeesChartData(blocks)),
labels: blocks.map((b) => b.number.toString()).reverse(), [toggleChart, blocks]
datasets: [ );
{ const chartOptions = toggleChart ? gasChartOptions : burntFeesChartOptions;
label: "Burnt fees (Gwei)",
data: blocks
.map((b) => b.gasUsed.mul(b.baseFeePerGas!).toNumber() / 1e9)
.reverse(),
fill: true,
backgroundColor: "#FDBA74",
borderColor: "#F97316",
tension: 0.2,
},
],
};
}, [blocks]);
// On page reload, pre-populate the last N blocks // On page reload, pre-populate the last N blocks
useEffect( useEffect(
@ -130,6 +95,8 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
await addBlock(i); await addBlock(i);
} }
}; };
setBlocks([]);
addPreviousBlocks(); addPreviousBlocks();
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -139,6 +106,7 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
return ( return (
<div className="w-full mb-auto"> <div className="w-full mb-auto">
<div className="px-9 pt-3 pb-12 divide-y-2"> <div className="px-9 pt-3 pb-12 divide-y-2">
<div className="relative">
<div className="flex justify-center items-baseline space-x-2 px-3 pb-2 text-lg text-orange-500 "> <div className="flex justify-center items-baseline space-x-2 px-3 pb-2 text-lg text-orange-500 ">
<span> <span>
<FontAwesomeIcon icon={faBurn} /> <FontAwesomeIcon icon={faBurn} />
@ -148,10 +116,16 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
<FontAwesomeIcon icon={faBurn} /> <FontAwesomeIcon icon={faBurn} />
</span> </span>
</div> </div>
<div> <div className="absolute right-0 top-0 border rounded shadow-md px-2 py-1 text-sm text-link-blue hover:bg-gray-50 hover:text-link-blue-hover">
<Line data={data} height={100} options={options} /> <button onClick={() => setToggleChart(!toggleChart)}>
{toggleChart ? "Gas usage" : "Burnt fees"}
</button>
</div> </div>
<div className="mt-5 grid grid-cols-8 px-3 py-2"> </div>
<div>
<Line data={data} height={100} options={chartOptions} />
</div>
<div className="mt-5 grid grid-cols-8 gap-x-2 px-3 py-2">
<div className="flex space-x-1 items-baseline"> <div className="flex space-x-1 items-baseline">
<span className="text-gray-500"> <span className="text-gray-500">
<FontAwesomeIcon icon={faCube} /> <FontAwesomeIcon icon={faCube} />
@ -185,7 +159,7 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
<span>Age</span> <span>Age</span>
</div> </div>
</div> </div>
{blocks.map((b, i) => ( {blocks.map((b, i, all) => (
<Transition <Transition
key={b.hash} key={b.hash}
show={i < MAX_BLOCK_HISTORY} show={i < MAX_BLOCK_HISTORY}
@ -197,7 +171,15 @@ const Blocks: React.FC<BlocksProps> = ({ latestBlock, targetBlockNumber }) => {
leaveFrom="opacity-100 translate-y-0" leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-10" leaveTo="opacity-0 translate-y-10"
> >
<BlockRow now={now} block={b} /> <BlockRow
now={now}
block={b}
baseFeeDelta={
i < all.length - 1
? b.baseFeePerGas!.sub(all[i + 1].baseFeePerGas!).toNumber()
: 0
}
/>
</Transition> </Transition>
))} ))}
</div> </div>

161
src/special/london/chart.ts Normal file
View File

@ -0,0 +1,161 @@
import { ethers } from "ethers";
import { ChartData, ChartOptions } from "chart.js";
import { ExtendedBlock } from "../../useErigonHooks";
export const burntFeesChartOptions: ChartOptions = {
animation: false,
plugins: {
legend: {
display: false,
},
},
scales: {
x: {
ticks: {
callback: function (v) {
// @ts-ignore
return ethers.utils.commify(this.getLabelForValue(v));
},
},
},
y: {
beginAtZero: true,
title: {
display: true,
text: "Burnt fees",
},
ticks: {
callback: (v) => `${v} Gwei`,
},
},
yBaseFee: {
position: "right",
beginAtZero: true,
title: {
display: true,
text: "Base fee",
},
ticks: {
callback: (v) => `${v} wei`,
},
grid: {
drawOnChartArea: false,
},
},
},
};
export const burntFeesChartData = (blocks: ExtendedBlock[]): ChartData => ({
labels: blocks.map((b) => b.number.toString()).reverse(),
datasets: [
{
label: "Burnt fees (Gwei)",
data: blocks
.map((b) => b.gasUsed.mul(b.baseFeePerGas!).toNumber() / 1e9)
.reverse(),
fill: true,
backgroundColor: "#FDBA7470",
borderColor: "#FB923C",
tension: 0.2,
},
{
label: "Base fee (wei)",
data: blocks.map((b) => b.baseFeePerGas!.toNumber()).reverse(),
yAxisID: "yBaseFee",
borderColor: "#38BDF8",
tension: 0.2,
},
],
});
export const gasChartOptions: ChartOptions = {
animation: false,
interaction: {
mode: "index",
intersect: false,
},
plugins: {
legend: {
display: false,
},
},
scales: {
x: {
ticks: {
callback: function (v) {
// @ts-ignore
return ethers.utils.commify(this.getLabelForValue(v));
},
},
},
y: {
beginAtZero: true,
title: {
display: true,
text: "Gas",
},
},
yBaseFee: {
position: "right",
beginAtZero: true,
title: {
display: true,
text: "Base fee",
},
ticks: {
callback: (v) => `${v} wei`,
},
grid: {
drawOnChartArea: false,
},
},
},
};
export const gasChartData = (blocks: ExtendedBlock[]): ChartData => ({
labels: blocks.map((b) => b.number.toString()).reverse(),
datasets: [
{
label: "Gas used",
data: blocks.map((b) => b.gasUsed.toNumber()).reverse(),
fill: true,
segment: {
backgroundColor: (ctx, x) =>
ctx.p1.parsed.y >
Math.round(blocks[ctx.p1DataIndex].gasLimit.toNumber() / 2)
? "#22C55E70"
: "#EF444470",
borderColor: (ctx) =>
ctx.p1.parsed.y >
Math.round(blocks[ctx.p1DataIndex].gasLimit.toNumber() / 2)
? "#22C55E"
: "#EF4444",
},
tension: 0.2,
},
{
label: "Gas target",
data: blocks.map((b) => Math.round(b.gasLimit.toNumber() / 2)).reverse(),
borderColor: "#FCA5A5",
borderDash: [5, 5],
borderWidth: 2,
tension: 0.2,
pointStyle: "dash",
},
{
label: "Gas limit",
data: blocks.map((b) => b.gasLimit.toNumber()).reverse(),
borderColor: "#B91C1CF0",
tension: 0.2,
pointStyle: "crossRot",
radius: 5,
},
{
label: "Base fee (wei)",
data: blocks.map((b) => b.baseFeePerGas!.toNumber()).reverse(),
yAxisID: "yBaseFee",
borderColor: "#38BDF8",
tension: 0.2,
},
],
});

View File

@ -3,6 +3,7 @@ import { ethers } from "ethers";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
faCheckCircle, faCheckCircle,
faCube,
faTimesCircle, faTimesCircle,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import ContentFrame from "../ContentFrame"; import ContentFrame from "../ContentFrame";
@ -12,6 +13,7 @@ import BlockConfirmations from "../components/BlockConfirmations";
import AddressHighlighter from "../components/AddressHighlighter"; import AddressHighlighter from "../components/AddressHighlighter";
import DecoratedAddressLink from "../components/DecoratedAddressLink"; import DecoratedAddressLink from "../components/DecoratedAddressLink";
import Copy from "../components/Copy"; import Copy from "../components/Copy";
import Nonce from "../components/Nonce";
import Timestamp from "../components/Timestamp"; import Timestamp from "../components/Timestamp";
import InternalTransactionOperation from "../components/InternalTransactionOperation"; import InternalTransactionOperation from "../components/InternalTransactionOperation";
import MethodName from "../components/MethodName"; import MethodName from "../components/MethodName";
@ -23,6 +25,8 @@ import TokenTransferItem from "../TokenTransferItem";
import { TransactionData, InternalOperation } from "../types"; import { TransactionData, InternalOperation } from "../types";
import PercentageBar from "../components/PercentageBar"; import PercentageBar from "../components/PercentageBar";
import ExternalLink from "../components/ExternalLink"; import ExternalLink from "../components/ExternalLink";
import RelativePosition from "../components/RelativePosition";
import PercentagePosition from "../components/PercentagePosition";
type DetailsProps = { type DetailsProps = {
txData: TransactionData; txData: TransactionData;
@ -60,17 +64,34 @@ const Details: React.FC<DetailsProps> = ({
</span> </span>
)} )}
</InfoRow> </InfoRow>
<InfoRow title="Block"> <InfoRow title="Block / Position">
<div className="flex items-baseline space-x-2"> <div className="flex items-baseline divide-x-2 divide-dotted divide-gray-300">
<div className="flex space-x-1 items-baseline mr-3">
<span className="text-orange-500">
<FontAwesomeIcon icon={faCube} />
</span>
<BlockLink blockTag={txData.blockNumber} /> <BlockLink blockTag={txData.blockNumber} />
<BlockConfirmations confirmations={txData.confirmations} /> <BlockConfirmations confirmations={txData.confirmations} />
</div> </div>
<div className="flex space-x-2 items-baseline pl-3">
<RelativePosition
pos={txData.transactionIndex}
total={txData.blockTransactionCount - 1}
/>
<PercentagePosition
perc={
txData.transactionIndex / (txData.blockTransactionCount - 1)
}
/>
</div>
</div>
</InfoRow> </InfoRow>
<InfoRow title="Timestamp"> <InfoRow title="Timestamp">
<Timestamp value={txData.timestamp} /> <Timestamp value={txData.timestamp} />
</InfoRow> </InfoRow>
<InfoRow title="From"> <InfoRow title="From / Nonce">
<div className="flex items-baseline space-x-2 -ml-1"> <div className="flex divide-x-2 divide-dotted divide-gray-300">
<div className="flex items-baseline space-x-2 -ml-1 mr-3">
<AddressHighlighter address={txData.from}> <AddressHighlighter address={txData.from}>
<DecoratedAddressLink <DecoratedAddressLink
address={txData.from} address={txData.from}
@ -80,6 +101,10 @@ const Details: React.FC<DetailsProps> = ({
</AddressHighlighter> </AddressHighlighter>
<Copy value={txData.from} /> <Copy value={txData.from} />
</div> </div>
<div className="flex items-baseline pl-3">
<Nonce value={txData.nonce} />
</div>
</div>
</InfoRow> </InfoRow>
<InfoRow title={txData.to ? "Interacted With (To)" : "Contract Created"}> <InfoRow title={txData.to ? "Interacted With (To)" : "Contract Created"}>
{txData.to ? ( {txData.to ? (
@ -105,7 +130,7 @@ const Details: React.FC<DetailsProps> = ({
<Copy value={txData.createdContractAddress!} /> <Copy value={txData.createdContractAddress!} />
</div> </div>
)} )}
{internalOps && ( {internalOps && internalOps.length > 0 && (
<div className="mt-2 space-y-1"> <div className="mt-2 space-y-1">
{internalOps.map((op, i) => ( {internalOps.map((op, i) => (
<InternalTransactionOperation <InternalTransactionOperation
@ -192,8 +217,10 @@ const Details: React.FC<DetailsProps> = ({
<InfoRow title="Gas Used / Limit"> <InfoRow title="Gas Used / Limit">
<div className="flex space-x-3 items-baseline"> <div className="flex space-x-3 items-baseline">
<div> <div>
<GasValue value={txData.gasUsed} /> /{" "} <RelativePosition
<GasValue value={txData.gasLimit} /> pos={<GasValue value={txData.gasUsed} />}
total={<GasValue value={txData.gasLimit} />}
/>
</div> </div>
<PercentageBar <PercentageBar
perc={ perc={
@ -226,12 +253,6 @@ const Details: React.FC<DetailsProps> = ({
</div> </div>
</InfoRow> </InfoRow>
<InfoRow title="Ether Price">N/A</InfoRow> <InfoRow title="Ether Price">N/A</InfoRow>
<InfoRow title="Nonce">{txData.nonce}</InfoRow>
<InfoRow title="Position in Block">
<span className="rounded px-2 py-1 bg-gray-100 text-gray-500 text-xs">
{txData.transactionIndex}
</span>
</InfoRow>
<InfoRow title="Input Data"> <InfoRow title="Input Data">
<textarea <textarea
className="w-full h-40 bg-gray-50 text-gray-500 font-mono focus:outline-none border rounded p-2" className="w-full h-40 bg-gray-50 text-gray-500 font-mono focus:outline-none border rounded p-2"

View File

@ -40,6 +40,7 @@ export type TransactionData = {
status: boolean; status: boolean;
blockNumber: number; blockNumber: number;
transactionIndex: number; transactionIndex: number;
blockTransactionCount: number;
confirmations: number; confirmations: number;
timestamp: number; timestamp: number;
miner?: string; miner?: string;

View File

@ -1,7 +1,19 @@
import { ethers, BigNumber } from "ethers";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { ethers, BigNumber } from "ethers";
import { BlockWithTransactions } from "@ethersproject/abstract-provider";
import { getInternalOperations } from "./nodeFunctions"; import { getInternalOperations } from "./nodeFunctions";
import { TransactionData, InternalOperation } from "./types"; import {
TokenMetas,
TokenTransfer,
TransactionData,
InternalOperation,
ProcessedTransaction,
OperationType,
} from "./types";
import erc20 from "./erc20.json";
const TRANSFER_TOPIC =
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
export interface ExtendedBlock extends ethers.providers.Block { export interface ExtendedBlock extends ethers.providers.Block {
blockReward: BigNumber; blockReward: BigNumber;
@ -11,6 +23,7 @@ export interface ExtendedBlock extends ethers.providers.Block {
sha3Uncles: string; sha3Uncles: string;
stateRoot: string; stateRoot: string;
totalDifficulty: BigNumber; totalDifficulty: BigNumber;
transactionCount: number;
} }
export const readBlock = async ( export const readBlock = async (
@ -19,43 +32,122 @@ export const readBlock = async (
) => { ) => {
let blockPromise: Promise<any>; let blockPromise: Promise<any>;
if (ethers.utils.isHexString(blockNumberOrHash, 32)) { if (ethers.utils.isHexString(blockNumberOrHash, 32)) {
// TODO: fix
blockPromise = provider.send("eth_getBlockByHash", [ blockPromise = provider.send("eth_getBlockByHash", [
blockNumberOrHash, blockNumberOrHash,
false, false,
]); ]);
} else { } else {
blockPromise = provider.send("eth_getBlockByNumber", [ blockPromise = provider.send("ots_getBlockDetails", [blockNumberOrHash]);
blockNumberOrHash,
false,
]);
} }
const [_rawBlock, _rawIssuance, _rawReceipts] = await Promise.all([
blockPromise,
provider.send("erigon_issuance", [blockNumberOrHash]),
provider.send("eth_getBlockReceipts", [blockNumberOrHash]),
]);
const receipts = (_rawReceipts as any[]).map((r) =>
provider.formatter.receipt(r)
);
const fees = receipts.reduce(
(acc, r) => acc.add(r.effectiveGasPrice.mul(r.gasUsed)),
BigNumber.from(0)
);
const _block = provider.formatter.block(_rawBlock); const _rawBlock = await blockPromise;
const _block = provider.formatter.block(_rawBlock.block);
const _rawIssuance = _rawBlock.issuance;
const fees = provider.formatter.bigNumber(_rawBlock.totalFees);
const extBlock: ExtendedBlock = { const extBlock: ExtendedBlock = {
blockReward: provider.formatter.bigNumber(_rawIssuance.blockReward ?? 0), blockReward: provider.formatter.bigNumber(_rawIssuance.blockReward ?? 0),
unclesReward: provider.formatter.bigNumber(_rawIssuance.uncleReward ?? 0), unclesReward: provider.formatter.bigNumber(_rawIssuance.uncleReward ?? 0),
feeReward: fees, feeReward: fees,
size: provider.formatter.number(_rawBlock.size), size: provider.formatter.number(_rawBlock.block.size),
sha3Uncles: _rawBlock.sha3Uncles, sha3Uncles: _rawBlock.block.sha3Uncles,
stateRoot: _rawBlock.stateRoot, stateRoot: _rawBlock.block.stateRoot,
totalDifficulty: provider.formatter.bigNumber(_rawBlock.totalDifficulty), totalDifficulty: provider.formatter.bigNumber(
_rawBlock.block.totalDifficulty
),
transactionCount: provider.formatter.number(
_rawBlock.block.transactionCount
),
..._block, ..._block,
}; };
return extBlock; return extBlock;
}; };
export const useBlockTransactions = (
provider: ethers.providers.JsonRpcProvider | undefined,
blockNumber: number,
pageNumber: number,
pageSize: number
): [number | undefined, ProcessedTransaction[] | undefined] => {
const [totalTxs, setTotalTxs] = useState<number>();
const [txs, setTxs] = useState<ProcessedTransaction[]>();
useEffect(() => {
if (!provider) {
return;
}
const readBlock = async () => {
const result = await provider.send("ots_getBlockTransactions", [
blockNumber,
pageNumber,
pageSize,
]);
const _block = provider.formatter.blockWithTransactions(
result.fullblock
) as unknown as BlockWithTransactions;
const _receipts = result.receipts;
const rawTxs = _block.transactions
.map(
(t, i): ProcessedTransaction => ({
blockNumber: blockNumber,
timestamp: _block.timestamp,
miner: _block.miner,
idx: i,
hash: t.hash,
from: t.from,
to: t.to,
createdContractAddress: _receipts[i].contractAddress,
value: t.value,
fee:
t.type !== 2
? provider.formatter
.bigNumber(_receipts[i].gasUsed)
.mul(t.gasPrice!)
: provider.formatter
.bigNumber(_receipts[i].gasUsed)
.mul(t.maxPriorityFeePerGas!.add(_block.baseFeePerGas!)),
gasPrice:
t.type !== 2
? t.gasPrice!
: t.maxPriorityFeePerGas!.add(_block.baseFeePerGas!),
data: t.data,
status: provider.formatter.number(_receipts[i].status),
})
)
.reverse();
setTxs(rawTxs);
setTotalTxs(result.fullblock.transactionCount);
const checkTouchMinerAddr = await Promise.all(
rawTxs.map(async (res) => {
const ops = await getInternalOperations(provider, res.hash);
return (
ops.findIndex(
(op) =>
op.type === OperationType.TRANSFER &&
res.miner !== undefined &&
res.miner === ethers.utils.getAddress(op.to)
) !== -1
);
})
);
const processedTxs = rawTxs.map(
(r, i): ProcessedTransaction => ({
...r,
internalMinerInteraction: checkTouchMinerAddr[i],
})
);
setTxs(processedTxs);
};
readBlock();
}, [provider, blockNumber, pageNumber, pageSize]);
return [totalTxs, txs];
};
export const useBlockData = ( export const useBlockData = (
provider: ethers.providers.JsonRpcProvider | undefined, provider: ethers.providers.JsonRpcProvider | undefined,
blockNumberOrHash: string blockNumberOrHash: string
@ -76,6 +168,99 @@ export const useBlockData = (
return block; return block;
}; };
export const useTxData = (
provider: ethers.providers.JsonRpcProvider | undefined,
txhash: string
): TransactionData | undefined => {
const [txData, setTxData] = useState<TransactionData>();
useEffect(() => {
if (!provider) {
return;
}
const readTxData = async () => {
const [_response, _receipt] = await Promise.all([
provider.getTransaction(txhash),
provider.getTransactionReceipt(txhash),
]);
const _block = await readBlock(provider, _receipt.blockNumber.toString());
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.getAddress(
ethers.utils.hexDataSlice(ethers.utils.arrayify(l.topics[1]), 12)
),
to: ethers.utils.getAddress(
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,
blockTransactionCount: _block.transactionCount,
confirmations: _receipt.confirmations,
timestamp: _block.timestamp,
miner: _block.miner,
from: _receipt.from,
to: _receipt.to,
createdContractAddress: _receipt.contractAddress,
value: _response.value,
tokenTransfers,
tokenMetas,
type: _response.type ?? 0,
fee: _response.gasPrice!.mul(_receipt.gasUsed),
blockBaseFeePerGas: _block.baseFeePerGas,
maxFeePerGas: _response.maxFeePerGas,
maxPriorityFeePerGas: _response.maxPriorityFeePerGas,
gasPrice: _response.gasPrice!,
gasUsed: _receipt.gasUsed,
gasLimit: _response.gasLimit,
nonce: _response.nonce,
data: _response.data,
logs: _receipt.logs,
});
};
readTxData();
}, [provider, txhash]);
return txData;
};
export const useInternalOperations = ( export const useInternalOperations = (
provider: ethers.providers.JsonRpcProvider | undefined, provider: ethers.providers.JsonRpcProvider | undefined,
txData: TransactionData | undefined txData: TransactionData | undefined
@ -88,7 +273,10 @@ export const useInternalOperations = (
return; return;
} }
const _transfers = await getInternalOperations(provider, txData); const _transfers = await getInternalOperations(
provider,
txData.transactionHash
);
for (const t of _transfers) { for (const t of _transfers) {
t.from = provider.formatter.address(t.from); t.from = provider.formatter.address(t.from);
t.to = provider.formatter.address(t.to); t.to = provider.formatter.address(t.to);

View File

@ -33,7 +33,12 @@ export const useProvider = (
setConnStatus(ConnectionStatus.CONNECTING); setConnStatus(ConnectionStatus.CONNECTING);
const tryToConnect = async () => { const tryToConnect = async () => {
const provider = new ethers.providers.JsonRpcProvider(erigonURL); let provider: ethers.providers.JsonRpcProvider;
if (erigonURL?.startsWith("ws://") || erigonURL?.startsWith("wss://")) {
provider = new ethers.providers.WebSocketProvider(erigonURL);
} else {
provider = new ethers.providers.JsonRpcProvider(erigonURL);
}
// Check if it is at least a regular ETH node // Check if it is at least a regular ETH node
let blockNumber: number = 0; let blockNumber: number = 0;