@black-market/sdk 0.14.1 → 0.15.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 (68) hide show
  1. package/README.md +4 -3
  2. package/dist/lifecycle/authors.js +2 -2
  3. package/dist/lifecycle/authors.js.map +1 -1
  4. package/dist/lifecycle/gas-profile.d.ts +35 -0
  5. package/dist/lifecycle/gas-profile.d.ts.map +1 -0
  6. package/dist/lifecycle/gas-profile.js +183 -0
  7. package/dist/lifecycle/gas-profile.js.map +1 -0
  8. package/dist/lifecycle/index.d.ts +3 -1
  9. package/dist/lifecycle/index.d.ts.map +1 -1
  10. package/dist/lifecycle/index.js +3 -1
  11. package/dist/lifecycle/index.js.map +1 -1
  12. package/dist/lifecycle/opening-buys.d.ts +13 -0
  13. package/dist/lifecycle/opening-buys.d.ts.map +1 -0
  14. package/dist/lifecycle/opening-buys.js +171 -0
  15. package/dist/lifecycle/opening-buys.js.map +1 -0
  16. package/dist/lifecycle/planner.d.ts +2 -2
  17. package/dist/lifecycle/planner.d.ts.map +1 -1
  18. package/dist/lifecycle/planner.js +178 -163
  19. package/dist/lifecycle/planner.js.map +1 -1
  20. package/dist/lifecycle/presets.d.ts +3 -0
  21. package/dist/lifecycle/presets.d.ts.map +1 -1
  22. package/dist/lifecycle/presets.js +3 -0
  23. package/dist/lifecycle/presets.js.map +1 -1
  24. package/dist/lifecycle/progress.d.ts +1 -1
  25. package/dist/lifecycle/progress.d.ts.map +1 -1
  26. package/dist/lifecycle/progress.js +21 -20
  27. package/dist/lifecycle/progress.js.map +1 -1
  28. package/dist/lifecycle/rpc.d.ts +24 -19
  29. package/dist/lifecycle/rpc.d.ts.map +1 -1
  30. package/dist/lifecycle/rpc.js +58 -50
  31. package/dist/lifecycle/rpc.js.map +1 -1
  32. package/dist/lifecycle/simulation.d.ts +3 -4
  33. package/dist/lifecycle/simulation.d.ts.map +1 -1
  34. package/dist/lifecycle/simulation.js +98 -247
  35. package/dist/lifecycle/simulation.js.map +1 -1
  36. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  37. package/dist/lifecycle/snapshot.js +1 -2
  38. package/dist/lifecycle/snapshot.js.map +1 -1
  39. package/dist/lifecycle/transactions.d.ts +19 -3
  40. package/dist/lifecycle/transactions.d.ts.map +1 -1
  41. package/dist/lifecycle/transactions.js +25 -20
  42. package/dist/lifecycle/transactions.js.map +1 -1
  43. package/dist/lifecycle/types.d.ts +53 -45
  44. package/dist/lifecycle/types.d.ts.map +1 -1
  45. package/dist/lifecycle/types.js.map +1 -1
  46. package/dist/lifecycle/ufg.d.ts.map +1 -1
  47. package/dist/lifecycle/ufg.js +3 -7
  48. package/dist/lifecycle/ufg.js.map +1 -1
  49. package/docs/launch-lifecycle.md +67 -78
  50. package/docs/lifecycle-gas-profiles.md +90 -0
  51. package/docs/reference.md +43 -45
  52. package/examples/README.md +11 -8
  53. package/examples/launch-example.ts +35 -25
  54. package/examples/launch-lifecycle-smoke.ts +5 -3
  55. package/package.json +1 -1
  56. package/src/lifecycle/authors.ts +2 -2
  57. package/src/lifecycle/gas-profile.ts +175 -0
  58. package/src/lifecycle/index.ts +3 -1
  59. package/src/lifecycle/opening-buys.ts +157 -0
  60. package/src/lifecycle/planner.ts +164 -151
  61. package/src/lifecycle/presets.ts +5 -0
  62. package/src/lifecycle/progress.ts +19 -20
  63. package/src/lifecycle/rpc.ts +56 -44
  64. package/src/lifecycle/simulation.ts +85 -231
  65. package/src/lifecycle/snapshot.ts +1 -2
  66. package/src/lifecycle/transactions.ts +33 -21
  67. package/src/lifecycle/types.ts +52 -42
  68. package/src/lifecycle/ufg.ts +3 -7
