feature-826/enable-evm-arbitrum-chain-

This commit is contained in:
JulianMerlo 2023-12-11 16:43:02 -03:00
parent cff86e4469
commit 5cc39fc2a4
9 changed files with 480 additions and 9 deletions

View File

@ -91,6 +91,13 @@
"rpcs": ["https://alfajores-forno.celo-testnet.org"],
"timeout": 10000
},
"arbitrum": {
"name": "arbitrum",
"network": "goerli",
"chainId": 23,
"rpcs": ["https://goerli-rollup.arbitrum.io/rpc"],
"timeout": 10000
},
"optimism": {
"name": "optimism",
"network": "goerli",

View File

@ -40,6 +40,10 @@
"network": "mainnet",
"rpcs": ["https://forno.celo.org"]
},
"arbitrum": {
"network": "mainnet",
"rpcs": ["https://rpc.ankr.com/arbitrum"]
},
"optimism": {
"network": "mainnet",
"rpcs": ["https://rpc.ankr.com/optimism"]

View File

@ -29,6 +29,7 @@ const EVM_CHAINS = new Map([
["optimism", "evmRepo"],
["base", "evmRepo"],
["bsc", "bsc-evmRepo"],
["arbitrum", "arbitrum-evmRepo"],
]);
export class RepositoriesBuilder {
@ -69,6 +70,10 @@ export class RepositoriesBuilder {
};
this.repositories.set("evmRepo", new EvmJsonRPCBlockRepository(repoCfg, httpClient));
this.repositories.set("bsc-evmRepo", new BscEvmJsonRPCBlockRepository(repoCfg, httpClient));
this.repositories.set(
"arbitrum-evmRepo",
new ArbitrumEvmJsonRPCBlockRepository(repoCfg, httpClient, this.getMetadataRepository())
);
}
});

View File

