705 lines
18 KiB
TypeScript
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;
|
|
};
|