@black-market/sdk 0.6.4 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +28 -4
  2. package/dist/addresses.d.ts +1 -1
  3. package/dist/addresses.js +4 -4
  4. package/dist/launch.d.ts +61 -0
  5. package/dist/launch.d.ts.map +1 -1
  6. package/dist/launch.js +171 -6
  7. package/dist/launch.js.map +1 -1
  8. package/dist/lifecycle/abi.d.ts +1999 -253
  9. package/dist/lifecycle/abi.d.ts.map +1 -1
  10. package/dist/lifecycle/abi.js +43 -14
  11. package/dist/lifecycle/abi.js.map +1 -1
  12. package/dist/lifecycle/allocations.d.ts +11 -0
  13. package/dist/lifecycle/allocations.d.ts.map +1 -0
  14. package/dist/lifecycle/allocations.js +36 -0
  15. package/dist/lifecycle/allocations.js.map +1 -0
  16. package/dist/lifecycle/calibration.d.ts +19 -0
  17. package/dist/lifecycle/calibration.d.ts.map +1 -0
  18. package/dist/lifecycle/calibration.js +65 -0
  19. package/dist/lifecycle/calibration.js.map +1 -0
  20. package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.d.ts +11 -0
  21. package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.d.ts.map +1 -0
  22. package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.js +10 -0
  23. package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.js.map +1 -0
  24. package/dist/lifecycle/index.d.ts +4 -2
  25. package/dist/lifecycle/index.d.ts.map +1 -1
  26. package/dist/lifecycle/index.js +4 -2
  27. package/dist/lifecycle/index.js.map +1 -1
  28. package/dist/lifecycle/markets.d.ts +170 -6
  29. package/dist/lifecycle/markets.d.ts.map +1 -1
  30. package/dist/lifecycle/markets.js +164 -37
  31. package/dist/lifecycle/markets.js.map +1 -1
  32. package/dist/lifecycle/planner.d.ts +7 -14
  33. package/dist/lifecycle/planner.d.ts.map +1 -1
  34. package/dist/lifecycle/planner.js +354 -400
  35. package/dist/lifecycle/planner.js.map +1 -1
  36. package/dist/lifecycle/presets.d.ts +62 -0
  37. package/dist/lifecycle/presets.d.ts.map +1 -0
  38. package/dist/lifecycle/presets.js +195 -0
  39. package/dist/lifecycle/presets.js.map +1 -0
  40. package/dist/lifecycle/progress.d.ts +5 -1
  41. package/dist/lifecycle/progress.d.ts.map +1 -1
  42. package/dist/lifecycle/progress.js +212 -53
  43. package/dist/lifecycle/progress.js.map +1 -1
  44. package/dist/lifecycle/rpc.d.ts +13 -2
  45. package/dist/lifecycle/rpc.d.ts.map +1 -1
  46. package/dist/lifecycle/rpc.js +92 -8
  47. package/dist/lifecycle/rpc.js.map +1 -1
  48. package/dist/lifecycle/simulation.d.ts +14 -4
  49. package/dist/lifecycle/simulation.d.ts.map +1 -1
  50. package/dist/lifecycle/simulation.js +381 -118
  51. package/dist/lifecycle/simulation.js.map +1 -1
  52. package/dist/lifecycle/transactions.d.ts +6 -0
  53. package/dist/lifecycle/transactions.d.ts.map +1 -0
  54. package/dist/lifecycle/transactions.js +40 -0
  55. package/dist/lifecycle/transactions.js.map +1 -0
  56. package/dist/lifecycle/types.d.ts +39 -2
  57. package/dist/lifecycle/types.d.ts.map +1 -1
  58. package/dist/lifecycle/types.js.map +1 -1
  59. package/docs/launch-lifecycle.md +228 -54
  60. package/examples/README.md +108 -18
  61. package/examples/generate-lifecycle-commitment-fixture.mjs +3 -1
  62. package/examples/launch-example-support.mjs +1 -1
  63. package/examples/launch-example.mjs +73 -26
  64. package/examples/launch-lifecycle-smoke.mjs +13 -11
  65. package/examples/launch.mjs +12 -11
  66. package/examples/quickstart.mjs +1 -1
  67. package/package.json +1 -1
  68. package/src/addresses.ts +4 -4
  69. package/src/launch.ts +213 -6
  70. package/src/lifecycle/abi.ts +43 -14
  71. package/src/lifecycle/allocations.ts +35 -0
  72. package/src/lifecycle/calibration.ts +59 -0
  73. package/src/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.ts +11 -0
  74. package/src/lifecycle/index.ts +4 -2
  75. package/src/lifecycle/markets.ts +169 -35
  76. package/src/lifecycle/planner.ts +326 -330
  77. package/src/lifecycle/presets.ts +243 -0
  78. package/src/lifecycle/progress.ts +193 -50
  79. package/src/lifecycle/rpc.ts +97 -9
  80. package/src/lifecycle/simulation.ts +369 -111
  81. package/src/lifecycle/transactions.ts +34 -0
  82. package/src/lifecycle/types.ts +35 -3
@@ -1,8 +1,64 @@
1
- import { decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctionResult, toHex } from "viem";
2
- import { launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
3
- import { LifecyclePhase } from "./schema.js";
4
- import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readNitroPosterGas, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity } from "./rpc.js";
1
+ import { decodeErrorResult, decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctionResult, parseAbi, toHex } from "viem";
2
+ import { fixedFeePoolHookV1Abi, launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
3
+ import { hashLaunchIdentity, hashLaunchPlan, LifecyclePhase } from "./schema.js";
4
+ import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readLifecycleChainId, readNitroPosterGas, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity } from "./rpc.js";
5
5
  import { LifecyclePlanningError } from "./types.js";
