@black-market/sdk 0.14.0 → 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 (73) 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/lens.d.ts +1 -1
  13. package/dist/lifecycle/lens.d.ts.map +1 -1
  14. package/dist/lifecycle/lens.js +1 -1
  15. package/dist/lifecycle/lens.js.map +1 -1
  16. package/dist/lifecycle/opening-buys.d.ts +13 -0
  17. package/dist/lifecycle/opening-buys.d.ts.map +1 -0
  18. package/dist/lifecycle/opening-buys.js +171 -0
  19. package/dist/lifecycle/opening-buys.js.map +1 -0
  20. package/dist/lifecycle/planner.d.ts +2 -2
  21. package/dist/lifecycle/planner.d.ts.map +1 -1
  22. package/dist/lifecycle/planner.js +178 -163
  23. package/dist/lifecycle/planner.js.map +1 -1
  24. package/dist/lifecycle/presets.d.ts +6 -1
  25. package/dist/lifecycle/presets.d.ts.map +1 -1
  26. package/dist/lifecycle/presets.js +7 -0
  27. package/dist/lifecycle/presets.js.map +1 -1
  28. package/dist/lifecycle/progress.d.ts +1 -1
  29. package/dist/lifecycle/progress.d.ts.map +1 -1
  30. package/dist/lifecycle/progress.js +21 -20
  31. package/dist/lifecycle/progress.js.map +1 -1
  32. package/dist/lifecycle/rpc.d.ts +24 -19
  33. package/dist/lifecycle/rpc.d.ts.map +1 -1
  34. package/dist/lifecycle/rpc.js +58 -50
  35. package/dist/lifecycle/rpc.js.map +1 -1
  36. package/dist/lifecycle/simulation.d.ts +3 -4
  37. package/dist/lifecycle/simulation.d.ts.map +1 -1
  38. package/dist/lifecycle/simulation.js +98 -247
  39. package/dist/lifecycle/simulation.js.map +1 -1
  40. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  41. package/dist/lifecycle/snapshot.js +1 -2
  42. package/dist/lifecycle/snapshot.js.map +1 -1
  43. package/dist/lifecycle/transactions.d.ts +19 -3
  44. package/dist/lifecycle/transactions.d.ts.map +1 -1
  45. package/dist/lifecycle/transactions.js +25 -20
  46. package/dist/lifecycle/transactions.js.map +1 -1
  47. package/dist/lifecycle/types.d.ts +53 -45
  48. package/dist/lifecycle/types.d.ts.map +1 -1
  49. package/dist/lifecycle/types.js.map +1 -1
  50. package/dist/lifecycle/ufg.d.ts.map +1 -1
  51. package/dist/lifecycle/ufg.js +3 -7
  52. package/dist/lifecycle/ufg.js.map +1 -1
  53. package/docs/launch-lifecycle.md +71 -82
  54. package/docs/lifecycle-gas-profiles.md +90 -0
  55. package/docs/reference.md +53 -51
  56. package/examples/README.md +13 -10
  57. package/examples/launch-example.ts +35 -25
  58. package/examples/launch-lifecycle-smoke.ts +5 -3
  59. package/package.json +1 -1
  60. package/src/lifecycle/authors.ts +2 -2
  61. package/src/lifecycle/gas-profile.ts +175 -0
  62. package/src/lifecycle/index.ts +3 -1
  63. package/src/lifecycle/lens.ts +1 -1
  64. package/src/lifecycle/opening-buys.ts +157 -0
  65. package/src/lifecycle/planner.ts +164 -151
  66. package/src/lifecycle/presets.ts +12 -1
  67. package/src/lifecycle/progress.ts +19 -20
  68. package/src/lifecycle/rpc.ts +56 -44
  69. package/src/lifecycle/simulation.ts +85 -231
  70. package/src/lifecycle/snapshot.ts +1 -2
  71. package/src/lifecycle/transactions.ts +33 -21
  72. package/src/lifecycle/types.ts +52 -42
  73. package/src/lifecycle/ufg.ts +3 -7
@@ -1,11 +1,9 @@
1
1
  import { decodeErrorResult, decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctionResult, parseAbi, toHex, type Hex } from "viem";
2
2
  import { fixedFeePoolHookV1Abi, launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
3
3
  import { hashLaunchIdentity, hashLaunchPlan, LifecyclePhase, type LaunchPlanV1, type LaunchProgressV1, type LaunchReceiptV1 } 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, type LifecycleLimitObservation } 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, type LifecycleLimitObservation } from "./rpc.js";
5
5
  import { LifecyclePlanningError, type ControlledLifecycleFork, type LifecycleBlock, type LifecycleLimitContext, type LifecyclePostcondition, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleSimulationStep, type LifecycleTransaction, type PlannedLaunch, type ResolvedLifecycleLimits, type SimulateLaunchPlanOptions } from "./types.js";
6
6
  import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion } from "./markets.js";
7
- import { hmmV2ForwardingReserve } from "./presets.js";
8
- import { transactionGasEstimate } from "./transactions.js";
9
7
  import { hashUfgLaunchConfig } from "./ufg-config.js";
10
8
 
