@black-market/sdk 0.5.1 → 0.6.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.
@@ -209,7 +209,7 @@ function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", prog
209
209
  function attachEstimates(transactions: readonly LifecycleTransaction[], simulation: LifecycleSimulation): LifecycleTransaction[] {
210
210
  return transactions.map((transaction) => {
211
211
  const estimate = simulation.steps.find((step) => step.transactionId === transaction.id)?.estimate;
212
- return { ...transaction, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, admission: { admitted: simulation.admitted, confidence: simulation.confidence, reason: simulation.reason, limits: simulation.limits } };
212
+ return { ...transaction, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, admission: { admitted: simulation.admitted, confidence: simulation.confidence, executionProof: simulation.executionProof, protocolFit: simulation.protocolFit, transportPreflight: simulation.transportPreflight, blockNumber: simulation.blockNumber, blockHash: simulation.blockHash, account: simulation.account, chainId: simulation.chainId, reason: simulation.reason, limits: simulation.limits } };
213
213
  });
214
214
  }
215
215
 
@@ -301,7 +301,7 @@ export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLau
301
301
  const predictedToken = await readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block);
302
302
  const { tokenFactory, tokenFactoryCodeHash } = await readTokenFactoryBinding(client, plan.orchestrator, block);
303
303
  const { limits } = await resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits);
304
- const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [] };
304
+ const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [] };
305
305
  const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1 };
306
306
  const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts }, block);
307
307
  let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, atomicAttempt: placeholder, preparationBatchSize: plan.markets.length, limits: options.limits };
@@ -318,7 +318,7 @@ export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLau
318
318
  }
319
319
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
320
320
  await assertLifecycleBlock(client, block);
321
- return { ...planned, simulation: { ...placeholder, confidence: "stateful", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
321
+ return { ...planned, simulation: { ...placeholder, confidence: "stateful", executionProof: "proved", protocolFit: "proved", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
322
322
  }
323
323
  const prerequisites = progress.canonical.phase === LifecyclePhase.None ? await readFunding(client, plan, block) : { prerequisites: [], approvals: [], nativeValue: 0n, reasons: [] };
324
324
  const validation = await validatePendingInputs(client, planned, progress, block);
@@ -351,6 +351,33 @@ export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLau
351
351
  return { ...planned, atomicAttempt, simulation, preparationBatchSize, transactions: attachEstimates(transactions, simulation) };
352
352
  }
353
353
 
354
+ async function preflightNextTransaction(options: BuildNextTransactionOptions, next: LifecycleTransaction, simulation: LifecycleSimulation): Promise<LifecycleTransaction> {
355
+ if (next.gas === undefined || next.gas <= 0n || next.gas >= 1n << 64n || next.gasPrice === undefined || next.gasPrice < 0n ||
356
+ simulation.executionProof !== "proved" || simulation.protocolFit !== "proved" || !simulation.admitted ||
357
+ next.estimate === undefined || next.estimate.gasLimit !== next.gas || next.estimate.gasPrice !== next.gasPrice ||
358
+ simulation.limits.transactionGasCeiling !== undefined && next.gas > simulation.limits.transactionGasCeiling) {
359
+ throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction has no current-state exact gas/headroom/fee-envelope proof", simulation);
360
+ }
361
+ const submissionClient = options.submissionClient;
362
+ if (submissionClient === undefined) return { ...next, dependencies: [] };
363
+ try {
364
+ if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId) throw new Error("Active submission RPC chain differs from the reviewed transaction");
365
+ const estimated = rpcQuantity(await lifecycleRpc(submissionClient, "eth_estimateGas", [{
366
+ from: next.from, to: next.to, data: next.data, value: toHex(next.value), gas: toHex(next.gas), gasPrice: toHex(next.gasPrice),
367
+ }, "latest"]), "submission gas estimate");
368
+ if (estimated <= 0n || estimated > next.gas) throw new Error("Submission RPC gas estimate exceeds the exact reviewed envelope or is zero");
369
+ if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId) throw new Error("Active submission RPC chain changed during preflight");
370
+ } catch (failure) {
371
+ const reason = `Exact next-transaction submission preflight failed: ${failure instanceof Error ? failure.message : String(failure)}`;
372
+ throw new LifecyclePlanningError("SUBMISSION_PREFLIGHT_FAILED", reason, { ...simulation, transportPreflight: "failed", reason });
373
+ }
374
+ const canonical = await readLifecycleBlock(options.client, toHex(simulation.blockNumber));
375
+ if (canonical.hash.toLowerCase() !== simulation.blockHash.toLowerCase()) throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch proof changed during submission preflight; rebuild from canonical progress");
376
+ if (rpcQuantity(await lifecycleRpc(options.client, "eth_chainId"), "chain ID") !== simulation.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during submission preflight");
377
+ if (next.admission === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction omitted its exact simulation admission", simulation);
378
+ return { ...next, dependencies: [], admission: { ...next.admission, transportPreflight: "passed" } };
379
+ }
380
+
354
381
  /** Re-reads canonical state, revalidates all prerequisites, and proves the remaining sequence afresh. */
355
382
  export async function buildNextTransaction(options: BuildNextTransactionOptions): Promise<LifecycleTransaction | undefined> {
356
383
  const block = await readLifecycleBlock(options.client);
@@ -368,17 +395,18 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
368
395
  if (progress.canonical.phase === LifecyclePhase.None) throw new LifecyclePlanningError("NOT_STARTED", "An unstarted plan has no launch escrow to cancel");
369
396
  const { planned } = options;
370
397
  const transaction: LifecycleTransaction = { id: "cancel", kind: "cancel", chainId: Number(planned.chainId), from: planned.account, to: planned.plan.orchestrator, data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "cancelLaunch", args: [planned.plan] }), value: 0n, dependencies: [], postconditions: [{ kind: "launch", phase: "Cancelled", preparedMarkets: progress.canonical.preparedMarkets }] };
371
- const simulation = await simulateLaunchTransactions({ client: options.client, planned, limits: options.limits ?? planned.limits, fork: options.fork }, [transaction], block);
398
+ const simulation = await simulateLaunchTransactions({ client: options.client, planned, limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork }, [transaction], block);
372
399
  if (!simulation.admitted) throw new LifecyclePlanningError("CANCEL_NOT_ADMITTED", simulation.reason ?? "Cancellation could not be proved against current canonical state", simulation);
373
- return attachEstimates([transaction], simulation)[0];
400
+ const next = attachEstimates([transaction], simulation)[0];
401
+ if (next === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
402
+ return preflightNextTransaction(options, next, simulation);
374
403
  }
375
- const refreshed = await planLaunch({ client: options.client, plan: options.planned.plan, account: options.planned.account, mode: options.planned.mode, limits: options.limits ?? options.planned.limits, fork: options.fork, receipts: options.receipts, confirmations: options.confirmations ?? options.planned.confirmations });
404
+ const refreshed = await planLaunch({ client: options.client, plan: options.planned.plan, account: options.planned.account, mode: options.planned.mode, limits: options.limits === undefined ? options.planned.limits : options.limits, fork: options.fork, receipts: options.receipts, confirmations: options.confirmations ?? options.planned.confirmations });
376
405
  if (!refreshed.simulation.admitted) throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", refreshed.simulation.reason ?? "Remaining exact launch sequence is not admitted", refreshed.simulation);
377
406
  assertPlannedDeploymentBindings(options.planned, refreshed);
378
407
  const next = refreshed.transactions[0];
379
408
  if (next === undefined) return undefined;
380
- if (next.gas === undefined || next.gasPrice === undefined || next.gas > refreshed.simulation.limits.executionGasCeiling) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction has no current-state exact gas/headroom/fee-envelope proof");
381
- return { ...next, dependencies: [] };
409
+ return preflightNextTransaction(options, next, refreshed.simulation);
382
410
  }
