@black-market/sdk 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +96 -96
  2. package/dist/lifecycle/index.d.ts +3 -1
  3. package/dist/lifecycle/index.d.ts.map +1 -1
  4. package/dist/lifecycle/index.js +3 -1
  5. package/dist/lifecycle/index.js.map +1 -1
  6. package/dist/lifecycle/lens.d.ts +409 -0
  7. package/dist/lifecycle/lens.d.ts.map +1 -0
  8. package/dist/lifecycle/lens.js +30 -0
  9. package/dist/lifecycle/lens.js.map +1 -0
  10. package/dist/lifecycle/mining.d.ts +47 -0
  11. package/dist/lifecycle/mining.d.ts.map +1 -0
  12. package/dist/lifecycle/mining.js +89 -0
  13. package/dist/lifecycle/mining.js.map +1 -0
  14. package/dist/lifecycle/planner.d.ts.map +1 -1
  15. package/dist/lifecycle/planner.js +121 -78
  16. package/dist/lifecycle/planner.js.map +1 -1
  17. package/dist/lifecycle/presets.d.ts +3 -0
  18. package/dist/lifecycle/presets.d.ts.map +1 -1
  19. package/dist/lifecycle/presets.js +1 -0
  20. package/dist/lifecycle/presets.js.map +1 -1
  21. package/dist/lifecycle/progress.d.ts +51 -1
  22. package/dist/lifecycle/progress.d.ts.map +1 -1
  23. package/dist/lifecycle/progress.js +149 -73
  24. package/dist/lifecycle/progress.js.map +1 -1
  25. package/dist/lifecycle/rpc.d.ts +14 -4
  26. package/dist/lifecycle/rpc.d.ts.map +1 -1
  27. package/dist/lifecycle/rpc.js +171 -12
  28. package/dist/lifecycle/rpc.js.map +1 -1
  29. package/dist/lifecycle/simulation.d.ts +5 -3
  30. package/dist/lifecycle/simulation.d.ts.map +1 -1
  31. package/dist/lifecycle/simulation.js +137 -115
  32. package/dist/lifecycle/simulation.js.map +1 -1
  33. package/dist/lifecycle/snapshot.d.ts +8 -0
  34. package/dist/lifecycle/snapshot.d.ts.map +1 -0
  35. package/dist/lifecycle/snapshot.js +79 -0
  36. package/dist/lifecycle/snapshot.js.map +1 -0
  37. package/dist/lifecycle/types.d.ts +27 -5
  38. package/dist/lifecycle/types.d.ts.map +1 -1
  39. package/dist/lifecycle/types.js.map +1 -1
  40. package/docs/launch-lifecycle.md +627 -1495
  41. package/docs/reference.md +365 -0
  42. package/examples/.env.example +3 -2
  43. package/examples/README.md +171 -552
  44. package/examples/launch-example-support.ts +11 -3
  45. package/examples/launch-example.ts +1 -1
  46. package/examples/launch-lifecycle-smoke.ts +1 -1
  47. package/package.json +1 -1
  48. package/src/lifecycle/index.ts +5 -1
  49. package/src/lifecycle/lens.ts +42 -0
  50. package/src/lifecycle/mining.ts +80 -0
  51. package/src/lifecycle/planner.ts +122 -73
  52. package/src/lifecycle/presets.ts +4 -0
  53. package/src/lifecycle/progress.ts +132 -59
  54. package/src/lifecycle/rpc.ts +163 -13
  55. package/src/lifecycle/simulation.ts +134 -106
  56. package/src/lifecycle/snapshot.ts +72 -0
  57. package/src/lifecycle/types.ts +24 -6
@@ -1,7 +1,7 @@
1
1
  import { decodeErrorResult, decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctionResult, parseAbi, toHex } from "viem";
2
2
  import { fixedFeePoolHookV1Abi, launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
3
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";
4
+ import { assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleRpc, lifecycleSessionRead, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readLifecycleChainId, readLifecycleGasPrice, readNitroPosterGas, registerLifecycleFinalCheck, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity } from "./rpc.js";
5
5
  import { LifecyclePlanningError } from "./types.js";
6
6
  import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion } from "./markets.js";
