@black-market/sdk 0.9.1 → 0.11.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 (72) hide show
  1. package/README.md +23 -17
  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/{hmm-dynamic-pool-hook-v1-20261011.d.ts → hmm-dynamic-pool-hook-v2-20261017.d.ts} +21 -21
  10. package/dist/lifecycle/deployments/{hmm-dynamic-pool-hook-v1-20261011.d.ts.map → hmm-dynamic-pool-hook-v2-20261017.d.ts.map} +1 -1
  11. package/dist/lifecycle/deployments/hmm-dynamic-pool-hook-v2-20261017.js +69 -0
  12. package/dist/lifecycle/deployments/{hmm-dynamic-pool-hook-v1-20261011.js.map → hmm-dynamic-pool-hook-v2-20261017.js.map} +1 -1
  13. package/dist/lifecycle/deployments/{hmm-fixed-pool-hook-v1-20261011.d.ts → hmm-fixed-pool-hook-v2-20261017.d.ts} +21 -21
  14. package/dist/lifecycle/deployments/{hmm-fixed-pool-hook-v1-20261011.d.ts.map → hmm-fixed-pool-hook-v2-20261017.d.ts.map} +1 -1
  15. package/dist/lifecycle/deployments/hmm-fixed-pool-hook-v2-20261017.js +69 -0
  16. package/dist/lifecycle/deployments/{hmm-fixed-pool-hook-v1-20261011.js.map → hmm-fixed-pool-hook-v2-20261017.js.map} +1 -1
  17. package/dist/lifecycle/hmm-abi.d.ts +4544 -0
  18. package/dist/lifecycle/hmm-abi.d.ts.map +1 -0
  19. package/dist/lifecycle/hmm-abi.js +299 -0
  20. package/dist/lifecycle/hmm-abi.js.map +1 -0
  21. package/dist/lifecycle/hmm.d.ts +19 -3
  22. package/dist/lifecycle/hmm.d.ts.map +1 -1
  23. package/dist/lifecycle/hmm.js +11 -7
  24. package/dist/lifecycle/hmm.js.map +1 -1
  25. package/dist/lifecycle/markets.d.ts.map +1 -1
  26. package/dist/lifecycle/markets.js +2 -0
  27. package/dist/lifecycle/markets.js.map +1 -1
  28. package/dist/lifecycle/planner.d.ts.map +1 -1
  29. package/dist/lifecycle/planner.js +44 -35
  30. package/dist/lifecycle/planner.js.map +1 -1
  31. package/dist/lifecycle/presets.d.ts +7 -2
  32. package/dist/lifecycle/presets.d.ts.map +1 -1
  33. package/dist/lifecycle/presets.js +19 -5
  34. package/dist/lifecycle/presets.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 +115 -174
  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 +6 -3
  46. package/dist/lifecycle/types.d.ts.map +1 -1
  47. package/dist/lifecycle/types.js.map +1 -1
  48. package/docs/launch-lifecycle.md +197 -86
  49. package/examples/.env.example +4 -0
  50. package/examples/README.md +27 -6
  51. package/examples/hmm-launch-config.ts +4 -1
  52. package/examples/launch-example-support.ts +1 -0
  53. package/examples/launch-example.ts +1 -0
  54. package/examples/launch.ts +1 -0
  55. package/package.json +2 -2
  56. package/src/lifecycle/abi.ts +1 -0
  57. package/src/lifecycle/calibration.ts +1 -0
  58. package/src/lifecycle/deployments/hmm-dynamic-pool-hook-v2-20261017.ts +71 -0
  59. package/src/lifecycle/deployments/hmm-fixed-pool-hook-v2-20261017.ts +71 -0
  60. package/src/lifecycle/hmm-abi.ts +302 -0
  61. package/src/lifecycle/hmm.ts +12 -7
  62. package/src/lifecycle/markets.ts +1 -0
  63. package/src/lifecycle/planner.ts +41 -32
  64. package/src/lifecycle/presets.ts +20 -6
  65. package/src/lifecycle/rpc.ts +3 -2
  66. package/src/lifecycle/simulation.ts +111 -165
  67. package/src/lifecycle/transactions.ts +38 -1
  68. package/src/lifecycle/types.ts +9 -4
  69. package/dist/lifecycle/deployments/hmm-dynamic-pool-hook-v1-20261011.js +0 -69
  70. package/dist/lifecycle/deployments/hmm-fixed-pool-hook-v1-20261011.js +0 -69
  71. package/src/lifecycle/deployments/hmm-dynamic-pool-hook-v1-20261011.ts +0 -71
  72. package/src/lifecycle/deployments/hmm-fixed-pool-hook-v1-20261011.ts +0 -71