6
+ import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion } from "./markets.js";
7
+ function committedPositionCount(plan) {
8
+ let count = 0;
9
+ for (const market of plan.markets) {
10
+ if (market.configVersion === 1)
11
+ count += decodeAbyssLifecycleMarketConfig(market.config).positions.length;
12
+ else if (market.configVersion === 4)
13
+ count += decodeV4LifecycleMarketConfig(market.config).positions.length;
14
+ else if (isPoolBoundV4ConfigVersion(market.configVersion))
15
+ count += decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion).positions.length;
16
+ else
17
+ throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", "Activation requires a supported committed position schema");
18
+ }
19
+ return count;
20
+ }
21
+ const executionGuardErrors = [
22
+ ...fixedFeePoolHookV1Abi.filter((item) => item.type === "error"),
23
+ ...parseAbi([
24
+ "error WrongDomain()", "error PlanMismatch()", "error LaunchAlreadyExists()", "error InvalidMode()",
25
+ "error InvalidPhase()", "error InvalidPreparationOrder()", "error DeadlineExpired()", "error InvalidBinding()",
26
+ "error InvalidMarket()", "error InsufficientEscrow()", "error InvalidPositions()", "error InvalidPlan()",
27
+ "error InvalidFunding()", "error InvalidFeePolicy()", "error InvalidBuy()", "error DuplicateMarket()",
28
+ "error IneligibleImplementation()", "error QuoteDebtForbidden()", "error SlippageExceeded()", "error FeeBelowMinimum()",
29
+ "error DeploymentFailed()", "error Error(string reason)", "error Panic(uint256 code)",
30
+ ]),
31
+ ];
32
+ function executionFailure(returnData, error) {
33
+ const native = error !== null && typeof error === "object" && !Array.isArray(error) ? error : undefined;
34
+ let nativeData = native?.data;
35
+ for (let depth = 0; depth < 3 && nativeData !== null && typeof nativeData === "object" && !Array.isArray(nativeData); depth += 1)
36
+ nativeData = nativeData.data;
37
+ const nativeErrorDataBytes = typeof nativeData === "string" && /^0x(?:[0-9a-fA-F]{2})*$/.test(nativeData) ? (nativeData.length - 2) / 2 : undefined;
38
+ const nativeErrorCode = typeof native?.code === "number" && Number.isSafeInteger(native.code) ? native.code : undefined;
39
+ const message = typeof native?.message === "string" ? native.message : "";
40
+ const nativeGasCapped = message.endsWith(" (gas limit was capped by the RPC server's global gas cap)");
41
+ // Native eth_simulateV1 uses -32015 for VM errors; -32000 is also used by RPC clients.
42
+ // Empty data and an explicit OOG report are required. Malformed data or proximity is not proof.
43
+ const outOfGas = (returnData === undefined || returnData === "0x") && (native?.data === undefined || nativeErrorDataBytes === 0) &&
44
+ (nativeErrorCode === -32015 || nativeErrorCode === -32000) && /^out of gas\b/i.test(message);
45
+ const nativeErrorKind = native === undefined ? "missing" : outOfGas ? "out-of-gas" : nativeErrorCode === 3 ? "execution-reverted" : nativeErrorCode === -32015 ? "vm-error" : "other";
46
+ const diagnostic = { nativeErrorCode, nativeErrorKind, nativeErrorDataBytes, nativeGasCapped };
47
+ const data = returnData === undefined || returnData === "0x" ? nativeData : returnData;
48
+ if (typeof data === "string" && /^0x(?:[0-9a-fA-F]{2}){4,}$/.test(data)) {
49
+ try {
50
+ const { errorName } = decodeErrorResult({ abi: executionGuardErrors, data: data });
51
+ return { ...diagnostic, failureCategory: errorName === "DeploymentFailed" ? "opaque" : "semantic", decodedError: errorName, error: `Execution reverted (${errorName})` };
52
+ }
53
+ catch {
54
+ return { ...diagnostic, failureCategory: "opaque", error: "Execution reverted without a reviewed error" };
55
+ }
56
+ }
57
+ if (outOfGas)
58
+ return { ...diagnostic, failureCategory: nativeGasCapped ? "source" : "capacity", error: nativeGasCapped ? "Native execution ran out of gas under the reported RPC gas cap" : "Native execution ran out of gas" };
59
+ return { ...diagnostic, failureCategory: "opaque", error: nativeErrorKind === "execution-reverted" ? "Native execution reverted without a reviewed error" : "Execution failed without a reviewed error" };
60
+ }
61
+ const nitroProofsByInvocation = new WeakMap();
6
62
  function conditionRequest(condition, planned) {
7
63
  if (condition.kind === "allowance")
8
64
  return {
@@ -93,9 +149,8 @@ async function simulateRpcPass(client, planned, block, transactions, gasLimits,
93
149
  throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Simulated gas use is outside the exact requested envelope");
94
150
  const returnData = rpcHex(call.returnData, "simulated return data");
95
151
  const success = rpcQuantity(call.status, "simulated status") === 1n;
96
- let error;
97
- if (!success)
98
- error = call.error === undefined ? `Execution reverted: ${returnData}` : JSON.stringify(call.error);
152
+ const failureDiagnostic = success ? undefined : executionFailure(returnData, call.error);
153
+ let error = failureDiagnostic?.error;
99
154
  if (success) {
100
155
  try {
101
156
  // Verification reads must not become synthetic transactions that consume creator nonce/fees.
@@ -120,8 +175,9 @@ async function simulateRpcPass(client, planned, block, transactions, gasLimits,
120
175
  else if (transaction.kind === "atomic" || transaction.kind === "activate") {
121
176
  const functionName = transaction.kind === "atomic" ? "launchAtomic" : "activateLaunch";
122
177
  const receipt = decodeFunctionResult({ abi: launchLifecycleAbi, functionName, data: returnData });
123
- if (receipt.planHash.toLowerCase() !== planned.planHash.toLowerCase() || receipt.launchId.toLowerCase() !== planned.launchId.toLowerCase() || receipt.token.toLowerCase() !== planned.predictedToken.toLowerCase() || receipt.marketCount !== planned.plan.markets.length || receipt.quoteSpent.length !== planned.plan.buys.length || receipt.tokenOut.length !== planned.plan.buys.length || !events.some((event) => event.eventName === "LaunchActivated" && event.args.planHash.toLowerCase() === planned.planHash.toLowerCase() && event.args.token.toLowerCase() === planned.predictedToken.toLowerCase()))
124
- throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not atomically complete the committed launch");
178
+ const positionCount = committedPositionCount(planned.plan);
179
+ if (receipt.planHash.toLowerCase() !== planned.planHash.toLowerCase() || receipt.launchId.toLowerCase() !== planned.launchId.toLowerCase() || receipt.token.toLowerCase() !== planned.predictedToken.toLowerCase() || receipt.marketCount !== planned.plan.markets.length || receipt.positionCount !== positionCount || receipt.quoteSpent.length !== planned.plan.buys.length || receipt.tokenOut.length !== planned.plan.buys.length || !events.some((event) => event.eventName === "LaunchActivated" && event.args.launchId.toLowerCase() === planned.launchId.toLowerCase() && event.args.planHash.toLowerCase() === planned.planHash.toLowerCase() && event.args.token.toLowerCase() === planned.predictedToken.toLowerCase() && event.args.marketCount === planned.plan.markets.length && event.args.positionCount === positionCount))
180
+ throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not atomically complete every committed market, position and ordered buy");
125
181
  for (let buyIndex = 0; buyIndex < planned.plan.buys.length; buyIndex += 1) {
126
182
  const buy = planned.plan.buys[buyIndex];
127
183
  if (buy === undefined || (receipt.quoteSpent[buyIndex] ?? buy.quoteAmountIn + 1n) > buy.quoteAmountIn || (receipt.tokenOut[buyIndex] ?? 0n) < buy.minTokenOut)
@@ -134,21 +190,38 @@ async function simulateRpcPass(client, planned, block, transactions, gasLimits,
134
190
  throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Funding approval returned failure");
135
191
  }
136
192
  catch (failure) {
137
- error = failure instanceof Error ? failure.message : String(failure);
193
+ error = failure instanceof LifecyclePlanningError ? failure.message : "Simulation returned an invalid committed postcondition";
138
194
  }
139
195
  }
140
- steps.push({ transactionId: transaction.id, success: success && error === undefined, gasUsed, gasRequired, gasLimit: gasLimits[index], returnData, error });
196
+ steps.push({ transactionId: transaction.id, success: success && error === undefined, gasUsed, gasRequired, gasLimit: gasLimits[index], returnData, error, ...failureDiagnostic, ...(success && error !== undefined ? { failureCategory: "postcondition" } : {}) });
141
197
  if (!success || error !== undefined)
142
198
  break;
143
199
  }
144
200
  return steps;
145
201
  }
146
- /** A failure that consumed nearly the whole limit is treated as possible gas-envelope
147
- * exhaustion (EIP-150 retention starves forwarded sub-calls; the surfaced revert bytes
148
- * may be a wrapped inner bubble rather than empty). Discovery is safe: a genuine
149
- * business revert also fails at the exact ceiling, so it can never be masked. */
202
+ /** A decoded semantic refusal or opaque deployment wrapper is never gas evidence. */
150
203
  function looksGasExhausted(step, gasLimit) {
151
- return !step.success && step.gasUsed !== undefined && step.gasUsed >= gasLimit - gasLimit / 64n;
204
+ return !step.success && step.failureCategory === "capacity" && step.gasUsed !== undefined && step.gasUsed >= gasLimit - gasLimit / 64n;
205
+ }
206
+ function simulationFailureKind(failure) {
207
+ for (let depth = 0; depth < 8 && failure !== null && typeof failure === "object"; depth += 1) {
208
+ const error = failure;
209
+ if (error.name === "AbortError")
210
+ return "abort";
211
+ const status = error.status ?? error.statusCode;
212
+ if (typeof status === "number" && status >= 400 && status <= 599)
213
+ return "source";
214
+ const message = typeof error.details === "string" ? error.details : typeof error.message === "string" ? error.message : "";
215
+ const execution = executionFailure(undefined, { ...error, message });
216
+ if (execution.nativeErrorKind === "out-of-gas" && execution.nativeGasCapped)
217
+ return "source";
218
+ if (execution.failureCategory === "capacity")
219
+ return "out-of-gas";
220
+ if (error.code === 3 || error.code === -32000 && /^execution (?:failed|reverted)\b/i.test(message))
221
+ return "execution";
222
+ failure = error.cause;
223
+ }
224
+ return "source";
152
225
  }
153
226
  async function waitForkReceipt(client, transactionHash, timeoutMs) {
154
227
  const deadline = Date.now() + timeoutMs;
@@ -232,7 +305,7 @@ async function simulateForkPass(fork, planned, block, transactions, gasLimits, g
232
305
  const activated = events.filter((event) => event.eventName === "LaunchActivated");
233
306
  const buys = events.filter((event) => event.eventName === "InitialBuyExecuted");
234
307
  if (activated.length !== 1 || !activated.some((event) => event.eventName === "LaunchActivated" && event.args.launchId.toLowerCase() === planned.launchId.toLowerCase() && event.args.planHash.toLowerCase() === planned.planHash.toLowerCase() && event.args.token.toLowerCase() === planned.predictedToken.toLowerCase()) ||
235
- progress.phase !== LifecyclePhase.Active || progress.planHash.toLowerCase() !== planned.planHash.toLowerCase() || progress.token.toLowerCase() !== planned.predictedToken.toLowerCase() ||
308
+ progress.phase !== LifecyclePhase.Active || progress.planHash.toLowerCase() !== planned.planHash.toLowerCase() || progress.token.toLowerCase() !== planned.predictedToken.toLowerCase() || progress.marketCount !== planned.plan.markets.length || progress.positionCount !== committedPositionCount(planned.plan) ||
236
309
  buys.length !== planned.plan.buys.length)
237
310
  throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Actual fork activation differs from the exact committed launch");
238
311
  const quoteSpent = [];
@@ -261,7 +334,7 @@ async function simulateForkPass(fork, planned, block, transactions, gasLimits, g
261
334
  steps.push({ transactionId: transaction.id, success: true, gasUsed, gasLimit: gas, gasRequired, returnData });
262
335
  }
263
336
  catch (failure) {
264
- steps.push({ transactionId: transaction.id, success: false, gasLimit: gas, error: failure instanceof Error ? failure.message : String(failure) });
337
+ steps.push({ transactionId: transaction.id, success: false, gasLimit: gas, error: lifecycleFailureReason(failure) });
265
338
  break;
266
339
  }
267
340
  }
@@ -278,98 +351,223 @@ async function simulateForkPass(fork, planned, block, transactions, gasLimits, g
278
351
  }
279
352
  }
280
353
  }
281
- /** Internal one-shot continuation. Only SDK reads can produce its context; callers
282
- * cannot supply resolved policy or account/chain observations as authority. */
354
+ function observedFields(value) {
355
+ const keys = Object.keys(value);
356
+ const values = keys.map((key) => value[key]);
357
+ return () => keys.length === Object.keys(value).length && keys.every((key, index) => value[key] === values[index]);
358
+ }
359
+ /** Internal continuation owned by one SDK read invocation. Only its positive
360
+ * capability proof is reused; every candidate executes its entire economic sequence. */
283
361
  export function prepareLaunchSimulation(options, block, hasTransactions = true) {
284
- const { client, planned } = options;
285
- const sourceClient = lifecycleSourceClient(client);
286
- const blockNumber = block.number, blockHash = block.hash;
287
- const context = { client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId };
362
+ const { client, planned, fork } = options;
363
+ const signal = options.signal;
364
+ if (signal?.aborted)
365
+ throw Object.assign(new Error("Lifecycle simulation cancelled"), { name: "AbortError" });
366
+ const sourceClient = lifecycleSourceClient(client), sourceRequest = sourceClient.request;
367
+ const sourceBatching = sourceClient.supportsReadBatching;
368
+ const reusable = assertLifecycleReadClientOpen(client);
369
+ const plan = planned.plan, planHash = planned.planHash, launchId = planned.launchId;
370
+ const optionsLimits = options.limits, plannedLimits = planned.limits;
371
+ const policy = optionsLimits === undefined ? plannedLimits : optionsLimits;
372
+ const blockMatches = observedFields(block);
373
+ const forkMatches = fork === undefined ? undefined : observedFields(fork);
374
+ const forkRequest = fork?.client.request;
375
+ const plannedMatches = observedFields(planned);
376
+ const context = Object.freeze({ client, account: planned.account, orchestrator: plan.orchestrator, block: Object.freeze({ ...block }), chainId: planned.chainId });
377
+ let closed = false, active = false, consumed = false, capabilityProved = false;
378
+ let capabilityPending;
379
+ const assertContext = () => {
380
+ if (closed)
381
+ throw new LifecyclePlanningError("INVALID_SIMULATION", "Prepared simulation continuation has closed");
382
+ if (signal?.aborted)
383
+ throw Object.assign(new Error("Lifecycle simulation cancelled"), { name: "AbortError" });
384
+ assertLifecycleReadClientOpen(client);
385
+ if (options.client !== client || options.planned !== planned || options.limits !== optionsLimits || options.fork !== fork || options.signal !== signal ||
386
+ lifecycleSourceClient(client) !== sourceClient || sourceClient.request !== sourceRequest || !plannedMatches() || !blockMatches() ||
387
+ sourceClient.supportsReadBatching !== sourceBatching ||
388
+ planned.plan !== plan || planned.account !== context.account || plan.orchestrator !== context.orchestrator || planned.chainId !== context.chainId ||
389
+ planned.limits !== plannedLimits || forkMatches !== undefined && !forkMatches() || fork?.client.request !== forkRequest) {
390
+ throw new LifecyclePlanningError("LIMIT_CONTEXT_MISMATCH", "Prepared simulation differs from its source/account/core/plan/header/policy context");
391
+ }
392
+ if (hashLaunchPlan(plan).toLowerCase() !== planHash.toLowerCase() || hashLaunchIdentity(plan).toLowerCase() !== launchId.toLowerCase()) {
393
+ throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed after its simulation context was bound");
394
+ }
395
+ };
288
396
  const pending = (async () => {
289
- if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === sourceClient)
397
+ if (fork !== undefined && lifecycleSourceClient(fork.client) === sourceClient)
290
398
  throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
291
399
  return lifecycleStage(client, "simulation.context", () => Promise.all([
292
- resolveLifecycleLimits(context, options.limits === undefined ? planned.limits : options.limits),
293
- lifecycleRpc(client, "eth_chainId"),
294
- lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(block.number)], block),
400
+ resolveLifecycleLimits(context, policy),
401
+ readLifecycleChainId(client),
402
+ lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block),
295
403
  hasTransactions ? lifecycleRpc(client, "eth_gasPrice") : undefined,
296
404
  ]));
297
405
  })().then((observations) => ({ observations }), (failure) => ({ failure }));
298
- // Attach the failure handler before input validation can refuse or throw.
299
- // Preparation is read-only; no probe, measurement, replay or fork work starts.
300
- let consumed = false;
301
- return async (transactions) => {
302
- if (consumed)
303
- throw new LifecyclePlanningError("INVALID_SIMULATION", "Prepared simulation context is invocation-local and may only be consumed once");
406
+ // Refused inputs may discard this read-only preparation before it settles.
407
+ // Its rejection is handled immediately; no capability or economic work starts.
408
+ const run = async (transactions, diagnostic, nextEnvelope) => {
409
+ if (active || consumed && !reusable || !hasTransactions && transactions.length !== 0) {
410
+ closed = true;
411
+ throw new LifecyclePlanningError("INVALID_SIMULATION", "Prepared candidates require sequential consumption by their SDK invocation owner");
412
+ }
413
+ active = true;
304
414
  consumed = true;
305
- const result = await pending;
306
- if ("failure" in result)
307
- throw result.failure;
308
- if (options.client !== client || options.planned !== planned || context.client !== client || context.block !== block || lifecycleSourceClient(client) !== sourceClient ||
309
- planned.account !== context.account || planned.plan.orchestrator !== context.orchestrator || planned.chainId !== context.chainId ||
310
- block.number !== blockNumber || block.hash !== blockHash)
311
- throw new LifecyclePlanningError("LIMIT_CONTEXT_MISMATCH", "Prepared simulation context differs from the bound source/chain/account/core/block");
312
- return simulateLaunchTransactionsWithContext(options, transactions, context, result.observations);
415
+ try {
416
+ assertContext();
417
+ const envelopeMatches = nextEnvelope === undefined ? undefined : observedFields(nextEnvelope);
418
+ const candidates = transactions.map((transaction) => ({
419
+ transaction, matches: observedFields(transaction),
420
+ dependencies: [...transaction.dependencies],
421
+ conditions: transaction.postconditions.map((condition) => ({ condition, matches: observedFields(condition) })),
422
+ }));
423
+ const result = await pending;
424
+ if ("failure" in result)
425
+ throw result.failure;
426
+ const assertCurrent = () => {
427
+ assertContext();
428
+ result.observations[0].assertCurrent();
429
+ if (envelopeMatches !== undefined && !envelopeMatches())
430
+ throw new LifecyclePlanningError("INVALID_SIMULATION", "Reviewed gas envelope changed during its complete economic proof");
431
+ if (transactions.length !== candidates.length || candidates.some(({ transaction, matches, dependencies, conditions }, index) => transactions[index] !== transaction || !matches() || transaction.dependencies.length !== dependencies.length ||
432
+ dependencies.some((dependency, ordinal) => transaction.dependencies[ordinal] !== dependency) ||
433
+ transaction.postconditions.length !== conditions.length || conditions.some(({ condition, matches: conditionMatches }, ordinal) => transaction.postconditions[ordinal] !== condition || !conditionMatches()))) {
434
+ throw new LifecyclePlanningError("INVALID_SIMULATION", "Candidate transactions changed during their complete economic proof");
435
+ }
436
+ };
437
+ assertCurrent();
438
+ const proveBackend = async (limits, gasPrice) => {
439
+ assertCurrent();
440
+ if (capabilityProved)
441
+ return;
442
+ if (capabilityPending === undefined) {
443
+ const key = JSON.stringify([context.chainId.toString(), context.account.toLowerCase(), context.orchestrator.toLowerCase(), context.block.number.toString(), context.block.hash.toLowerCase(), context.block.timestamp.toString(), context.block.gasLimit.toString(), limits.arbOSVersion?.toString(), limits.maxTxComputeGas?.toString(), limits.maxBlockComputeGas?.toString(), limits.executionGasCeiling.toString(), limits.maxSimulationGas?.toString(), gasPrice.toString()]);
444
+ let proofs = reusable ? nitroProofsByInvocation.get(client) : undefined;
445
+ if (reusable && proofs === undefined) {
446
+ proofs = new Map();
447
+ nitroProofsByInvocation.set(client, proofs);
448
+ }
449
+ const ownedProofs = proofs;
450
+ let nativeProof = ownedProofs?.get(key);
451
+ if (nativeProof === undefined) {
452
+ nativeProof = lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, context.block, limits, gasPrice));
453
+ ownedProofs?.set(key, nativeProof);
454
+ const pending = nativeProof;
455
+ void pending.catch(() => { if (ownedProofs?.get(key) === pending)
456
+ ownedProofs?.delete(key); });
457
+ }
458
+ const proof = nativeProof.then(() => {
459
+ assertContext();
460
+ result.observations[0].assertCurrent();
461
+ capabilityProved = true;
462
+ });
463
+ capabilityPending = proof;
464
+ void proof.then(() => { if (capabilityPending === proof)
465
+ capabilityPending = undefined; }, () => { if (capabilityPending === proof)
466
+ capabilityPending = undefined; });
467
+ }
468
+ await capabilityPending;
469
+ assertCurrent();
470
+ };
471
+ const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope);
472
+ assertCurrent();
473
+ return simulation;
474
+ }
475
+ catch (failure) {
476
+ closed = true;
477
+ capabilityProved = false;
478
+ capabilityPending = undefined;
479
+ throw failure;
480
+ }
481
+ finally {
482
+ active = false;
483
+ }
313
484
  };