7
7
  import { hmmV2ForwardingReserve } from "./presets.js";
@@ -61,7 +61,6 @@ function executionFailure(returnData, error) {
61
61
  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" };
62
62
  return { ...diagnostic, failureCategory: "opaque", error: nativeErrorKind === "execution-reverted" ? "Native execution reverted without a reviewed error" : "Execution failed without a reviewed error" };
63
63
  }
64
- const nitroProofsByInvocation = new WeakMap();
65
64
  function conditionRequest(condition, planned) {
66
65
  if (condition.kind === "allowance")
67
66
  return {
@@ -118,8 +117,6 @@ async function proveNitroBackend(client, planned, block, limits, gasPrice) {
118
117
  throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Simulated ArbOS version/compute getters differ from the exact pinned chain context");
119
118
  }
120
119
  }
121
- // The enclosing simulation rechecks chain and canonical hash after its proofs
122
- // (including every refusal path); a separate probe-only recheck is redundant.
123
120
  }
124
121
  async function simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, validation) {
125
122
  // Natural child-block fees must govern fee-bearing admission; overriding
@@ -129,7 +126,7 @@ async function simulateRpcPass(client, planned, block, transactions, gasLimits,
129
126
  // parent gasLimit are set.
130
127
  const blockStateCalls = transactions.map((transaction, index) => ({
131
128
  blockOverrides: { number: toHex(block.number + BigInt(index + 1)), time: toHex(block.timestamp + BigInt(index + 1)), gasLimit: toHex(block.gasLimit) },
132
- calls: [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gasLimits[index] ?? limits.executionGasCeiling), gasPrice: toHex(gasPrice) }],
129
+ calls: [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gasLimits[index] ?? limits.executionGasCeiling), gasPrice: toHex(gasPrice), ...(transaction.nonce === undefined ? {} : { nonce: toHex(transaction.nonce) }) }],
133
130
  }));
134
131
  const result = await lifecycleRpc(client, "eth_simulateV1", [{ blockStateCalls, validation, traceTransfers: false, returnFullTransactions: false }, toHex(block.number)]);
135
132
  if (!Array.isArray(result) || result.length !== transactions.length)
@@ -199,10 +196,6 @@ async function simulateRpcPass(client, planned, block, transactions, gasLimits,
199
196
  }
200
197
  return steps;
201
198
  }
202
- /** A decoded semantic refusal or opaque deployment wrapper is never gas evidence. */
203
- function looksGasExhausted(step, gasLimit) {
204
- return !step.success && step.failureCategory === "capacity" && step.gasUsed !== undefined && step.gasUsed >= gasLimit - gasLimit / 64n;
205
- }
206
199
  function simulationFailureKind(failure) {
207
200
  for (let depth = 0; depth < 8 && failure !== null && typeof failure === "object"; depth += 1) {
208
201
  const error = failure;
@@ -223,6 +216,19 @@ function simulationFailureKind(failure) {
223
216
  }
224
217
  return "source";
225
218
  }
219
+ function upfrontFundsFailure(failure) {
220
+ for (let depth = 0; depth < 8 && failure !== null && typeof failure === "object"; depth += 1) {
221
+ const error = failure;
222
+ const status = error.status ?? error.statusCode;
223
+ if (error.name === "AbortError" || typeof status === "number" && status >= 400 && status <= 599 || error.code === 3)
224
+ return false;
225
+ const message = typeof error.details === "string" ? error.details : typeof error.message === "string" ? error.message : "";
226
+ if (error.code === -38014 || (error.code === undefined || error.code === -32000) && /^insufficient funds\b/i.test(message))
227
+ return true;
228
+ failure = error.cause;
229
+ }
230
+ return false;
231
+ }
226
232
  async function waitForkReceipt(client, transactionHash, timeoutMs) {
227
233
  const deadline = Date.now() + timeoutMs;
228
234
  while (Date.now() <= deadline) {
@@ -269,13 +275,13 @@ async function simulateForkPass(fork, planned, block, transactions, gasLimits, g
269
275
  throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing fork transaction/gas context");
270
276
  try {
271
277
  if (measure) {
272
- const estimated = rpcQuantity(await lifecycleRpc(fork.client, "eth_estimateGas", [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gasPrice: toHex(gasPrice) }]), "fork sequential gas estimate");
278
+ const estimated = rpcQuantity(await lifecycleRpc(fork.client, "eth_estimateGas", [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gasPrice: toHex(gasPrice), ...(transaction.nonce === undefined ? {} : { nonce: toHex(transaction.nonce) }) }]), "fork sequential gas estimate");
273
279
  gasRequired = estimated;
274
280
  const buffered = (estimated * BigInt(10000 + headroomBps) + 9999n) / 10000n;
275
281
  if (buffered < gas)
276
282
  gas = buffered;
277
283
  }
278
- const hash = rpcHex(await lifecycleRpc(fork.client, "eth_sendTransaction", [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gas), gasPrice: toHex(gasPrice) }]), "fork transaction hash");
284
+ const hash = rpcHex(await lifecycleRpc(fork.client, "eth_sendTransaction", [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gas), gasPrice: toHex(gasPrice), ...(transaction.nonce === undefined ? {} : { nonce: toHex(transaction.nonce) }) }]), "fork transaction hash");
279
285
  const receipt = await waitForkReceipt(fork.client, hash, fork.receiptTimeoutMs ?? 10_000);