383
411
 
384
412
  /** Re-simulates only canonical unfinished work with the same submitted receipt
@@ -388,7 +416,7 @@ export async function simulateLaunchPlan(options: SimulateLaunchPlanOptions): Pr
388
416
  if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan no longer matches its commitment");
389
417
  const current = await planLaunch({
390
418
  client, plan: planned.plan, account: planned.account, mode: planned.mode,
391
- limits: options.limits ?? planned.limits, fork: options.fork,
419
+ limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork,
392
420
  receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations,
393
421
  });
394
422
  assertPlannedDeploymentBindings(planned, current);
@@ -1,5 +1,5 @@
1
- import { decodeFunctionResult, encodeFunctionData, isHex, toHex, type Abi, type Address, type Hex } from "viem";
2
- import { LifecyclePlanningError, type LifecycleBlock, type LifecycleLimitContext, type LifecycleLimitSource, type LifecycleLimits, type LifecycleRpcClient, type ResolvedLifecycleLimits } from "./types.js";
1
+ import { decodeFunctionResult, encodeFunctionData, isAddress, isHex, parseAbi, toHex, type Abi, type Address, type Hex } from "viem";
2
+ import { LifecyclePlanningError, type LifecycleBlock, type LifecycleLimitContext, type LifecycleLimitSource, type LifecycleLimits, type LifecycleRpcClient, type LifecycleTransaction, type ResolvedLifecycleLimits } from "./types.js";
3
3
  import { lifecycleRegistryAbi } from "./abi.js";
4
4
  import type { ProfileTopologyV1 } from "./schema.js";
5
5
 
@@ -31,7 +31,8 @@ export async function readLifecycleBlock(client: LifecycleRpcClient, tag: Hex |
31
31
  baseFeePerGas: value.baseFeePerGas === undefined || value.baseFeePerGas === null ? undefined : rpcQuantity(value.baseFeePerGas, "base fee"),
32
32
  };
33
33
  }
34
- export async function assertLifecycleBlock(client: LifecycleRpcClient, block: LifecycleBlock): Promise<void> {
34
+ export async function assertLifecycleBlock(client: LifecycleRpcClient, block: LifecycleBlock, expectedChainId?: bigint): Promise<void> {
35
+ if (expectedChainId !== undefined && rpcQuantity(await lifecycleRpc(client, "eth_chainId"), "chain ID") !== expectedChainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during the pinned operation");
35
36
  const current = await readLifecycleBlock(client, toHex(block.number));
36
37
  if (current.hash.toLowerCase() !== block.hash.toLowerCase()) throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch state changed during the operation; read canonical progress again");
37
38
  }
@@ -49,40 +50,86 @@ export async function readLifecycleProfileTopology(client: LifecycleRpcClient, r
49
50
  if (topology.hookTopology !== 0 && topology.hookTopology !== 1 && topology.hookTopology !== 2) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Registry returned an unsupported hook topology");
50
51
  return { ...topology, hookTopology: topology.hookTopology };
51
52
  }
53
+ export const nitroArbSys = "0x0000000000000000000000000000000000000064";
54
+ export const nitroGasInfo = "0x000000000000000000000000000000000000006c";
55
+ export const nitroNodeInterface = "0x00000000000000000000000000000000000000c8";
56
+ export const nitroArbSysAbi = parseAbi(["function arbOSVersion() view returns (uint256)"]);
57
+ export const nitroGasInfoAbi = parseAbi(["function getMaxTxGasLimit() view returns (uint256)", "function getMaxBlockGasLimit() view returns (uint64)"]);
58
+ const nitroNodeInterfaceAbi = parseAbi(["function gasEstimateL1Component(address to, bool contractCreation, bytes data) payable returns (uint64 gasEstimateForL1, uint256 baseFee, uint256 l1BaseFeeEstimate)"]);
59
+ const maxUint64 = (1n << 64n) - 1n;
60
+
61
+ /** A poster quote is an envelope budget, never an execution/compute proof. */
62
+ export async function readNitroPosterGas(transaction: LifecycleTransaction, context: LifecycleLimitContext): Promise<{ posterGas: bigint; baseFee: bigint; l1BaseFeeEstimate: bigint }> {
63
+ const data = encodeFunctionData({ abi: nitroNodeInterfaceAbi, functionName: "gasEstimateL1Component", args: [transaction.to, false, transaction.data] });
64
+ const result = rpcHex(await lifecycleRpc(context.client, "eth_call", [{ from: transaction.from, to: nitroNodeInterface, data, value: toHex(transaction.value) }, toHex(context.block.number)]), "Nitro poster estimate");
65
+ if (result.length !== 194) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Nitro poster estimate must contain exactly three ABI words");
66
+ const [posterGas, baseFee, l1BaseFeeEstimate] = decodeFunctionResult({ abi: nitroNodeInterfaceAbi, functionName: "gasEstimateL1Component", data: result });
67
+ if (posterGas < 0n || posterGas > maxUint64 || baseFee <= 0n || l1BaseFeeEstimate < 0n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Nitro poster estimate has invalid gas or fee values");
68
+ return { posterGas, baseFee, l1BaseFeeEstimate };
69
+ }
70
+
71
+ async function readNitroScalar(context: LifecycleLimitContext, to: Address, abi: Abi, functionName: string): Promise<bigint> {
72
+ const data = encodeFunctionData({ abi, functionName });
73
+ const result = rpcHex(await lifecycleRpc(context.client, "eth_call", [{ to, data }, toHex(context.block.number)]), functionName);
74
+ if (result.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", `${functionName} must return exactly one ABI word`);
75
+ return decodeFunctionResult({ abi, functionName, data: result }) as bigint;
76
+ }
77
+
52
78
  export async function resolveLifecycleLimits(context: LifecycleLimitContext, source?: LifecycleLimitSource): Promise<{ limits: ResolvedLifecycleLimits; configuration: LifecycleLimits }> {
53
- let configuration: LifecycleLimits = {};
54
- let sourceError: string | undefined;
55
- try { configuration = typeof source === "function" ? await source(context) : source ?? {}; }
56
- catch (failure) { sourceError = failure instanceof Error ? failure.message : String(failure); }
57
- const headroomBps = configuration.headroomBps ?? 1500;
79
+ let configuration: LifecycleLimits;
80
+ try { configuration = source === undefined ? {} : typeof source === "function" ? await source(context) : source; }
81
+ catch (failure) { throw new LifecyclePlanningError("LIMIT_SOURCE_FAILED", `Supplied execution policy could not be resolved: ${failure instanceof Error ? failure.message : String(failure)}`); }
82
+ if (configuration === null || typeof configuration !== "object" || Array.isArray(configuration)) throw new LifecyclePlanningError("INVALID_LIMITS", "Supplied execution policy must be an object");
83
+ const headroomBps = configuration.headroomBps === undefined ? 1500 : configuration.headroomBps;
58
84
  if (!Number.isSafeInteger(headroomBps) || headroomBps < 0 || headroomBps > 10000) throw new LifecyclePlanningError("INVALID_LIMITS", "Headroom must be between zero and 10000 basis points");
59
- const ceilings = [context.block.gasLimit, configuration.chainGasLimit, configuration.rpcGasLimit, configuration.accountGasLimit].filter((limit): limit is bigint => limit !== undefined);
60
- if (ceilings.some((limit) => typeof limit !== "bigint" || limit <= 0n)) throw new LifecyclePlanningError("INVALID_LIMITS", "Execution gas caps must be positive exact bigint values");
85
+ for (const limit of [context.block.gasLimit, configuration.chainGasLimit, configuration.rpcGasLimit, configuration.accountGasLimit, configuration.maxSimulationGas]) {
86
+ if (limit !== undefined && (typeof limit !== "bigint" || limit <= 0n || limit > maxUint64)) throw new LifecyclePlanningError("INVALID_LIMITS", "Gas caps must be positive exact uint64 bigint values");
87
+ }
61
88
  if (configuration.maxCalldataBytes !== undefined && (!Number.isSafeInteger(configuration.maxCalldataBytes) || configuration.maxCalldataBytes <= 0)) throw new LifecyclePlanningError("INVALID_LIMITS", "Calldata cap must be a positive byte count");
62
- if (configuration.maxSimulationGas !== undefined && configuration.maxSimulationGas <= 0n) throw new LifecyclePlanningError("INVALID_LIMITS", "Simulation aggregate gas cap must be positive");
89
+ if (configuration.estimateDataFee !== undefined && typeof configuration.estimateDataFee !== "function") throw new LifecyclePlanningError("INVALID_LIMITS", "External data-fee estimator must be a function");
90
+ if (configuration.chainId !== undefined && (typeof configuration.chainId !== "bigint" || configuration.chainId !== context.chainId) ||
91
+ configuration.observedBlockNumber !== undefined && (typeof configuration.observedBlockNumber !== "bigint" || configuration.observedBlockNumber !== context.block.number) ||
92
+ configuration.observedBlockHash !== undefined && (typeof configuration.observedBlockHash !== "string" || !/^0x[\da-fA-F]{64}$/.test(configuration.observedBlockHash) || configuration.observedBlockHash.toLowerCase() !== context.block.hash.toLowerCase()) ||
93
+ configuration.account !== undefined && (typeof configuration.account !== "string" || !isAddress(configuration.account) || configuration.account.toLowerCase() !== context.account.toLowerCase()) ||
94
+ configuration.orchestrator !== undefined && (typeof configuration.orchestrator !== "string" || !isAddress(configuration.orchestrator) || configuration.orchestrator.toLowerCase() !== context.orchestrator.toLowerCase())) {
95
+ throw new LifecyclePlanningError("LIMIT_CONTEXT_MISMATCH", "Supplied execution policy provenance differs from the canonical chain/account/core/block context");
96
+ }
97
+ const protocol = context.chainId === 4663n ? "nitro" : "evm";
98
+ let arbOSVersion: bigint | undefined; let maxTxComputeGas: bigint | undefined; let maxBlockComputeGas: bigint | undefined;
99
+ if (protocol === "nitro") {
100
+ try {
101
+ const rawVersion = await readNitroScalar(context, nitroArbSys, nitroArbSysAbi, "arbOSVersion");
102
+ // ArbSys alone adds 55 to the logical Nitro ArbOS version.
103
+ if (typeof rawVersion !== "bigint" || rawVersion < 105n) throw new Error("ArbOS version 50 or newer is required (ArbSys raw version >= 105)");
104
+ arbOSVersion = rawVersion - 55n;
105
+ maxTxComputeGas = await readNitroScalar(context, nitroGasInfo, nitroGasInfoAbi, "getMaxTxGasLimit");
106
+ maxBlockComputeGas = await readNitroScalar(context, nitroGasInfo, nitroGasInfoAbi, "getMaxBlockGasLimit");
107
+ if ([maxTxComputeGas, maxBlockComputeGas].some((limit) => typeof limit !== "bigint" || limit <= 0n || limit > maxUint64)) throw new Error("Nitro compute getters must be positive uint64 values");
108
+ } catch (failure) { throw new LifecyclePlanningError("NITRO_LIMITS_UNAVAILABLE", `Pinned Nitro compute limits could not be established: ${failure instanceof Error ? failure.message : String(failure)}; header gasLimit is not a fallback`); }
109
+ if (configuration.estimateDataFee !== undefined) throw new LifecyclePlanningError("INVALID_LIMITS", "Nitro poster fees are already included in gas; an external data-fee estimator would double-charge them");
110
+ }
111
+ const envelopeCaps = [configuration.chainGasLimit, configuration.rpcGasLimit, configuration.accountGasLimit].filter((limit): limit is bigint => limit !== undefined);
112
+ if (protocol === "evm") envelopeCaps.push(context.block.gasLimit);
113
+ const transactionGasCeiling = envelopeCaps.length === 0 ? undefined : envelopeCaps.reduce((minimum, limit) => limit < minimum ? limit : minimum);
114
+ const executionGasCeiling = maxTxComputeGas !== undefined && maxBlockComputeGas !== undefined
115
+ ? (maxTxComputeGas < maxBlockComputeGas ? maxTxComputeGas : maxBlockComputeGas)
116
+ : transactionGasCeiling ?? context.block.gasLimit;
63
117
  const unknownExecutionConstraints = [
64
- ...(configuration.chainGasLimit === undefined ? ["Chain per-transaction execution cap is unknown; block gas limit is not an equivalent on every chain"] : []),
65
- ...(configuration.rpcGasLimit === undefined ? ["RPC transaction execution cap is unknown"] : []),
66
- ...(configuration.accountGasLimit === undefined ? ["Current account/wallet transaction execution cap is unknown"] : []),
118
+ ...(protocol === "evm" && configuration.chainGasLimit === undefined ? ["Independent chain per-transaction execution cap is unknown; proof is bounded by the pinned header and validated backend"] : []),
119
+ ...(configuration.rpcGasLimit === undefined ? ["RPC transaction gas-envelope cap is unknown"] : []),
120
+ ...(configuration.accountGasLimit === undefined ? ["Current account/wallet transaction gas-envelope cap is unknown"] : []),
67
121
  ...(configuration.maxCalldataBytes === undefined ? ["Applicable chain/account/RPC calldata byte cap is unknown"] : []),
68
- ...(configuration.chainId !== context.chainId ? ["Current limit chain provenance is missing or mismatched"] : []),
69
- ...(configuration.account?.toLowerCase() !== context.account.toLowerCase() ? ["Current limit account provenance is missing or mismatched"] : []),
70
- ...(configuration.orchestrator?.toLowerCase() !== context.orchestrator.toLowerCase() ? ["Current limit orchestrator provenance is missing or mismatched"] : []),
71
- ...(configuration.observedBlockNumber !== context.block.number || configuration.observedBlockHash?.toLowerCase() !== context.block.hash.toLowerCase() ? ["Current limit block provenance is missing, stale, or mismatched"] : []),
72
- ...(sourceError === undefined ? [] : [`Current limit source failed: ${sourceError}`]),
122
+ ...(configuration.maxSimulationGas === undefined ? ["RPC aggregate simulation gas cap is unknown"] : []),
73
123
  ];
74
124
  return {
75
125
  configuration,
76
126
  limits: {
77
- executionGasCeiling: ceilings.reduce((minimum, limit) => limit < minimum ? limit : minimum), headroomBps,
127
+ protocol, executionGasCeiling, transactionGasCeiling, arbOSVersion, maxTxComputeGas, maxBlockComputeGas, headroomBps,
78
128
  maxCalldataBytes: configuration.maxCalldataBytes, maxSimulationGas: configuration.maxSimulationGas,
79
129
  blockGasLimit: context.block.gasLimit, chainGasLimit: configuration.chainGasLimit,
80
130
  rpcGasLimit: configuration.rpcGasLimit, accountGasLimit: configuration.accountGasLimit,
81
- admissionKnown: unknownExecutionConstraints.length === 0, unknownExecutionConstraints,
82
- unknownConstraints: [
83
- ...unknownExecutionConstraints,
84
- ...(configuration.estimateDataFee === undefined ? ["Chain-specific data fee is unavailable"] : []),
85
- ],
131
+ unknownExecutionConstraints,
132
+ unknownConstraints: [...unknownExecutionConstraints, ...(protocol === "evm" && configuration.estimateDataFee === undefined ? ["External chain-specific data fee is unavailable"] : [])],
86
133
  },
87
134
  };
88
135
  }
@@ -1,7 +1,7 @@
1
1
  import { decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctionResult, toHex, type Hex } from "viem";
2
2
  import { launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
3
3
  import { LifecyclePhase, type LaunchProgressV1, type LaunchReceiptV1 } from "./schema.js";
4
- import { assertLifecycleBlock, lifecycleRpc, readLifecycleBlock, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity } from "./rpc.js";
4
+ import { assertLifecycleBlock, lifecycleRpc, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readNitroPosterGas, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity } from "./rpc.js";
5
5
  import { LifecyclePlanningError, type ControlledLifecycleFork, type LifecycleBlock, type LifecyclePostcondition, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleSimulationStep, type LifecycleTransaction, type PlannedLaunch, type ResolvedLifecycleLimits, type SimulateLaunchPlanOptions } from "./types.js";
6
6
 
7
7
  function conditionRequest(condition: LifecyclePostcondition, planned: PlannedLaunch): { to: Hex; data: Hex } {
@@ -24,6 +24,40 @@ function checkCondition(condition: LifecyclePostcondition, data: Hex, planned: P
24
24
  }
25
25
  }
26
26
 
27
+ /** Native Nitro eth_simulateV1 executes commit-mode ArbOS charging, including its
28
+ * compute hold after the actual poster charge. A generic EVM/fork is not this proof. */
29
+ async function proveNitroBackend(client: LifecycleRpcClient, planned: PlannedLaunch, block: LifecycleBlock, limits: ResolvedLifecycleLimits, gasPrice: bigint): Promise<void> {
30
+ if (limits.arbOSVersion === undefined || limits.maxTxComputeGas === undefined || limits.maxBlockComputeGas === undefined) throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Pinned Nitro metering context is incomplete");
31
+ const probes = [
32
+ { to: nitroArbSys, data: encodeFunctionData({ abi: nitroArbSysAbi, functionName: "arbOSVersion" }), expected: limits.arbOSVersion + 55n },
33
+ { to: nitroGasInfo, data: encodeFunctionData({ abi: nitroGasInfoAbi, functionName: "getMaxTxGasLimit" }), expected: limits.maxTxComputeGas },
34
+ { to: nitroGasInfo, data: encodeFunctionData({ abi: nitroGasInfoAbi, functionName: "getMaxBlockGasLimit" }), expected: limits.maxBlockComputeGas },
35
+ ];
36
+ const gas = limits.executionGasCeiling < 100_000n ? limits.executionGasCeiling : 100_000n;
37
+ 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");
38
+ // This isolated read-only request does not add synthetic verification transactions to the launch sequence.
39
+ const result = await lifecycleRpc(client, "eth_simulateV1", [{
40
+ validation: true, traceTransfers: false, returnFullTransactions: false,
41
+ blockStateCalls: [{
42
+ // Capability reads are isolated from payer affordability. Real launch replay never overrides balances.
43
+ stateOverrides: { [planned.account]: { balance: toHex((1n << 256n) - 1n) } },
44
+ blockOverrides: { number: toHex(block.number + 1n), time: toHex(block.timestamp + 1n), gasLimit: toHex(block.gasLimit) },
45
+ calls: probes.map(({ to, data }) => ({ from: planned.account, to, data, value: "0x0", gas: toHex(gas), gasPrice: toHex(gasPrice) })),
46
+ }],
47
+ }, toHex(block.number)]);
48
+ if (!Array.isArray(result) || result.length !== 1) throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Native simulation omitted the pinned ArbOS metering probe");
49
+ const simulated = rpcObject(result[0], "Nitro metering probe block");
50
+ if (!Array.isArray(simulated.calls) || simulated.calls.length !== probes.length) throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Native simulation omitted ArbOS version or compute getters");
51
+ for (let index = 0; index < probes.length; index += 1) {
52
+ const call = rpcObject(simulated.calls[index], "Nitro metering probe");
53
+ const data = rpcHex(call.returnData, "simulated Nitro metering result");
54
+ 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) {
55
+ throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Simulated ArbOS version/compute getters differ from the exact pinned chain context");
56
+ }
57
+ }
58
+ await assertLifecycleBlock(client, block, planned.chainId);
59
+ }
60
+
27
61
  async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunch, block: LifecycleBlock, transactions: readonly LifecycleTransaction[], gasLimits: readonly bigint[], limits: ResolvedLifecycleLimits, gasPrice: bigint, validation: boolean): Promise<LifecycleSimulationStep[]> {
28
62
  const totalRequestedGas = gasLimits.reduce((total, gas) => total + gas, 0n);
29
63
  if (limits.maxSimulationGas !== undefined && totalRequestedGas > limits.maxSimulationGas) throw new LifecyclePlanningError("SIMULATION_RPC_CAP", "Sequential request exceeds the current RPC aggregate simulation gas cap; use an isolated controlled fork");
@@ -46,6 +80,8 @@ async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunc
46
80
  if (!Array.isArray(simulatedBlock.calls) || simulatedBlock.calls.length !== 1) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Sequential simulation omitted transaction results");
47
81
  const call = rpcObject(simulatedBlock.calls[0], "simulated transaction");
48
82
  const gasUsed = rpcQuantity(call.gasUsed, "simulated gas used");
83
+ const gasRequired = call.maxUsedGas === undefined ? gasUsed : rpcQuantity(call.maxUsedGas, "simulated gross gas");
84
+ if (gasRequired < gasUsed || gasRequired > (gasLimits[index] ?? 0n)) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Simulated gas use is outside the exact requested envelope");
49
85
  const returnData = rpcHex(call.returnData, "simulated return data");
50
86
  const success = rpcQuantity(call.status, "simulated status") === 1n;
51
87
  let error: string | undefined;
@@ -78,7 +114,7 @@ async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunc
78
114
  else if ((transaction.kind === "approve" || transaction.kind === "approve-reset") && returnData !== "0x" && decodeFunctionResult({ abi: lifecycleErc20Abi, functionName: "approve", data: returnData }) !== true) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Funding approval returned failure");
79
115
  } catch (failure) { error = failure instanceof Error ? failure.message : String(failure); }
80
116
  }