485
+ const simulate = Object.assign((transactions, nextEnvelope) => run(transactions, false, nextEnvelope), { measure: (transactions) => run(transactions, true),
486
+ close() { closed = true; capabilityProved = false; capabilityPending = undefined; } });
487
+ return simulate;
314
488
  }
315
- export async function simulateLaunchTransactions(options, transactions, pinnedBlock) {
489
+ export async function simulateLaunchTransactions(options, transactions, pinnedBlock, nextEnvelope) {
316
490
  // Standalone calls always produce fresh context, including live chain and fees.
317
491
  if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === lifecycleSourceClient(options.client))
318
492
  throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
319
493
  const block = pinnedBlock ?? await readLifecycleBlock(options.client);
320
- return prepareLaunchSimulation(options, block, transactions.length !== 0)(transactions);
494
+ const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0);
495
+ try {
496
+ return await prepared(transactions, nextEnvelope);
497
+ }
498
+ finally {
499
+ prepared.close();
500
+ }
321
501
  }
322
- async function simulateLaunchTransactionsWithContext(options, transactions, context, observations) {
502
+ async function simulateLaunchTransactionsWithContext(options, transactions, context, observations, proveBackend, assertCurrent, diagnostic, nextEnvelope) {
323
503
  const { client, planned } = options;
324
504
  const sourceClient = lifecycleSourceClient(client);
325
505
  const { block } = context;
326
506
  const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = observations;
327
507
  const base = { confidence: "stateful", executionProof: "failed", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits };
328
- if (rpcQuantity(chainId, "chain ID") !== planned.chainId)
508
+ if (chainId !== planned.chainId)
329
509
  throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
330
510
  const accountCode = rpcHex(accountCodeValue, "account code");
331
- if (accountCode !== "0x")
511
+ if (accountCode !== "0x") {
512
+ await assertLifecycleBlock(client, block, planned.chainId);
332
513
  return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
514
+ }
333
515
  for (const transaction of transactions) {
334
516
  if (transaction.from.toLowerCase() !== planned.account.toLowerCase() || BigInt(transaction.chainId) !== planned.chainId)
335
517
  throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Simulation transaction differs from the committed chain/account");
336
- if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes)
337
- return { ...base, executionProof: "unavailable", protocolFit: "failed", backend: "unavailable", admitted: false, steps: [], failedTransactionId: transaction.id, reason: "Exact calldata exceeds the supplied chain/account/RPC byte cap" };
518
+ if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) {
519
+ await assertLifecycleBlock(client, block, planned.chainId);
520
+ return { ...base, executionProof: "unavailable", protocolFit: "failed", backend: "unavailable", admitted: false, steps: [], failedTransactionId: transaction.id, failureCategory: "capacity", capacityConstraint: "calldata", reason: "Exact calldata exceeds the supplied chain/account/RPC byte cap" };
521
+ }
338
522
  }