280
286
  const gasUsed = rpcQuantity(receipt.gasUsed, "fork gas used");
281
287
  if (rpcQuantity(receipt.status, "fork transaction status") !== 1n) {
@@ -358,7 +364,7 @@ function observedFields(value) {
358
364
  }
359
365
  /** Internal continuation owned by one SDK read invocation. Only its positive
360
366
  * capability proof is reused; every candidate executes its entire economic sequence. */
361
- export function prepareLaunchSimulation(options, block, hasTransactions = true, reviewedEnvelope) {
367
+ export function prepareLaunchSimulation(options, block, hasTransactions = true, reviewedEnvelope, selectedGasPrice, sizing) {
362
368
  const { client, planned, fork } = options;
363
369
  const signal = options.signal;
364
370
  if (signal?.aborted)
@@ -401,8 +407,8 @@ export function prepareLaunchSimulation(options, block, hasTransactions = true,
401
407
  return lifecycleStage(client, "simulation.context", () => Promise.all([
402
408
  resolveLifecycleLimits(context, policy),
403
409
  readLifecycleChainId(client),
404
- lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block),
405
- hasTransactions && reviewedEnvelope === undefined ? lifecycleRpc(client, "eth_gasPrice") : undefined,
410
+ lifecycleSessionRead(client, `${planned.chainId}:account-code:${context.account.toLowerCase()}`, () => lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block)),
411
+ hasTransactions && reviewedEnvelope === undefined ? selectedGasPrice ?? readLifecycleGasPrice(client) : undefined,
406
412
  ]));
407
413
  })().then((observations) => ({ observations }), (failure) => ({ failure }));
408
414
  // Refused inputs may discard this read-only preparation before it settles.
@@ -442,21 +448,8 @@ export function prepareLaunchSimulation(options, block, hasTransactions = true,
442
448
  if (capabilityProved)
443
449
  return;
444
450
  if (capabilityPending === undefined) {
445
- 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()]);
446
- let proofs = reusable ? nitroProofsByInvocation.get(client) : undefined;
447
- if (reusable && proofs === undefined) {
448
- proofs = new Map();
449
- nitroProofsByInvocation.set(client, proofs);
450
- }
451
- const ownedProofs = proofs;
452
- let nativeProof = ownedProofs?.get(key);
453
- if (nativeProof === undefined) {
454
- nativeProof = lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, context.block, limits, gasPrice));
455
- ownedProofs?.set(key, nativeProof);
456
- const pending = nativeProof;
457
- void pending.catch(() => { if (ownedProofs?.get(key) === pending)
458
- ownedProofs?.delete(key); });
459
- }
451
+ const key = `${context.chainId}:nitro-backend`;
452
+ const nativeProof = lifecycleSessionRead(client, key, () => lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, context.block, limits, gasPrice)));
460
453
  const proof = nativeProof.then(() => {
461
454
  assertContext();
462
455
  result.observations[0].assertCurrent();
@@ -470,8 +463,9 @@ export function prepareLaunchSimulation(options, block, hasTransactions = true,
470
463
  await capabilityPending;
471
464
  assertCurrent();
472
465
  };
473
- const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope);
466
+ const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, selectedGasPrice, sizing);
474
467
  assertCurrent();