@ -0,0 +1,153 @@
import { FileMetadataRepository } from "../FileMetadataRepository";
import { MetadataRepository } from "../../../domain/repositories";
import { HttpClientError } from "../../errors/HttpClientError";
import { HttpClient } from "../../rpc/http/HttpClient";
import { EvmTag } from "../../../domain/entities";
import winston from "../../log";
import {
EvmJsonRPCBlockRepository,
EvmJsonRPCBlockRepositoryCfg,
} from "./EvmJsonRPCBlockRepository";
const FINALIZED = "finalized";
const ETHEREUM = "ethereum";
export class ArbitrumEvmJsonRPCBlockRepository extends EvmJsonRPCBlockRepository {
override readonly logger = winston.child({ module: "ArbitrumEvmJsonRPCBlockRepository" });
private metadataRepo: MetadataRepository<PersistedBlock[]>;
private latestL2Finalized = 0;
constructor(
cfg: EvmJsonRPCBlockRepositoryCfg,
httpClient: HttpClient,
metadataRepo: MetadataRepository<any>
) {
super(cfg, httpClient);
this.metadataRepo = metadataRepo;
}
async getBlockHeight(chain: string, finality: EvmTag): Promise<bigint> {
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.httpClient.post<typeof response>(
chainCfg.rpc.href,
{
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[] | undefined = await this.metadataRepo.get(
`arbitrum-${finality}`
);
const auxPersistedBlocks = this.removeDuplicates(persistedBlocks);
// Only update the persisted block list, if the L2 block number is newer
this.saveAssociatedL1Block(auxPersistedBlocks, associatedL1Block, l2BlockNumber);
// 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(`arbitrum-${finality}`, [...auxPersistedBlocks]);
const latestL2FinalizedToBigInt = this.latestL2Finalized;
return BigInt(latestL2FinalizedToBigInt);
}
private removeDuplicates(persistedBlocks: PersistedBlock[] | undefined): 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[];
};

View File

@ -13,9 +13,9 @@ import { ChainRPCConfig } from "../../config";
const HEXADECIMAL_PREFIX = "0x";
export class EvmJsonRPCBlockRepository implements EvmBlockRepository {
private httpClient: HttpClient;
protected httpClient: HttpClient;
private cfg: EvmJsonRPCBlockRepositoryCfg;
private readonly logger;
protected readonly logger;
constructor(cfg: EvmJsonRPCBlockRepositoryCfg, httpClient: HttpClient) {
this.httpClient = httpClient;
@ -59,7 +59,7 @@ export class EvmJsonRPCBlockRepository implements EvmBlockRepository {
retries: chainCfg.retries,
});
} catch (e: HttpClientError | any) {
this.handleError(chain, e, "eth_getBlockByNumber");
this.handleError(chain, e, "getBlocks", "eth_getBlockByNumber");
throw e;
}
@ -145,7 +145,7 @@ export class EvmJsonRPCBlockRepository implements EvmBlockRepository {
{ timeout: chainCfg.timeout, retries: chainCfg.retries }
);
} catch (e: HttpClientError | any) {
this.handleError(chain, e, "eth_getLogs");
this.handleError(chain, e, "getFilteredLogs", "eth_getLogs");
throw e;
}
@ -188,7 +188,7 @@ export class EvmJsonRPCBlockRepository implements EvmBlockRepository {
{ timeout: chainCfg.timeout, retries: chainCfg.retries }
);
} catch (e: HttpClientError | any) {
this.handleError(chain, e, "eth_getBlockByNumber");
this.handleError(chain, e, "getBlock", "eth_getBlockByNumber");
throw e;
}
@ -207,22 +207,22 @@ export class EvmJsonRPCBlockRepository implements EvmBlockRepository {
);
}
private handleError(chain: string, e: any, method: string) {
protected handleError(chain: string, e: any, method: string, apiMethod: string) {
const chainCfg = this.getCurrentChain(chain);
if (e instanceof HttpClientError) {
this.logger.error(
`[${chain}][getBlock] Got ${e.status} from ${chainCfg.rpc.hostname}/${method}. ${
`[${chain}][${method}] Got ${e.status} from ${chainCfg.rpc.hostname}/${apiMethod}. ${
e?.message ?? `${e?.message}`
}`
);
} else {
this.logger.error(
`[${chain}][getBlock] Got error ${e} from ${chainCfg.rpc.hostname}/${method}`
`[${chain}][${method}] Got error ${e} from ${chainCfg.rpc.hostname}/${apiMethod}`
);
}
}
private getCurrentChain(chain: string) {
protected getCurrentChain(chain: string) {
const cfg = this.cfg.chains[chain];
return {
chainId: cfg.chainId,

View File

@ -11,6 +11,7 @@ export * from "./FileMetadataRepository";
export * from "./SnsEventRepository";
export * from "./evm/EvmJsonRPCBlockRepository";
export * from "./evm/BscEvmJsonRPCBlockRepository";
export * from "./evm/ArbitrumEvmJsonRPCBlockRepository";
export * from "./PromStatRepository";
export * from "./StaticJobRepository";
export * from "./solana/Web3SolanaSlotRepository";

View File

@ -0,0 +1,176 @@
import { describe, it, expect, afterEach, afterAll, jest } from "@jest/globals";
import { ArbitrumEvmJsonRPCBlockRepository } from "../../../src/infrastructure/repositories";
import { MetadataRepository } from "../../../src/domain/repositories";
import { HttpClient } from "../../../src/infrastructure/rpc/http/HttpClient";
import { EvmTag } from "../../../src/domain/entities/evm";
import axios from "axios";
import nock from "nock";
import fs from "fs";
const dirPath = "./metadata-repo";
axios.defaults.adapter = "http"; // needed by nock
const ethereum = "ethereum";
const arbitrum = "arbitrum";
const rpc = "http://localhost";
let repo: ArbitrumEvmJsonRPCBlockRepository;
let metadataSaveSpy: jest.SpiedFunction<MetadataRepository<PersistedBlock[]>["save"]>;
let metadataGetSpy: jest.SpiedFunction<MetadataRepository<PersistedBlock[]>["get"]>;
let metadataRepo: MetadataRepository<PersistedBlock[]>;
describe("ArbitrumEvmJsonRPCBlockRepository", () => {
afterAll(() => {
nock.restore();
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
});
afterEach(() => {
nock.cleanAll();
fs.rm(dirPath, () => {});
});
it("should be able to get block height with arbitrum latest commitment and eth finalized commitment", async () => {
// Given
const originalBlock = 19808090n;
const expectedBlock = 157542621n;
givenARepo();
givenL2Block("latest");
givenBlockHeightIs(originalBlock, "finalized");
// When
const result = await repo.getBlockHeight(arbitrum, "latest");
// Then
expect(result).toBe(expectedBlock);
const fileExists = fs.existsSync(`${dirPath}/arbitrum-latest.json`);
expect(fileExists).toBe(true);
});
it("should be throw error because unable to parse empty result for latest block", async () => {
// Given
givenARepo();
nock(rpc)
.post("/", {
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: ["latest", false],
id: 1,
})
.reply(200, {
jsonrpc: "2.0",
id: 1,
result: {},
});
try {
// When
await repo.getBlockHeight(arbitrum, "latest");
} catch (e: Error | any) {
// Then
expect(e).toBeInstanceOf(Error);
}
});
});
const givenARepo = () => {
repo = new ArbitrumEvmJsonRPCBlockRepository(
{
chains: {
ethereum: { rpcs: [rpc], timeout: 100, name: ethereum, network: "mainnet", chainId: 2 },
arbitrum: {
rpcs: [rpc],
timeout: 100,
name: arbitrum,
network: "mainnet",
chainId: 23,
},
},
},
new HttpClient(),
givenMetadataRepository([{ associatedL1Block: 18764852, l2BlockNumber: 157542621 }])
);
};
const givenL2Block = (commitment: EvmTag) => {
nock(rpc)
.post("/", {
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [commitment, false],
id: 1,
})
.reply(200, {
jsonrpc: "2.0",
id: 1,
result: {
baseFeePerGas: "0x5f5e100",
difficulty: "0x1",
extraData: "0xe24ff00c699874b42c1eb6a325ae6c672c502c529de9dbb24ed9ff51563ab5ec",
gasLimit: "0x4000000000000",
gasUsed: "0x44a679",
hash: "0xff1461564bad17aa8b047fc819f2d167b48e172f9143725815b2ec57b5aef429",
l1BlockNumber: "0x11dcb25",
logsBloom:
"0x00000002000000100000000000020000800000000002000040000000004100000000000000000000000000000000000000005000020020000000000080200000000000010000004980404008000000000202000000000000000000000000004000000020000400000000100400000000040000100000000240000010000800000000000000080000000000000000000400000000000000080000000000000110030000000202000000000400800000000000000000800001000200000080400000000002001000000080001020040000000000000000000080000000000000008010000000000000000000440000000000004000000000000000000000000000",
miner: "0xa4b000000000000000000073657175656e636572",
mixHash: "0x00000000000184c900000000011dcb25000000000000000a0000000000000000",
nonce: "0x000000000012b586",
number: "0x963e8dd",
parentHash: "0x648eff1f5f81b60f3bd9031114d54af71a1787f3f73e62fdbcf90cc228e976e9",
receiptsRoot: "0x25a3340b2150c6db9bab6f82b8885d7f08a0ddbf06b4a5ab765c882a869fc594",
sendCount: "0x184c9",
sendRoot: "0xe24ff00c699874b42c1eb6a325ae6c672c502c529de9dbb24ed9ff51563ab5ec",
sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
size: "0x51f",
stateRoot: "0x3ab24e412a3954b82927e92294e42ad73051ecf95e561a57c5bf6fcbb8114cc2",
timestamp: "0x6570deb5",
totalDifficulty: "0x8110b95",
transactions: [
"0xbfd339af3216f5a029e3cb96b75f0a09ef81ad49458af64c6f621d94476de2da",
"0x3fb701d004eae54917c73e2008ca69e96faa46e8bc9631923170a3c88b19150f",
"0x538622dae61bdfd20ddad66a3c06e49c94cba6bdca848a9320b9486b5dc1c8ad",
],
transactionsRoot: "0xc9f0d07e2b5d4f0d33e28917fc9e8af0ad5e0558c7d885fd6f48034bccafff06",
},
});
};
const givenMetadataRepository = (data: PersistedBlock[]) => {
metadataRepo = {
get: () => Promise.resolve(data),
save: () => Promise.resolve(),
};
metadataGetSpy = jest.spyOn(metadataRepo, "get");
metadataSaveSpy = jest.spyOn(metadataRepo, "save");
return metadataRepo;
};
const givenBlockHeightIs = (height: bigint, commitment: EvmTag) => {
nock(rpc)
.post("/", {
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [commitment, false],
id: 1,
})
.reply(200, {
jsonrpc: "2.0",
id: 1,
result: {
number: `0x${height.toString(16)}`,
hash: blockHash(height),
timestamp: "0x654a892f",
},
});
};
type PersistedBlock = {
associatedL1Block: number;
l2BlockNumber: number;
};
const blockHash = (blockNumber: bigint) => `0x${blockNumber.toString(16)}`;

View File

@ -0,0 +1,69 @@
import { describe, it, expect, afterEach, afterAll } from "@jest/globals";
import { BscEvmJsonRPCBlockRepository } from "../../../src/infrastructure/repositories";
import { HttpClient } from "../../../src/infrastructure/rpc/http/HttpClient";
import { EvmTag } from "../../../src/domain/entities/evm";
import axios from "axios";
import nock from "nock";
axios.defaults.adapter = "http"; // needed by nock
const rpc = "http://localhost";
const bsc = "bsc";
let repo: BscEvmJsonRPCBlockRepository;
describe("BscEvmJsonRPCBlockRepository", () => {
afterAll(() => {
nock.restore();
});
afterEach(() => {
nock.cleanAll();
});
it("should be able to get block height with the latest commitment", async () => {
// Given
const originalBlock = 1980809n;
const expectedBlock = 1980794n;
givenARepo();
givenBlockHeightIs(originalBlock, "latest");
// When
const result = await repo.getBlockHeight(bsc, "latest");
// Then
expect(result).toBe(expectedBlock);
});
});
const givenARepo = () => {
repo = new BscEvmJsonRPCBlockRepository(
{
chains: {
bsc: { rpcs: [rpc], timeout: 100, name: bsc, network: "mainnet", chainId: 4 },
},
},
new HttpClient()
);
};
const givenBlockHeightIs = (height: bigint, commitment: EvmTag) => {
nock(rpc)
.post("/", {
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [commitment, false],
id: 1,
})
.reply(200, {
jsonrpc: "2.0",
id: 1,
result: {
number: `0x${height.toString(16)}`,
hash: blockHash(height),
timestamp: "0x654a892f",
},
});
};
const blockHash = (blockNumber: bigint) => `0x${blockNumber.toString(16)}`;

View File

@ -175,6 +175,34 @@ data:
}
}
]
},
{
"id": "poll-log-message-published-arbitrum",
"chain": "arbitrum",
"source": {
"action": "PollEvmLogs",
"config": {
"blockBatchSize": 100,
"commitment": "latest",
"interval": 30000,
"addresses": ["0xC7A204bDBFe983FCD8d8E61D02b475D4073fF97e"],
"chain": "arbitrum"
}
},
"handlers": [
{
"action": "HandleEvmLogs",
"target": "sns",
"mapper": "evmLogMessagePublishedMapper",
"config": {
"abi": "event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel)",
"filter": {
"addresses": ["0xC7A204bDBFe983FCD8d8E61D02b475D4073fF97e"],
"topics": ["0x6eb224fb001ed210e379b335e35efe88672a8ce935d981a6896b27ffdf52a3b2"]
}
}
}
]
}
]
mainnet-jobs.json: |-
@ -318,6 +346,34 @@ data:
}
}
]
},
{
"id": "poll-log-message-published-arbitrum",
"chain": "arbitrum",
"source": {
"action": "PollEvmLogs",
"config": {
"blockBatchSize": 100,
"commitment": "latest",
"interval": 30000,
"addresses": ["0xa5f208e072434bC67592E4C49C1B991BA79BCA46"],
"chain": "arbitrum"
}
},
"handlers": [
{
"action": "HandleEvmLogs",
"target": "sns",
"mapper": "evmLogMessagePublishedMapper",
"config": {
"abi": "event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel)",
"filter": {
"addresses": ["0xa5f208e072434bC67592E4C49C1B991BA79BCA46"],
"topics": ["0x6eb224fb001ed210e379b335e35efe88672a8ce935d981a6896b27ffdf52a3b2"]
}
}
}
]
}
]
---