11
9
  function committedPositionCount(plan: LaunchPlanV1): number {
@@ -80,11 +78,9 @@ function checkCondition(condition: LifecyclePostcondition, data: Hex, planned: P
80
78
  /** Native Nitro eth_simulateV1 executes commit-mode ArbOS charging, including its
81
79
  * compute hold after the actual poster charge. A generic EVM/fork is not this proof. */
82
80
  async function proveNitroBackend(client: LifecycleRpcClient, planned: PlannedLaunch, block: LifecycleBlock, limits: ResolvedLifecycleLimits, gasPrice: bigint): Promise<void> {
83
- if (limits.arbOSVersion === undefined || limits.maxTxComputeGas === undefined || limits.maxBlockComputeGas === undefined) throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Pinned Nitro metering context is incomplete");
81
+ if (limits.arbOSVersion === undefined) throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Pinned Nitro version is unavailable");
84
82
  const probes = [
85
83
  { to: nitroArbSys, data: encodeFunctionData({ abi: nitroArbSysAbi, functionName: "arbOSVersion" }), expected: limits.arbOSVersion + 55n },
86
- { to: nitroGasInfo, data: encodeFunctionData({ abi: nitroGasInfoAbi, functionName: "getMaxTxGasLimit" }), expected: limits.maxTxComputeGas },
87
- { to: nitroGasInfo, data: encodeFunctionData({ abi: nitroGasInfoAbi, functionName: "getMaxBlockGasLimit" }), expected: limits.maxBlockComputeGas },
88
84
  ];
89
85
  const gas = limits.executionGasCeiling < 100_000n ? limits.executionGasCeiling : 100_000n;
90
86
  if (limits.maxSimulationGas !== undefined && gas * BigInt(probes.length) > limits.maxSimulationGas) throw new LifecyclePlanningError("SIMULATION_RPC_CAP", "Nitro metering probe exceeds the supplied RPC aggregate simulation cap");
@@ -100,12 +96,12 @@ async function proveNitroBackend(client: LifecycleRpcClient, planned: PlannedLau
100
96
  }, toHex(block.number)]);
101
97
  if (!Array.isArray(result) || result.length !== 1) throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Native simulation omitted the pinned ArbOS metering probe");
102
98
  const simulated = rpcObject(result[0], "Nitro metering probe block");
103
- if (!Array.isArray(simulated.calls) || simulated.calls.length !== probes.length) throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Native simulation omitted ArbOS version or compute getters");
99
+ if (!Array.isArray(simulated.calls) || simulated.calls.length !== probes.length) throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Native simulation omitted the ArbOS version");
104
100
  for (let index = 0; index < probes.length; index += 1) {
105
101
  const call = rpcObject(simulated.calls[index], "Nitro metering probe");
106
102
  const data = rpcHex(call.returnData, "simulated Nitro metering result");
107
103
  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) {
108
- throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Simulated ArbOS version/compute getters differ from the exact pinned chain context");
104
+ throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Simulated ArbOS version differs from the exact pinned chain context");
109
105
  }
110
106
  }
111
107
  }
@@ -118,7 +114,7 @@ async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunc
118
114
  // parent gasLimit are set.
119
115
  const blockStateCalls = transactions.map((transaction, index) => ({
120
116
  blockOverrides: { number: toHex(block.number + BigInt(index + 1)), time: toHex(block.timestamp + BigInt(index + 1)), gasLimit: toHex(block.gasLimit) },
121
- 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) }) }],
117
+ calls: [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gasLimits[index] ?? limits.executionGasCeiling), gasPrice: toHex(gasPrice) }],
122
118
  }));
123
119
  const result = await lifecycleRpc(client, "eth_simulateV1", [{ blockStateCalls, validation, traceTransfers: false, returnFullTransactions: false }, toHex(block.number)]);
124
120
  if (!Array.isArray(result) || result.length !== transactions.length) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Sequential simulation omitted blocks");
@@ -208,7 +204,7 @@ async function waitForkReceipt(client: LifecycleRpcClient, transactionHash: Hex,
208
204
  }
209
205
  throw new LifecyclePlanningError("FORK_RECEIPT_TIMEOUT", "The disposable fork did not mine its simulation transaction");
210
206
  }
211
- async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedLaunch, block: LifecycleBlock, transactions: readonly LifecycleTransaction[], gasLimits: readonly bigint[], gasPrice: bigint, measure: boolean, headroomBps: number): Promise<LifecycleSimulationStep[]> {
207
+ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedLaunch, block: LifecycleBlock, transactions: readonly LifecycleTransaction[], gasLimits: readonly bigint[], gasPrice: bigint): Promise<LifecycleSimulationStep[]> {
212
208
  if (fork.isolation !== "disposable" || fork.allowTransactions !== true || !fork.sourceRpcUrl || !fork.forkRpcUrl) throw new LifecyclePlanningError("UNSAFE_FORK", "Actual simulation transactions require an explicitly distinct disposable controlled fork");
213
209
  const sourceUrl = new URL(fork.sourceRpcUrl);
214
210
  const forkUrl = new URL(fork.forkRpcUrl);
@@ -222,7 +218,7 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
222
218
  await lifecycleRpc(fork.client, fork.resetMethod ?? "anvil_reset", [{ forking: { jsonRpcUrl: fork.sourceRpcUrl, blockNumber: Number(block.number) } }]);
223
219
  forkBlock = await readLifecycleBlock(fork.client);
224
220
  }
225
- const forkChain = rpcQuantity(await lifecycleRpc(fork.client, "eth_chainId"), "fork chain ID");
221
+ const forkChain = await readLifecycleChainId(fork.client);
226
222
  if (forkChain !== planned.chainId || forkChain !== (fork.expectedChainId ?? planned.chainId) || forkBlock.hash.toLowerCase() !== block.hash.toLowerCase() || forkBlock.hash.toLowerCase() !== (fork.expectedBlockHash ?? block.hash).toLowerCase()) throw new LifecyclePlanningError("FORK_STATE_MISMATCH", "Controlled fork must match the exact source chain and pinned latest block before simulation");
227
223
  const snapshot = rpcHex(await lifecycleRpc(fork.client, "evm_snapshot"), "fork snapshot");
228
224
  const steps: LifecycleSimulationStep[] = [];
@@ -234,17 +230,10 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
234
230
  }