339
523
  if (transactions.length === 0) {
340
524
  await assertLifecycleBlock(client, block, planned.chainId);
341
525
  return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] };
342
526
  }
343
- const gasPrice = rpcQuantity(gasPriceValue, "gas price");
527
+ const recommendedGasPrice = rpcQuantity(gasPriceValue, "gas price");
528
+ if (recommendedGasPrice >= 1n << 256n)
529
+ throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Recommended gas price must be a uint256 quantity");
530
+ // Initial construction reserves twice the pinned base fee if the quote lags.
531
+ // Reviewed fees are not new recommendations: native replay must accept them
532
+ // under natural child fees, with no base-fee override or silent fee increase.
533
+ const baseFeeEnvelope = (block.baseFeePerGas ?? 0n) * 2n;
534
+ const gasPrice = nextEnvelope?.gasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
344
535
  if (gasPrice >= 1n << 256n)
345
536
  throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
537
+ if (nextEnvelope !== undefined && (typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas <= 0n || nextEnvelope.gas >= 1n << 64n ||
538
+ typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
539
+ throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Reviewed gas and gas price must be exact positive uint64 gas and nonnegative uint256 price");
540
+ }
346
541
  const buffer = (gas) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
347
542
  const computeCeiling = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
348
- const capGas = transactions.map(() => computeCeiling);
349
- let parallelMeasurement;
543
+ // Nonnegative poster gas cannot make this complete discovery request smaller.
544
+ // A generic controlled fork retains the existing RPC-to-fork fallback.
545
+ if (limits.maxSimulationGas !== undefined && BigInt(transactions.length) * computeCeiling > limits.maxSimulationGas &&
546
+ (limits.protocol === "nitro" || options.fork === undefined)) {
547
+ assertCurrent();
548
+ await assertLifecycleBlock(client, block, planned.chainId);
549
+ 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."}` };
550
+ }
350
551
  const posterGas = [];
351
552
  if (limits.protocol === "nitro") {
352
- // The isolated capability probe, poster quote and permissive cap measurement
353
- // have no data dependency. Only an actually batching source overlaps all three.
354
- const nitroEvidence = Promise.all([
355
- lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, block, limits, gasPrice)),
356
- lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
357
- ]);
358
- if (sourceClient.supportsReadBatching === true) {
359
- // Handle settlement immediately so a failed measurement cannot escape while
360
- // the independent metering evidence is still pending. It is never admission.
361
- parallelMeasurement = lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false))
362
- .then((steps) => ({ steps }), (failure) => ({ failure }));
363
- }
553
+ // Lightweight poster reads may overlap the isolated capability request.
554
+ // Economic work is not even queued until both prerequisites have succeeded.
364
555
  try {
365
- const [, estimates] = await nitroEvidence;
556
+ const [, estimates] = await Promise.all([
557
+ proveBackend(limits, gasPrice),
558
+ lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
559
+ ]);
366
560
  posterGas.push(...estimates);
367
561
  }