@@ -1,11 +1,9 @@
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 { assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleRpc, lifecycleSessionRead, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readLifecycleChainId, readLifecycleGasPrice, readNitroPosterGas, registerLifecycleFinalCheck, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity } from "./rpc.js";
4
+ import { assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleRpc, lifecycleSessionRead, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroPosterGasBound, readLifecycleBlock, readLifecycleChainId, readLifecycleGasPrice, readNitroPrices, registerLifecycleFinalCheck, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
5
5
  import { LifecyclePlanningError } from "./types.js";
6
6
  import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion } from "./markets.js";
7
- import { hmmV2ForwardingReserve } from "./presets.js";
8
- import { transactionGasEstimate } from "./transactions.js";
9
7
  import { hashUfgLaunchConfig } from "./ufg-config.js";
10
8
  function committedPositionCount(plan) {
11
9
  let count = 0;
@@ -85,12 +83,10 @@ function checkCondition(condition, data, planned) {
85
83
  /** Native Nitro eth_simulateV1 executes commit-mode ArbOS charging, including its
86
84
  * compute hold after the actual poster charge. A generic EVM/fork is not this proof. */
87
85
  async function proveNitroBackend(client, planned, block, limits, gasPrice) {
88
- if (limits.arbOSVersion === undefined || limits.maxTxComputeGas === undefined || limits.maxBlockComputeGas === undefined)
89
- throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Pinned Nitro metering context is incomplete");
86
+ if (limits.arbOSVersion === undefined)
87
+ throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Pinned Nitro version is unavailable");
90
88
  const probes = [
91
89
  { to: nitroArbSys, data: encodeFunctionData({ abi: nitroArbSysAbi, functionName: "arbOSVersion" }), expected: limits.arbOSVersion + 55n },
92
- { to: nitroGasInfo, data: encodeFunctionData({ abi: nitroGasInfoAbi, functionName: "getMaxTxGasLimit" }), expected: limits.maxTxComputeGas },
93
- { to: nitroGasInfo, data: encodeFunctionData({ abi: nitroGasInfoAbi, functionName: "getMaxBlockGasLimit" }), expected: limits.maxBlockComputeGas },
94
90
  ];
95
91
  const gas = limits.executionGasCeiling < 100000n ? limits.executionGasCeiling : 100000n;
96
92
  if (limits.maxSimulationGas !== undefined && gas * BigInt(probes.length) > limits.maxSimulationGas)
@@ -109,12 +105,12 @@ async function proveNitroBackend(client, planned, block, limits, gasPrice) {
109
105
  throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Native simulation omitted the pinned ArbOS metering probe");
110
106
  const simulated = rpcObject(result[0], "Nitro metering probe block");
111
107
  if (!Array.isArray(simulated.calls) || simulated.calls.length !== probes.length)
112
- throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Native simulation omitted ArbOS version or compute getters");
108
+ throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Native simulation omitted the ArbOS version");
113
109
  for (let index = 0; index < probes.length; index += 1) {
114
110
  const call = rpcObject(simulated.calls[index], "Nitro metering probe");
115
111
  const data = rpcHex(call.returnData, "simulated Nitro metering result");
116
112
  if (rpcQuantity(call.status, "Nitro metering probe status") !== 1n || rpcQuantity(call.gasUsed, "Nitro metering probe gas") > gas || data.length !== 66 || BigInt(data) !== probes[index]?.expected) {
117
- throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Simulated ArbOS version/compute getters differ from the exact pinned chain context");
113
+ throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Simulated ArbOS version differs from the exact pinned chain context");
118
114
  }
119
115
  }
120
116
  }
@@ -126,7 +122,7 @@ async function simulateRpcPass(client, planned, block, transactions, gasLimits,
126
122
  // parent gasLimit are set.
127
123
  const blockStateCalls = transactions.map((transaction, index) => ({
128
124
  blockOverrides: { number: toHex(block.number + BigInt(index + 1)), time: toHex(block.timestamp + BigInt(index + 1)), gasLimit: toHex(block.gasLimit) },
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) }) }],
125
+ calls: [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gasLimits[index] ?? limits.executionGasCeiling), gasPrice: toHex(gasPrice) }],
130
126
  }));