235
231
  for (let index = 0; index < transactions.length; index += 1) {
236
232
  const transaction = transactions[index];
237
- let gas = gasLimits[index];
238
- let gasRequired: bigint | undefined;
233
+ const gas = gasLimits[index];
239
234
  if (transaction === undefined || gas === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing fork transaction/gas context");
240
235
  try {
241
- if (measure) {
242
- 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");
243
- gasRequired = estimated;
244
- const buffered = (estimated * BigInt(10000 + headroomBps) + 9999n) / 10000n;
245
- if (buffered < gas) gas = buffered;
246
- }
247
- 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");
236
+ 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");
248
237
  const receipt = await waitForkReceipt(fork.client, hash, fork.receiptTimeoutMs ?? 10_000);
249
238
  const gasUsed = rpcQuantity(receipt.gasUsed, "fork gas used");
250
239
  if (rpcQuantity(receipt.status, "fork transaction status") !== 1n) {
@@ -288,7 +277,7 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
288
277
  const data = rpcHex(await lifecycleRpc(fork.client, "eth_call", [{ ...request, from: planned.account }, toHex(currentBlock.number)]), "fork postcondition");
289
278
  checkCondition(condition, data, planned);
290
279
  }
291
- steps.push({ transactionId: transaction.id, success: true, gasUsed, gasLimit: gas, gasRequired, returnData });
280
+ steps.push({ transactionId: transaction.id, success: true, gasUsed, gasLimit: gas, returnData });
292
281
  } catch (failure) {
293
282
  steps.push({ transactionId: transaction.id, success: false, gasLimit: gas, error: lifecycleFailureReason(failure) });
294
283
  break;
@@ -307,15 +296,13 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
307
296
  type SimulationObservations = [LifecycleLimitObservation, unknown, unknown, unknown];
308
297
  /** Internal, operation-local fee envelope for the already matched immediate step. */
309
298
  export type NextTransactionEnvelope = Readonly<{ gas: bigint; gasPrice: bigint }>;
310
- /** Compute-only sizing observations; native poster charges are never included. */
311
- export type GasSizingHints = ReadonlyMap<string, bigint>;
312
299
  export type PreparedLaunchSimulation = ((transactions: readonly LifecycleTransaction[], nextEnvelope?: NextTransactionEnvelope) => Promise<LifecycleSimulation>) & {
313
300
  /** Unadmitted diagnostic results only; final measurement validates exact calldata. */
314
301
  measure(transactions: readonly LifecycleTransaction[]): Promise<LifecycleSimulation>;
315
302
  close(): void;
316
303
  };
317
304
 
318
- function observedFields<T extends object>(value: T): () => boolean {
305
+ export function observedFields<T extends object>(value: T): () => boolean {
319
306
  const keys = Object.keys(value) as (keyof T)[];
320
307
  const values = keys.map((key) => value[key]);
321
308
  return () => keys.length === Object.keys(value).length && keys.every((key, index) => value[key] === values[index]);
@@ -323,7 +310,7 @@ function observedFields<T extends object>(value: T): () => boolean {
323
310
 
324
311
  /** Internal continuation owned by one SDK read invocation. Only its positive
325
312
  * capability proof is reused; every candidate executes its entire economic sequence. */
326
- export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true, reviewedEnvelope?: NextTransactionEnvelope, selectedGasPrice?: bigint, sizing?: GasSizingHints): PreparedLaunchSimulation {
313
+ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true, immediate = false): PreparedLaunchSimulation {
327
314
  const { client, planned, fork } = options;
328
315
  const signal = options.signal;
329
316
  if (signal?.aborted) throw Object.assign(new Error("Lifecycle simulation cancelled"), { name: "AbortError" });
@@ -362,7 +349,7 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
362
349
  resolveLifecycleLimits(context, policy),
363
350
  readLifecycleChainId(client),
364
351
  lifecycleSessionRead(client, `${planned.chainId}:account-code:${context.account.toLowerCase()}`, () => lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block)),
365
- hasTransactions && reviewedEnvelope === undefined ? selectedGasPrice ?? readLifecycleGasPrice(client) : undefined,
352
+ hasTransactions ? readLifecycleGasPrice(client, block, planned.chainId) : undefined,
366
353
  ]));
367
354
  })().then((observations) => ({ observations }), (failure: unknown) => ({ failure }));
368
355
  // Refused inputs may discard this read-only preparation before it settles.
@@ -418,7 +405,7 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
418
405
  await capabilityPending;
419
406
  assertCurrent();
420
407
  };
421
- const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, selectedGasPrice, sizing);
408
+ const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, immediate);
422
409
  assertCurrent();
423
410
  registerLifecycleFinalCheck(client, result.observations[0].assertCurrent);
424
411
  return simulation;
@@ -437,13 +424,15 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
437
424
  return simulate;
438
425
  }
439
426
 
440
- export async function simulateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], pinnedBlock?: LifecycleBlock, nextEnvelope?: NextTransactionEnvelope, sizing?: GasSizingHints): Promise<LifecycleSimulation> {
441
- // Standalone calls always produce fresh context, including live chain and fees.
427
+ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], pinnedBlock?: LifecycleBlock, nextEnvelope?: NextTransactionEnvelope): Promise<LifecycleSimulation> {
428
+ // Standalone calls own one fresh block/pricing context, not fresh chain guards.
442
429
  if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === lifecycleSourceClient(options.client)) throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
443
- const block = pinnedBlock ?? await readLifecycleBlock(options.client);
444
- const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope, undefined, sizing);
445
- try { return await prepared(transactions, nextEnvelope); }
446
- finally { prepared.close(); }
430
+ return withLifecycleReadClient(options.client, async (client) => {
431
+ const [block] = await Promise.all([pinnedBlock ?? readLifecycleBlock(client), readLifecycleChainId(client)]);
432
+ const prepared = prepareLaunchSimulation({ ...options, client }, block, transactions.length !== 0);
433
+ try { return await prepared(transactions, nextEnvelope); }
434
+ finally { prepared.close(); }
435
+ });
447
436
  }
448
437
 