468
+ registerLifecycleFinalCheck(client, result.observations[0].assertCurrent);
475
469
  return simulation;
476
470
  }
477
471
  catch (failure) {
@@ -488,12 +482,12 @@ export function prepareLaunchSimulation(options, block, hasTransactions = true,
488
482
  close() { closed = true; capabilityProved = false; capabilityPending = undefined; } });
489
483
  return simulate;
490
484
  }
491
- export async function simulateLaunchTransactions(options, transactions, pinnedBlock, nextEnvelope) {
485
+ export async function simulateLaunchTransactions(options, transactions, pinnedBlock, nextEnvelope, sizing) {
492
486
  // Standalone calls always produce fresh context, including live chain and fees.
493
487
  if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === lifecycleSourceClient(options.client))
494
488
  throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
495
489
  const block = pinnedBlock ?? await readLifecycleBlock(options.client);
496
- const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope);
490
+ const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope, undefined, sizing);
497
491
  try {
498
492
  return await prepared(transactions, nextEnvelope);
499
493
  }
@@ -501,7 +495,7 @@ export async function simulateLaunchTransactions(options, transactions, pinnedBl
501
495
  prepared.close();
502
496
  }
503
497
  }
504
- async function simulateLaunchTransactionsWithContext(options, transactions, context, observations, proveBackend, assertCurrent, diagnostic, nextEnvelope) {
498
+ async function simulateLaunchTransactionsWithContext(options, transactions, context, observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, selectedGasPrice, sizing) {
505
499
  const { client, planned } = options;
506
500
  const sourceClient = lifecycleSourceClient(client);
507
501
  const { block } = context;
@@ -511,19 +505,16 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
511
505
  throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
512
506
  const accountCode = rpcHex(accountCodeValue, "account code");
513
507
  if (accountCode !== "0x") {
514
- await assertLifecycleBlock(client, block, planned.chainId);
515
508
  return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
516
509
  }
517
510
  for (const transaction of transactions) {
518
511
  if (transaction.from.toLowerCase() !== planned.account.toLowerCase() || BigInt(transaction.chainId) !== planned.chainId)
519
512
  throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Simulation transaction differs from the committed chain/account");
520
513
  if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) {
521
- await assertLifecycleBlock(client, block, planned.chainId);
522
514
  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" };
523
515
  }
524
516
  }
525
517
  if (transactions.length === 0) {
526
- await assertLifecycleBlock(client, block, planned.chainId);
527
518
  return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] };
528
519
  }
529
520
  if (nextEnvelope !== undefined && (transactions.length !== 1 || typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas <= 0n || nextEnvelope.gas >= 1n << 64n ||
@@ -552,15 +543,14 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
552
543
  const reason = exceedsCompute
553
544
  ? `${atomic ? "Atomic launch" : "Immediate transaction"} needs about ${gasInMillions(estimatedGas)}M gas, above the ${gasInMillions(computeCap)}M per-transaction limit${atomic ? "; use staged" : ""}`
554
545
  : "The immediate transaction estimate exceeds the RPC aggregate simulation gas cap; no prefix can be proved";
555
- await assertLifecycleBlock(client, block, planned.chainId);
556
546
  return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: exceedsCompute ? "compute" : "simulation-gas", reason };
557
547
  }
558
548
  }
559
- const recommendedGasPrice = nextEnvelope === undefined ? rpcQuantity(gasPriceValue, "gas price") : nextEnvelope.gasPrice;
549
+ const recommendedGasPrice = nextEnvelope === undefined ? typeof gasPriceValue === "bigint" ? gasPriceValue : rpcQuantity(gasPriceValue, "gas price") : nextEnvelope.gasPrice;
560
550
  // New recommendations cover lagging quotes; reviewed fees are replayed unchanged
561
551
  // under natural child fees, never refreshed or silently increased.
