@black-market/sdk 0.10.0 → 0.12.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 (83) hide show
  1. package/README.md +21 -2
  2. package/dist/lifecycle/abi.d.ts +1 -0
  3. package/dist/lifecycle/abi.d.ts.map +1 -1
  4. package/dist/lifecycle/abi.js +1 -0
  5. package/dist/lifecycle/abi.js.map +1 -1
  6. package/dist/lifecycle/calibration.d.ts.map +1 -1
  7. package/dist/lifecycle/calibration.js +2 -0
  8. package/dist/lifecycle/calibration.js.map +1 -1
  9. package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.d.ts +71 -0
  10. package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.d.ts.map +1 -0
  11. package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.js +73 -0
  12. package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.js.map +1 -0
  13. package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.d.ts +71 -0
  14. package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.d.ts.map +1 -0
  15. package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.js +73 -0
  16. package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.js.map +1 -0
  17. package/dist/lifecycle/index.d.ts +1 -0
  18. package/dist/lifecycle/index.d.ts.map +1 -1
  19. package/dist/lifecycle/index.js +1 -0
  20. package/dist/lifecycle/index.js.map +1 -1
  21. package/dist/lifecycle/markets.d.ts +3 -1
  22. package/dist/lifecycle/markets.d.ts.map +1 -1
  23. package/dist/lifecycle/markets.js +20 -3
  24. package/dist/lifecycle/markets.js.map +1 -1
  25. package/dist/lifecycle/planner.d.ts.map +1 -1
  26. package/dist/lifecycle/planner.js +83 -54
  27. package/dist/lifecycle/planner.js.map +1 -1
  28. package/dist/lifecycle/presets.d.ts +9 -2
  29. package/dist/lifecycle/presets.d.ts.map +1 -1
  30. package/dist/lifecycle/presets.js +26 -5
  31. package/dist/lifecycle/presets.js.map +1 -1
  32. package/dist/lifecycle/progress.d.ts.map +1 -1
  33. package/dist/lifecycle/progress.js +26 -5
  34. package/dist/lifecycle/progress.js.map +1 -1
  35. package/dist/lifecycle/rpc.js +3 -2
  36. package/dist/lifecycle/rpc.js.map +1 -1
  37. package/dist/lifecycle/simulation.d.ts +1 -1
  38. package/dist/lifecycle/simulation.d.ts.map +1 -1
  39. package/dist/lifecycle/simulation.js +119 -175
  40. package/dist/lifecycle/simulation.js.map +1 -1
  41. package/dist/lifecycle/transactions.d.ts +6 -0
  42. package/dist/lifecycle/transactions.d.ts.map +1 -1
  43. package/dist/lifecycle/transactions.js +39 -0
  44. package/dist/lifecycle/transactions.js.map +1 -1
  45. package/dist/lifecycle/types.d.ts +17 -4
  46. package/dist/lifecycle/types.d.ts.map +1 -1
  47. package/dist/lifecycle/types.js.map +1 -1
  48. package/dist/lifecycle/ufg-abi.d.ts +2386 -0
  49. package/dist/lifecycle/ufg-abi.d.ts.map +1 -0
  50. package/dist/lifecycle/ufg-abi.js +171 -0
  51. package/dist/lifecycle/ufg-abi.js.map +1 -0
  52. package/dist/lifecycle/ufg-config.d.ts +38 -0
  53. package/dist/lifecycle/ufg-config.d.ts.map +1 -0
  54. package/dist/lifecycle/ufg-config.js +75 -0
  55. package/dist/lifecycle/ufg-config.js.map +1 -0
  56. package/dist/lifecycle/ufg.d.ts +21 -0
  57. package/dist/lifecycle/ufg.d.ts.map +1 -0
  58. package/dist/lifecycle/ufg.js +98 -0
  59. package/dist/lifecycle/ufg.js.map +1 -0
  60. package/docs/launch-lifecycle.md +215 -32
  61. package/examples/.env.example +4 -0
  62. package/examples/README.md +35 -2
  63. package/examples/launch-example-support.ts +1 -0
  64. package/examples/launch-example.ts +1 -0
  65. package/examples/launch.ts +1 -0
  66. package/examples/ufg-launch-config.ts +31 -0
  67. package/package.json +1 -1
  68. package/src/lifecycle/abi.ts +1 -0
  69. package/src/lifecycle/calibration.ts +1 -0
  70. package/src/lifecycle/deployments/ufg-dynamic-pool-20261021.ts +74 -0
  71. package/src/lifecycle/deployments/ufg-fixed-pool-20261021.ts +74 -0
  72. package/src/lifecycle/index.ts +1 -0
  73. package/src/lifecycle/markets.ts +16 -3
  74. package/src/lifecycle/planner.ts +75 -51
  75. package/src/lifecycle/presets.ts +27 -6
  76. package/src/lifecycle/progress.ts +22 -5
  77. package/src/lifecycle/rpc.ts +3 -2
  78. package/src/lifecycle/simulation.ts +114 -166
  79. package/src/lifecycle/transactions.ts +38 -1
  80. package/src/lifecycle/types.ts +15 -5
  81. package/src/lifecycle/ufg-abi.ts +173 -0
  82. package/src/lifecycle/ufg-config.ts +77 -0
  83. package/src/lifecycle/ufg.ts +86 -0
@@ -6,6 +6,8 @@ import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureRe
6
6
  import { LifecyclePlanningError, type CanonicalLaunchProgress, type LifecycleBlock, type LifecycleMarketProgress, type LifecycleProfile, type LifecycleReceiptReference, type LifecycleReceiptStatus, type LifecycleRpcClient, type PoolBoundHookDeployment, type ReadLaunchProgressOptions } from "./types.js";