449
438
  async function simulateLaunchTransactionsWithContext(
@@ -455,235 +444,100 @@ async function simulateLaunchTransactionsWithContext(
455
444
  assertCurrent: () => void,
456
445
  diagnostic: boolean,
457
446
  nextEnvelope?: NextTransactionEnvelope,
458
- selectedGasPrice?: bigint,
459
- sizing?: GasSizingHints,
447
+ immediate = false,
460
448
  ): Promise<LifecycleSimulation> {
461
- const { client, planned } = options;
449
+ const { client, planned } = options, { block } = context;
462
450
  const sourceClient = lifecycleSourceClient(client);
463
- const { block } = context;
464
451
  const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = observations;
465
- const base = { confidence: "stateful" as const, executionProof: "failed" as const, protocolFit: "unknown" as const, transportPreflight: "not-requested" as const, blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits, deferredTransactionIds: [] as string[] };
466
- if (chainId !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
467
- const accountCode = rpcHex(accountCodeValue, "account code");
468
- if (accountCode !== "0x") {
469
-
470
- return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
471
- }
452
+ const base = { confidence: "stateful" as const, executionProof: "failed" as const, protocolFit: "unknown" as const,
453
+ blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits, deferredTransactionIds: [] as string[] };
454
+ if (chainId !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Bound RPC chain differs from the committed launch");
455
+ if (rpcHex(accountCodeValue, "account code") !== "0x") return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
472
456
  for (const transaction of transactions) {
473
457
  if (transaction.from.toLowerCase() !== planned.account.toLowerCase() || BigInt(transaction.chainId) !== planned.chainId) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Simulation transaction differs from the committed chain/account");
474
- if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) {
475
-
476
- 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" };
477
- }
458
+ if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) 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" };
478
459
  }
479
- if (transactions.length === 0) { return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] }; }
480
- if (nextEnvelope !== undefined && (transactions.length !== 1 || typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas <= 0n || nextEnvelope.gas >= 1n << 64n ||
460
+ if (transactions.length === 0) return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] };
461
+ const gasLimit = limits.executionGasCeiling;
462
+ if (nextEnvelope !== undefined && (transactions.length !== 1 || typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas !== gasLimit ||
481
463
  typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
482
- throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Reviewed replay requires one immediate transaction with exact uint64 gas and uint256 price");
483
- }
484
- const allTransactions = transactions;
485
- if (limits.maxSimulationGas !== undefined && nextEnvelope === undefined) {
486
- let estimatedGas = 0n, count = 0;
487
- for (const transaction of transactions) {
488
- estimatedGas += transactionGasEstimate(planned.plan, transaction);
489
- if (estimatedGas > limits.maxSimulationGas) break;
490
- count += 1;
491
- }
492
- transactions = transactions.slice(0, count);
493
- base.deferredTransactionIds = allTransactions.slice(count).map((transaction) => transaction.id);
494
- if (count === 0) {
495
- const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
496
- const exceedsCompute = estimatedGas > computeCap;
497
- const gasInMillions = (gas: bigint) => {
498
- const tenths = (gas + 50_000n) / 100_000n;
499
- return `${tenths / 10n}.${tenths % 10n}`;
500
- };
501
- const atomic = allTransactions[0]!.kind === "atomic";
502
- const reason = exceedsCompute
503
- ? `${atomic ? "Atomic launch" : "Immediate transaction"} needs about ${gasInMillions(estimatedGas)}M gas, above the ${gasInMillions(computeCap)}M per-transaction limit${atomic ? "; use staged" : ""}`
504
- : "The immediate transaction estimate exceeds the RPC aggregate simulation gas cap; no prefix can be proved";
505
-
506
- return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: exceedsCompute ? "compute" : "simulation-gas", reason };
507
- }
464
+ throw new LifecyclePlanningError("STALE_REVIEWED_GAS", "Reviewed gas differs from the native transaction ceiling; review the launch again");
508
465
  }
509
- const recommendedGasPrice = nextEnvelope === undefined ? typeof gasPriceValue === "bigint" ? gasPriceValue : rpcQuantity(gasPriceValue, "gas price") : nextEnvelope.gasPrice;
510
- // New recommendations cover lagging quotes; reviewed fees are replayed unchanged
511
- // under natural child fees, never refreshed or silently increased.
512
- const baseFeeEnvelope = (block.baseFeePerGas ?? 0n) * 2n;
513
- const gasPrice = nextEnvelope?.gasPrice ?? selectedGasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
514
- if (gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
515
- const buffer = (gas: bigint) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
466
+ if (limits.transactionGasCeiling !== undefined && gasLimit > limits.transactionGasCeiling) 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 };
467
+ const recommendation = typeof gasPriceValue === "bigint" ? gasPriceValue : rpcQuantity(gasPriceValue, "gas price");
468
+ const baseFee = block.baseFeePerGas ?? 0n;
469
+ const requiredPrice = limits.protocol === "nitro" && recommendation > baseFee ? recommendation : baseFee;
470
+ const gasPrice = nextEnvelope?.gasPrice ?? (recommendation > baseFee * 2n ? recommendation : baseFee * 2n);
471
+ if (gasPrice >= 1n << 256n || transactions[0]!.value + gasLimit * gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and native-ceiling fee must fit uint256");
472
+ if (gasPrice < requiredPrice) return { ...base, executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failedTransactionId: transactions[0]!.id, reason: "Reviewed gas price is below the pinned required fee" };
473
+ const balance = rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance");
474
+ if (transactions[0]!.value + gasLimit * gasPrice > balance) return { ...base, executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "affordability", reason: "Creator native balance cannot reserve transaction value and the native gas ceiling" };
516
475
  const posterGas: bigint[] = [];
517
476
  if (limits.protocol === "nitro") {
518
477
  try {
519
- const [, estimates] = await Promise.all([
520
- proveBackend(limits, gasPrice),
521
- !diagnostic && nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster", async () => [buffer((await readNitroPosterGas(transactions[0]!, context)).posterGas)]) : [],
522
- ]);
523
- posterGas.push(...estimates);
478
+ const [, prices] = await Promise.all([proveBackend(limits, gasPrice), readNitroPrices(client, block)]);
479
+ for (const transaction of transactions) posterGas.push(nitroPosterGasBound(transaction, prices).posterGas);
524
480
  } catch (failure) {
525
481
  if (simulationFailureKind(failure) === "abort") throw failure;
526
482
  assertCurrent();
527
-
528
- 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.` };
483
+ 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.` };
529
484
  }
530
485
  }
486
+ const gasLimits = transactions.map(() => gasLimit);
487
+ let backend: "eth_simulateV1" | "controlled-fork" = (immediate || nextEnvelope !== undefined) && limits.protocol === "evm" && options.fork !== undefined ? "controlled-fork" : "eth_simulateV1";
488
+ let verified: LifecycleSimulationStep[];
531
489
  assertCurrent();
