import type {
BlockFilter,
Factory,
Filter,
InternalBlock,
InternalLog,
InternalTrace,
InternalTransaction,
LogFactory,
LogFilter,
RequiredBlockColumns,
RequiredLogColumns,
RequiredTraceColumns,
RequiredTransactionColumns,
RequiredTransactionReceiptColumns,
SyncBlock,
SyncBlockHeader,
SyncLog,
SyncTrace,
SyncTransaction,
TraceFilter,
TransactionFilter,
TransferFilter,
} from "@/internal/types.js";
import type {
Block,
Log,
Trace,
Transaction,
TransactionReceipt,
} from "@/types/eth.js";
import { toLowerCase } from "@/utils/lowercase.js";
import { type Address, hexToNumber } from "viem";
/** Returns true if `address` is an address filter. */
export const isAddressFactory = (
address: Address | Address[] | Factory | undefined | null,
): address is LogFactory => {
if (
address === undefined ||
address === null ||
typeof address === "string"
) {
return false;
}
return Array.isArray(address) ? isAddressFactory(address[0]) : true;
};
export const getChildAddress = ({
log,
factory,
}: { log: SyncLog; factory: Factory }): Address => {
if (factory.childAddressLocation.startsWith("offset")) {
const childAddressOffset = Number(
factory.childAddressLocation.substring(6),
);
const start = 2 + 12 * 2 + childAddressOffset * 2;
const length = 20 * 2;
return `0x${log.data.substring(start, start + length)}`;
} else {
const start = 2 + 12 * 2;
const length = 20 * 2;
const topicIndex =
factory.childAddressLocation === "topic1"
? 1
: factory.childAddressLocation === "topic2"
? 2
: 3;
return `0x${log.topics[topicIndex]!.substring(start, start + length)}`;
}
};
export const isAddressMatched = ({
address,
blockNumber,
childAddresses,
}: {
address: Address | undefined;
blockNumber: number;
childAddresses: Map
;
}) => {
if (address === undefined) return false;
if (
childAddresses.has(toLowerCase(address)) &&
childAddresses.get(toLowerCase(address))! <= blockNumber
) {
return true;
}
return false;
};
const isValueMatched = (
filterValue: T | T[] | null | undefined,
eventValue: T | undefined,
): boolean => {
// match all
if (filterValue === null || filterValue === undefined) return true;
// missing value
if (eventValue === undefined) return false;
// array
if (
Array.isArray(filterValue) &&
filterValue.some((v) => v === toLowerCase(eventValue))
) {
return true;
}
// single
if (filterValue === toLowerCase(eventValue)) return true;
return false;
};
/**
* Returns `true` if `log` matches `factory`
*/
export const isLogFactoryMatched = ({
factory,
log,
}: { factory: LogFactory; log: InternalLog | SyncLog }): boolean => {
if (factory.address !== undefined) {
const addresses = Array.isArray(factory.address)
? factory.address
: [factory.address];
if (addresses.every((address) => address !== toLowerCase(log.address))) {
return false;
}
}
if (log.topics.length === 0) return false;
if (factory.eventSelector !== toLowerCase(log.topics[0]!)) return false;
if (
factory.fromBlock !== undefined &&
(typeof log.blockNumber === "number"
? factory.fromBlock > log.blockNumber
: factory.fromBlock > hexToNumber(log.blockNumber))
)
return false;
if (
factory.toBlock !== undefined &&
(typeof log.blockNumber === "number"
? factory.toBlock < log.blockNumber
: factory.toBlock < hexToNumber(log.blockNumber))
)
return false;
return true;
};
/**
* Returns `true` if `log` matches `filter`
*/
export const isLogFilterMatched = ({
filter,
log,
}: {
filter: LogFilter;
log: InternalLog | SyncLog;
}): boolean => {
// Return `false` for out of range blocks
if (
Number(log.blockNumber) < (filter.fromBlock ?? 0) ||
Number(log.blockNumber) > (filter.toBlock ?? Number.POSITIVE_INFINITY)
) {
return false;
}
if (isValueMatched(filter.topic0, log.topics[0]) === false) return false;
if (isValueMatched(filter.topic1, log.topics[1]) === false) return false;
if (isValueMatched(filter.topic2, log.topics[2]) === false) return false;
if (isValueMatched(filter.topic3, log.topics[3]) === false) return false;
if (
isAddressFactory(filter.address) === false &&
isValueMatched(
filter.address as Address | Address[] | undefined,
log.address,
) === false
) {
return false;
}
return true;
};
/**
* Returns `true` if `transaction` matches `filter`
*/
export const isTransactionFilterMatched = ({
filter,
transaction,
}: {
filter: TransactionFilter;
transaction: InternalTransaction | SyncTransaction;
}): boolean => {
// Return `false` for out of range blocks
if (
Number(transaction.blockNumber) < (filter.fromBlock ?? 0) ||
Number(transaction.blockNumber) >
(filter.toBlock ?? Number.POSITIVE_INFINITY)
) {
return false;
}
if (
isAddressFactory(filter.fromAddress) === false &&
isValueMatched(
filter.fromAddress as Address | Address[] | undefined,
transaction.from,
) === false
) {
return false;
}
if (
isAddressFactory(filter.toAddress) === false &&
isValueMatched(
filter.toAddress as Address | Address[] | undefined,
transaction.to ?? undefined,
) === false
) {
return false;
}
// NOTE: `filter.includeReverted` is intentionally ignored
return true;
};
/**
* Returns `true` if `trace` matches `filter`
*/
export const isTraceFilterMatched = ({
filter,
trace,
block,
}: {
filter: TraceFilter;
trace: InternalTrace | SyncTrace["trace"];
block: Pick;
}): boolean => {
// Return `false` for out of range blocks
if (
Number(block.number) < (filter.fromBlock ?? 0) ||
Number(block.number) > (filter.toBlock ?? Number.POSITIVE_INFINITY)
) {
return false;
}
if (
isAddressFactory(filter.fromAddress) === false &&
isValueMatched(
filter.fromAddress as Address | Address[] | undefined,
trace.from,
) === false
) {
return false;
}
if (
isAddressFactory(filter.toAddress) === false &&
isValueMatched(
filter.toAddress as Address | Address[] | undefined,
trace.to ?? undefined,
) === false
) {
return false;
}
if (
isValueMatched(filter.functionSelector, trace.input.slice(0, 10)) === false
) {
return false;
}
// NOTE: `filter.callType` and `filter.includeReverted` is intentionally ignored
return true;
};
/**
* Returns `true` if `trace` matches `filter`
*/
export const isTransferFilterMatched = ({
filter,
trace,
block,
}: {
filter: TransferFilter;
trace: InternalTrace | SyncTrace["trace"];
block: Pick;
}): boolean => {
// Return `false` for out of range blocks
if (
Number(block.number) < (filter.fromBlock ?? 0) ||
Number(block.number) > (filter.toBlock ?? Number.POSITIVE_INFINITY)
) {
return false;
}
if (
trace.value === undefined ||
trace.value === null ||
BigInt(trace.value) === 0n
) {
return false;
}
if (
isAddressFactory(filter.fromAddress) === false &&
isValueMatched(
filter.fromAddress as Address | Address[] | undefined,
trace.from,
) === false
) {
return false;
}
if (
isAddressFactory(filter.toAddress) === false &&
isValueMatched(
filter.toAddress as Address | Address[] | undefined,
trace.to ?? undefined,
) === false
) {
return false;
}
// NOTE: `filter.includeReverted` is intentionally ignored
return true;
};
/**
* Returns `true` if `block` matches `filter`
*/
export const isBlockFilterMatched = ({
filter,
block,
}: {
filter: BlockFilter;
block: Pick;
}): boolean => {
// Return `false` for out of range blocks
if (
Number(block.number) < (filter.fromBlock ?? 0) ||
Number(block.number) > (filter.toBlock ?? Number.POSITIVE_INFINITY)
) {
return false;
}
return (Number(block.number) - filter.offset) % filter.interval === 0;
};
export const getFilterFactories = (filter: Filter): Factory[] => {
const factories: Factory[] = [];
switch (filter.type) {
case "log":
if (isAddressFactory(filter.address)) {
factories.push(filter.address);
}
break;
case "trace":
case "transfer":
case "transaction": {
if (isAddressFactory(filter.fromAddress)) {
factories.push(filter.fromAddress);
}
if (isAddressFactory(filter.toAddress)) {
factories.push(filter.toAddress);
}
break;
}
}
return factories;
};
export const getFilterFromBlock = (filter: Filter): number => {
const blocks: number[] = [filter.fromBlock ?? 0];
switch (filter.type) {
case "log":
if (isAddressFactory(filter.address)) {
blocks.push(filter.address.fromBlock ?? 0);
}
break;
case "transaction":
case "trace":
case "transfer":
if (isAddressFactory(filter.fromAddress)) {
blocks.push(filter.fromAddress.fromBlock ?? 0);
}
if (isAddressFactory(filter.toAddress)) {
blocks.push(filter.toAddress.fromBlock ?? 0);
}
}
return Math.min(...blocks);
};
export const getFilterToBlock = (filter: Filter): number => {
const blocks: number[] = [filter.toBlock ?? Number.POSITIVE_INFINITY];
// Note: factories cannot have toBlock > `filter.toBlock`
switch (filter.type) {
case "log":
if (isAddressFactory(filter.address)) {
blocks.push(filter.address.toBlock ?? Number.POSITIVE_INFINITY);
}
break;
case "transaction":
case "trace":
case "transfer":
if (isAddressFactory(filter.fromAddress)) {
blocks.push(filter.fromAddress.toBlock ?? Number.POSITIVE_INFINITY);
}
if (isAddressFactory(filter.toAddress)) {
blocks.push(filter.toAddress.toBlock ?? Number.POSITIVE_INFINITY);
}
}
return Math.max(...blocks);
};
export const isBlockInFilter = (filter: Filter, blockNumber: number) => {
// Return `false` for out of range blocks
if (
blockNumber < (filter.fromBlock ?? 0) ||
blockNumber > (filter.toBlock ?? Number.POSITIVE_INFINITY)
) {
return false;
}
return true;
};
export const defaultBlockInclude: (keyof Block)[] = [
"baseFeePerGas",
"difficulty",
"extraData",
"gasLimit",
"gasUsed",
"hash",
"logsBloom",
"miner",
"mixHash",
"totalDifficulty",
"nonce",
"number",
"parentHash",
"receiptsRoot",
"sha3Uncles",
"size",
"stateRoot",
"timestamp",
"transactionsRoot",
];
export const requiredBlockInclude: RequiredBlockColumns[] = [
"timestamp",
"number",
"hash",
];
export const defaultTransactionInclude: (keyof Transaction)[] = [
"from",
"gas",
"hash",
"input",
"nonce",
"r",
"s",
"to",
"transactionIndex",
"v",
"value",
"type",
"gasPrice",
"accessList",
"maxFeePerGas",
"maxPriorityFeePerGas",
];
export const requiredTransactionInclude: RequiredTransactionColumns[] = [
"transactionIndex",
"from",
"to",
"hash",
"type",
];
export const defaultTransactionReceiptInclude: (keyof TransactionReceipt)[] = [
"contractAddress",
"cumulativeGasUsed",
"effectiveGasPrice",
"from",
"gasUsed",
"logsBloom",
"status",
"to",
"type",
];
export const requiredTransactionReceiptInclude: RequiredTransactionReceiptColumns[] =
["status", "from", "to"];
export const defaultTraceInclude: (keyof Trace)[] = [
"traceIndex",
"type",
"from",
"to",
"gas",
"gasUsed",
"input",
"output",
"error",
"revertReason",
"value",
"subcalls",
];
export const requiredTraceInclude: RequiredTraceColumns[] = [
"traceIndex",
"type",
"from",
"to",
"input",
"output",
"error",
"value",
];
export const defaultLogInclude: (keyof Log)[] = [
"address",
"data",
"logIndex",
"removed",
"topics",
];
export const requiredLogInclude: RequiredLogColumns[] = defaultLogInclude;
export const defaultBlockFilterInclude: BlockFilter["include"] =
defaultBlockInclude.map((value) => `block.${value}` as const);
export const requiredBlockFilterInclude: BlockFilter["include"] =
requiredBlockInclude.map((value) => `block.${value}` as const);
export const defaultLogFilterInclude: LogFilter["include"] = [
...defaultLogInclude.map((value) => `log.${value}` as const),
...defaultTransactionInclude.map((value) => `transaction.${value}` as const),
...defaultBlockInclude.map((value) => `block.${value}` as const),
];
export const requiredLogFilterInclude: LogFilter["include"] = [
...requiredLogInclude.map((value) => `log.${value}` as const),
...requiredTransactionInclude.map((value) => `transaction.${value}` as const),
...requiredBlockInclude.map((value) => `block.${value}` as const),
];
export const defaultTransactionFilterInclude: TransactionFilter["include"] = [
...defaultTransactionInclude.map((value) => `transaction.${value}` as const),
...defaultTransactionReceiptInclude.map(
(value) => `transactionReceipt.${value}` as const,
),
...defaultBlockInclude.map((value) => `block.${value}` as const),
];
export const requiredTransactionFilterInclude: TransactionFilter["include"] = [
...requiredTransactionInclude.map((value) => `transaction.${value}` as const),
...requiredTransactionReceiptInclude.map(
(value) => `transactionReceipt.${value}` as const,
),
...requiredBlockInclude.map((value) => `block.${value}` as const),
];
export const defaultTraceFilterInclude: TraceFilter["include"] = [
...defaultBlockInclude.map((value) => `block.${value}` as const),
...defaultTransactionInclude.map((value) => `transaction.${value}` as const),
...defaultTraceInclude.map((value) => `trace.${value}` as const),
];
export const requiredTraceFilterInclude: TraceFilter["include"] = [
...requiredBlockInclude.map((value) => `block.${value}` as const),
...requiredTransactionInclude.map((value) => `transaction.${value}` as const),
...requiredTraceInclude.map((value) => `trace.${value}` as const),
];
export const defaultTransferFilterInclude: TransferFilter["include"] = [
...defaultBlockInclude.map((value) => `block.${value}` as const),
...defaultTransactionInclude.map((value) => `transaction.${value}` as const),
...defaultTraceInclude.map((value) => `trace.${value}` as const),
];
export const requiredTransferFilterInclude: TransferFilter["include"] = [
...requiredBlockInclude.map((value) => `block.${value}` as const),
...requiredTransactionInclude.map((value) => `transaction.${value}` as const),
...requiredTraceInclude.map((value) => `trace.${value}` as const),
];
export const unionFilterIncludeBlock = (filters: Filter[]): (keyof Block)[] => {
const includeBlock = new Set();
for (const filter of filters) {
for (const include of filter.include) {
const [data, column] = include.split(".") as [string, keyof Block];
if (data === "block") {
includeBlock.add(column);
}
}
}
return Array.from(includeBlock);
};
export const unionFilterIncludeTransaction = (
filters: Filter[],
): (keyof Transaction)[] => {
const includeTransaction = new Set();
for (const filter of filters) {
for (const include of filter.include) {
const [data, column] = include.split(".") as [string, keyof Transaction];
if (data === "transaction") {
includeTransaction.add(column);
}
}
}
return Array.from(includeTransaction);
};
export const unionFilterIncludeTransactionReceipt = (
filters: Filter[],
): (keyof TransactionReceipt)[] => {
const includeTransactionReceipt = new Set();
for (const filter of filters) {
for (const include of filter.include) {
const [data, column] = include.split(".") as [
string,
keyof TransactionReceipt,
];
if (data === "transactionReceipt") {
includeTransactionReceipt.add(column);
}
}
}
return Array.from(includeTransactionReceipt);
};
export const unionFilterIncludeTrace = (filters: Filter[]): (keyof Trace)[] => {
const includeTrace = new Set();
for (const filter of filters) {
for (const include of filter.include) {
const [data, column] = include.split(".") as [string, keyof Trace];
if (data === "trace") {
includeTrace.add(column);
}
}
}
return Array.from(includeTrace);
};