@black-market/sdk 0.12.0 → 0.14.0

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.
Files changed (57) hide show
  1. package/README.md +96 -96
  2. package/dist/lifecycle/index.d.ts +3 -1
  3. package/dist/lifecycle/index.d.ts.map +1 -1
  4. package/dist/lifecycle/index.js +3 -1
  5. package/dist/lifecycle/index.js.map +1 -1
  6. package/dist/lifecycle/lens.d.ts +409 -0
  7. package/dist/lifecycle/lens.d.ts.map +1 -0
  8. package/dist/lifecycle/lens.js +30 -0
  9. package/dist/lifecycle/lens.js.map +1 -0
  10. package/dist/lifecycle/mining.d.ts +47 -0
  11. package/dist/lifecycle/mining.d.ts.map +1 -0
  12. package/dist/lifecycle/mining.js +89 -0
  13. package/dist/lifecycle/mining.js.map +1 -0
  14. package/dist/lifecycle/planner.d.ts.map +1 -1
  15. package/dist/lifecycle/planner.js +121 -78
  16. package/dist/lifecycle/planner.js.map +1 -1
  17. package/dist/lifecycle/presets.d.ts +3 -0
  18. package/dist/lifecycle/presets.d.ts.map +1 -1
  19. package/dist/lifecycle/presets.js +1 -0
  20. package/dist/lifecycle/presets.js.map +1 -1
  21. package/dist/lifecycle/progress.d.ts +51 -1
  22. package/dist/lifecycle/progress.d.ts.map +1 -1
  23. package/dist/lifecycle/progress.js +149 -73
  24. package/dist/lifecycle/progress.js.map +1 -1
  25. package/dist/lifecycle/rpc.d.ts +14 -4
  26. package/dist/lifecycle/rpc.d.ts.map +1 -1
  27. package/dist/lifecycle/rpc.js +171 -12
  28. package/dist/lifecycle/rpc.js.map +1 -1
  29. package/dist/lifecycle/simulation.d.ts +5 -3
  30. package/dist/lifecycle/simulation.d.ts.map +1 -1
  31. package/dist/lifecycle/simulation.js +137 -115
  32. package/dist/lifecycle/simulation.js.map +1 -1
  33. package/dist/lifecycle/snapshot.d.ts +8 -0
  34. package/dist/lifecycle/snapshot.d.ts.map +1 -0
  35. package/dist/lifecycle/snapshot.js +79 -0
  36. package/dist/lifecycle/snapshot.js.map +1 -0
  37. package/dist/lifecycle/types.d.ts +27 -5
  38. package/dist/lifecycle/types.d.ts.map +1 -1
  39. package/dist/lifecycle/types.js.map +1 -1
  40. package/docs/launch-lifecycle.md +627 -1495
  41. package/docs/reference.md +365 -0
  42. package/examples/.env.example +3 -2
  43. package/examples/README.md +171 -552
  44. package/examples/launch-example-support.ts +11 -3
  45. package/examples/launch-example.ts +1 -1
  46. package/examples/launch-lifecycle-smoke.ts +1 -1
  47. package/package.json +1 -1
  48. package/src/lifecycle/index.ts +5 -1
  49. package/src/lifecycle/lens.ts +42 -0
  50. package/src/lifecycle/mining.ts +80 -0
  51. package/src/lifecycle/planner.ts +122 -73
  52. package/src/lifecycle/presets.ts +4 -0
  53. package/src/lifecycle/progress.ts +132 -59
  54. package/src/lifecycle/rpc.ts +163 -13
  55. package/src/lifecycle/simulation.ts +134 -106
  56. package/src/lifecycle/snapshot.ts +72 -0
  57. package/src/lifecycle/types.ts +24 -6
@@ -1,7 +1,7 @@
1
1
  import { decodeErrorResult, decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctionResult, parseAbi, toHex, type Hex } from "viem";
2
2
  import { fixedFeePoolHookV1Abi, launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
3
3
  import { hashLaunchIdentity, hashLaunchPlan, LifecyclePhase, type LaunchPlanV1, type LaunchProgressV1, type LaunchReceiptV1 } from "./schema.js";
4
- import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readLifecycleChainId, readNitroPosterGas, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity, type LifecycleLimitObservation } from "./rpc.js";
4
+ import { assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleRpc, lifecycleSessionRead, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readLifecycleChainId, readLifecycleGasPrice, readNitroPosterGas, registerLifecycleFinalCheck, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity, type LifecycleLimitObservation } from "./rpc.js";
5
5
  import { LifecyclePlanningError, type ControlledLifecycleFork, type LifecycleBlock, type LifecycleLimitContext, type LifecyclePostcondition, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleSimulationStep, type LifecycleTransaction, type PlannedLaunch, type ResolvedLifecycleLimits, type SimulateLaunchPlanOptions } from "./types.js";
6
6
  import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion } from "./markets.js";
7
7
  import { hmmV2ForwardingReserve } from "./presets.js";
@@ -56,7 +56,6 @@ function executionFailure(returnData: Hex | undefined, error: unknown): Pick<Lif
56
56
  return { ...diagnostic, failureCategory: "opaque", error: nativeErrorKind === "execution-reverted" ? "Native execution reverted without a reviewed error" : "Execution failed without a reviewed error" };
57
57
  }
58
58
 
59
- const nitroProofsByInvocation = new WeakMap<LifecycleRpcClient, Map<string, Promise<void>>>();
60
59
 