532
- const discoveryGas = transactions.map(() => {
533
- const total = limits.protocol === "nitro" ? limits.maxSimulationGas ?? block.gasLimit : limits.executionGasCeiling;
534
- const capped = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < total ? limits.transactionGasCeiling : total;
535
- if (capped <= 0n || capped >= 1n << 64n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Compute/poster envelope exceeds uint64 gas");
536
- return capped;
537
- });
538
- if (discoveryGas[0]! * gasPrice + transactions[0]!.value >= 1n << 256n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
539
- const balance = rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance");
540
- let measured: LifecycleSimulationStep[] = [];
541
- const hasSizing = !diagnostic && transactions.every((transaction) => sizing?.has(transaction.id));
542
- let backend: "eth_simulateV1" | "controlled-fork" = (nextEnvelope !== undefined || hasSizing) && limits.protocol === "evm" && options.fork !== undefined ? "controlled-fork" : "eth_simulateV1";
543
- if (nextEnvelope === undefined && !hasSizing) {
544
- // Nitro checks upfront funds even with validation:false. Bound discovery
545
- // budgets before dispatch, without overriding the payer's real balance.
546
- const value = transactions.reduce((total, transaction) => total + transaction.value, 0n);
547
- let available = balance - value;
548
- if (gasPrice !== 0n) available /= gasPrice;
549
- const sizingGas = discoveryGas.map((ceiling, index) => {
550
- // Upfront reservation is per call; unused gas is refunded before the next.
551
- const computeAndPoster = limits.executionGasCeiling + (posterGas[index] ?? 0n);
552
- const capped = computeAndPoster < ceiling ? computeAndPoster : ceiling;
553
- return gasPrice === 0n || capped < available ? capped : available;
554
- });
555
- let retriedForFunds = false;
556
- if (balance < value || sizingGas.some((gas) => gas <= 0n)) return { ...base, executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "affordability", reason: "Creator native balance cannot fund transaction value and sizing fee budgets" };
557
- assertCurrent();
558
- try {
490
+ try {
491
+ verified = await lifecycleStage(client, "simulation.measure", async () => {
559
492
  try {
560
- measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, sizingGas, limits, gasPrice, false));
493
+ return backend === "controlled-fork"
494
+ ? await simulateForkPass(options.fork!, planned, block, transactions, gasLimits, gasPrice)
495
+ : await simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, !diagnostic);
561
496
  } catch (failure) {
562
- if (!upfrontFundsFailure(failure)) throw failure;
497
+ if (backend !== "eth_simulateV1" || limits.protocol !== "evm" || options.fork === undefined ||
498
+ upfrontFundsFailure(failure) || simulationFailureKind(failure) !== "source") throw failure;
563
499
  assertCurrent();
564
- retriedForFunds = true;
565
- let remaining = available;
566
- const sharedGas = sizingGas.map((ceiling, index) => {
567
- if (gasPrice === 0n) return ceiling;
568
- const share = remaining / BigInt(transactions.length - index);
569
- const gas = ceiling < share ? ceiling : share;
570
- remaining -= gas;
571
- return gas;
572
- });
573
- measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, sharedGas, limits, gasPrice, false));
574
- }
575
- } catch (failure) {
576
- if (upfrontFundsFailure(failure)) return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "affordability", reason: "Native balance cannot cover the sequential sizing fee envelopes" };
577
- const kind = simulationFailureKind(failure);
578
- if (kind === "abort") throw failure;
579
- assertCurrent();
580
- if (options.fork === undefined || limits.protocol === "nitro" || kind !== "source") {
581
- 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."}` };
500
+ backend = "controlled-fork";
501
+ return simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice);
582
502
  }