@@ -10,7 +10,7 @@ import { prepareLaunchSimulation, simulateLaunchTransactions, type NextTransacti
10
10
  import { LifecyclePlanningError, type BuildNextTransactionOptions, type CanonicalLaunchProgress, type LifecycleBlock, type LifecycleConstructionProfile, type LifecycleFundingPrerequisite, type LifecyclePoolBoundHookDeployment, type LifecycleProfile, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleTransaction, type PlannedLaunch, type PlanLaunchOptions, type PrepareAndPlanLifecycleLaunchOptions, type SimulateLaunchPlanOptions } from "./types.js";
11
11
  import { deriveKnownPoolBoundHookDeployment, protectOrderedBuyMinimums, validateBuySlippageBps } from "./calibration.js";
12
12
  import { getKnownLifecycleDeployment, getKnownLifecycleProfile } from "./presets.js";
13
- import { buildLaunchTransactions } from "./transactions.js";
13
+ import { buildLaunchTransactions, packPreparationBatches } from "./transactions.js";
14
14
  import { assertLifecycleMarketAllocation, assertLifecycleSupplyAllocation } from "./allocations.js";
15
15
 
16
16
  type ValidatedPlanShape = {
@@ -132,11 +132,9 @@ async function readPlanMetadata(client: LifecycleRpcClient, plan: LaunchPlanV1,
132
132
  return { profiles, hookDeployments, reasons: live.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Unlisted lifecycle profile is unavailable") };
133
133
  }
134
134
 
135
- function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", progress: CanonicalLaunchProgress, approvals: readonly LifecycleTransaction[], batchSize: number): LifecycleTransaction[] {
135
+ function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", progress: CanonicalLaunchProgress, approvals: readonly LifecycleTransaction[], batches: readonly number[]): LifecycleTransaction[] {
136
136
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) return [];
137
137
  const prepared = progress.canonical.preparedMarkets;
138
- const batches: number[] = prepared > 0 ? [prepared] : [];
139
- for (let first = prepared; first < plan.markets.length; first += batchSize) batches.push(Math.min(batchSize, plan.markets.length - first));
140
138
  const commands = buildLaunchTransactions({ plan, mode, preparationBatches: mode === "staged" ? batches : undefined }).filter((transaction) =>
141
139
  (transaction.kind !== "begin" && transaction.kind !== "atomic" || progress.canonical.phase === LifecyclePhase.None) &&
142
140
  (transaction.kind !== "prepare" || (transaction.marketStart ?? 0) >= prepared));
@@ -147,7 +145,8 @@ function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", prog
147
145
  function attachEstimates(transactions: readonly LifecycleTransaction[], simulation: LifecycleSimulation): LifecycleTransaction[] {
148
146
  return transactions.map((transaction) => {
149
147
  const estimate = simulation.steps.find((step) => step.transactionId === transaction.id)?.estimate;
150
- return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, admission: { admitted: simulation.admitted, confidence: simulation.confidence, executionProof: simulation.executionProof, protocolFit: simulation.protocolFit, transportPreflight: simulation.transportPreflight, blockNumber: simulation.blockNumber, blockHash: simulation.blockHash, account: simulation.account, chainId: simulation.chainId, reason: simulation.reason, limits: simulation.limits } };
148
+ return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate,
149
+ admission: estimate === undefined ? undefined : { admitted: simulation.admitted, confidence: simulation.confidence, executionProof: simulation.executionProof, protocolFit: simulation.protocolFit, transportPreflight: simulation.transportPreflight, blockNumber: simulation.blockNumber, blockHash: simulation.blockHash, account: simulation.account, chainId: simulation.chainId, reason: simulation.reason, limits: simulation.limits } };
151
150
  });
152
151
  }