81
- steps.push({ transactionId: transaction.id, success: success && error === undefined, gasUsed, gasLimit: gasLimits[index], returnData, error });
117
+ steps.push({ transactionId: transaction.id, success: success && error === undefined, gasUsed, gasRequired, gasLimit: gasLimits[index], returnData, error });
82
118
  if (!success || error !== undefined) break;
83
119
  }
84
120
  return steps;
@@ -201,36 +237,59 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
201
237
  if (options.fork?.client === client) throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
202
238
  const block = pinnedBlock ?? await readLifecycleBlock(client);
203
239
  const context = { client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId };
204
- const { limits, configuration } = await resolveLifecycleLimits(context, options.limits ?? planned.limits);
205
- const base = { confidence: "stateful" as const, blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits };
240
+ const { limits, configuration } = await resolveLifecycleLimits(context, options.limits === undefined ? planned.limits : options.limits);
241
+ 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 };
206
242
  if (rpcQuantity(await lifecycleRpc(client, "eth_chainId"), "chain ID") !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
207
243
  const accountCode = rpcHex(await lifecycleRpc(client, "eth_getCode", [planned.account, toHex(block.number)]), "account code");
208
- if (accountCode !== "0x") return { ...base, confidence: "provisional", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
244
+ if (accountCode !== "0x") return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
209
245
  for (const transaction of transactions) {
210
- if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) return { ...base, backend: "unavailable", admitted: false, steps: [], failedTransactionId: transaction.id, reason: "Exact calldata exceeds the current chain/account/RPC byte cap" };
246
+ 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");
247
+ 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, reason: "Exact calldata exceeds the supplied chain/account/RPC byte cap" };
211
248
  }
