2023-11-30 07:05:43 -08:00
|
|
|
import { EvmLog } from "../../entities";
|
|
|
|
import { RunPollingJob } from "../RunPollingJob";
|
2023-12-04 04:47:02 -08:00
|
|
|
import { GetEvmLogs } from "./GetEvmLogs";
|
2023-11-30 07:05:43 -08:00
|
|
|
import { EvmBlockRepository, MetadataRepository, StatRepository } from "../../repositories";
|
2023-11-28 11:00:45 -08:00
|
|
|
import winston from "winston";
|
|
|
|
|
|
|
|
const ID = "watch-evm-logs";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* PollEvmLogs is an action that watches for new blocks and extracts logs from them.
|
|
|
|
*/
|
|
|
|
export class PollEvmLogs extends RunPollingJob {
|
|
|
|
protected readonly logger: winston.Logger;
|
|
|
|
|
|
|
|
private readonly blockRepo: EvmBlockRepository;
|
|
|
|
private readonly metadataRepo: MetadataRepository<PollEvmLogsMetadata>;
|
|
|
|
private readonly statsRepository: StatRepository;
|
2023-12-04 04:47:02 -08:00
|
|
|
private readonly getEvmLogs: GetEvmLogs;
|
2023-11-28 11:00:45 -08:00
|
|
|
private cfg: PollEvmLogsConfig;
|
|
|
|
|
|
|
|
private latestBlockHeight?: bigint;
|
|
|
|
private blockHeightCursor?: bigint;
|
|
|
|
private lastRange?: { fromBlock: bigint; toBlock: bigint };
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
blockRepo: EvmBlockRepository,
|
|
|
|
metadataRepo: MetadataRepository<PollEvmLogsMetadata>,
|
|
|
|
statsRepository: StatRepository,
|
|
|
|
cfg: PollEvmLogsConfig
|
|
|
|
) {
|
2023-11-30 07:05:43 -08:00
|
|
|
super(cfg.interval ?? 1_000, cfg.id, statsRepository);
|
2023-11-28 11:00:45 -08:00
|
|
|
this.blockRepo = blockRepo;
|
|
|
|
this.metadataRepo = metadataRepo;
|
|
|
|
this.statsRepository = statsRepository;
|
|
|
|
this.cfg = cfg;
|
2023-12-04 04:47:02 -08:00
|
|
|
this.getEvmLogs = new GetEvmLogs(blockRepo);
|
2023-11-28 11:00:45 -08:00
|
|
|
this.logger = winston.child({ module: "PollEvmLogs", label: this.cfg.id });
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async preHook(): Promise<void> {
|
|
|
|
const metadata = await this.metadataRepo.get(this.cfg.id);
|
|
|
|
if (metadata) {
|
|
|
|
this.blockHeightCursor = BigInt(metadata.lastBlock);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async hasNext(): Promise<boolean> {
|
|
|
|
const hasFinished = this.cfg.hasFinished(this.blockHeightCursor);
|
|
|
|
if (hasFinished) {
|
|
|
|
this.logger.info(
|
2023-11-30 07:05:43 -08:00
|
|
|
`[hasNext] PollEvmLogs: (${this.cfg.id}) Finished processing all blocks from ${this.cfg.fromBlock} to ${this.cfg.toBlock}`
|
2023-11-28 11:00:45 -08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return !hasFinished;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async get(): Promise<EvmLog[]> {
|
|
|
|
this.report();
|
|
|
|
|
2023-12-04 04:47:02 -08:00
|
|
|
this.latestBlockHeight = await this.blockRepo.getBlockHeight(
|
|
|
|
this.cfg.chain,
|
|
|
|
this.cfg.getCommitment()
|
|
|
|
);
|
2023-11-28 11:00:45 -08:00
|
|
|
|
|
|
|
const range = this.getBlockRange(this.latestBlockHeight);
|
|
|
|
|
2023-12-04 04:47:02 -08:00
|
|
|
const logs = await this.getEvmLogs.execute(range, {
|
|
|
|
chain: this.cfg.chain,
|
|
|
|
addresses: this.cfg.addresses,
|
|
|
|
topics: this.cfg.topics,
|
2023-11-28 11:00:45 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
this.lastRange = range;
|
|
|
|
|
|
|
|
return logs;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async persist(): Promise<void> {
|
|
|
|
this.blockHeightCursor = this.lastRange?.toBlock ?? this.blockHeightCursor;
|
|
|
|
if (this.blockHeightCursor) {
|
|
|
|
await this.metadataRepo.save(this.cfg.id, { lastBlock: this.blockHeightCursor });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the block range to extract.
|
|
|
|
* @param latestBlockHeight - the latest known height of the chain
|
|
|
|
* @returns an always valid range, in the sense from is always <= to
|
|
|
|
*/
|
|
|
|
private getBlockRange(latestBlockHeight: bigint): {
|
|
|
|
fromBlock: bigint;
|
|
|
|
toBlock: bigint;
|
|
|
|
} {
|
|
|
|
let fromBlock = this.blockHeightCursor
|
|
|
|
? this.blockHeightCursor + 1n
|
|
|
|
: this.cfg.fromBlock ?? latestBlockHeight;
|
|
|
|
// fromBlock is configured and is greater than current block height, then we allow to skip blocks.
|
|
|
|
if (
|
|
|
|
this.blockHeightCursor &&
|
|
|
|
this.cfg.fromBlock &&
|
|
|
|
this.cfg.fromBlock > this.blockHeightCursor
|
|
|
|
) {
|
|
|
|
fromBlock = this.cfg.fromBlock;
|
|
|
|
}
|
|
|
|
|
|
|
|
let toBlock = BigInt(fromBlock) + BigInt(this.cfg.getBlockBatchSize());
|
|
|
|
// limit toBlock to obtained block height
|
|
|
|
if (toBlock > fromBlock && toBlock > latestBlockHeight) {
|
|
|
|
toBlock = latestBlockHeight;
|
|
|
|
}
|
|
|
|
// limit toBlock to configured toBlock
|
|
|
|
if (this.cfg.toBlock && toBlock > this.cfg.toBlock) {
|
|
|
|
toBlock = this.cfg.toBlock;
|
|
|
|
}
|
|
|
|
|
2023-12-12 11:55:09 -08:00
|
|
|
return { fromBlock: BigInt(fromBlock), toBlock: BigInt(toBlock) };
|
2023-11-28 11:00:45 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
private report(): void {
|
|
|
|
const labels = {
|
|
|
|
job: this.cfg.id,
|
|
|
|
chain: this.cfg.chain ?? "",
|
|
|
|
commitment: this.cfg.getCommitment(),
|
|
|
|
};
|
|
|
|
this.statsRepository.count("job_execution", labels);
|
2023-12-12 11:55:09 -08:00
|
|
|
this.statsRepository.measure("polling_cursor", this.latestBlockHeight ?? 0n, {
|
|
|
|
...labels,
|
|
|
|
type: "max",
|
|
|
|
});
|
|
|
|
this.statsRepository.measure("polling_cursor", this.blockHeightCursor ?? 0n, {
|
|
|
|
...labels,
|
|
|
|
type: "current",
|
|
|
|
});
|
2023-11-28 11:00:45 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export type PollEvmLogsMetadata = {
|
|
|
|
lastBlock: bigint;
|
|
|
|
};
|
|
|
|
|
|
|
|
export interface PollEvmLogsConfigProps {
|
|
|
|
fromBlock?: bigint;
|
|
|
|
toBlock?: bigint;
|
|
|
|
blockBatchSize?: number;
|
|
|
|
commitment?: string;
|
|
|
|
interval?: number;
|
|
|
|
addresses: string[];
|
|
|
|
topics: string[];
|
|
|
|
id?: string;
|
2023-12-04 04:47:02 -08:00
|
|
|
chain: string;
|
2023-11-28 11:00:45 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
export class PollEvmLogsConfig {
|
|
|
|
private props: PollEvmLogsConfigProps;
|
|
|
|
|
2023-12-04 04:47:02 -08:00
|
|
|
constructor(props: PollEvmLogsConfigProps) {
|
2023-11-28 11:00:45 -08:00
|
|
|
if (props.fromBlock && props.toBlock && props.fromBlock > props.toBlock) {
|
|
|
|
throw new Error("fromBlock must be less than or equal to toBlock");
|
|
|
|
}
|
|
|
|
|
|
|
|
this.props = props;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getBlockBatchSize() {
|
|
|
|
return this.props.blockBatchSize ?? 100;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getCommitment() {
|
|
|
|
return this.props.commitment ?? "latest";
|
|
|
|
}
|
|
|
|
|
|
|
|
public hasFinished(currentFromBlock?: bigint): boolean {
|
|
|
|
return (
|
|
|
|
currentFromBlock != undefined &&
|
|
|
|
this.props.toBlock != undefined &&
|
|
|
|
currentFromBlock >= this.props.toBlock
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public get fromBlock() {
|
|
|
|
return this.props.fromBlock ? BigInt(this.props.fromBlock) : undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
public setFromBlock(fromBlock: bigint | undefined) {
|
|
|
|
this.props.fromBlock = fromBlock;
|
|
|
|
}
|
|
|
|
|
|
|
|
public get toBlock() {
|
|
|
|
return this.props.toBlock;
|
|
|
|
}
|
|
|
|
|
|
|
|
public get interval() {
|
|
|
|
return this.props.interval;
|
|
|
|
}
|
|
|
|
|
|
|
|
public get addresses() {
|
|
|
|
return this.props.addresses;
|
|
|
|
}
|
|
|
|
|
|
|
|
public get topics() {
|
|
|
|
return this.props.topics;
|
|
|
|
}
|
|
|
|
|
|
|
|
public get id() {
|
|
|
|
return this.props.id ?? ID;
|
|
|
|
}
|
|
|
|
|
|
|
|
public get chain() {
|
|
|
|
return this.props.chain;
|
|
|
|
}
|
|
|
|
|
2023-12-04 04:47:02 -08:00
|
|
|
static fromBlock(chain: string, fromBlock: bigint) {
|
|
|
|
return new PollEvmLogsConfig({ chain, fromBlock, addresses: [], topics: [] });
|
2023-11-28 11:00:45 -08:00
|
|
|
}
|
|
|
|
}
|