562
552
  const baseFeeEnvelope = (block.baseFeePerGas ?? 0n) * 2n;
563
- const gasPrice = nextEnvelope?.gasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
553
+ const gasPrice = nextEnvelope?.gasPrice ?? selectedGasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
564
554
  if (gasPrice >= 1n << 256n)
565
555
  throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
566
556
  const buffer = (gas) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
@@ -569,7 +559,7 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
569
559
  try {
570
560
  const [, estimates] = await Promise.all([
571
561
  proveBackend(limits, gasPrice),
572
- nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))) : [],
562
+ !diagnostic && nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster", async () => [buffer((await readNitroPosterGas(transactions[0], context)).posterGas)]) : [],
573
563
  ]);
574
564
  posterGas.push(...estimates);
575
565
  }
@@ -577,37 +567,81 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
577
567
  if (simulationFailureKind(failure) === "abort")
578
568
  throw failure;
579
569
  assertCurrent();
580
- await assertLifecycleBlock(client, block, planned.chainId);
581
570
  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.` };
582
571
  }
583
572
  }
584
573
  assertCurrent();
585
- const discoveryGas = transactions.map((_, index) => {
586
- const total = limits.executionGasCeiling + (posterGas[index] ?? 0n);
574
+ const discoveryGas = transactions.map(() => {
575
+ const total = limits.protocol === "nitro" ? limits.maxSimulationGas ?? block.gasLimit : limits.executionGasCeiling;
587
576
  const capped = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < total ? limits.transactionGasCeiling : total;
588
577
  if (capped <= 0n || capped >= 1n << 64n)
589
578
  throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Compute/poster envelope exceeds uint64 gas");
590
579
  return capped;
591
580
  });
592
- let backend = nextEnvelope !== undefined && limits.protocol === "evm" && options.fork !== undefined ? "controlled-fork" : "eth_simulateV1";
581
+ if (discoveryGas[0] * gasPrice + transactions[0].value >= 1n << 256n)
582
+ throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
583
+ const balance = rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance");
593
584
  let measured = [];
594
- if (nextEnvelope === undefined) {
585
+ const hasSizing = !diagnostic && transactions.every((transaction) => sizing?.has(transaction.id));
586
+ let backend = (nextEnvelope !== undefined || hasSizing) && limits.protocol === "evm" && options.fork !== undefined ? "controlled-fork" : "eth_simulateV1";
587
+ if (nextEnvelope === undefined && !hasSizing) {
588
+ // Nitro checks upfront funds even with validation:false. Bound discovery
589
+ // budgets before dispatch, without overriding the payer's real balance.
590
+ const value = transactions.reduce((total, transaction) => total + transaction.value, 0n);
591
+ let available = balance - value;
592
+ if (gasPrice !== 0n)
593
+ available /= gasPrice;
594
+ const sizingGas = discoveryGas.map((ceiling, index) => {
595
+ // Upfront reservation is per call; unused gas is refunded before the next.
596
+ const computeAndPoster = limits.executionGasCeiling + (posterGas[index] ?? 0n);
597
+ const capped = computeAndPoster < ceiling ? computeAndPoster : ceiling;
598
+ return gasPrice === 0n || capped < available ? capped : available;
599
+ });
600
+ let retriedForFunds = false;
601
+ if (balance < value || sizingGas.some((gas) => gas <= 0n))
602
+ return { ...base, executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "affordability", reason: "Creator native balance cannot fund transaction value and sizing fee budgets" };
603
+ assertCurrent();
595
604
  try {
596
- measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, discoveryGas, limits, gasPrice, false));
605
+ try {
606
+ measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, sizingGas, limits, gasPrice, false));
607
+ }
608
+ catch (failure) {
609
+ if (!upfrontFundsFailure(failure))
610
+ throw failure;
611
+ assertCurrent();
612
+ retriedForFunds = true;
613
+ let remaining = available;
614
+ const sharedGas = sizingGas.map((ceiling, index) => {
615
+ if (gasPrice === 0n)
616
+ return ceiling;
617
+ const share = remaining / BigInt(transactions.length - index);
618
+ const gas = ceiling < share ? ceiling : share;
619
+ remaining -= gas;
620
+ return gas;
621
+ });
622
+ measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, sharedGas, limits, gasPrice, false));
623
+ }
597
624
  }
598
625
  catch (failure) {
599
- if (simulationFailureKind(failure) === "abort")
626
+ if (upfrontFundsFailure(failure))
627
+ return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "affordability", reason: "Native balance cannot cover the sequential sizing fee envelopes" };
628
+ const kind = simulationFailureKind(failure);
629
+ if (kind === "abort")
600
630
  throw failure;
601
631
  assertCurrent();
602
- if (options.fork === undefined || limits.protocol === "nitro") {
603
- await assertLifecycleBlock(client, block, planned.chainId);
632
+ if (options.fork === undefined || limits.protocol === "nitro" || kind !== "source") {
604
633
  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."}` };
