@black-market/sdk 0.6.4 → 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 +354 -400
- 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 +212 -53
- 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 -4
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +383 -116
- 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 +228 -54
- 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 -330
- package/src/lifecycle/presets.ts +243 -0
- package/src/lifecycle/progress.ts +193 -50
- package/src/lifecycle/rpc.ts +97 -9
- package/src/lifecycle/simulation.ts +370 -109
- package/src/lifecycle/transactions.ts +34 -0
- package/src/lifecycle/types.ts +35 -3
|
@@ -1,8 +1,64 @@
|
|
|
1
|
-
import { decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctionResult, toHex } from "viem";
|
|
2
|
-
import { launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
|
|
3
|
-
import { LifecyclePhase } from "./schema.js";
|
|
4
|
-
import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readNitroPosterGas, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity } from "./rpc.js";
|
|
1
|
+
import { decodeErrorResult, decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctionResult, parseAbi, toHex } from "viem";
|
|
2
|
+
import { fixedFeePoolHookV1Abi, launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
|
|
3
|
+
import { hashLaunchIdentity, hashLaunchPlan, LifecyclePhase } from "./schema.js";
|
|
4
|
+
import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readLifecycleChainId, readNitroPosterGas, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity } from "./rpc.js";
|
|
5
5
|
import { LifecyclePlanningError } from "./types.js";
|
|
6
|
+
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion } from "./markets.js";
|
|
7
|
+
function committedPositionCount(plan) {
|
|
8
|
+
let count = 0;
|
|
9
|
+
for (const market of plan.markets) {
|
|
10
|
+
if (market.configVersion === 1)
|
|
11
|
+
count += decodeAbyssLifecycleMarketConfig(market.config).positions.length;
|
|
12
|
+
else if (market.configVersion === 4)
|
|
13
|
+
count += decodeV4LifecycleMarketConfig(market.config).positions.length;
|
|
14
|
+
else if (isPoolBoundV4ConfigVersion(market.configVersion))
|
|
15
|
+
count += decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion).positions.length;
|
|
16
|
+
else
|
|
17
|
+
throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", "Activation requires a supported committed position schema");
|
|
18
|
+
}
|
|
19
|
+
return count;
|
|
20
|
+
}
|
|
21
|
+
const executionGuardErrors = [
|
|
22
|
+
...fixedFeePoolHookV1Abi.filter((item) => item.type === "error"),
|
|
23
|
+
...parseAbi([
|
|
24
|
+
"error WrongDomain()", "error PlanMismatch()", "error LaunchAlreadyExists()", "error InvalidMode()",
|
|
25
|
+
"error InvalidPhase()", "error InvalidPreparationOrder()", "error DeadlineExpired()", "error InvalidBinding()",
|
|
26
|
+
"error InvalidMarket()", "error InsufficientEscrow()", "error InvalidPositions()", "error InvalidPlan()",
|
|
27
|
+
"error InvalidFunding()", "error InvalidFeePolicy()", "error InvalidBuy()", "error DuplicateMarket()",
|
|
28
|
+
"error IneligibleImplementation()", "error QuoteDebtForbidden()", "error SlippageExceeded()", "error FeeBelowMinimum()",
|
|
29
|
+
"error DeploymentFailed()", "error Error(string reason)", "error Panic(uint256 code)",
|
|
30
|
+
]),
|
|
31
|
+
];
|
|
32
|
+
function executionFailure(returnData, error) {
|
|
33
|
+
const native = error !== null && typeof error === "object" && !Array.isArray(error) ? error : undefined;
|
|
34
|
+
let nativeData = native?.data;
|
|
35
|
+
for (let depth = 0; depth < 3 && nativeData !== null && typeof nativeData === "object" && !Array.isArray(nativeData); depth += 1)
|
|
36
|
+
nativeData = nativeData.data;
|
|
37
|
+
const nativeErrorDataBytes = typeof nativeData === "string" && /^0x(?:[0-9a-fA-F]{2})*$/.test(nativeData) ? (nativeData.length - 2) / 2 : undefined;
|
|
38
|
+
const nativeErrorCode = typeof native?.code === "number" && Number.isSafeInteger(native.code) ? native.code : undefined;
|
|
39
|
+
const message = typeof native?.message === "string" ? native.message : "";
|
|
40
|
+
const nativeGasCapped = message.endsWith(" (gas limit was capped by the RPC server's global gas cap)");
|
|
41
|
+
// Native eth_simulateV1 uses -32015 for VM errors; -32000 is also used by RPC clients.
|
|
42
|
+
// Empty data and an explicit OOG report are required. Malformed data or proximity is not proof.
|
|
43
|
+
const outOfGas = (returnData === undefined || returnData === "0x") && (native?.data === undefined || nativeErrorDataBytes === 0) &&
|
|
44
|
+
(nativeErrorCode === -32015 || nativeErrorCode === -32000) && /^out of gas\b/i.test(message);
|
|
45
|
+
const nativeErrorKind = native === undefined ? "missing" : outOfGas ? "out-of-gas" : nativeErrorCode === 3 ? "execution-reverted" : nativeErrorCode === -32015 ? "vm-error" : "other";
|
|
46
|
+
const diagnostic = { nativeErrorCode, nativeErrorKind, nativeErrorDataBytes, nativeGasCapped };
|
|
47
|
+
const data = returnData === undefined || returnData === "0x" ? nativeData : returnData;
|
|
48
|
+
if (typeof data === "string" && /^0x(?:[0-9a-fA-F]{2}){4,}$/.test(data)) {
|
|
49
|
+
try {
|
|
50
|
+
const { errorName } = decodeErrorResult({ abi: executionGuardErrors, data: data });
|
|
51
|
+
return { ...diagnostic, failureCategory: errorName === "DeploymentFailed" ? "opaque" : "semantic", decodedError: errorName, error: `Execution reverted (${errorName})` };
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { ...diagnostic, failureCategory: "opaque", error: "Execution reverted without a reviewed error" };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (outOfGas)
|
|
58
|
+
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" };
|
|
59
|
+
return { ...diagnostic, failureCategory: "opaque", error: nativeErrorKind === "execution-reverted" ? "Native execution reverted without a reviewed error" : "Execution failed without a reviewed error" };
|
|
60
|
+
}
|
|
61
|
+
const nitroProofsByInvocation = new WeakMap();
|
|
6
62
|
function conditionRequest(condition, planned) {
|
|
7
63
|
if (condition.kind === "allowance")
|
|
8
64
|
return {
|
|
@@ -93,9 +149,8 @@ async function simulateRpcPass(client, planned, block, transactions, gasLimits,
|
|
|
93
149
|
throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Simulated gas use is outside the exact requested envelope");
|
|
94
150
|
const returnData = rpcHex(call.returnData, "simulated return data");
|
|
95
151
|
const success = rpcQuantity(call.status, "simulated status") === 1n;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
error = call.error === undefined ? `Execution reverted: ${returnData}` : JSON.stringify(call.error);
|
|
152
|
+
const failureDiagnostic = success ? undefined : executionFailure(returnData, call.error);
|
|
153
|
+
let error = failureDiagnostic?.error;
|
|
99
154
|
if (success) {
|
|
100
155
|
try {
|
|
101
156
|
// Verification reads must not become synthetic transactions that consume creator nonce/fees.
|
|
@@ -120,8 +175,9 @@ async function simulateRpcPass(client, planned, block, transactions, gasLimits,
|
|
|
120
175
|
else if (transaction.kind === "atomic" || transaction.kind === "activate") {
|
|
121
176
|
const functionName = transaction.kind === "atomic" ? "launchAtomic" : "activateLaunch";
|
|
122
177
|
const receipt = decodeFunctionResult({ abi: launchLifecycleAbi, functionName, data: returnData });
|
|
123
|
-
|
|
124
|
-
|
|
178
|
+
const positionCount = committedPositionCount(planned.plan);
|
|
179
|
+
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))
|
|
180
|
+
throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not atomically complete every committed market, position and ordered buy");
|
|
125
181
|
for (let buyIndex = 0; buyIndex < planned.plan.buys.length; buyIndex += 1) {
|
|
126
182
|
const buy = planned.plan.buys[buyIndex];
|
|
127
183
|
if (buy === undefined || (receipt.quoteSpent[buyIndex] ?? buy.quoteAmountIn + 1n) > buy.quoteAmountIn || (receipt.tokenOut[buyIndex] ?? 0n) < buy.minTokenOut)
|
|
@@ -134,21 +190,38 @@ async function simulateRpcPass(client, planned, block, transactions, gasLimits,
|
|
|
134
190
|
throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Funding approval returned failure");
|
|
135
191
|
}
|
|
136
192
|
catch (failure) {
|
|
137
|
-
error = failure instanceof
|
|
193
|
+
error = failure instanceof LifecyclePlanningError ? failure.message : "Simulation returned an invalid committed postcondition";
|
|
138
194
|
}
|
|
139
195
|
}
|
|
140
|
-
steps.push({ transactionId: transaction.id, success: success && error === undefined, gasUsed, gasRequired, gasLimit: gasLimits[index], returnData, error });
|
|
196
|
+
steps.push({ transactionId: transaction.id, success: success && error === undefined, gasUsed, gasRequired, gasLimit: gasLimits[index], returnData, error, ...failureDiagnostic, ...(success && error !== undefined ? { failureCategory: "postcondition" } : {}) });
|
|
141
197
|
if (!success || error !== undefined)
|
|
142
198
|
break;
|
|
143
199
|
}
|
|
144
200
|
return steps;
|
|
145
201
|
}
|
|
146
|
-
/** A
|
|
147
|
-
* exhaustion (EIP-150 retention starves forwarded sub-calls; the surfaced revert bytes
|
|
148
|
-
* may be a wrapped inner bubble rather than empty). Discovery is safe: a genuine
|
|
149
|
-
* business revert also fails at the exact ceiling, so it can never be masked. */
|
|
202
|
+
/** A decoded semantic refusal or opaque deployment wrapper is never gas evidence. */
|
|
150
203
|
function looksGasExhausted(step, gasLimit) {
|
|
151
|
-
return !step.success && step.gasUsed !== undefined && step.gasUsed >= gasLimit - gasLimit / 64n;
|
|
204
|
+
return !step.success && step.failureCategory === "capacity" && step.gasUsed !== undefined && step.gasUsed >= gasLimit - gasLimit / 64n;
|
|
205
|
+
}
|
|
206
|
+
function simulationFailureKind(failure) {
|
|
207
|
+
for (let depth = 0; depth < 8 && failure !== null && typeof failure === "object"; depth += 1) {
|
|
208
|
+
const error = failure;
|
|
209
|
+
if (error.name === "AbortError")
|
|
210
|
+
return "abort";
|
|
211
|
+
const status = error.status ?? error.statusCode;
|
|
212
|
+
if (typeof status === "number" && status >= 400 && status <= 599)
|
|
213
|
+
return "source";
|
|
214
|
+
const message = typeof error.details === "string" ? error.details : typeof error.message === "string" ? error.message : "";
|
|
215
|
+
const execution = executionFailure(undefined, { ...error, message });
|
|
216
|
+
if (execution.nativeErrorKind === "out-of-gas" && execution.nativeGasCapped)
|
|
217
|
+
return "source";
|
|
218
|
+
if (execution.failureCategory === "capacity")
|
|
219
|
+
return "out-of-gas";
|
|
220
|
+
if (error.code === 3 || error.code === -32000 && /^execution (?:failed|reverted)\b/i.test(message))
|
|
221
|
+
return "execution";
|
|
222
|
+
failure = error.cause;
|
|
223
|
+
}
|
|
224
|
+
return "source";
|
|
152
225
|
}
|
|
153
226
|
async function waitForkReceipt(client, transactionHash, timeoutMs) {
|
|
154
227
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -232,7 +305,7 @@ async function simulateForkPass(fork, planned, block, transactions, gasLimits, g
|
|
|
232
305
|
const activated = events.filter((event) => event.eventName === "LaunchActivated");
|
|
233
306
|
const buys = events.filter((event) => event.eventName === "InitialBuyExecuted");
|
|
234
307
|
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()) ||
|
|
235
|
-
progress.phase !== LifecyclePhase.Active || progress.planHash.toLowerCase() !== planned.planHash.toLowerCase() || progress.token.toLowerCase() !== planned.predictedToken.toLowerCase() ||
|
|
308
|
+
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) ||
|
|
236
309
|
buys.length !== planned.plan.buys.length)
|
|
237
310
|
throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Actual fork activation differs from the exact committed launch");
|
|
238
311
|
const quoteSpent = [];
|
|
@@ -261,7 +334,7 @@ async function simulateForkPass(fork, planned, block, transactions, gasLimits, g
|
|
|
261
334
|
steps.push({ transactionId: transaction.id, success: true, gasUsed, gasLimit: gas, gasRequired, returnData });
|
|
262
335
|
}
|
|
263
336
|
catch (failure) {
|
|
264
|
-
steps.push({ transactionId: transaction.id, success: false, gasLimit: gas, error:
|
|
337
|
+
steps.push({ transactionId: transaction.id, success: false, gasLimit: gas, error: lifecycleFailureReason(failure) });
|
|
265
338
|
break;
|
|
266
339
|
}
|
|
267
340
|
}
|
|
@@ -278,98 +351,223 @@ async function simulateForkPass(fork, planned, block, transactions, gasLimits, g
|
|
|
278
351
|
}
|
|
279
352
|
}
|
|
280
353
|
}
|
|
281
|
-
|
|
282
|
-
|
|
354
|
+
function observedFields(value) {
|
|
355
|
+
const keys = Object.keys(value);
|
|
356
|
+
const values = keys.map((key) => value[key]);
|
|
357
|
+
return () => keys.length === Object.keys(value).length && keys.every((key, index) => value[key] === values[index]);
|
|
358
|
+
}
|
|
359
|
+
/** Internal continuation owned by one SDK read invocation. Only its positive
|
|
360
|
+
* capability proof is reused; every candidate executes its entire economic sequence. */
|
|
283
361
|
export function prepareLaunchSimulation(options, block, hasTransactions = true) {
|
|
284
|
-
const { client, planned } = options;
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
362
|
+
const { client, planned, fork } = options;
|
|
363
|
+
const signal = options.signal;
|
|
364
|
+
if (signal?.aborted)
|
|
365
|
+
throw Object.assign(new Error("Lifecycle simulation cancelled"), { name: "AbortError" });
|
|
366
|
+
const sourceClient = lifecycleSourceClient(client), sourceRequest = sourceClient.request;
|
|
367
|
+
const sourceBatching = sourceClient.supportsReadBatching;
|
|
368
|
+
const reusable = assertLifecycleReadClientOpen(client);
|
|
369
|
+
const plan = planned.plan, planHash = planned.planHash, launchId = planned.launchId;
|
|
370
|
+
const optionsLimits = options.limits, plannedLimits = planned.limits;
|
|
371
|
+
const policy = optionsLimits === undefined ? plannedLimits : optionsLimits;
|
|
372
|
+
const blockMatches = observedFields(block);
|
|
373
|
+
const forkMatches = fork === undefined ? undefined : observedFields(fork);
|
|
374
|
+
const forkRequest = fork?.client.request;
|
|
375
|
+
const plannedMatches = observedFields(planned);
|
|
376
|
+
const context = Object.freeze({ client, account: planned.account, orchestrator: plan.orchestrator, block: Object.freeze({ ...block }), chainId: planned.chainId });
|
|
377
|
+
let closed = false, active = false, consumed = false, capabilityProved = false;
|
|
378
|
+
let capabilityPending;
|
|
379
|
+
const assertContext = () => {
|
|
380
|
+
if (closed)
|
|
381
|
+
throw new LifecyclePlanningError("INVALID_SIMULATION", "Prepared simulation continuation has closed");
|
|
382
|
+
if (signal?.aborted)
|
|
383
|
+
throw Object.assign(new Error("Lifecycle simulation cancelled"), { name: "AbortError" });
|
|
384
|
+
assertLifecycleReadClientOpen(client);
|
|
385
|
+
if (options.client !== client || options.planned !== planned || options.limits !== optionsLimits || options.fork !== fork || options.signal !== signal ||
|
|
386
|
+
lifecycleSourceClient(client) !== sourceClient || sourceClient.request !== sourceRequest || !plannedMatches() || !blockMatches() ||
|
|
387
|
+
sourceClient.supportsReadBatching !== sourceBatching ||
|
|
388
|
+
planned.plan !== plan || planned.account !== context.account || plan.orchestrator !== context.orchestrator || planned.chainId !== context.chainId ||
|
|
389
|
+
planned.limits !== plannedLimits || forkMatches !== undefined && !forkMatches() || fork?.client.request !== forkRequest) {
|
|
390
|
+
throw new LifecyclePlanningError("LIMIT_CONTEXT_MISMATCH", "Prepared simulation differs from its source/account/core/plan/header/policy context");
|
|
391
|
+
}
|
|
392
|
+
if (hashLaunchPlan(plan).toLowerCase() !== planHash.toLowerCase() || hashLaunchIdentity(plan).toLowerCase() !== launchId.toLowerCase()) {
|
|
393
|
+
throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed after its simulation context was bound");
|
|
394
|
+
}
|
|
395
|
+
};
|
|
288
396
|
const pending = (async () => {
|
|
289
|
-
if (
|
|
397
|
+
if (fork !== undefined && lifecycleSourceClient(fork.client) === sourceClient)
|
|
290
398
|
throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
|
|
291
399
|
return lifecycleStage(client, "simulation.context", () => Promise.all([
|
|
292
|
-
resolveLifecycleLimits(context,
|
|
293
|
-
|
|
294
|
-
lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(block.number)], block),
|
|
400
|
+
resolveLifecycleLimits(context, policy),
|
|
401
|
+
readLifecycleChainId(client),
|
|
402
|
+
lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block),
|
|
295
403
|
hasTransactions ? lifecycleRpc(client, "eth_gasPrice") : undefined,
|
|
296
404
|
]));
|
|
297
405
|
})().then((observations) => ({ observations }), (failure) => ({ failure }));
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
throw new LifecyclePlanningError("INVALID_SIMULATION", "Prepared
|
|
406
|
+
// Refused inputs may discard this read-only preparation before it settles.
|
|
407
|
+
// Its rejection is handled immediately; no capability or economic work starts.
|
|
408
|
+
const run = async (transactions, diagnostic, nextEnvelope) => {
|
|
409
|
+
if (active || consumed && !reusable || !hasTransactions && transactions.length !== 0) {
|
|
410
|
+
closed = true;
|
|
411
|
+
throw new LifecyclePlanningError("INVALID_SIMULATION", "Prepared candidates require sequential consumption by their SDK invocation owner");
|
|
412
|
+
}
|
|
413
|
+
active = true;
|
|
304
414
|
consumed = true;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
415
|
+
try {
|
|
416
|
+
assertContext();
|
|
417
|
+
const envelopeMatches = nextEnvelope === undefined ? undefined : observedFields(nextEnvelope);
|
|
418
|
+
const candidates = transactions.map((transaction) => ({
|
|
419
|
+
transaction, matches: observedFields(transaction),
|
|
420
|
+
dependencies: [...transaction.dependencies],
|
|
421
|
+
conditions: transaction.postconditions.map((condition) => ({ condition, matches: observedFields(condition) })),
|
|
422
|
+
}));
|
|
423
|
+
const result = await pending;
|
|
424
|
+
if ("failure" in result)
|
|
425
|
+
throw result.failure;
|
|
426
|
+
const assertCurrent = () => {
|
|
427
|
+
assertContext();
|
|
428
|
+
result.observations[0].assertCurrent();
|
|
429
|
+
if (envelopeMatches !== undefined && !envelopeMatches())
|
|
430
|
+
throw new LifecyclePlanningError("INVALID_SIMULATION", "Reviewed gas envelope changed during its complete economic proof");
|
|
431
|
+
if (transactions.length !== candidates.length || candidates.some(({ transaction, matches, dependencies, conditions }, index) => transactions[index] !== transaction || !matches() || transaction.dependencies.length !== dependencies.length ||
|
|
432
|
+
dependencies.some((dependency, ordinal) => transaction.dependencies[ordinal] !== dependency) ||
|
|
433
|
+
transaction.postconditions.length !== conditions.length || conditions.some(({ condition, matches: conditionMatches }, ordinal) => transaction.postconditions[ordinal] !== condition || !conditionMatches()))) {
|
|
434
|
+
throw new LifecyclePlanningError("INVALID_SIMULATION", "Candidate transactions changed during their complete economic proof");
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
assertCurrent();
|
|
438
|
+
const proveBackend = async (limits, gasPrice) => {
|
|
439
|
+
assertCurrent();
|
|
440
|
+
if (capabilityProved)
|
|
441
|
+
return;
|
|
442
|
+
if (capabilityPending === undefined) {
|
|
443
|
+
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()]);
|
|
444
|
+
let proofs = reusable ? nitroProofsByInvocation.get(client) : undefined;
|
|
445
|
+
if (reusable && proofs === undefined) {
|
|
446
|
+
proofs = new Map();
|
|
447
|
+
nitroProofsByInvocation.set(client, proofs);
|
|
448
|
+
}
|
|
449
|
+
const ownedProofs = proofs;
|
|
450
|
+
let nativeProof = ownedProofs?.get(key);
|
|
451
|
+
if (nativeProof === undefined) {
|
|
452
|
+
nativeProof = lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, context.block, limits, gasPrice));
|
|
453
|
+
ownedProofs?.set(key, nativeProof);
|
|
454
|
+
const pending = nativeProof;
|
|
455
|
+
void pending.catch(() => { if (ownedProofs?.get(key) === pending)
|
|
456
|
+
ownedProofs?.delete(key); });
|
|
457
|
+
}
|
|
458
|
+
const proof = nativeProof.then(() => {
|
|
459
|
+
assertContext();
|
|
460
|
+
result.observations[0].assertCurrent();
|
|
461
|
+
capabilityProved = true;
|
|
462
|
+
});
|
|
463
|
+
capabilityPending = proof;
|
|
464
|
+
void proof.then(() => { if (capabilityPending === proof)
|
|
465
|
+
capabilityPending = undefined; }, () => { if (capabilityPending === proof)
|
|
466
|
+
capabilityPending = undefined; });
|
|
467
|
+
}
|
|
468
|
+
await capabilityPending;
|
|
469
|
+
assertCurrent();
|
|
470
|
+
};
|
|
471
|
+
const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope);
|
|
472
|
+
assertCurrent();
|
|
473
|
+
return simulation;
|
|
474
|
+
}
|
|
475
|
+
catch (failure) {
|
|
476
|
+
closed = true;
|
|
477
|
+
capabilityProved = false;
|
|
478
|
+
capabilityPending = undefined;
|
|
479
|
+
throw failure;
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
active = false;
|
|
483
|
+
}
|
|
313
484
|
};
|
|
485
|
+
const simulate = Object.assign((transactions, nextEnvelope) => run(transactions, false, nextEnvelope), { measure: (transactions) => run(transactions, true),
|
|
486
|
+
close() { closed = true; capabilityProved = false; capabilityPending = undefined; } });
|
|
487
|
+
return simulate;
|
|
314
488
|
}
|
|
315
|
-
export async function simulateLaunchTransactions(options, transactions, pinnedBlock) {
|
|
489
|
+
export async function simulateLaunchTransactions(options, transactions, pinnedBlock, nextEnvelope) {
|
|
316
490
|
// Standalone calls always produce fresh context, including live chain and fees.
|
|
317
491
|
if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === lifecycleSourceClient(options.client))
|
|
318
492
|
throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
|
|
319
493
|
const block = pinnedBlock ?? await readLifecycleBlock(options.client);
|
|
320
|
-
|
|
494
|
+
const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0);
|
|
495
|
+
try {
|
|
496
|
+
return await prepared(transactions, nextEnvelope);
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
prepared.close();
|
|
500
|
+
}
|
|
321
501
|
}
|
|
322
|
-
async function simulateLaunchTransactionsWithContext(options, transactions, context, observations) {
|
|
502
|
+
async function simulateLaunchTransactionsWithContext(options, transactions, context, observations, proveBackend, assertCurrent, diagnostic, nextEnvelope) {
|
|
323
503
|
const { client, planned } = options;
|
|
324
504
|
const sourceClient = lifecycleSourceClient(client);
|
|
325
505
|
const { block } = context;
|
|
326
506
|
const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = observations;
|
|
327
507
|
const base = { confidence: "stateful", executionProof: "failed", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits };
|
|
328
|
-
if (
|
|
508
|
+
if (chainId !== planned.chainId)
|
|
329
509
|
throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
|
|
330
510
|
const accountCode = rpcHex(accountCodeValue, "account code");
|
|
331
|
-
if (accountCode !== "0x")
|
|
511
|
+
if (accountCode !== "0x") {
|
|
512
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
332
513
|
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
|
|
514
|
+
}
|
|
333
515
|
for (const transaction of transactions) {
|
|
334
516
|
if (transaction.from.toLowerCase() !== planned.account.toLowerCase() || BigInt(transaction.chainId) !== planned.chainId)
|
|
335
517
|
throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Simulation transaction differs from the committed chain/account");
|
|
336
|
-
if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes)
|
|
337
|
-
|
|
518
|
+
if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) {
|
|
519
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
520
|
+
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" };
|
|
521
|
+
}
|
|
338
522
|
}
|
|
339
523
|
if (transactions.length === 0) {
|
|
340
524
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
341
525
|
return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] };
|
|
342
526
|
}
|
|
343
|
-
const
|
|
527
|
+
const recommendedGasPrice = rpcQuantity(gasPriceValue, "gas price");
|
|
528
|
+
if (recommendedGasPrice >= 1n << 256n)
|
|
529
|
+
throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Recommended gas price must be a uint256 quantity");
|
|
530
|
+
// Initial construction reserves twice the pinned base fee if the quote lags.
|
|
531
|
+
// Reviewed fees are not new recommendations: native replay must accept them
|
|
532
|
+
// under natural child fees, with no base-fee override or silent fee increase.
|
|
533
|
+
const baseFeeEnvelope = (block.baseFeePerGas ?? 0n) * 2n;
|
|
534
|
+
const gasPrice = nextEnvelope?.gasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
|
|
344
535
|
if (gasPrice >= 1n << 256n)
|
|
345
536
|
throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
|
|
537
|
+
if (nextEnvelope !== undefined && (typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas <= 0n || nextEnvelope.gas >= 1n << 64n ||
|
|
538
|
+
typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
|
|
539
|
+
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Reviewed gas and gas price must be exact positive uint64 gas and nonnegative uint256 price");
|
|
540
|
+
}
|
|
346
541
|
const buffer = (gas) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
|
|
347
542
|
const computeCeiling = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
|
|
348
|
-
|
|
349
|
-
|
|
543
|
+
// Nonnegative poster gas cannot make this complete discovery request smaller.
|
|
544
|
+
// A generic controlled fork retains the existing RPC-to-fork fallback.
|
|
545
|
+
if (limits.maxSimulationGas !== undefined && BigInt(transactions.length) * computeCeiling > limits.maxSimulationGas &&
|
|
546
|
+
(limits.protocol === "nitro" || options.fork === undefined)) {
|
|
547
|
+
assertCurrent();
|
|
548
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
549
|
+
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."}` };
|
|
550
|
+
}
|
|
350
551
|
const posterGas = [];
|
|
351
552
|
if (limits.protocol === "nitro") {
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
const nitroEvidence = Promise.all([
|
|
355
|
-
lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, block, limits, gasPrice)),
|
|
356
|
-
lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
|
|
357
|
-
]);
|
|
358
|
-
if (sourceClient.supportsReadBatching === true) {
|
|
359
|
-
// Handle settlement immediately so a failed measurement cannot escape while
|
|
360
|
-
// the independent metering evidence is still pending. It is never admission.
|
|
361
|
-
parallelMeasurement = lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false))
|
|
362
|
-
.then((steps) => ({ steps }), (failure) => ({ failure }));
|
|
363
|
-
}
|
|
553
|
+
// Lightweight poster reads may overlap the isolated capability request.
|
|
554
|
+
// Economic work is not even queued until both prerequisites have succeeded.
|
|
364
555
|
try {
|
|
365
|
-
const [, estimates] = await
|
|
556
|
+
const [, estimates] = await Promise.all([
|
|
557
|
+
proveBackend(limits, gasPrice),
|
|
558
|
+
lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
|
|
559
|
+
]);
|
|
366
560
|
posterGas.push(...estimates);
|
|
367
561
|
}
|
|
368
562
|
catch (failure) {
|
|
563
|
+
if (simulationFailureKind(failure) === "abort")
|
|
564
|
+
throw failure;
|
|
565
|
+
assertCurrent();
|
|
369
566
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
370
|
-
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Native Nitro compute/poster proof unavailable: ${
|
|
567
|
+
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.` };
|
|
371
568
|
}
|
|
372
569
|
}
|
|
570
|
+
assertCurrent();
|
|
373
571
|
// The poster quote allocates gas only. The native exact replay enforces actual
|
|
374
572
|
// compute hold; subtracting this approximate budget from gasUsed would not.
|
|
375
573
|
const discoveryGas = transactions.map((_, index) => {
|
|
@@ -381,45 +579,57 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
|
|
|
381
579
|
});
|
|
382
580
|
let backend = "eth_simulateV1";
|
|
383
581
|
let measured;
|
|
384
|
-
//
|
|
582
|
+
// A permissive measurement needs the legal complete compute + poster envelope.
|
|
583
|
+
// It is not proof: fixed compute/headroom checks and exact validated replay still follow.
|
|
385
584
|
try {
|
|
386
|
-
|
|
387
|
-
measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false));
|
|
388
|
-
else {
|
|
389
|
-
const observation = await parallelMeasurement;
|
|
390
|
-
if ("failure" in observation)
|
|
391
|
-
throw observation.failure;
|
|
392
|
-
measured = observation.steps;
|
|
393
|
-
}
|
|
585
|
+
measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, discoveryGas, limits, gasPrice, false));
|
|
394
586
|
}
|
|
395
587
|
catch (failure) {
|
|
588
|
+
if (simulationFailureKind(failure) === "abort")
|
|
589
|
+
throw failure;
|
|
590
|
+
assertCurrent();
|
|
396
591
|
if (options.fork === undefined || limits.protocol === "nitro") {
|
|
397
592
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
398
|
-
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Stateful sequential RPC simulation unavailable: ${
|
|
593
|
+
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."}` };
|
|
399
594
|
}
|
|
400
595
|
backend = "controlled-fork";
|
|
401
|
-
measured = await simulateForkPass(options.fork, planned, block, transactions,
|
|
596
|
+
measured = await simulateForkPass(options.fork, planned, block, transactions, discoveryGas, gasPrice, true, limits.headroomBps);
|
|
402
597
|
}
|
|
598
|
+
assertCurrent();
|
|
403
599
|
const failed = measured.find((step) => !step.success);
|
|
404
600
|
if (failed !== undefined) {
|
|
405
601
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
406
|
-
return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId };
|
|
602
|
+
return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId, failureCategory: failed.failureCategory ?? "opaque", capacityConstraint: failed.failureCategory === "capacity" ? "compute" : undefined };
|
|
603
|
+
}
|
|
604
|
+
if (diagnostic) {
|
|
605
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
606
|
+
return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
|
|
407
607
|
}
|
|
408
608
|
const computeGas = measured.map((step) => buffer(step.gasRequired ?? step.gasUsed ?? computeCeiling));
|
|
409
609
|
const gasLimits = computeGas.map((gas, index) => gas + (posterGas[index] ?? 0n));
|
|
410
610
|
const exceeds = gasLimits.findIndex((gas, index) => gas <= 0n || gas >= 1n << 64n || (computeGas[index] ?? 0n) > limits.executionGasCeiling || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling);
|
|
411
611
|
if (exceeds >= 0) {
|
|
412
612
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
413
|
-
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 };
|
|
613
|
+
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 };
|
|
614
|
+
}
|
|
615
|
+
if (nextEnvelope !== undefined) {
|
|
616
|
+
if (nextEnvelope.gas < (gasLimits[0] ?? 0n) || limits.transactionGasCeiling !== undefined && nextEnvelope.gas > limits.transactionGasCeiling) {
|
|
617
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
618
|
+
return { ...base, executionProof: "unavailable", protocolFit: "failed", backend, admitted: false, steps: measured,
|
|
619
|
+
failureCategory: "capacity", capacityConstraint: "gas-envelope", failedTransactionId: transactions[0]?.id,
|
|
620
|
+
reason: "Reviewed gas cannot cover current minimum compute/headroom/poster requirements or exceeds the current transaction ceiling" };
|
|
621
|
+
}
|
|
622
|
+
gasLimits[0] = nextEnvelope.gas;
|
|
414
623
|
}
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
624
|
+
// Envelope discovery may enlarge future unfinished steps, never the reviewed
|
|
625
|
+
// immediate one. Every retry still executes the entire sequence at held price.
|
|
626
|
+
const envelopeDiscoveryGas = nextEnvelope === undefined ? discoveryGas : discoveryGas.map((gas, index) => index === 0 ? nextEnvelope.gas : gas);
|
|
627
|
+
// Replay the complete sequence from its original state at the exact buffered
|
|
628
|
+
// limits. Only explicit native OOG can discover one larger legal envelope;
|
|
629
|
+
// semantic, opaque and transport failures cannot authorize a capacity retry.
|
|
421
630
|
let verified;
|
|
422
631
|
let validatedReplayError;
|
|
632
|
+
assertCurrent();
|
|
423
633
|
try {
|
|
424
634
|
if (backend === "eth_simulateV1")
|
|
425
635
|
verified = await lifecycleStage(client, "simulation.replay", () => simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true));
|
|
@@ -430,73 +640,128 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
|
|
|
430
640
|
}
|
|
431
641
|
}
|
|
432
642
|
catch (failure) {
|
|
433
|
-
const
|
|
643
|
+
const kind = simulationFailureKind(failure);
|
|
644
|
+
if (kind === "abort")
|
|
645
|
+
throw failure;
|
|
646
|
+
assertCurrent();
|
|
647
|
+
const message = lifecycleFailureReason(failure);
|
|
434
648
|
if (backend !== "eth_simulateV1") {
|
|
435
649
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
436
650
|
return { ...base, backend, admitted: false, steps: [], reason: `Exact-gas validated replay failed: ${message}` };
|
|
437
651
|
}
|
|
652
|
+
if (kind !== "out-of-gas") {
|
|
653
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
654
|
+
return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, failureCategory: kind === "source" ? "source" : "opaque", reason: `Exact-gas validated replay unavailable: ${message}` };
|
|
655
|
+
}
|
|
438
656
|
validatedReplayError = message;
|
|
439
657
|
verified = [];
|
|
440
658
|
}
|
|
659
|
+
assertCurrent();
|
|
441
660
|
let verifiedSteps = verified;
|
|
442
661
|
let admittedGasLimits = gasLimits;
|
|
443
662
|
let finalFailure = verifiedSteps.find((step) => !step.success);
|
|
663
|
+
let finalFailureReason;
|
|
444
664
|
if (finalFailure === undefined && validatedReplayError !== undefined) {
|
|
665
|
+
if (!envelopeDiscoveryGas.some((gas, index) => gas > (gasLimits[index] ?? gas))) {
|
|
666
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
667
|
+
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling failed: ${validatedReplayError}` };
|
|
668
|
+
}
|
|
445
669
|
// The buffered validated replay threw before yielding steps. Attempt one
|
|
446
670
|
// explicit envelope-discovery replay at the exact execution ceiling on the
|
|
447
671
|
// same pinned block; headroom is never silently raised — the carried gas is
|
|
448
672
|
// the validated envelope itself.
|
|
673
|
+
assertCurrent();
|
|
449
674
|
try {
|
|
450
|
-
const ceilingLimits =
|
|
675
|
+
const ceilingLimits = envelopeDiscoveryGas;
|
|
451
676
|
const retried = backend === "eth_simulateV1"
|
|
452
677
|
? await lifecycleStage(client, "simulation.envelope", () => simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true))
|
|
453
678
|
: options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
|
|
454
|
-
|
|
679
|
+
const retryFailureIndex = retried?.findIndex((step) => !step.success) ?? -1;
|
|
680
|
+
const retryFailure = retried?.[retryFailureIndex];
|
|
681
|
+
if (retried === undefined) {
|
|
455
682
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
456
|
-
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${
|
|
683
|
+
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${validatedReplayError}` };
|
|
457
684
|
}
|
|
458
685
|
admittedGasLimits = ceilingLimits;
|
|
459
686
|
verifiedSteps = retried;
|
|
460
|
-
finalFailure =
|
|
687
|
+
finalFailure = retryFailure;
|
|
461
688
|
validatedReplayError = undefined;
|
|
689
|
+
if (retryFailure !== undefined) {
|
|
690
|
+
const kind = transactions[retryFailureIndex]?.kind;
|
|
691
|
+
const indivisible = kind === "activate" || kind === "atomic";
|
|
692
|
+
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" : ""}`;
|
|
693
|
+
}
|
|
462
694
|
}
|
|
463
|
-
catch {
|
|
695
|
+
catch (failure) {
|
|
696
|
+
const kind = simulationFailureKind(failure);
|
|
697
|
+
if (kind === "abort")
|
|
698
|
+
throw failure;
|
|
699
|
+
assertCurrent();
|
|
464
700
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
465
|
-
|
|
701
|
+
if (kind !== "out-of-gas")
|
|
702
|
+
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)}` };
|
|
703
|
+
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` };
|
|
466
704
|
}
|
|
467
705
|
}
|
|
468
|
-
if (finalFailure !== undefined) {
|
|
706
|
+
else if (finalFailure !== undefined) {
|
|
469
707
|
const failedIndex = verifiedSteps.findIndex((step) => !step.success);
|
|
470
708
|
const bufferedLimit = gasLimits[failedIndex];
|
|
471
709
|
const failedTransaction = transactions[failedIndex];
|
|
472
|
-
if (bufferedLimit !== undefined && failedTransaction !== undefined &&
|
|
710
|
+
if (bufferedLimit !== undefined && failedTransaction !== undefined && (nextEnvelope === undefined || failedIndex !== 0) &&
|
|
711
|
+
looksGasExhausted(finalFailure, bufferedLimit) && (envelopeDiscoveryGas[failedIndex] ?? 0n) > bufferedLimit) {
|
|
473
712
|
// Buffered replay returned an exhaustion-shaped failed step; learn the true
|
|
474
713
|
// validated envelope with one explicit replay at the exact execution ceiling.
|
|
714
|
+
assertCurrent();
|
|
475
715
|
try {
|
|
476
|
-
const ceilingLimits =
|
|
716
|
+
const ceilingLimits = envelopeDiscoveryGas;
|
|
477
717
|
const retried = backend === "eth_simulateV1"
|
|
478
|
-
? await simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true)
|
|
718
|
+
? await lifecycleStage(client, "simulation.envelope", () => simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true))
|
|
479
719
|
: options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
|
|
480
|
-
if (retried
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
720
|
+
if (retried === undefined) {
|
|
721
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
722
|
+
return { ...base, backend, admitted: false, steps: measured, reason: "Validated execution at the exact current gas ceiling omitted transaction results" };
|
|
723
|
+
}
|
|
724
|
+
const retryFailureIndex = retried.findIndex((step) => !step.success);
|
|
725
|
+
const retryFailure = retried[retryFailureIndex];
|
|
726
|
+
admittedGasLimits = ceilingLimits;
|
|
727
|
+
verifiedSteps = retried;
|
|
728
|
+
finalFailure = retryFailure;
|
|
729
|
+
if (retryFailure !== undefined) {
|
|
730
|
+
const kind = transactions[retryFailureIndex]?.kind;
|
|
731
|
+
const indivisible = kind === "activate" || kind === "atomic";
|
|
732
|
+
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" : ""}`;
|
|
492
733
|
}
|
|
493
734
|
}
|
|
494
|
-
catch
|
|
735
|
+
catch (failure) {
|
|
736
|
+
const kind = simulationFailureKind(failure);
|
|
737
|
+
if (kind === "abort")
|
|
738
|
+
throw failure;
|
|
739
|
+
assertCurrent();
|
|
740
|
+
if (kind !== "out-of-gas") {
|
|
741
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
742
|
+
return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, reason: `Legal-envelope validated replay unavailable: ${lifecycleFailureReason(failure)}` };
|
|
743
|
+
}
|
|
744
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
745
|
+
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)}` };
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
assertCurrent();
|
|
750
|
+
if (nextEnvelope !== undefined && finalFailure === undefined) {
|
|
751
|
+
const currentMinimum = buffer(verifiedSteps[0]?.gasRequired ?? verifiedSteps[0]?.gasUsed ?? nextEnvelope.gas);
|
|
752
|
+
if (nextEnvelope.gas < currentMinimum) {
|
|
753
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
754
|
+
return { ...base, backend, admitted: false, executionProof: "proved", protocolFit: "failed", steps: verifiedSteps,
|
|
755
|
+
failureCategory: "capacity", capacityConstraint: "gas-envelope", failedTransactionId: transactions[0]?.id,
|
|
756
|
+
reason: "Exact reviewed replay leaves insufficient current minimum gas headroom" };
|
|
495
757
|
}
|
|
496
758
|
}
|
|
497
759
|
const estimates = [];
|
|
498
|
-
|
|
499
|
-
|
|
760
|
+
// A definitive returned replay failure already refuses execution. Retain its
|
|
761
|
+
// fee observations without inventing a balance or doing redundant payer work.
|
|
762
|
+
let remainingBalance = finalFailure === undefined
|
|
763
|
+
? rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance")
|
|
764
|
+
: undefined;
|
|
500
765
|
let affordabilityFailure;
|
|
501
766
|
const feeContext = configuration.estimateDataFee === undefined || sourceClient === client ? context : { ...context, client: sourceClient };
|
|
502
767
|
for (let index = 0; index < verifiedSteps.length; index += 1) {
|
|
@@ -505,7 +770,7 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
|
|
|
505
770
|
const gasLimit = admittedGasLimits[index];
|
|
506
771
|
if (step === undefined || transaction === undefined || gasLimit === undefined)
|
|
507
772
|
throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing verified fee context");
|
|
508
|
-
const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit }, feeContext);
|
|
773
|
+
const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit, gasPrice }, feeContext);
|
|
509
774
|
if (dataFee !== undefined && (typeof dataFee !== "bigint" || dataFee < 0n || dataFee >= 1n << 256n))
|
|
510
775
|
throw new LifecyclePlanningError("INVALID_DATA_FEE", "Data-fee oracle must return a nonnegative uint256 bigint amount");
|
|
511
776
|
// Nitro gas includes poster charges. Never add the quoted poster fee again.
|
|
@@ -513,13 +778,15 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
|
|
|
513
778
|
const required = transaction.value + executionFee + (dataFee ?? 0n);
|
|
514
779
|
if (required >= 1n << 256n)
|
|
515
780
|
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
|
|
516
|
-
if (
|
|
517
|
-
|
|
518
|
-
|
|
781
|
+
if (remainingBalance !== undefined) {
|
|
782
|
+
if (required > remainingBalance && affordabilityFailure === undefined)
|
|
783
|
+
affordabilityFailure = "Creator native balance cannot fund the remaining transaction value and execution/data fee envelopes";
|
|
784
|
+
remainingBalance -= required;
|
|
785
|
+
}
|
|
519
786
|
const poster = posterGas[index];
|
|
520
787
|
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" } });
|
|
521
788
|
}
|
|
522
789
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
523
|
-
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 };
|
|
790
|
+
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 };
|
|
524
791
|
}
|
|
525
792
|
//# sourceMappingURL=simulation.js.map
|