131
127
  const result = await lifecycleRpc(client, "eth_simulateV1", [{ blockStateCalls, validation, traceTransfers: false, returnFullTransactions: false }, toHex(block.number)]);
132
128
  if (!Array.isArray(result) || result.length !== transactions.length)
@@ -239,7 +235,7 @@ async function waitForkReceipt(client, transactionHash, timeoutMs) {
239
235
  }
240
236
  throw new LifecyclePlanningError("FORK_RECEIPT_TIMEOUT", "The disposable fork did not mine its simulation transaction");
241
237
  }
242
- async function simulateForkPass(fork, planned, block, transactions, gasLimits, gasPrice, measure, headroomBps) {
238
+ async function simulateForkPass(fork, planned, block, transactions, gasLimits, gasPrice) {
243
239
  if (fork.isolation !== "disposable" || fork.allowTransactions !== true || !fork.sourceRpcUrl || !fork.forkRpcUrl)
244
240
  throw new LifecyclePlanningError("UNSAFE_FORK", "Actual simulation transactions require an explicitly distinct disposable controlled fork");
245
241
  const sourceUrl = new URL(fork.sourceRpcUrl);
@@ -256,7 +252,7 @@ async function simulateForkPass(fork, planned, block, transactions, gasLimits, g
256
252
  await lifecycleRpc(fork.client, fork.resetMethod ?? "anvil_reset", [{ forking: { jsonRpcUrl: fork.sourceRpcUrl, blockNumber: Number(block.number) } }]);
257
253
  forkBlock = await readLifecycleBlock(fork.client);
258
254
  }
259
- const forkChain = rpcQuantity(await lifecycleRpc(fork.client, "eth_chainId"), "fork chain ID");
255
+ const forkChain = await readLifecycleChainId(fork.client);
260
256
  if (forkChain !== planned.chainId || forkChain !== (fork.expectedChainId ?? planned.chainId) || forkBlock.hash.toLowerCase() !== block.hash.toLowerCase() || forkBlock.hash.toLowerCase() !== (fork.expectedBlockHash ?? block.hash).toLowerCase())
261
257
  throw new LifecyclePlanningError("FORK_STATE_MISMATCH", "Controlled fork must match the exact source chain and pinned latest block before simulation");
262
258
  const snapshot = rpcHex(await lifecycleRpc(fork.client, "evm_snapshot"), "fork snapshot");
@@ -269,19 +265,11 @@ async function simulateForkPass(fork, planned, block, transactions, gasLimits, g
269
265
  }
270
266
  for (let index = 0; index < transactions.length; index += 1) {
271
267
  const transaction = transactions[index];
272
- let gas = gasLimits[index];
273
- let gasRequired;
268
+ const gas = gasLimits[index];
274
269
  if (transaction === undefined || gas === undefined)
275
270
  throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing fork transaction/gas context");
276
271
  try {
277
- if (measure) {
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");
279
- gasRequired = estimated;
280
- const buffered = (estimated * BigInt(10000 + headroomBps) + 9999n) / 10000n;
281
- if (buffered < gas)
282
- gas = buffered;
283
- }
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");
272
+ 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");
285
273
  const receipt = await waitForkReceipt(fork.client, hash, fork.receiptTimeoutMs ?? 10_000);
286
274
  const gasUsed = rpcQuantity(receipt.gasUsed, "fork gas used");
287
275
  if (rpcQuantity(receipt.status, "fork transaction status") !== 1n) {
@@ -337,7 +325,7 @@ async function simulateForkPass(fork, planned, block, transactions, gasLimits, g
337
325
  const data = rpcHex(await lifecycleRpc(fork.client, "eth_call", [{ ...request, from: planned.account }, toHex(currentBlock.number)]), "fork postcondition");
338
326
  checkCondition(condition, data, planned);
339
327
  }
340
- steps.push({ transactionId: transaction.id, success: true, gasUsed, gasLimit: gas, gasRequired, returnData });
328
+ steps.push({ transactionId: transaction.id, success: true, gasUsed, gasLimit: gas, returnData });
341
329
  }
342
330
  catch (failure) {
343
331
  steps.push({ transactionId: transaction.id, success: false, gasLimit: gas, error: lifecycleFailureReason(failure) });
@@ -357,14 +345,14 @@ async function simulateForkPass(fork, planned, block, transactions, gasLimits, g
357
345
  }
358
346
  }
359
347
  }
360
- function observedFields(value) {
348
+ export function observedFields(value) {
361
349
  const keys = Object.keys(value);
362
350
  const values = keys.map((key) => value[key]);
363
351
  return () => keys.length === Object.keys(value).length && keys.every((key, index) => value[key] === values[index]);
364
352
  }
365
353
  /** Internal continuation owned by one SDK read invocation. Only its positive
366
354
  * capability proof is reused; every candidate executes its entire economic sequence. */
367
- export function prepareLaunchSimulation(options, block, hasTransactions = true, reviewedEnvelope, selectedGasPrice, sizing) {
355
+ export function prepareLaunchSimulation(options, block, hasTransactions = true, immediate = false) {
368
356
  const { client, planned, fork } = options;
369
357
  const signal = options.signal;
370
358
  if (signal?.aborted)
@@ -408,7 +396,7 @@ export function prepareLaunchSimulation(options, block, hasTransactions = true,
408
396
  resolveLifecycleLimits(context, policy),
409
397
  readLifecycleChainId(client),
410
398
  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,
399
+ hasTransactions ? readLifecycleGasPrice(client, block, planned.chainId) : undefined,
412
400
  ]));
413
401
  })().then((observations) => ({ observations }), (failure) => ({ failure }));
414
402
  // Refused inputs may discard this read-only preparation before it settles.
@@ -463,7 +451,7 @@ export function prepareLaunchSimulation(options, block, hasTransactions = true,
463
451
  await capabilityPending;
464
452
  assertCurrent();
465
453
  };
466
- const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, selectedGasPrice, sizing);
454
+ const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, immediate);
467
455
  assertCurrent();