61
60
  function conditionRequest(condition: LifecyclePostcondition, planned: PlannedLaunch): { to: Hex; data: Hex } {
62
61
  if (condition.kind === "allowance") return {
@@ -109,8 +108,6 @@ async function proveNitroBackend(client: LifecycleRpcClient, planned: PlannedLau
109
108
  throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Simulated ArbOS version/compute getters differ from the exact pinned chain context");
110
109
  }
111
110
  }
112
- // The enclosing simulation rechecks chain and canonical hash after its proofs
113
- // (including every refusal path); a separate probe-only recheck is redundant.
114
111
  }
115
112
 
116
113
  async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunch, block: LifecycleBlock, transactions: readonly LifecycleTransaction[], gasLimits: readonly bigint[], limits: ResolvedLifecycleLimits, gasPrice: bigint, validation: boolean): Promise<LifecycleSimulationStep[]> {
@@ -121,7 +118,7 @@ async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunc
121
118
  // parent gasLimit are set.
122
119
  const blockStateCalls = transactions.map((transaction, index) => ({
123
120
  blockOverrides: { number: toHex(block.number + BigInt(index + 1)), time: toHex(block.timestamp + BigInt(index + 1)), gasLimit: toHex(block.gasLimit) },
124
- calls: [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gasLimits[index] ?? limits.executionGasCeiling), gasPrice: toHex(gasPrice) }],
121
+ calls: [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gasLimits[index] ?? limits.executionGasCeiling), gasPrice: toHex(gasPrice), ...(transaction.nonce === undefined ? {} : { nonce: toHex(transaction.nonce) }) }],
125
122
  }));
126
123
  const result = await lifecycleRpc(client, "eth_simulateV1", [{ blockStateCalls, validation, traceTransfers: false, returnFullTransactions: false }, toHex(block.number)]);
127
124
  if (!Array.isArray(result) || result.length !== transactions.length) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Sequential simulation omitted blocks");
@@ -173,10 +170,6 @@ async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunc
173
170
  }
174
171
  return steps;
175
172
  }
176
- /** A decoded semantic refusal or opaque deployment wrapper is never gas evidence. */
177
- function looksGasExhausted(step: LifecycleSimulationStep, gasLimit: bigint): boolean {
178
- return !step.success && step.failureCategory === "capacity" && step.gasUsed !== undefined && step.gasUsed >= gasLimit - gasLimit / 64n;
179
- }
180
173
 
181
174
  function simulationFailureKind(failure: unknown): "abort" | "out-of-gas" | "execution" | "source" {
182
175
  for (let depth = 0; depth < 8 && failure !== null && typeof failure === "object"; depth += 1) {
@@ -194,6 +187,18 @@ function simulationFailureKind(failure: unknown): "abort" | "out-of-gas" | "exec
194
187
  return "source";
195
188
  }
196
189
 
190
+ function upfrontFundsFailure(failure: unknown): boolean {
191
+ for (let depth = 0; depth < 8 && failure !== null && typeof failure === "object"; depth += 1) {
192
+ const error = failure as { name?: unknown; code?: unknown; status?: unknown; statusCode?: unknown; message?: unknown; details?: unknown; cause?: unknown };
193
+ const status = error.status ?? error.statusCode;
194
+ if (error.name === "AbortError" || typeof status === "number" && status >= 400 && status <= 599 || error.code === 3) return false;
195
+ const message = typeof error.details === "string" ? error.details : typeof error.message === "string" ? error.message : "";
196
+ if (error.code === -38014 || (error.code === undefined || error.code === -32000) && /^insufficient funds\b/i.test(message)) return true;
197
+ failure = error.cause;
198
+ }
199
+ return false;
200
+ }
201
+
197
202
  async function waitForkReceipt(client: LifecycleRpcClient, transactionHash: Hex, timeoutMs: number): Promise<Record<string, unknown>> {
198
203
  const deadline = Date.now() + timeoutMs;
199
204
  while (Date.now() <= deadline) {
@@ -234,12 +239,12 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
234
239
  if (transaction === undefined || gas === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing fork transaction/gas context");
235
240
  try {
236
241
  if (measure) {
237
- const estimated = rpcQuantity(await lifecycleRpc(fork.client, "eth_estimateGas", [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gasPrice: toHex(gasPrice) }]), "fork sequential gas estimate");
242
+ const estimated = rpcQuantity(await lifecycleRpc(fork.client, "eth_estimateGas", [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gasPrice: toHex(gasPrice), ...(transaction.nonce === undefined ? {} : { nonce: toHex(transaction.nonce) }) }]), "fork sequential gas estimate");
238
243
  gasRequired = estimated;
239
244
  const buffered = (estimated * BigInt(10000 + headroomBps) + 9999n) / 10000n;
240
245
  if (buffered < gas) gas = buffered;
241
246
  }
242
- const hash = rpcHex(await lifecycleRpc(fork.client, "eth_sendTransaction", [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gas), gasPrice: toHex(gasPrice) }]), "fork transaction hash");
247
+ const hash = rpcHex(await lifecycleRpc(fork.client, "eth_sendTransaction", [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gas), gasPrice: toHex(gasPrice), ...(transaction.nonce === undefined ? {} : { nonce: toHex(transaction.nonce) }) }]), "fork transaction hash");
243
248
  const receipt = await waitForkReceipt(fork.client, hash, fork.receiptTimeoutMs ?? 10_000);
244
249
  const gasUsed = rpcQuantity(receipt.gasUsed, "fork gas used");