153
152
  type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value"> & NextTransactionEnvelope;
@@ -364,8 +363,8 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
364
363
  }),
365
364
  ]));
366
365
  assertPlanningActive(options.signal);
367
- const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [] };
368
- let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, preparationBatchSize: plan.markets.length, limits: options.limits,
366
+ const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [], deferredTransactionIds: [] };
367
+ let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, limits: options.limits,
369
368
  allowTopologyGaps: options.allowTopologyGaps === true, allowMissingPermanentTail: options.allowMissingPermanentTail === true };
370
369
  const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
371
370
  if (!progress.confirmationSafe || receiptsPending) {
@@ -393,48 +392,58 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
393
392
  ]));
394
393
  assertFinalPlan(planned, options.signal);
395
394
  planned = { ...planned, prerequisites: funding.prerequisites, profiles: metadata.profiles, hookDeployments: metadata.hookDeployments };
396
- let preparationBatchSize = plan.markets.length;
397
- let transactions = orderedTransactions(plan, mode, progress, funding.approvals, preparationBatchSize);
395
+ const prepared = progress.canonical.preparedMarkets;
396
+ const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
397
+ let batches: number[] = [];
398
+ if (mode === "staged") batches = reviewed === undefined ? packPreparationBatches(plan, prepared, computeCap)
399
+ : [...(prepared > 0 ? [prepared] : []), ...(prepared < plan.markets.length ? [plan.markets.length - prepared] : [])];
400
+ if (reviewed !== undefined && reviewed.kind === "prepare") {
401
+ // Canonical progress fixes the start; the exact held calldata fixes its span.
402
+ // Never rediscover a batch or enlarge a reviewed transaction.
403
+ const match = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
404
+ const start = Number(match?.[1]), count = Number(match?.[2]);
405
+ if (start !== prepared || !Number.isSafeInteger(count) || count <= 0 || start + count > plan.markets.length) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation differs from canonical unfinished progress");
406
+ batches = [...(prepared > 0 ? [prepared] : []), count, ...(start + count < plan.markets.length ? [plan.markets.length - start - count] : [])];
407
+ }
408
+ let transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches);
409
+ if (reviewed !== undefined) {
410
+ if (!matchesReviewedNextTransaction(transactions[0], reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
411
+ transactions = transactions.slice(0, 1);
412
+ }
398
413
  if (metadata.reasons.length > 0) {
399
414
  await assertLifecycleBlock(client, block, plan.chainId);
400
415
  return { ...planned, simulation: { ...placeholder, confidence: "stateful", reason: metadata.reasons.join("; "), failureCategory: "semantic" }, transactions, totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
401
416
  }
402
- const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block);
417
+ const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block, true, reviewed);
403
418
  try {
404
- const execute = diagnostic ? preparedSimulation.measure : (candidate: readonly LifecycleTransaction[]) => {
405
- if (reviewed === undefined) return preparedSimulation(candidate);
406
- if (!matchesReviewedNextTransaction(candidate[0], reviewed)) {
407
- // A fresh prepare batch may still require SDK-owned capacity discovery.
408
- // Never use held fees for a different step or caller-selected calldata.
409
- if (candidate[0]?.kind === "prepare" && reviewed.kind === "prepare") return preparedSimulation(candidate);
410
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
411
- }
412
- return preparedSimulation(candidate, reviewed);
413
- };
419
+ const execute = diagnostic ? preparedSimulation.measure : (candidate: readonly LifecycleTransaction[]) => preparedSimulation(candidate, reviewed);
414
420
  let simulation = await execute(transactions);
415
421
  assertFinalPlan(planned, options.signal);
416
422
  if (mode === "staged") {
417
- // Only explicit gas/compute evidence can partition empty preparations.
418
- // Byte caps cannot shrink: each prepare still carries the entire plan.
419
- while (!simulation.admitted && simulation.failureCategory === "capacity" &&
420
- (simulation.capacityConstraint === "compute" || simulation.capacityConstraint === "gas-envelope") &&
421
- !(reviewed !== undefined && simulation.capacityConstraint === "gas-envelope" && simulation.failedTransactionId === transactions[0]?.id && matchesReviewedNextTransaction(transactions[0], reviewed)) &&
422
- simulation.failedTransactionId?.startsWith("prepare:") && preparationBatchSize > 1) {
423
- const failedMarketCount = transactions.find((transaction) => transaction.id === simulation.failedTransactionId)?.marketCount ?? 0;
424
- if (failedMarketCount <= 1) break;
425
- preparationBatchSize = Math.ceil(failedMarketCount / 2);
426
- transactions = orderedTransactions(plan, mode, progress, funding.approvals, preparationBatchSize);
423
+ // Only a measurement that consumed almost its entire legal envelope
424
+ // invalidates packing estimates, including gas hidden by CREATE wrappers.
425
+ while (reviewed === undefined && !simulation.admitted && simulation.failedTransactionId?.startsWith("prepare:")) {
426
+ const failedIndex = transactions.findIndex((transaction) => transaction.id === simulation.failedTransactionId);
427
+ const failedTransaction = transactions[failedIndex];
428
+ const step = simulation.steps.find((candidate) => candidate.transactionId === simulation.failedTransactionId);
429
+ const count = failedTransaction?.marketCount ?? 0;
430
+ if (count <= 1 || step?.estimate !== undefined || step?.gasUsed === undefined || step.gasLimit === undefined || step.gasUsed * 100n < step.gasLimit * 95n) break;
431
+ let first = 0;
432
+ const batchIndex = batches.findIndex((batch) => { const matches = first === failedTransaction?.marketStart; first += batch; return matches; });
433
+ const left = Math.ceil(count / 2);
434
+ batches.splice(batchIndex, 1, left, count - left);
435
+ transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches);
427
436
  simulation = await execute(transactions);
428
437
  assertFinalPlan(planned, options.signal);
429
438
  }
430
439
  if (!simulation.admitted && simulation.failedTransactionId === "activate") simulation = { ...simulation, reason: `${simulation.reason ?? "Final activation failed"}; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned` };
431
- } else if (!diagnostic && !simulation.admitted) {
440
+ } else if (!diagnostic && !simulation.admitted && !(simulation.capacityConstraint === "compute" && simulation.confidence === "provisional")) {
432
441
  simulation = { ...simulation, reason: `${simulation.reason ?? "Exact atomic plan is not admitted"}; choose staged explicitly only if empty preparation can be partitioned and complete activation fits` };
433
442
  }
434
443
  if (reviewed !== undefined && !matchesReviewedNextTransaction(transactions[0], reviewed)) {
435
444
  throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
436
445
  }
437
- return { ...planned, simulation, preparationBatchSize, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
446
+ return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
438
447
  } finally { preparedSimulation.close(); }
439
448
  }
440
449
 
@@ -1,13 +1,13 @@
1
1
  import type { Address, Hex } from "viem";
2
2
  import type {
3
- AdapterRegistrationV1, LaunchEnvelopeV2, ProfileRegistrationV1, ProfileTopologyV1,
3
+ AdapterRegistrationV1, LaunchEnvelopeV2, LaunchPlanV1, ProfileRegistrationV1, ProfileTopologyV1,
4
4
  } from "./schema.js";
5
5
  import type { LifecycleProfile } from "./types.js";
6
6
  import { fixedFeePoolHookV1CreationCode } from "./deployments/fixed-fee-pool-hook-v1-20261008.js";
7
7
  import { dynamicFeePoolHookV1CreationCode } from "./deployments/dynamic-fee-pool-hook-v1-20261011.js";
8
- import { hmmFixedEnvelope, hmmFixedAdapter, hmmFixedRegistration, hmmFixedCreationCode } from "./deployments/hmm-fixed-pool-hook-v1-20261011.js";
9
- import { hmmDynamicEnvelope, hmmDynamicAdapter, hmmDynamicRegistration, hmmDynamicCreationCode } from "./deployments/hmm-dynamic-pool-hook-v1-20261011.js";
10
- import { HMM_FIXED_PROFILE_ID, HMM_DYNAMIC_PROFILE_ID } from "./hmm.js";
8
+ import { hmmFixedEnvelope, hmmFixedAdapter, hmmFixedRegistration, hmmFixedCreationCode } from "./deployments/hmm-fixed-pool-hook-v2-20261017.js";
9
+ import { hmmDynamicEnvelope, hmmDynamicAdapter, hmmDynamicRegistration, hmmDynamicCreationCode } from "./deployments/hmm-dynamic-pool-hook-v2-20261017.js";
10
+ import { HMM_FIXED_PROFILE_ID, HMM_DYNAMIC_PROFILE_ID, isHmmProfile } from "./hmm.js";
11
11
 
12
12
  export enum KnownLifecycleProfile {
13
13
  Abyss0 = "abyss-0",
@@ -23,8 +23,8 @@ export enum KnownLifecycleProfile {
23
23
  export enum KnownLifecycleHook {
24
24
  FixedFeePoolV1 = "fixed-fee-pool-v1",
25
25
  DynamicFeePoolV1 = "dynamic-fee-pool-v1",
26
- HmmFixedPoolV1 = "hmm-fixed-pool-v1",
27
- HmmDynamicPoolV1 = "hmm-dynamic-pool-v1",
26
+ HmmFixedPoolV2 = "hmm-fixed-pool-v2",
27
+ HmmDynamicPoolV2 = "hmm-dynamic-pool-v2",
28
28
  }
29
29
 
30
30
  Object.freeze(KnownLifecycleProfile);
@@ -280,6 +280,20 @@ const dynamicFeePoolPreset: LifecycleProfilePreset = Object.freeze({
280
280
  }),
281
281
  });
282
282
 
283
+ // HMMLiquidityHookV2.sol:98-119 requires ~3.06M available before manager calls,
284
+ // including opening and swaps. Robinhood fork measurements: activation minima
285
+ // 9.60M/15.40M/21.11M for 1/2/3 markets despite used gas 5.85M/11.13M/16.54M.
286
+ // Apply this forwarding target once per activate/atomic envelope, not per callback.
287
+ export const HMM_V2_FORWARDING_BASE_GAS = 4_250_000n;
288
+ export const HMM_V2_FORWARDING_EXTRA_MARKET_GAS = 500_000n;
289
+ export const HMM_V2_PREPARE_GAS = 14_500_000n;
290
+
291
+ export function hmmV2ForwardingReserve(plan: LaunchPlanV1): bigint {
292
+ let count = 0;
293
+ for (const market of plan.markets) if (isHmmProfile(market.profileId)) count += 1;
294
+ return count === 0 ? 0n : HMM_V2_FORWARDING_BASE_GAS + HMM_V2_FORWARDING_EXTRA_MARKET_GAS * BigInt(count - 1);
295
+ }
296
+
283
297
  function hmmPreset(key: KnownLifecycleProfile, id: Hex, envelope: LaunchEnvelopeV2,
284
298
  adapter: AdapterRegistrationV1, registration: ProfileRegistrationV1, creationCode: Hex): LifecycleProfilePreset {
285
299
  return Object.freeze({
@@ -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,8 @@ 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";
7
9
 
8
10
  function committedPositionCount(plan: LaunchPlanV1): number {
9
11
  let count = 0;
@@ -111,8 +113,6 @@ async function proveNitroBackend(client: LifecycleRpcClient, planned: PlannedLau
111
113
  }
112
114
 
113
115
  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
116
  // Natural child-block fees must govern fee-bearing admission; overriding
117
117
  // baseFeePerGas with the parent's would fabricate a lower envelope and admit
118
118
  // calls that the actual chain would reject. eth_simulateV1 derives the child
@@ -315,7 +315,7 @@ function observedFields<T extends object>(value: T): () => boolean {
315
315
 
316
316
  /** Internal continuation owned by one SDK read invocation. Only its positive
317
317
  * capability proof is reused; every candidate executes its entire economic sequence. */
318
- export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true): PreparedLaunchSimulation {
318
+ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true, reviewedEnvelope?: NextTransactionEnvelope): PreparedLaunchSimulation {
319
319
  const { client, planned, fork } = options;
320
320
  const signal = options.signal;
321
321
  if (signal?.aborted) throw Object.assign(new Error("Lifecycle simulation cancelled"), { name: "AbortError" });
@@ -353,7 +353,7 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
353
353
  resolveLifecycleLimits(context, policy),
354
354
  readLifecycleChainId(client),
355
355
  lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block),
356
- hasTransactions ? lifecycleRpc(client, "eth_gasPrice") : undefined,
356
+ hasTransactions && reviewedEnvelope === undefined ? lifecycleRpc(client, "eth_gasPrice") : undefined,
357
357
  ]));
358
358
  })().then((observations) => ({ observations }), (failure: unknown) => ({ failure }));