468
456
  registerLifecycleFinalCheck(client, result.observations[0].assertCurrent);
469
457
  return simulation;
@@ -482,273 +470,136 @@ export function prepareLaunchSimulation(options, block, hasTransactions = true,
482
470
  close() { closed = true; capabilityProved = false; capabilityPending = undefined; } });
483
471
  return simulate;
484
472
  }
485
- export async function simulateLaunchTransactions(options, transactions, pinnedBlock, nextEnvelope, sizing) {
486
- // Standalone calls always produce fresh context, including live chain and fees.
473
+ export async function simulateLaunchTransactions(options, transactions, pinnedBlock, nextEnvelope) {
474
+ // Standalone calls own one fresh block/pricing context, not fresh chain guards.
487
475
  if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === lifecycleSourceClient(options.client))
488
476
  throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
489
- const block = pinnedBlock ?? await readLifecycleBlock(options.client);
490
- const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope, undefined, sizing);
491
- try {
492
- return await prepared(transactions, nextEnvelope);
493
- }
494
- finally {
495
- prepared.close();
496
- }
477
+ return withLifecycleReadClient(options.client, async (client) => {
478
+ const [block] = await Promise.all([pinnedBlock ?? readLifecycleBlock(client), readLifecycleChainId(client)]);
479
+ const prepared = prepareLaunchSimulation({ ...options, client }, block, transactions.length !== 0);
480
+ try {
481
+ return await prepared(transactions, nextEnvelope);
482
+ }
483
+ finally {
484
+ prepared.close();
485
+ }
486
+ });
497
487
  }