605
634
  }
606
635
  backend = "controlled-fork";
607
- measured = await simulateForkPass(options.fork, planned, block, transactions, discoveryGas, gasPrice, true, limits.headroomBps);
636
+ measured = await simulateForkPass(options.fork, planned, block, transactions, sizingGas, gasPrice, true, limits.headroomBps);
608
637
  }
609
638
  assertCurrent();
610
639
  const failedIndex = measured.findIndex((step) => !step.success);
640
+ const refusal = measured[failedIndex];
641
+ if (retriedForFunds && refusal?.failureCategory === "capacity" && refusal.gasLimit !== undefined && refusal.gasLimit < limits.executionGasCeiling) {
642
+ return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: measured,
643
+ failedTransactionId: refusal.transactionId, failureCategory: "affordability", reason: "Native balance does not provide the sequential sizing margin; an exact transaction envelope was not proved" };
644
+ }
611
645
  if (failedIndex >= 0 && limits.maxSimulationGas !== undefined &&
612
646
  measured.reduce((gas, step) => gas + (step.gasUsed ?? 0n), 0n) * 100n >= limits.maxSimulationGas * 95n) {
613
647
  // The provider exhausted its aggregate used-gas budget, not the contract.
@@ -616,100 +650,89 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
616
650
  transactions = transactions.slice(0, failedIndex);
617
651
  base.deferredTransactionIds = allTransactions.slice(failedIndex).map((transaction) => transaction.id);
618
652
  if (transactions.length === 0) {
619
- await assertLifecycleBlock(client, block, planned.chainId);
620
653
  return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "source", reason: "RPC aggregate simulation gas cap left no successful prefix to prove" };
621
654
  }
622
655
  }
623
656
  const failed = measured.find((step) => !step.success);
624
- if (failed !== undefined) {
625
- await assertLifecycleBlock(client, block, planned.chainId);
657
+ if (diagnostic && failed !== undefined)
626
658
  return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId, failureCategory: failed.failureCategory ?? "opaque", capacityConstraint: failed.failureCategory === "capacity" ? "compute" : undefined };
627
- }
628
659
  if (diagnostic) {
629
- await assertLifecycleBlock(client, block, planned.chainId);
630
660
  return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
631
661
  }
632
662
  }
