@black-market/sdk 0.13.0 → 0.14.1

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 (53) hide show
  1. package/README.md +4 -4
  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 +70 -62
  16. package/dist/lifecycle/planner.js.map +1 -1
  17. package/dist/lifecycle/presets.d.ts +5 -0
  18. package/dist/lifecycle/presets.d.ts.map +1 -1
  19. package/dist/lifecycle/presets.js +5 -0
  20. package/dist/lifecycle/presets.js.map +1 -1
  21. package/dist/lifecycle/progress.d.ts +48 -0
  22. package/dist/lifecycle/progress.d.ts.map +1 -1
  23. package/dist/lifecycle/progress.js +31 -20
  24. package/dist/lifecycle/progress.js.map +1 -1
  25. package/dist/lifecycle/rpc.d.ts +6 -2
  26. package/dist/lifecycle/rpc.d.ts.map +1 -1
  27. package/dist/lifecycle/rpc.js +47 -44
  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 +130 -109
  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 +15 -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 +42 -20
  41. package/docs/reference.md +43 -6
  42. package/examples/README.md +2 -2
  43. package/package.json +1 -1
  44. package/src/lifecycle/index.ts +5 -1
  45. package/src/lifecycle/lens.ts +42 -0
  46. package/src/lifecycle/mining.ts +80 -0
  47. package/src/lifecycle/planner.ts +72 -58
  48. package/src/lifecycle/presets.ts +10 -0
  49. package/src/lifecycle/progress.ts +26 -16
  50. package/src/lifecycle/rpc.ts +45 -43
  51. package/src/lifecycle/simulation.ts +126 -100
  52. package/src/lifecycle/snapshot.ts +72 -0
  53. package/src/lifecycle/types.ts +15 -5
@@ -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, readLifecycleGasPrice, readNitroPosterGas, registerLifecycleFinalCheck, 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[]> {
@@ -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) {
@@ -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, selectedGasPrice?: bigint): 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,7 +361,7 @@ 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),
364
+ lifecycleSessionRead(client, `${planned.chainId}:account-code:${context.account.toLowerCase()}`, () => lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block)),
358
365
  hasTransactions && reviewedEnvelope === undefined ? selectedGasPrice ?? readLifecycleGasPrice(client) : undefined,
359
366
  ]));
360
367
  })().then((observations) => ({ observations }), (failure: unknown) => ({ failure }));
@@ -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,7 +418,7 @@ 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, selectedGasPrice);
421
+ const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, selectedGasPrice, sizing);
423
422
  assertCurrent();
424
423
  registerLifecycleFinalCheck(client, result.observations[0].assertCurrent);
425
424
  return simulation;
@@ -438,11 +437,11 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
438
437
  return simulate;
439
438
  }
440
439
 
441
- 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> {
442
441
  // Standalone calls always produce fresh context, including live chain and fees.
443
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");
444
443
  const block = pinnedBlock ?? await readLifecycleBlock(options.client);
445
- const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope);
444
+ const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope, undefined, sizing);
446
445
  try { return await prepared(transactions, nextEnvelope); }
447
446
  finally { prepared.close(); }
448
447
  }
@@ -457,6 +456,7 @@ async function simulateLaunchTransactionsWithContext(
457
456
  diagnostic: boolean,
458
457
  nextEnvelope?: NextTransactionEnvelope,
459
458
  selectedGasPrice?: bigint,
459
+ sizing?: GasSizingHints,
460
460
  ): Promise<LifecycleSimulation> {
461
461
  const { client, planned } = options;
462
462
  const sourceClient = lifecycleSourceClient(client);
@@ -466,17 +466,17 @@ async function simulateLaunchTransactionsWithContext(
466
466
  if (chainId !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
467
467
  const accountCode = rpcHex(accountCodeValue, "account code");
468
468
  if (accountCode !== "0x") {
469
- await assertLifecycleBlock(client, block, planned.chainId);
469
+
470
470
  return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
471
471
  }
472
472
  for (const transaction of transactions) {
473
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");
474
474
  if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) {
475
- await assertLifecycleBlock(client, block, planned.chainId);
475
+
476
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" };
477
477
  }
478
478
  }
479
- 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: [] }; }
480
480
  if (nextEnvelope !== undefined && (transactions.length !== 1 || typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas <= 0n || nextEnvelope.gas >= 1n << 64n ||
481
481
  typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
482
482
  throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Reviewed replay requires one immediate transaction with exact uint64 gas and uint256 price");
@@ -502,7 +502,7 @@ async function simulateLaunchTransactionsWithContext(
502
502
  const reason = exceedsCompute
503
503
  ? `${atomic ? "Atomic launch" : "Immediate transaction"} needs about ${gasInMillions(estimatedGas)}M gas, above the ${gasInMillions(computeCap)}M per-transaction limit${atomic ? "; use staged" : ""}`
504
504
  : "The immediate transaction estimate exceeds the RPC aggregate simulation gas cap; no prefix can be proved";
505
- await assertLifecycleBlock(client, block, planned.chainId);
505
+
506
506
  return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: exceedsCompute ? "compute" : "simulation-gas", reason };
507
507
  }
508
508
  }