498
- async function simulateLaunchTransactionsWithContext(options, transactions, context, observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, selectedGasPrice, sizing) {
499
- const { client, planned } = options;
488
+ async function simulateLaunchTransactionsWithContext(options, transactions, context, observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, immediate = false) {
489
+ const { client, planned } = options, { block } = context;
500
490
  const sourceClient = lifecycleSourceClient(client);
501
- const { block } = context;
502
491
  const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = observations;
503
- const base = { confidence: "stateful", executionProof: "failed", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits, deferredTransactionIds: [] };
492
+ const base = { confidence: "stateful", executionProof: "failed", protocolFit: "unknown",
493
+ blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits, deferredTransactionIds: [] };
504
494
  if (chainId !== planned.chainId)
505
- throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
506
- const accountCode = rpcHex(accountCodeValue, "account code");
507
- if (accountCode !== "0x") {
495
+ throw new LifecyclePlanningError("CHAIN_MISMATCH", "Bound RPC chain differs from the committed launch");
496
+ if (rpcHex(accountCodeValue, "account code") !== "0x")
508
497
  return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
509
- }
510
498
  for (const transaction of transactions) {
511
499
  if (transaction.from.toLowerCase() !== planned.account.toLowerCase() || BigInt(transaction.chainId) !== planned.chainId)
512
500
  throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Simulation transaction differs from the committed chain/account");
513
- if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) {
501
+ if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes)
514
502
  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" };
515
- }
516
503
  }
517
- if (transactions.length === 0) {
504
+ if (transactions.length === 0)
518
505
  return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] };
