@aztec/ethereum 0.0.1-commit.f650c0a5c → 0.0.1-commit.f7ea82942

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.ts CHANGED
@@ -36,6 +36,24 @@ export function makeL1HttpTransport(rpcUrls: string[], opts?: { timeout?: number
36
36
  return fallback(rpcUrls.map(url => http(url, { batch: false, timeout: opts?.timeout })));
37
37
  }
38
38
 
39
+ /**
40
+ * Returns the individual RPC URLs underlying a viem public client that was constructed with a
41
+ * fallback HTTP transport (see {@link makeL1HttpTransport}). Returns an empty array if the
42
+ * transport shape is not recognized (e.g. mock clients in tests, or non-fallback transports).
43
+ */
44
+ export function getRpcUrlsFromClient(client: ViemPublicClient): string[] {
45
+ const transport = client.transport as unknown as {
46
+ transports?: { value?: { url?: string } }[];
47
+ value?: { url?: string };
48
+ url?: string;
49
+ };
50
+ if (Array.isArray(transport?.transports)) {
51
+ return transport.transports.map(t => t?.value?.url).filter((url): url is string => typeof url === 'string');
52
+ }
53
+ const singleUrl = transport?.value?.url ?? transport?.url;
54
+ return typeof singleUrl === 'string' ? [singleUrl] : [];
55
+ }
56
+
39
57
  // TODO: Use these methods to abstract the creation of viem clients.
40
58
 
41
59
  /** Returns a viem public client given the L1 config. */
@@ -0,0 +1,147 @@
1
+ import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
2
+ import type { CheckpointNumber } from '@aztec/foundation/branded-types';
3
+ import type { Fr } from '@aztec/foundation/curves/bn254';
4
+
5
+ import type { StateOverride } from 'viem';
6
+
7
+ import { type FeeHeader, RollupContract } from './rollup.js';
8
+
9
+ export type PendingCheckpointOverrideState = {
10
+ archive?: Fr;
11
+ feeHeader?: FeeHeader;
12
+ };
13
+
14
+ /** Describes the simulated L1 rollup state that downstream calls should observe. */
15
+ export type SimulationOverridesPlan = {
16
+ pendingCheckpointNumber?: CheckpointNumber;
17
+ pendingCheckpointState?: PendingCheckpointOverrideState;
18
+ disableBlobCheck?: boolean;
19
+ };
20
+
21
+ /** Builds a single-checkpoint simulation plan before it is translated into a viem state override. */
22
+ export class SimulationOverridesBuilder {
23
+ private pendingCheckpointNumber?: CheckpointNumber;
24
+ private pendingCheckpointState?: PendingCheckpointOverrideState;
25
+ private disableBlobCheck = false;
26
+
27
+ /** Starts from an existing plan so callers can extend or specialize it. */
28
+ public static from(plan: SimulationOverridesPlan | undefined): SimulationOverridesBuilder {
29
+ return new SimulationOverridesBuilder().merge(plan);
30
+ }
31
+
32
+ /** Merges another plan into this builder. Later values win. */
33
+ public merge(plan: SimulationOverridesPlan | undefined): this {
34
+ if (!plan) {
35
+ return this;
36
+ }
37
+
38
+ this.pendingCheckpointNumber = plan.pendingCheckpointNumber;
39
+ this.pendingCheckpointState = plan.pendingCheckpointState
40
+ ? { ...(this.pendingCheckpointState ?? {}), ...plan.pendingCheckpointState }
41
+ : this.pendingCheckpointState;
42
+ this.disableBlobCheck = this.disableBlobCheck || (plan.disableBlobCheck ?? false);
43
+
44
+ return this;
45
+ }
46
+
47
+ /** Sets the checkpoint number that archive and fee header overrides should attach to. */
48
+ public forPendingCheckpoint(pendingCheckpointNumber: CheckpointNumber | undefined): this {
49
+ this.pendingCheckpointNumber = pendingCheckpointNumber;
50
+ return this;
51
+ }
52
+
53
+ /** Overrides the archive root for the configured pending checkpoint. */
54
+ public withPendingArchive(archive: Fr): this {
55
+ this.assertPendingCheckpointNumber();
56
+ this.pendingCheckpointState = { ...(this.pendingCheckpointState ?? {}), archive };
57
+ return this;
58
+ }
59
+
60
+ /** Overrides the fee header for the configured pending checkpoint. */
61
+ public withPendingFeeHeader(feeHeader: FeeHeader): this {
62
+ this.assertPendingCheckpointNumber();
63
+ this.pendingCheckpointState = { ...(this.pendingCheckpointState ?? {}), feeHeader };
64
+ return this;
65
+ }
66
+
67
+ /** Disables blob checking for simulations that cannot provide DA inputs. */
68
+ public withoutBlobCheck(): this {
69
+ this.disableBlobCheck = true;
70
+ return this;
71
+ }
72
+
73
+ /** Builds the final plan, or `undefined` when no overrides were configured. */
74
+ public build(): SimulationOverridesPlan | undefined {
75
+ if (!this.pendingCheckpointState && this.pendingCheckpointNumber === undefined && !this.disableBlobCheck) {
76
+ return undefined;
77
+ }
78
+
79
+ return {
80
+ pendingCheckpointNumber: this.pendingCheckpointNumber,
81
+ pendingCheckpointState: this.pendingCheckpointState,
82
+ disableBlobCheck: this.disableBlobCheck || undefined,
83
+ };
84
+ }
85
+
86
+ private assertPendingCheckpointNumber(): void {
87
+ if (this.pendingCheckpointNumber === undefined) {
88
+ throw new Error('pendingCheckpointNumber must be set before attaching archive or fee header overrides');
89
+ }
90
+ }
91
+ }
92
+
93
+ /** Translates a simulation plan into the viem state override shape expected by rollup calls. */
94
+ export async function buildSimulationOverridesStateOverride(
95
+ rollup: RollupContract,
96
+ plan: SimulationOverridesPlan | undefined,
97
+ ): Promise<StateOverride> {
98
+ if (!plan) {
99
+ return [];
100
+ }
101
+
102
+ const rollupStateDiff: NonNullable<StateOverride[number]['stateDiff']> = [];
103
+
104
+ if (plan.pendingCheckpointNumber !== undefined) {
105
+ rollupStateDiff.push(
106
+ ...extractRollupStateDiff(await rollup.makePendingCheckpointNumberOverride(plan.pendingCheckpointNumber)),
107
+ );
108
+ }
109
+
110
+ if (plan.pendingCheckpointState && plan.pendingCheckpointNumber === undefined) {
111
+ throw new Error('pendingCheckpointState requires pendingCheckpointNumber to be set');
112
+ }
113
+
114
+ if (plan.pendingCheckpointState?.archive) {
115
+ rollupStateDiff.push(
116
+ ...extractRollupStateDiff(
117
+ rollup.makeArchiveOverride(plan.pendingCheckpointNumber!, plan.pendingCheckpointState.archive),
118
+ ),
119
+ );
120
+ }
121
+
122
+ if (plan.pendingCheckpointState?.feeHeader) {
123
+ rollupStateDiff.push(
124
+ ...extractRollupStateDiff(
125
+ await rollup.makeFeeHeaderOverride(plan.pendingCheckpointNumber!, plan.pendingCheckpointState.feeHeader),
126
+ ),
127
+ );
128
+ }
129
+
130
+ if (plan.disableBlobCheck) {
131
+ rollupStateDiff.push({
132
+ slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true),
133
+ value: toPaddedHex(0n, true),
134
+ });
135
+ }
136
+
137
+ if (rollupStateDiff.length === 0) {
138
+ return [];
139
+ }
140
+
141
+ return [{ address: rollup.address, stateDiff: rollupStateDiff }];
142
+ }
143
+
144
+ function extractRollupStateDiff(override: StateOverride | StateOverride[number] | undefined) {
145
+ const entries = Array.isArray(override) ? override : override ? [override] : [];
146
+ return entries.flatMap(entry => entry.stateDiff ?? []);
147
+ }
@@ -1,3 +1,4 @@
1
+ import { maxBigint } from '@aztec/foundation/bigint';
1
2
  import { CheckpointNumber } from '@aztec/foundation/branded-types';