245
250
  if (rpcQuantity(receipt.status, "fork transaction status") !== 1n) {
@@ -302,8 +307,10 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
302
307
  type SimulationObservations = [LifecycleLimitObservation, unknown, unknown, unknown];
303
308
  /** Internal, operation-local fee envelope for the already matched immediate step. */
304
309
  export type NextTransactionEnvelope = Readonly<{ gas: bigint; gasPrice: bigint }>;
310
+ /** Compute-only sizing observations; native poster charges are never included. */
311
+ export type GasSizingHints = ReadonlyMap<string, bigint>;
305
312
  export type PreparedLaunchSimulation = ((transactions: readonly LifecycleTransaction[], nextEnvelope?: NextTransactionEnvelope) => Promise<LifecycleSimulation>) & {
306
- /** Unadmitted diagnostic results only; final admission always performs validated replay. */
313
+ /** Unadmitted diagnostic results only; final measurement validates exact calldata. */
307
314
  measure(transactions: readonly LifecycleTransaction[]): Promise<LifecycleSimulation>;
308
315
  close(): void;
309
316
  };
@@ -316,7 +323,7 @@ function observedFields<T extends object>(value: T): () => boolean {
316
323
 
317
324
  /** Internal continuation owned by one SDK read invocation. Only its positive
318
325
  * capability proof is reused; every candidate executes its entire economic sequence. */
319
- export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true, reviewedEnvelope?: NextTransactionEnvelope): PreparedLaunchSimulation {
326
+ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true, reviewedEnvelope?: NextTransactionEnvelope, selectedGasPrice?: bigint, sizing?: GasSizingHints): PreparedLaunchSimulation {
320
327
  const { client, planned, fork } = options;
321
328
  const signal = options.signal;
322
329
  if (signal?.aborted) throw Object.assign(new Error("Lifecycle simulation cancelled"), { name: "AbortError" });
@@ -354,8 +361,8 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
354
361
  return lifecycleStage(client, "simulation.context", () => Promise.all([
355
362
  resolveLifecycleLimits(context, policy),
356
363
  readLifecycleChainId(client),
357
- lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block),
358
- hasTransactions && reviewedEnvelope === undefined ? lifecycleRpc(client, "eth_gasPrice") : undefined,
364
+ lifecycleSessionRead(client, `${planned.chainId}:account-code:${context.account.toLowerCase()}`, () => lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block)),
365
+ hasTransactions && reviewedEnvelope === undefined ? selectedGasPrice ?? readLifecycleGasPrice(client) : undefined,
359
366
  ]));
360
367
  })().then((observations) => ({ observations }), (failure: unknown) => ({ failure }));
361
368
  // Refused inputs may discard this read-only preparation before it settles.
@@ -394,17 +401,9 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
394
401
  assertCurrent();
395
402
  if (capabilityProved) return;
396
403
  if (capabilityPending === undefined) {
397
- const key = JSON.stringify([context.chainId.toString(), context.account.toLowerCase(), context.orchestrator.toLowerCase(), context.block.number.toString(), context.block.hash.toLowerCase(), context.block.timestamp.toString(), context.block.gasLimit.toString(), limits.arbOSVersion?.toString(), limits.maxTxComputeGas?.toString(), limits.maxBlockComputeGas?.toString(), limits.executionGasCeiling.toString(), limits.maxSimulationGas?.toString(), gasPrice.toString()]);
398
- let proofs = reusable ? nitroProofsByInvocation.get(client) : undefined;
399
- if (reusable && proofs === undefined) { proofs = new Map(); nitroProofsByInvocation.set(client, proofs); }
400
- const ownedProofs = proofs;
401
- let nativeProof = ownedProofs?.get(key);
402
- if (nativeProof === undefined) {
403
- nativeProof = lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, context.block, limits, gasPrice));
404
- ownedProofs?.set(key, nativeProof);
405
- const pending = nativeProof;
406
- void pending.catch(() => { if (ownedProofs?.get(key) === pending) ownedProofs?.delete(key); });
407
- }
404
+ const key = `${context.chainId}:nitro-backend`;
405
+ const nativeProof = lifecycleSessionRead(client, key, () =>
406
+ lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, context.block, limits, gasPrice)));
408
407
  const proof = nativeProof.then(() => {
409
408
  assertContext();
410
409
  result.observations[0].assertCurrent();
@@ -419,8 +418,9 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
419
418
  await capabilityPending;
420
419
  assertCurrent();
421
420
  };
422
- const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope);
421
+ const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, selectedGasPrice, sizing);
423
422
  assertCurrent();
423
+ registerLifecycleFinalCheck(client, result.observations[0].assertCurrent);
424
424
  return simulation;
425
425
  } catch (failure) {
426
426
  closed = true;
@@ -437,11 +437,11 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
437
437
  return simulate;
438
438
  }
439
439
 
440
- export async function simulateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], pinnedBlock?: LifecycleBlock, nextEnvelope?: NextTransactionEnvelope): Promise<LifecycleSimulation> {
440
+ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], pinnedBlock?: LifecycleBlock, nextEnvelope?: NextTransactionEnvelope, sizing?: GasSizingHints): Promise<LifecycleSimulation> {
441
441
  // Standalone calls always produce fresh context, including live chain and fees.
442
442
  if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === lifecycleSourceClient(options.client)) throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
443
443
  const block = pinnedBlock ?? await readLifecycleBlock(options.client);
444
- const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope);
444
+ const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope, undefined, sizing);
445
445
  try { return await prepared(transactions, nextEnvelope); }
446
446
  finally { prepared.close(); }
447
447
  }