212
- if (transactions.length === 0) { await assertLifecycleBlock(client, block); return { ...base, backend: "unavailable", admitted: true, steps: [] }; }
249
+ if (transactions.length === 0) { await assertLifecycleBlock(client, block, planned.chainId); return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] }; }
213
250
  const gasPrice = rpcQuantity(await lifecycleRpc(client, "eth_gasPrice"), "gas price");
214
- const capGas = transactions.map(() => limits.executionGasCeiling);
251
+ if (gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
252
+ const buffer = (gas: bigint) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
253
+ const posterGas: bigint[] = [];
254
+ if (limits.protocol === "nitro") {
255
+ try {
256
+ await proveNitroBackend(client, planned, block, limits, gasPrice);
257
+ for (const transaction of transactions) posterGas.push(buffer((await readNitroPosterGas(transaction, context)).posterGas));
258
+ } catch (failure) {
259
+ await assertLifecycleBlock(client, block, planned.chainId);
260
+ return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Native Nitro compute/poster proof unavailable: ${failure instanceof Error ? failure.message : String(failure)}. Generic Anvil/Hardhat forks cannot prove ArbOS metering.` };
261
+ }
262
+ }
263
+ const computeCeiling = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
264
+ const capGas = transactions.map(() => computeCeiling);
265
+ // The poster quote allocates gas only. The native exact replay enforces actual
266
+ // compute hold; subtracting this approximate budget from gasUsed would not.
267
+ const discoveryGas = transactions.map((_, index) => {
268
+ const total = limits.executionGasCeiling + (posterGas[index] ?? 0n);
269
+ const capped = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < total ? limits.transactionGasCeiling : total;
270
+ if (capped <= 0n || capped >= 1n << 64n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Compute/poster envelope exceeds uint64 gas");
271
+ return capped;
272
+ });
215
273
  let backend: "eth_simulateV1" | "controlled-fork" = "eth_simulateV1";
216
274
  let measured: LifecycleSimulationStep[];
217
275
  // Permissive cap measurement is not proof; only the exact-gas validated replay admits a plan.
218
276
  try { measured = await simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false); }
219
277
  catch (failure) {
220
- if (options.fork === undefined) {
221
- await assertLifecycleBlock(client, block);
222
- return { ...base, confidence: "provisional", backend: "unavailable", admitted: false, steps: [], reason: `Stateful sequential RPC simulation unavailable: ${failure instanceof Error ? failure.message : String(failure)}. Dependent eth_call estimates are not proof.` };
278
+ if (options.fork === undefined || limits.protocol === "nitro") {
279
+ await assertLifecycleBlock(client, block, planned.chainId);
280
+ return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Stateful sequential RPC simulation unavailable: ${failure instanceof Error ? failure.message : String(failure)}. ${limits.protocol === "nitro" ? "Generic Anvil/Hardhat forks cannot prove ArbOS compute/poster metering." : "Dependent eth_call estimates are not proof."}` };
223
281
  }
224
282
  backend = "controlled-fork";
225
283
  measured = await simulateForkPass(options.fork, planned, block, transactions, capGas, gasPrice, true, limits.headroomBps);
226
284
  }