7
7
  import { deriveKnownPoolBoundHookDeployment } from "./calibration.js";
8
8
  import { getKnownLifecycleDeployment, getKnownLifecycleProfile } from "./presets.js";
9
+ import { ufgLaunchProxyV1Abi } from "./ufg-abi.js";
10
+ import { assertUfgLaunchAccount, hashUfgLaunchConfig, UFG_LAUNCH_PROXY_ADDRESS, type UfgLaunchConfig } from "./ufg-config.js";
9
11
 
10
12
  export const ABYSS_LIFECYCLE_CONFIG_SCHEMA = keccak256(stringToHex("(uint8,uint24,bytes32,uint160,(int24,int24,uint128,uint256)[])"));
11
13
 
@@ -460,12 +462,17 @@ async function transactionMatchesLaunch(transaction: Record<string, unknown>, op
460
462
  if (transaction.chainId !== undefined && rpcQuantity(transaction.chainId, "receipt transaction chain") !== planned.plan.chainId) throw new LifecyclePlanningError("RECEIPT_CHAIN", "Receipt transaction belongs to a different chain");
461
463
  const to = transaction.to === null ? zeroAddress : rpcHex(transaction.to, "receipt transaction target");
462
464
  const input = rpcHex(transaction.input ?? transaction.data, "receipt transaction calldata");
463
- if (to.toLowerCase() === planned.plan.orchestrator.toLowerCase()) {
465
+ const target = planned.ufg === undefined ? planned.plan.orchestrator : UFG_LAUNCH_PROXY_ADDRESS;
466
+ if (to.toLowerCase() === target.toLowerCase()) {
464
467
  try {
465
- const decoded = decodeFunctionData({ abi: launchLifecycleAbi, data: input });
468
+ const decoded = decodeFunctionData({ abi: planned.ufg === undefined ? launchLifecycleAbi : ufgLaunchProxyV1Abi, data: input });
466
469
  if (!["launchAtomic", "beginLaunch", "prepareMarkets", "activateLaunch", "cancelLaunch"].includes(decoded.functionName) || !Array.isArray(decoded.args)) return false;
467
470
  // Decoded plan layout is validated by the exact ABI, not trusted raw RPC JSON.
468
471
  const plan = decoded.args[0] as LaunchPlanV1;
472
+ if (planned.ufg !== undefined && (decoded.functionName === "launchAtomic" || decoded.functionName === "beginLaunch")) {
473
+ const config = decoded.args[decoded.functionName === "launchAtomic" ? 1 : 2] as UfgLaunchConfig;
474
+ if (hashUfgLaunchConfig(config) !== planned.ufg.configHash) return false;
475
+ }
469
476
  return hashLaunchPlan(plan).toLowerCase() === planned.planHash.toLowerCase();
470
477
  } catch { return false; }
471
478
  }
@@ -477,7 +484,7 @@ async function transactionMatchesLaunch(transaction: Record<string, unknown>, op
477
484
  if (decoded.functionName !== "approve" || rpcQuantity(transaction.value ?? "0x0", "approval native value") !== 0n) return false;
478
485
  spender = decoded.args[0];
479
486
  } catch { return false; }
480
- const escrow = await readLifecycleContract<Address>(options.client, planned.plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], block);
487
+ const escrow = planned.ufg !== undefined ? UFG_LAUNCH_PROXY_ADDRESS : await readLifecycleContract<Address>(options.client, planned.plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], block);
481
488
  return spender.toLowerCase() === escrow.toLowerCase();
482
489
  }
483
490
 
@@ -542,7 +549,10 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
542
549
  assertActive();
543
550
  const chainId = chainIdValue;