633
- const gasLimits = nextEnvelope === undefined ? measured.map((step, index) => {
634
- const gross = step.gasRequired ?? step.gasUsed;
635
- const reserve = transactions[index]?.kind === "activate" || transactions[index]?.kind === "atomic" ? hmmV2ForwardingReserve(planned.plan) : 0n;
636
- const desired = gross + reserve > buffer(gross) ? gross + reserve : buffer(gross);
637
- const available = discoveryGas[index] - (posterGas[index] ?? 0n);
638
- return (desired < available ? desired : available) + (posterGas[index] ?? 0n);
663
+ const sizedTransactions = measured.length === 0 ? transactions : transactions.slice(0, measured.length);
664
+ const minimumGas = sizedTransactions.map((transaction, index) => {
665
+ return nextEnvelope !== undefined ? 0n : hasSizing ? sizing.get(transaction.id) : measured[index].gasRequired ?? measured[index].gasUsed;
666
+ });
667
+ const gasLimits = nextEnvelope === undefined ? sizedTransactions.map((transaction, index) => {
668
+ // Suffix budgets prove feasibility, not a future transaction's fee estimate.
669
+ if (index > 0)
670
+ return discoveryGas[index];
671
+ const gross = minimumGas[index];
672
+ const reserve = transaction.kind === "activate" || transaction.kind === "atomic" ? hmmV2ForwardingReserve(planned.plan) : 0n;
673
+ const buffered = gross + reserve > buffer(gross) ? gross + reserve : buffer(gross);
674
+ const desired = buffered + (posterGas[0] ?? 0n);
675
+ return desired < discoveryGas[index] ? desired : discoveryGas[index];
639
676
  }) : [nextEnvelope.gas];
677
+ if (gasLimits[0] * gasPrice + transactions[0].value >= 1n << 256n)
678
+ throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
679
+ const validate = nextEnvelope !== undefined || measured.every((step) => step.success);
680
+ if (nextEnvelope === undefined) {
681
+ const value = transactions.reduce((total, transaction) => total + transaction.value, 0n);
682
+ let available = balance - value;
683
+ let suffixMinimum = minimumGas.slice(1).reduce((total, gas) => total + gas, 0n);
684
+ if (gasPrice !== 0n)
685
+ available /= gasPrice;
686
+ for (let index = 0; index < gasLimits.length && gasPrice !== 0n; index += 1) {
687
+ const minimum = minimumGas[index];
688
+ if (index > 0)
689
+ suffixMinimum -= minimum;
690
+ const budget = index === 0 ? available - suffixMinimum : minimum + (available - minimum - suffixMinimum) / BigInt(gasLimits.length - index);
691
+ if (gasLimits[index] > budget)
692
+ gasLimits[index] = budget;
693
+ available -= gasLimits[index];
694
+ }
695
+ if (balance < value || gasLimits.some((gas, index) => gas <= 0n && minimumGas[index] > 0n))
696
+ return { ...base, executionProof: "unavailable", backend, admitted: false, steps: measured, failureCategory: "affordability", reason: "Creator native balance cannot fund transaction value and execution fee envelopes" };
697
+ }
640
698
  const exceeds = gasLimits.findIndex((gas, index) => gas <= 0n || gas >= 1n << 64n || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling ||
641
- nextEnvelope === undefined && (measured[index].gasRequired ?? measured[index].gasUsed) > gas - (posterGas[index] ?? 0n) ||
699
+ nextEnvelope === undefined && !hasSizing && (measured[index].gasRequired ?? measured[index].gasUsed) > gas ||
642
700
  limits.protocol === "evm" && gas > limits.executionGasCeiling);
643
701
  if (exceeds >= 0) {
644
- await assertLifecycleBlock(client, block, planned.chainId);
645
702
  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 };
646
703
  }
647
- let verifiedSteps = [];
648
- let admittedGasLimits = gasLimits;
649
- let replayError;
650
- const replay = (gas) => backend === "eth_simulateV1"
651
- ? simulateRpcPass(client, planned, block, transactions, gas, limits, gasPrice, true)
652
- : simulateForkPass(options.fork, planned, block, transactions, gas, gasPrice, false, limits.headroomBps);
653
- try {
654
- verifiedSteps = await lifecycleStage(client, "simulation.replay", () => replay(gasLimits));
655
- }
656
- catch (failure) {
657
- const kind = simulationFailureKind(failure);
658
- if (kind === "abort")
659
- throw failure;
660
- assertCurrent();
661
- if (kind !== "out-of-gas" || backend !== "eth_simulateV1" || nextEnvelope !== undefined) {
662
- await assertLifecycleBlock(client, block, planned.chainId);
663
- 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)}` };
664
- }
665
- replayError = lifecycleFailureReason(failure);
666
- }
667
- assertCurrent();
668
- let finalFailure = verifiedSteps.find((step) => !step.success);
669
- const failedIndex = verifiedSteps.findIndex((step) => !step.success);
670
- const rescue = nextEnvelope === undefined && backend === "eth_simulateV1" &&
671
- (replayError !== undefined && discoveryGas.some((gas, index) => index < transactions.length && gas > gasLimits[index]) ||
672
- finalFailure !== undefined && looksGasExhausted(finalFailure, gasLimits[failedIndex]) && discoveryGas[failedIndex] > gasLimits[failedIndex]);
673
- // One legal-envelope rescue covers thrown/returned genuine OOG. Never retry
674
- // semantic errors, switch replay backends, or enlarge a held immediate envelope.
675
- if (rescue) {
676
- assertCurrent();
704
+ // Size first, then prove and return this exact executable envelope. Diagnostic
705
+ // gas budgets and caller-held proposals are never execution admission.
706
+ let verifiedSteps = measured;
707
+ if (validate) {
677
708
  try {
678
- admittedGasLimits = discoveryGas.slice(0, transactions.length);
679
- verifiedSteps = await lifecycleStage(client, "simulation.envelope", () => replay(admittedGasLimits));
680
- finalFailure = verifiedSteps.find((step) => !step.success);
681
- replayError = undefined;
709
+ verifiedSteps = await lifecycleStage(client, "simulation.measure", () => backend === "eth_simulateV1"
710
+ ? simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true)
711
+ : simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice, false, limits.headroomBps));
682
712
  }
683
713
  catch (failure) {
684
- const kind = simulationFailureKind(failure);
685
- if (kind === "abort")
714
+ if (simulationFailureKind(failure) === "abort")
686
715
  throw failure;
687
716
  assertCurrent();
688
- await assertLifecycleBlock(client, block, planned.chainId);
689
- if (kind !== "out-of-gas")
690
- 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)}` };
691
- return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${lifecycleFailureReason(failure)}` };
717
+ return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [], reason: `Exact transaction simulation unavailable: ${lifecycleFailureReason(failure)}` };
692
718
  }
693
719
  }
694
- if (replayError !== undefined) {
695
- await assertLifecycleBlock(client, block, planned.chainId);
696
- return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling failed: ${replayError}` };
697
- }
698
720
  assertCurrent();