359
359
  // Refused inputs may discard this read-only preparation before it settles.
@@ -439,7 +439,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
439
439
  // Standalone calls always produce fresh context, including live chain and fees.
440
440
  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
441
  const block = pinnedBlock ?? await readLifecycleBlock(options.client);
442
- const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0);
442
+ const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope);
443
443
  try { return await prepared(transactions, nextEnvelope); }
444
444
  finally { prepared.close(); }
445
445
  }
@@ -458,7 +458,7 @@ async function simulateLaunchTransactionsWithContext(
458
458
  const sourceClient = lifecycleSourceClient(client);
459
459
  const { block } = context;
460
460
  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 };
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, deferredTransactionIds: [] as string[] };
462
462
  if (chainId !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
463
463
  const accountCode = rpcHex(accountCodeValue, "account code");
464
464
  if (accountCode !== "0x") {
@@ -473,36 +473,48 @@ async function simulateLaunchTransactionsWithContext(
473
473
  }
474
474
  }
475
475
  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.
476
+ if (nextEnvelope !== undefined && (transactions.length !== 1 || typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas <= 0n || nextEnvelope.gas >= 1n << 64n ||
477
+ typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
478
+ throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Reviewed replay requires one immediate transaction with exact uint64 gas and uint256 price");
479
+ }
480
+ const allTransactions = transactions;
481
+ if (limits.maxSimulationGas !== undefined && nextEnvelope === undefined) {
482
+ let estimatedGas = 0n, count = 0;
483
+ for (const transaction of transactions) {
484
+ estimatedGas += transactionGasEstimate(planned.plan, transaction);
485
+ if (estimatedGas > limits.maxSimulationGas) break;
486
+ count += 1;
487
+ }
488
+ transactions = transactions.slice(0, count);
489
+ base.deferredTransactionIds = allTransactions.slice(count).map((transaction) => transaction.id);
490
+ if (count === 0) {
491
+ const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
492
+ const exceedsCompute = estimatedGas > computeCap;
493
+ const gasInMillions = (gas: bigint) => {
494
+ const tenths = (gas + 50_000n) / 100_000n;
495
+ return `${tenths / 10n}.${tenths % 10n}`;
496
+ };
497
+ const atomic = allTransactions[0]!.kind === "atomic";
498
+ const reason = exceedsCompute
499
+ ? `${atomic ? "Atomic launch" : "Immediate transaction"} needs about ${gasInMillions(estimatedGas)}M gas, above the ${gasInMillions(computeCap)}M per-transaction limit${atomic ? "; use staged" : ""}`
500
+ : "The immediate transaction estimate exceeds the RPC aggregate simulation gas cap; no prefix can be proved";
501
+ await assertLifecycleBlock(client, block, planned.chainId);
502
+ return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: exceedsCompute ? "compute" : "simulation-gas", reason };
503
+ }
504
+ }
505
+ const recommendedGasPrice = nextEnvelope === undefined ? rpcQuantity(gasPriceValue, "gas price") : nextEnvelope.gasPrice;
506
+ // New recommendations cover lagging quotes; reviewed fees are replayed unchanged
507
+ // under natural child fees, never refreshed or silently increased.
481
508
  const baseFeeEnvelope = (block.baseFeePerGas ?? 0n) * 2n;
482
509
  const gasPrice = nextEnvelope?.gasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
483
510
  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
511
  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
512
  const posterGas: bigint[] = [];
499
513
  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
514
  try {
503
515
  const [, estimates] = await Promise.all([
504
516
  proveBackend(limits, gasPrice),
505
- lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
517
+ nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))) : [],
506
518
  ]);