583
- backend = "controlled-fork";
584
- measured = await simulateForkPass(options.fork, planned, block, transactions, sizingGas, gasPrice, true, limits.headroomBps);
585
- }
503
+ });
504
+ } catch (failure) {
505
+ if (simulationFailureKind(failure) === "abort") throw failure;
586
506
  assertCurrent();
587
- const failedIndex = measured.findIndex((step) => !step.success);
588
- const refusal = measured[failedIndex];
589
- if (retriedForFunds && refusal?.failureCategory === "capacity" && refusal.gasLimit !== undefined && refusal.gasLimit < limits.executionGasCeiling) {
590
- return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: measured,
591
- failedTransactionId: refusal.transactionId, failureCategory: "affordability", reason: "Native balance does not provide the sequential sizing margin; an exact transaction envelope was not proved" };
592
- }
593
- if (failedIndex >= 0 && limits.maxSimulationGas !== undefined &&
594
- measured.reduce((gas, step) => gas + (step.gasUsed ?? 0n), 0n) * 100n >= limits.maxSimulationGas * 95n) {
595
- // The provider exhausted its aggregate used-gas budget, not the contract.
596
- // Preserve only actual successful results; the failed call is deferred too.
597
- measured = measured.slice(0, failedIndex);
598
- transactions = transactions.slice(0, failedIndex);
599
- base.deferredTransactionIds = allTransactions.slice(failedIndex).map((transaction) => transaction.id);
600
- if (transactions.length === 0) {
601
-
602
- return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "source", reason: "RPC aggregate simulation gas cap left no successful prefix to prove" };
603
- }
604
- }
605
- const failed = measured.find((step) => !step.success);
606
- if (diagnostic && failed !== undefined) return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId, failureCategory: failed.failureCategory ?? "opaque", capacityConstraint: failed.failureCategory === "capacity" ? "compute" : undefined };
607
- if (diagnostic) {
608
-
609
- return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
610
- }
611
- }
612
- const sizedTransactions = measured.length === 0 ? transactions : transactions.slice(0, measured.length);
613
- const minimumGas = sizedTransactions.map((transaction, index) => {
614
- return nextEnvelope !== undefined ? 0n : hasSizing ? sizing!.get(transaction.id)! : measured[index]!.gasRequired ?? measured[index]!.gasUsed!;
615
- });
616
- const gasLimits = nextEnvelope === undefined ? sizedTransactions.map((transaction, index) => {
617
- // Suffix budgets prove feasibility, not a future transaction's fee estimate.
618
- if (index > 0) return discoveryGas[index]!;
619
- const gross = minimumGas[index]!;
620
- const reserve = transaction.kind === "activate" || transaction.kind === "atomic" ? hmmV2ForwardingReserve(planned.plan) : 0n;
621
- const buffered = gross + reserve > buffer(gross) ? gross + reserve : buffer(gross);
622
- const desired = buffered + (posterGas[0] ?? 0n);
623
- return desired < discoveryGas[index]! ? desired : discoveryGas[index]!;
624
- }) : [nextEnvelope.gas];
625
- if (gasLimits[0]! * gasPrice + transactions[0]!.value >= 1n << 256n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
626
- const validate = nextEnvelope !== undefined || measured.every((step) => step.success);
627
- if (nextEnvelope === undefined) {
628
- const value = transactions.reduce((total, transaction) => total + transaction.value, 0n);
629
- let available = balance - value;
630
- let suffixMinimum = minimumGas.slice(1).reduce((total, gas) => total + gas, 0n);
631
- if (gasPrice !== 0n) available /= gasPrice;
632
- for (let index = 0; index < gasLimits.length && gasPrice !== 0n; index += 1) {
633
- const minimum = minimumGas[index]!;
634
- if (index > 0) suffixMinimum -= minimum;
635
- const budget = index === 0 ? available - suffixMinimum : minimum + (available - minimum - suffixMinimum) / BigInt(gasLimits.length - index);
636
- if (gasLimits[index]! > budget) gasLimits[index] = budget;
637
- available -= gasLimits[index]!;
638
- }
639
- if (balance < value || gasLimits.some((gas, index) => gas <= 0n && minimumGas[index]! > 0n)) return { ...base, executionProof: "unavailable", backend, admitted: false, steps: measured, failureCategory: "affordability", reason: "Creator native balance cannot fund transaction value and execution fee envelopes" };
640
- }
641
- const exceeds = gasLimits.findIndex((gas, index) =>
642
- gas <= 0n || gas >= 1n << 64n || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling ||
643
- nextEnvelope === undefined && !hasSizing && (measured[index]!.gasRequired ?? measured[index]!.gasUsed!) > gas ||
644
- limits.protocol === "evm" && gas > limits.executionGasCeiling);
645
- if (exceeds >= 0) {
646
-
647
- 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 };
648
- }
649
- // Size first, then prove and return this exact executable envelope. Diagnostic
650
- // gas budgets and caller-held proposals are never execution admission.
651
- let verifiedSteps = measured;
652
- if (validate) {
653
- try {
654
- verifiedSteps = await lifecycleStage(client, "simulation.measure", () => backend === "eth_simulateV1"
655
- ? simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true)
656
- : simulateForkPass(options.fork!, planned, block, transactions, gasLimits, gasPrice, false, limits.headroomBps));
657
- } catch (failure) {
658
- if (simulationFailureKind(failure) === "abort") throw failure;
659
- assertCurrent();
660
- return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [], reason: `Exact transaction simulation unavailable: ${lifecycleFailureReason(failure)}` };
661
- }
507
+ if (upfrontFundsFailure(failure)) return { ...base, backend, confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [], failureCategory: "affordability", reason: "Native balance cannot reserve the fixed-ceiling sequence fee envelopes" };
508
+ return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [], reason: `Exact transaction simulation unavailable: ${lifecycleFailureReason(failure)}` };
662
509
  }
663
510
  assertCurrent();
664
- const finalFailure = verifiedSteps.find((step) => !step.success);
511
+ if (limits.maxSimulationGas !== undefined && verified.reduce((total, step) => total + (step.gasUsed ?? 0n), 0n) > limits.maxSimulationGas) {
512
+ return { ...base, backend, confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [],
513
+ failureCategory: "source", capacityConstraint: "simulation-gas", deferredTransactionIds: transactions.map((transaction) => transaction.id),
514
+ reason: "Complete launch execution exceeds the supplied aggregate simulation gas budget" };
515
+ }
516
+ const failure = verified.find((step) => !step.success);
517
+ if (diagnostic) return { ...base, backend, executionProof: "unavailable", admitted: false, steps: verified, reason: failure?.error, failedTransactionId: failure?.transactionId, failureCategory: failure?.failureCategory };
665
518
  const estimates: LifecycleSimulationStep[] = [];
666
- let remainingBalance = finalFailure === undefined ? balance : undefined;
667
519
  let affordabilityFailure: string | undefined;
668
520
  const feeContext = configuration.estimateDataFee === undefined || sourceClient === client ? context : { ...context, client: sourceClient };