@@ -518,40 +518,78 @@ async function simulateLaunchTransactionsWithContext(
518
518
  try {
519
519
  const [, estimates] = await Promise.all([
520
520
  proveBackend(limits, gasPrice),
521
- 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)]) : [],
522
522
  ]);
523
523
  posterGas.push(...estimates);
524
524
  } catch (failure) {
525
525
  if (simulationFailureKind(failure) === "abort") throw failure;
526
526
  assertCurrent();
527
- await assertLifecycleBlock(client, block, planned.chainId);
527
+
528
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.` };
529
529
  }
530
530
  }
531
531
  assertCurrent();
532
- const discoveryGas = transactions.map((_, index) => {
533
- const total = limits.executionGasCeiling + (posterGas[index] ?? 0n);
532
+ const discoveryGas = transactions.map(() => {
533
+ const total = limits.protocol === "nitro" ? limits.maxSimulationGas ?? block.gasLimit : limits.executionGasCeiling;
534
534
  const capped = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < total ? limits.transactionGasCeiling : total;
535
535
  if (capped <= 0n || capped >= 1n << 64n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Compute/poster envelope exceeds uint64 gas");
536
536
  return capped;
537
537
  });
538
- 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");
539
540
  let measured: LifecycleSimulationStep[] = [];
540
- 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();
541
558
  try {
542
- 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
+ }
543
575
  } catch (failure) {
544
- 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;
545
579
  assertCurrent();
546
- if (options.fork === undefined || limits.protocol === "nitro") {
547
- await assertLifecycleBlock(client, block, planned.chainId);
580
+ if (options.fork === undefined || limits.protocol === "nitro" || kind !== "source") {
548
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."}` };
549
582
  }
550
583
  backend = "controlled-fork";
551
- 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);
552
585
  }
553
586
  assertCurrent();
554
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
+ }
555
593
  if (failedIndex >= 0 && limits.maxSimulationGas !== undefined &&
556
594
  measured.reduce((gas, step) => gas + (step.gasUsed ?? 0n), 0n) * 100n >= limits.maxSimulationGas * 95n) {
557
595
  // The provider exhausted its aggregate used-gas budget, not the contract.
@@ -560,90 +598,78 @@ async function simulateLaunchTransactionsWithContext(
560
598
  transactions = transactions.slice(0, failedIndex);
561
599
  base.deferredTransactionIds = allTransactions.slice(failedIndex).map((transaction) => transaction.id);
562
600
  if (transactions.length === 0) {
563
- await assertLifecycleBlock(client, block, planned.chainId);
601
+
564
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" };
565
603
  }
566
604
  }
567
605
  const failed = measured.find((step) => !step.success);
568
- 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 };
569
607
  if (diagnostic) {
570
- await assertLifecycleBlock(client, block, planned.chainId);
608
+
571
609
  return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
572
610
  }
573
611
  }
574
- const gasLimits = nextEnvelope === undefined ? measured.map((step, index) => {
575
- const gross = step.gasRequired ?? step.gasUsed!;
576
- const reserve = transactions[index]?.kind === "activate" || transactions[index]?.kind === "atomic" ? hmmV2ForwardingReserve(planned.plan) : 0n;
577
- const desired = gross + reserve > buffer(gross) ? gross + reserve : buffer(gross);
578
- const available = discoveryGas[index]! - (posterGas[index] ?? 0n);
579
- 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]!;
580
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
+ }
581
641
  const exceeds = gasLimits.findIndex((gas, index) =>
582
642
  gas <= 0n || gas >= 1n << 64n || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling ||
583
- nextEnvelope === undefined && (measured[index]!.gasRequired ?? measured[index]!.gasUsed!) > gas - (posterGas[index] ?? 0n) ||
643
+ nextEnvelope === undefined && !hasSizing && (measured[index]!.gasRequired ?? measured[index]!.gasUsed!) > gas ||
584
644
  limits.protocol === "evm" && gas > limits.executionGasCeiling);