544
551
  if (chainId !== planned.plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed launch");
545
- if (planned.account.toLowerCase() !== planned.plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
552
+ if (planned.ufg !== undefined) {
553
+ assertUfgLaunchAccount(planned.plan, planned.account);
554
+ if (hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG config differs from its finalized commitment");
555
+ } else if (planned.account.toLowerCase() !== planned.plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
546
556
  if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan no longer matches its commitment");
547
557
  const [head, confirmedProgress, token, confirmedNonce, headNonce, pendingNonce] = await Promise.all([
548
558
  readLifecycleContract<LaunchProgressV1>(client, planned.plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [planned.launchId], block, planned.account),
@@ -556,10 +566,17 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
556
566
  const canonical = confirmedProgress ?? head;
557
567
  if (token.toLowerCase() !== planned.predictedToken.toLowerCase()) throw new LifecyclePlanningError("TOKEN_IDENTITY", "Predicted token changed for the committed token configuration");
558
568
  for (const progress of [canonical, head]) if (progress.phase !== LifecyclePhase.None) {
559
- if (progress.planHash.toLowerCase() !== planned.planHash.toLowerCase() || progress.launchId.toLowerCase() !== planned.launchId.toLowerCase() || progress.creator.toLowerCase() !== planned.account.toLowerCase() || progress.nonce !== planned.plan.nonce || progress.token.toLowerCase() !== token.toLowerCase() || progress.marketCount !== planned.plan.markets.length || progress.buyCount !== planned.plan.buys.length || progress.deadline !== planned.plan.deadline) throw new LifecyclePlanningError("PLAN_REPLAY", "This creator nonce is already bound to a different economic launch");
569
+ if (progress.planHash.toLowerCase() !== planned.planHash.toLowerCase() || progress.launchId.toLowerCase() !== planned.launchId.toLowerCase() || progress.creator.toLowerCase() !== planned.plan.creator.toLowerCase() || progress.nonce !== planned.plan.nonce || progress.token.toLowerCase() !== token.toLowerCase() || progress.marketCount !== planned.plan.markets.length || progress.buyCount !== planned.plan.buys.length || progress.deadline !== planned.plan.deadline) throw new LifecyclePlanningError("PLAN_REPLAY", "This creator nonce is already bound to a different economic launch");
560
570
  if (progress.mode !== (planned.mode === "atomic" ? LifecycleMode.Atomic : LifecycleMode.Staged)) throw new LifecyclePlanningError("MODE_MISMATCH", "Execution mode differs from the already recorded launch");
561
571
  if (progress.preparedMarkets > progress.marketCount || progress.phase === LifecyclePhase.Activating || (progress.phase === LifecyclePhase.Ready && progress.preparedMarkets !== progress.marketCount)) throw new LifecyclePlanningError("INVALID_PROGRESS", "Canonical progress violates the committed ordered state machine");
562
572
  }
573
+ if (planned.ufg !== undefined && head.phase !== LifecyclePhase.None) {
574
+ const [principal, configHash] = await Promise.all([
575
+ readLifecycleContract<Address>(client, UFG_LAUNCH_PROXY_ADDRESS, ufgLaunchProxyV1Abi, "principalOf", [planned.launchId], block),
576
+ readLifecycleContract<Hash>(client, UFG_LAUNCH_PROXY_ADDRESS, ufgLaunchProxyV1Abi, "launchConfigHash", [planned.launchId], block),
577
+ ]);
578
+ if (principal.toLowerCase() !== planned.account.toLowerCase() || configHash.toLowerCase() !== planned.ufg.configHash.toLowerCase()) throw new LifecyclePlanningError("PLAN_REPLAY", "UFG proxy principal or launch config differs from the stored launch");
579
+ }
563
580
  const confirmedAccountNonce = rpcQuantity(confirmedNonce, "confirmed account nonce");
564
581
  const headAccountNonce = headNonce === undefined ? confirmedAccountNonce : rpcQuantity(headNonce, "head account nonce");
565
582
  const pendingAccountNonce = rpcQuantity(pendingNonce, "pending account nonce");
@@ -433,6 +433,7 @@ async function resolveLifecycleLimitsFresh(context: LifecycleLimitContext, sourc
433
433
  throw new LifecyclePlanningError("LIMIT_CONTEXT_MISMATCH", "Supplied execution policy provenance differs from the canonical chain/account/core/block context");
434
434
  }
435
435
  const protocol = context.chainId === 4663n ? "nitro" : "evm";
436
+ const maxSimulationGas = configuration.maxSimulationGas ?? (context.chainId === 4663n ? 50_000_000n : undefined);
436
437
  let arbOSVersion: bigint | undefined; let maxTxComputeGas: bigint | undefined; let maxBlockComputeGas: bigint | undefined;
437
438
  if (protocol === "nitro") {
438
439
  try {
@@ -464,14 +465,14 @@ async function resolveLifecycleLimitsFresh(context: LifecycleLimitContext, sourc
464
465
  ...(configuration.rpcGasLimit === undefined ? ["RPC transaction gas-envelope cap is unknown"] : []),
465
466
  ...(configuration.accountGasLimit === undefined ? ["Current account/wallet transaction gas-envelope cap is unknown"] : []),
466
467
  ...(configuration.maxCalldataBytes === undefined ? ["Applicable chain/account/RPC calldata byte cap is unknown"] : []),
467
- ...(configuration.maxSimulationGas === undefined ? ["RPC aggregate simulation gas cap is unknown"] : []),
468
+ ...(maxSimulationGas === undefined ? ["RPC aggregate simulation gas cap is unknown"] : []),
468
469
  ];
469
470
  assertCurrent();
470
471
  return {
471
472
  configuration, assertCurrent,
472
473
  limits: Object.freeze({
473
474
  protocol, executionGasCeiling, transactionGasCeiling, arbOSVersion, maxTxComputeGas, maxBlockComputeGas, headroomBps,
474
- maxCalldataBytes: configuration.maxCalldataBytes, maxSimulationGas: configuration.maxSimulationGas,
475
+ maxCalldataBytes: configuration.maxCalldataBytes, maxSimulationGas,
475
476
  blockGasLimit: context.block.gasLimit, chainGasLimit: configuration.chainGasLimit,
476
477
  rpcGasLimit: configuration.rpcGasLimit, accountGasLimit: configuration.accountGasLimit,
477
478
  unknownExecutionConstraints: Object.freeze(unknownExecutionConstraints),
@@ -4,6 +4,9 @@ import { hashLaunchIdentity, hashLaunchPlan, LifecyclePhase, type LaunchPlanV1,
4
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";
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
+ import { hmmV2ForwardingReserve } from "./presets.js";
8
+ import { transactionGasEstimate } from "./transactions.js";
9
+ import { hashUfgLaunchConfig } from "./ufg-config.js";
7
10
 
8
11
  function committedPositionCount(plan: LaunchPlanV1): number {
9
12
  let count = 0;
@@ -111,8 +114,6 @@ async function proveNitroBackend(client: LifecycleRpcClient, planned: PlannedLau
111
114
  }
112
115
 
113
116
  async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunch, block: LifecycleBlock, transactions: readonly LifecycleTransaction[], gasLimits: readonly bigint[], limits: ResolvedLifecycleLimits, gasPrice: bigint, validation: boolean): Promise<LifecycleSimulationStep[]> {
114
- const totalRequestedGas = gasLimits.reduce((total, gas) => total + gas, 0n);
115
- if (limits.maxSimulationGas !== undefined && totalRequestedGas > limits.maxSimulationGas) throw new LifecyclePlanningError("SIMULATION_RPC_CAP", "Sequential request exceeds the current RPC aggregate simulation gas cap; use an isolated controlled fork");
116
117
  // Natural child-block fees must govern fee-bearing admission; overriding
117
118
  // baseFeePerGas with the parent's would fabricate a lower envelope and admit
118
119
  // calls that the actual chain would reject. eth_simulateV1 derives the child
@@ -163,7 +164,7 @@ async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunc
163
164
  const buy = planned.plan.buys[buyIndex];
164
165
  if (buy === undefined || (receipt.quoteSpent[buyIndex] ?? buy.quoteAmountIn + 1n) > buy.quoteAmountIn || (receipt.tokenOut[buyIndex] ?? 0n) < buy.minTokenOut) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not satisfy every ordered buy's budget and output minimum");
165
166
  }
166
- } else if (transaction.kind === "cancel" && !events.some((event) => event.eventName === "LaunchCancelled" && event.args.launchId.toLowerCase() === planned.launchId.toLowerCase() && event.args.creator.toLowerCase() === planned.account.toLowerCase())) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not cancel the exact committed launch");
167
+ } else if (transaction.kind === "cancel" && !events.some((event) => event.eventName === "LaunchCancelled" && event.args.launchId.toLowerCase() === planned.launchId.toLowerCase() && event.args.creator.toLowerCase() === planned.plan.creator.toLowerCase())) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not cancel the exact committed launch");
167
168
  else if ((transaction.kind === "approve" || transaction.kind === "approve-reset") && returnData !== "0x" && decodeFunctionResult({ abi: lifecycleErc20Abi, functionName: "approve", data: returnData }) !== true) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Funding approval returned failure");
168
169
  } catch (failure) { error = failure instanceof LifecyclePlanningError ? failure.message : "Simulation returned an invalid committed postcondition"; }
169
170
  }
@@ -315,7 +316,7 @@ function observedFields<T extends object>(value: T): () => boolean {
315
316
 
316
317
  /** Internal continuation owned by one SDK read invocation. Only its positive
317
318
  * capability proof is reused; every candidate executes its entire economic sequence. */
318
- export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true): PreparedLaunchSimulation {
319
+ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true, reviewedEnvelope?: NextTransactionEnvelope): PreparedLaunchSimulation {
319
320
  const { client, planned, fork } = options;
320
321
  const signal = options.signal;
321
322
  if (signal?.aborted) throw Object.assign(new Error("Lifecycle simulation cancelled"), { name: "AbortError" });
@@ -346,6 +347,7 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
346
347
  if (hashLaunchPlan(plan).toLowerCase() !== planHash.toLowerCase() || hashLaunchIdentity(plan).toLowerCase() !== launchId.toLowerCase()) {
347
348
  throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed after its simulation context was bound");
348
349
  }
350
+ if (planned.ufg !== undefined && hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG treasury config changed after its simulation context was bound");
349
351
  };
350
352
  const pending = (async () => {
351
353
  if (fork !== undefined && lifecycleSourceClient(fork.client) === sourceClient) throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
@@ -353,7 +355,7 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
353
355
  resolveLifecycleLimits(context, policy),
354
356
  readLifecycleChainId(client),
355
357
  lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block),
356
- hasTransactions ? lifecycleRpc(client, "eth_gasPrice") : undefined,
358
+ hasTransactions && reviewedEnvelope === undefined ? lifecycleRpc(client, "eth_gasPrice") : undefined,
357
359
  ]));
358
360
  })().then((observations) => ({ observations }), (failure: unknown) => ({ failure }));
359
361
  // Refused inputs may discard this read-only preparation before it settles.
@@ -439,7 +441,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
439
441
  // Standalone calls always produce fresh context, including live chain and fees.
440
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");
441
443
  const block = pinnedBlock ?? await readLifecycleBlock(options.client);
442
- const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0);
444
+ const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope);
443
445
  try { return await prepared(transactions, nextEnvelope); }
444
446
  finally { prepared.close(); }
445
447
  }
@@ -458,7 +460,7 @@ async function simulateLaunchTransactionsWithContext(
458
460
  const sourceClient = lifecycleSourceClient(client);
459
461
  const { block } = context;
460
462
  const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = observations;
461
- const base = { confidence: "stateful" as const, executionProof: "failed" as const, protocolFit: "unknown" as const, transportPreflight: "not-requested" as const, blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits };
463
+ const base = { confidence: "stateful" as const, executionProof: "failed" as const, protocolFit: "unknown" as const, transportPreflight: "not-requested" as const, blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits, deferredTransactionIds: [] as string[] };
462
464
  if (chainId !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
463
465
  const accountCode = rpcHex(accountCodeValue, "account code");
464
466
  if (accountCode !== "0x") {
@@ -473,36 +475,48 @@ async function simulateLaunchTransactionsWithContext(
473
475
  }
474
476
  }
475
477
  if (transactions.length === 0) { await assertLifecycleBlock(client, block, planned.chainId); return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] }; }
476
- const recommendedGasPrice = rpcQuantity(gasPriceValue, "gas price");
477
- if (recommendedGasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Recommended gas price must be a uint256 quantity");
478
- // Initial construction reserves twice the pinned base fee if the quote lags.
479
- // Reviewed fees are not new recommendations: native replay must accept them
480
- // under natural child fees, with no base-fee override or silent fee increase.
478
+ if (nextEnvelope !== undefined && (transactions.length !== 1 || typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas <= 0n || nextEnvelope.gas >= 1n << 64n ||
479
+ typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
480
+ throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Reviewed replay requires one immediate transaction with exact uint64 gas and uint256 price");
481
+ }
482
+ const allTransactions = transactions;
483
+ if (limits.maxSimulationGas !== undefined && nextEnvelope === undefined) {
484
+ let estimatedGas = 0n, count = 0;
485
+ for (const transaction of transactions) {
486
+ estimatedGas += transactionGasEstimate(planned.plan, transaction);
487
+ if (estimatedGas > limits.maxSimulationGas) break;
488
+ count += 1;
489
+ }
490
+ transactions = transactions.slice(0, count);
491
+ base.deferredTransactionIds = allTransactions.slice(count).map((transaction) => transaction.id);
492
+ if (count === 0) {
493
+ const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
494
+ const exceedsCompute = estimatedGas > computeCap;
495
+ const gasInMillions = (gas: bigint) => {
496
+ const tenths = (gas + 50_000n) / 100_000n;
497
+ return `${tenths / 10n}.${tenths % 10n}`;
498
+ };
499
+ const atomic = allTransactions[0]!.kind === "atomic";
500
+ const reason = exceedsCompute
501
+ ? `${atomic ? "Atomic launch" : "Immediate transaction"} needs about ${gasInMillions(estimatedGas)}M gas, above the ${gasInMillions(computeCap)}M per-transaction limit${atomic ? "; use staged" : ""}`
502
+ : "The immediate transaction estimate exceeds the RPC aggregate simulation gas cap; no prefix can be proved";
503
+ await assertLifecycleBlock(client, block, planned.chainId);
504
+ return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: exceedsCompute ? "compute" : "simulation-gas", reason };
505
+ }
506
+ }
507
+ const recommendedGasPrice = nextEnvelope === undefined ? rpcQuantity(gasPriceValue, "gas price") : nextEnvelope.gasPrice;
508
+ // New recommendations cover lagging quotes; reviewed fees are replayed unchanged
509
+ // under natural child fees, never refreshed or silently increased.
481
510
  const baseFeeEnvelope = (block.baseFeePerGas ?? 0n) * 2n;
482
511
  const gasPrice = nextEnvelope?.gasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
483
512
  if (gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
484
- if (nextEnvelope !== undefined && (typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas <= 0n || nextEnvelope.gas >= 1n << 64n ||
485
- typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
486
- throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Reviewed gas and gas price must be exact positive uint64 gas and nonnegative uint256 price");
487
- }
488
513
  const buffer = (gas: bigint) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
489
- const computeCeiling = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
490
- // Nonnegative poster gas cannot make this complete discovery request smaller.
491
- // A generic controlled fork retains the existing RPC-to-fork fallback.
492
- if (limits.maxSimulationGas !== undefined && BigInt(transactions.length) * computeCeiling > limits.maxSimulationGas &&
493
- (limits.protocol === "nitro" || options.fork === undefined)) {
494
- assertCurrent();
495
- await assertLifecycleBlock(client, block, planned.chainId);
496
- return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: "simulation-gas", reason: `Stateful sequential RPC simulation unavailable: Sequential request exceeds the current RPC aggregate simulation gas cap; use an isolated controlled fork. ${limits.protocol === "nitro" ? "Generic Anvil/Hardhat forks cannot prove ArbOS compute/poster metering." : "Dependent eth_call estimates are not proof."}` };
497
- }
498
514
  const posterGas: bigint[] = [];
499
515
  if (limits.protocol === "nitro") {
500
- // Lightweight poster reads may overlap the isolated capability request.
501
- // Economic work is not even queued until both prerequisites have succeeded.
502
516
  try {
503
517
  const [, estimates] = await Promise.all([
504
518
  proveBackend(limits, gasPrice),
505
- lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
519
+ nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))) : [],
506
520
  ]);
507
521
  posterGas.push(...estimates);
508
522
  } catch (failure) {
@@ -513,176 +527,110 @@ async function simulateLaunchTransactionsWithContext(
513
527
  }
514
528
  }
515
529
  assertCurrent();
516
- // The poster quote allocates gas only. The native exact replay enforces actual
517
- // compute hold; subtracting this approximate budget from gasUsed would not.
518
530
  const discoveryGas = transactions.map((_, index) => {
519
531
  const total = limits.executionGasCeiling + (posterGas[index] ?? 0n);
520
532
  const capped = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < total ? limits.transactionGasCeiling : total;
521
533
  if (capped <= 0n || capped >= 1n << 64n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Compute/poster envelope exceeds uint64 gas");
522
534
  return capped;
523
535
  });
524
- let backend: "eth_simulateV1" | "controlled-fork" = "eth_simulateV1";
525
- let measured: LifecycleSimulationStep[];
526
- // A permissive measurement needs the legal complete compute + poster envelope.
527
- // It is not proof: fixed compute/headroom checks and exact validated replay still follow.
528
- try {
529
- measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, discoveryGas, limits, gasPrice, false));
530
- }
531
- catch (failure) {
532
- if (simulationFailureKind(failure) === "abort") throw failure;
536
+ let backend: "eth_simulateV1" | "controlled-fork" = nextEnvelope !== undefined && limits.protocol === "evm" && options.fork !== undefined ? "controlled-fork" : "eth_simulateV1";
537
+ let measured: LifecycleSimulationStep[] = [];
538
+ if (nextEnvelope === undefined) {
539
+ try {
540
+ measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, discoveryGas, limits, gasPrice, false));
541
+ } catch (failure) {
542
+ if (simulationFailureKind(failure) === "abort") throw failure;
543
+ assertCurrent();
544
+ if (options.fork === undefined || limits.protocol === "nitro") {
545
+ await assertLifecycleBlock(client, block, planned.chainId);
546
+ 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
+ }
548
+ backend = "controlled-fork";
549
+ measured = await simulateForkPass(options.fork, planned, block, transactions, discoveryGas, gasPrice, true, limits.headroomBps);
550
+ }
533
551
  assertCurrent();
534
- if (options.fork === undefined || limits.protocol === "nitro") {
552
+ const failedIndex = measured.findIndex((step) => !step.success);
553
+ if (failedIndex >= 0 && limits.maxSimulationGas !== undefined &&
554
+ measured.reduce((gas, step) => gas + (step.gasUsed ?? 0n), 0n) * 100n >= limits.maxSimulationGas * 95n) {
555
+ // The provider exhausted its aggregate used-gas budget, not the contract.
556
+ // Preserve only actual successful results; the failed call is deferred too.
557
+ measured = measured.slice(0, failedIndex);
558
+ transactions = transactions.slice(0, failedIndex);
559
+ base.deferredTransactionIds = allTransactions.slice(failedIndex).map((transaction) => transaction.id);
560
+ if (transactions.length === 0) {
561
+ await assertLifecycleBlock(client, block, planned.chainId);
562
+ 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
+ }
564
+ }
565
+ 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 }; }
567
+ if (diagnostic) {
535
568
  await assertLifecycleBlock(client, block, planned.chainId);
536
- 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."}` };
569
+ return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
537
570
  }