669
- for (let index = 0; index < verifiedSteps.length; index += 1) {
670
- const step = verifiedSteps[index]; const transaction = transactions[index]; const gasLimit = gasLimits[index];
671
- if (step === undefined || transaction === undefined || gasLimit === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing verified fee context");
672
- if (index > 0) { estimates.push({ ...step, computeGasRequired: minimumGas[index], provisional: true }); continue; }
521
+ for (const [index, step] of verified.entries()) {
522
+ const transaction = transactions[index]!;
523
+ if (index > 0 || transaction.kind === "approve" || transaction.kind === "approve-reset") {
524
+ estimates.push({ ...step, ...(index > 0 ? { provisional: true } : {}) }); continue;
525
+ }
673
526
  const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit, gasPrice }, feeContext);
674
527
  if (dataFee !== undefined && (typeof dataFee !== "bigint" || dataFee < 0n || dataFee >= 1n << 256n)) throw new LifecyclePlanningError("INVALID_DATA_FEE", "Data-fee oracle must return a nonnegative uint256 bigint amount");
675
- // Nitro gas includes poster charges. Never add the quoted poster fee again.
676
- const executionFee = gasLimit * gasPrice;
677
- const required = transaction.value + executionFee + (dataFee ?? 0n);
528
+ const executionFee = gasLimit * gasPrice, required = transaction.value + executionFee + (dataFee ?? 0n);
678
529
  if (required >= 1n << 256n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
679
- if (remainingBalance !== undefined) {
680
- if (required > remainingBalance && affordabilityFailure === undefined) affordabilityFailure = "Creator native balance cannot fund the remaining transaction value and execution/data fee envelopes";
681
- remainingBalance -= required;
682
- }
530
+ if (required > balance) affordabilityFailure = "Creator native balance cannot reserve transaction value and the native-ceiling/data fee envelope";
683
531
  const poster = posterGas[index];
684
- 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" } });
532
+ estimates.push({ ...step, estimate: { gasUsed: step.gasUsed, gasLimit, gasPrice, executionFee, dataFee, posterGas: poster,
533
+ posterFee: poster === undefined ? undefined : poster * gasPrice, dataFeeIncludedInGas: limits.protocol === "nitro",
534
+ totalFee: dataFee === undefined ? undefined : executionFee + dataFee, feeConfidence: dataFee === undefined ? "execution-only" : "execution-and-data" } });
685
535
  }
686
-
687
- 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 };
536
+ const activationFailure = failure?.transactionId === "activate" && failure.failureCategory === "capacity";
537
+ return { ...base, backend, executionProof: failure === undefined ? "proved" : "failed", protocolFit: failure === undefined ? "proved" : "unknown",
538
+ admitted: failure === undefined && affordabilityFailure === undefined, steps: estimates,
539
+ reason: activationFailure ? "Too many positions or pools for one activation. Reduce them to fit the native transaction ceiling." : failure?.error ?? affordabilityFailure,
540
+ failedTransactionId: failure?.transactionId, failureCategory: failure?.failureCategory ?? (affordabilityFailure === undefined ? undefined : "affordability"),
541
+ capacityConstraint: activationFailure ? "activation" : failure?.failureCategory === "capacity" ? "compute" : undefined };
688
542
  }
689
543
 
@@ -37,8 +37,7 @@ export async function readLifecyclePlanningBlock(client: LifecycleRpcClient, pla
37
37
  }
38
38
  }
39
39
  add(nitroArbSys, nitroArbSysAbi, "arbOSVersion", []);
40
- add(nitroGasInfo, nitroGasInfoAbi, "getMaxTxGasLimit", []);
41
- add(nitroGasInfo, nitroGasInfoAbi, "getMaxBlockGasLimit", []);
40
+ add(nitroGasInfo, nitroGasInfoAbi, "getPricesInWei", []);
42
41
  await attestLifecycleMulticall(client, block);
43
42
  const calls = reads.map((read) => ({ target: read.to, allowFailure: true, callData: encodeFunctionData({ abi: read.abi, functionName: read.functionName, args: read.args }) }));
44
43
  const data = encodeFunctionData({ abi: multicall3Abi, functionName: "aggregate3", args: [calls] });
@@ -1,14 +1,15 @@
1
- import { encodeFunctionData } from "viem";
1
+ import { encodeFunctionData, type Address } from "viem";
2
2
  import { launchLifecycleAbi } from "./abi.js";
3
3
  import { LifecycleFundingKind, LifecycleMode, type LaunchPlanV1 } from "./schema.js";
4
4
  import { LifecyclePlanningError, type BuildLaunchTransactionsOptions, type LifecycleTransaction } from "./types.js";
5
5
  import { assertLifecycleTokenAllocation } from "./allocations.js";
6
- import { HMM_V2_PREPARE_GAS, hmmV2ForwardingReserve } from "./presets.js";
6
+ import { HMM_V2_PREPARE_GAS } from "./presets.js";
7
7
  import { isHmmProfile } from "./hmm.js";
8
+ import { ufgLaunchProxyV1Abi } from "./ufg-abi.js";
9
+ import { UFG_LAUNCH_PROXY_ADDRESS } from "./ufg-config.js";
10
+ import type { UfgLaunchConfig } from "./ufg-config.js";
8
11
 
9
12
  const OTHER_MARKET_PREPARE_GAS = 16_000_000n;
10
- const BEGIN_GAS = 9_500_000n;
11
- const ACTIVATE_MARKET_GAS = 6_000_000n;
12
13
 