227
285
  const failed = measured.find((step) => !step.success);
228
- if (failed !== undefined) { await assertLifecycleBlock(client, block); return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId }; }
229
- const gasLimits = measured.map((step) => ((step.gasRequired ?? step.gasUsed ?? limits.executionGasCeiling) * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n);
230
- const exceeds = gasLimits.findIndex((gas) => gas > limits.executionGasCeiling);
286
+ if (failed !== undefined) { await assertLifecycleBlock(client, block, planned.chainId); return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId }; }
287
+ const computeGas = measured.map((step) => buffer(step.gasRequired ?? step.gasUsed ?? computeCeiling));
288
+ const gasLimits = computeGas.map((gas, index) => gas + (posterGas[index] ?? 0n));
289
+ const exceeds = gasLimits.findIndex((gas, index) => gas <= 0n || gas >= 1n << 64n || (computeGas[index] ?? 0n) > limits.executionGasCeiling || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling);
231
290
  if (exceeds >= 0) {
232
- await assertLifecycleBlock(client, block);
233
- return { ...base, backend, admitted: false, steps: measured, reason: "Execution gas plus conservative headroom exceeds current transaction limits", failedTransactionId: transactions[exceeds]?.id };
291
+ await assertLifecycleBlock(client, block, planned.chainId);
292
+ return { ...base, executionProof: "unavailable", protocolFit: "failed", backend, admitted: false, steps: measured, reason: "Execution compute/headroom or complete poster gas envelope is outside positive uint64 gas and current protocol/known transaction limits", failedTransactionId: transactions[exceeds]?.id };
234
293
  }
