diff --git a/src/getBatchPosters.integration.test.ts b/src/getBatchPosters.integration.test.ts
new file mode 100644
index 00000000..b846d00f
--- /dev/null
+++ b/src/getBatchPosters.integration.test.ts
@@ -0,0 +1,166 @@
+import { describe, it, expect } from 'vitest';
+import { Address, createPublicClient, http } from 'viem';
+import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
+
+import { nitroTestnodeL2 } from './chains';
+import { sequencerInboxActions } from './decorators/sequencerInboxActions';
+import { getInformationFromTestnode, getNitroTestnodePrivateKeyAccounts } from './testHelpers';
+import { getBatchPosters } from './getBatchPosters';
+
+const { l3RollupOwner } = getNitroTestnodePrivateKeyAccounts();
+const { l3Rollup, l3UpgradeExecutor, l3SequencerInbox } = getInformationFromTestnode();
+
+const client = createPublicClient({
+ chain: nitroTestnodeL2,
+ transport: http(),
+}).extend(
+ sequencerInboxActions({
+ sequencerInbox: l3SequencerInbox,
+ }),
+);
+
+async function setBatchPoster(batchPoster: Address, state: boolean) {
+ const tx = await client.sequencerInboxPrepareTransactionRequest({
+ functionName: 'setIsBatchPoster',
+ args: [batchPoster, state],
+ account: l3RollupOwner.address,
+ upgradeExecutor: l3UpgradeExecutor,
+ sequencerInbox: l3SequencerInbox,
+ });
+
+ const txHash = await client.sendRawTransaction({
+ serializedTransaction: await l3RollupOwner.signTransaction(tx),
+ });
+
+ await client.waitForTransactionReceipt({
+ hash: txHash,
+ });
+}
+
+// Tests can be enabled once we run one node per integration test
+describe.skip('successfully get batch posters', () => {
+ it('when disabling the same batch posters multiple time', async () => {
+ const randomAccount = privateKeyToAccount(generatePrivateKey()).address;
+
+ const { isAccurate: isAccurateInitially, batchPosters: initialBatchPosters } =
+ await getBatchPosters(client, {
+ rollup: l3Rollup,
+ sequencerInbox: l3SequencerInbox,
+ });
+
+ // By default, chains from nitro testnode has 1 batch poster
+ expect(initialBatchPosters).toHaveLength(1);
+ expect(isAccurateInitially).toBeTruthy();
+
+ await setBatchPoster(randomAccount, false);
+ await setBatchPoster(randomAccount, false);
+
+ const { isAccurate: isStillAccurate, batchPosters: newBatchPosters } = await getBatchPosters(
+ client,
+ {
+ rollup: l3Rollup,
+ sequencerInbox: l3SequencerInbox,
+ },
+ );
+ // Setting the same batch poster multiple time to false doesn't add new batch posters
+ expect(newBatchPosters).toEqual(initialBatchPosters);
+ expect(isStillAccurate).toBeTruthy();
+
+ await setBatchPoster(randomAccount, true);
+ const { batchPosters, isAccurate } = await getBatchPosters(client, {
+ rollup: l3Rollup,
+ sequencerInbox: l3SequencerInbox,
+ });
+
+ expect(batchPosters).toEqual(initialBatchPosters.concat(randomAccount));
+ expect(isAccurate).toBeTruthy();
+
+ // Reset state for future tests
+ await setBatchPoster(randomAccount, false);
+ const { isAccurate: isAccurateFinal, batchPosters: batchPostersFinal } = await getBatchPosters(
+ client,
+ {
+ rollup: l3Rollup,
+ sequencerInbox: l3SequencerInbox,
+ },
+ );
+ expect(batchPostersFinal).toEqual(initialBatchPosters);
+ expect(isAccurateFinal).toBeTruthy();
+ });
+
+ it('when enabling the same batch poster multiple time', async () => {
+ const randomAccount = privateKeyToAccount(generatePrivateKey()).address;
+
+ const { isAccurate: isAccurateInitially, batchPosters: initialBatchPosters } =
+ await getBatchPosters(client, {
+ rollup: l3Rollup,
+ sequencerInbox: l3SequencerInbox,
+ });
+ // By default, chains from nitro testnode has 1 batch poster
+ expect(initialBatchPosters).toHaveLength(1);
+ expect(isAccurateInitially).toBeTruthy();
+
+ await setBatchPoster(randomAccount, true);
+ await setBatchPoster(randomAccount, true);
+ const { isAccurate: isStillAccurate, batchPosters: newBatchPosters } = await getBatchPosters(
+ client,
+ {
+ rollup: l3Rollup,
+ sequencerInbox: l3SequencerInbox,
+ },
+ );
+
+ expect(newBatchPosters).toEqual(initialBatchPosters.concat(randomAccount));
+ expect(isStillAccurate).toBeTruthy();
+
+ // Reset state for futures tests
+ await setBatchPoster(randomAccount, false);
+ const { batchPosters, isAccurate } = await getBatchPosters(client, {
+ rollup: l3Rollup,
+ sequencerInbox: l3SequencerInbox,
+ });
+ expect(batchPosters).toEqual(initialBatchPosters);
+ expect(isAccurate).toBeTruthy();
+ });
+
+ it('when adding an existing batch poster', async () => {
+ const { isAccurate: isAccurateInitially, batchPosters: initialBatchPosters } =
+ await getBatchPosters(client, { rollup: l3Rollup, sequencerInbox: l3SequencerInbox });
+ expect(initialBatchPosters).toHaveLength(1);
+ expect(isAccurateInitially).toBeTruthy();
+
+ const firstBatchPoster = initialBatchPosters[0];
+ await setBatchPoster(firstBatchPoster, true);
+
+ const { isAccurate, batchPosters } = await getBatchPosters(client, {
+ rollup: l3Rollup,
+ sequencerInbox: l3SequencerInbox,
+ });
+ expect(batchPosters).toEqual(initialBatchPosters);
+ expect(isAccurate).toBeTruthy();
+ });
+
+ it('when removing an existing batch poster', async () => {
+ const { isAccurate: isAccurateInitially, batchPosters: initialBatchPosters } =
+ await getBatchPosters(client, { rollup: l3Rollup, sequencerInbox: l3SequencerInbox });
+ expect(initialBatchPosters).toHaveLength(1);
+ expect(isAccurateInitially).toBeTruthy();
+
+ const lastBatchPoster = initialBatchPosters[initialBatchPosters.length - 1];
+ await setBatchPoster(lastBatchPoster, false);
+ const { isAccurate, batchPosters } = await getBatchPosters(client, {
+ rollup: l3Rollup,
+ sequencerInbox: l3SequencerInbox,
+ });
+ expect(batchPosters).toEqual(initialBatchPosters.slice(0, -1));
+ expect(isAccurate).toBeTruthy();
+
+ await setBatchPoster(lastBatchPoster, true);
+ const { isAccurate: isAccurateFinal, batchPosters: batchPostersFinal } = await getBatchPosters(
+ client,
+ { rollup: l3Rollup, sequencerInbox: l3SequencerInbox },
+ );
+ expect(batchPostersFinal).toEqual(initialBatchPosters);
+ expect(isAccurateFinal).toBeTruthy();
+ });
+});
diff --git a/src/getBatchPosters.ts b/src/getBatchPosters.ts
new file mode 100644
index 00000000..7dd3e50c
--- /dev/null
+++ b/src/getBatchPosters.ts
@@ -0,0 +1,181 @@
+import {
+ Address,
+ Chain,
+ Hex,
+ PublicClient,
+ Transport,
+ decodeFunctionData,
+ getAbiItem,
+ getFunctionSelector,
+} from 'viem';
+import { rollupCreator, upgradeExecutor } from './contracts';
+import { safeL2ABI, sequencerInboxABI } from './abi';
+import { createRollupFetchTransactionHash } from './createRollupFetchTransactionHash';
+
+const createRollupABI = getAbiItem({ abi: rollupCreator.abi, name: 'createRollup' });
+const createRollupFunctionSelector = getFunctionSelector(createRollupABI);
+
+const setIsBatchPosterABI = getAbiItem({ abi: sequencerInboxABI, name: 'setIsBatchPoster' });
+const setIsBatchPosterFunctionSelector = getFunctionSelector(setIsBatchPosterABI);
+
+const executeCallABI = getAbiItem({ abi: upgradeExecutor.abi, name: 'executeCall' });
+const upgradeExecutorExecuteCallFunctionSelector = getFunctionSelector(executeCallABI);
+
+const execTransactionABI = getAbiItem({ abi: safeL2ABI, name: 'execTransaction' });
+const safeL2FunctionSelector = getFunctionSelector(execTransactionABI);
+
+const ownerFunctionCalledEventAbi = getAbiItem({
+ abi: sequencerInboxABI,
+ name: 'OwnerFunctionCalled',
+});
+
+function getBatchPostersFromFunctionData<
+ TAbi extends (typeof createRollupABI)[] | (typeof setIsBatchPosterABI)[],
+>({ abi, data }: { abi: TAbi; data: Hex }) {
+ const { args } = decodeFunctionData({
+ abi,
+ data,
+ });
+ return args;
+}
+
+function updateAccumulator(acc: Set
, input: Hex) {
+ const [batchPoster, isAdd] = getBatchPostersFromFunctionData({
+ abi: [setIsBatchPosterABI],
+ data: input,
+ });
+
+ if (isAdd) {
+ acc.add(batchPoster);
+ } else {
+ acc.delete(batchPoster);
+ }
+
+ return acc;
+}
+
+export type GetBatchPostersParams = {
+ /** Address of the rollup we're getting list of batch posters from */
+ rollup: Address;
+ /** Address of the sequencerInbox we're getting logs from */
+ sequencerInbox: Address;
+};
+export type GetBatchPostersReturnType = {
+ /**
+ * If logs contain unknown signature, batch posters list might:
+ * - contain false positives (batch posters that were removed, but returned as batch poster)
+ * - contain false negatives (batch posters that were added, but not present in the list)
+ */
+ isAccurate: boolean;
+ /** List of batch posters for the given rollup */
+ batchPosters: Address[];
+};
+
+/**
+ *
+ * @param {PublicClient} publicClient - The chain Viem Public Client
+ * @param {GetBatchPostersParams} GetBatchPostersParams {@link GetBatchPostersParams}
+ *
+ * @returns Promise<{@link GetBatchPostersReturnType}>
+ *
+ * @remarks Batch posters list is not guaranteed to be exhaustive if the `isAccurate` flag is false.
+ * It might contain false positive (batch posters that were removed, but returned as batch poster)
+ * or false negative (batch posters that were added, but not present in the list)
+ *
+ * @example
+ * const { isAccurate, batchPosters } = getBatchPosters(client, {
+ * rollup: '0xc47dacfbaa80bd9d8112f4e8069482c2a3221336',
+ * sequencerInbox: '0x995a9d3ca121D48d21087eDE20bc8acb2398c8B1'
+ * });
+ *
+ * if (isAccurate) {
+ * // batch posters were all fetched properly
+ * } else {
+ * // batch posters list is not guaranteed to be accurate
+ * }
+ */
+export async function getBatchPosters(
+ publicClient: PublicClient,
+ { rollup, sequencerInbox }: GetBatchPostersParams,
+): Promise {
+ let blockNumber: bigint | 'earliest';
+ let createRollupTransactionHash: Address | null = null;
+ try {
+ createRollupTransactionHash = await createRollupFetchTransactionHash({
+ rollup,
+ publicClient,
+ });
+ const receipt = await publicClient.waitForTransactionReceipt({
+ hash: createRollupTransactionHash,
+ });
+ blockNumber = receipt.blockNumber;
+ } catch (e) {
+ blockNumber = 'earliest';
+ }
+
+ const sequencerInboxEvents = await publicClient.getLogs({
+ address: sequencerInbox,
+ event: ownerFunctionCalledEventAbi,
+ args: { id: 1n },
+ fromBlock: blockNumber,
+ toBlock: 'latest',
+ });
+
+ const events = createRollupTransactionHash
+ ? [{ transactionHash: createRollupTransactionHash }, ...sequencerInboxEvents]
+ : sequencerInboxEvents;
+ const txs = await Promise.all(
+ events.map((event) =>
+ publicClient.getTransaction({
+ hash: event.transactionHash,
+ }),
+ ),
+ );
+
+ let isAccurate = true;
+ const batchPosters = txs.reduce((acc, tx) => {
+ const txSelectedFunction = tx.input.slice(0, 10);
+
+ switch (txSelectedFunction) {
+ case createRollupFunctionSelector: {
+ const [{ batchPoster }] = getBatchPostersFromFunctionData({
+ abi: [createRollupABI],
+ data: tx.input,
+ });
+
+ return new Set([...acc, batchPoster]);
+ }
+ case setIsBatchPosterFunctionSelector: {
+ return updateAccumulator(acc, tx.input);
+ }
+ case upgradeExecutorExecuteCallFunctionSelector: {
+ const { args: executeCallCalldata } = decodeFunctionData({
+ abi: [executeCallABI],
+ data: tx.input,
+ });
+ return updateAccumulator(acc, executeCallCalldata[1]);
+ }
+ case safeL2FunctionSelector: {
+ const { args: execTransactionCalldata } = decodeFunctionData({
+ abi: [execTransactionABI],
+ data: tx.input,
+ });
+ const { args: executeCallCalldata } = decodeFunctionData({
+ abi: [executeCallABI],
+ data: execTransactionCalldata[2],
+ });
+ return updateAccumulator(acc, executeCallCalldata[1]);
+ }
+ default: {
+ console.warn(`[getBatchPosters] unknown 4bytes, tx id: ${tx.hash}`);
+ isAccurate = false;
+ return acc;
+ }
+ }
+ }, new Set());
+
+ return {
+ isAccurate,
+ batchPosters: [...batchPosters],
+ };
+}
diff --git a/src/getBatchPosters.unit.test.ts b/src/getBatchPosters.unit.test.ts
new file mode 100644
index 00000000..1dd965bf
--- /dev/null
+++ b/src/getBatchPosters.unit.test.ts
@@ -0,0 +1,588 @@
+import {
+ Address,
+ EIP1193RequestFn,
+ Hex,
+ createPublicClient,
+ createTransport,
+ encodeFunctionData,
+ http,
+} from 'viem';
+import { arbitrum, arbitrumSepolia } from 'viem/chains';
+import { it, expect, vi, describe } from 'vitest';
+import { safeL2ABI, sequencerInboxABI } from './abi';
+import { sequencerInboxPrepareFunctionData } from './sequencerInboxPrepareTransactionRequest';
+import { getBatchPosters } from './getBatchPosters';
+
+const client = createPublicClient({
+ chain: arbitrum,
+ transport: http(),
+});
+
+function mockLog(transactionHash: string) {
+ return {
+ address: '0x193e2887031c148ab54f5e856ea51ae521661200',
+ args: { id: 1n },
+ blockHash: '0x3bafb9574d8a3a7c09070935dc3ca936a5df06e2abd09cbd2a3cd489562e748f',
+ blockNumber: 55635757n,
+ data: '0x',
+ eventName: 'OwnerFunctionCalled',
+ logIndex: 42,
+ removed: false,
+ topics: [
+ '0xea8787f128d10b2cc0317b0c3960f9ad447f7f6c1ed189db1083ccffd20f456e',
+ '0x0000000000000000000000000000000000000000000000000000000000000001',
+ ],
+ transactionHash,
+ transactionIndex: 3,
+ };
+}
+function mockTransaction(data: Hex) {
+ return {
+ accessList: [],
+ blockHash: '0x3bafb9574d8a3a7c09070935dc3ca936a5df06e2abd09cbd2a3cd489562e748f',
+ blockNumber: 55635757n,
+ chainId: 421614,
+ from: '0xfd5735380689a53e6b048e980f34cb94be9fd0c7',
+ gas: 7149526n,
+ gasPrice: 147390000n,
+ hash: '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10',
+ input: data,
+ maxFeePerGas: 174300000n,
+ maxPriorityFeePerGas: 0n,
+ nonce: 82,
+ r: '0x49cabfb3327a9f8c0fb8da0d8b3b81daf2f46d09ebc3c640b09d788f51159b7f',
+ s: '0x2ee5ae568a892beed3a763c33c8da4a3368be0b2422f2785fe00be76ecad08f8',
+ to: '0x06e341073b2749e0bb9912461351f716decda9b0',
+ transactionIndex: 3,
+ type: 'eip1559',
+ typeHex: '0x2',
+ v: 1n,
+ value: 0n,
+ yParity: 1,
+ };
+}
+function mockData({
+ logs,
+ method,
+ params,
+}: {
+ logs: {
+ [transactionHash: string]: Hex;
+ };
+ method:
+ | 'eth_getLogs'
+ | 'eth_getTransactionByHash'
+ | 'eth_getTransactionReceipt'
+ | 'eth_blockNumber';
+ params: string;
+}) {
+ if (method === 'eth_getLogs') {
+ return Object.keys(logs).map((transactionHash) => mockLog(transactionHash));
+ }
+
+ if (method === 'eth_getTransactionByHash') {
+ return mockTransaction(logs[params]);
+ }
+
+ if (method === 'eth_getTransactionReceipt') {
+ return {
+ blockNumber: 36723964,
+ };
+ }
+
+ if (method === 'eth_blockNumber') {
+ return 36723964;
+ }
+
+ return null;
+}
+
+// Taken from https://sepolia.arbiscan.io/tx/0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10 (Input data)
+const validInput =
+ '0xcb73d6e2000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000725d217057e509dd284ee0e13ac846cfea0b7bb100000000000000000000000000000000000000000000000000000000000005400000000000000000000000000000000000000000000000000000000000019999000000000000000000000000d1fe5b0a963a0e557aabb59cb61ffdd568b4605c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000000000009600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016345785d8a00000754e09320c381566cc0449904c377a52bd34a6b9404432e80afd573b67f7b17000000000000000000000000fd5735380689a53e6b048e980f34cb94be9fd0c700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000fe22e119e00000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001680000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000000000000000000e1000000000000000000000000000000000000000000000000000000000000002757b22686f6d657374656164426c6f636b223a302c2264616f466f726b426c6f636b223a6e756c6c2c2264616f466f726b537570706f7274223a747275652c22656970313530426c6f636b223a302c2265697031353048617368223a22307830303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030222c22656970313535426c6f636b223a302c22656970313538426c6f636b223a302c2262797a616e7469756d426c6f636b223a302c22636f6e7374616e74696e6f706c65426c6f636b223a302c2270657465727362757267426c6f636b223a302c22697374616e62756c426c6f636b223a302c226d756972476c6163696572426c6f636b223a302c226265726c696e426c6f636b223a302c226c6f6e646f6e426c6f636b223a302c22636c69717565223a7b22706572696f64223a302c2265706f6368223a307d2c22617262697472756d223a7b22456e61626c654172624f53223a747275652c22416c6c6f774465627567507265636f6d70696c6573223a66616c73652c2244617461417661696c6162696c697479436f6d6d6974746565223a747275652c22496e697469616c4172624f5356657273696f6e223a31312c2247656e65736973426c6f636b4e756d223a302c224d6178436f646553697a65223a32343537362c224d6178496e6974436f646553697a65223a34393135322c22496e697469616c436861696e4f776e6572223a22307846643537333533383036383941353365364230343865393830463334634239346265396644306337227d2c22636861696e4964223a36383231393137393432327d000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006a23ccc1c36d2aaa98aef2a4471cf807dd22e45b';
+const upgradeExecutorAddress = '0x314f3c2f9803f8828a1050127be5bb29a4940f29';
+const rollupAddress = '0xe0875cbd144fe66c015a95e5b2d2c15c3b612179';
+const sequencerInboxAddress = '0x041f85dd87c46b941dc9b15c6628b19ee5358485';
+
+function setBatchPosterHelper(args: [Address, boolean]) {
+ return encodeFunctionData({
+ abi: sequencerInboxABI,
+ functionName: 'setIsBatchPoster',
+ args,
+ });
+}
+function upgradeExecutorSetBatchPosterHelper(args: [Address, boolean]) {
+ return sequencerInboxPrepareFunctionData({
+ sequencerInbox: '0x995a9d3ca121D48d21087eDE20bc8acb2398c8B1',
+ functionName: 'setIsBatchPoster',
+ args,
+ abi: sequencerInboxABI,
+ upgradeExecutor: upgradeExecutorAddress,
+ }).data;
+}
+function safeSetBatchPosterHelper(args: [Address, boolean]) {
+ const bytes = upgradeExecutorSetBatchPosterHelper(args);
+ return encodeFunctionData({
+ abi: safeL2ABI,
+ functionName: 'execTransaction',
+ args: [
+ rollupAddress,
+ 0n,
+ bytes,
+ 0,
+ 0n,
+ 0n,
+ 0n,
+ '0x0000000000000000000000000000000000000000',
+ '0x0000000000000000000000000000000000000000',
+ '0x000000000000000000000000AD46AD093FD26B4464B756AC1B56985AF87399E7000000000000000000000000000000000000000000000000000000000000000001ABFD0989138206FEC57AE925D0B8CC27ECBB4484DC4CE1133D90E2BA4A644E6179F6640360B48976145461BBC820378F733421ADFCF78730FD20408BB10C284F1B',
+ ],
+ });
+}
+
+it('getBatchPosters returns all batch posters (Xai)', async () => {
+ const { isAccurate, batchPosters } = await getBatchPosters(client, {
+ rollup: '0xc47dacfbaa80bd9d8112f4e8069482c2a3221336',
+ sequencerInbox: '0x995a9d3ca121D48d21087eDE20bc8acb2398c8B1',
+ });
+ expect(batchPosters).toEqual(['0x7F68dba68E72a250004812fe04F1123Fca89aBa9']);
+ expect(isAccurate).toBeTruthy();
+});
+
+describe('createRollupFunctionSelector', () => {
+ it('getBatchPosters returns all batch posters with isAccurate flag set to true', async () => {
+ const mockTransport = () =>
+ createTransport({
+ key: 'mock',
+ name: 'Mock Transport',
+ request: vi.fn(({ method, params }) => {
+ return mockData({
+ logs: {
+ '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10': validInput,
+ },
+ method,
+ params,
+ });
+ }) as unknown as EIP1193RequestFn,
+ type: 'mock',
+ });
+
+ const mockClient = createPublicClient({
+ transport: mockTransport,
+ chain: arbitrumSepolia,
+ });
+
+ const { batchPosters, isAccurate } = await getBatchPosters(mockClient, {
+ rollup: rollupAddress,
+ sequencerInbox: '0x995a9d3ca121d48d21087ede20bc8acb2398c8b1',
+ });
+
+ expect(batchPosters).toEqual(['0x725D217057e509Dd284eE0e13aC846Cfea0B7BB1']);
+ expect(isAccurate).toBeTruthy();
+ });
+
+ it('getBatchPosters returns batch posters with isAccurate flag set to false when there are non-parsable calldata from event logs', async () => {
+ const mockTransport = () =>
+ createTransport({
+ key: 'mock',
+ name: 'Mock Transport',
+ request: vi.fn(({ method, params }) => {
+ return mockData({
+ logs: {
+ '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10': validInput,
+ '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c': '0xdeadbeef',
+ },
+ method,
+ params,
+ });
+ }) as unknown as EIP1193RequestFn,
+ type: 'mock',
+ });
+
+ const mockClient = createPublicClient({
+ transport: mockTransport,
+ });
+
+ const { batchPosters, isAccurate } = await getBatchPosters(mockClient, {
+ rollup: rollupAddress,
+ sequencerInbox: sequencerInboxAddress,
+ });
+
+ expect(batchPosters).toEqual(['0x725D217057e509Dd284eE0e13aC846Cfea0B7BB1']);
+ expect(isAccurate).toBeFalsy();
+ });
+});
+
+describe('setBatchPosterFunctionSelector', () => {
+ it('getBatchPosters returns all batch posters with isAccurate flag set to true', async () => {
+ const mockTransport = () =>
+ createTransport({
+ key: 'mock',
+ name: 'Mock Transport',
+ request: vi.fn(({ method, params }) => {
+ return mockData({
+ logs: {
+ '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10':
+ setBatchPosterHelper(['0x25EA41f0bDa921a0eBf48291961B1F10b59BC6b8', true]),
+ '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c':
+ setBatchPosterHelper(['0x6a23CcC1c36D2aaA98AeF2a4471cf807DD22e45b', true]),
+ },
+ method,
+ params,
+ });
+ }) as unknown as EIP1193RequestFn,
+ type: 'mock',
+ });
+
+ const mockClient = createPublicClient({
+ transport: mockTransport,
+ });
+
+ const { batchPosters, isAccurate } = await getBatchPosters(mockClient, {
+ rollup: rollupAddress,
+ sequencerInbox: sequencerInboxAddress,
+ });
+
+ expect(batchPosters).toEqual([
+ '0x25EA41f0bDa921a0eBf48291961B1F10b59BC6b8',
+ '0x6a23CcC1c36D2aaA98AeF2a4471cf807DD22e45b',
+ ]);
+ expect(isAccurate).toBeTruthy();
+ });
+
+ it('getBatchPosters returns batch posters with isAccurate flag set to false when there are non-parsable calldata from event logs', async () => {
+ const mockTransport = () =>
+ createTransport({
+ key: 'mock',
+ name: 'Mock Transport',
+ request: vi.fn(({ method, params }) => {
+ return mockData({
+ logs: {
+ '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10':
+ setBatchPosterHelper(['0x25EA41f0bDa921a0eBf48291961B1F10b59BC6b8', true]),
+ '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c': '0xdeadbeef',
+ '0x10f4f4d214af281a67713ddaf799f0524f833c57818863e8c1b117394e872f3a':
+ setBatchPosterHelper(['0x6a23CcC1c36D2aaA98AeF2a4471cf807DD22e45b', true]),
+ },
+ method,
+ params,
+ });
+ }) as unknown as EIP1193RequestFn,
+ type: 'mock',
+ });
+
+ const mockClient = createPublicClient({
+ transport: mockTransport,
+ });
+
+ const { batchPosters, isAccurate } = await getBatchPosters(mockClient, {
+ rollup: rollupAddress,
+ sequencerInbox: sequencerInboxAddress,
+ });
+
+ expect(batchPosters).toEqual([
+ '0x25EA41f0bDa921a0eBf48291961B1F10b59BC6b8',
+ '0x6a23CcC1c36D2aaA98AeF2a4471cf807DD22e45b',
+ ]);
+ expect(isAccurate).toBeFalsy();
+ });
+});
+
+describe('upgradeExecutorExecuteCallFunctionSelector', () => {
+ it('getBatchPosters returns all batch posters with isAccurate flag set to true', async () => {
+ const mockTransport = () =>
+ createTransport({
+ key: 'mock',
+ name: 'Mock Transport',
+ request: vi.fn(({ method, params }) => {
+ return mockData({
+ logs: {
+ '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10':
+ upgradeExecutorSetBatchPosterHelper([
+ '0x81209B63188f27339441B741518fF73F18b4Efd4',
+ true,
+ ]),
+ '0x10f4f4d214af281a67713ddaf799f0524f833c57818863e8c1b117394e872f3a':
+ upgradeExecutorSetBatchPosterHelper([
+ '0x9481eF9e2CA814fc94676dEa3E8c3097B06b3a33',
+ true,
+ ]),
+ },
+ method,
+ params,
+ });
+ }) as unknown as EIP1193RequestFn,
+ type: 'mock',
+ });
+
+ const mockClient = createPublicClient({
+ transport: mockTransport,
+ });
+
+ const { batchPosters, isAccurate } = await getBatchPosters(mockClient, {
+ rollup: rollupAddress,
+ sequencerInbox: sequencerInboxAddress,
+ });
+
+ expect(batchPosters).toEqual([
+ '0x81209B63188f27339441B741518fF73F18b4Efd4',
+ '0x9481eF9e2CA814fc94676dEa3E8c3097B06b3a33',
+ ]);
+ expect(isAccurate).toBeTruthy();
+ });
+
+ it('getBatchPosters returns batch posters with isAccurate flag set to false when there are non-parsable calldata from event logs', async () => {
+ const mockTransport = () =>
+ createTransport({
+ key: 'mock',
+ name: 'Mock Transport',
+ request: vi.fn(({ method, params }) => {
+ return mockData({
+ logs: {
+ '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c': '0xdeadbeef',
+ '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10':
+ upgradeExecutorSetBatchPosterHelper([
+ '0x81209B63188f27339441B741518fF73F18b4Efd4',
+ true,
+ ]),
+ '0x10f4f4d214af281a67713ddaf799f0524f833c57818863e8c1b117394e872f3a':
+ upgradeExecutorSetBatchPosterHelper([
+ '0x6a23CcC1c36D2aaA98AeF2a4471cf807DD22e45b',
+ false,
+ ]),
+ },
+ method,
+ params,
+ });
+ }) as unknown as EIP1193RequestFn,
+ type: 'mock',
+ });
+
+ const mockClient = createPublicClient({
+ transport: mockTransport,
+ });
+
+ const { batchPosters, isAccurate } = await getBatchPosters(mockClient, {
+ rollup: rollupAddress,
+ sequencerInbox: sequencerInboxAddress,
+ });
+
+ expect(batchPosters).toEqual(['0x81209B63188f27339441B741518fF73F18b4Efd4']);
+ expect(isAccurate).toBeFalsy();
+ });
+});
+
+describe('safeL2FunctionSelector', () => {
+ it('getBatchPosters returns all batch posters with isAccurate flag set to true', async () => {
+ const mockTransport = () =>
+ createTransport({
+ key: 'mock',
+ name: 'Mock Transport',
+ request: vi.fn(({ method, params }) => {
+ return mockData({
+ logs: {
+ '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10':
+ safeSetBatchPosterHelper(['0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962', true]),
+ '0x10f4f4d214af281a67713ddaf799f0524f833c57818863e8c1b117394e872f3a':
+ safeSetBatchPosterHelper(['0x9481eF9e2CA814fc94676dEa3E8c3097B06b3a33', false]),
+ },
+ method,
+ params,
+ });
+ }) as unknown as EIP1193RequestFn,
+ type: 'mock',
+ });
+
+ const mockClient = createPublicClient({
+ transport: mockTransport,
+ });
+
+ const { batchPosters, isAccurate } = await getBatchPosters(mockClient, {
+ rollup: rollupAddress,
+ sequencerInbox: sequencerInboxAddress,
+ });
+
+ expect(batchPosters).toEqual(['0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962']);
+ expect(isAccurate).toBeTruthy();
+ });
+
+ it('getBatchPosters returns batch posters with isAccurate flag set to false when there are non-parsable calldata from event logs', async () => {
+ const mockTransport = () =>
+ createTransport({
+ key: 'mock',
+ name: 'Mock Transport',
+ request: vi.fn(({ method, params }) => {
+ return mockData({
+ logs: {
+ '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c': '0xdeadbeef',
+ '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10':
+ safeSetBatchPosterHelper(['0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962', true]),
+ '0x10f4f4d214af281a67713ddaf799f0524f833c57818863e8c1b117394e872f3a':
+ safeSetBatchPosterHelper(['0x9481eF9e2CA814fc94676dEa3E8c3097B06b3a33', true]),
+ },
+ method,
+ params,
+ });
+ }) as unknown as EIP1193RequestFn,
+ type: 'mock',
+ });
+
+ const mockClient = createPublicClient({
+ transport: mockTransport,
+ });
+
+ const { batchPosters, isAccurate } = await getBatchPosters(mockClient, {
+ rollup: rollupAddress,
+ sequencerInbox: sequencerInboxAddress,
+ });
+
+ expect(batchPosters).toEqual([
+ '0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962',
+ '0x9481eF9e2CA814fc94676dEa3E8c3097B06b3a33',
+ ]);
+ expect(isAccurate).toBeFalsy();
+ });
+});
+
+describe('Detect batch posters added or removed multiple times', () => {
+ it('when disabling the same batch poster multiple time', async () => {
+ const batchPoster = '0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962';
+ const mockTransport = () =>
+ createTransport({
+ key: 'mock',
+ name: 'Mock Transport',
+ request: vi.fn(({ method, params }) => {
+ return mockData({
+ logs: {
+ '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10':
+ safeSetBatchPosterHelper([batchPoster, false]),
+ '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c':
+ safeSetBatchPosterHelper([batchPoster, false]),
+ '0x10f4f4d214af281a67713ddaf799f0524f833c57818863e8c1b117394e872f3a':
+ safeSetBatchPosterHelper([batchPoster, true]),
+ '0x1acada0382b0ae715c41365c71b780871ec5adcfced8e57f0ae009c4b8738e2a':
+ safeSetBatchPosterHelper([batchPoster, false]),
+ },
+ method,
+ params,
+ });
+ }) as unknown as EIP1193RequestFn,
+ type: 'mock',
+ });
+
+ const mockClient = createPublicClient({
+ transport: mockTransport,
+ });
+
+ const { batchPosters, isAccurate } = await getBatchPosters(mockClient, {
+ rollup: rollupAddress,
+ sequencerInbox: sequencerInboxAddress,
+ });
+
+ expect(batchPosters).toEqual([]);
+ expect(isAccurate).toBeTruthy();
+ });
+ it('when enabling the same batch posters multiple time', async () => {
+ const batchPoster = '0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962';
+ const mockTransport = () =>
+ createTransport({
+ key: 'mock',
+ name: 'Mock Transport',
+ request: vi.fn(({ method, params }) => {
+ return mockData({
+ logs: {
+ '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10':
+ safeSetBatchPosterHelper([batchPoster, true]),
+ '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c':
+ safeSetBatchPosterHelper([batchPoster, true]),
+ '0x1acada0382b0ae715c41365c71b780871ec5adcfced8e57f0ae009c4b8738e2a':
+ safeSetBatchPosterHelper([batchPoster, false]),
+ },
+ method,
+ params,
+ });
+ }) as unknown as EIP1193RequestFn,
+ type: 'mock',
+ });
+
+ const mockClient = createPublicClient({
+ transport: mockTransport,
+ });
+
+ const { batchPosters, isAccurate } = await getBatchPosters(mockClient, {
+ rollup: rollupAddress,
+ sequencerInbox: sequencerInboxAddress,
+ });
+
+ expect(batchPosters).toEqual([]);
+ expect(isAccurate).toBeTruthy();
+ });
+ it('when adding an existing batch poster', async () => {
+ const batchPoster = '0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962';
+ const mockTransport = () =>
+ createTransport({
+ key: 'mock',
+ name: 'Mock Transport',
+ request: vi.fn(({ method, params }) => {
+ return mockData({
+ logs: {
+ '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10':
+ safeSetBatchPosterHelper([batchPoster, true]),
+ '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c':
+ safeSetBatchPosterHelper([batchPoster, true]),
+ },
+ method,
+ params,
+ });
+ }) as unknown as EIP1193RequestFn,
+ type: 'mock',
+ });
+
+ const mockClient = createPublicClient({
+ transport: mockTransport,
+ });
+
+ const { batchPosters, isAccurate } = await getBatchPosters(mockClient, {
+ rollup: rollupAddress,
+ sequencerInbox: sequencerInboxAddress,
+ });
+
+ expect(batchPosters).toEqual([batchPoster]);
+ expect(isAccurate).toBeTruthy();
+ });
+ it('when removing an existing batchPoster', async () => {
+ const batchPoster = '0xC0b97e2998edB3Bf5c6369e7f7eFfb49c36fA962';
+ const mockTransport = () =>
+ createTransport({
+ key: 'mock',
+ name: 'Mock Transport',
+ request: vi.fn(({ method, params }) => {
+ return mockData({
+ logs: {
+ '0x448fdaac1651fba39640e2103d83f78ff4695f95727f318f0f9e62c3e2d77a10':
+ safeSetBatchPosterHelper([batchPoster, false]),
+ '0x6e29af776e4b08f92b484a3d4ecc506a4b6455bbd335a2547c4e97d6151f588c':
+ safeSetBatchPosterHelper([batchPoster, false]),
+ '0x1acada0382b0ae715c41365c71b780871ec5adcfced8e57f0ae009c4b8738e2a':
+ safeSetBatchPosterHelper([batchPoster, true]),
+ },
+ method,
+ params,
+ });
+ }) as unknown as EIP1193RequestFn,
+ type: 'mock',
+ });
+
+ const mockClient = createPublicClient({
+ transport: mockTransport,
+ });
+
+ const { batchPosters, isAccurate } = await getBatchPosters(mockClient, {
+ rollup: rollupAddress,
+ sequencerInbox: sequencerInboxAddress,
+ });
+
+ expect(batchPosters).toEqual([batchPoster]);
+ expect(isAccurate).toBeTruthy();
+ });
+});
diff --git a/src/getValidators.unit.test.ts b/src/getValidators.unit.test.ts
index e98fbb34..18186056 100644
--- a/src/getValidators.unit.test.ts
+++ b/src/getValidators.unit.test.ts
@@ -7,7 +7,7 @@ import {
encodeFunctionData,
http,
} from 'viem';
-import { arbitrum } from 'viem/chains';
+import { arbitrum, arbitrumSepolia } from 'viem/chains';
import { it, expect, vi, describe } from 'vitest';
import { getValidators } from './getValidators';
import { rollupAdminLogicABI, safeL2ABI } from './abi';
@@ -23,7 +23,7 @@ function mockLog(transactionHash: string) {
address: '0x193e2887031c148ab54f5e856ea51ae521661200',
args: { id: 6n },
blockHash: '0x3bafb9574d8a3a7c09070935dc3ca936a5df06e2abd09cbd2a3cd489562e748f',
- blockNumber: 36723964n,
+ blockNumber: 35635757n,
data: '0x',
eventName: 'OwnerFunctionCalled',
logIndex: 42,
@@ -40,7 +40,7 @@ function mockTransaction(data: Hex) {
return {
accessList: [],
blockHash: '0x3bafb9574d8a3a7c09070935dc3ca936a5df06e2abd09cbd2a3cd489562e748f',
- blockNumber: 36723964n,
+ blockNumber: 35635757n,
chainId: 421614,
from: '0xfd5735380689a53e6b048e980f34cb94be9fd0c7',
gas: 7149526n,
@@ -69,7 +69,11 @@ function mockData({
logs: {
[transactionHash: string]: Hex;
};
- method: 'eth_getLogs' | 'eth_getTransactionByHash';
+ method:
+ | 'eth_getLogs'
+ | 'eth_getTransactionByHash'
+ | 'eth_getTransactionReceipt'
+ | 'eth_blockNumber';
params: string;
}) {
if (method === 'eth_getLogs') {
@@ -80,6 +84,16 @@ function mockData({
return mockTransaction(logs[params]);
}
+ if (method === 'eth_getTransactionReceipt') {
+ return {
+ blockNumber: 35635757,
+ };
+ }
+
+ if (method === 'eth_blockNumber') {
+ return 35635757;
+ }
+
return null;
}
@@ -133,7 +147,7 @@ it('getValidators return all validators (Xai)', async () => {
});
describe('createRollupFunctionSelector', () => {
- it('getValidators return all validators with complete flag set to true', async () => {
+ it('getValidators return all validators with isAccurate flag set to true', async () => {
const mockTransport = () =>
createTransport({
key: 'mock',
@@ -152,6 +166,7 @@ describe('createRollupFunctionSelector', () => {
const mockClient = createPublicClient({
transport: mockTransport,
+ chain: arbitrumSepolia,
});
const { validators, isAccurate } = await getValidators(mockClient, {
@@ -162,7 +177,7 @@ describe('createRollupFunctionSelector', () => {
expect(isAccurate).toBeTruthy();
});
- it('getValidators return all validators with complete flag set to false', async () => {
+ it('getValidators return all validators with isAccurate flag set to false', async () => {
const mockTransport = () =>
createTransport({
key: 'mock',
@@ -182,6 +197,7 @@ describe('createRollupFunctionSelector', () => {
const mockClient = createPublicClient({
transport: mockTransport,
+ chain: arbitrumSepolia,
});
const { validators, isAccurate } = await getValidators(mockClient, {
@@ -194,7 +210,7 @@ describe('createRollupFunctionSelector', () => {
});
describe('setValidatorFunctionSelector', () => {
- it('getValidators return all validators with complete flag set to true', async () => {
+ it('getValidators return all validators with isAccurate flag set to true', async () => {
const mockTransport = () =>
createTransport({
key: 'mock',
@@ -222,6 +238,7 @@ describe('setValidatorFunctionSelector', () => {
const mockClient = createPublicClient({
transport: mockTransport,
+ chain: arbitrumSepolia,
});
const { validators, isAccurate } = await getValidators(mockClient, {
@@ -235,7 +252,7 @@ describe('setValidatorFunctionSelector', () => {
expect(isAccurate).toBeTruthy();
});
- it('getValidators return all validators with complete flag set to false', async () => {
+ it('getValidators return all validators with isAccurate flag set to false', async () => {
const mockTransport = () =>
createTransport({
key: 'mock',
@@ -264,6 +281,7 @@ describe('setValidatorFunctionSelector', () => {
const mockClient = createPublicClient({
transport: mockTransport,
+ chain: arbitrumSepolia,
});
const { validators, isAccurate } = await getValidators(mockClient, {
@@ -279,7 +297,7 @@ describe('setValidatorFunctionSelector', () => {
});
describe('upgradeExecutorExecuteCallFunctionSelector', () => {
- it('getValidators return all validators with complete flag set to true', async () => {
+ it('getValidators return all validators with isAccurate flag set to true', async () => {
const mockTransport = () =>
createTransport({
key: 'mock',
@@ -310,6 +328,7 @@ describe('upgradeExecutorExecuteCallFunctionSelector', () => {
const mockClient = createPublicClient({
transport: mockTransport,
+ chain: arbitrumSepolia,
});
const { validators, isAccurate } = await getValidators(mockClient, {
@@ -324,7 +343,7 @@ describe('upgradeExecutorExecuteCallFunctionSelector', () => {
expect(isAccurate).toBeTruthy();
});
- it('getValidators return all validators with complete flag set to false', async () => {
+ it('getValidators return all validators with isAccurate flag set to false', async () => {
const mockTransport = () =>
createTransport({
key: 'mock',
@@ -356,6 +375,7 @@ describe('upgradeExecutorExecuteCallFunctionSelector', () => {
const mockClient = createPublicClient({
transport: mockTransport,
+ chain: arbitrumSepolia,
});
const { validators, isAccurate } = await getValidators(mockClient, {
@@ -368,7 +388,7 @@ describe('upgradeExecutorExecuteCallFunctionSelector', () => {
});
describe('safeL2FunctionSelector', () => {
- it('getValidators return all validators with complete flag set to true', async () => {
+ it('getValidators return all validators with isAccurate flag set to true', async () => {
const mockTransport = () =>
createTransport({
key: 'mock',
@@ -396,6 +416,7 @@ describe('safeL2FunctionSelector', () => {
const mockClient = createPublicClient({
transport: mockTransport,
+ chain: arbitrumSepolia,
});
const { validators, isAccurate } = await getValidators(mockClient, {
@@ -409,7 +430,7 @@ describe('safeL2FunctionSelector', () => {
expect(isAccurate).toBeTruthy();
});
- it('getValidators return all validators with complete flag set to false', async () => {
+ it('getValidators return all validators with isAccurate flag set to false', async () => {
const mockTransport = () =>
createTransport({
key: 'mock',
@@ -438,6 +459,7 @@ describe('safeL2FunctionSelector', () => {
const mockClient = createPublicClient({
transport: mockTransport,
+ chain: arbitrumSepolia,
});
const { validators, isAccurate } = await getValidators(mockClient, {
@@ -480,6 +502,7 @@ describe('Detect validators added or removed multiple times', () => {
const mockClient = createPublicClient({
transport: mockTransport,
+ chain: arbitrumSepolia,
});
const { validators, isAccurate } = await getValidators(mockClient, {
@@ -514,6 +537,7 @@ describe('Detect validators added or removed multiple times', () => {
const mockClient = createPublicClient({
transport: mockTransport,
+ chain: arbitrumSepolia,
});
const { validators, isAccurate } = await getValidators(mockClient, {
@@ -546,6 +570,7 @@ describe('Detect validators added or removed multiple times', () => {
const mockClient = createPublicClient({
transport: mockTransport,
+ chain: arbitrumSepolia,
});
const { validators, isAccurate } = await getValidators(mockClient, {
@@ -580,6 +605,7 @@ describe('Detect validators added or removed multiple times', () => {
const mockClient = createPublicClient({
transport: mockTransport,
+ chain: arbitrumSepolia,
});
const { validators, isAccurate } = await getValidators(mockClient, {
diff --git a/src/index.ts b/src/index.ts
index 44df6553..e53666c0 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -101,6 +101,11 @@ import {
SequencerInboxMaxTimeVariation,
} from './getDefaultSequencerInboxMaxTimeVariation';
import { getValidators, GetValidatorsParams, GetValidatorsReturnType } from './getValidators';
+import {
+ getBatchPosters,
+ GetBatchPostersParams,
+ GetBatchPostersReturnType,
+} from './getBatchPosters';
export {
arbOwnerPublicActions,
@@ -186,4 +191,8 @@ export {
getValidators,
GetValidatorsParams,
GetValidatorsReturnType,
+ //
+ getBatchPosters,
+ GetBatchPostersParams,
+ GetBatchPostersReturnType,
};
diff --git a/src/sequencerInboxPrepareTransactionRequest.ts b/src/sequencerInboxPrepareTransactionRequest.ts
index 241f430f..2e539566 100644
--- a/src/sequencerInboxPrepareTransactionRequest.ts
+++ b/src/sequencerInboxPrepareTransactionRequest.ts
@@ -36,7 +36,7 @@ type SequencerInboxPrepareFunctionDataParameters(
+export function sequencerInboxPrepareFunctionData(
params: SequencerInboxPrepareFunctionDataParameters,
) {
const { upgradeExecutor } = params;