721
+ const finalFailure = verifiedSteps.find((step) => !step.success);
699
722
  const estimates = [];
700
- // A definitive returned replay failure already refuses execution. Retain its
701
- // fee observations without inventing a balance or doing redundant payer work.
702
- let remainingBalance = finalFailure === undefined
703
- ? rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance")
704
- : undefined;
723
+ let remainingBalance = finalFailure === undefined ? balance : undefined;
705
724
  let affordabilityFailure;
706
725
  const feeContext = configuration.estimateDataFee === undefined || sourceClient === client ? context : { ...context, client: sourceClient };
707
726
  for (let index = 0; index < verifiedSteps.length; index += 1) {
708
727
  const step = verifiedSteps[index];
709
728
  const transaction = transactions[index];
710
- const gasLimit = admittedGasLimits[index];
729
+ const gasLimit = gasLimits[index];
711
730
  if (step === undefined || transaction === undefined || gasLimit === undefined)
712
731
  throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing verified fee context");
732
+ if (index > 0) {
733
+ estimates.push({ ...step, computeGasRequired: minimumGas[index], provisional: true });
734
+ continue;
735
+ }
713
736
  const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit, gasPrice }, feeContext);
714
737
  if (dataFee !== undefined && (typeof dataFee !== "bigint" || dataFee < 0n || dataFee >= 1n << 256n))
715
738
  throw new LifecyclePlanningError("INVALID_DATA_FEE", "Data-fee oracle must return a nonnegative uint256 bigint amount");
@@ -724,9 +747,8 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
724
747
  remainingBalance -= required;
725
748
  }
726
749
  const poster = posterGas[index];
727
- 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" } });
750
+ estimates.push({ ...step, computeGasRequired: nextEnvelope === undefined ? minimumGas[index] : undefined, estimate: { gasUsed: step.gasUsed ?? 0n, gasLimit, gasPrice, executionFee, dataFee, posterGas: poster, posterFee: poster === undefined ? undefined : poster * gasPrice, dataFeeIncludedInGas: limits.protocol === "nitro", totalFee: dataFee === undefined ? undefined : executionFee + dataFee, feeConfidence: dataFee === undefined ? "execution-only" : "execution-and-data" } });
728
751
  }
729
- await assertLifecycleBlock(client, block, planned.chainId);
730
752
  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 };
731
753
  }
732
754
  //# sourceMappingURL=simulation.js.map