585
645
  if (exceeds >= 0) {
586
- await assertLifecycleBlock(client, block, planned.chainId);
646
+
587
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 };
588
648
  }
589
- let verifiedSteps: LifecycleSimulationStep[] = [];
590
- let admittedGasLimits = gasLimits;
591
- let replayError: string | undefined;
592
- const replay = (gas: readonly bigint[]) => backend === "eth_simulateV1"
593
- ? simulateRpcPass(client, planned, block, transactions, gas, limits, gasPrice, true)
594
- : simulateForkPass(options.fork!, planned, block, transactions, gas, gasPrice, false, limits.headroomBps);
595
- try {
596
- verifiedSteps = await lifecycleStage(client, "simulation.replay", () => replay(gasLimits));
597
- } catch (failure) {
598
- const kind = simulationFailureKind(failure);
599
- if (kind === "abort") throw failure;
600
- assertCurrent();
601
- if (kind !== "out-of-gas" || backend !== "eth_simulateV1" || nextEnvelope !== undefined) {
602
- await assertLifecycleBlock(client, block, planned.chainId);
603
- 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)}` };
604
- }
605
- replayError = lifecycleFailureReason(failure);
606
- }
607
- assertCurrent();
608
- let finalFailure = verifiedSteps.find((step) => !step.success);
609
- const failedIndex = verifiedSteps.findIndex((step) => !step.success);
610
- const rescue = nextEnvelope === undefined && backend === "eth_simulateV1" &&
611
- (replayError !== undefined && discoveryGas.some((gas, index) => index < transactions.length && gas > gasLimits[index]!) ||
612
- finalFailure !== undefined && looksGasExhausted(finalFailure, gasLimits[failedIndex]!) && discoveryGas[failedIndex]! > gasLimits[failedIndex]!);
613
- // One legal-envelope rescue covers thrown/returned genuine OOG. Never retry
614
- // semantic errors, switch replay backends, or enlarge a held immediate envelope.
615
- if (rescue) {
616
- 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) {
617
653
  try {
618
- admittedGasLimits = discoveryGas.slice(0, transactions.length);
619
- verifiedSteps = await lifecycleStage(client, "simulation.envelope", () => replay(admittedGasLimits));
620
- finalFailure = verifiedSteps.find((step) => !step.success);
621
- 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));
622
657
  } catch (failure) {
623
- const kind = simulationFailureKind(failure);
624
- if (kind === "abort") throw failure;
658
+ if (simulationFailureKind(failure) === "abort") throw failure;
625
659
  assertCurrent();
626
- await assertLifecycleBlock(client, block, planned.chainId);
627
- 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)}` };
628
- 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)}` };
629
661
  }
630
662
  }
631
- if (replayError !== undefined) {
632
- await assertLifecycleBlock(client, block, planned.chainId);
633
- return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling failed: ${replayError}` };
634
- }
635
663
  assertCurrent();
664
+ const finalFailure = verifiedSteps.find((step) => !step.success);
636
665
  const estimates: LifecycleSimulationStep[] = [];
637
- // A definitive returned replay failure already refuses execution. Retain its
638
- // fee observations without inventing a balance or doing redundant payer work.
639
- let remainingBalance = finalFailure === undefined
640
- ? rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance")
641
- : undefined;
666
+ let remainingBalance = finalFailure === undefined ? balance : undefined;
642
667
  let affordabilityFailure: string | undefined;
643
668
  const feeContext = configuration.estimateDataFee === undefined || sourceClient === client ? context : { ...context, client: sourceClient };
644
669
  for (let index = 0; index < verifiedSteps.length; index += 1) {
645
- 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];
646
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; }
647
673
  const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit, gasPrice }, feeContext);
648
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");
649
675
  // Nitro gas includes poster charges. Never add the quoted poster fee again.
@@ -655,9 +681,9 @@ async function simulateLaunchTransactionsWithContext(
655
681
  remainingBalance -= required;
656
682
  }
657
683
  const poster = posterGas[index];
658
- 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" } });
659
685
  }
660
- await assertLifecycleBlock(client, block, planned.chainId);
686
+
661
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 };
662
688
  }
663
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
+ }
@@ -5,11 +5,13 @@ import type {
5
5
  MarketLiveStateV1, PreparedMarketV1, ProfileRegistrationV1, ProfileTopologyV1, LaunchEnvelopeV2, LifecycleDeveloperTerms,
6
6
  } from "./schema.js";
7
7
 