368
562
  catch (failure) {
563
+ if (simulationFailureKind(failure) === "abort")
564
+ throw failure;
565
+ assertCurrent();
369
566
  await assertLifecycleBlock(client, block, planned.chainId);
370
- return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Native Nitro compute/poster proof unavailable: ${failure instanceof Error ? failure.message : String(failure)}. Generic Anvil/Hardhat forks cannot prove ArbOS metering.` };
567
+ return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Native Nitro compute/poster proof unavailable: ${lifecycleFailureReason(failure)}. Generic Anvil/Hardhat forks cannot prove ArbOS metering.` };
371
568
  }
372
569
  }
570
+ assertCurrent();
373
571
  // The poster quote allocates gas only. The native exact replay enforces actual
374
572
  // compute hold; subtracting this approximate budget from gasUsed would not.
375
573
  const discoveryGas = transactions.map((_, index) => {
@@ -381,45 +579,53 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
381
579
  });
382
580
  let backend = "eth_simulateV1";
383
581
  let measured;
384
- // Permissive cap measurement is not proof; only the exact-gas validated replay admits a plan.
582
+ // A permissive measurement needs the legal complete compute + poster envelope.
583
+ // It is not proof: fixed compute/headroom checks and exact validated replay still follow.
385
584
  try {
386
- if (parallelMeasurement === undefined)
387
- measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false));
388
- else {
389
- const observation = await parallelMeasurement;
390
- if ("failure" in observation)
391
- throw observation.failure;
392
- measured = observation.steps;
393
- }
585
+ measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, discoveryGas, limits, gasPrice, false));
394
586
  }
