wormhole-explorer/blockchain-watcher/src/infrastructure/repositories/evm/ArbitrumEvmJsonRPCBlockRepo...

164 lines
5.1 KiB
TypeScript

import { EvmTag } from "../../../domain/entities";
import { MetadataRepository } from "../../../domain/repositories";
import { HttpClientError } from "../../errors/HttpClientError";
import winston from "../../log";
import {
EvmJsonRPCBlockRepository,
EvmJsonRPCBlockRepositoryCfg,
ProviderPoolMap,
} from "./EvmJsonRPCBlockRepository";
const FINALIZED = "finalized";
const ETHEREUM = "ethereum";
export class ArbitrumEvmJsonRPCBlockRepository extends EvmJsonRPCBlockRepository {
override readonly logger = winston.child({ module: "ArbitrumEvmJsonRPCBlockRepository" });
private latestL2Finalized: number;
private metadataRepo: MetadataRepository<PersistedBlock[]>;
private latestEthTime: number;
constructor(
cfg: EvmJsonRPCBlockRepositoryCfg,
pools: ProviderPoolMap,
metadataRepo: MetadataRepository<any>
) {
super(cfg, pools);
this.metadataRepo = metadataRepo;
this.latestL2Finalized = 0;
this.latestEthTime = 0;
}
async getBlockHeight(chain: string, finality: EvmTag): Promise<bigint> {
const metadataFileName = `arbitrum-${finality}`;
const chainCfg = this.getCurrentChain(chain);
let response: { result: BlockByNumberResult };
try {
// This gets the latest L2 block so we can get the associated L1 block number
response = await this.getChainProvider(chain).post<typeof response>(
chain,
{
jsonrpc: "2.0",
id: 1,
method: "eth_getBlockByNumber",
params: [finality, false],
},
{ timeout: chainCfg.timeout, retries: chainCfg.retries }
);
} catch (e: HttpClientError | any) {
this.handleError(chain, e, "getBlockHeight", "eth_getBlockByNumber");
throw e;
}
const l2Logs = response.result;
const l1BlockNumber = l2Logs.l1BlockNumber;
const l2Number = l2Logs.number;
if (!l2Logs || !l1BlockNumber || !l2Number)
throw new Error(`[getBlockHeight] Unable to parse result for latest block on ${chain}`);
const associatedL1Block: number = parseInt(l1BlockNumber, 16);
const l2BlockNumber: number = parseInt(l2Number, 16);
const persistedBlocks: PersistedBlock[] = (await this.metadataRepo.get(metadataFileName)) ?? [];
const auxPersistedBlocks = this.removeDuplicates(persistedBlocks);
// Only update the persisted block list, if the L2 block number is newer
this.saveAssociatedL1Block(auxPersistedBlocks, associatedL1Block, l2BlockNumber);
// Only check every 30 seconds
const now = Date.now();
if (now - this.latestEthTime < 30_000) {
return BigInt(this.latestL2Finalized);
}
this.latestEthTime = now;
// Get the latest finalized L1 block number
const latestL1BlockNumber: bigint = await super.getBlockHeight(ETHEREUM, FINALIZED);
// Search in the persisted list looking for finalized L2 block number
this.searchFinalizedBlock(auxPersistedBlocks, latestL1BlockNumber);
await this.metadataRepo.save(metadataFileName, [...auxPersistedBlocks]);
this.logger.info(
`[${chain}] Blocks status: [PersistedBlocksLength: ${auxPersistedBlocks?.length}][LatestL2Finalized: ${this.latestL2Finalized}]`
);
const latestL2FinalizedToBigInt = this.latestL2Finalized;
return BigInt(latestL2FinalizedToBigInt);
}
private removeDuplicates(persistedBlocks: PersistedBlock[]): PersistedBlock[] {
const uniqueObjects = new Set();
return persistedBlocks?.filter((obj) => {
const key = JSON.stringify(obj);
return !uniqueObjects.has(key) && uniqueObjects.add(key);
});
}
private saveAssociatedL1Block(
auxPersistedBlocks: PersistedBlock[],
associatedL1Block: number,
l2BlockNumber: number
): void {
const findAssociatedL1Block = auxPersistedBlocks.find(
(block) => block.associatedL1Block == associatedL1Block
)?.associatedL1Block;
if (!findAssociatedL1Block || findAssociatedL1Block < l2BlockNumber) {
auxPersistedBlocks.push({ associatedL1Block, l2BlockNumber });
}
}
private searchFinalizedBlock(
auxPersistedBlocks: PersistedBlock[],
latestL1BlockNumber: bigint
): void {
const latestL1BlockNumberToNumber = Number(latestL1BlockNumber);
for (let index = auxPersistedBlocks.length - 1; index >= 0; index--) {
const associatedL1Block = auxPersistedBlocks[index].associatedL1Block;
if (associatedL1Block <= latestL1BlockNumberToNumber) {
const l2BlockNumber = auxPersistedBlocks[index].l2BlockNumber;
this.latestL2Finalized = l2BlockNumber;
auxPersistedBlocks.splice(index, 1);
}
}
}
}
type PersistedBlock = {
associatedL1Block: number;
l2BlockNumber: number;
};
type BlockByNumberResult = {
baseFeePerGas: string;
difficulty: string;
extraData: string;
gasLimit: string;
gasUsed: string;
hash: string;
l1BlockNumber: string;
logsBloom: string;
miner: string;
mixHash: string;
nonce: string;
number: string;
parentHash: string;
receiptsRoot: string;
sendCount: string;
sendRoot: string;
sha3Uncles: string;
size: string;
stateRoot: string;
timestamp: string;
totalDifficulty: string;
transactions: string[];
transactionsRoot: string;
uncles: string[];
};