519
- }
520
- if (nextEnvelope !== undefined && (transactions.length !== 1 || typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas <= 0n || nextEnvelope.gas >= 1n << 64n ||
506
+ const gasLimit = limits.executionGasCeiling;
507
+ if (nextEnvelope !== undefined && (transactions.length !== 1 || typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas !== gasLimit ||
521
508
  typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
522
- throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Reviewed replay requires one immediate transaction with exact uint64 gas and uint256 price");
509
+ throw new LifecyclePlanningError("STALE_REVIEWED_GAS", "Reviewed gas differs from the native transaction ceiling; review the launch again");
523
510
  }
524
- const allTransactions = transactions;
525
- if (limits.maxSimulationGas !== undefined && nextEnvelope === undefined) {
526
- let estimatedGas = 0n, count = 0;
527
- for (const transaction of transactions) {
528
- estimatedGas += transactionGasEstimate(planned.plan, transaction);
529
- if (estimatedGas > limits.maxSimulationGas)
530
- break;
531
- count += 1;
532
- }
533
- transactions = transactions.slice(0, count);
534
- base.deferredTransactionIds = allTransactions.slice(count).map((transaction) => transaction.id);
535
- if (count === 0) {
536
- const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
537
- const exceedsCompute = estimatedGas > computeCap;
538
- const gasInMillions = (gas) => {
539
- const tenths = (gas + 50000n) / 100000n;
540
- return `${tenths / 10n}.${tenths % 10n}`;
541
- };
542
- const atomic = allTransactions[0].kind === "atomic";
543
- const reason = exceedsCompute
544
- ? `${atomic ? "Atomic launch" : "Immediate transaction"} needs about ${gasInMillions(estimatedGas)}M gas, above the ${gasInMillions(computeCap)}M per-transaction limit${atomic ? "; use staged" : ""}`
545
- : "The immediate transaction estimate exceeds the RPC aggregate simulation gas cap; no prefix can be proved";
546
- return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: exceedsCompute ? "compute" : "simulation-gas", reason };
547
- }
548
- }
549
- const recommendedGasPrice = nextEnvelope === undefined ? typeof gasPriceValue === "bigint" ? gasPriceValue : rpcQuantity(gasPriceValue, "gas price") : nextEnvelope.gasPrice;
550
- // New recommendations cover lagging quotes; reviewed fees are replayed unchanged
551
- // under natural child fees, never refreshed or silently increased.
552
- const baseFeeEnvelope = (block.baseFeePerGas ?? 0n) * 2n;
553
- const gasPrice = nextEnvelope?.gasPrice ?? selectedGasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
554
- if (gasPrice >= 1n << 256n)
555
- throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
556
- const buffer = (gas) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
511
+ if (limits.transactionGasCeiling !== undefined && gasLimit > limits.transactionGasCeiling)
512
+ return { ...base, executionProof: "unavailable", protocolFit: "failed", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: "gas-envelope", reason: "Native transaction ceiling exceeds the supplied complete gas policy", failedTransactionId: transactions[0].id };
513
+ const recommendation = typeof gasPriceValue === "bigint" ? gasPriceValue : rpcQuantity(gasPriceValue, "gas price");
514
+ const baseFee = block.baseFeePerGas ?? 0n;
515
+ const requiredPrice = limits.protocol === "nitro" && recommendation > baseFee ? recommendation : baseFee;
516
+ const gasPrice = nextEnvelope?.gasPrice ?? (recommendation > baseFee * 2n ? recommendation : baseFee * 2n);
517
+ if (gasPrice >= 1n << 256n || transactions[0].value + gasLimit * gasPrice >= 1n << 256n)
518
+ throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and native-ceiling fee must fit uint256");
519
+ if (gasPrice < requiredPrice)
520
+ return { ...base, executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failedTransactionId: transactions[0].id, reason: "Reviewed gas price is below the pinned required fee" };
521
+ const balance = rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance");
522
+ if (transactions[0].value + gasLimit * gasPrice > balance)
523
+ return { ...base, executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "affordability", reason: "Creator native balance cannot reserve transaction value and the native gas ceiling" };
557
524
  const posterGas = [];
558
525
  if (limits.protocol === "nitro") {
559
526
  try {
560
- const [, estimates] = await Promise.all([
561
- proveBackend(limits, gasPrice),
562
- !diagnostic && nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster", async () => [buffer((await readNitroPosterGas(transactions[0], context)).posterGas)]) : [],
563
- ]);
564
- posterGas.push(...estimates);
527
+ const [, prices] = await Promise.all([proveBackend(limits, gasPrice), readNitroPrices(client, block)]);
528
+ for (const transaction of transactions)
529
+ posterGas.push(nitroPosterGasBound(transaction, prices).posterGas);
565
530
  }
566
531
  catch (failure) {
567
532
  if (simulationFailureKind(failure) === "abort")
568
533
  throw failure;
569
534
  assertCurrent();
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.` };
535
+ return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Native Nitro execution unavailable: ${lifecycleFailureReason(failure)}. Generic Anvil/Hardhat forks cannot prove ArbOS metering.` };
571
536
  }
572
537
  }
538
+ const gasLimits = transactions.map(() => gasLimit);
539
+ let backend = (immediate || nextEnvelope !== undefined) && limits.protocol === "evm" && options.fork !== undefined ? "controlled-fork" : "eth_simulateV1";
540
+ let verified;
573
541
  assertCurrent();
574
- const discoveryGas = transactions.map(() => {
575
- const total = limits.protocol === "nitro" ? limits.maxSimulationGas ?? block.gasLimit : limits.executionGasCeiling;
576
- const capped = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < total ? limits.transactionGasCeiling : total;
577
- if (capped <= 0n || capped >= 1n << 64n)
578
- throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Compute/poster envelope exceeds uint64 gas");
579
- return capped;
580
- });
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");
584
- let measured = [];
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();
604
- try {
542
+ try {
543
+ verified = await lifecycleStage(client, "simulation.measure", async () => {
605
544
  try {
606
- measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, sizingGas, limits, gasPrice, false));
545
+ return backend === "controlled-fork"
546
+ ? await simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice)
547
+ : await simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, !diagnostic);
607
548
  }
608
549
  catch (failure) {
609
- if (!upfrontFundsFailure(failure))
550
+ if (backend !== "eth_simulateV1" || limits.protocol !== "evm" || options.fork === undefined ||
551
+ upfrontFundsFailure(failure) || simulationFailureKind(failure) !== "source")
610
552
  throw failure;
611
553
  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));