8
- /** Local opt-in timing only: never contains request arguments, addresses, results or errors. */
8
+ /** Local opt-in timing only: never contains request arguments, addresses, results or errors.
9
+ * Since 0.14.0, removed replay/envelope stages are represented by simulation.measure,
10
+ * which includes the final validated execution at the returned gas envelope. */
9
11
  export type LifecycleDiagnosticStage =
10
12
  | "plan" | "plan.context" | "plan.domain" | "plan.progress" | "plan.inputs" | "rpc"
11
13
  | "profile.certification" | "simulation.context" | "simulation.nitro"
12
- | "simulation.poster" | "simulation.measure" | "simulation.replay" | "simulation.envelope" | "session-cache";
14
+ | "simulation.poster" | "simulation.measure" | "session-cache";
13
15
  export type LifecycleDiagnosticEvent = {
14
16
  stage: LifecycleDiagnosticStage;
15
17
  phase: "queued" | "start" | "success" | "failure" | "reuse";
@@ -35,7 +37,7 @@ export type LifecycleRpcClient = {
35
37
  sessionCache?: LifecycleSessionCache;
36
38
  };
37
39
  export type LifecycleBlock = { number: bigint; hash: Hash; timestamp: bigint; gasLimit: bigint; baseFeePerGas?: bigint };
38
- /** A session retains only static reads at one canonical block, never execution proof. */
40
+ /** A session retains pinned static reads and deployment/backend capability observations. */
39
41
  export type LifecycleSessionCache = {
40
42
  readonly block: Readonly<Pick<LifecycleBlock, "number" | "hash">> | undefined;
41
43
  /** Call on wallet/chain changes and after every confirmed transaction. */
@@ -79,7 +81,8 @@ export type LifecycleTransaction = {
79
81
  marketStart?: number; marketCount?: number; gas?: bigint; gasPrice?: bigint; nonce?: bigint;
80
82
  /** Exact encoded byte count; populated on SDK-built commands, including approvals. */
81
83
  calldataBytes?: number;
82
- /** Exact current-state execution admission, not a wallet batch or future guarantee. */
84
+ /** Only the immediate command receives exact gas/price/estimate/admission.
85
+ * Future commands must be re-sized and proved by buildNextTransaction. */
83
86
  admission?: LifecycleAdmission;
84
87
  estimate?: LifecycleTransactionEstimate;
85
88
  };
@@ -104,7 +107,12 @@ export type LifecycleFailureCategory = "semantic" | "capacity" | "opaque" | "sou
104
107
  export type LifecycleCapacityConstraint = "compute" | "gas-envelope" | "calldata" | "simulation-gas";
105
108
  export type LifecycleSimulationStep = {
106
109
  transactionId: string; success: boolean; gasUsed?: bigint; gasLimit?: bigint; gasRequired?: bigint;
110
+ /** Compute-only sizing observation, excluding Nitro poster gas; not admission. */
111
+ computeGasRequired?: bigint;
107
112
  returnData?: Hex; error?: string; estimate?: LifecycleTransactionEstimate;
113
+ /** Suffix feasibility budget only, not a transaction estimate or execution admission.
114
+ * buildNextTransaction obtains a fresh poster quote and exact envelope at its turn. */
115
+ provisional?: boolean;
108
116
  failureCategory?: LifecycleFailureCategory; decodedError?: string;
109
117
  /** Safe failed-call facts only; native messages and error-data bytes are never copied here. */
110
118
  nativeErrorCode?: number;
@@ -115,13 +123,15 @@ export type LifecycleSimulationStep = {
115
123
  };
116
124
  export type LifecycleSimulation = LifecycleProofOutcomes & {
117
125
  backend: "eth_simulateV1" | "controlled-fork" | "unavailable";
118
- /** Admission/proved outcomes cover the successful prefix, not the deferred suffix. */
126
+ /** Full successful prefix feasibility; only its immediate envelope is final. */
119
127
  confidence: "stateful" | "provisional"; admitted: boolean;
120
128
  blockNumber: bigint; blockHash: Hash; account: Address; chainId: bigint;
121
129
  steps: readonly LifecycleSimulationStep[]; limits: ResolvedLifecycleLimits;
122
130
  /** Unproved suffix. These transactions receive no gas, price, estimate or admission. */
123
131
  deferredTransactionIds: readonly string[];
124
132
  reason?: string; failedTransactionId?: string;
133
+ /** Affordability can mean insufficient sizing margin, even if smaller exact
134
+ * envelopes could fit. A refused RPC exposes no prefix results to refine them. */
125
135
  failureCategory?: LifecycleFailureCategory; capacityConstraint?: LifecycleCapacityConstraint;
126
136
  };
127
137
  export type LifecycleFundingPrerequisite = {