@@ -455,6 +455,8 @@ async function simulateLaunchTransactionsWithContext(
455
455
  assertCurrent: () => void,
456
456
  diagnostic: boolean,
457
457
  nextEnvelope?: NextTransactionEnvelope,
458
+ selectedGasPrice?: bigint,
459
+ sizing?: GasSizingHints,
458
460
  ): Promise<LifecycleSimulation> {
459
461
  const { client, planned } = options;
460
462
  const sourceClient = lifecycleSourceClient(client);
@@ -464,17 +466,17 @@ async function simulateLaunchTransactionsWithContext(
464
466
  if (chainId !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
465
467
  const accountCode = rpcHex(accountCodeValue, "account code");
466
468
  if (accountCode !== "0x") {
467
- await assertLifecycleBlock(client, block, planned.chainId);
469
+
468
470
  return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
469
471
  }
470
472
  for (const transaction of transactions) {
471
473
  if (transaction.from.toLowerCase() !== planned.account.toLowerCase() || BigInt(transaction.chainId) !== planned.chainId) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Simulation transaction differs from the committed chain/account");
472
474
  if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) {
473
- await assertLifecycleBlock(client, block, planned.chainId);
475
+
474
476
  return { ...base, executionProof: "unavailable", protocolFit: "failed", backend: "unavailable", admitted: false, steps: [], failedTransactionId: transaction.id, failureCategory: "capacity", capacityConstraint: "calldata", reason: "Exact calldata exceeds the supplied chain/account/RPC byte cap" };
475
477
  }
476
478
  }
477
- if (transactions.length === 0) { await assertLifecycleBlock(client, block, planned.chainId); return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] }; }
479
+ if (transactions.length === 0) { return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] }; }
478
480
  if (nextEnvelope !== undefined && (transactions.length !== 1 || typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas <= 0n || nextEnvelope.gas >= 1n << 64n ||
479
481
  typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
480
482
  throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Reviewed replay requires one immediate transaction with exact uint64 gas and uint256 price");
@@ -500,15 +502,15 @@ async function simulateLaunchTransactionsWithContext(
500
502
  const reason = exceedsCompute
501
503
  ? `${atomic ? "Atomic launch" : "Immediate transaction"} needs about ${gasInMillions(estimatedGas)}M gas, above the ${gasInMillions(computeCap)}M per-transaction limit${atomic ? "; use staged" : ""}`
502
504
  : "The immediate transaction estimate exceeds the RPC aggregate simulation gas cap; no prefix can be proved";
503
- await assertLifecycleBlock(client, block, planned.chainId);
505
+
504
506
  return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: exceedsCompute ? "compute" : "simulation-gas", reason };
505
507
  }
506
508
  }
507
- const recommendedGasPrice = nextEnvelope === undefined ? rpcQuantity(gasPriceValue, "gas price") : nextEnvelope.gasPrice;
509
+ const recommendedGasPrice = nextEnvelope === undefined ? typeof gasPriceValue === "bigint" ? gasPriceValue : rpcQuantity(gasPriceValue, "gas price") : nextEnvelope.gasPrice;
508
510
  // New recommendations cover lagging quotes; reviewed fees are replayed unchanged
509
511
  // under natural child fees, never refreshed or silently increased.
510
512
  const baseFeeEnvelope = (block.baseFeePerGas ?? 0n) * 2n;
511
- const gasPrice = nextEnvelope?.gasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
513
+ const gasPrice = nextEnvelope?.gasPrice ?? selectedGasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
512
514
  if (gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
513
515
  const buffer = (gas: bigint) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
514
516
  const posterGas: bigint[] = [];
@@ -516,40 +518,78 @@ async function simulateLaunchTransactionsWithContext(
516
518
  try {
517
519
  const [, estimates] = await Promise.all([
518
520
  proveBackend(limits, gasPrice),
519
- nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))) : [],
521
+ !diagnostic && nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster", async () => [buffer((await readNitroPosterGas(transactions[0]!, context)).posterGas)]) : [],
520
522
  ]);
521
523
  posterGas.push(...estimates);