13
14
  export function preparationGasEstimate(plan: LaunchPlanV1, first: number, count: number): bigint {
14
15
  let gas = 0n;
@@ -30,42 +31,53 @@ export function packPreparationBatches(plan: LaunchPlanV1, prepared: number, com
30
31
  return batches;
31
32
  }
32
33
 
33
- /** Estimates select RPC proof coverage only; measurement and exact replay size gas. */
34
- export function transactionGasEstimate(plan: LaunchPlanV1, transaction: LifecycleTransaction): bigint {
35
- switch (transaction.kind) {
36
- case "begin": return BEGIN_GAS;
37
- case "prepare": return preparationGasEstimate(plan, transaction.marketStart!, transaction.marketCount!);
38
- case "activate": return ACTIVATE_MARKET_GAS * BigInt(plan.markets.length) + hmmV2ForwardingReserve(plan);
39
- case "atomic": return BEGIN_GAS + preparationGasEstimate(plan, 0, plan.markets.length) + ACTIVATE_MARKET_GAS * BigInt(plan.markets.length) + hmmV2ForwardingReserve(plan);
40
- default: return 100_000n;
34
+ export type LifecycleCommand =
35
+ | { kind: "atomic" | "begin" | "activate" }
36
+ | { kind: "prepare"; first: number; count: number };
37
+ type LifecycleUfgRoute = { account: Address; config: UfgLaunchConfig };
38
+
39
+ /** Internal encoder shared by complete plans and the one-command continuation. */
40
+ export function buildLifecycleCommand(plan: LaunchPlanV1, command: LifecycleCommand, ufg?: LifecycleUfgRoute): LifecycleTransaction {
41
+ const abi = ufg === undefined ? launchLifecycleAbi : ufgLaunchProxyV1Abi;
42
+ const base = { chainId: Number(plan.chainId), from: ufg?.account ?? plan.creator, to: ufg === undefined ? plan.orchestrator : UFG_LAUNCH_PROXY_ADDRESS };
43
+ const value = command.kind === "atomic" || command.kind === "begin"
44
+ ? plan.funding.reduce((total, funding) => total + (funding.kind === LifecycleFundingKind.NativeWrap || funding.kind === LifecycleFundingKind.Swap && BigInt(funding.inputAsset) === 0n ? funding.inputAmount : 0n), 0n) : 0n;
45
+ switch (command.kind) {
46
+ case "atomic": return { ...base, id: "atomic", kind: "atomic", data: encodeFunctionData({ abi, functionName: "launchAtomic", args: ufg === undefined ? [plan] : [plan, ufg.config] }), value, dependencies: [], postconditions: [{ kind: "launch", phase: "Active", preparedMarkets: plan.markets.length }] };
47
+ case "begin": return { ...base, id: "begin", kind: "begin", data: encodeFunctionData({ abi, functionName: "beginLaunch", args: ufg === undefined ? [plan, LifecycleMode.Staged] : [plan, LifecycleMode.Staged, ufg.config] }), value, dependencies: [], postconditions: [{ kind: "launch", phase: "Preparing", preparedMarkets: 0 }] };
48
+ case "activate": return { ...base, id: "activate", kind: "activate", data: encodeFunctionData({ abi, functionName: "activateLaunch", args: [plan] }), value, dependencies: [], postconditions: [{ kind: "launch", phase: "Active", preparedMarkets: plan.markets.length }] };
49
+ case "prepare": return { ...base, id: `prepare:${command.first}:${command.count}`, kind: "prepare", data: encodeFunctionData({ abi, functionName: "prepareMarkets", args: [plan, command.first, command.count] }), value, dependencies: [], marketStart: command.first, marketCount: command.count,
50
+ postconditions: [{ kind: "launch", phase: command.first + command.count === plan.markets.length ? "Ready" : "Preparing", preparedMarkets: command.first + command.count }] };
41
51
  }
42
52
  }
43
53
 
44
54
  /** Pure unsigned launch commands. Every command carries the complete commitment;
45
55
  * preparation splits only empty markets, never positions, buys or public opening.
46
56
  * Allowance discovery, recovery and execution proof belong to the planner. */
47
- export function buildLaunchTransactions({ plan, mode, preparationBatches }: BuildLaunchTransactionsOptions): LifecycleTransaction[] {
57
+ export function buildLifecycleCommands({ plan, mode, preparationBatches }: BuildLaunchTransactionsOptions, ufg?: LifecycleUfgRoute): LifecycleTransaction[] {
48
58
  if (mode !== "atomic" && mode !== "staged") throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose atomic or staged explicitly; staged is never silently substituted");
49
59
  if (plan.chainId <= 0n || plan.chainId > BigInt(Number.MAX_SAFE_INTEGER)) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Chain ID must be a positive exact JavaScript integer");
50
60
  if (plan.markets.length === 0 || plan.markets.length > 16) throw new LifecyclePlanningError("INVALID_MARKET_COUNT", "A launch requires between one and sixteen markets");
51
61
  if (mode === "atomic" && preparationBatches !== undefined) throw new LifecyclePlanningError("INVALID_PREPARATION_BATCHES", "Atomic execution has no separate preparation batches");
52
62
  assertLifecycleTokenAllocation(plan);
53
- const nativeValue = plan.funding.reduce((total, funding) => total + (funding.kind === LifecycleFundingKind.NativeWrap || funding.kind === LifecycleFundingKind.Swap && BigInt(funding.inputAsset) === 0n ? funding.inputAmount : 0n), 0n);
54
- const base = { chainId: Number(plan.chainId), from: plan.creator, to: plan.orchestrator };
55
63
  const transactions: LifecycleTransaction[] = [];
56
64
  if (mode === "atomic") {
57
- transactions.push({ ...base, id: "atomic", kind: "atomic", data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "launchAtomic", args: [plan] }), value: nativeValue, dependencies: [], postconditions: [{ kind: "launch", phase: "Active", preparedMarkets: plan.markets.length }] });
65
+ transactions.push(buildLifecycleCommand(plan, { kind: "atomic" }, ufg));
58
66
  } else {
59
67
  const batches = preparationBatches ?? [plan.markets.length];
60
68
  if (batches.length === 0 || batches.some((count) => !Number.isSafeInteger(count) || count <= 0) || batches.reduce((total, count) => total + count, 0) !== plan.markets.length) throw new LifecyclePlanningError("INVALID_PREPARATION_BATCHES", "Positive ordered preparation counts must cover every market exactly");
61
- transactions.push({ ...base, id: "begin", kind: "begin", data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "beginLaunch", args: [plan, LifecycleMode.Staged] }), value: nativeValue, dependencies: [], postconditions: [{ kind: "launch", phase: "Preparing", preparedMarkets: 0 }] });
69
+ transactions.push(buildLifecycleCommand(plan, { kind: "begin" }, ufg));
62
70
  let first = 0;
63
71
  for (const count of batches) {
64
- const completed = first + count;
65
- transactions.push({ ...base, id: `prepare:${first}:${count}`, kind: "prepare", data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "prepareMarkets", args: [plan, first, count] }), value: 0n, dependencies: [], marketStart: first, marketCount: count, postconditions: [{ kind: "launch", phase: completed === plan.markets.length ? "Ready" : "Preparing", preparedMarkets: completed }] });
66
- first = completed;
72
+ transactions.push(buildLifecycleCommand(plan, { kind: "prepare", first, count }, ufg));
73
+ first += count;
67
74
  }
68
- transactions.push({ ...base, id: "activate", kind: "activate", data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "activateLaunch", args: [plan] }), value: 0n, dependencies: [], postconditions: [{ kind: "launch", phase: "Active", preparedMarkets: plan.markets.length }] });
75
+ transactions.push(buildLifecycleCommand(plan, { kind: "activate" }, ufg));
69
76
  }
70
77
  return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1]!.id] }));
71
78
  }
79
+
80
+ /** Pure unsigned core commands, with no account nonce assigned by the SDK. */
81
+ export function buildLaunchTransactions(options: BuildLaunchTransactionsOptions): LifecycleTransaction[] {
82
+ return buildLifecycleCommands(options);
83
+ }