507
519
  posterGas.push(...estimates);
508
520
  } catch (failure) {
@@ -513,176 +525,110 @@ async function simulateLaunchTransactionsWithContext(
513
525
  }
514
526
  }
515
527
  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
528
  const discoveryGas = transactions.map((_, index) => {
519
529
  const total = limits.executionGasCeiling + (posterGas[index] ?? 0n);
520
530
  const capped = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < total ? limits.transactionGasCeiling : total;
521
531
  if (capped <= 0n || capped >= 1n << 64n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Compute/poster envelope exceeds uint64 gas");
522
532
  return capped;
523
533
  });
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;
534
+ let backend: "eth_simulateV1" | "controlled-fork" = nextEnvelope !== undefined && limits.protocol === "evm" && options.fork !== undefined ? "controlled-fork" : "eth_simulateV1";
535
+ let measured: LifecycleSimulationStep[] = [];
536
+ if (nextEnvelope === undefined) {
537
+ try {
538
+ measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, discoveryGas, limits, gasPrice, false));
539
+ } catch (failure) {
540
+ if (simulationFailureKind(failure) === "abort") throw failure;
541
+ assertCurrent();
542
+ if (options.fork === undefined || limits.protocol === "nitro") {
543
+ await assertLifecycleBlock(client, block, planned.chainId);
544
+ 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."}` };
545
+ }
546
+ backend = "controlled-fork";
547
+ measured = await simulateForkPass(options.fork, planned, block, transactions, discoveryGas, gasPrice, true, limits.headroomBps);
548
+ }
533
549
  assertCurrent();