395
587
  catch (failure) {
588
+ if (simulationFailureKind(failure) === "abort")
589
+ throw failure;
590
+ assertCurrent();
396
591
  if (options.fork === undefined || limits.protocol === "nitro") {
397
592
  await assertLifecycleBlock(client, block, planned.chainId);
398
- return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Stateful sequential RPC simulation unavailable: ${failure instanceof Error ? failure.message : String(failure)}. ${limits.protocol === "nitro" ? "Generic Anvil/Hardhat forks cannot prove ArbOS compute/poster metering." : "Dependent eth_call estimates are not proof."}` };
593
+ 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."}` };
399
594
  }
400
595
  backend = "controlled-fork";
401
- measured = await simulateForkPass(options.fork, planned, block, transactions, capGas, gasPrice, true, limits.headroomBps);
596
+ measured = await simulateForkPass(options.fork, planned, block, transactions, discoveryGas, gasPrice, true, limits.headroomBps);
402
597
  }
598
+ assertCurrent();
403
599
  const failed = measured.find((step) => !step.success);
404
600
  if (failed !== undefined) {
405
601
  await assertLifecycleBlock(client, block, planned.chainId);
406
- return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId };
602
+ return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId, failureCategory: failed.failureCategory ?? "opaque", capacityConstraint: failed.failureCategory === "capacity" ? "compute" : undefined };
603
+ }
604
+ if (diagnostic) {
605
+ await assertLifecycleBlock(client, block, planned.chainId);
606
+ return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
407
607
  }
408
- const computeGas = measured.map((step) => buffer(step.gasRequired ?? step.gasUsed ?? computeCeiling));
409
- const gasLimits = computeGas.map((gas, index) => gas + (posterGas[index] ?? 0n));
608
+ // Padding belongs to new envelopes. A reviewed envelope may consume its
609
+ // original margin; only exact replay at its held gas/price proves it still fits.
610
+ const computeGas = measured.map((step, index) => {
611
+ const required = step.gasRequired ?? step.gasUsed ?? computeCeiling;
612
+ return nextEnvelope !== undefined && index === 0 ? required : buffer(required);
613
+ });
614
+ const gasLimits = computeGas.map((gas, index) => nextEnvelope !== undefined && index === 0 ? nextEnvelope.gas : gas + (posterGas[index] ?? 0n));
410
615
  const exceeds = gasLimits.findIndex((gas, index) => gas <= 0n || gas >= 1n << 64n || (computeGas[index] ?? 0n) > limits.executionGasCeiling || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling);