538
- backend = "controlled-fork";
539
- measured = await simulateForkPass(options.fork, planned, block, transactions, discoveryGas, gasPrice, true, limits.headroomBps);
540
- }
541
- assertCurrent();
542
- const failed = measured.find((step) => !step.success);
543
- 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 }; }
544
- if (diagnostic) {
545
- await assertLifecycleBlock(client, block, planned.chainId);
546
- return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
547
571
  }
548
- // Padding belongs to new envelopes. A reviewed envelope may consume its
549
- // original margin; only exact replay at its held gas/price proves it still fits.
550
- const computeGas = measured.map((step, index) => {
551
- const required = step.gasRequired ?? step.gasUsed ?? computeCeiling;
552
- return nextEnvelope !== undefined && index === 0 ? required : buffer(required);
553
- });
554
- const gasLimits = computeGas.map((gas, index) =>
555
- nextEnvelope !== undefined && index === 0 ? nextEnvelope.gas : gas + (posterGas[index] ?? 0n));
556
- const exceeds = gasLimits.findIndex((gas, index) => gas <= 0n || gas >= 1n << 64n || (computeGas[index] ?? 0n) > limits.executionGasCeiling || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling);
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);
578
+ }) : [nextEnvelope.gas];
579
+ const exceeds = gasLimits.findIndex((gas, index) =>
580
+ gas <= 0n || gas >= 1n << 64n || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling ||
581
+ nextEnvelope === undefined && (measured[index]!.gasRequired ?? measured[index]!.gasUsed!) > gas - (posterGas[index] ?? 0n) ||
582
+ limits.protocol === "evm" && gas > limits.executionGasCeiling);
557
583
  if (exceeds >= 0) {
558
584
  await assertLifecycleBlock(client, block, planned.chainId);
559
- return { ...base, executionProof: "unavailable", protocolFit: "failed", backend, admitted: false, steps: measured, failureCategory: "capacity", capacityConstraint: (computeGas[exceeds] ?? 0n) > limits.executionGasCeiling ? "compute" : "gas-envelope", reason: "Execution compute/headroom or complete poster gas envelope is outside positive uint64 gas and current protocol/known transaction limits", failedTransactionId: transactions[exceeds]?.id };
585
+ 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 };
560
586
  }