534
- if (options.fork === undefined || limits.protocol === "nitro") {
550
+ const failedIndex = measured.findIndex((step) => !step.success);
551
+ if (failedIndex >= 0 && limits.maxSimulationGas !== undefined &&
552
+ measured.reduce((gas, step) => gas + (step.gasUsed ?? 0n), 0n) * 100n >= limits.maxSimulationGas * 95n) {
553
+ // The provider exhausted its aggregate used-gas budget, not the contract.
554
+ // Preserve only actual successful results; the failed call is deferred too.
555
+ measured = measured.slice(0, failedIndex);
556
+ transactions = transactions.slice(0, failedIndex);
557
+ base.deferredTransactionIds = allTransactions.slice(failedIndex).map((transaction) => transaction.id);
558
+ if (transactions.length === 0) {
559
+ await assertLifecycleBlock(client, block, planned.chainId);
560
+ return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "source", reason: "RPC aggregate simulation gas cap left no successful prefix to prove" };
561
+ }
562
+ }
563
+ const failed = measured.find((step) => !step.success);
564
+ 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 }; }
565
+ if (diagnostic) {
535
566
  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."}` };
567
+ return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
537
568
  }
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
569
  }
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);
570
+ const gasLimits = nextEnvelope === undefined ? measured.map((step, index) => {
571
+ const gross = step.gasRequired ?? step.gasUsed!;
572
+ const reserve = transactions[index]?.kind === "activate" || transactions[index]?.kind === "atomic" ? hmmV2ForwardingReserve(planned.plan) : 0n;
573
+ const desired = gross + reserve > buffer(gross) ? gross + reserve : buffer(gross);
574
+ const available = discoveryGas[index]! - (posterGas[index] ?? 0n);
575
+ return (desired < available ? desired : available) + (posterGas[index] ?? 0n);
576
+ }) : [nextEnvelope.gas];
577
+ const exceeds = gasLimits.findIndex((gas, index) =>
578
+ gas <= 0n || gas >= 1n << 64n || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling ||
579
+ nextEnvelope === undefined && (measured[index]!.gasRequired ?? measured[index]!.gasUsed!) > gas - (posterGas[index] ?? 0n) ||
580
+ limits.protocol === "evm" && gas > limits.executionGasCeiling);
557
581
  if (exceeds >= 0) {
558
582
  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 };