522
524
  } catch (failure) {
523
525
  if (simulationFailureKind(failure) === "abort") throw failure;
524
526
  assertCurrent();
525
- await assertLifecycleBlock(client, block, planned.chainId);
527
+
526
528
  return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Native Nitro compute/poster proof unavailable: ${lifecycleFailureReason(failure)}. Generic Anvil/Hardhat forks cannot prove ArbOS metering.` };
527
529
  }
528
530
  }
529
531
  assertCurrent();
530
- const discoveryGas = transactions.map((_, index) => {
531
- const total = limits.executionGasCeiling + (posterGas[index] ?? 0n);
532
+ const discoveryGas = transactions.map(() => {
533
+ const total = limits.protocol === "nitro" ? limits.maxSimulationGas ?? block.gasLimit : limits.executionGasCeiling;
532
534
  const capped = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < total ? limits.transactionGasCeiling : total;
533
535
  if (capped <= 0n || capped >= 1n << 64n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Compute/poster envelope exceeds uint64 gas");
534
536
  return capped;
535
537
  });
536
- let backend: "eth_simulateV1" | "controlled-fork" = nextEnvelope !== undefined && limits.protocol === "evm" && options.fork !== undefined ? "controlled-fork" : "eth_simulateV1";
538
+ if (discoveryGas[0]! * gasPrice + transactions[0]!.value >= 1n << 256n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
539
+ const balance = rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance");
537
540
  let measured: LifecycleSimulationStep[] = [];
538
- if (nextEnvelope === undefined) {
541
+ const hasSizing = !diagnostic && transactions.every((transaction) => sizing?.has(transaction.id));
542
+ let backend: "eth_simulateV1" | "controlled-fork" = (nextEnvelope !== undefined || hasSizing) && limits.protocol === "evm" && options.fork !== undefined ? "controlled-fork" : "eth_simulateV1";
543
+ if (nextEnvelope === undefined && !hasSizing) {
544
+ // Nitro checks upfront funds even with validation:false. Bound discovery
545
+ // budgets before dispatch, without overriding the payer's real balance.
546
+ const value = transactions.reduce((total, transaction) => total + transaction.value, 0n);
547
+ let available = balance - value;
548
+ if (gasPrice !== 0n) available /= gasPrice;
549
+ const sizingGas = discoveryGas.map((ceiling, index) => {
550
+ // Upfront reservation is per call; unused gas is refunded before the next.
551
+ const computeAndPoster = limits.executionGasCeiling + (posterGas[index] ?? 0n);
552
+ const capped = computeAndPoster < ceiling ? computeAndPoster : ceiling;
553
+ return gasPrice === 0n || capped < available ? capped : available;
554
+ });
555
+ let retriedForFunds = false;
556
+ if (balance < value || sizingGas.some((gas) => gas <= 0n)) return { ...base, executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "affordability", reason: "Creator native balance cannot fund transaction value and sizing fee budgets" };
557
+ assertCurrent();
539
558
  try {
540
- measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, discoveryGas, limits, gasPrice, false));
559
+ try {
560
+ measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, sizingGas, limits, gasPrice, false));
561
+ } catch (failure) {
562
+ if (!upfrontFundsFailure(failure)) throw failure;
563
+ assertCurrent();
564
+ retriedForFunds = true;
565
+ let remaining = available;
566
+ const sharedGas = sizingGas.map((ceiling, index) => {
567
+ if (gasPrice === 0n) return ceiling;
568
+ const share = remaining / BigInt(transactions.length - index);
569
+ const gas = ceiling < share ? ceiling : share;
570
+ remaining -= gas;
571
+ return gas;
572
+ });
573
+ measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, sharedGas, limits, gasPrice, false));
574
+ }
541
575
  } catch (failure) {
542
- if (simulationFailureKind(failure) === "abort") throw failure;
576
+ if (upfrontFundsFailure(failure)) return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "affordability", reason: "Native balance cannot cover the sequential sizing fee envelopes" };
577
+ const kind = simulationFailureKind(failure);
578
+ if (kind === "abort") throw failure;
543
579
  assertCurrent();
544
- if (options.fork === undefined || limits.protocol === "nitro") {
545
- await assertLifecycleBlock(client, block, planned.chainId);
580
+ if (options.fork === undefined || limits.protocol === "nitro" || kind !== "source") {
546
581
  return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Stateful sequential RPC simulation unavailable: ${lifecycleFailureReason(failure)}. ${limits.protocol === "nitro" ? "Generic Anvil/Hardhat forks cannot prove ArbOS compute/poster metering." : "Dependent eth_call estimates are not proof."}` };
547
582
  }
548
583
  backend = "controlled-fork";
549
- measured = await simulateForkPass(options.fork, planned, block, transactions, discoveryGas, gasPrice, true, limits.headroomBps);
584
+ measured = await simulateForkPass(options.fork, planned, block, transactions, sizingGas, gasPrice, true, limits.headroomBps);
550
585
  }
551
586
  assertCurrent();
552
587
  const failedIndex = measured.findIndex((step) => !step.success);
588
+ const refusal = measured[failedIndex];
589
+ if (retriedForFunds && refusal?.failureCategory === "capacity" && refusal.gasLimit !== undefined && refusal.gasLimit < limits.executionGasCeiling) {
590
+ return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: measured,
591
+ failedTransactionId: refusal.transactionId, failureCategory: "affordability", reason: "Native balance does not provide the sequential sizing margin; an exact transaction envelope was not proved" };
592
+ }
553
593
  if (failedIndex >= 0 && limits.maxSimulationGas !== undefined &&
554
594
  measured.reduce((gas, step) => gas + (step.gasUsed ?? 0n), 0n) * 100n >= limits.maxSimulationGas * 95n) {
555
595
  // The provider exhausted its aggregate used-gas budget, not the contract.
@@ -558,90 +598,78 @@ async function simulateLaunchTransactionsWithContext(
558
598
  transactions = transactions.slice(0, failedIndex);
559
599
  base.deferredTransactionIds = allTransactions.slice(failedIndex).map((transaction) => transaction.id);
560
600
  if (transactions.length === 0) {
561
- await assertLifecycleBlock(client, block, planned.chainId);
601
+
562
602
  return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "source", reason: "RPC aggregate simulation gas cap left no successful prefix to prove" };
563
603
  }
564
604
  }
565
605
  const failed = measured.find((step) => !step.success);
566
- if (failed !== undefined) { await assertLifecycleBlock(client, block, planned.chainId); return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId, failureCategory: failed.failureCategory ?? "opaque", capacityConstraint: failed.failureCategory === "capacity" ? "compute" : undefined }; }
606
+ if (diagnostic && failed !== undefined) return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId, failureCategory: failed.failureCategory ?? "opaque", capacityConstraint: failed.failureCategory === "capacity" ? "compute" : undefined };
567
607
  if (diagnostic) {
568
- await assertLifecycleBlock(client, block, planned.chainId);
608
+
569
609
  return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
570
610
  }
571
611
  }