554
+ backend = "controlled-fork";
555
+ return simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice);
623
556
  }
624
- }
625
- catch (failure) {
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")
630
- throw failure;
631
- assertCurrent();
632
- if (options.fork === undefined || limits.protocol === "nitro" || kind !== "source") {
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."}` };
634
- }
635
- backend = "controlled-fork";
636
- measured = await simulateForkPass(options.fork, planned, block, transactions, sizingGas, gasPrice, true, limits.headroomBps);
637
- }
638
- assertCurrent();
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
- }
645
- if (failedIndex >= 0 && limits.maxSimulationGas !== undefined &&
646
- measured.reduce((gas, step) => gas + (step.gasUsed ?? 0n), 0n) * 100n >= limits.maxSimulationGas * 95n) {
647
- // The provider exhausted its aggregate used-gas budget, not the contract.
648
- // Preserve only actual successful results; the failed call is deferred too.
649
- measured = measured.slice(0, failedIndex);
650
- transactions = transactions.slice(0, failedIndex);
651
- base.deferredTransactionIds = allTransactions.slice(failedIndex).map((transaction) => transaction.id);
652
- if (transactions.length === 0) {
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" };
654
- }
655
- }
656
- const failed = measured.find((step) => !step.success);
657
- if (diagnostic && failed !== undefined)
658
- return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId, failureCategory: failed.failureCategory ?? "opaque", capacityConstraint: failed.failureCategory === "capacity" ? "compute" : undefined };
659
- if (diagnostic) {
660
- return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
661
- }
662
- }
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];
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
- }
698
- const exceeds = gasLimits.findIndex((gas, index) => gas <= 0n || gas >= 1n << 64n || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling ||
699
- nextEnvelope === undefined && !hasSizing && (measured[index].gasRequired ?? measured[index].gasUsed) > gas ||
700
- limits.protocol === "evm" && gas > limits.executionGasCeiling);
701
- if (exceeds >= 0) {
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 };
557
+ });
703
558
  }
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) {
708
- try {
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));
712
- }
713
- catch (failure) {
714
- if (simulationFailureKind(failure) === "abort")
715
- throw failure;
716
- assertCurrent();
717
- return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [], reason: `Exact transaction simulation unavailable: ${lifecycleFailureReason(failure)}` };
718
- }
559
+ catch (failure) {
560
+ if (simulationFailureKind(failure) === "abort")
561
+ throw failure;
562
+ assertCurrent();
563
+ if (upfrontFundsFailure(failure))
564
+ return { ...base, backend, confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [], failureCategory: "affordability", reason: "Native balance cannot reserve the fixed-ceiling sequence fee envelopes" };
565
+ return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [], reason: `Exact transaction simulation unavailable: ${lifecycleFailureReason(failure)}` };
719
566
  }
720
567
  assertCurrent();
721
- const finalFailure = verifiedSteps.find((step) => !step.success);
568
+ if (limits.maxSimulationGas !== undefined && verified.reduce((total, step) => total + (step.gasUsed ?? 0n), 0n) > limits.maxSimulationGas) {
569
+ return { ...base, backend, confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [],
570
+ failureCategory: "source", capacityConstraint: "simulation-gas", deferredTransactionIds: transactions.map((transaction) => transaction.id),
571
+ reason: "Complete launch execution exceeds the supplied aggregate simulation gas budget" };
572
+ }
573
+ const failure = verified.find((step) => !step.success);
574
+ if (diagnostic)
575
+ return { ...base, backend, executionProof: "unavailable", admitted: false, steps: verified, reason: failure?.error, failedTransactionId: failure?.transactionId, failureCategory: failure?.failureCategory };
722
576
  const estimates = [];