235
294
  // Replay from the same initial state with exact headroom limits: gasUsed alone is not an EIP-150/gasleft proof.
236
295
  // eth_simulateV1 with validation raises an RPC error on any revert (a generic
@@ -249,7 +308,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
249
308
  } catch (failure) {
250
309
  const message = failure instanceof Error ? failure.message : String(failure);
251
310
  if (backend !== "eth_simulateV1") {
252
- await assertLifecycleBlock(client, block);
311
+ await assertLifecycleBlock(client, block, planned.chainId);
253
312
  return { ...base, backend, admitted: false, steps: [], reason: `Exact-gas validated replay failed: ${message}` };
254
313
  }
255
314
  validatedReplayError = message;
@@ -264,12 +323,12 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
264
323
  // same pinned block; headroom is never silently raised — the carried gas is
265
324
  // the validated envelope itself.
266
325
  try {
267
- const ceilingLimits = transactions.map(() => limits.executionGasCeiling);
326
+ const ceilingLimits = discoveryGas;
268
327
  const retried = backend === "eth_simulateV1"
269
328
  ? await simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true)
270
329
  : options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
271
330
  if (retried === undefined || retried.some((step) => !step.success)) {
272
- await assertLifecycleBlock(client, block);
331
+ await assertLifecycleBlock(client, block, planned.chainId);
273
332
  return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${retried?.find((step) => !step.success)?.error ?? validatedReplayError}; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned`, failedTransactionId: transactions[0]?.id };
274
333
  }
275
334
  admittedGasLimits = ceilingLimits;
@@ -277,7 +336,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
277
336
  finalFailure = undefined;
278
337
  validatedReplayError = undefined;
279
338
  } catch {
280
- await assertLifecycleBlock(client, block);
339
+ await assertLifecycleBlock(client, block, planned.chainId);
281
340
  return { ...base, backend, admitted: false, steps: measured, reason: `Validated buffered replay failed (${validatedReplayError}) and envelope discovery at the exact gas ceiling was rejected by the simulation backend; final mint/lock/all-buys/public-opening is indivisible`, failedTransactionId: transactions[0]?.id };
282
341
  }
283
342
  }
@@ -285,11 +344,11 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
285
344
  const failedIndex = verifiedSteps.findIndex((step) => !step.success);
286
345
  const bufferedLimit = gasLimits[failedIndex];
287
346
  const failedTransaction = transactions[failedIndex];
288
- if (bufferedLimit !== undefined && failedTransaction !== undefined && looksGasExhausted(finalFailure, bufferedLimit) && limits.executionGasCeiling > bufferedLimit) {
347
+ if (bufferedLimit !== undefined && failedTransaction !== undefined && looksGasExhausted(finalFailure, bufferedLimit) && (discoveryGas[failedIndex] ?? 0n) > bufferedLimit) {
289
348
  // Buffered replay returned an exhaustion-shaped failed step; learn the true
290
349
  // validated envelope with one explicit replay at the exact execution ceiling.
291
350
  try {
292
- const ceilingLimits = transactions.map(() => limits.executionGasCeiling);
351
+ const ceilingLimits = discoveryGas;
293
352
  const retried = backend === "eth_simulateV1"
294
353
  ? await simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true)
295
354
  : options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
@@ -301,7 +360,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
301
360
  finalFailure = undefined;
302
361
  } else {
303
362
  const indivisible = failedTransaction.kind === "activate" || failedTransaction.kind === "atomic";
304
- await assertLifecycleBlock(client, block);
363
+ await assertLifecycleBlock(client, block, planned.chainId);
305
364
  return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${retryFailure.error ?? "the true gas envelope exceeds the current execution limit"}${indivisible ? "; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned" : ""}`, failedTransactionId: failedTransaction.id };
306
365
  }
