otterscan/src/useErigonHooks.ts

705 lines
18 KiB
TypeScript

import { useState, useEffect, useMemo } from "react";
import {
Block,
BlockWithTransactions,
BlockTag,
} from "@ethersproject/abstract-provider";
import { JsonRpcProvider } from "@ethersproject/providers";
import { getAddress } from "@ethersproject/address";
import { Contract } from "@ethersproject/contracts";
import { defaultAbiCoder } from "@ethersproject/abi";
import { BigNumber } from "@ethersproject/bignumber";
import { arrayify, hexDataSlice, isHexString } from "@ethersproject/bytes";
import { AddressZero } from "@ethersproject/constants";
import useSWR from "swr";
import useSWRImmutable from "swr/immutable";
import {
TokenTransfer,
TransactionData,
InternalOperation,
ProcessedTransaction,
OperationType,
ChecksummedAddress,
TokenMeta,
} from "./types";
import erc20 from "./erc20.json";
const TRANSFER_TOPIC =
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
export interface ExtendedBlock extends Block {
blockReward: BigNumber;
unclesReward: BigNumber;
feeReward: BigNumber;
size: number;
sha3Uncles: string;
stateRoot: string;
totalDifficulty: BigNumber;
transactionCount: number;
}
export const readBlock = async (
provider: JsonRpcProvider,
blockNumberOrHash: string
): Promise<ExtendedBlock | null> => {
let blockPromise: Promise<any>;
if (isHexString(blockNumberOrHash, 32)) {
blockPromise = provider.send("ots_getBlockDetailsByHash", [
blockNumberOrHash,
]);
} else {
const blockNumber = parseInt(blockNumberOrHash);
if (isNaN(blockNumber) || blockNumber < 0) {
return null;
}
blockPromise = provider.send("ots_getBlockDetails", [blockNumber]);
}
const _rawBlock = await blockPromise;
if (_rawBlock === null) {
return null;
}
const _block = provider.formatter.block(_rawBlock.block);
const _rawIssuance = _rawBlock.issuance;
const extBlock: ExtendedBlock = {
blockReward: provider.formatter.bigNumber(_rawIssuance.blockReward ?? 0),
unclesReward: provider.formatter.bigNumber(_rawIssuance.uncleReward ?? 0),
feeReward: provider.formatter.bigNumber(_rawBlock.totalFees),
size: provider.formatter.number(_rawBlock.block.size),
sha3Uncles: _rawBlock.block.sha3Uncles,
stateRoot: _rawBlock.block.stateRoot,
totalDifficulty: provider.formatter.bigNumber(
_rawBlock.block.totalDifficulty
),
transactionCount: provider.formatter.number(
_rawBlock.block.transactionCount
),
..._block,
};
return extBlock;
};
export const useBlockTransactions = (
provider: 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 => {
const _rawReceipt = _receipts[i];
// Empty logs on purpose because of ethers formatter requires it
_rawReceipt.logs = [];
const _receipt = provider.formatter.receipt(_rawReceipt);
return {
blockNumber: blockNumber,
timestamp: _block.timestamp,
miner: _block.miner,
idx: i,
hash: t.hash,
from: t.from,
to: t.to ?? null,
createdContractAddress: _receipt.contractAddress,
value: t.value,
fee:
t.type !== 2
? provider.formatter
.bigNumber(_receipt.gasUsed)
.mul(t.gasPrice!)
: provider.formatter
.bigNumber(_receipt.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(_receipt.status),
};
})
.reverse();
setTxs(rawTxs);
setTotalTxs(result.fullblock.transactionCount);
};
readBlock();
}, [provider, blockNumber, pageNumber, pageSize]);
return [totalTxs, txs];
};
const blockDataFetcher = async (
provider: JsonRpcProvider,
blockNumberOrHash: string
) => {
return await readBlock(provider, blockNumberOrHash);
};
// TODO: some callers may use only block headers?
export const useBlockData = (
provider: JsonRpcProvider | undefined,
blockNumberOrHash: string | undefined
): ExtendedBlock | null | undefined => {
const { data, error } = useSWRImmutable(
provider !== undefined && blockNumberOrHash !== undefined
? [provider, blockNumberOrHash]
: null,
blockDataFetcher
);
if (error) {
return undefined;
}
return data;
};
export const useBlockDataFromTransaction = (
provider: JsonRpcProvider | undefined,
txData: TransactionData | null | undefined
): ExtendedBlock | null | undefined => {
const block = useBlockData(
provider,
txData?.confirmedData
? txData.confirmedData.blockNumber.toString()
: undefined
);
return block;
};
export const useTxData = (
provider: JsonRpcProvider | undefined,
txhash: string
): TransactionData | undefined | null => {
const [txData, setTxData] = useState<TransactionData | undefined | null>();
useEffect(() => {
if (!provider) {
return;
}
const readTxData = async () => {
try {
const [_response, _receipt] = await Promise.all([
provider.getTransaction(txhash),
provider.getTransactionReceipt(txhash),
]);
if (_response === null) {
setTxData(null);
return;
}
setTxData({
transactionHash: _response.hash,
from: _response.from,
to: _response.to,
value: _response.value,
type: _response.type ?? 0,
maxFeePerGas: _response.maxFeePerGas,
maxPriorityFeePerGas: _response.maxPriorityFeePerGas,
gasPrice: _response.gasPrice!,
gasLimit: _response.gasLimit,
nonce: _response.nonce,
data: _response.data,
confirmedData:
_receipt === null
? undefined
: {
status: _receipt.status === 1,
blockNumber: _receipt.blockNumber,
transactionIndex: _receipt.transactionIndex,
confirmations: _receipt.confirmations,
createdContractAddress: _receipt.contractAddress,
fee: _response.gasPrice!.mul(_receipt.gasUsed),
gasUsed: _receipt.gasUsed,
logs: _receipt.logs,
},
});
} catch (err) {
console.error(err);
setTxData(null);
}
};
readTxData();
}, [provider, txhash]);
return txData;
};
export const useTokenTransfers = (
txData: TransactionData
): TokenTransfer[] | undefined => {
const transfers = useMemo(() => {
if (!txData.confirmedData) {
return undefined;
}
return txData.confirmedData.logs
.filter((l) => l.topics.length === 3 && l.topics[0] === TRANSFER_TOPIC)
.map((l) => ({
token: l.address,
from: getAddress(hexDataSlice(arrayify(l.topics[1]), 12)),
to: getAddress(hexDataSlice(arrayify(l.topics[2]), 12)),
value: BigNumber.from(l.data),
}));
}, [txData]);
return transfers;
};
export const useInternalOperations = (
provider: JsonRpcProvider | undefined,
txHash: string | undefined
): InternalOperation[] | undefined => {
const { data, error } = useSWRImmutable(
provider !== undefined && txHash !== undefined
? ["ots_getInternalOperations", txHash]
: null,
providerFetcher(provider)
);
const _transfers = useMemo(() => {
if (provider === undefined || error || data === undefined) {
return undefined;
}
const _t: InternalOperation[] = [];
for (const t of data) {
_t.push({
type: t.type,
from: provider.formatter.address(getAddress(t.from)),
to: provider.formatter.address(getAddress(t.to)),
value: provider.formatter.bigNumber(t.value),
});
}
return _t;
}, [provider, data]);
return _transfers;
};
export const useSendsToMiner = (
provider: JsonRpcProvider | undefined,
txHash: string | undefined,
miner: string | undefined
): [boolean, InternalOperation[]] | [undefined, undefined] => {
const ops = useInternalOperations(provider, txHash);
if (ops === undefined) {
return [undefined, undefined];
}
const send =
ops.findIndex(
(op) =>
op.type === OperationType.TRANSFER &&
miner !== undefined &&
miner === getAddress(op.to)
) !== -1;
return [send, ops];
};
export type TraceEntry = {
type: string;
depth: number;
from: string;
to: string;
value: BigNumber;
input: string;
};
export type TraceGroup = TraceEntry & {
children: TraceGroup[] | null;
};
export const useTraceTransaction = (
provider: JsonRpcProvider | undefined,
txHash: string
): TraceGroup[] | undefined => {
const [traceGroups, setTraceGroups] = useState<TraceGroup[] | undefined>();
useEffect(() => {
if (!provider) {
setTraceGroups(undefined);
return;
}
const traceTx = async () => {
const results = await provider.send("ots_traceTransaction", [txHash]);
// Implement better formatter
for (let i = 0; i < results.length; i++) {
results[i].from = provider.formatter.address(results[i].from);
results[i].to = provider.formatter.address(results[i].to);
results[i].value =
results[i].value === null
? null
: provider.formatter.bigNumber(results[i].value);
}
// Build trace tree
const buildTraceTree = (
flatList: TraceEntry[],
depth: number = 0
): TraceGroup[] => {
const entries: TraceGroup[] = [];
let children: TraceEntry[] | null = null;
for (let i = 0; i < flatList.length; i++) {
if (flatList[i].depth === depth) {
if (children !== null) {
const childrenTree = buildTraceTree(children, depth + 1);
const prev = entries.pop();
if (prev) {
prev.children = childrenTree;
entries.push(prev);
}
}
entries.push({
...flatList[i],
children: null,
});
children = null;
} else {
if (children === null) {
children = [];
}
children.push(flatList[i]);
}
}
if (children !== null) {
const childrenTree = buildTraceTree(children, depth + 1);
const prev = entries.pop();
if (prev) {
prev.children = childrenTree;
entries.push(prev);
}
}
return entries;
};
const traceTree = buildTraceTree(results);
setTraceGroups(traceTree);
};
traceTx();
}, [provider, txHash]);
return traceGroups;
};
// Error(string)
const ERROR_MESSAGE_SELECTOR = "0x08c379a0";
export const useTransactionError = (
provider: JsonRpcProvider | undefined,
txHash: string
): [string | undefined, string | undefined, boolean | undefined] => {
const [errorMsg, setErrorMsg] = useState<string | undefined>();
const [data, setData] = useState<string | undefined>();
const [isCustomError, setCustomError] = useState<boolean | undefined>();
useEffect(() => {
// Reset
setErrorMsg(undefined);
setData(undefined);
setCustomError(undefined);
if (provider === undefined) {
return;
}
const readCodes = async () => {
const result = (await provider.send("ots_getTransactionError", [
txHash,
])) as string;
// Empty or success
if (result === "0x") {
setErrorMsg(undefined);
setData(result);
setCustomError(false);
return;
}
// Filter hardcoded Error(string) selector because ethers don't let us
// construct it
const selector = result.substr(0, 10);
if (selector === ERROR_MESSAGE_SELECTOR) {
const msg = defaultAbiCoder.decode(
["string"],
"0x" + result.substr(10)
);
setErrorMsg(msg[0]);
setData(result);
setCustomError(false);
return;
}
setErrorMsg(undefined);
setData(result);
setCustomError(true);
};
readCodes();
}, [provider, txHash]);
return [errorMsg, data, isCustomError];
};
export const useTransactionCount = (
provider: JsonRpcProvider | undefined,
sender: ChecksummedAddress | undefined
): number | undefined => {
const { data, error } = useSWR(
provider && sender ? { provider, sender } : null,
async ({ provider, sender }): Promise<number | undefined> =>
provider.getTransactionCount(sender)
);
if (error) {
return undefined;
}
return data;
};
type TransactionBySenderAndNonceKey = {
network: number;
sender: ChecksummedAddress;
nonce: number;
};
const getTransactionBySenderAndNonceFetcher =
(provider: JsonRpcProvider) =>
async ({
network,
sender,
nonce,
}: TransactionBySenderAndNonceKey): Promise<string | null | undefined> => {
if (nonce < 0) {
return undefined;
}
const result = (await provider.send("ots_getTransactionBySenderAndNonce", [
sender,
nonce,
])) as string;
// Empty or success
return result;
};
export const useTransactionBySenderAndNonce = (
provider: JsonRpcProvider | undefined,
sender: ChecksummedAddress | undefined,
nonce: number | undefined
): string | null | undefined => {
const { data, error } = useSWR<
string | null | undefined,
any,
TransactionBySenderAndNonceKey | null
>(
provider && sender && nonce !== undefined
? {
network: provider.network.chainId,
sender,
nonce,
}
: null,
getTransactionBySenderAndNonceFetcher(provider!)
);
if (error) {
return undefined;
}
return data;
};
type ContractCreatorKey = {
type: "cc";
network: number;
address: ChecksummedAddress;
};
type ContractCreator = {
hash: string;
creator: ChecksummedAddress;
};
export const useContractCreator = (
provider: JsonRpcProvider | undefined,
address: ChecksummedAddress | undefined
): ContractCreator | null | undefined => {
const { data, error } = useSWR<
ContractCreator | null | undefined,
any,
ContractCreatorKey | null
>(
provider && address
? {
type: "cc",
network: provider.network.chainId,
address,
}
: null,
getContractCreatorFetcher(provider!)
);
if (error) {
return undefined;
}
return data as ContractCreator;
};
const getContractCreatorFetcher =
(provider: JsonRpcProvider) =>
async ({
network,
address,
}: ContractCreatorKey): Promise<ContractCreator | null | undefined> => {
const result = (await provider.send("ots_getContractCreator", [
address,
])) as ContractCreator;
// Empty or success
if (result) {
result.creator = provider.formatter.address(result.creator);
}
return result;
};
export const useAddressBalance = (
provider: JsonRpcProvider | undefined,
address: ChecksummedAddress | undefined
): BigNumber | null | undefined => {
const [balance, setBalance] = useState<BigNumber | undefined>();
useEffect(() => {
if (!provider || !address) {
return undefined;
}
const readBalance = async () => {
const _balance = await provider.getBalance(address);
setBalance(_balance);
};
readBalance();
}, [provider, address]);
return balance;
};
/**
* This is a generic fetch for SWR, where the key is an array, whose
* element 0 is the JSON-RPC method, and the remaining are the method
* arguments.
*/
export const providerFetcher =
(provider: JsonRpcProvider | undefined) =>
async (...key: any[]): Promise<any | undefined> => {
if (provider === undefined) {
return undefined;
}
for (const a of key) {
if (a === undefined) {
return undefined;
}
}
const method = key[0];
const args = key.slice(1);
const result = await provider.send(method, args);
return result;
};
export const useHasCode = (
provider: JsonRpcProvider | undefined,
address: ChecksummedAddress | undefined,
blockTag: BlockTag = "latest"
): boolean | undefined => {
const fetcher = providerFetcher(provider);
const { data, error } = useSWRImmutable(
["ots_hasCode", address, blockTag],
fetcher
);
if (error) {
return undefined;
}
return data as boolean | undefined;
};
const ERC20_PROTOTYPE = new Contract(AddressZero, erc20);
const tokenMetadataFetcher =
(provider: JsonRpcProvider | undefined) =>
async (
_: "tokenmeta",
address: ChecksummedAddress
): Promise<TokenMeta | null> => {
if (provider === undefined) {
return null;
}
const erc20Contract = ERC20_PROTOTYPE.connect(provider).attach(address);
try {
const name = (await erc20Contract.name()) as string;
if (!name.trim()) {
return null;
}
const [symbol, decimals] = (await Promise.all([
erc20Contract.symbol(),
erc20Contract.decimals(),
])) as [string, number];
// Prevent faulty tokens with empty name/symbol
if (!symbol.trim()) {
return null;
}
return {
name,
symbol,
decimals,
};
} catch (err) {
// Ignore on purpose; this indicates the probe failed and the address
// is not a token
return null;
}
};
export const useTokenMetadata = (
provider: JsonRpcProvider | undefined,
address: ChecksummedAddress | undefined
): TokenMeta | null | undefined => {
const fetcher = tokenMetadataFetcher(provider);
const { data, error } = useSWRImmutable(
provider !== undefined && address !== undefined
? ["tokenmeta", address]
: null,
fetcher
);
if (error) {
return undefined;
}
return data;
};