583
+ 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
584
  }
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();
585
+ let verifiedSteps: LifecycleSimulationStep[] = [];
586
+ let admittedGasLimits = gasLimits;
587
+ let replayError: string | undefined;
588
+ const replay = (gas: readonly bigint[]) => backend === "eth_simulateV1"
589
+ ? simulateRpcPass(client, planned, block, transactions, gas, limits, gasPrice, true)
590
+ : simulateForkPass(options.fork!, planned, block, transactions, gas, gasPrice, false, limits.headroomBps);
570
591
  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
- }
592
+ verifiedSteps = await lifecycleStage(client, "simulation.replay", () => replay(gasLimits));
576
593
  } catch (failure) {
577
594
  const kind = simulationFailureKind(failure);
578
595
  if (kind === "abort") throw failure;
579
596
  assertCurrent();
580
- const message = lifecycleFailureReason(failure);
581
- if (backend !== "eth_simulateV1") {
597
+ if (kind !== "out-of-gas" || backend !== "eth_simulateV1" || nextEnvelope !== undefined) {
582
598
  await assertLifecycleBlock(client, block, planned.chainId);
583
- return { ...base, backend, admitted: false, steps: [], reason: `Exact-gas validated replay failed: ${message}` };
599
+ 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
600
  }
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 = [];
601
+ replayError = lifecycleFailureReason(failure);
591
602
  }
592
603
  assertCurrent();
593
- let verifiedSteps = verified;
594
- let admittedGasLimits = gasLimits;
595
604
  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.
605
+ const failedIndex = verifiedSteps.findIndex((step) => !step.success);
606
+ const rescue = nextEnvelope === undefined && backend === "eth_simulateV1" &&
607
+ (replayError !== undefined && discoveryGas.some((gas, index) => index < transactions.length && gas > gasLimits[index]!) ||
608
+ finalFailure !== undefined && looksGasExhausted(finalFailure, gasLimits[failedIndex]!) && discoveryGas[failedIndex]! > gasLimits[failedIndex]!);
609
+ // One legal-envelope rescue covers thrown/returned genuine OOG. Never retry
610
+ // semantic errors, switch replay backends, or enlarge a held immediate envelope.
611
+ if (rescue) {
606
612
  assertCurrent();
607
613
  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
- }
614
+ admittedGasLimits = discoveryGas.slice(0, transactions.length);
615
+ verifiedSteps = await lifecycleStage(client, "simulation.envelope", () => replay(admittedGasLimits));
616
+ finalFailure = verifiedSteps.find((step) => !step.success);
617
+ replayError = undefined;
627
618
  } catch (failure) {
628
619
  const kind = simulationFailureKind(failure);
629
620
  if (kind === "abort") throw failure;
630
621
  assertCurrent();
631
622
  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
- }
623
+ 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)}` };
624
+ return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${lifecycleFailureReason(failure)}` };
674
625
  }
675
626
  }
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
- }
627
+ if (replayError !== undefined) {
628
+ await assertLifecycleBlock(client, block, planned.chainId);
629
+ return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling failed: ${replayError}` };
685
630
  }
631
+ assertCurrent();
686
632
  const estimates: LifecycleSimulationStep[] = [];
687
633
  // A definitive returned replay failure already refuses execution. Retain its
688
634
  // fee observations without inventing a balance or doing redundant payer work.
@@ -708,6 +654,6 @@ async function simulateLaunchTransactionsWithContext(
708
654
  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
655
  }
710
656
  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 };
657
+ 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
658
  }
713
659