307
366
  }
@@ -315,15 +374,18 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
315
374
  for (let index = 0; index < verifiedSteps.length; index += 1) {
316
375
  const step = verifiedSteps[index]; const transaction = transactions[index]; const gasLimit = admittedGasLimits[index];
317
376
  if (step === undefined || transaction === undefined || gasLimit === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing verified fee context");
318
- const dataFee = configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit }, context);
319
- if (dataFee !== undefined && dataFee < 0n) throw new LifecyclePlanningError("INVALID_DATA_FEE", "Data-fee oracle returned a negative amount");
377
+ const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit }, context);
378
+ 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");
379
+ // Nitro gas includes poster charges. Never add the quoted poster fee again.
320
380
  const executionFee = gasLimit * gasPrice;
321
381
  const required = transaction.value + executionFee + (dataFee ?? 0n);
382
+ if (required >= 1n << 256n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
322
383
  if (required > remainingBalance && affordabilityFailure === undefined) affordabilityFailure = "Creator native balance cannot fund the remaining transaction value and execution/data fee envelopes";
323
384
  remainingBalance -= required;
324
- estimates.push({ ...step, estimate: { gasUsed: step.gasUsed ?? 0n, gasLimit, gasPrice, executionFee, dataFee, totalFee: dataFee === undefined ? undefined : executionFee + dataFee, feeConfidence: dataFee === undefined ? "execution-only" : "execution-and-data" } });
385
+ const poster = posterGas[index];
386
+ estimates.push({ ...step, estimate: { gasUsed: step.gasUsed ?? 0n, gasLimit, gasPrice, executionFee, dataFee, posterGas: poster, posterFee: poster === undefined ? undefined : poster * gasPrice, dataFeeIncludedInGas: limits.protocol === "nitro", totalFee: dataFee === undefined ? undefined : executionFee + dataFee, feeConfidence: dataFee === undefined ? "execution-only" : "execution-and-data" } });
325
387
  }
