@black-market/sdk 0.10.0 → 0.12.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.
- package/README.md +21 -2
- package/dist/lifecycle/abi.d.ts +1 -0
- package/dist/lifecycle/abi.d.ts.map +1 -1
- package/dist/lifecycle/abi.js +1 -0
- package/dist/lifecycle/abi.js.map +1 -1
- package/dist/lifecycle/calibration.d.ts.map +1 -1
- package/dist/lifecycle/calibration.js +2 -0
- package/dist/lifecycle/calibration.js.map +1 -1
- package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.d.ts +71 -0
- package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.d.ts.map +1 -0
- package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.js +73 -0
- package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.js.map +1 -0
- package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.d.ts +71 -0
- package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.d.ts.map +1 -0
- package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.js +73 -0
- package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.js.map +1 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +1 -0
- package/dist/lifecycle/index.js.map +1 -1
- package/dist/lifecycle/markets.d.ts +3 -1
- package/dist/lifecycle/markets.d.ts.map +1 -1
- package/dist/lifecycle/markets.js +20 -3
- package/dist/lifecycle/markets.js.map +1 -1
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +83 -54
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/presets.d.ts +9 -2
- package/dist/lifecycle/presets.d.ts.map +1 -1
- package/dist/lifecycle/presets.js +26 -5
- package/dist/lifecycle/presets.js.map +1 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +26 -5
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.js +3 -2
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts +1 -1
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +119 -175
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/transactions.d.ts +6 -0
- package/dist/lifecycle/transactions.d.ts.map +1 -1
- package/dist/lifecycle/transactions.js +39 -0
- package/dist/lifecycle/transactions.js.map +1 -1
- package/dist/lifecycle/types.d.ts +17 -4
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/dist/lifecycle/ufg-abi.d.ts +2386 -0
- package/dist/lifecycle/ufg-abi.d.ts.map +1 -0
- package/dist/lifecycle/ufg-abi.js +171 -0
- package/dist/lifecycle/ufg-abi.js.map +1 -0
- package/dist/lifecycle/ufg-config.d.ts +38 -0
- package/dist/lifecycle/ufg-config.d.ts.map +1 -0
- package/dist/lifecycle/ufg-config.js +75 -0
- package/dist/lifecycle/ufg-config.js.map +1 -0
- package/dist/lifecycle/ufg.d.ts +21 -0
- package/dist/lifecycle/ufg.d.ts.map +1 -0
- package/dist/lifecycle/ufg.js +98 -0
- package/dist/lifecycle/ufg.js.map +1 -0
- package/docs/launch-lifecycle.md +215 -32
- package/examples/.env.example +4 -0
- package/examples/README.md +35 -2
- package/examples/launch-example-support.ts +1 -0
- package/examples/launch-example.ts +1 -0
- package/examples/launch.ts +1 -0
- package/examples/ufg-launch-config.ts +31 -0
- package/package.json +1 -1
- package/src/lifecycle/abi.ts +1 -0
- package/src/lifecycle/calibration.ts +1 -0
- package/src/lifecycle/deployments/ufg-dynamic-pool-20261021.ts +74 -0
- package/src/lifecycle/deployments/ufg-fixed-pool-20261021.ts +74 -0
- package/src/lifecycle/index.ts +1 -0
- package/src/lifecycle/markets.ts +16 -3
- package/src/lifecycle/planner.ts +75 -51
- package/src/lifecycle/presets.ts +27 -6
- package/src/lifecycle/progress.ts +22 -5
- package/src/lifecycle/rpc.ts +3 -2
- package/src/lifecycle/simulation.ts +114 -166
- package/src/lifecycle/transactions.ts +38 -1
- package/src/lifecycle/types.ts +15 -5
- package/src/lifecycle/ufg-abi.ts +173 -0
- package/src/lifecycle/ufg-config.ts +77 -0
- package/src/lifecycle/ufg.ts +86 -0
|
@@ -6,6 +6,8 @@ import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureRe
|
|
|
6
6
|
import { LifecyclePlanningError, type CanonicalLaunchProgress, type LifecycleBlock, type LifecycleMarketProgress, type LifecycleProfile, type LifecycleReceiptReference, type LifecycleReceiptStatus, type LifecycleRpcClient, type PoolBoundHookDeployment, type ReadLaunchProgressOptions } from "./types.js";
|
|
7
7
|
import { deriveKnownPoolBoundHookDeployment } from "./calibration.js";
|
|
8
8
|
import { getKnownLifecycleDeployment, getKnownLifecycleProfile } from "./presets.js";
|
|
9
|
+
import { ufgLaunchProxyV1Abi } from "./ufg-abi.js";
|
|
10
|
+
import { assertUfgLaunchAccount, hashUfgLaunchConfig, UFG_LAUNCH_PROXY_ADDRESS, type UfgLaunchConfig } from "./ufg-config.js";
|
|
9
11
|
|
|
10
12
|
export const ABYSS_LIFECYCLE_CONFIG_SCHEMA = keccak256(stringToHex("(uint8,uint24,bytes32,uint160,(int24,int24,uint128,uint256)[])"));
|
|
11
13
|
|
|
@@ -460,12 +462,17 @@ async function transactionMatchesLaunch(transaction: Record<string, unknown>, op
|
|
|
460
462
|
if (transaction.chainId !== undefined && rpcQuantity(transaction.chainId, "receipt transaction chain") !== planned.plan.chainId) throw new LifecyclePlanningError("RECEIPT_CHAIN", "Receipt transaction belongs to a different chain");
|
|
461
463
|
const to = transaction.to === null ? zeroAddress : rpcHex(transaction.to, "receipt transaction target");
|
|
462
464
|
const input = rpcHex(transaction.input ?? transaction.data, "receipt transaction calldata");
|
|
463
|
-
|
|
465
|
+
const target = planned.ufg === undefined ? planned.plan.orchestrator : UFG_LAUNCH_PROXY_ADDRESS;
|
|
466
|
+
if (to.toLowerCase() === target.toLowerCase()) {
|
|
464
467
|
try {
|
|
465
|
-
const decoded = decodeFunctionData({ abi: launchLifecycleAbi, data: input });
|
|
468
|
+
const decoded = decodeFunctionData({ abi: planned.ufg === undefined ? launchLifecycleAbi : ufgLaunchProxyV1Abi, data: input });
|
|
466
469
|
if (!["launchAtomic", "beginLaunch", "prepareMarkets", "activateLaunch", "cancelLaunch"].includes(decoded.functionName) || !Array.isArray(decoded.args)) return false;
|
|
467
470
|
// Decoded plan layout is validated by the exact ABI, not trusted raw RPC JSON.
|
|
468
471
|
const plan = decoded.args[0] as LaunchPlanV1;
|
|
472
|
+
if (planned.ufg !== undefined && (decoded.functionName === "launchAtomic" || decoded.functionName === "beginLaunch")) {
|
|
473
|
+
const config = decoded.args[decoded.functionName === "launchAtomic" ? 1 : 2] as UfgLaunchConfig;
|
|
474
|
+
if (hashUfgLaunchConfig(config) !== planned.ufg.configHash) return false;
|
|
475
|
+
}
|
|
469
476
|
return hashLaunchPlan(plan).toLowerCase() === planned.planHash.toLowerCase();
|
|
470
477
|
} catch { return false; }
|
|
471
478
|
}
|
|
@@ -477,7 +484,7 @@ async function transactionMatchesLaunch(transaction: Record<string, unknown>, op
|
|
|
477
484
|
if (decoded.functionName !== "approve" || rpcQuantity(transaction.value ?? "0x0", "approval native value") !== 0n) return false;
|
|
478
485
|
spender = decoded.args[0];
|
|
479
486
|
} catch { return false; }
|
|
480
|
-
const escrow = await readLifecycleContract<Address>(options.client, planned.plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], block);
|
|
487
|
+
const escrow = planned.ufg !== undefined ? UFG_LAUNCH_PROXY_ADDRESS : await readLifecycleContract<Address>(options.client, planned.plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], block);
|
|
481
488
|
return spender.toLowerCase() === escrow.toLowerCase();
|
|
482
489
|
}
|
|
483
490
|
|
|
@@ -542,7 +549,10 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
|
|
|
542
549
|
assertActive();
|
|
543
550
|
const chainId = chainIdValue;
|
|
544
551
|
if (chainId !== planned.plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed launch");
|
|
545
|
-
if (planned.
|
|
552
|
+
if (planned.ufg !== undefined) {
|
|
553
|
+
assertUfgLaunchAccount(planned.plan, planned.account);
|
|
554
|
+
if (hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG config differs from its finalized commitment");
|
|
555
|
+
} else if (planned.account.toLowerCase() !== planned.plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
|
|
546
556
|
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");
|
|
547
557
|
const [head, confirmedProgress, token, confirmedNonce, headNonce, pendingNonce] = await Promise.all([
|
|
548
558
|
readLifecycleContract<LaunchProgressV1>(client, planned.plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [planned.launchId], block, planned.account),
|
|
@@ -556,10 +566,17 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
|
|
|
556
566
|
const canonical = confirmedProgress ?? head;
|
|
557
567
|
if (token.toLowerCase() !== planned.predictedToken.toLowerCase()) throw new LifecyclePlanningError("TOKEN_IDENTITY", "Predicted token changed for the committed token configuration");
|
|
558
568
|
for (const progress of [canonical, head]) if (progress.phase !== LifecyclePhase.None) {
|
|
559
|
-
if (progress.planHash.toLowerCase() !== planned.planHash.toLowerCase() || progress.launchId.toLowerCase() !== planned.launchId.toLowerCase() || progress.creator.toLowerCase() !== planned.
|
|
569
|
+
if (progress.planHash.toLowerCase() !== planned.planHash.toLowerCase() || progress.launchId.toLowerCase() !== planned.launchId.toLowerCase() || progress.creator.toLowerCase() !== planned.plan.creator.toLowerCase() || progress.nonce !== planned.plan.nonce || progress.token.toLowerCase() !== token.toLowerCase() || progress.marketCount !== planned.plan.markets.length || progress.buyCount !== planned.plan.buys.length || progress.deadline !== planned.plan.deadline) throw new LifecyclePlanningError("PLAN_REPLAY", "This creator nonce is already bound to a different economic launch");
|
|
560
570
|
if (progress.mode !== (planned.mode === "atomic" ? LifecycleMode.Atomic : LifecycleMode.Staged)) throw new LifecyclePlanningError("MODE_MISMATCH", "Execution mode differs from the already recorded launch");
|
|
561
571
|
if (progress.preparedMarkets > progress.marketCount || progress.phase === LifecyclePhase.Activating || (progress.phase === LifecyclePhase.Ready && progress.preparedMarkets !== progress.marketCount)) throw new LifecyclePlanningError("INVALID_PROGRESS", "Canonical progress violates the committed ordered state machine");
|
|
562
572
|
}
|
|
573
|
+
if (planned.ufg !== undefined && head.phase !== LifecyclePhase.None) {
|
|
574
|
+
const [principal, configHash] = await Promise.all([
|
|
575
|
+
readLifecycleContract<Address>(client, UFG_LAUNCH_PROXY_ADDRESS, ufgLaunchProxyV1Abi, "principalOf", [planned.launchId], block),
|
|
576
|
+
readLifecycleContract<Hash>(client, UFG_LAUNCH_PROXY_ADDRESS, ufgLaunchProxyV1Abi, "launchConfigHash", [planned.launchId], block),
|
|
577
|
+
]);
|
|
578
|
+
if (principal.toLowerCase() !== planned.account.toLowerCase() || configHash.toLowerCase() !== planned.ufg.configHash.toLowerCase()) throw new LifecyclePlanningError("PLAN_REPLAY", "UFG proxy principal or launch config differs from the stored launch");
|
|
579
|
+
}
|
|
563
580
|
const confirmedAccountNonce = rpcQuantity(confirmedNonce, "confirmed account nonce");
|
|
564
581
|
const headAccountNonce = headNonce === undefined ? confirmedAccountNonce : rpcQuantity(headNonce, "head account nonce");
|
|
565
582
|
const pendingAccountNonce = rpcQuantity(pendingNonce, "pending account nonce");
|
package/src/lifecycle/rpc.ts
CHANGED
|
@@ -433,6 +433,7 @@ async function resolveLifecycleLimitsFresh(context: LifecycleLimitContext, sourc
|
|
|
433
433
|
throw new LifecyclePlanningError("LIMIT_CONTEXT_MISMATCH", "Supplied execution policy provenance differs from the canonical chain/account/core/block context");
|
|
434
434
|
}
|
|
435
435
|
const protocol = context.chainId === 4663n ? "nitro" : "evm";
|
|
436
|
+
const maxSimulationGas = configuration.maxSimulationGas ?? (context.chainId === 4663n ? 50_000_000n : undefined);
|
|
436
437
|
let arbOSVersion: bigint | undefined; let maxTxComputeGas: bigint | undefined; let maxBlockComputeGas: bigint | undefined;
|
|
437
438
|
if (protocol === "nitro") {
|
|
438
439
|
try {
|
|
@@ -464,14 +465,14 @@ async function resolveLifecycleLimitsFresh(context: LifecycleLimitContext, sourc
|
|
|
464
465
|
...(configuration.rpcGasLimit === undefined ? ["RPC transaction gas-envelope cap is unknown"] : []),
|
|
465
466
|
...(configuration.accountGasLimit === undefined ? ["Current account/wallet transaction gas-envelope cap is unknown"] : []),
|
|
466
467
|
...(configuration.maxCalldataBytes === undefined ? ["Applicable chain/account/RPC calldata byte cap is unknown"] : []),
|
|
467
|
-
...(
|
|
468
|
+
...(maxSimulationGas === undefined ? ["RPC aggregate simulation gas cap is unknown"] : []),
|
|
468
469
|
];
|
|
469
470
|
assertCurrent();
|
|
470
471
|
return {
|
|
471
472
|
configuration, assertCurrent,
|
|
472
473
|
limits: Object.freeze({
|
|
473
474
|
protocol, executionGasCeiling, transactionGasCeiling, arbOSVersion, maxTxComputeGas, maxBlockComputeGas, headroomBps,
|
|
474
|
-
maxCalldataBytes: configuration.maxCalldataBytes, maxSimulationGas
|
|
475
|
+
maxCalldataBytes: configuration.maxCalldataBytes, maxSimulationGas,
|
|
475
476
|
blockGasLimit: context.block.gasLimit, chainGasLimit: configuration.chainGasLimit,
|
|
476
477
|
rpcGasLimit: configuration.rpcGasLimit, accountGasLimit: configuration.accountGasLimit,
|
|
477
478
|
unknownExecutionConstraints: Object.freeze(unknownExecutionConstraints),
|
|
@@ -4,6 +4,9 @@ import { hashLaunchIdentity, hashLaunchPlan, LifecyclePhase, type LaunchPlanV1,
|
|
|
4
4
|
import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readLifecycleChainId, readNitroPosterGas, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity, 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
|
+
import { hashUfgLaunchConfig } from "./ufg-config.js";
|
|
7
10
|
|
|
8
11
|
function committedPositionCount(plan: LaunchPlanV1): number {
|
|
9
12
|
let count = 0;
|
|
@@ -111,8 +114,6 @@ async function proveNitroBackend(client: LifecycleRpcClient, planned: PlannedLau
|
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunch, block: LifecycleBlock, transactions: readonly LifecycleTransaction[], gasLimits: readonly bigint[], limits: ResolvedLifecycleLimits, gasPrice: bigint, validation: boolean): Promise<LifecycleSimulationStep[]> {
|
|
114
|
-
const totalRequestedGas = gasLimits.reduce((total, gas) => total + gas, 0n);
|
|
115
|
-
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");
|
|
116
117
|
// Natural child-block fees must govern fee-bearing admission; overriding
|
|
117
118
|
// baseFeePerGas with the parent's would fabricate a lower envelope and admit
|
|
118
119
|
// calls that the actual chain would reject. eth_simulateV1 derives the child
|
|
@@ -163,7 +164,7 @@ async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunc
|
|
|
163
164
|
const buy = planned.plan.buys[buyIndex];
|
|
164
165
|
if (buy === undefined || (receipt.quoteSpent[buyIndex] ?? buy.quoteAmountIn + 1n) > buy.quoteAmountIn || (receipt.tokenOut[buyIndex] ?? 0n) < buy.minTokenOut) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not satisfy every ordered buy's budget and output minimum");
|
|
165
166
|
}
|
|
166
|
-
} else if (transaction.kind === "cancel" && !events.some((event) => event.eventName === "LaunchCancelled" && event.args.launchId.toLowerCase() === planned.launchId.toLowerCase() && event.args.creator.toLowerCase() === planned.
|
|
167
|
+
} else if (transaction.kind === "cancel" && !events.some((event) => event.eventName === "LaunchCancelled" && event.args.launchId.toLowerCase() === planned.launchId.toLowerCase() && event.args.creator.toLowerCase() === planned.plan.creator.toLowerCase())) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not cancel the exact committed launch");
|
|
167
168
|
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");
|
|
168
169
|
} catch (failure) { error = failure instanceof LifecyclePlanningError ? failure.message : "Simulation returned an invalid committed postcondition"; }
|
|
169
170
|
}
|
|
@@ -315,7 +316,7 @@ function observedFields<T extends object>(value: T): () => boolean {
|
|
|
315
316
|
|
|
316
317
|
/** Internal continuation owned by one SDK read invocation. Only its positive
|
|
317
318
|
* capability proof is reused; every candidate executes its entire economic sequence. */
|
|
318
|
-
export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true): PreparedLaunchSimulation {
|
|
319
|
+
export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true, reviewedEnvelope?: NextTransactionEnvelope): PreparedLaunchSimulation {
|
|
319
320
|
const { client, planned, fork } = options;
|
|
320
321
|
const signal = options.signal;
|
|
321
322
|
if (signal?.aborted) throw Object.assign(new Error("Lifecycle simulation cancelled"), { name: "AbortError" });
|
|
@@ -346,6 +347,7 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
|
|
|
346
347
|
if (hashLaunchPlan(plan).toLowerCase() !== planHash.toLowerCase() || hashLaunchIdentity(plan).toLowerCase() !== launchId.toLowerCase()) {
|
|
347
348
|
throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed after its simulation context was bound");
|
|
348
349
|
}
|
|
350
|
+
if (planned.ufg !== undefined && hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG treasury config changed after its simulation context was bound");
|
|
349
351
|
};
|
|
350
352
|
const pending = (async () => {
|
|
351
353
|
if (fork !== undefined && lifecycleSourceClient(fork.client) === sourceClient) throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
|
|
@@ -353,7 +355,7 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
|
|
|
353
355
|
resolveLifecycleLimits(context, policy),
|
|
354
356
|
readLifecycleChainId(client),
|
|
355
357
|
lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block),
|
|
356
|
-
hasTransactions ? lifecycleRpc(client, "eth_gasPrice") : undefined,
|
|
358
|
+
hasTransactions && reviewedEnvelope === undefined ? lifecycleRpc(client, "eth_gasPrice") : undefined,
|
|
357
359
|
]));
|
|
358
360
|
})().then((observations) => ({ observations }), (failure: unknown) => ({ failure }));
|
|
359
361
|
// Refused inputs may discard this read-only preparation before it settles.
|
|
@@ -439,7 +441,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
439
441
|
// Standalone calls always produce fresh context, including live chain and fees.
|
|
440
442
|
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");
|
|
441
443
|
const block = pinnedBlock ?? await readLifecycleBlock(options.client);
|
|
442
|
-
const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0);
|
|
444
|
+
const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope);
|
|
443
445
|
try { return await prepared(transactions, nextEnvelope); }
|
|
444
446
|
finally { prepared.close(); }
|
|
445
447
|
}
|
|
@@ -458,7 +460,7 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
458
460
|
const sourceClient = lifecycleSourceClient(client);
|
|
459
461
|
const { block } = context;
|
|
460
462
|
const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = observations;
|
|
461
|
-
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 };
|
|
463
|
+
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[] };
|
|
462
464
|
if (chainId !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
|
|
463
465
|
const accountCode = rpcHex(accountCodeValue, "account code");
|
|
464
466
|
if (accountCode !== "0x") {
|
|
@@ -473,36 +475,48 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
473
475
|
}
|
|
474
476
|
}
|
|
475
477
|
if (transactions.length === 0) { await assertLifecycleBlock(client, block, planned.chainId); return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] }; }
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
478
|
+
if (nextEnvelope !== undefined && (transactions.length !== 1 || typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas <= 0n || nextEnvelope.gas >= 1n << 64n ||
|
|
479
|
+
typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
|
|
480
|
+
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Reviewed replay requires one immediate transaction with exact uint64 gas and uint256 price");
|
|
481
|
+
}
|
|
482
|
+
const allTransactions = transactions;
|
|
483
|
+
if (limits.maxSimulationGas !== undefined && nextEnvelope === undefined) {
|
|
484
|
+
let estimatedGas = 0n, count = 0;
|
|
485
|
+
for (const transaction of transactions) {
|
|
486
|
+
estimatedGas += transactionGasEstimate(planned.plan, transaction);
|
|
487
|
+
if (estimatedGas > limits.maxSimulationGas) break;
|
|
488
|
+
count += 1;
|
|
489
|
+
}
|
|
490
|
+
transactions = transactions.slice(0, count);
|
|
491
|
+
base.deferredTransactionIds = allTransactions.slice(count).map((transaction) => transaction.id);
|
|
492
|
+
if (count === 0) {
|
|
493
|
+
const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
|
|
494
|
+
const exceedsCompute = estimatedGas > computeCap;
|
|
495
|
+
const gasInMillions = (gas: bigint) => {
|
|
496
|
+
const tenths = (gas + 50_000n) / 100_000n;
|
|
497
|
+
return `${tenths / 10n}.${tenths % 10n}`;
|
|
498
|
+
};
|
|
499
|
+
const atomic = allTransactions[0]!.kind === "atomic";
|
|
500
|
+
const reason = exceedsCompute
|
|
501
|
+
? `${atomic ? "Atomic launch" : "Immediate transaction"} needs about ${gasInMillions(estimatedGas)}M gas, above the ${gasInMillions(computeCap)}M per-transaction limit${atomic ? "; use staged" : ""}`
|
|
502
|
+
: "The immediate transaction estimate exceeds the RPC aggregate simulation gas cap; no prefix can be proved";
|
|
503
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
504
|
+
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: exceedsCompute ? "compute" : "simulation-gas", reason };
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const recommendedGasPrice = nextEnvelope === undefined ? rpcQuantity(gasPriceValue, "gas price") : nextEnvelope.gasPrice;
|
|
508
|
+
// New recommendations cover lagging quotes; reviewed fees are replayed unchanged
|
|
509
|
+
// under natural child fees, never refreshed or silently increased.
|
|
481
510
|
const baseFeeEnvelope = (block.baseFeePerGas ?? 0n) * 2n;
|
|
482
511
|
const gasPrice = nextEnvelope?.gasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
|
|
483
512
|
if (gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
|
|
484
|
-
if (nextEnvelope !== undefined && (typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas <= 0n || nextEnvelope.gas >= 1n << 64n ||
|
|
485
|
-
typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
|
|
486
|
-
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Reviewed gas and gas price must be exact positive uint64 gas and nonnegative uint256 price");
|
|
487
|
-
}
|
|
488
513
|
const buffer = (gas: bigint) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
|
|
489
|
-
const computeCeiling = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
|
|
490
|
-
// Nonnegative poster gas cannot make this complete discovery request smaller.
|
|
491
|
-
// A generic controlled fork retains the existing RPC-to-fork fallback.
|
|
492
|
-
if (limits.maxSimulationGas !== undefined && BigInt(transactions.length) * computeCeiling > limits.maxSimulationGas &&
|
|
493
|
-
(limits.protocol === "nitro" || options.fork === undefined)) {
|
|
494
|
-
assertCurrent();
|
|
495
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
496
|
-
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: "simulation-gas", reason: `Stateful sequential RPC simulation unavailable: Sequential request exceeds the current RPC aggregate simulation gas cap; use an isolated controlled fork. ${limits.protocol === "nitro" ? "Generic Anvil/Hardhat forks cannot prove ArbOS compute/poster metering." : "Dependent eth_call estimates are not proof."}` };
|
|
497
|
-
}
|
|
498
514
|
const posterGas: bigint[] = [];
|
|
499
515
|
if (limits.protocol === "nitro") {
|
|
500
|
-
// Lightweight poster reads may overlap the isolated capability request.
|
|
501
|
-
// Economic work is not even queued until both prerequisites have succeeded.
|
|
502
516
|
try {
|
|
503
517
|
const [, estimates] = await Promise.all([
|
|
504
518
|
proveBackend(limits, gasPrice),
|
|
505
|
-
lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
|
|
519
|
+
nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))) : [],
|
|
506
520
|
]);
|
|
507
521
|
posterGas.push(...estimates);
|
|
508
522
|
} catch (failure) {
|
|
@@ -513,176 +527,110 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
513
527
|
}
|
|
514
528
|
}
|
|
515
529
|
assertCurrent();
|
|
516
|
-
// The poster quote allocates gas only. The native exact replay enforces actual
|
|
517
|
-
// compute hold; subtracting this approximate budget from gasUsed would not.
|
|
518
530
|
const discoveryGas = transactions.map((_, index) => {
|
|
519
531
|
const total = limits.executionGasCeiling + (posterGas[index] ?? 0n);
|
|
520
532
|
const capped = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < total ? limits.transactionGasCeiling : total;
|
|
521
533
|
if (capped <= 0n || capped >= 1n << 64n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Compute/poster envelope exceeds uint64 gas");
|
|
522
534
|
return capped;
|
|
523
535
|
});
|
|
524
|
-
let backend: "eth_simulateV1" | "controlled-fork" = "eth_simulateV1";
|
|
525
|
-
let measured: LifecycleSimulationStep[];
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
536
|
+
let backend: "eth_simulateV1" | "controlled-fork" = nextEnvelope !== undefined && limits.protocol === "evm" && options.fork !== undefined ? "controlled-fork" : "eth_simulateV1";
|
|
537
|
+
let measured: LifecycleSimulationStep[] = [];
|
|
538
|
+
if (nextEnvelope === undefined) {
|
|
539
|
+
try {
|
|
540
|
+
measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, discoveryGas, limits, gasPrice, false));
|
|
541
|
+
} catch (failure) {
|
|
542
|
+
if (simulationFailureKind(failure) === "abort") throw failure;
|
|
543
|
+
assertCurrent();
|
|
544
|
+
if (options.fork === undefined || limits.protocol === "nitro") {
|
|
545
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
546
|
+
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."}` };
|
|
547
|
+
}
|
|
548
|
+
backend = "controlled-fork";
|
|
549
|
+
measured = await simulateForkPass(options.fork, planned, block, transactions, discoveryGas, gasPrice, true, limits.headroomBps);
|
|
550
|
+
}
|
|
533
551
|
assertCurrent();
|
|
534
|
-
|
|
552
|
+
const failedIndex = measured.findIndex((step) => !step.success);
|
|
553
|
+
if (failedIndex >= 0 && limits.maxSimulationGas !== undefined &&
|
|
554
|
+
measured.reduce((gas, step) => gas + (step.gasUsed ?? 0n), 0n) * 100n >= limits.maxSimulationGas * 95n) {
|
|
555
|
+
// The provider exhausted its aggregate used-gas budget, not the contract.
|
|
556
|
+
// Preserve only actual successful results; the failed call is deferred too.
|
|
557
|
+
measured = measured.slice(0, failedIndex);
|
|
558
|
+
transactions = transactions.slice(0, failedIndex);
|
|
559
|
+
base.deferredTransactionIds = allTransactions.slice(failedIndex).map((transaction) => transaction.id);
|
|
560
|
+
if (transactions.length === 0) {
|
|
561
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
562
|
+
return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "source", reason: "RPC aggregate simulation gas cap left no successful prefix to prove" };
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const failed = measured.find((step) => !step.success);
|
|
566
|
+
if (failed !== undefined) { await assertLifecycleBlock(client, block, planned.chainId); return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId, failureCategory: failed.failureCategory ?? "opaque", capacityConstraint: failed.failureCategory === "capacity" ? "compute" : undefined }; }
|
|
567
|
+
if (diagnostic) {
|
|
535
568
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
536
|
-
return { ...base,
|
|
569
|
+
return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
|
|
537
570
|
}
|
|
538
|
-
backend = "controlled-fork";
|
|
539
|
-
measured = await simulateForkPass(options.fork, planned, block, transactions, discoveryGas, gasPrice, true, limits.headroomBps);
|
|
540
|
-
}
|
|
541
|
-
assertCurrent();
|
|
542
|
-
const failed = measured.find((step) => !step.success);
|
|
543
|
-
if (failed !== undefined) { await assertLifecycleBlock(client, block, planned.chainId); return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId, failureCategory: failed.failureCategory ?? "opaque", capacityConstraint: failed.failureCategory === "capacity" ? "compute" : undefined }; }
|
|
544
|
-
if (diagnostic) {
|
|
545
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
546
|
-
return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
|
|
547
571
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
572
|
+
const gasLimits = nextEnvelope === undefined ? measured.map((step, index) => {
|
|
573
|
+
const gross = step.gasRequired ?? step.gasUsed!;
|
|
574
|
+
const reserve = transactions[index]?.kind === "activate" || transactions[index]?.kind === "atomic" ? hmmV2ForwardingReserve(planned.plan) : 0n;
|
|
575
|
+
const desired = gross + reserve > buffer(gross) ? gross + reserve : buffer(gross);
|
|
576
|
+
const available = discoveryGas[index]! - (posterGas[index] ?? 0n);
|
|
577
|
+
return (desired < available ? desired : available) + (posterGas[index] ?? 0n);
|
|
578
|
+
}) : [nextEnvelope.gas];
|
|
579
|
+
const exceeds = gasLimits.findIndex((gas, index) =>
|
|
580
|
+
gas <= 0n || gas >= 1n << 64n || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling ||
|
|
581
|
+
nextEnvelope === undefined && (measured[index]!.gasRequired ?? measured[index]!.gasUsed!) > gas - (posterGas[index] ?? 0n) ||
|
|
582
|
+
limits.protocol === "evm" && gas > limits.executionGasCeiling);
|
|
557
583
|
if (exceeds >= 0) {
|
|
558
584
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
559
|
-
return { ...base, executionProof: "unavailable", protocolFit: "failed", backend, admitted: false, steps: measured, failureCategory: "capacity", capacityConstraint:
|
|
585
|
+
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 };
|
|
560
586
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
let verified: LifecycleSimulationStep[];
|
|
568
|
-
let validatedReplayError: string | undefined;
|
|
569
|
-
assertCurrent();
|
|
587
|
+
let verifiedSteps: LifecycleSimulationStep[] = [];
|
|
588
|
+
let admittedGasLimits = gasLimits;
|
|
589
|
+
let replayError: string | undefined;
|
|
590
|
+
const replay = (gas: readonly bigint[]) => backend === "eth_simulateV1"
|
|
591
|
+
? simulateRpcPass(client, planned, block, transactions, gas, limits, gasPrice, true)
|
|
592
|
+
: simulateForkPass(options.fork!, planned, block, transactions, gas, gasPrice, false, limits.headroomBps);
|
|
570
593
|
try {
|
|
571
|
-
|
|
572
|
-
else {
|
|
573
|
-
if (options.fork === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Controlled fork is missing for the exact-gas replay");
|
|
574
|
-
verified = await simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice, false, limits.headroomBps);
|
|
575
|
-
}
|
|
594
|
+
verifiedSteps = await lifecycleStage(client, "simulation.replay", () => replay(gasLimits));
|
|
576
595
|
} catch (failure) {
|
|
577
596
|
const kind = simulationFailureKind(failure);
|
|
578
597
|
if (kind === "abort") throw failure;
|
|
579
598
|
assertCurrent();
|
|
580
|
-
|
|
581
|
-
if (backend !== "eth_simulateV1") {
|
|
599
|
+
if (kind !== "out-of-gas" || backend !== "eth_simulateV1" || nextEnvelope !== undefined) {
|
|
582
600
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
583
|
-
return { ...base, backend, admitted: false, steps:
|
|
601
|
+
return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, failureCategory: kind === "source" ? "source" : "opaque", reason: `Exact-gas validated replay unavailable: ${lifecycleFailureReason(failure)}` };
|
|
584
602
|
}
|
|
585
|
-
|
|
586
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
587
|
-
return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, failureCategory: kind === "source" ? "source" : "opaque", reason: `Exact-gas validated replay unavailable: ${message}` };
|
|
588
|
-
}
|
|
589
|
-
validatedReplayError = message;
|
|
590
|
-
verified = [];
|
|
603
|
+
replayError = lifecycleFailureReason(failure);
|
|
591
604
|
}
|
|
592
605
|
assertCurrent();
|
|
593
|
-
let verifiedSteps = verified;
|
|
594
|
-
let admittedGasLimits = gasLimits;
|
|
595
606
|
let finalFailure = verifiedSteps.find((step) => !step.success);
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
// explicit envelope-discovery replay at the exact execution ceiling on the
|
|
604
|
-
// same pinned block; headroom is never silently raised — the carried gas is
|
|
605
|
-
// the validated envelope itself.
|
|
607
|
+
const failedIndex = verifiedSteps.findIndex((step) => !step.success);
|
|
608
|
+
const rescue = nextEnvelope === undefined && backend === "eth_simulateV1" &&
|
|
609
|
+
(replayError !== undefined && discoveryGas.some((gas, index) => index < transactions.length && gas > gasLimits[index]!) ||
|
|
610
|
+
finalFailure !== undefined && looksGasExhausted(finalFailure, gasLimits[failedIndex]!) && discoveryGas[failedIndex]! > gasLimits[failedIndex]!);
|
|
611
|
+
// One legal-envelope rescue covers thrown/returned genuine OOG. Never retry
|
|
612
|
+
// semantic errors, switch replay backends, or enlarge a held immediate envelope.
|
|
613
|
+
if (rescue) {
|
|
606
614
|
assertCurrent();
|
|
607
615
|
try {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
const retryFailureIndex = retried?.findIndex((step) => !step.success) ?? -1;
|
|
613
|
-
const retryFailure = retried?.[retryFailureIndex];
|
|
614
|
-
if (retried === undefined) {
|
|
615
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
616
|
-
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${validatedReplayError}` };
|
|
617
|
-
}
|
|
618
|
-
admittedGasLimits = ceilingLimits;
|
|
619
|
-
verifiedSteps = retried;
|
|
620
|
-
finalFailure = retryFailure;
|
|
621
|
-
validatedReplayError = undefined;
|
|
622
|
-
if (retryFailure !== undefined) {
|
|
623
|
-
const kind = transactions[retryFailureIndex]?.kind;
|
|
624
|
-
const indivisible = kind === "activate" || kind === "atomic";
|
|
625
|
-
finalFailureReason = `Validated execution at the exact current gas ceiling still failed: ${retryFailure.error ?? "Execution reverted"}${indivisible ? "; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned" : ""}`;
|
|
626
|
-
}
|
|
616
|
+
admittedGasLimits = discoveryGas.slice(0, transactions.length);
|
|
617
|
+
verifiedSteps = await lifecycleStage(client, "simulation.envelope", () => replay(admittedGasLimits));
|
|
618
|
+
finalFailure = verifiedSteps.find((step) => !step.success);
|
|
619
|
+
replayError = undefined;
|
|
627
620
|
} catch (failure) {
|
|
628
621
|
const kind = simulationFailureKind(failure);
|
|
629
622
|
if (kind === "abort") throw failure;
|
|
630
623
|
assertCurrent();
|
|
631
624
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
632
|
-
if (kind !== "out-of-gas") return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, failureCategory: kind === "source" ? "source" : "opaque", reason: `Legal-envelope validated replay unavailable
|
|
633
|
-
return { ...base, backend, admitted: false, steps: measured, reason: `Validated
|
|
634
|
-
}
|
|
635
|
-
} else if (finalFailure !== undefined) {
|
|
636
|
-
const failedIndex = verifiedSteps.findIndex((step) => !step.success);
|
|
637
|
-
const bufferedLimit = gasLimits[failedIndex];
|
|
638
|
-
const failedTransaction = transactions[failedIndex];
|
|
639
|
-
if (bufferedLimit !== undefined && failedTransaction !== undefined && (nextEnvelope === undefined || failedIndex !== 0) &&
|
|
640
|
-
looksGasExhausted(finalFailure, bufferedLimit) && (envelopeDiscoveryGas[failedIndex] ?? 0n) > bufferedLimit) {
|
|
641
|
-
// Buffered replay returned an exhaustion-shaped failed step; learn the true
|
|
642
|
-
// validated envelope with one explicit replay at the exact execution ceiling.
|
|
643
|
-
assertCurrent();
|
|
644
|
-
try {
|
|
645
|
-
const ceilingLimits = envelopeDiscoveryGas;
|
|
646
|
-
const retried = backend === "eth_simulateV1"
|
|
647
|
-
? await lifecycleStage(client, "simulation.envelope", () => simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true))
|
|
648
|
-
: options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
|
|
649
|
-
if (retried === undefined) {
|
|
650
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
651
|
-
return { ...base, backend, admitted: false, steps: measured, reason: "Validated execution at the exact current gas ceiling omitted transaction results" };
|
|
652
|
-
}
|
|
653
|
-
const retryFailureIndex = retried.findIndex((step) => !step.success);
|
|
654
|
-
const retryFailure = retried[retryFailureIndex];
|
|
655
|
-
admittedGasLimits = ceilingLimits;
|
|
656
|
-
verifiedSteps = retried;
|
|
657
|
-
finalFailure = retryFailure;
|
|
658
|
-
if (retryFailure !== undefined) {
|
|
659
|
-
const kind = transactions[retryFailureIndex]?.kind;
|
|
660
|
-
const indivisible = kind === "activate" || kind === "atomic";
|
|
661
|
-
finalFailureReason = `Validated execution at the exact current gas ceiling still failed: ${retryFailure.error ?? "Execution reverted"}${indivisible ? "; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned" : ""}`;
|
|
662
|
-
}
|
|
663
|
-
} catch (failure) {
|
|
664
|
-
const kind = simulationFailureKind(failure);
|
|
665
|
-
if (kind === "abort") throw failure;
|
|
666
|
-
assertCurrent();
|
|
667
|
-
if (kind !== "out-of-gas") {
|
|
668
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
669
|
-
return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, reason: `Legal-envelope validated replay unavailable: ${lifecycleFailureReason(failure)}` };
|
|
670
|
-
}
|
|
671
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
672
|
-
return { ...base, backend, admitted: false, steps: measured, reason: `Validated buffered replay failed (${finalFailure?.error ?? "Execution reverted"}) and execution at the exact gas ceiling still reverted: ${lifecycleFailureReason(failure)}` };
|
|
673
|
-
}
|
|
625
|
+
if (kind !== "out-of-gas") return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, failureCategory: kind === "source" ? "source" : "opaque", reason: `Legal-envelope validated replay unavailable: ${lifecycleFailureReason(failure)}` };
|
|
626
|
+
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${lifecycleFailureReason(failure)}` };
|
|
674
627
|
}
|
|
675
628
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
if (nextEnvelope.gas < currentMinimum) {
|
|
680
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
681
|
-
return { ...base, backend, admitted: false, executionProof: "proved", protocolFit: "failed", steps: verifiedSteps,
|
|
682
|
-
failureCategory: "capacity", capacityConstraint: "gas-envelope", failedTransactionId: transactions[0]?.id,
|
|
683
|
-
reason: "Exact reviewed replay exceeds the held gas envelope" };
|
|
684
|
-
}
|
|
629
|
+
if (replayError !== undefined) {
|
|
630
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
631
|
+
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling failed: ${replayError}` };
|
|
685
632
|
}
|
|
633
|
+
assertCurrent();
|
|
686
634
|
const estimates: LifecycleSimulationStep[] = [];
|
|
687
635
|
// A definitive returned replay failure already refuses execution. Retain its
|
|
688
636
|
// fee observations without inventing a balance or doing redundant payer work.
|
|
@@ -708,6 +656,6 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
708
656
|
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" } });
|
|
709
657
|
}
|
|
710
658
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
711
|
-
return { ...base, backend, executionProof: finalFailure === undefined ? "proved" : "failed", protocolFit: finalFailure === undefined ? "proved" : "unknown", admitted: finalFailure === undefined && affordabilityFailure === undefined, steps: estimates, reason:
|
|
659
|
+
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 };
|
|
712
660
|
}
|
|
713
661
|
|
|
@@ -1,8 +1,45 @@
|
|
|
1
1
|
import { encodeFunctionData } from "viem";
|
|
2
2
|
import { launchLifecycleAbi } from "./abi.js";
|
|
3
|
-
import { LifecycleFundingKind, LifecycleMode } from "./schema.js";
|
|
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";
|
|
7
|
+
import { isHmmProfile } from "./hmm.js";
|
|
8
|
+
|
|
9
|
+
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
|
+
export function preparationGasEstimate(plan: LaunchPlanV1, first: number, count: number): bigint {
|
|
14
|
+
let gas = 0n;
|
|
15
|
+
for (let index = first; index < first + count; index += 1) gas += isHmmProfile(plan.markets[index]!.profileId) ? HMM_V2_PREPARE_GAS : OTHER_MARKET_PREPARE_GAS;
|
|
16
|
+
return gas;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Contiguous cost packing preserves committed market order and recovery progress. */
|
|
20
|
+
export function packPreparationBatches(plan: LaunchPlanV1, prepared: number, computeCap: bigint): number[] {
|
|
21
|
+
const batches = prepared > 0 ? [prepared] : [];
|
|
22
|
+
let count = 0, gas = 0n;
|
|
23
|
+
for (let index = prepared; index < plan.markets.length; index += 1) {
|
|
24
|
+
const estimate = preparationGasEstimate(plan, index, 1);
|
|
25
|
+
if (count > 0 && gas + estimate > computeCap) { batches.push(count); count = 0; gas = 0n; }
|
|
26
|
+
count += 1;
|
|
27
|
+
gas += estimate;
|
|
28
|
+
}
|
|
29
|
+
if (count > 0) batches.push(count);
|
|
30
|
+
return batches;
|
|
31
|
+
}
|
|
32
|
+
|
|
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;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
6
43
|
|
|
7
44
|
/** Pure unsigned launch commands. Every command carries the complete commitment;
|
|
8
45
|
* preparation splits only empty markets, never positions, buys or public opening.
|