572
- const gasLimits = nextEnvelope === undefined ? measured.map((step, index) => {
573
- const gross = step.gasRequired ?? step.gasUsed!;
574
- const reserve = transactions[index]?.kind === "activate" || transactions[index]?.kind === "atomic" ? hmmV2ForwardingReserve(planned.plan) : 0n;
575
- const desired = gross + reserve > buffer(gross) ? gross + reserve : buffer(gross);
576
- const available = discoveryGas[index]! - (posterGas[index] ?? 0n);
577
- return (desired < available ? desired : available) + (posterGas[index] ?? 0n);
612
+ const sizedTransactions = measured.length === 0 ? transactions : transactions.slice(0, measured.length);
613
+ const minimumGas = sizedTransactions.map((transaction, index) => {
614
+ return nextEnvelope !== undefined ? 0n : hasSizing ? sizing!.get(transaction.id)! : measured[index]!.gasRequired ?? measured[index]!.gasUsed!;
615
+ });
616
+ const gasLimits = nextEnvelope === undefined ? sizedTransactions.map((transaction, index) => {
617
+ // Suffix budgets prove feasibility, not a future transaction's fee estimate.
618
+ if (index > 0) return discoveryGas[index]!;
619
+ const gross = minimumGas[index]!;
620
+ const reserve = transaction.kind === "activate" || transaction.kind === "atomic" ? hmmV2ForwardingReserve(planned.plan) : 0n;
621
+ const buffered = gross + reserve > buffer(gross) ? gross + reserve : buffer(gross);
622
+ const desired = buffered + (posterGas[0] ?? 0n);
623
+ return desired < discoveryGas[index]! ? desired : discoveryGas[index]!;
578
624
  }) : [nextEnvelope.gas];
625
+ if (gasLimits[0]! * gasPrice + transactions[0]!.value >= 1n << 256n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
626
+ const validate = nextEnvelope !== undefined || measured.every((step) => step.success);
627
+ if (nextEnvelope === undefined) {
628
+ const value = transactions.reduce((total, transaction) => total + transaction.value, 0n);
629
+ let available = balance - value;
630
+ let suffixMinimum = minimumGas.slice(1).reduce((total, gas) => total + gas, 0n);
631
+ if (gasPrice !== 0n) available /= gasPrice;
632
+ for (let index = 0; index < gasLimits.length && gasPrice !== 0n; index += 1) {
633
+ const minimum = minimumGas[index]!;
634
+ if (index > 0) suffixMinimum -= minimum;
635
+ const budget = index === 0 ? available - suffixMinimum : minimum + (available - minimum - suffixMinimum) / BigInt(gasLimits.length - index);
636
+ if (gasLimits[index]! > budget) gasLimits[index] = budget;
637
+ available -= gasLimits[index]!;
638
+ }
639
+ if (balance < value || gasLimits.some((gas, index) => gas <= 0n && minimumGas[index]! > 0n)) return { ...base, executionProof: "unavailable", backend, admitted: false, steps: measured, failureCategory: "affordability", reason: "Creator native balance cannot fund transaction value and execution fee envelopes" };
640
+ }
579
641
  const exceeds = gasLimits.findIndex((gas, index) =>
580
642
  gas <= 0n || gas >= 1n << 64n || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling ||
581
- nextEnvelope === undefined && (measured[index]!.gasRequired ?? measured[index]!.gasUsed!) > gas - (posterGas[index] ?? 0n) ||
643
+ nextEnvelope === undefined && !hasSizing && (measured[index]!.gasRequired ?? measured[index]!.gasUsed!) > gas ||
582
644
  limits.protocol === "evm" && gas > limits.executionGasCeiling);
583
645
  if (exceeds >= 0) {
584
- await assertLifecycleBlock(client, block, planned.chainId);
646
+
585
647
  return { ...base, executionProof: "unavailable", protocolFit: "failed", backend, admitted: false, steps: measured, failureCategory: "capacity", capacityConstraint: "gas-envelope", reason: "Measured execution or reviewed gas exceeds the current compute/complete transaction envelope", failedTransactionId: transactions[exceeds]?.id };
586
648
  }
587
- let verifiedSteps: LifecycleSimulationStep[] = [];
588
- let admittedGasLimits = gasLimits;
589
- let replayError: string | undefined;
590
- const replay = (gas: readonly bigint[]) => backend === "eth_simulateV1"
591
- ? simulateRpcPass(client, planned, block, transactions, gas, limits, gasPrice, true)
592
- : simulateForkPass(options.fork!, planned, block, transactions, gas, gasPrice, false, limits.headroomBps);
593
- try {
594
- verifiedSteps = await lifecycleStage(client, "simulation.replay", () => replay(gasLimits));
595
- } catch (failure) {
596
- const kind = simulationFailureKind(failure);
597
- if (kind === "abort") throw failure;
598
- assertCurrent();
599
- if (kind !== "out-of-gas" || backend !== "eth_simulateV1" || nextEnvelope !== undefined) {
600
- await assertLifecycleBlock(client, block, planned.chainId);
601
- return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, failureCategory: kind === "source" ? "source" : "opaque", reason: `Exact-gas validated replay unavailable: ${lifecycleFailureReason(failure)}` };
602
- }
603
- replayError = lifecycleFailureReason(failure);
604
- }
605
- assertCurrent();
606
- let finalFailure = verifiedSteps.find((step) => !step.success);
607
- const failedIndex = verifiedSteps.findIndex((step) => !step.success);
608
- const rescue = nextEnvelope === undefined && backend === "eth_simulateV1" &&
609
- (replayError !== undefined && discoveryGas.some((gas, index) => index < transactions.length && gas > gasLimits[index]!) ||
610
- finalFailure !== undefined && looksGasExhausted(finalFailure, gasLimits[failedIndex]!) && discoveryGas[failedIndex]! > gasLimits[failedIndex]!);
611
- // One legal-envelope rescue covers thrown/returned genuine OOG. Never retry
612
- // semantic errors, switch replay backends, or enlarge a held immediate envelope.
613
- if (rescue) {
614
- assertCurrent();
649
+ // Size first, then prove and return this exact executable envelope. Diagnostic
650
+ // gas budgets and caller-held proposals are never execution admission.
651
+ let verifiedSteps = measured;
652
+ if (validate) {
615
653
  try {
616
- admittedGasLimits = discoveryGas.slice(0, transactions.length);
617
- verifiedSteps = await lifecycleStage(client, "simulation.envelope", () => replay(admittedGasLimits));
618
- finalFailure = verifiedSteps.find((step) => !step.success);
619
- replayError = undefined;
654
+ verifiedSteps = await lifecycleStage(client, "simulation.measure", () => backend === "eth_simulateV1"
655
+ ? simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true)
656
+ : simulateForkPass(options.fork!, planned, block, transactions, gasLimits, gasPrice, false, limits.headroomBps));
620
657
  } catch (failure) {
621
- const kind = simulationFailureKind(failure);
622
- if (kind === "abort") throw failure;
658
+ if (simulationFailureKind(failure) === "abort") throw failure;
623
659
  assertCurrent();
624
- await assertLifecycleBlock(client, block, planned.chainId);
625
- if (kind !== "out-of-gas") return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, failureCategory: kind === "source" ? "source" : "opaque", reason: `Legal-envelope validated replay unavailable: ${lifecycleFailureReason(failure)}` };
626
- return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${lifecycleFailureReason(failure)}` };
660
+ return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [], reason: `Exact transaction simulation unavailable: ${lifecycleFailureReason(failure)}` };
627
661
  }
628
662
  }
629
- if (replayError !== undefined) {
630
- await assertLifecycleBlock(client, block, planned.chainId);
631
- return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling failed: ${replayError}` };
632
- }
633
663
  assertCurrent();
664
+ const finalFailure = verifiedSteps.find((step) => !step.success);
634
665
  const estimates: LifecycleSimulationStep[] = [];
635
- // A definitive returned replay failure already refuses execution. Retain its
636
- // fee observations without inventing a balance or doing redundant payer work.
637
- let remainingBalance = finalFailure === undefined
638
- ? rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance")
639
- : undefined;
666
+ let remainingBalance = finalFailure === undefined ? balance : undefined;
640
667
  let affordabilityFailure: string | undefined;
641
668
  const feeContext = configuration.estimateDataFee === undefined || sourceClient === client ? context : { ...context, client: sourceClient };
642
669
  for (let index = 0; index < verifiedSteps.length; index += 1) {
643
- const step = verifiedSteps[index]; const transaction = transactions[index]; const gasLimit = admittedGasLimits[index];
670
+ const step = verifiedSteps[index]; const transaction = transactions[index]; const gasLimit = gasLimits[index];
644
671
  if (step === undefined || transaction === undefined || gasLimit === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing verified fee context");
672
+ if (index > 0) { estimates.push({ ...step, computeGasRequired: minimumGas[index], provisional: true }); continue; }
645
673
  const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit, gasPrice }, feeContext);
646
674
  if (dataFee !== undefined && (typeof dataFee !== "bigint" || dataFee < 0n || dataFee >= 1n << 256n)) throw new LifecyclePlanningError("INVALID_DATA_FEE", "Data-fee oracle must return a nonnegative uint256 bigint amount");
647
675
  // Nitro gas includes poster charges. Never add the quoted poster fee again.
@@ -653,9 +681,9 @@ async function simulateLaunchTransactionsWithContext(
653
681
  remainingBalance -= required;
654
682
  }
655
683
  const poster = posterGas[index];
656
- estimates.push({ ...step, estimate: { gasUsed: step.gasUsed ?? 0n, gasLimit, gasPrice, executionFee, dataFee, posterGas: poster, posterFee: poster === undefined ? undefined : poster * gasPrice, dataFeeIncludedInGas: limits.protocol === "nitro", totalFee: dataFee === undefined ? undefined : executionFee + dataFee, feeConfidence: dataFee === undefined ? "execution-only" : "execution-and-data" } });
684
+ estimates.push({ ...step, computeGasRequired: nextEnvelope === undefined ? minimumGas[index] : undefined, estimate: { gasUsed: step.gasUsed ?? 0n, gasLimit, gasPrice, executionFee, dataFee, posterGas: poster, posterFee: poster === undefined ? undefined : poster * gasPrice, dataFeeIncludedInGas: limits.protocol === "nitro", totalFee: dataFee === undefined ? undefined : executionFee + dataFee, feeConfidence: dataFee === undefined ? "execution-only" : "execution-and-data" } });
657
685
  }
