@black-market/sdk 0.6.3 → 0.7.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 +28 -4
- package/dist/addresses.d.ts +1 -1
- package/dist/addresses.js +4 -4
- package/dist/launch.d.ts +61 -0
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js +171 -6
- package/dist/launch.js.map +1 -1
- package/dist/lifecycle/abi.d.ts +1999 -253
- package/dist/lifecycle/abi.d.ts.map +1 -1
- package/dist/lifecycle/abi.js +43 -14
- package/dist/lifecycle/abi.js.map +1 -1
- package/dist/lifecycle/allocations.d.ts +11 -0
- package/dist/lifecycle/allocations.d.ts.map +1 -0
- package/dist/lifecycle/allocations.js +36 -0
- package/dist/lifecycle/allocations.js.map +1 -0
- package/dist/lifecycle/calibration.d.ts +19 -0
- package/dist/lifecycle/calibration.d.ts.map +1 -0
- package/dist/lifecycle/calibration.js +65 -0
- package/dist/lifecycle/calibration.js.map +1 -0
- package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.d.ts +11 -0
- package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.d.ts.map +1 -0
- package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.js +10 -0
- package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.js.map +1 -0
- package/dist/lifecycle/index.d.ts +4 -2
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +4 -2
- package/dist/lifecycle/index.js.map +1 -1
- package/dist/lifecycle/markets.d.ts +170 -6
- package/dist/lifecycle/markets.d.ts.map +1 -1
- package/dist/lifecycle/markets.js +164 -37
- package/dist/lifecycle/markets.js.map +1 -1
- package/dist/lifecycle/planner.d.ts +7 -14
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +355 -392
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/presets.d.ts +62 -0
- package/dist/lifecycle/presets.d.ts.map +1 -0
- package/dist/lifecycle/presets.js +195 -0
- package/dist/lifecycle/presets.js.map +1 -0
- package/dist/lifecycle/progress.d.ts +5 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +286 -118
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +13 -2
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +92 -8
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts +14 -1
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +400 -100
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/transactions.d.ts +6 -0
- package/dist/lifecycle/transactions.d.ts.map +1 -0
- package/dist/lifecycle/transactions.js +40 -0
- package/dist/lifecycle/transactions.js.map +1 -0
- package/dist/lifecycle/types.d.ts +39 -2
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +236 -39
- package/examples/README.md +108 -18
- package/examples/generate-lifecycle-commitment-fixture.mjs +3 -1
- package/examples/launch-example-support.mjs +1 -1
- package/examples/launch-example.mjs +73 -26
- package/examples/launch-lifecycle-smoke.mjs +13 -11
- package/examples/launch.mjs +12 -11
- package/examples/quickstart.mjs +1 -1
- package/package.json +1 -1
- package/src/addresses.ts +4 -4
- package/src/launch.ts +213 -6
- package/src/lifecycle/abi.ts +43 -14
- package/src/lifecycle/allocations.ts +35 -0
- package/src/lifecycle/calibration.ts +59 -0
- package/src/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.ts +11 -0
- package/src/lifecycle/index.ts +4 -2
- package/src/lifecycle/markets.ts +169 -35
- package/src/lifecycle/planner.ts +326 -321
- package/src/lifecycle/presets.ts +243 -0
- package/src/lifecycle/progress.ts +254 -102
- package/src/lifecycle/rpc.ts +97 -9
- package/src/lifecycle/simulation.ts +391 -94
- package/src/lifecycle/transactions.ts +34 -0
- package/src/lifecycle/types.ts +35 -3
|
@@ -1,8 +1,59 @@
|
|
|
1
|
-
import { decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctionResult, toHex, type Hex } from "viem";
|
|
2
|
-
import { launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
|
|
3
|
-
import { LifecyclePhase, type LaunchProgressV1, type LaunchReceiptV1 } from "./schema.js";
|
|
4
|
-
import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readNitroPosterGas, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity } from "./rpc.js";
|
|
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";
|
|
1
|
+
import { decodeErrorResult, decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctionResult, parseAbi, toHex, type Hex } from "viem";
|
|
2
|
+
import { fixedFeePoolHookV1Abi, launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
|
|
3
|
+
import { hashLaunchIdentity, hashLaunchPlan, LifecyclePhase, type LaunchPlanV1, type LaunchProgressV1, type LaunchReceiptV1 } from "./schema.js";
|
|
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
|
+
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
|
+
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion } from "./markets.js";
|
|
7
|
+
|
|
8
|
+
function committedPositionCount(plan: LaunchPlanV1): number {
|
|
9
|
+
let count = 0;
|
|
10
|
+
for (const market of plan.markets) {
|
|
11
|
+
if (market.configVersion === 1) count += decodeAbyssLifecycleMarketConfig(market.config).positions.length;
|
|
12
|
+
else if (market.configVersion === 4) count += decodeV4LifecycleMarketConfig(market.config).positions.length;
|
|
13
|
+
else if (isPoolBoundV4ConfigVersion(market.configVersion)) count += decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion).positions.length;
|
|
14
|
+
else throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", "Activation requires a supported committed position schema");
|
|
15
|
+
}
|
|
16
|
+
return count;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const executionGuardErrors = [
|
|
20
|
+
...fixedFeePoolHookV1Abi.filter((item) => item.type === "error"),
|
|
21
|
+
...parseAbi([
|
|
22
|
+
"error WrongDomain()", "error PlanMismatch()", "error LaunchAlreadyExists()", "error InvalidMode()",
|
|
23
|
+
"error InvalidPhase()", "error InvalidPreparationOrder()", "error DeadlineExpired()", "error InvalidBinding()",
|
|
24
|
+
"error InvalidMarket()", "error InsufficientEscrow()", "error InvalidPositions()", "error InvalidPlan()",
|
|
25
|
+
"error InvalidFunding()", "error InvalidFeePolicy()", "error InvalidBuy()", "error DuplicateMarket()",
|
|
26
|
+
"error IneligibleImplementation()", "error QuoteDebtForbidden()", "error SlippageExceeded()", "error FeeBelowMinimum()",
|
|
27
|
+
"error DeploymentFailed()", "error Error(string reason)", "error Panic(uint256 code)",
|
|
28
|
+
]),
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function executionFailure(returnData: Hex | undefined, error: unknown): Pick<LifecycleSimulationStep, "failureCategory" | "decodedError" | "nativeErrorCode" | "nativeErrorKind" | "nativeErrorDataBytes" | "nativeGasCapped" | "error"> {
|
|
32
|
+
const native = error !== null && typeof error === "object" && !Array.isArray(error) ? error as Record<string, unknown> : undefined;
|
|
33
|
+
let nativeData: unknown = native?.data;
|
|
34
|
+
for (let depth = 0; depth < 3 && nativeData !== null && typeof nativeData === "object" && !Array.isArray(nativeData); depth += 1) nativeData = (nativeData as Record<string, unknown>).data;
|
|
35
|
+
const nativeErrorDataBytes = typeof nativeData === "string" && /^0x(?:[0-9a-fA-F]{2})*$/.test(nativeData) ? (nativeData.length - 2) / 2 : undefined;
|
|
36
|
+
const nativeErrorCode = typeof native?.code === "number" && Number.isSafeInteger(native.code) ? native.code : undefined;
|
|
37
|
+
const message = typeof native?.message === "string" ? native.message : "";
|
|
38
|
+
const nativeGasCapped = message.endsWith(" (gas limit was capped by the RPC server's global gas cap)");
|
|
39
|
+
// Native eth_simulateV1 uses -32015 for VM errors; -32000 is also used by RPC clients.
|
|
40
|
+
// Empty data and an explicit OOG report are required. Malformed data or proximity is not proof.
|
|
41
|
+
const outOfGas = (returnData === undefined || returnData === "0x") && (native?.data === undefined || nativeErrorDataBytes === 0) &&
|
|
42
|
+
(nativeErrorCode === -32015 || nativeErrorCode === -32000) && /^out of gas\b/i.test(message);
|
|
43
|
+
const nativeErrorKind = native === undefined ? "missing" : outOfGas ? "out-of-gas" : nativeErrorCode === 3 ? "execution-reverted" : nativeErrorCode === -32015 ? "vm-error" : "other";
|
|
44
|
+
const diagnostic = { nativeErrorCode, nativeErrorKind, nativeErrorDataBytes, nativeGasCapped } as const;
|
|
45
|
+
const data = returnData === undefined || returnData === "0x" ? nativeData : returnData;
|
|
46
|
+
if (typeof data === "string" && /^0x(?:[0-9a-fA-F]{2}){4,}$/.test(data)) {
|
|
47
|
+
try {
|
|
48
|
+
const { errorName } = decodeErrorResult({ abi: executionGuardErrors, data: data as Hex });
|
|
49
|
+
return { ...diagnostic, failureCategory: errorName === "DeploymentFailed" ? "opaque" : "semantic", decodedError: errorName, error: `Execution reverted (${errorName})` };
|
|
50
|
+
} catch { return { ...diagnostic, failureCategory: "opaque", error: "Execution reverted without a reviewed error" }; }
|
|
51
|
+
}
|
|
52
|
+
if (outOfGas) return { ...diagnostic, failureCategory: nativeGasCapped ? "source" : "capacity", error: nativeGasCapped ? "Native execution ran out of gas under the reported RPC gas cap" : "Native execution ran out of gas" };
|
|
53
|
+
return { ...diagnostic, failureCategory: "opaque", error: nativeErrorKind === "execution-reverted" ? "Native execution reverted without a reviewed error" : "Execution failed without a reviewed error" };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const nitroProofsByInvocation = new WeakMap<LifecycleRpcClient, Map<string, Promise<void>>>();
|
|
6
57
|
|
|
7
58
|
function conditionRequest(condition: LifecyclePostcondition, planned: PlannedLaunch): { to: Hex; data: Hex } {
|
|
8
59
|
if (condition.kind === "allowance") return {
|
|
@@ -85,8 +136,8 @@ async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunc
|
|
|
85
136
|
if (gasRequired < gasUsed || gasRequired > (gasLimits[index] ?? 0n)) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Simulated gas use is outside the exact requested envelope");
|
|
86
137
|
const returnData = rpcHex(call.returnData, "simulated return data");
|
|
87
138
|
const success = rpcQuantity(call.status, "simulated status") === 1n;
|
|
88
|
-
|
|
89
|
-
|
|
139
|
+
const failureDiagnostic = success ? undefined : executionFailure(returnData, call.error);
|
|
140
|
+
let error = failureDiagnostic?.error;
|
|
90
141
|
if (success) {
|
|
91
142
|
try {
|
|
92
143
|
// Verification reads must not become synthetic transactions that consume creator nonce/fees.
|
|
@@ -106,26 +157,40 @@ async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunc
|
|
|
106
157
|
} else if (transaction.kind === "atomic" || transaction.kind === "activate") {
|
|
107
158
|
const functionName = transaction.kind === "atomic" ? "launchAtomic" : "activateLaunch";
|
|
108
159
|
const receipt = decodeFunctionResult({ abi: launchLifecycleAbi, functionName, data: returnData }) as LaunchReceiptV1;
|
|
109
|
-
|
|
160
|
+
const positionCount = committedPositionCount(planned.plan);
|
|
161
|
+
if (receipt.planHash.toLowerCase() !== planned.planHash.toLowerCase() || receipt.launchId.toLowerCase() !== planned.launchId.toLowerCase() || receipt.token.toLowerCase() !== planned.predictedToken.toLowerCase() || receipt.marketCount !== planned.plan.markets.length || receipt.positionCount !== positionCount || receipt.quoteSpent.length !== planned.plan.buys.length || receipt.tokenOut.length !== planned.plan.buys.length || !events.some((event) => event.eventName === "LaunchActivated" && event.args.launchId.toLowerCase() === planned.launchId.toLowerCase() && event.args.planHash.toLowerCase() === planned.planHash.toLowerCase() && event.args.token.toLowerCase() === planned.predictedToken.toLowerCase() && event.args.marketCount === planned.plan.markets.length && event.args.positionCount === positionCount)) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not atomically complete every committed market, position and ordered buy");
|
|
110
162
|
for (let buyIndex = 0; buyIndex < planned.plan.buys.length; buyIndex += 1) {
|
|
111
163
|
const buy = planned.plan.buys[buyIndex];
|
|
112
164
|
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");
|
|
113
165
|
}
|
|
114
166
|
} else if (transaction.kind === "cancel" && !events.some((event) => event.eventName === "LaunchCancelled" && event.args.launchId.toLowerCase() === planned.launchId.toLowerCase() && event.args.creator.toLowerCase() === planned.account.toLowerCase())) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not cancel the exact committed launch");
|
|
115
167
|
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");
|
|
116
|
-
} catch (failure) { error = failure instanceof
|
|
168
|
+
} catch (failure) { error = failure instanceof LifecyclePlanningError ? failure.message : "Simulation returned an invalid committed postcondition"; }
|
|
117
169
|
}
|
|
118
|
-
steps.push({ transactionId: transaction.id, success: success && error === undefined, gasUsed, gasRequired, gasLimit: gasLimits[index], returnData, error });
|
|
170
|
+
steps.push({ transactionId: transaction.id, success: success && error === undefined, gasUsed, gasRequired, gasLimit: gasLimits[index], returnData, error, ...failureDiagnostic, ...(success && error !== undefined ? { failureCategory: "postcondition" as const } : {}) });
|
|
119
171
|
if (!success || error !== undefined) break;
|
|
120
172
|
}
|
|
121
173
|
return steps;
|
|
122
174
|
}
|
|
123
|
-
/** A
|
|
124
|
-
* exhaustion (EIP-150 retention starves forwarded sub-calls; the surfaced revert bytes
|
|
125
|
-
* may be a wrapped inner bubble rather than empty). Discovery is safe: a genuine
|
|
126
|
-
* business revert also fails at the exact ceiling, so it can never be masked. */
|
|
175
|
+
/** A decoded semantic refusal or opaque deployment wrapper is never gas evidence. */
|
|
127
176
|
function looksGasExhausted(step: LifecycleSimulationStep, gasLimit: bigint): boolean {
|
|
128
|
-
return !step.success && step.gasUsed !== undefined && step.gasUsed >= gasLimit - gasLimit / 64n;
|
|
177
|
+
return !step.success && step.failureCategory === "capacity" && step.gasUsed !== undefined && step.gasUsed >= gasLimit - gasLimit / 64n;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function simulationFailureKind(failure: unknown): "abort" | "out-of-gas" | "execution" | "source" {
|
|
181
|
+
for (let depth = 0; depth < 8 && failure !== null && typeof failure === "object"; depth += 1) {
|
|
182
|
+
const error = failure as { name?: unknown; code?: unknown; status?: unknown; statusCode?: unknown; message?: unknown; details?: unknown; data?: unknown; cause?: unknown };
|
|
183
|
+
if (error.name === "AbortError") return "abort";
|
|
184
|
+
const status = error.status ?? error.statusCode;
|
|
185
|
+
if (typeof status === "number" && status >= 400 && status <= 599) return "source";
|
|
186
|
+
const message = typeof error.details === "string" ? error.details : typeof error.message === "string" ? error.message : "";
|
|
187
|
+
const execution = executionFailure(undefined, { ...error, message });
|
|
188
|
+
if (execution.nativeErrorKind === "out-of-gas" && execution.nativeGasCapped) return "source";
|
|
189
|
+
if (execution.failureCategory === "capacity") return "out-of-gas";
|
|
190
|
+
if (error.code === 3 || error.code === -32000 && /^execution (?:failed|reverted)\b/i.test(message)) return "execution";
|
|
191
|
+
failure = error.cause;
|
|
192
|
+
}
|
|
193
|
+
return "source";
|
|
129
194
|
}
|
|
130
195
|
|
|
131
196
|
async function waitForkReceipt(client: LifecycleRpcClient, transactionHash: Hex, timeoutMs: number): Promise<Record<string, unknown>> {
|
|
@@ -196,7 +261,7 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
|
|
|
196
261
|
const activated = events.filter((event) => event.eventName === "LaunchActivated");
|
|
197
262
|
const buys = events.filter((event) => event.eventName === "InitialBuyExecuted");
|
|
198
263
|
if (activated.length !== 1 || !activated.some((event) => event.eventName === "LaunchActivated" && event.args.launchId.toLowerCase() === planned.launchId.toLowerCase() && event.args.planHash.toLowerCase() === planned.planHash.toLowerCase() && event.args.token.toLowerCase() === planned.predictedToken.toLowerCase()) ||
|
|
199
|
-
progress.phase !== LifecyclePhase.Active || progress.planHash.toLowerCase() !== planned.planHash.toLowerCase() || progress.token.toLowerCase() !== planned.predictedToken.toLowerCase() ||
|
|
264
|
+
progress.phase !== LifecyclePhase.Active || progress.planHash.toLowerCase() !== planned.planHash.toLowerCase() || progress.token.toLowerCase() !== planned.predictedToken.toLowerCase() || progress.marketCount !== planned.plan.markets.length || progress.positionCount !== committedPositionCount(planned.plan) ||
|
|
200
265
|
buys.length !== planned.plan.buys.length) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Actual fork activation differs from the exact committed launch");
|
|
201
266
|
const quoteSpent: bigint[] = []; const tokenOut: bigint[] = [];
|
|
202
267
|
for (let buyIndex = 0; buyIndex < buys.length; buyIndex += 1) {
|
|
@@ -219,7 +284,7 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
|
|
|
219
284
|
}
|
|
220
285
|
steps.push({ transactionId: transaction.id, success: true, gasUsed, gasLimit: gas, gasRequired, returnData });
|
|
221
286
|
} catch (failure) {
|
|
222
|
-
steps.push({ transactionId: transaction.id, success: false, gasLimit: gas, error:
|
|
287
|
+
steps.push({ transactionId: transaction.id, success: false, gasLimit: gas, error: lifecycleFailureReason(failure) });
|
|
223
288
|
break;
|
|
224
289
|
}
|
|
225
290
|
}
|
|
@@ -233,55 +298,221 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
|
|
|
233
298
|
}
|
|
234
299
|
}
|
|
235
300
|
|
|
236
|
-
|
|
301
|
+
type SimulationObservations = [LifecycleLimitObservation, unknown, unknown, unknown];
|
|
302
|
+
/** Internal, operation-local fee envelope for the already matched immediate step. */
|
|
303
|
+
export type NextTransactionEnvelope = Readonly<{ gas: bigint; gasPrice: bigint }>;
|
|
304
|
+
export type PreparedLaunchSimulation = ((transactions: readonly LifecycleTransaction[], nextEnvelope?: NextTransactionEnvelope) => Promise<LifecycleSimulation>) & {
|
|
305
|
+
/** Unadmitted diagnostic results only; final admission always performs validated replay. */
|
|
306
|
+
measure(transactions: readonly LifecycleTransaction[]): Promise<LifecycleSimulation>;
|
|
307
|
+
close(): void;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
function observedFields<T extends object>(value: T): () => boolean {
|
|
311
|
+
const keys = Object.keys(value) as (keyof T)[];
|
|
312
|
+
const values = keys.map((key) => value[key]);
|
|
313
|
+
return () => keys.length === Object.keys(value).length && keys.every((key, index) => value[key] === values[index]);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Internal continuation owned by one SDK read invocation. Only its positive
|
|
317
|
+
* capability proof is reused; every candidate executes its entire economic sequence. */
|
|
318
|
+
export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true): PreparedLaunchSimulation {
|
|
319
|
+
const { client, planned, fork } = options;
|
|
320
|
+
const signal = options.signal;
|
|
321
|
+
if (signal?.aborted) throw Object.assign(new Error("Lifecycle simulation cancelled"), { name: "AbortError" });
|
|
322
|
+
const sourceClient = lifecycleSourceClient(client), sourceRequest = sourceClient.request;
|
|
323
|
+
const sourceBatching = sourceClient.supportsReadBatching;
|
|
324
|
+
const reusable = assertLifecycleReadClientOpen(client);
|
|
325
|
+
const plan = planned.plan, planHash = planned.planHash, launchId = planned.launchId;
|
|
326
|
+
const optionsLimits = options.limits, plannedLimits = planned.limits;
|
|
327
|
+
const policy = optionsLimits === undefined ? plannedLimits : optionsLimits;
|
|
328
|
+
const blockMatches = observedFields(block);
|
|
329
|
+
const forkMatches = fork === undefined ? undefined : observedFields(fork);
|
|
330
|
+
const forkRequest = fork?.client.request;
|
|
331
|
+
const plannedMatches = observedFields(planned);
|
|
332
|
+
const context: LifecycleLimitContext = Object.freeze({ client, account: planned.account, orchestrator: plan.orchestrator, block: Object.freeze({ ...block }), chainId: planned.chainId });
|
|
333
|
+
let closed = false, active = false, consumed = false, capabilityProved = false;
|
|
334
|
+
let capabilityPending: Promise<void> | undefined;
|
|
335
|
+
const assertContext = () => {
|
|
336
|
+
if (closed) throw new LifecyclePlanningError("INVALID_SIMULATION", "Prepared simulation continuation has closed");
|
|
337
|
+
if (signal?.aborted) throw Object.assign(new Error("Lifecycle simulation cancelled"), { name: "AbortError" });
|
|
338
|
+
assertLifecycleReadClientOpen(client);
|
|
339
|
+
if (options.client !== client || options.planned !== planned || options.limits !== optionsLimits || options.fork !== fork || options.signal !== signal ||
|
|
340
|
+
lifecycleSourceClient(client) !== sourceClient || sourceClient.request !== sourceRequest || !plannedMatches() || !blockMatches() ||
|
|
341
|
+
sourceClient.supportsReadBatching !== sourceBatching ||
|
|
342
|
+
planned.plan !== plan || planned.account !== context.account || plan.orchestrator !== context.orchestrator || planned.chainId !== context.chainId ||
|
|
343
|
+
planned.limits !== plannedLimits || forkMatches !== undefined && !forkMatches() || fork?.client.request !== forkRequest) {
|
|
344
|
+
throw new LifecyclePlanningError("LIMIT_CONTEXT_MISMATCH", "Prepared simulation differs from its source/account/core/plan/header/policy context");
|
|
345
|
+
}
|
|
346
|
+
if (hashLaunchPlan(plan).toLowerCase() !== planHash.toLowerCase() || hashLaunchIdentity(plan).toLowerCase() !== launchId.toLowerCase()) {
|
|
347
|
+
throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed after its simulation context was bound");
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
const pending = (async () => {
|
|
351
|
+
if (fork !== undefined && lifecycleSourceClient(fork.client) === sourceClient) throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
|
|
352
|
+
return lifecycleStage(client, "simulation.context", () => Promise.all([
|
|
353
|
+
resolveLifecycleLimits(context, policy),
|
|
354
|
+
readLifecycleChainId(client),
|
|
355
|
+
lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block),
|
|
356
|
+
hasTransactions ? lifecycleRpc(client, "eth_gasPrice") : undefined,
|
|
357
|
+
]));
|
|
358
|
+
})().then((observations) => ({ observations }), (failure: unknown) => ({ failure }));
|
|
359
|
+
// Refused inputs may discard this read-only preparation before it settles.
|
|
360
|
+
// Its rejection is handled immediately; no capability or economic work starts.
|
|
361
|
+
const run = async (transactions: readonly LifecycleTransaction[], diagnostic: boolean, nextEnvelope?: NextTransactionEnvelope): Promise<LifecycleSimulation> => {
|
|
362
|
+
if (active || consumed && !reusable || !hasTransactions && transactions.length !== 0) {
|
|
363
|
+
closed = true;
|
|
364
|
+
throw new LifecyclePlanningError("INVALID_SIMULATION", "Prepared candidates require sequential consumption by their SDK invocation owner");
|
|
365
|
+
}
|
|
366
|
+
active = true;
|
|
367
|
+
consumed = true;
|
|
368
|
+
try {
|
|
369
|
+
assertContext();
|
|
370
|
+
const envelopeMatches = nextEnvelope === undefined ? undefined : observedFields(nextEnvelope);
|
|
371
|
+
const candidates = transactions.map((transaction) => ({
|
|
372
|
+
transaction, matches: observedFields(transaction),
|
|
373
|
+
dependencies: [...transaction.dependencies],
|
|
374
|
+
conditions: transaction.postconditions.map((condition) => ({ condition, matches: observedFields(condition) })),
|
|
375
|
+
}));
|
|
376
|
+
const result = await pending;
|
|
377
|
+
if ("failure" in result) throw result.failure;
|
|
378
|
+
const assertCurrent = () => {
|
|
379
|
+
assertContext();
|
|
380
|
+
result.observations[0].assertCurrent();
|
|
381
|
+
if (envelopeMatches !== undefined && !envelopeMatches()) throw new LifecyclePlanningError("INVALID_SIMULATION", "Reviewed gas envelope changed during its complete economic proof");
|
|
382
|
+
if (transactions.length !== candidates.length || candidates.some(({ transaction, matches, dependencies, conditions }, index) =>
|
|
383
|
+
transactions[index] !== transaction || !matches() || transaction.dependencies.length !== dependencies.length ||
|
|
384
|
+
dependencies.some((dependency, ordinal) => transaction.dependencies[ordinal] !== dependency) ||
|
|
385
|
+
transaction.postconditions.length !== conditions.length || conditions.some(({ condition, matches: conditionMatches }, ordinal) =>
|
|
386
|
+
transaction.postconditions[ordinal] !== condition || !conditionMatches()))) {
|
|
387
|
+
throw new LifecyclePlanningError("INVALID_SIMULATION", "Candidate transactions changed during their complete economic proof");
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
assertCurrent();
|
|
391
|
+
const proveBackend = async (limits: ResolvedLifecycleLimits, gasPrice: bigint) => {
|
|
392
|
+
assertCurrent();
|
|
393
|
+
if (capabilityProved) return;
|
|
394
|
+
if (capabilityPending === undefined) {
|
|
395
|
+
const key = JSON.stringify([context.chainId.toString(), context.account.toLowerCase(), context.orchestrator.toLowerCase(), context.block.number.toString(), context.block.hash.toLowerCase(), context.block.timestamp.toString(), context.block.gasLimit.toString(), limits.arbOSVersion?.toString(), limits.maxTxComputeGas?.toString(), limits.maxBlockComputeGas?.toString(), limits.executionGasCeiling.toString(), limits.maxSimulationGas?.toString(), gasPrice.toString()]);
|
|
396
|
+
let proofs = reusable ? nitroProofsByInvocation.get(client) : undefined;
|
|
397
|
+
if (reusable && proofs === undefined) { proofs = new Map(); nitroProofsByInvocation.set(client, proofs); }
|
|
398
|
+
const ownedProofs = proofs;
|
|
399
|
+
let nativeProof = ownedProofs?.get(key);
|
|
400
|
+
if (nativeProof === undefined) {
|
|
401
|
+
nativeProof = lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, context.block, limits, gasPrice));
|
|
402
|
+
ownedProofs?.set(key, nativeProof);
|
|
403
|
+
const pending = nativeProof;
|
|
404
|
+
void pending.catch(() => { if (ownedProofs?.get(key) === pending) ownedProofs?.delete(key); });
|
|
405
|
+
}
|
|
406
|
+
const proof = nativeProof.then(() => {
|
|
407
|
+
assertContext();
|
|
408
|
+
result.observations[0].assertCurrent();
|
|
409
|
+
capabilityProved = true;
|
|
410
|
+
});
|
|
411
|
+
capabilityPending = proof;
|
|
412
|
+
void proof.then(
|
|
413
|
+
() => { if (capabilityPending === proof) capabilityPending = undefined; },
|
|
414
|
+
() => { if (capabilityPending === proof) capabilityPending = undefined; },
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
await capabilityPending;
|
|
418
|
+
assertCurrent();
|
|
419
|
+
};
|
|
420
|
+
const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope);
|
|
421
|
+
assertCurrent();
|
|
422
|
+
return simulation;
|
|
423
|
+
} catch (failure) {
|
|
424
|
+
closed = true;
|
|
425
|
+
capabilityProved = false;
|
|
426
|
+
capabilityPending = undefined;
|
|
427
|
+
throw failure;
|
|
428
|
+
} finally { active = false; }
|
|
429
|
+
};
|
|
430
|
+
const simulate: PreparedLaunchSimulation = Object.assign(
|
|
431
|
+
(transactions: readonly LifecycleTransaction[], nextEnvelope?: NextTransactionEnvelope) => run(transactions, false, nextEnvelope),
|
|
432
|
+
{ measure: (transactions: readonly LifecycleTransaction[]) => run(transactions, true),
|
|
433
|
+
close() { closed = true; capabilityProved = false; capabilityPending = undefined; } },
|
|
434
|
+
);
|
|
435
|
+
return simulate;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export async function simulateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], pinnedBlock?: LifecycleBlock, nextEnvelope?: NextTransactionEnvelope): Promise<LifecycleSimulation> {
|
|
439
|
+
// Standalone calls always produce fresh context, including live chain and fees.
|
|
440
|
+
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
|
+
const block = pinnedBlock ?? await readLifecycleBlock(options.client);
|
|
442
|
+
const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0);
|
|
443
|
+
try { return await prepared(transactions, nextEnvelope); }
|
|
444
|
+
finally { prepared.close(); }
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function simulateLaunchTransactionsWithContext(
|
|
448
|
+
options: SimulateLaunchPlanOptions,
|
|
449
|
+
transactions: readonly LifecycleTransaction[],
|
|
450
|
+
context: LifecycleLimitContext,
|
|
451
|
+
observations: SimulationObservations,
|
|
452
|
+
proveBackend: (limits: ResolvedLifecycleLimits, gasPrice: bigint) => Promise<void>,
|
|
453
|
+
assertCurrent: () => void,
|
|
454
|
+
diagnostic: boolean,
|
|
455
|
+
nextEnvelope?: NextTransactionEnvelope,
|
|
456
|
+
): Promise<LifecycleSimulation> {
|
|
237
457
|
const { client, planned } = options;
|
|
238
458
|
const sourceClient = lifecycleSourceClient(client);
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
const context = { client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId };
|
|
242
|
-
const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = await lifecycleStage(client, "simulation.context", () => Promise.all([
|
|
243
|
-
resolveLifecycleLimits(context, options.limits === undefined ? planned.limits : options.limits),
|
|
244
|
-
lifecycleRpc(client, "eth_chainId"),
|
|
245
|
-
lifecyclePinnedRpc(client, "eth_getCode", [planned.account, toHex(block.number)], block),
|
|
246
|
-
transactions.length === 0 ? undefined : lifecycleRpc(client, "eth_gasPrice"),
|
|
247
|
-
]));
|
|
459
|
+
const { block } = context;
|
|
460
|
+
const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = observations;
|
|
248
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 };
|
|
249
|
-
if (
|
|
462
|
+
if (chainId !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
|
|
250
463
|
const accountCode = rpcHex(accountCodeValue, "account code");
|
|
251
|
-
if (accountCode !== "0x")
|
|
464
|
+
if (accountCode !== "0x") {
|
|
465
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
466
|
+
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
|
|
467
|
+
}
|
|
252
468
|
for (const transaction of transactions) {
|
|
253
469
|
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");
|
|
254
|
-
if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes)
|
|
470
|
+
if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) {
|
|
471
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
472
|
+
return { ...base, executionProof: "unavailable", protocolFit: "failed", backend: "unavailable", admitted: false, steps: [], failedTransactionId: transaction.id, failureCategory: "capacity", capacityConstraint: "calldata", reason: "Exact calldata exceeds the supplied chain/account/RPC byte cap" };
|
|
473
|
+
}
|
|
255
474
|
}
|
|
256
475
|
if (transactions.length === 0) { await assertLifecycleBlock(client, block, planned.chainId); return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] }; }
|
|
257
|
-
const
|
|
476
|
+
const recommendedGasPrice = rpcQuantity(gasPriceValue, "gas price");
|
|
477
|
+
if (recommendedGasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Recommended gas price must be a uint256 quantity");
|
|
478
|
+
// Initial construction reserves twice the pinned base fee if the quote lags.
|
|
479
|
+
// Reviewed fees are not new recommendations: native replay must accept them
|
|
480
|
+
// under natural child fees, with no base-fee override or silent fee increase.
|
|
481
|
+
const baseFeeEnvelope = (block.baseFeePerGas ?? 0n) * 2n;
|
|
482
|
+
const gasPrice = nextEnvelope?.gasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
|
|
258
483
|
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
|
+
}
|
|
259
488
|
const buffer = (gas: bigint) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
|
|
260
489
|
const computeCeiling = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
|
|
261
|
-
|
|
262
|
-
|
|
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
|
+
}
|
|
263
498
|
const posterGas: bigint[] = [];
|
|
264
499
|
if (limits.protocol === "nitro") {
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
const nitroEvidence = Promise.all([
|
|
268
|
-
lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, block, limits, gasPrice)),
|
|
269
|
-
lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
|
|
270
|
-
]);
|
|
271
|
-
if (sourceClient.supportsReadBatching === true) {
|
|
272
|
-
// Handle settlement immediately so a failed measurement cannot escape while
|
|
273
|
-
// the independent metering evidence is still pending. It is never admission.
|
|
274
|
-
parallelMeasurement = lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false))
|
|
275
|
-
.then((steps) => ({ steps }), (failure: unknown) => ({ failure }));
|
|
276
|
-
}
|
|
500
|
+
// Lightweight poster reads may overlap the isolated capability request.
|
|
501
|
+
// Economic work is not even queued until both prerequisites have succeeded.
|
|
277
502
|
try {
|
|
278
|
-
const [, estimates] = await
|
|
503
|
+
const [, estimates] = await Promise.all([
|
|
504
|
+
proveBackend(limits, gasPrice),
|
|
505
|
+
lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
|
|
506
|
+
]);
|
|
279
507
|
posterGas.push(...estimates);
|
|
280
508
|
} catch (failure) {
|
|
509
|
+
if (simulationFailureKind(failure) === "abort") throw failure;
|
|
510
|
+
assertCurrent();
|
|
281
511
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
282
|
-
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Native Nitro compute/poster proof unavailable: ${
|
|
512
|
+
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Native Nitro compute/poster proof unavailable: ${lifecycleFailureReason(failure)}. Generic Anvil/Hardhat forks cannot prove ArbOS metering.` };
|
|
283
513
|
}
|
|
284
514
|
}
|
|
515
|
+
assertCurrent();
|
|
285
516
|
// The poster quote allocates gas only. The native exact replay enforces actual
|
|
286
517
|
// compute hold; subtracting this approximate budget from gasUsed would not.
|
|
287
518
|
const discoveryGas = transactions.map((_, index) => {
|
|
@@ -292,40 +523,53 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
292
523
|
});
|
|
293
524
|
let backend: "eth_simulateV1" | "controlled-fork" = "eth_simulateV1";
|
|
294
525
|
let measured: LifecycleSimulationStep[];
|
|
295
|
-
//
|
|
526
|
+
// A permissive measurement needs the legal complete compute + poster envelope.
|
|
527
|
+
// It is not proof: fixed compute/headroom checks and exact validated replay still follow.
|
|
296
528
|
try {
|
|
297
|
-
|
|
298
|
-
else {
|
|
299
|
-
const observation = await parallelMeasurement;
|
|
300
|
-
if ("failure" in observation) throw observation.failure;
|
|
301
|
-
measured = observation.steps;
|
|
302
|
-
}
|
|
529
|
+
measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, discoveryGas, limits, gasPrice, false));
|
|
303
530
|
}
|
|
304
531
|
catch (failure) {
|
|
532
|
+
if (simulationFailureKind(failure) === "abort") throw failure;
|
|
533
|
+
assertCurrent();
|
|
305
534
|
if (options.fork === undefined || limits.protocol === "nitro") {
|
|
306
535
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
307
|
-
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Stateful sequential RPC simulation unavailable: ${
|
|
536
|
+
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."}` };
|
|
308
537
|
}
|
|
309
538
|
backend = "controlled-fork";
|
|
310
|
-
measured = await simulateForkPass(options.fork, planned, block, transactions,
|
|
539
|
+
measured = await simulateForkPass(options.fork, planned, block, transactions, discoveryGas, gasPrice, true, limits.headroomBps);
|
|
311
540
|
}
|
|
541
|
+
assertCurrent();
|
|
312
542
|
const failed = measured.find((step) => !step.success);
|
|
313
|
-
if (failed !== undefined) { await assertLifecycleBlock(client, block, planned.chainId); return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId }; }
|
|
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
|
+
}
|
|
314
548
|
const computeGas = measured.map((step) => buffer(step.gasRequired ?? step.gasUsed ?? computeCeiling));
|
|
315
549
|
const gasLimits = computeGas.map((gas, index) => gas + (posterGas[index] ?? 0n));
|
|
316
550
|
const exceeds = gasLimits.findIndex((gas, index) => gas <= 0n || gas >= 1n << 64n || (computeGas[index] ?? 0n) > limits.executionGasCeiling || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling);
|
|
317
551
|
if (exceeds >= 0) {
|
|
318
552
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
319
|
-
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 };
|
|
553
|
+
return { ...base, executionProof: "unavailable", protocolFit: "failed", backend, admitted: false, steps: measured, failureCategory: "capacity", capacityConstraint: (computeGas[exceeds] ?? 0n) > limits.executionGasCeiling ? "compute" : "gas-envelope", reason: "Execution compute/headroom or complete poster gas envelope is outside positive uint64 gas and current protocol/known transaction limits", failedTransactionId: transactions[exceeds]?.id };
|
|
554
|
+
}
|
|
555
|
+
if (nextEnvelope !== undefined) {
|
|
556
|
+
if (nextEnvelope.gas < (gasLimits[0] ?? 0n) || limits.transactionGasCeiling !== undefined && nextEnvelope.gas > limits.transactionGasCeiling) {
|
|
557
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
558
|
+
return { ...base, executionProof: "unavailable", protocolFit: "failed", backend, admitted: false, steps: measured,
|
|
559
|
+
failureCategory: "capacity", capacityConstraint: "gas-envelope", failedTransactionId: transactions[0]?.id,
|
|
560
|
+
reason: "Reviewed gas cannot cover current minimum compute/headroom/poster requirements or exceeds the current transaction ceiling" };
|
|
561
|
+
}
|
|
562
|
+
gasLimits[0] = nextEnvelope.gas;
|
|
320
563
|
}
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
564
|
+
// Envelope discovery may enlarge future unfinished steps, never the reviewed
|
|
565
|
+
// immediate one. Every retry still executes the entire sequence at held price.
|
|
566
|
+
const envelopeDiscoveryGas = nextEnvelope === undefined ? discoveryGas : discoveryGas.map((gas, index) => index === 0 ? nextEnvelope.gas : gas);
|
|
567
|
+
// Replay the complete sequence from its original state at the exact buffered
|
|
568
|
+
// limits. Only explicit native OOG can discover one larger legal envelope;
|
|
569
|
+
// semantic, opaque and transport failures cannot authorize a capacity retry.
|
|
327
570
|
let verified: LifecycleSimulationStep[];
|
|
328
571
|
let validatedReplayError: string | undefined;
|
|
572
|
+
assertCurrent();
|
|
329
573
|
try {
|
|
330
574
|
if (backend === "eth_simulateV1") verified = await lifecycleStage(client, "simulation.replay", () => simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true));
|
|
331
575
|
else {
|
|
@@ -333,87 +577,140 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
333
577
|
verified = await simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice, false, limits.headroomBps);
|
|
334
578
|
}
|
|
335
579
|
} catch (failure) {
|
|
336
|
-
const
|
|
580
|
+
const kind = simulationFailureKind(failure);
|
|
581
|
+
if (kind === "abort") throw failure;
|
|
582
|
+
assertCurrent();
|
|
583
|
+
const message = lifecycleFailureReason(failure);
|
|
337
584
|
if (backend !== "eth_simulateV1") {
|
|
338
585
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
339
586
|
return { ...base, backend, admitted: false, steps: [], reason: `Exact-gas validated replay failed: ${message}` };
|
|
340
587
|
}
|
|
588
|
+
if (kind !== "out-of-gas") {
|
|
589
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
590
|
+
return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, failureCategory: kind === "source" ? "source" : "opaque", reason: `Exact-gas validated replay unavailable: ${message}` };
|
|
591
|
+
}
|
|
341
592
|
validatedReplayError = message;
|
|
342
593
|
verified = [];
|
|
343
594
|
}
|
|
595
|
+
assertCurrent();
|
|
344
596
|
let verifiedSteps = verified;
|
|
345
597
|
let admittedGasLimits = gasLimits;
|
|
346
598
|
let finalFailure = verifiedSteps.find((step) => !step.success);
|
|
599
|
+
let finalFailureReason: string | undefined;
|
|
347
600
|
if (finalFailure === undefined && validatedReplayError !== undefined) {
|
|
601
|
+
if (!envelopeDiscoveryGas.some((gas, index) => gas > (gasLimits[index] ?? gas))) {
|
|
602
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
603
|
+
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling failed: ${validatedReplayError}` };
|
|
604
|
+
}
|
|
348
605
|
// The buffered validated replay threw before yielding steps. Attempt one
|
|
349
606
|
// explicit envelope-discovery replay at the exact execution ceiling on the
|
|
350
607
|
// same pinned block; headroom is never silently raised — the carried gas is
|
|
351
608
|
// the validated envelope itself.
|
|
609
|
+
assertCurrent();
|
|
352
610
|
try {
|
|
353
|
-
const ceilingLimits =
|
|
611
|
+
const ceilingLimits = envelopeDiscoveryGas;
|
|
354
612
|
const retried = backend === "eth_simulateV1"
|
|
355
613
|
? await lifecycleStage(client, "simulation.envelope", () => simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true))
|
|
356
614
|
: options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
|
|
357
|
-
|
|
615
|
+
const retryFailureIndex = retried?.findIndex((step) => !step.success) ?? -1;
|
|
616
|
+
const retryFailure = retried?.[retryFailureIndex];
|
|
617
|
+
if (retried === undefined) {
|
|
358
618
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
359
|
-
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${
|
|
619
|
+
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${validatedReplayError}` };
|
|
360
620
|
}
|
|
361
621
|
admittedGasLimits = ceilingLimits;
|
|
362
622
|
verifiedSteps = retried;
|
|
363
|
-
finalFailure =
|
|
623
|
+
finalFailure = retryFailure;
|
|
364
624
|
validatedReplayError = undefined;
|
|
365
|
-
|
|
625
|
+
if (retryFailure !== undefined) {
|
|
626
|
+
const kind = transactions[retryFailureIndex]?.kind;
|
|
627
|
+
const indivisible = kind === "activate" || kind === "atomic";
|
|
628
|
+
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" : ""}`;
|
|
629
|
+
}
|
|
630
|
+
} catch (failure) {
|
|
631
|
+
const kind = simulationFailureKind(failure);
|
|
632
|
+
if (kind === "abort") throw failure;
|
|
633
|
+
assertCurrent();
|
|
366
634
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
367
|
-
return { ...base, backend, admitted: false, steps: measured,
|
|
635
|
+
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 after buffered execution refusal: ${lifecycleFailureReason(failure)}` };
|
|
636
|
+
return { ...base, backend, admitted: false, steps: measured, reason: `Validated buffered replay failed (${validatedReplayError}) and execution at the exact gas ceiling still reverted; final mint/lock/all-buys/public-opening is indivisible` };
|
|
368
637
|
}
|
|
369
|
-
}
|
|
370
|
-
if (finalFailure !== undefined) {
|
|
638
|
+
} else if (finalFailure !== undefined) {
|
|
371
639
|
const failedIndex = verifiedSteps.findIndex((step) => !step.success);
|
|
372
640
|
const bufferedLimit = gasLimits[failedIndex];
|
|
373
641
|
const failedTransaction = transactions[failedIndex];
|
|
374
|
-
if (bufferedLimit !== undefined && failedTransaction !== undefined &&
|
|
642
|
+
if (bufferedLimit !== undefined && failedTransaction !== undefined && (nextEnvelope === undefined || failedIndex !== 0) &&
|
|
643
|
+
looksGasExhausted(finalFailure, bufferedLimit) && (envelopeDiscoveryGas[failedIndex] ?? 0n) > bufferedLimit) {
|
|
375
644
|
// Buffered replay returned an exhaustion-shaped failed step; learn the true
|
|
376
645
|
// validated envelope with one explicit replay at the exact execution ceiling.
|
|
646
|
+
assertCurrent();
|
|
377
647
|
try {
|
|
378
|
-
const ceilingLimits =
|
|
648
|
+
const ceilingLimits = envelopeDiscoveryGas;
|
|
379
649
|
const retried = backend === "eth_simulateV1"
|
|
380
|
-
? await simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true)
|
|
650
|
+
? await lifecycleStage(client, "simulation.envelope", () => simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true))
|
|
381
651
|
: options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
|
|
382
|
-
if (retried
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
admittedGasLimits = ceilingLimits;
|
|
386
|
-
verifiedSteps = retried;
|
|
387
|
-
finalFailure = undefined;
|
|
388
|
-
} else {
|
|
389
|
-
const indivisible = failedTransaction.kind === "activate" || failedTransaction.kind === "atomic";
|
|
390
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
391
|
-
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 };
|
|
392
|
-
}
|
|
652
|
+
if (retried === undefined) {
|
|
653
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
654
|
+
return { ...base, backend, admitted: false, steps: measured, reason: "Validated execution at the exact current gas ceiling omitted transaction results" };
|
|
393
655
|
}
|
|
394
|
-
|
|
656
|
+
const retryFailureIndex = retried.findIndex((step) => !step.success);
|
|
657
|
+
const retryFailure = retried[retryFailureIndex];
|
|
658
|
+
admittedGasLimits = ceilingLimits;
|
|
659
|
+
verifiedSteps = retried;
|
|
660
|
+
finalFailure = retryFailure;
|
|
661
|
+
if (retryFailure !== undefined) {
|
|
662
|
+
const kind = transactions[retryFailureIndex]?.kind;
|
|
663
|
+
const indivisible = kind === "activate" || kind === "atomic";
|
|
664
|
+
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" : ""}`;
|
|
665
|
+
}
|
|
666
|
+
} catch (failure) {
|
|
667
|
+
const kind = simulationFailureKind(failure);
|
|
668
|
+
if (kind === "abort") throw failure;
|
|
669
|
+
assertCurrent();
|
|
670
|
+
if (kind !== "out-of-gas") {
|
|
671
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
672
|
+
return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, reason: `Legal-envelope validated replay unavailable: ${lifecycleFailureReason(failure)}` };
|
|
673
|
+
}
|
|
674
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
675
|
+
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)}` };
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
assertCurrent();
|
|
680
|
+
if (nextEnvelope !== undefined && finalFailure === undefined) {
|
|
681
|
+
const currentMinimum = buffer(verifiedSteps[0]?.gasRequired ?? verifiedSteps[0]?.gasUsed ?? nextEnvelope.gas);
|
|
682
|
+
if (nextEnvelope.gas < currentMinimum) {
|
|
683
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
684
|
+
return { ...base, backend, admitted: false, executionProof: "proved", protocolFit: "failed", steps: verifiedSteps,
|
|
685
|
+
failureCategory: "capacity", capacityConstraint: "gas-envelope", failedTransactionId: transactions[0]?.id,
|
|
686
|
+
reason: "Exact reviewed replay leaves insufficient current minimum gas headroom" };
|
|
395
687
|
}
|
|
396
688
|
}
|
|
397
689
|
const estimates: LifecycleSimulationStep[] = [];
|
|
398
|
-
|
|
399
|
-
|
|
690
|
+
// A definitive returned replay failure already refuses execution. Retain its
|
|
691
|
+
// fee observations without inventing a balance or doing redundant payer work.
|
|
692
|
+
let remainingBalance = finalFailure === undefined
|
|
693
|
+
? rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance")
|
|
694
|
+
: undefined;
|
|
400
695
|
let affordabilityFailure: string | undefined;
|
|
401
696
|
const feeContext = configuration.estimateDataFee === undefined || sourceClient === client ? context : { ...context, client: sourceClient };
|
|
402
697
|
for (let index = 0; index < verifiedSteps.length; index += 1) {
|
|
403
698
|
const step = verifiedSteps[index]; const transaction = transactions[index]; const gasLimit = admittedGasLimits[index];
|
|
404
699
|
if (step === undefined || transaction === undefined || gasLimit === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing verified fee context");
|
|
405
|
-
const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit }, feeContext);
|
|
700
|
+
const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit, gasPrice }, feeContext);
|
|
406
701
|
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");
|
|
407
702
|
// Nitro gas includes poster charges. Never add the quoted poster fee again.
|
|
408
703
|
const executionFee = gasLimit * gasPrice;
|
|
409
704
|
const required = transaction.value + executionFee + (dataFee ?? 0n);
|
|
410
705
|
if (required >= 1n << 256n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
|
|
411
|
-
if (
|
|
412
|
-
|
|
706
|
+
if (remainingBalance !== undefined) {
|
|
707
|
+
if (required > remainingBalance && affordabilityFailure === undefined) affordabilityFailure = "Creator native balance cannot fund the remaining transaction value and execution/data fee envelopes";
|
|
708
|
+
remainingBalance -= required;
|
|
709
|
+
}
|
|
413
710
|
const poster = posterGas[index];
|
|
414
711
|
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" } });
|
|
415
712
|
}
|
|
416
713
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
417
|
-
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 };
|
|
714
|
+
return { ...base, backend, executionProof: finalFailure === undefined ? "proved" : "failed", protocolFit: finalFailure === undefined ? "proved" : "unknown", admitted: finalFailure === undefined && affordabilityFailure === undefined, steps: estimates, reason: finalFailureReason ?? finalFailure?.error ?? affordabilityFailure, failedTransactionId: finalFailure?.transactionId, failureCategory: finalFailure?.failureCategory ?? (affordabilityFailure === undefined ? undefined : "affordability"), capacityConstraint: finalFailure?.failureCategory === "capacity" ? "compute" : undefined };
|
|
418
715
|
}
|
|
419
716
|
|