411
616
  if (exceeds >= 0) {
412
617
  await assertLifecycleBlock(client, block, planned.chainId);
413
- return { ...base, executionProof: "unavailable", protocolFit: "failed", backend, admitted: false, steps: measured, reason: "Execution compute/headroom or complete poster gas envelope is outside positive uint64 gas and current protocol/known transaction limits", failedTransactionId: transactions[exceeds]?.id };
618
+ 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 };
414
619
  }
415
- // Replay from the same initial state with exact headroom limits: gasUsed alone is not an EIP-150/gasleft proof.
416
- // eth_simulateV1 with validation raises an RPC error on any revert (a generic
417
- // -32000 "execution failed" carries no gasUsed/returnData), so a validated
418
- // replay failure surfaces as a thrown error here. Route it through envelope
419
- // discovery too: EIP-150 retention can starve a forwarded sub-call below the
420
- // buffered limit while the true envelope still fits the execution ceiling.
620
+ // Envelope discovery may enlarge future unfinished steps, never the reviewed
621
+ // immediate one. Every retry still executes the entire sequence at held price.
622
+ const envelopeDiscoveryGas = nextEnvelope === undefined ? discoveryGas : discoveryGas.map((gas, index) => index === 0 ? nextEnvelope.gas : gas);
623
+ // Replay the complete sequence from its original state at the exact buffered
624
+ // limits. Only explicit native OOG can discover one larger legal envelope;
625
+ // semantic, opaque and transport failures cannot authorize a capacity retry.
421
626
  let verified;
422
627
  let validatedReplayError;
628
+ assertCurrent();
423
629
  try {
424
630
  if (backend === "eth_simulateV1")
425
631
  verified = await lifecycleStage(client, "simulation.replay", () => simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true));
@@ -430,73 +636,128 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
430
636
  }
431
637
  }
432
638
  catch (failure) {
433
- const message = failure instanceof Error ? failure.message : String(failure);
639
+ const kind = simulationFailureKind(failure);
640
+ if (kind === "abort")
641
+ throw failure;
642
+ assertCurrent();
643
+ const message = lifecycleFailureReason(failure);
434
644
  if (backend !== "eth_simulateV1") {
435
645
  await assertLifecycleBlock(client, block, planned.chainId);
436
646
  return { ...base, backend, admitted: false, steps: [], reason: `Exact-gas validated replay failed: ${message}` };
437
647
  }
648
+ if (kind !== "out-of-gas") {
649
+ await assertLifecycleBlock(client, block, planned.chainId);
650
+ return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, failureCategory: kind === "source" ? "source" : "opaque", reason: `Exact-gas validated replay unavailable: ${message}` };
651
+ }
438
652
  validatedReplayError = message;
439
653
  verified = [];
440
654
  }
655
+ assertCurrent();
441
656
  let verifiedSteps = verified;
442
657
  let admittedGasLimits = gasLimits;
443
658
  let finalFailure = verifiedSteps.find((step) => !step.success);
659
+ let finalFailureReason;
444
660
  if (finalFailure === undefined && validatedReplayError !== undefined) {
661
+ if (!envelopeDiscoveryGas.some((gas, index) => gas > (gasLimits[index] ?? gas))) {
662
+ await assertLifecycleBlock(client, block, planned.chainId);
663
+ return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling failed: ${validatedReplayError}` };
664
+ }
445
665
  // The buffered validated replay threw before yielding steps. Attempt one
446
666
  // explicit envelope-discovery replay at the exact execution ceiling on the
447
667
  // same pinned block; headroom is never silently raised — the carried gas is
448
668
  // the validated envelope itself.
669
+ assertCurrent();
449
670
  try {
450
- const ceilingLimits = discoveryGas;
671
+ const ceilingLimits = envelopeDiscoveryGas;
451
672
  const retried = backend === "eth_simulateV1"
452
673
  ? await lifecycleStage(client, "simulation.envelope", () => simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true))
453
674
  : options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
454
- if (retried === undefined || retried.some((step) => !step.success)) {
675
+ const retryFailureIndex = retried?.findIndex((step) => !step.success) ?? -1;
676
+ const retryFailure = retried?.[retryFailureIndex];
677
+ if (retried === undefined) {
455
678
  await assertLifecycleBlock(client, block, planned.chainId);
456
- return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${retried?.find((step) => !step.success)?.error ?? validatedReplayError}; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned`, failedTransactionId: transactions[0]?.id };
679
+ return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${validatedReplayError}` };
457
680
  }
458
681
  admittedGasLimits = ceilingLimits;
459
682
  verifiedSteps = retried;
460
- finalFailure = undefined;
683
+ finalFailure = retryFailure;
461
684
  validatedReplayError = undefined;
685
+ if (retryFailure !== undefined) {
686
+ const kind = transactions[retryFailureIndex]?.kind;
687
+ const indivisible = kind === "activate" || kind === "atomic";
688
+ 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" : ""}`;
689
+ }
462
690
  }
463
- catch {
691
+ catch (failure) {
692
+ const kind = simulationFailureKind(failure);
693
+ if (kind === "abort")
694
+ throw failure;
695
+ assertCurrent();
464
696
  await assertLifecycleBlock(client, block, planned.chainId);
465
- return { ...base, backend, admitted: false, steps: measured, reason: `Validated buffered replay failed (${validatedReplayError}) and envelope discovery at the exact gas ceiling was rejected by the simulation backend; final mint/lock/all-buys/public-opening is indivisible`, failedTransactionId: transactions[0]?.id };
697
+ if (kind !== "out-of-gas")
698
+ 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)}` };
699
+ 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` };
466
700
  }
467
701
  }