2
3
  import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
3
4
  import { Fr } from '@aztec/foundation/curves/bn254';
@@ -82,9 +83,16 @@ export class InboxContract {
82
83
  .map(log => this.mapMessageSentLog(log));
83
84
  }
84
85
 
85
- /** Fetches MessageSent events for a specific message hash at a specific block. */
86
- async getMessageSentEventByHash(msgHash: Hex, l1BlockHash: Hex): Promise<MessageSentLog> {
87
- const [log] = await this.inbox.getEvents.MessageSent({ hash: msgHash }, { blockHash: l1BlockHash });
86
+ /** Fetches MessageSent events for a specific message hash around a specific block. */
87
+ async getMessageSentEventByHash(msgHash: Hex, aroundL1BlockNumber: bigint): Promise<MessageSentLog> {
88
+ // We don't use blockHash here because we don't want the query to throw if the L1 block number no longer exists on chain
89
+ // due to an L1 reorg. The use case for this method is usually checking if a message still exists on the Inbox after
90
+ // a reorg, so it's possible the message was moved one block up or down, and that the original L1 block where we
91
+ // saw it no longer exists, rendering the block-by-hash approach invalid.
92
+ const [log] = await this.inbox.getEvents.MessageSent(
93
+ { hash: msgHash },
94
+ { fromBlock: maxBigint(aroundL1BlockNumber - 5n, 1n), toBlock: aroundL1BlockNumber + 5n },
95
+ );
88
96
  return log && this.mapMessageSentLog(log);
89
97
  }
90
98
 
@@ -1,3 +1,4 @@
1
+ export * from './chain_state_override.js';
1
2
  export * from './empire_base.js';
2
3
  export * from './errors.js';
3
4
  export * from './fee_asset_handler.js';
@@ -15,6 +15,7 @@ import {
15
15
  type Account,
16
16
  type GetContractReturnType,
17
17
  type Hex,
18
+ type Log,
18
19
  type StateOverride,
19
20
  type WatchContractEventReturnType,
20
21
  encodeAbiParameters,
@@ -128,6 +129,17 @@ export type L1FeeData = {
128
129
  blobFee: bigint;
129
130
  };
130
131
 
132
+ /** Field offsets within the CompressedTempCheckpointLog struct in Solidity storage. */
133
+ export enum TempCheckpointLogField {
134
+ HeaderHash = 0,
135
+ BlobCommitmentsHash = 1,
136
+ OutHash = 2,
137
+ AttestationsHash = 3,
138
+ PayloadDigest = 4,
139
+ SlotNumber = 5,
140
+ FeeHeader = 6,
141
+ }
142
+
131
143
  /** Components of the minimum fee per mana, as returned by the L1 rollup contract. */
132
144
  export type ManaMinFeeComponents = {
133
145
  sequencerCost: bigint;
@@ -514,8 +526,9 @@ export class RollupContract {
514
526
  return this.rollup.read.getCheckpointReward();
515
527
  }
516
528
 
517
- async getCheckpointNumber(): Promise<CheckpointNumber> {
518
- return CheckpointNumber.fromBigInt(await this.rollup.read.getPendingCheckpointNumber());
529
+ async getCheckpointNumber(options?: { blockNumber?: bigint }): Promise<CheckpointNumber> {
530
+ await checkBlockTag(options?.blockNumber, this.client);
531
+ return CheckpointNumber.fromBigInt(await this.rollup.read.getPendingCheckpointNumber(options));
519
532
  }
520
533
 
521
534
  async getProvenCheckpointNumber(options?: { blockNumber?: bigint }): Promise<CheckpointNumber> {
@@ -523,18 +536,31 @@ export class RollupContract {
523
536
  return CheckpointNumber.fromBigInt(await this.rollup.read.getProvenCheckpointNumber(options));
524
537
  }
525
538
 
526
- async getSlotNumber(): Promise<SlotNumber> {
527
- return SlotNumber.fromBigInt(await this.rollup.read.getCurrentSlot());
539
+ async getSlotNumber(options?: { blockNumber?: bigint }): Promise<SlotNumber> {
540
+ await checkBlockTag(options?.blockNumber, this.client);
541
+ return SlotNumber.fromBigInt(await this.rollup.read.getCurrentSlot(options));
528
542
  }
529
543
 
530
- async getL1FeesAt(timestamp: bigint): Promise<L1FeeData> {
531
- const result = await this.rollup.read.getL1FeesAt([timestamp]);
544
+ async getL1FeesAt(timestamp: bigint, options?: { blockNumber?: bigint }): Promise<L1FeeData> {
545
+ await checkBlockTag(options?.blockNumber, this.client);
546
+ const result = await this.rollup.read.getL1FeesAt([timestamp], options);
532
547
  return {
533
548
  baseFee: result.baseFee,
534
549
  blobFee: result.blobFee,
535
550
  };
536
551
  }
537
552
 
553
+ async getFeeHeader(checkpointNumber: bigint): Promise<FeeHeader> {
554
+ const result = await this.rollup.read.getFeeHeader([checkpointNumber]);
555
+ return {
556
+ excessMana: result.excessMana,
557
+ manaUsed: result.manaUsed,
558
+ ethPerFeeAsset: result.ethPerFeeAsset,
559
+ congestionCost: result.congestionCost,
560
+ proverCost: result.proverCost,
561
+ };
562
+ }
563
+
538
564
  getEthPerFeeAsset(): Promise<bigint> {
539
565
  return this.rollup.read.getEthPerFeeAsset();
540
566
  }
@@ -609,8 +635,9 @@ export class RollupContract {
609
635
  return EthAddress.fromString(result);
610
636
  }
611
637
 
612
- async getCheckpoint(checkpointNumber: CheckpointNumber): Promise<CheckpointLog> {
613
- const result = await this.rollup.read.getCheckpoint([BigInt(checkpointNumber)]);
638
+ async getCheckpoint(checkpointNumber: CheckpointNumber, options?: { blockNumber?: bigint }): Promise<CheckpointLog> {
639
+ await checkBlockTag(options?.blockNumber, this.client);
640
+ const result = await this.rollup.read.getCheckpoint([BigInt(checkpointNumber)], options);
614
641
  return {
615
642
  archive: Fr.fromString(result.archive),
616
643
  headerHash: Buffer32.fromString(result.headerHash),
@@ -643,6 +670,30 @@ export class RollupContract {
643
670
  );
644
671
  }
645
672
 
673
+ /**
674
+ * Returns the effective pending checkpoint, accounting for potential prunes.
675
+ * When a prune can happen, the L1 contract uses the proven checkpoint instead of the pending one.
676
+ * This mirrors the behavior of getEffectivePendingCheckpointNumber in STFLib.sol.
677
+ * @param atTimestamp - The timestamp to evaluate pruneability at. Defaults to the current L1 block timestamp.
678
+ * @param options - Optional L1 block number to pin the queries to.
679
+ */
680
+ getEffectivePendingCheckpoint(atTimestamp?: bigint, options?: { blockNumber?: bigint }) {
681
+ return retry(
682
+ async () => {
683
+ const timestamp = atTimestamp ?? (await this.client.getBlock()).timestamp;
684
+ const canPrune = await this.canPruneAtTime(timestamp, options);
685
+ if (canPrune) {
686
+ const provenCheckpointNumber = await this.getProvenCheckpointNumber(options);
687
+ return await this.getCheckpoint(provenCheckpointNumber, options);
688
+ }
689
+ const pendingCheckpointNumber = await this.getCheckpointNumber(options);
690
+ return await this.getCheckpoint(pendingCheckpointNumber, options);
691
+ },
692
+ 'getting effective pending checkpoint',
693
+ makeBackoff([0.5, 0.5, 0.5]),
694
+ );
695
+ }
696
+
646
697
  async getTips(): Promise<{ pending: CheckpointNumber; proven: CheckpointNumber }> {
647
698
  const { pending, proven } = await this.rollup.read.getTips();
648
699
  return {
@@ -765,19 +816,11 @@ export class RollupContract {
765
816
  archive: Buffer,
766
817
  account: `0x${string}` | Account,
767
818
  timestamp: bigint,
768
- opts: {
769
- forcePendingCheckpointNumber?: CheckpointNumber;
770
- forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
771
- } = {},
819
+ stateOverride: StateOverride = [],
772
820
  ): Promise<{ slot: SlotNumber; checkpointNumber: CheckpointNumber; timeOfNextL1Slot: bigint }> {
773
821
  const timeOfNextL1Slot = timestamp;
774
822
  const who = typeof account === 'string' ? account : account.address;
775
823
 
776
- const stateOverride = RollupContract.mergeStateOverrides(
777
- await this.makePendingCheckpointNumberOverride(opts.forcePendingCheckpointNumber),
778
- opts.forceArchive ? this.makeArchiveOverride(opts.forceArchive.checkpointNumber, opts.forceArchive.archive) : [],
779
- );
780
-
781
824
  try {
782
825
  const {
783
826
  result: [slot, checkpointNumber],
@@ -1182,7 +1225,7 @@ export class RollupContract {
1182
1225
  }
1183
1226
 
1184
1227
  public listenToCheckpointInvalidated(
1185
- callback: (args: { checkpointNumber: CheckpointNumber }) => unknown,
1228
+ callback: (args: { checkpointNumber: CheckpointNumber; event: Log }) => unknown,
1186
1229
  ): WatchContractEventReturnType {
1187
1230
  return this.rollup.watchEvent.CheckpointInvalidated(
1188
1231
  {},
@@ -1191,7 +1234,7 @@ export class RollupContract {
1191
1234
  for (const log of logs) {
1192
1235
  const args = log.args;
1193
1236
  if (args.checkpointNumber !== undefined) {
1194
- callback({ checkpointNumber: CheckpointNumber.fromBigInt(args.checkpointNumber) });
1237
+ callback({ checkpointNumber: CheckpointNumber.fromBigInt(args.checkpointNumber), event: log });
1195
1238
  }
1196
1239
  }
1197
1240
  },
@@ -1224,6 +1267,17 @@ export class RollupContract {
1224
1267
  );
1225
1268
  }
1226
1269
 
1270
+ /**
1271
+ * Fetches OwnershipTransferred events emitted on the L1 block this rollup was deployed on.
1272
+ * The Rollup inherits from Ownable and emits this event in its constructor, so the event
1273
+ * is guaranteed to exist on `l1StartBlock` for any correctly deployed rollup. Used as a
1274
+ * probe to detect RPC nodes that prune historical logs.
1275
+ */
1276
+ async getOwnershipTransferredEventsAtDeploy() {
1277
+ const l1StartBlock = await this.getL1StartBlock();
1278
+ return await this.rollup.getEvents.OwnershipTransferred({}, { fromBlock: l1StartBlock, toBlock: l1StartBlock });
1279
+ }
1280
+
1227
1281
  /** Fetches CheckpointProposed events within the given block range. */
1228
1282
  async getCheckpointProposedEvents(fromBlock: bigint, toBlock: bigint): Promise<CheckpointProposedLog[]> {
1229
1283
  const logs = await this.rollup.getEvents.CheckpointProposed({}, { fromBlock, toBlock });
@@ -1256,4 +1310,39 @@ export class RollupContract {
1256
1310
  },
1257
1311
  }));
1258
1312
  }
1313
+
1314
+ /** Packs pending and proven checkpoint numbers into the chain tips storage format. */
1315
+ static packChainTips(pendingCheckpointNumber: bigint, provenCheckpointNumber: bigint): bigint {
1316
+ return (pendingCheckpointNumber << 128n) | (provenCheckpointNumber & ((1n << 128n) - 1n));
1317
+ }
1318
+
1319
+ /** Storage slot for the chain tips (offset 0 within the STF storage struct). */
1320
+ static get chainTipsStorageSlot(): bigint {
1321
+ return BigInt(RollupContract.stfStorageSlot);
1322
+ }
1323
+
1324
+ /**
1325
+ * Computes the storage slot for a field within a tempCheckpointLog entry.
1326
+ * @param checkpointNumber - The checkpoint number
1327
+ * @param field - The field within the CompressedTempCheckpointLog struct
1328
+ */
1329
+ async getTempCheckpointLogStorageSlot(
1330
+ checkpointNumber: CheckpointNumber,
1331
+ field: TempCheckpointLogField,
1332
+ ): Promise<bigint> {
1333
+ const fieldOffset = BigInt(field);
1334
+ const [epochDuration, proofSubmissionEpochs] = await Promise.all([
1335
+ this.getEpochDuration(),
1336
+ this.getProofSubmissionEpochs(),
1337
+ ]);
1338
+ const roundaboutSize = BigInt(epochDuration) * (BigInt(proofSubmissionEpochs) + 1n) + 1n;
1339
+ const tempCheckpointLogsBase = BigInt(RollupContract.stfStorageSlot) + 2n;
1340
+ const circularIndex = BigInt(checkpointNumber) % roundaboutSize;
1341
+ const entryBase = BigInt(
1342
+ keccak256(
1343
+ encodeAbiParameters([{ type: 'uint256' }, { type: 'uint256' }], [circularIndex, tempCheckpointLogsBase]),
1344
+ ),
1345
+ );
1346
+ return entryBase + fieldOffset;
1347
+ }
1259
1348
  }
package/src/queries.ts CHANGED
@@ -1,11 +1,77 @@
1
1
  import { EthAddress } from '@aztec/foundation/eth-address';
2
2
 
3
+ import { BaseError, type Block } from 'viem';
4
+
3
5
  import { DefaultL1ContractsConfig, type L1ContractsConfig } from './config.js';
4
6
  import { ReadOnlyGovernanceContract } from './contracts/governance.js';
5
7
  import { GovernanceProposerContract } from './contracts/governance_proposer.js';
6
8
  import { InboxContract } from './contracts/inbox.js';
7
9
  import { RollupContract } from './contracts/rollup.js';
8
- import type { ViemPublicClient } from './types.js';
10
+ import type { ViemClient, ViemPublicClient } from './types.js';
11
+
12
+ /**
13
+ * Returns the L1 finalized block, or `undefined` if the chain does not yet have one
14
+ * (common on freshly started devnets). Rethrows any other RPC error.
15
+ */
16
+ export async function getFinalizedL1Block(client: ViemClient): Promise<Block<bigint, false, 'finalized'> | undefined> {
17
+ try {
18
+ return await client.getBlock({ blockTag: 'finalized', includeTransactions: false });
19
+ } catch (err) {
20
+ if (isFinalizedBlockTagNotFoundError(err)) {
21
+ return undefined;
22
+ }
23
+ throw err;
24
+ }
25
+ }
26
+
27
+ // Error messages returned by popular Ethereum execution clients when
28
+ // eth_getBlockByNumber / eth_call is called with blockTag "finalized" or
29
+ // "safe" and no such block has been produced yet:
30
+ //
31
+ // geth "finalized block not found"
32
+ // "safe block not found"
33
+ // reth "block not found: finalized"
34
+ // "block not found: safe"
35
+ // nethermind "Unknown block error" (same for both tags)
36
+ // besu "Unknown block"
37
+ // erigon 'block "finalized" not available (head block: N)'
38
+ // 'block "safe" not available (head block: N)'
39
+ //
40
+ // A combined regex covers all five:
41
+ const FINALIZED_BLOCK_TAG_NOT_FOUND_MESSAGE_RE =
42
+ /(finalized|safe) block not found|block not found: (finalized|safe)|unknown block|block "(finalized|safe)" not available/i;
43
+
44
+ /**
45
+ * Returns true if the error originates from an RPC call that failed because
46
+ * the "finalized" (or "safe") block tag is not yet available on the chain.
47
+ */
48
+ export function isFinalizedBlockTagNotFoundError(err: unknown): boolean {
49
+ if (!err) {
50
+ return false;
51
+ }
52
+
53
+ if (err instanceof BaseError) {
54
+ const hit = err.walk((e: any) => matchesFinalizedBlockTagNotFound(e));
55
+ if (hit) {
56
+ return true;
57
+ }
58
+ }
59
+
60
+ for (let cur: any = err, i = 0; cur && i < 10; cur = cur.cause, i++) {
61
+ if (matchesFinalizedBlockTagNotFound(cur)) {
62
+ return true;
63
+ }
64
+ }
65
+ return false;
66
+ }
67
+
68
+ function matchesFinalizedBlockTagNotFound(e: any): boolean {
69
+ if (!e) {
70
+ return false;
71
+ }
72
+ const text = `${e.details ?? ''} ${e.message ?? ''} ${e.shortMessage ?? ''}`;
73
+ return FINALIZED_BLOCK_TAG_NOT_FOUND_MESSAGE_RE.test(text);
74
+ }
9
75
 
10
76
  /** Reads the L1ContractsConfig from L1 contracts. */
11
77
  export async function getL1ContractsConfig(
@@ -221,6 +221,11 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
221
221
  });
222
222
  }
223
223
 
224
+ public async waitUntilNextL2Slot(): Promise<void> {
225
+ const targetSlot = SlotNumber.add((await this.run()).l2SlotNumber, 1);
226
+ return this.waitUntilL2Slot(targetSlot);
227
+ }
228
+
224
229
  public waitUntilL1Block(block: number | bigint): Promise<void> {
225
230
  const targetBlock = typeof block === 'bigint' ? block.valueOf() : block;
226
231
  if (this.l1BlockNumber >= targetBlock) {
@@ -53,7 +53,17 @@ export class RollupCheatCodes {
53
53
  /** Returns the current slot */
54
54
  public async getSlot(): Promise<SlotNumber> {
55
55
  const ts = BigInt((await this.client.getBlock()).timestamp);
56
- return SlotNumber.fromBigInt(await this.rollup.read.getSlotAt([ts]));
56
+ return this.getSlotAt(ts);
57
+ }
58
+
59
+ /** Returns the slot number at a given timestamp. */
60
+ public async getSlotAt(timestamp: bigint): Promise<SlotNumber> {
61
+ return SlotNumber.fromBigInt(await this.rollup.read.getSlotAt([timestamp]));
62
+ }
63
+
64
+ /** Returns the timestamp for the start of a given slot. */
65
+ public async getTimestampForSlot(slot: SlotNumber): Promise<bigint> {
66
+ return await this.rollup.read.getTimestampForSlot([BigInt(slot)]);
57
67
  }
58
68
 
59
69
  /** Returns the number of seconds until the start of the given slot based on L1 block timestamp. */