723
- let remainingBalance = finalFailure === undefined ? balance : undefined;
724
577
  let affordabilityFailure;
725
578
  const feeContext = configuration.estimateDataFee === undefined || sourceClient === client ? context : { ...context, client: sourceClient };
726
- for (let index = 0; index < verifiedSteps.length; index += 1) {
727
- const step = verifiedSteps[index];
579
+ for (const [index, step] of verified.entries()) {
728
580
  const transaction = transactions[index];
729
- const gasLimit = gasLimits[index];
730
- if (step === undefined || transaction === undefined || gasLimit === undefined)
731
- throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing verified fee context");
732
- if (index > 0) {
733
- estimates.push({ ...step, computeGasRequired: minimumGas[index], provisional: true });
581
+ if (index > 0 || transaction.kind === "approve" || transaction.kind === "approve-reset") {
582
+ estimates.push({ ...step, ...(index > 0 ? { provisional: true } : {}) });
734
583
  continue;
735
584
  }
736
585
  const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit, gasPrice }, feeContext);
737
586
  if (dataFee !== undefined && (typeof dataFee !== "bigint" || dataFee < 0n || dataFee >= 1n << 256n))
738
587
  throw new LifecyclePlanningError("INVALID_DATA_FEE", "Data-fee oracle must return a nonnegative uint256 bigint amount");
739
- // Nitro gas includes poster charges. Never add the quoted poster fee again.
740
- const executionFee = gasLimit * gasPrice;
741
- const required = transaction.value + executionFee + (dataFee ?? 0n);
588
+ const executionFee = gasLimit * gasPrice, required = transaction.value + executionFee + (dataFee ?? 0n);
742
589
  if (required >= 1n << 256n)
743
590
  throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
744
- if (remainingBalance !== undefined) {
745
- if (required > remainingBalance && affordabilityFailure === undefined)
746
- affordabilityFailure = "Creator native balance cannot fund the remaining transaction value and execution/data fee envelopes";
747
- remainingBalance -= required;
748
- }
591
+ if (required > balance)
592
+ affordabilityFailure = "Creator native balance cannot reserve transaction value and the native-ceiling/data fee envelope";
749
593
  const poster = posterGas[index];
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" } });
594
+ estimates.push({ ...step, estimate: { gasUsed: step.gasUsed, gasLimit, gasPrice, executionFee, dataFee, posterGas: poster,
595
+ posterFee: poster === undefined ? undefined : poster * gasPrice, dataFeeIncludedInGas: limits.protocol === "nitro",
596
+ totalFee: dataFee === undefined ? undefined : executionFee + dataFee, feeConfidence: dataFee === undefined ? "execution-only" : "execution-and-data" } });
751
597
  }
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 };
598
+ const activationFailure = failure?.transactionId === "activate" && failure.failureCategory === "capacity";
599
+ return { ...base, backend, executionProof: failure === undefined ? "proved" : "failed", protocolFit: failure === undefined ? "proved" : "unknown",
600
+ admitted: failure === undefined && affordabilityFailure === undefined, steps: estimates,
601
+ reason: activationFailure ? "Too many positions or pools for one activation. Reduce them to fit the native transaction ceiling." : failure?.error ?? affordabilityFailure,
602
+ failedTransactionId: failure?.transactionId, failureCategory: failure?.failureCategory ?? (affordabilityFailure === undefined ? undefined : "affordability"),
603
+ capacityConstraint: activationFailure ? "activation" : failure?.failureCategory === "capacity" ? "compute" : undefined };
753
604
  }
754
605
  //# sourceMappingURL=simulation.js.map