468
- if (finalFailure !== undefined) {
702
+ else if (finalFailure !== undefined) {
469
703
  const failedIndex = verifiedSteps.findIndex((step) => !step.success);
470
704
  const bufferedLimit = gasLimits[failedIndex];
471
705
  const failedTransaction = transactions[failedIndex];
472
- if (bufferedLimit !== undefined && failedTransaction !== undefined && looksGasExhausted(finalFailure, bufferedLimit) && (discoveryGas[failedIndex] ?? 0n) > bufferedLimit) {
706
+ if (bufferedLimit !== undefined && failedTransaction !== undefined && (nextEnvelope === undefined || failedIndex !== 0) &&
707
+ looksGasExhausted(finalFailure, bufferedLimit) && (envelopeDiscoveryGas[failedIndex] ?? 0n) > bufferedLimit) {
473
708
  // Buffered replay returned an exhaustion-shaped failed step; learn the true
474
709
  // validated envelope with one explicit replay at the exact execution ceiling.
710
+ assertCurrent();
475
711
  try {
476
- const ceilingLimits = discoveryGas;
712
+ const ceilingLimits = envelopeDiscoveryGas;
477
713
  const retried = backend === "eth_simulateV1"
478
- ? await simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true)
714
+ ? await lifecycleStage(client, "simulation.envelope", () => simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true))
479
715
  : options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
480
- if (retried !== undefined) {
481
- const retryFailure = retried.find((step) => !step.success);
482
- if (retryFailure === undefined) {
483
- admittedGasLimits = ceilingLimits;
484
- verifiedSteps = retried;
485
- finalFailure = undefined;
486
- }
487
- else {
488
- const indivisible = failedTransaction.kind === "activate" || failedTransaction.kind === "atomic";
489
- await assertLifecycleBlock(client, block, planned.chainId);
490
- return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${retryFailure.error ?? "the true gas envelope exceeds the current execution limit"}${indivisible ? "; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned" : ""}`, failedTransactionId: failedTransaction.id };
491
- }
716
+ if (retried === undefined) {
717
+ await assertLifecycleBlock(client, block, planned.chainId);
718
+ return { ...base, backend, admitted: false, steps: measured, reason: "Validated execution at the exact current gas ceiling omitted transaction results" };
719
+ }
720
+ const retryFailureIndex = retried.findIndex((step) => !step.success);
721
+ const retryFailure = retried[retryFailureIndex];
722
+ admittedGasLimits = ceilingLimits;
723
+ verifiedSteps = retried;
724
+ finalFailure = retryFailure;
725
+ if (retryFailure !== undefined) {
726
+ const kind = transactions[retryFailureIndex]?.kind;
727
+ const indivisible = kind === "activate" || kind === "atomic";
728
+ 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" : ""}`;
492
729
  }
493
730
  }
494
- catch { /* Envelope discovery unavailable; report the original buffered-limit failure unchanged. */ }
731
+ catch (failure) {
732
+ const kind = simulationFailureKind(failure);
733
+ if (kind === "abort")
734
+ throw failure;
735
+ assertCurrent();
736
+ if (kind !== "out-of-gas") {
737
+ await assertLifecycleBlock(client, block, planned.chainId);
738
+ return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, reason: `Legal-envelope validated replay unavailable: ${lifecycleFailureReason(failure)}` };
739
+ }
740
+ await assertLifecycleBlock(client, block, planned.chainId);
741
+ 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)}` };
742
+ }
743
+ }
744
+ }
745
+ assertCurrent();
746
+ if (nextEnvelope !== undefined && finalFailure === undefined) {
747
+ const currentMinimum = verifiedSteps[0]?.gasRequired ?? verifiedSteps[0]?.gasUsed ?? nextEnvelope.gas;
748
+ if (nextEnvelope.gas < currentMinimum) {
749
+ await assertLifecycleBlock(client, block, planned.chainId);
750
+ return { ...base, backend, admitted: false, executionProof: "proved", protocolFit: "failed", steps: verifiedSteps,
751
+ failureCategory: "capacity", capacityConstraint: "gas-envelope", failedTransactionId: transactions[0]?.id,
752
+ reason: "Exact reviewed replay exceeds the held gas envelope" };
495
753
  }
496
754
  }
497
755
  const estimates = [];
498
- const balance = rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance");
499
- let remainingBalance = balance;
756
+ // A definitive returned replay failure already refuses execution. Retain its
757
+ // fee observations without inventing a balance or doing redundant payer work.
758
+ let remainingBalance = finalFailure === undefined
759
+ ? rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance")
760
+ : undefined;
500
761
  let affordabilityFailure;
501
762
  const feeContext = configuration.estimateDataFee === undefined || sourceClient === client ? context : { ...context, client: sourceClient };
502
763
  for (let index = 0; index < verifiedSteps.length; index += 1) {
@@ -505,7 +766,7 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
505
766
  const gasLimit = admittedGasLimits[index];
506
767
  if (step === undefined || transaction === undefined || gasLimit === undefined)
507
768
  throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing verified fee context");
508
- const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit }, feeContext);
769
+ const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit, gasPrice }, feeContext);
509
770
  if (dataFee !== undefined && (typeof dataFee !== "bigint" || dataFee < 0n || dataFee >= 1n << 256n))
510
771
  throw new LifecyclePlanningError("INVALID_DATA_FEE", "Data-fee oracle must return a nonnegative uint256 bigint amount");
511
772
  // Nitro gas includes poster charges. Never add the quoted poster fee again.
@@ -513,13 +774,15 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
513
774
  const required = transaction.value + executionFee + (dataFee ?? 0n);
514
775
  if (required >= 1n << 256n)
515
776
  throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
516
- if (required > remainingBalance && affordabilityFailure === undefined)
517
- affordabilityFailure = "Creator native balance cannot fund the remaining transaction value and execution/data fee envelopes";
518
- remainingBalance -= required;
777
+ if (remainingBalance !== undefined) {
778
+ if (required > remainingBalance && affordabilityFailure === undefined)
779
+ affordabilityFailure = "Creator native balance cannot fund the remaining transaction value and execution/data fee envelopes";
780
+ remainingBalance -= required;
781
+ }
519
782
  const poster = posterGas[index];
520
783
  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" } });
521
784
  }
522
785
  await assertLifecycleBlock(client, block, planned.chainId);
523
- 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 };
786
+ 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 };
524
787
  }
525
788
  //# sourceMappingURL=simulation.js.map