326
- await assertLifecycleBlock(client, block);
327
- return { ...base, backend, admitted: finalFailure === undefined && affordabilityFailure === undefined && limits.admissionKnown, steps: estimates, reason: finalFailure?.error ?? affordabilityFailure ?? (limits.admissionKnown ? undefined : `Stateful execution was proved, but current execution admission constraints are incomplete: ${limits.unknownExecutionConstraints.join("; ")}`), failedTransactionId: finalFailure?.transactionId };
388
+ await assertLifecycleBlock(client, block, planned.chainId);
389
+ 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 };
328
390
  }
329
391
 
@@ -11,17 +11,22 @@ export type LifecycleRpcClient = {
11
11
  export type LifecycleBlock = { number: bigint; hash: Hash; timestamp: bigint; gasLimit: bigint; baseFeePerGas?: bigint };
12
12
  export type LifecycleLimitContext = { client: LifecycleRpcClient; account: Address; orchestrator: Address; block: LifecycleBlock; chainId: bigint };
13
13
  export type LifecycleLimits = {
14
+ /** Optional tightening caps on the complete transaction gas envelope, including Nitro poster gas. */
14
15
  headroomBps?: number; chainGasLimit?: bigint; rpcGasLimit?: bigint; accountGasLimit?: bigint;
15
16
  maxCalldataBytes?: number; maxSimulationGas?: bigint;
16
17
  chainId?: bigint; orchestrator?: Address; account?: Address; observedBlockNumber?: bigint; observedBlockHash?: Hash;
17
- /** Chain-specific L1/data-fee oracle, called with exact calldata and the pinned block. */
18
+ /** External data fee not already charged in gas, called with exact calldata and the pinned block.
19
+ * Nitro poster fees are included in gas and use the native NodeInterface instead. */
18
20
  estimateDataFee?: (transaction: LifecycleTransaction, context: LifecycleLimitContext) => Promise<bigint>;
19
21
  };
20
22
  export type LifecycleLimitSource = LifecycleLimits | ((context: LifecycleLimitContext) => Promise<LifecycleLimits>);
21
23
  export type ResolvedLifecycleLimits = {
22
- executionGasCeiling: bigint; headroomBps: number; maxCalldataBytes?: number; maxSimulationGas?: bigint;
24
+ /** Generic EVM gas ceiling; on Nitro this is the pinned compute ceiling, not total gas. */
25
+ executionGasCeiling: bigint; transactionGasCeiling?: bigint; protocol: "evm" | "nitro";
26
+ arbOSVersion?: bigint; maxTxComputeGas?: bigint; maxBlockComputeGas?: bigint;
27
+ headroomBps: number; maxCalldataBytes?: number; maxSimulationGas?: bigint;
23
28
  blockGasLimit: bigint; chainGasLimit?: bigint; rpcGasLimit?: bigint; accountGasLimit?: bigint;
24
- admissionKnown: boolean; unknownExecutionConstraints: readonly string[]; unknownConstraints: readonly string[];
29
+ unknownExecutionConstraints: readonly string[]; unknownConstraints: readonly string[];
25
30
  };
26
31
  /** Only explicitly disposable RPCs may execute snapshot-isolated simulation transactions. */
27
32
  export type ControlledLifecycleFork = {
@@ -44,17 +49,26 @@ export type LifecycleTransaction = {
44
49
  };
45
50
  export type LifecycleTransactionEstimate = {
46
51
  gasUsed: bigint; gasLimit: bigint; gasPrice: bigint; executionFee: bigint; dataFee?: bigint; totalFee?: bigint;
52
+ /** Estimated poster allocation within gasLimit, never an exact compute measurement or an extra fee. */
53
+ posterGas?: bigint; posterFee?: bigint; dataFeeIncludedInGas: boolean;
47
54
  feeConfidence: "execution-and-data" | "execution-only";
48
55
  };
49
- export type LifecycleAdmission = {
56
+ /** Proof scopes are independent: current execution is not guaranteed wallet submission. */
57
+ export type LifecycleProofOutcomes = {
58
+ executionProof: "proved" | "failed" | "unavailable";
59
+ protocolFit: "proved" | "failed" | "unknown";
60
+ transportPreflight: "not-requested" | "passed" | "failed";
61
+ };
62
+ export type LifecycleAdmission = LifecycleProofOutcomes & {
50
63
  admitted: boolean; confidence: "stateful" | "provisional";
64
+ blockNumber: bigint; blockHash: Hash; account: Address; chainId: bigint;
51
65
  reason?: string; limits: ResolvedLifecycleLimits;
52
66
  };
53
67
  export type LifecycleSimulationStep = {
54
68
  transactionId: string; success: boolean; gasUsed?: bigint; gasLimit?: bigint; gasRequired?: bigint;
55
69
  returnData?: Hex; error?: string; estimate?: LifecycleTransactionEstimate;
56
70
  };
57
- export type LifecycleSimulation = {
71
+ export type LifecycleSimulation = LifecycleProofOutcomes & {
58
72
  backend: "eth_simulateV1" | "controlled-fork" | "unavailable";
59
73
  confidence: "stateful" | "provisional"; admitted: boolean;
60
74
  blockNumber: bigint; blockHash: Hash; account: Address; chainId: bigint;
@@ -122,6 +136,8 @@ export type ReadLaunchProgressOptions = {
122
136
  };
123
137
  export type BuildNextTransactionOptions = ReadLaunchProgressOptions & {
124
138
  planned: PlannedLaunch; action?: "continue" | "cancel"; limits?: LifecycleLimitSource; fork?: ControlledLifecycleFork;
139
+ /** Optional active submission RPC. Only the immediate next exact transaction is estimated, read-only. */
140
+ submissionClient?: LifecycleRpcClient;
125
141
  };
126
142
  export class LifecyclePlanningError extends Error {
127
143
  readonly code: string;
package/assets/splash.png DELETED
Binary file