561
- // Envelope discovery may enlarge future unfinished steps, never the reviewed
562
- // immediate one. Every retry still executes the entire sequence at held price.
563
- const envelopeDiscoveryGas = nextEnvelope === undefined ? discoveryGas : discoveryGas.map((gas, index) => index === 0 ? nextEnvelope.gas : gas);
564
- // Replay the complete sequence from its original state at the exact buffered
565
- // limits. Only explicit native OOG can discover one larger legal envelope;
566
- // semantic, opaque and transport failures cannot authorize a capacity retry.
567
- let verified: LifecycleSimulationStep[];
568
- let validatedReplayError: string | undefined;
569
- assertCurrent();
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);
570
593
  try {
571
- if (backend === "eth_simulateV1") verified = await lifecycleStage(client, "simulation.replay", () => simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true));
572
- else {
573
- if (options.fork === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Controlled fork is missing for the exact-gas replay");
574
- verified = await simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice, false, limits.headroomBps);
575
- }
594
+ verifiedSteps = await lifecycleStage(client, "simulation.replay", () => replay(gasLimits));
576
595
  } catch (failure) {
577
596
  const kind = simulationFailureKind(failure);
578
597
  if (kind === "abort") throw failure;
579
598
  assertCurrent();
580
- const message = lifecycleFailureReason(failure);
581
- if (backend !== "eth_simulateV1") {
599
+ if (kind !== "out-of-gas" || backend !== "eth_simulateV1" || nextEnvelope !== undefined) {
582
600
  await assertLifecycleBlock(client, block, planned.chainId);
583
- return { ...base, backend, admitted: false, steps: [], reason: `Exact-gas validated replay failed: ${message}` };
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)}` };
584
602
  }
585
- if (kind !== "out-of-gas") {
586
- await assertLifecycleBlock(client, block, planned.chainId);
587
- return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, failureCategory: kind === "source" ? "source" : "opaque", reason: `Exact-gas validated replay unavailable: ${message}` };
588
- }
589
- validatedReplayError = message;
590
- verified = [];
603
+ replayError = lifecycleFailureReason(failure);
591
604
  }
592
605
  assertCurrent();
593
- let verifiedSteps = verified;
594
- let admittedGasLimits = gasLimits;
595
606
  let finalFailure = verifiedSteps.find((step) => !step.success);
596
- let finalFailureReason: string | undefined;
597
- if (finalFailure === undefined && validatedReplayError !== undefined) {
598
- if (!envelopeDiscoveryGas.some((gas, index) => gas > (gasLimits[index] ?? gas))) {
599
- await assertLifecycleBlock(client, block, planned.chainId);
600
- return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling failed: ${validatedReplayError}` };
601
- }
602
- // The buffered validated replay threw before yielding steps. Attempt one
603
- // explicit envelope-discovery replay at the exact execution ceiling on the
604
- // same pinned block; headroom is never silently raised — the carried gas is
605
- // the validated envelope itself.
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) {
606
614
  assertCurrent();
607
615
  try {
608
- const ceilingLimits = envelopeDiscoveryGas;
609
- const retried = backend === "eth_simulateV1"
610
- ? await lifecycleStage(client, "simulation.envelope", () => simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true))
611
- : options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
612
- const retryFailureIndex = retried?.findIndex((step) => !step.success) ?? -1;
613
- const retryFailure = retried?.[retryFailureIndex];
614
- if (retried === undefined) {
615
- await assertLifecycleBlock(client, block, planned.chainId);
616
- return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${validatedReplayError}` };
617
- }
618
- admittedGasLimits = ceilingLimits;
619
- verifiedSteps = retried;
620
- finalFailure = retryFailure;
621
- validatedReplayError = undefined;
622
- if (retryFailure !== undefined) {
623
- const kind = transactions[retryFailureIndex]?.kind;
624
- const indivisible = kind === "activate" || kind === "atomic";
625
- finalFailureReason = `Validated execution at the exact current gas ceiling still failed: ${retryFailure.error ?? "Execution reverted"}${indivisible ? "; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned" : ""}`;
626
- }
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;
627
620
  } catch (failure) {
628
621
  const kind = simulationFailureKind(failure);
629
622
  if (kind === "abort") throw failure;
630
623
  assertCurrent();
631
624
  await assertLifecycleBlock(client, block, planned.chainId);
632
- 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 after buffered execution refusal: ${lifecycleFailureReason(failure)}` };
633
- return { ...base, backend, admitted: false, steps: measured, reason: `Validated buffered replay failed (${validatedReplayError}) and execution at the exact gas ceiling still reverted; final mint/lock/all-buys/public-opening is indivisible` };
634
- }
635
- } else if (finalFailure !== undefined) {
636
- const failedIndex = verifiedSteps.findIndex((step) => !step.success);
637
- const bufferedLimit = gasLimits[failedIndex];
638
- const failedTransaction = transactions[failedIndex];
639
- if (bufferedLimit !== undefined && failedTransaction !== undefined && (nextEnvelope === undefined || failedIndex !== 0) &&
640
- looksGasExhausted(finalFailure, bufferedLimit) && (envelopeDiscoveryGas[failedIndex] ?? 0n) > bufferedLimit) {
641
- // Buffered replay returned an exhaustion-shaped failed step; learn the true
642
- // validated envelope with one explicit replay at the exact execution ceiling.
643
- assertCurrent();
644
- try {
645
- const ceilingLimits = envelopeDiscoveryGas;
646
- const retried = backend === "eth_simulateV1"
647
- ? await lifecycleStage(client, "simulation.envelope", () => simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true))
648
- : options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
649
- if (retried === undefined) {
650
- await assertLifecycleBlock(client, block, planned.chainId);
651
- return { ...base, backend, admitted: false, steps: measured, reason: "Validated execution at the exact current gas ceiling omitted transaction results" };
652
- }
653
- const retryFailureIndex = retried.findIndex((step) => !step.success);
654
- const retryFailure = retried[retryFailureIndex];
655
- admittedGasLimits = ceilingLimits;
656
- verifiedSteps = retried;
657
- finalFailure = retryFailure;
658
- if (retryFailure !== undefined) {
659
- const kind = transactions[retryFailureIndex]?.kind;
660
- const indivisible = kind === "activate" || kind === "atomic";
661
- finalFailureReason = `Validated execution at the exact current gas ceiling still failed: ${retryFailure.error ?? "Execution reverted"}${indivisible ? "; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned" : ""}`;
662
- }
663
- } catch (failure) {
664
- const kind = simulationFailureKind(failure);
665
- if (kind === "abort") throw failure;
666
- assertCurrent();
667
- if (kind !== "out-of-gas") {
668
- await assertLifecycleBlock(client, block, planned.chainId);
669
- return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, reason: `Legal-envelope validated replay unavailable: ${lifecycleFailureReason(failure)}` };
670
- }
671
- await assertLifecycleBlock(client, block, planned.chainId);
672
- return { ...base, backend, admitted: false, steps: measured, reason: `Validated buffered replay failed (${finalFailure?.error ?? "Execution reverted"}) and execution at the exact gas ceiling still reverted: ${lifecycleFailureReason(failure)}` };
673
- }
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)}` };
674
627
  }
675
628
  }
676
- assertCurrent();
677
- if (nextEnvelope !== undefined && finalFailure === undefined) {
678
- const currentMinimum = verifiedSteps[0]?.gasRequired ?? verifiedSteps[0]?.gasUsed ?? nextEnvelope.gas;
679
- if (nextEnvelope.gas < currentMinimum) {
680
- await assertLifecycleBlock(client, block, planned.chainId);
681
- return { ...base, backend, admitted: false, executionProof: "proved", protocolFit: "failed", steps: verifiedSteps,
682
- failureCategory: "capacity", capacityConstraint: "gas-envelope", failedTransactionId: transactions[0]?.id,
683
- reason: "Exact reviewed replay exceeds the held gas envelope" };
684
- }
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}` };
685
632
  }