658
- await assertLifecycleBlock(client, block, planned.chainId);
686
+
659
687
  return { ...base, backend, executionProof: finalFailure === undefined ? "proved" : "failed", protocolFit: finalFailure === undefined ? "proved" : "unknown", admitted: finalFailure === undefined && affordabilityFailure === undefined, steps: estimates, reason: finalFailure?.error ?? affordabilityFailure, failedTransactionId: finalFailure?.transactionId, failureCategory: finalFailure?.failureCategory ?? (affordabilityFailure === undefined ? undefined : "affordability"), capacityConstraint: finalFailure?.failureCategory === "capacity" ? "compute" : undefined };
660
688
  }
661
689
 
@@ -0,0 +1,72 @@
1
+ import { decodeFunctionResult, encodeFunctionData, encodeFunctionResult, multicall3Abi, toHex, zeroAddress, type Abi, type Address } from "viem";
2
+ import { robinhoodMainnet } from "../addresses.js";
3
+ import { launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
4
+ import { getKnownLifecycleDeployment, type LifecycleDeploymentPreset } from "./presets.js";
5
+ import { lifecycleTokenPredictionAbi } from "./progress.js";
6
+ import { launchLensAbi, observeLifecycleLens, type LifecycleLensSnapshot } from "./lens.js";
7
+ import { hashLaunchIdentity, LifecycleFundingKind, type LaunchPlanV1 } from "./schema.js";
8
+ import { attestLifecycleMulticall, lifecycleRpc, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readLifecycleChainId, rpcHex, seedLifecycleRead } from "./rpc.js";
9
+ import { LifecyclePlanningError, type LifecycleBlock, type LifecycleRpcClient } from "./types.js";
10
+ import { UFG_LAUNCH_PROXY_ADDRESS } from "./ufg-config.js";
11
+
12
+ type SnapshotRead = { to: Address; abi: Abi; functionName: string; args: readonly unknown[]; from?: Address };
13
+
14
+ /** Nitro's EVM BLOCKNUMBER/BLOCKHASH expose L1 values. One RPC header owns the L2
15
+ * pin; every planning getter is aggregated at that explicit L2 block number. */
16
+ export async function readLifecyclePlanningBlock(client: LifecycleRpcClient, plan: LaunchPlanV1, account: Address, ufg = false, mine = false,
17
+ preset: LifecycleDeploymentPreset | undefined = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator })): Promise<LifecycleBlock> {
18
+ const [block, chainId] = await Promise.all([readLifecycleBlock(client), readLifecycleChainId(client)]);
19
+ if (chainId !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
20
+ if (chainId !== 4663n) return block;
21
+ const to = robinhoodMainnet.contracts.multicall3.address;
22
+ const reads: SnapshotRead[] = [];
23
+ const add = (target: Address, abi: Abi, functionName: string, args: readonly unknown[], from?: Address) => reads.push({ to: target, abi, functionName, args, from });
24
+ const escrow = ufg ? UFG_LAUNCH_PROXY_ADDRESS : preset?.fundingEscrow;
25
+ const inputs = [...new Set(plan.funding.filter((funding) => funding.kind !== LifecycleFundingKind.NativeWrap && !(funding.kind === LifecycleFundingKind.Swap && funding.inputAsset === zeroAddress)).map((funding) => funding.inputAsset.toLowerCase() as Address))];
26
+ const hasLens = preset?.launchLens !== undefined;
27
+ if (hasLens) {
28
+ add(preset.launchLens!, launchLensAbi, "snapshot", [plan, account, escrow!, inputs, mine ? 150_000n : 0n]);
29
+ } else {
30
+ add(to, multicall3Abi, "getEthBalance", [account]);
31
+ if (preset === undefined) add(plan.orchestrator, launchLifecycleAbi, "predictToken", [plan]);
32
+ else add(preset.tokenFactory, lifecycleTokenPredictionAbi, "predictToken", [hashLaunchIdentity(plan), plan.token]);
33
+ add(plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [hashLaunchIdentity(plan)], account);
34
+ for (const asset of inputs) {
35
+ add(asset, lifecycleErc20Abi, "balanceOf", [account]);
36
+ if (escrow !== undefined) add(asset, lifecycleErc20Abi, "allowance", [account, escrow]);
37
+ }
38
+ }
39
+ add(nitroArbSys, nitroArbSysAbi, "arbOSVersion", []);
40
+ add(nitroGasInfo, nitroGasInfoAbi, "getMaxTxGasLimit", []);
41
+ add(nitroGasInfo, nitroGasInfoAbi, "getMaxBlockGasLimit", []);
42
+ await attestLifecycleMulticall(client, block);
43
+ const calls = reads.map((read) => ({ target: read.to, allowFailure: true, callData: encodeFunctionData({ abi: read.abi, functionName: read.functionName, args: read.args }) }));
44
+ const data = encodeFunctionData({ abi: multicall3Abi, functionName: "aggregate3", args: [calls] });
45
+ const raw = rpcHex(await lifecycleRpc(client, "eth_call", [{ to, data }, toHex(block.number)]), "planning snapshot");
46
+ const results = decodeFunctionResult({ abi: multicall3Abi, functionName: "aggregate3", data: raw });
47
+ if (results.length !== reads.length || !results[0]!.success) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Multicall3 omitted the planning balance snapshot");
48
+ const seed = (target: Address, abi: Abi, functionName: string, args: readonly unknown[], result: unknown, from?: Address) => {
49
+ seedLifecycleRead(client, "eth_call", [{ to: target, from, data: encodeFunctionData({ abi, functionName, args }) }, toHex(block.number)], block, encodeFunctionResult({ abi, functionName, result }));
50
+ };
51
+ if (hasLens) {
52
+ const snapshot = decodeFunctionResult({ abi: launchLensAbi, functionName: "snapshot", data: results[0]!.returnData }) as LifecycleLensSnapshot;
53
+ if (snapshot.chainId !== chainId || snapshot.blockNumber !== block.number) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "LaunchLens returned a different L2 planning domain");
54
+ observeLifecycleLens(client, block, snapshot);
55
+ seedLifecycleRead(client, "eth_getBalance", [account, toHex(block.number)], block, toHex(snapshot.nativeBalance));
56
+ seed(preset!.tokenFactory, lifecycleTokenPredictionAbi, "predictToken", [hashLaunchIdentity(plan), plan.token], snapshot.predictedToken);
57
+ seed(plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [hashLaunchIdentity(plan)], snapshot.progress, account);
58
+ for (const balance of snapshot.tokenBalances) {
59
+ seed(balance.token, lifecycleErc20Abi, "balanceOf", [account], balance.balance);
60
+ seed(balance.token, lifecycleErc20Abi, "allowance", [account, escrow!], balance.allowance);
61
+ }
62
+ } else {
63
+ const balance = decodeFunctionResult({ abi: multicall3Abi, functionName: "getEthBalance", data: results[0]!.returnData });
64
+ seedLifecycleRead(client, "eth_getBalance", [account, toHex(block.number)], block, toHex(balance));
65
+ }
66
+ for (let index = 1; index < reads.length; index += 1) {
67
+ if (!results[index]!.success) continue;
68
+ const read = reads[index]!;
69
+ seedLifecycleRead(client, "eth_call", [{ to: read.to, from: read.from, data: calls[index]!.callData }, toHex(block.number)], block, results[index]!.returnData);
70
+ }
71
+ return block;
72
+ }