633
+ assertCurrent();
686
634
  const estimates: LifecycleSimulationStep[] = [];
687
635
  // A definitive returned replay failure already refuses execution. Retain its
688
636
  // fee observations without inventing a balance or doing redundant payer work.
@@ -708,6 +656,6 @@ async function simulateLaunchTransactionsWithContext(
708
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" } });
709
657
  }
710
658
  await assertLifecycleBlock(client, block, planned.chainId);
711
- return { ...base, backend, executionProof: finalFailure === undefined ? "proved" : "failed", protocolFit: finalFailure === undefined ? "proved" : "unknown", admitted: finalFailure === undefined && affordabilityFailure === undefined, steps: estimates, reason: finalFailureReason ?? finalFailure?.error ?? affordabilityFailure, failedTransactionId: finalFailure?.transactionId, failureCategory: finalFailure?.failureCategory ?? (affordabilityFailure === undefined ? undefined : "affordability"), capacityConstraint: finalFailure?.failureCategory === "capacity" ? "compute" : undefined };
659
+ 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 };
712
660
  }
713
661
 
@@ -1,8 +1,45 @@
1
1
  import { encodeFunctionData } from "viem";
2
2
  import { launchLifecycleAbi } from "./abi.js";
3
- import { LifecycleFundingKind, LifecycleMode } from "./schema.js";
3
+ import { LifecycleFundingKind, LifecycleMode, type LaunchPlanV1 } from "./schema.js";
4
4
  import { LifecyclePlanningError, type BuildLaunchTransactionsOptions, type LifecycleTransaction } from "./types.js";
5
5
  import { assertLifecycleTokenAllocation } from "./allocations.js";
6
+ import { HMM_V2_PREPARE_GAS, hmmV2ForwardingReserve } from "./presets.js";
7
+ import { isHmmProfile } from "./hmm.js";
8
+
9
+ const OTHER_MARKET_PREPARE_GAS = 16_000_000n;
10
+ const BEGIN_GAS = 9_500_000n;
11
+ const ACTIVATE_MARKET_GAS = 6_000_000n;
12
+
13
+ export function preparationGasEstimate(plan: LaunchPlanV1, first: number, count: number): bigint {
14
+ let gas = 0n;
15
+ for (let index = first; index < first + count; index += 1) gas += isHmmProfile(plan.markets[index]!.profileId) ? HMM_V2_PREPARE_GAS : OTHER_MARKET_PREPARE_GAS;
16
+ return gas;
17
+ }
18
+
19
+ /** Contiguous cost packing preserves committed market order and recovery progress. */
20
+ export function packPreparationBatches(plan: LaunchPlanV1, prepared: number, computeCap: bigint): number[] {
21
+ const batches = prepared > 0 ? [prepared] : [];
22
+ let count = 0, gas = 0n;
23
+ for (let index = prepared; index < plan.markets.length; index += 1) {
24
+ const estimate = preparationGasEstimate(plan, index, 1);
25
+ if (count > 0 && gas + estimate > computeCap) { batches.push(count); count = 0; gas = 0n; }
26
+ count += 1;
27
+ gas += estimate;
28
+ }
29
+ if (count > 0) batches.push(count);
30
+ return batches;
31
+ }
32
+
33
+ /** Estimates select RPC proof coverage only; measurement and exact replay size gas. */
34
+ export function transactionGasEstimate(plan: LaunchPlanV1, transaction: LifecycleTransaction): bigint {
35
+ switch (transaction.kind) {
36
+ case "begin": return BEGIN_GAS;
37
+ case "prepare": return preparationGasEstimate(plan, transaction.marketStart!, transaction.marketCount!);
38
+ case "activate": return ACTIVATE_MARKET_GAS * BigInt(plan.markets.length) + hmmV2ForwardingReserve(plan);
39
+ case "atomic": return BEGIN_GAS + preparationGasEstimate(plan, 0, plan.markets.length) + ACTIVATE_MARKET_GAS * BigInt(plan.markets.length) + hmmV2ForwardingReserve(plan);
40
+ default: return 100_000n;
41
+ }
42
+ }
6
43
 
7
44
  /** Pure unsigned launch commands. Every command carries the complete commitment;
8
45
  * preparation splits only empty markets, never positions, buys or public opening.