@black-market/sdk 0.9.1 → 0.11.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 +23 -17
- package/dist/lifecycle/abi.d.ts +1 -0
- package/dist/lifecycle/abi.d.ts.map +1 -1
- package/dist/lifecycle/abi.js +1 -0
- package/dist/lifecycle/abi.js.map +1 -1
- package/dist/lifecycle/calibration.d.ts.map +1 -1
- package/dist/lifecycle/calibration.js +2 -0
- package/dist/lifecycle/calibration.js.map +1 -1
- package/dist/lifecycle/deployments/{hmm-dynamic-pool-hook-v1-20261011.d.ts → hmm-dynamic-pool-hook-v2-20261017.d.ts} +21 -21
- package/dist/lifecycle/deployments/{hmm-dynamic-pool-hook-v1-20261011.d.ts.map → hmm-dynamic-pool-hook-v2-20261017.d.ts.map} +1 -1
- package/dist/lifecycle/deployments/hmm-dynamic-pool-hook-v2-20261017.js +69 -0
- package/dist/lifecycle/deployments/{hmm-dynamic-pool-hook-v1-20261011.js.map → hmm-dynamic-pool-hook-v2-20261017.js.map} +1 -1
- package/dist/lifecycle/deployments/{hmm-fixed-pool-hook-v1-20261011.d.ts → hmm-fixed-pool-hook-v2-20261017.d.ts} +21 -21
- package/dist/lifecycle/deployments/{hmm-fixed-pool-hook-v1-20261011.d.ts.map → hmm-fixed-pool-hook-v2-20261017.d.ts.map} +1 -1
- package/dist/lifecycle/deployments/hmm-fixed-pool-hook-v2-20261017.js +69 -0
- package/dist/lifecycle/deployments/{hmm-fixed-pool-hook-v1-20261011.js.map → hmm-fixed-pool-hook-v2-20261017.js.map} +1 -1
- package/dist/lifecycle/hmm-abi.d.ts +4544 -0
- package/dist/lifecycle/hmm-abi.d.ts.map +1 -0
- package/dist/lifecycle/hmm-abi.js +299 -0
- package/dist/lifecycle/hmm-abi.js.map +1 -0
- package/dist/lifecycle/hmm.d.ts +19 -3
- package/dist/lifecycle/hmm.d.ts.map +1 -1
- package/dist/lifecycle/hmm.js +11 -7
- package/dist/lifecycle/hmm.js.map +1 -1
- package/dist/lifecycle/markets.d.ts.map +1 -1
- package/dist/lifecycle/markets.js +2 -0
- package/dist/lifecycle/markets.js.map +1 -1
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +44 -35
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/presets.d.ts +7 -2
- package/dist/lifecycle/presets.d.ts.map +1 -1
- package/dist/lifecycle/presets.js +19 -5
- package/dist/lifecycle/presets.js.map +1 -1
- package/dist/lifecycle/rpc.js +3 -2
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts +1 -1
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +115 -174
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/transactions.d.ts +6 -0
- package/dist/lifecycle/transactions.d.ts.map +1 -1
- package/dist/lifecycle/transactions.js +39 -0
- package/dist/lifecycle/transactions.js.map +1 -1
- package/dist/lifecycle/types.d.ts +6 -3
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +197 -86
- package/examples/.env.example +4 -0
- package/examples/README.md +27 -6
- package/examples/hmm-launch-config.ts +4 -1
- package/examples/launch-example-support.ts +1 -0
- package/examples/launch-example.ts +1 -0
- package/examples/launch.ts +1 -0
- package/package.json +2 -2
- package/src/lifecycle/abi.ts +1 -0
- package/src/lifecycle/calibration.ts +1 -0
- package/src/lifecycle/deployments/hmm-dynamic-pool-hook-v2-20261017.ts +71 -0
- package/src/lifecycle/deployments/hmm-fixed-pool-hook-v2-20261017.ts +71 -0
- package/src/lifecycle/hmm-abi.ts +302 -0
- package/src/lifecycle/hmm.ts +12 -7
- package/src/lifecycle/markets.ts +1 -0
- package/src/lifecycle/planner.ts +41 -32
- package/src/lifecycle/presets.ts +20 -6
- package/src/lifecycle/rpc.ts +3 -2
- package/src/lifecycle/simulation.ts +111 -165
- package/src/lifecycle/transactions.ts +38 -1
- package/src/lifecycle/types.ts +9 -4
- package/dist/lifecycle/deployments/hmm-dynamic-pool-hook-v1-20261011.js +0 -69
- package/dist/lifecycle/deployments/hmm-fixed-pool-hook-v1-20261011.js +0 -69
- package/src/lifecycle/deployments/hmm-dynamic-pool-hook-v1-20261011.ts +0 -71
- package/src/lifecycle/deployments/hmm-fixed-pool-hook-v1-20261011.ts +0 -71
package/src/lifecycle/planner.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { prepareLaunchSimulation, simulateLaunchTransactions, type NextTransacti
|
|
|
10
10
|
import { LifecyclePlanningError, type BuildNextTransactionOptions, type CanonicalLaunchProgress, type LifecycleBlock, type LifecycleConstructionProfile, type LifecycleFundingPrerequisite, type LifecyclePoolBoundHookDeployment, type LifecycleProfile, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleTransaction, type PlannedLaunch, type PlanLaunchOptions, type PrepareAndPlanLifecycleLaunchOptions, type SimulateLaunchPlanOptions } from "./types.js";
|
|
11
11
|
import { deriveKnownPoolBoundHookDeployment, protectOrderedBuyMinimums, validateBuySlippageBps } from "./calibration.js";
|
|
12
12
|
import { getKnownLifecycleDeployment, getKnownLifecycleProfile } from "./presets.js";
|
|
13
|
-
import { buildLaunchTransactions } from "./transactions.js";
|
|
13
|
+
import { buildLaunchTransactions, packPreparationBatches } from "./transactions.js";
|
|
14
14
|
import { assertLifecycleMarketAllocation, assertLifecycleSupplyAllocation } from "./allocations.js";
|
|
15
15
|
|
|
16
16
|
type ValidatedPlanShape = {
|
|
@@ -132,11 +132,9 @@ async function readPlanMetadata(client: LifecycleRpcClient, plan: LaunchPlanV1,
|
|
|
132
132
|
return { profiles, hookDeployments, reasons: live.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Unlisted lifecycle profile is unavailable") };
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", progress: CanonicalLaunchProgress, approvals: readonly LifecycleTransaction[],
|
|
135
|
+
function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", progress: CanonicalLaunchProgress, approvals: readonly LifecycleTransaction[], batches: readonly number[]): LifecycleTransaction[] {
|
|
136
136
|
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) return [];
|
|
137
137
|
const prepared = progress.canonical.preparedMarkets;
|
|
138
|
-
const batches: number[] = prepared > 0 ? [prepared] : [];
|
|
139
|
-
for (let first = prepared; first < plan.markets.length; first += batchSize) batches.push(Math.min(batchSize, plan.markets.length - first));
|
|
140
138
|
const commands = buildLaunchTransactions({ plan, mode, preparationBatches: mode === "staged" ? batches : undefined }).filter((transaction) =>
|
|
141
139
|
(transaction.kind !== "begin" && transaction.kind !== "atomic" || progress.canonical.phase === LifecyclePhase.None) &&
|
|
142
140
|
(transaction.kind !== "prepare" || (transaction.marketStart ?? 0) >= prepared));
|
|
@@ -147,7 +145,8 @@ function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", prog
|
|
|
147
145
|
function attachEstimates(transactions: readonly LifecycleTransaction[], simulation: LifecycleSimulation): LifecycleTransaction[] {
|
|
148
146
|
return transactions.map((transaction) => {
|
|
149
147
|
const estimate = simulation.steps.find((step) => step.transactionId === transaction.id)?.estimate;
|
|
150
|
-
return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate,
|
|
148
|
+
return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate,
|
|
149
|
+
admission: estimate === undefined ? undefined : { admitted: simulation.admitted, confidence: simulation.confidence, executionProof: simulation.executionProof, protocolFit: simulation.protocolFit, transportPreflight: simulation.transportPreflight, blockNumber: simulation.blockNumber, blockHash: simulation.blockHash, account: simulation.account, chainId: simulation.chainId, reason: simulation.reason, limits: simulation.limits } };
|
|
151
150
|
});
|
|
152
151
|
}
|
|
153
152
|
type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value"> & NextTransactionEnvelope;
|
|
@@ -364,8 +363,8 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
|
|
|
364
363
|
}),
|
|
365
364
|
]));
|
|
366
365
|
assertPlanningActive(options.signal);
|
|
367
|
-
const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [] };
|
|
368
|
-
let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder,
|
|
366
|
+
const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [], deferredTransactionIds: [] };
|
|
367
|
+
let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, limits: options.limits,
|
|
369
368
|
allowTopologyGaps: options.allowTopologyGaps === true, allowMissingPermanentTail: options.allowMissingPermanentTail === true };
|
|
370
369
|
const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
|
|
371
370
|
if (!progress.confirmationSafe || receiptsPending) {
|
|
@@ -393,48 +392,58 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
|
|
|
393
392
|
]));
|
|
394
393
|
assertFinalPlan(planned, options.signal);
|
|
395
394
|
planned = { ...planned, prerequisites: funding.prerequisites, profiles: metadata.profiles, hookDeployments: metadata.hookDeployments };
|
|
396
|
-
|
|
397
|
-
|
|
395
|
+
const prepared = progress.canonical.preparedMarkets;
|
|
396
|
+
const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
|
|
397
|
+
let batches: number[] = [];
|
|
398
|
+
if (mode === "staged") batches = reviewed === undefined ? packPreparationBatches(plan, prepared, computeCap)
|
|
399
|
+
: [...(prepared > 0 ? [prepared] : []), ...(prepared < plan.markets.length ? [plan.markets.length - prepared] : [])];
|
|
400
|
+
if (reviewed !== undefined && reviewed.kind === "prepare") {
|
|
401
|
+
// Canonical progress fixes the start; the exact held calldata fixes its span.
|
|
402
|
+
// Never rediscover a batch or enlarge a reviewed transaction.
|
|
403
|
+
const match = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
|
|
404
|
+
const start = Number(match?.[1]), count = Number(match?.[2]);
|
|
405
|
+
if (start !== prepared || !Number.isSafeInteger(count) || count <= 0 || start + count > plan.markets.length) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation differs from canonical unfinished progress");
|
|
406
|
+
batches = [...(prepared > 0 ? [prepared] : []), count, ...(start + count < plan.markets.length ? [plan.markets.length - start - count] : [])];
|
|
407
|
+
}
|
|
408
|
+
let transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches);
|
|
409
|
+
if (reviewed !== undefined) {
|
|
410
|
+
if (!matchesReviewedNextTransaction(transactions[0], reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
|
|
411
|
+
transactions = transactions.slice(0, 1);
|
|
412
|
+
}
|
|
398
413
|
if (metadata.reasons.length > 0) {
|
|
399
414
|
await assertLifecycleBlock(client, block, plan.chainId);
|
|
400
415
|
return { ...planned, simulation: { ...placeholder, confidence: "stateful", reason: metadata.reasons.join("; "), failureCategory: "semantic" }, transactions, totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
|
|
401
416
|
}
|
|
402
|
-
const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block);
|
|
417
|
+
const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block, true, reviewed);
|
|
403
418
|
try {
|
|
404
|
-
const execute = diagnostic ? preparedSimulation.measure : (candidate: readonly LifecycleTransaction[]) =>
|
|
405
|
-
if (reviewed === undefined) return preparedSimulation(candidate);
|
|
406
|
-
if (!matchesReviewedNextTransaction(candidate[0], reviewed)) {
|
|
407
|
-
// A fresh prepare batch may still require SDK-owned capacity discovery.
|
|
408
|
-
// Never use held fees for a different step or caller-selected calldata.
|
|
409
|
-
if (candidate[0]?.kind === "prepare" && reviewed.kind === "prepare") return preparedSimulation(candidate);
|
|
410
|
-
throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
|
|
411
|
-
}
|
|
412
|
-
return preparedSimulation(candidate, reviewed);
|
|
413
|
-
};
|
|
419
|
+
const execute = diagnostic ? preparedSimulation.measure : (candidate: readonly LifecycleTransaction[]) => preparedSimulation(candidate, reviewed);
|
|
414
420
|
let simulation = await execute(transactions);
|
|
415
421
|
assertFinalPlan(planned, options.signal);
|
|
416
422
|
if (mode === "staged") {
|
|
417
|
-
// Only
|
|
418
|
-
//
|
|
419
|
-
while (!simulation.admitted && simulation.
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
simulation.
|
|
423
|
-
const
|
|
424
|
-
if (
|
|
425
|
-
|
|
426
|
-
|
|
423
|
+
// Only a measurement that consumed almost its entire legal envelope
|
|
424
|
+
// invalidates packing estimates, including gas hidden by CREATE wrappers.
|
|
425
|
+
while (reviewed === undefined && !simulation.admitted && simulation.failedTransactionId?.startsWith("prepare:")) {
|
|
426
|
+
const failedIndex = transactions.findIndex((transaction) => transaction.id === simulation.failedTransactionId);
|
|
427
|
+
const failedTransaction = transactions[failedIndex];
|
|
428
|
+
const step = simulation.steps.find((candidate) => candidate.transactionId === simulation.failedTransactionId);
|
|
429
|
+
const count = failedTransaction?.marketCount ?? 0;
|
|
430
|
+
if (count <= 1 || step?.estimate !== undefined || step?.gasUsed === undefined || step.gasLimit === undefined || step.gasUsed * 100n < step.gasLimit * 95n) break;
|
|
431
|
+
let first = 0;
|
|
432
|
+
const batchIndex = batches.findIndex((batch) => { const matches = first === failedTransaction?.marketStart; first += batch; return matches; });
|
|
433
|
+
const left = Math.ceil(count / 2);
|
|
434
|
+
batches.splice(batchIndex, 1, left, count - left);
|
|
435
|
+
transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches);
|
|
427
436
|
simulation = await execute(transactions);
|
|
428
437
|
assertFinalPlan(planned, options.signal);
|
|
429
438
|
}
|
|
430
439
|
if (!simulation.admitted && simulation.failedTransactionId === "activate") simulation = { ...simulation, reason: `${simulation.reason ?? "Final activation failed"}; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned` };
|
|
431
|
-
} else if (!diagnostic && !simulation.admitted) {
|
|
440
|
+
} else if (!diagnostic && !simulation.admitted && !(simulation.capacityConstraint === "compute" && simulation.confidence === "provisional")) {
|
|
432
441
|
simulation = { ...simulation, reason: `${simulation.reason ?? "Exact atomic plan is not admitted"}; choose staged explicitly only if empty preparation can be partitioned and complete activation fits` };
|
|
433
442
|
}
|
|
434
443
|
if (reviewed !== undefined && !matchesReviewedNextTransaction(transactions[0], reviewed)) {
|
|
435
444
|
throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
|
|
436
445
|
}
|
|
437
|
-
return { ...planned, simulation,
|
|
446
|
+
return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
|
|
438
447
|
} finally { preparedSimulation.close(); }
|
|
439
448
|
}
|
|
440
449
|
|
package/src/lifecycle/presets.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { Address, Hex } from "viem";
|
|
2
2
|
import type {
|
|
3
|
-
AdapterRegistrationV1, LaunchEnvelopeV2, ProfileRegistrationV1, ProfileTopologyV1,
|
|
3
|
+
AdapterRegistrationV1, LaunchEnvelopeV2, LaunchPlanV1, ProfileRegistrationV1, ProfileTopologyV1,
|
|
4
4
|
} from "./schema.js";
|
|
5
5
|
import type { LifecycleProfile } from "./types.js";
|
|
6
6
|
import { fixedFeePoolHookV1CreationCode } from "./deployments/fixed-fee-pool-hook-v1-20261008.js";
|
|
7
7
|
import { dynamicFeePoolHookV1CreationCode } from "./deployments/dynamic-fee-pool-hook-v1-20261011.js";
|
|
8
|
-
import { hmmFixedEnvelope, hmmFixedAdapter, hmmFixedRegistration, hmmFixedCreationCode } from "./deployments/hmm-fixed-pool-hook-
|
|
9
|
-
import { hmmDynamicEnvelope, hmmDynamicAdapter, hmmDynamicRegistration, hmmDynamicCreationCode } from "./deployments/hmm-dynamic-pool-hook-
|
|
10
|
-
import { HMM_FIXED_PROFILE_ID, HMM_DYNAMIC_PROFILE_ID } from "./hmm.js";
|
|
8
|
+
import { hmmFixedEnvelope, hmmFixedAdapter, hmmFixedRegistration, hmmFixedCreationCode } from "./deployments/hmm-fixed-pool-hook-v2-20261017.js";
|
|
9
|
+
import { hmmDynamicEnvelope, hmmDynamicAdapter, hmmDynamicRegistration, hmmDynamicCreationCode } from "./deployments/hmm-dynamic-pool-hook-v2-20261017.js";
|
|
10
|
+
import { HMM_FIXED_PROFILE_ID, HMM_DYNAMIC_PROFILE_ID, isHmmProfile } from "./hmm.js";
|
|
11
11
|
|
|
12
12
|
export enum KnownLifecycleProfile {
|
|
13
13
|
Abyss0 = "abyss-0",
|
|
@@ -23,8 +23,8 @@ export enum KnownLifecycleProfile {
|
|
|
23
23
|
export enum KnownLifecycleHook {
|
|
24
24
|
FixedFeePoolV1 = "fixed-fee-pool-v1",
|
|
25
25
|
DynamicFeePoolV1 = "dynamic-fee-pool-v1",
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
HmmFixedPoolV2 = "hmm-fixed-pool-v2",
|
|
27
|
+
HmmDynamicPoolV2 = "hmm-dynamic-pool-v2",
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
Object.freeze(KnownLifecycleProfile);
|
|
@@ -280,6 +280,20 @@ const dynamicFeePoolPreset: LifecycleProfilePreset = Object.freeze({
|
|
|
280
280
|
}),
|
|
281
281
|
});
|
|
282
282
|
|
|
283
|
+
// HMMLiquidityHookV2.sol:98-119 requires ~3.06M available before manager calls,
|
|
284
|
+
// including opening and swaps. Robinhood fork measurements: activation minima
|
|
285
|
+
// 9.60M/15.40M/21.11M for 1/2/3 markets despite used gas 5.85M/11.13M/16.54M.
|
|
286
|
+
// Apply this forwarding target once per activate/atomic envelope, not per callback.
|
|
287
|
+
export const HMM_V2_FORWARDING_BASE_GAS = 4_250_000n;
|
|
288
|
+
export const HMM_V2_FORWARDING_EXTRA_MARKET_GAS = 500_000n;
|
|
289
|
+
export const HMM_V2_PREPARE_GAS = 14_500_000n;
|
|
290
|
+
|
|
291
|
+
export function hmmV2ForwardingReserve(plan: LaunchPlanV1): bigint {
|
|
292
|
+
let count = 0;
|
|
293
|
+
for (const market of plan.markets) if (isHmmProfile(market.profileId)) count += 1;
|
|
294
|
+
return count === 0 ? 0n : HMM_V2_FORWARDING_BASE_GAS + HMM_V2_FORWARDING_EXTRA_MARKET_GAS * BigInt(count - 1);
|
|
295
|
+
}
|
|
296
|
+
|
|
283
297
|
function hmmPreset(key: KnownLifecycleProfile, id: Hex, envelope: LaunchEnvelopeV2,
|
|
284
298
|
adapter: AdapterRegistrationV1, registration: ProfileRegistrationV1, creationCode: Hex): LifecycleProfilePreset {
|
|
285
299
|
return Object.freeze({
|
package/src/lifecycle/rpc.ts
CHANGED
|
@@ -433,6 +433,7 @@ async function resolveLifecycleLimitsFresh(context: LifecycleLimitContext, sourc
|
|
|
433
433
|
throw new LifecyclePlanningError("LIMIT_CONTEXT_MISMATCH", "Supplied execution policy provenance differs from the canonical chain/account/core/block context");
|
|
434
434
|
}
|
|
435
435
|
const protocol = context.chainId === 4663n ? "nitro" : "evm";
|
|
436
|
+
const maxSimulationGas = configuration.maxSimulationGas ?? (context.chainId === 4663n ? 50_000_000n : undefined);
|
|
436
437
|
let arbOSVersion: bigint | undefined; let maxTxComputeGas: bigint | undefined; let maxBlockComputeGas: bigint | undefined;
|
|
437
438
|
if (protocol === "nitro") {
|
|
438
439
|
try {
|
|
@@ -464,14 +465,14 @@ async function resolveLifecycleLimitsFresh(context: LifecycleLimitContext, sourc
|
|
|
464
465
|
...(configuration.rpcGasLimit === undefined ? ["RPC transaction gas-envelope cap is unknown"] : []),
|
|
465
466
|
...(configuration.accountGasLimit === undefined ? ["Current account/wallet transaction gas-envelope cap is unknown"] : []),
|
|
466
467
|
...(configuration.maxCalldataBytes === undefined ? ["Applicable chain/account/RPC calldata byte cap is unknown"] : []),
|
|
467
|
-
...(
|
|
468
|
+
...(maxSimulationGas === undefined ? ["RPC aggregate simulation gas cap is unknown"] : []),
|
|
468
469
|
];
|
|
469
470
|
assertCurrent();
|
|
470
471
|
return {
|
|
471
472
|
configuration, assertCurrent,
|
|
472
473
|
limits: Object.freeze({
|
|
473
474
|
protocol, executionGasCeiling, transactionGasCeiling, arbOSVersion, maxTxComputeGas, maxBlockComputeGas, headroomBps,
|
|
474
|
-
maxCalldataBytes: configuration.maxCalldataBytes, maxSimulationGas
|
|
475
|
+
maxCalldataBytes: configuration.maxCalldataBytes, maxSimulationGas,
|
|
475
476
|
blockGasLimit: context.block.gasLimit, chainGasLimit: configuration.chainGasLimit,
|
|
476
477
|
rpcGasLimit: configuration.rpcGasLimit, accountGasLimit: configuration.accountGasLimit,
|
|
477
478
|
unknownExecutionConstraints: Object.freeze(unknownExecutionConstraints),
|
|
@@ -4,6 +4,8 @@ import { hashLaunchIdentity, hashLaunchPlan, LifecyclePhase, type LaunchPlanV1,
|
|
|
4
4
|
import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readLifecycleChainId, readNitroPosterGas, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity, type LifecycleLimitObservation } from "./rpc.js";
|
|
5
5
|
import { LifecyclePlanningError, type ControlledLifecycleFork, type LifecycleBlock, type LifecycleLimitContext, type LifecyclePostcondition, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleSimulationStep, type LifecycleTransaction, type PlannedLaunch, type ResolvedLifecycleLimits, type SimulateLaunchPlanOptions } from "./types.js";
|
|
6
6
|
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion } from "./markets.js";
|
|
7
|
+
import { hmmV2ForwardingReserve } from "./presets.js";
|
|
8
|
+
import { transactionGasEstimate } from "./transactions.js";
|
|
7
9
|
|
|
8
10
|
function committedPositionCount(plan: LaunchPlanV1): number {
|
|
9
11
|
let count = 0;
|
|
@@ -111,8 +113,6 @@ async function proveNitroBackend(client: LifecycleRpcClient, planned: PlannedLau
|
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunch, block: LifecycleBlock, transactions: readonly LifecycleTransaction[], gasLimits: readonly bigint[], limits: ResolvedLifecycleLimits, gasPrice: bigint, validation: boolean): Promise<LifecycleSimulationStep[]> {
|
|
114
|
-
const totalRequestedGas = gasLimits.reduce((total, gas) => total + gas, 0n);
|
|
115
|
-
if (limits.maxSimulationGas !== undefined && totalRequestedGas > limits.maxSimulationGas) throw new LifecyclePlanningError("SIMULATION_RPC_CAP", "Sequential request exceeds the current RPC aggregate simulation gas cap; use an isolated controlled fork");
|
|
116
116
|
// Natural child-block fees must govern fee-bearing admission; overriding
|
|
117
117
|
// baseFeePerGas with the parent's would fabricate a lower envelope and admit
|
|
118
118
|
// calls that the actual chain would reject. eth_simulateV1 derives the child
|
|
@@ -315,7 +315,7 @@ function observedFields<T extends object>(value: T): () => boolean {
|
|
|
315
315
|
|
|
316
316
|
/** Internal continuation owned by one SDK read invocation. Only its positive
|
|
317
317
|
* capability proof is reused; every candidate executes its entire economic sequence. */
|
|
318
|
-
export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true): PreparedLaunchSimulation {
|
|
318
|
+
export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true, reviewedEnvelope?: NextTransactionEnvelope): PreparedLaunchSimulation {
|
|
319
319
|
const { client, planned, fork } = options;
|
|
320
320
|
const signal = options.signal;
|
|
321
321
|
if (signal?.aborted) throw Object.assign(new Error("Lifecycle simulation cancelled"), { name: "AbortError" });
|
|
@@ -353,7 +353,7 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
|
|
|
353
353
|
resolveLifecycleLimits(context, policy),
|
|
354
354
|
readLifecycleChainId(client),
|
|
355
355
|
lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block),
|
|
356
|
-
hasTransactions ? lifecycleRpc(client, "eth_gasPrice") : undefined,
|
|
356
|
+
hasTransactions && reviewedEnvelope === undefined ? lifecycleRpc(client, "eth_gasPrice") : undefined,
|
|
357
357
|
]));
|
|
358
358
|
})().then((observations) => ({ observations }), (failure: unknown) => ({ failure }));
|
|
359
359
|
// Refused inputs may discard this read-only preparation before it settles.
|
|
@@ -439,7 +439,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
439
439
|
// Standalone calls always produce fresh context, including live chain and fees.
|
|
440
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
441
|
const block = pinnedBlock ?? await readLifecycleBlock(options.client);
|
|
442
|
-
const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0);
|
|
442
|
+
const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope);
|
|
443
443
|
try { return await prepared(transactions, nextEnvelope); }
|
|
444
444
|
finally { prepared.close(); }
|
|
445
445
|
}
|
|
@@ -458,7 +458,7 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
458
458
|
const sourceClient = lifecycleSourceClient(client);
|
|
459
459
|
const { block } = context;
|
|
460
460
|
const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = observations;
|
|
461
|
-
const base = { confidence: "stateful" as const, executionProof: "failed" as const, protocolFit: "unknown" as const, transportPreflight: "not-requested" as const, blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits };
|
|
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, deferredTransactionIds: [] as string[] };
|
|
462
462
|
if (chainId !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
|
|
463
463
|
const accountCode = rpcHex(accountCodeValue, "account code");
|
|
464
464
|
if (accountCode !== "0x") {
|
|
@@ -473,36 +473,48 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
473
473
|
}
|
|
474
474
|
}
|
|
475
475
|
if (transactions.length === 0) { await assertLifecycleBlock(client, block, planned.chainId); return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] }; }
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
476
|
+
if (nextEnvelope !== undefined && (transactions.length !== 1 || typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas <= 0n || nextEnvelope.gas >= 1n << 64n ||
|
|
477
|
+
typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
|
|
478
|
+
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Reviewed replay requires one immediate transaction with exact uint64 gas and uint256 price");
|
|
479
|
+
}
|
|
480
|
+
const allTransactions = transactions;
|
|
481
|
+
if (limits.maxSimulationGas !== undefined && nextEnvelope === undefined) {
|
|
482
|
+
let estimatedGas = 0n, count = 0;
|
|
483
|
+
for (const transaction of transactions) {
|
|
484
|
+
estimatedGas += transactionGasEstimate(planned.plan, transaction);
|
|
485
|
+
if (estimatedGas > limits.maxSimulationGas) break;
|
|
486
|
+
count += 1;
|
|
487
|
+
}
|
|
488
|
+
transactions = transactions.slice(0, count);
|
|
489
|
+
base.deferredTransactionIds = allTransactions.slice(count).map((transaction) => transaction.id);
|
|
490
|
+
if (count === 0) {
|
|
491
|
+
const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
|
|
492
|
+
const exceedsCompute = estimatedGas > computeCap;
|
|
493
|
+
const gasInMillions = (gas: bigint) => {
|
|
494
|
+
const tenths = (gas + 50_000n) / 100_000n;
|
|
495
|
+
return `${tenths / 10n}.${tenths % 10n}`;
|
|
496
|
+
};
|
|
497
|
+
const atomic = allTransactions[0]!.kind === "atomic";
|
|
498
|
+
const reason = exceedsCompute
|
|
499
|
+
? `${atomic ? "Atomic launch" : "Immediate transaction"} needs about ${gasInMillions(estimatedGas)}M gas, above the ${gasInMillions(computeCap)}M per-transaction limit${atomic ? "; use staged" : ""}`
|
|
500
|
+
: "The immediate transaction estimate exceeds the RPC aggregate simulation gas cap; no prefix can be proved";
|
|
501
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
502
|
+
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: exceedsCompute ? "compute" : "simulation-gas", reason };
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const recommendedGasPrice = nextEnvelope === undefined ? rpcQuantity(gasPriceValue, "gas price") : nextEnvelope.gasPrice;
|
|
506
|
+
// New recommendations cover lagging quotes; reviewed fees are replayed unchanged
|
|
507
|
+
// under natural child fees, never refreshed or silently increased.
|
|
481
508
|
const baseFeeEnvelope = (block.baseFeePerGas ?? 0n) * 2n;
|
|
482
509
|
const gasPrice = nextEnvelope?.gasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
|
|
483
510
|
if (gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
|
|
484
|
-
if (nextEnvelope !== undefined && (typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas <= 0n || nextEnvelope.gas >= 1n << 64n ||
|
|
485
|
-
typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
|
|
486
|
-
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Reviewed gas and gas price must be exact positive uint64 gas and nonnegative uint256 price");
|
|
487
|
-
}
|
|
488
511
|
const buffer = (gas: bigint) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
|
|
489
|
-
const computeCeiling = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
|
|
490
|
-
// Nonnegative poster gas cannot make this complete discovery request smaller.
|
|
491
|
-
// A generic controlled fork retains the existing RPC-to-fork fallback.
|
|
492
|
-
if (limits.maxSimulationGas !== undefined && BigInt(transactions.length) * computeCeiling > limits.maxSimulationGas &&
|
|
493
|
-
(limits.protocol === "nitro" || options.fork === undefined)) {
|
|
494
|
-
assertCurrent();
|
|
495
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
496
|
-
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: "simulation-gas", reason: `Stateful sequential RPC simulation unavailable: Sequential request exceeds the current RPC aggregate simulation gas cap; use an isolated controlled fork. ${limits.protocol === "nitro" ? "Generic Anvil/Hardhat forks cannot prove ArbOS compute/poster metering." : "Dependent eth_call estimates are not proof."}` };
|
|
497
|
-
}
|
|
498
512
|
const posterGas: bigint[] = [];
|
|
499
513
|
if (limits.protocol === "nitro") {
|
|
500
|
-
// Lightweight poster reads may overlap the isolated capability request.
|
|
501
|
-
// Economic work is not even queued until both prerequisites have succeeded.
|
|
502
514
|
try {
|
|
503
515
|
const [, estimates] = await Promise.all([
|
|
504
516
|
proveBackend(limits, gasPrice),
|
|
505
|
-
lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
|
|
517
|
+
nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))) : [],
|
|
506
518
|
]);
|
|
507
519
|
posterGas.push(...estimates);
|
|
508
520
|
} catch (failure) {
|
|
@@ -513,176 +525,110 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
513
525
|
}
|
|
514
526
|
}
|
|
515
527
|
assertCurrent();
|
|
516
|
-
// The poster quote allocates gas only. The native exact replay enforces actual
|
|
517
|
-
// compute hold; subtracting this approximate budget from gasUsed would not.
|
|
518
528
|
const discoveryGas = transactions.map((_, index) => {
|
|
519
529
|
const total = limits.executionGasCeiling + (posterGas[index] ?? 0n);
|
|
520
530
|
const capped = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < total ? limits.transactionGasCeiling : total;
|
|
521
531
|
if (capped <= 0n || capped >= 1n << 64n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Compute/poster envelope exceeds uint64 gas");
|
|
522
532
|
return capped;
|
|
523
533
|
});
|
|
524
|
-
let backend: "eth_simulateV1" | "controlled-fork" = "eth_simulateV1";
|
|
525
|
-
let measured: LifecycleSimulationStep[];
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
534
|
+
let backend: "eth_simulateV1" | "controlled-fork" = nextEnvelope !== undefined && limits.protocol === "evm" && options.fork !== undefined ? "controlled-fork" : "eth_simulateV1";
|
|
535
|
+
let measured: LifecycleSimulationStep[] = [];
|
|
536
|
+
if (nextEnvelope === undefined) {
|
|
537
|
+
try {
|
|
538
|
+
measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, discoveryGas, limits, gasPrice, false));
|
|
539
|
+
} catch (failure) {
|
|
540
|
+
if (simulationFailureKind(failure) === "abort") throw failure;
|
|
541
|
+
assertCurrent();
|
|
542
|
+
if (options.fork === undefined || limits.protocol === "nitro") {
|
|
543
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
544
|
+
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."}` };
|
|
545
|
+
}
|
|
546
|
+
backend = "controlled-fork";
|
|
547
|
+
measured = await simulateForkPass(options.fork, planned, block, transactions, discoveryGas, gasPrice, true, limits.headroomBps);
|
|
548
|
+
}
|
|
533
549
|
assertCurrent();
|
|
534
|
-
|
|
550
|
+
const failedIndex = measured.findIndex((step) => !step.success);
|
|
551
|
+
if (failedIndex >= 0 && limits.maxSimulationGas !== undefined &&
|
|
552
|
+
measured.reduce((gas, step) => gas + (step.gasUsed ?? 0n), 0n) * 100n >= limits.maxSimulationGas * 95n) {
|
|
553
|
+
// The provider exhausted its aggregate used-gas budget, not the contract.
|
|
554
|
+
// Preserve only actual successful results; the failed call is deferred too.
|
|
555
|
+
measured = measured.slice(0, failedIndex);
|
|
556
|
+
transactions = transactions.slice(0, failedIndex);
|
|
557
|
+
base.deferredTransactionIds = allTransactions.slice(failedIndex).map((transaction) => transaction.id);
|
|
558
|
+
if (transactions.length === 0) {
|
|
559
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
560
|
+
return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "source", reason: "RPC aggregate simulation gas cap left no successful prefix to prove" };
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const failed = measured.find((step) => !step.success);
|
|
564
|
+
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 }; }
|
|
565
|
+
if (diagnostic) {
|
|
535
566
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
536
|
-
return { ...base,
|
|
567
|
+
return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
|
|
537
568
|
}
|
|
538
|
-
backend = "controlled-fork";
|
|
539
|
-
measured = await simulateForkPass(options.fork, planned, block, transactions, discoveryGas, gasPrice, true, limits.headroomBps);
|
|
540
|
-
}
|
|
541
|
-
assertCurrent();
|
|
542
|
-
const failed = measured.find((step) => !step.success);
|
|
543
|
-
if (failed !== undefined) { await assertLifecycleBlock(client, block, planned.chainId); return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId, failureCategory: failed.failureCategory ?? "opaque", capacityConstraint: failed.failureCategory === "capacity" ? "compute" : undefined }; }
|
|
544
|
-
if (diagnostic) {
|
|
545
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
546
|
-
return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
|
|
547
569
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
570
|
+
const gasLimits = nextEnvelope === undefined ? measured.map((step, index) => {
|
|
571
|
+
const gross = step.gasRequired ?? step.gasUsed!;
|
|
572
|
+
const reserve = transactions[index]?.kind === "activate" || transactions[index]?.kind === "atomic" ? hmmV2ForwardingReserve(planned.plan) : 0n;
|
|
573
|
+
const desired = gross + reserve > buffer(gross) ? gross + reserve : buffer(gross);
|
|
574
|
+
const available = discoveryGas[index]! - (posterGas[index] ?? 0n);
|
|
575
|
+
return (desired < available ? desired : available) + (posterGas[index] ?? 0n);
|
|
576
|
+
}) : [nextEnvelope.gas];
|
|
577
|
+
const exceeds = gasLimits.findIndex((gas, index) =>
|
|
578
|
+
gas <= 0n || gas >= 1n << 64n || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling ||
|
|
579
|
+
nextEnvelope === undefined && (measured[index]!.gasRequired ?? measured[index]!.gasUsed!) > gas - (posterGas[index] ?? 0n) ||
|
|
580
|
+
limits.protocol === "evm" && gas > limits.executionGasCeiling);
|
|
557
581
|
if (exceeds >= 0) {
|
|
558
582
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
559
|
-
return { ...base, executionProof: "unavailable", protocolFit: "failed", backend, admitted: false, steps: measured, failureCategory: "capacity", capacityConstraint:
|
|
583
|
+
return { ...base, executionProof: "unavailable", protocolFit: "failed", backend, admitted: false, steps: measured, failureCategory: "capacity", capacityConstraint: "gas-envelope", reason: "Measured execution or reviewed gas exceeds the current compute/complete transaction envelope", failedTransactionId: transactions[exceeds]?.id };
|
|
560
584
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
let verified: LifecycleSimulationStep[];
|
|
568
|
-
let validatedReplayError: string | undefined;
|
|
569
|
-
assertCurrent();
|
|
585
|
+
let verifiedSteps: LifecycleSimulationStep[] = [];
|
|
586
|
+
let admittedGasLimits = gasLimits;
|
|
587
|
+
let replayError: string | undefined;
|
|
588
|
+
const replay = (gas: readonly bigint[]) => backend === "eth_simulateV1"
|
|
589
|
+
? simulateRpcPass(client, planned, block, transactions, gas, limits, gasPrice, true)
|
|
590
|
+
: simulateForkPass(options.fork!, planned, block, transactions, gas, gasPrice, false, limits.headroomBps);
|
|
570
591
|
try {
|
|
571
|
-
|
|
572
|
-
else {
|
|
573
|
-
if (options.fork === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Controlled fork is missing for the exact-gas replay");
|
|
574
|
-
verified = await simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice, false, limits.headroomBps);
|
|
575
|
-
}
|
|
592
|
+
verifiedSteps = await lifecycleStage(client, "simulation.replay", () => replay(gasLimits));
|
|
576
593
|
} catch (failure) {
|
|
577
594
|
const kind = simulationFailureKind(failure);
|
|
578
595
|
if (kind === "abort") throw failure;
|
|
579
596
|
assertCurrent();
|
|
580
|
-
|
|
581
|
-
if (backend !== "eth_simulateV1") {
|
|
597
|
+
if (kind !== "out-of-gas" || backend !== "eth_simulateV1" || nextEnvelope !== undefined) {
|
|
582
598
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
583
|
-
return { ...base, backend, admitted: false, steps:
|
|
599
|
+
return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, failureCategory: kind === "source" ? "source" : "opaque", reason: `Exact-gas validated replay unavailable: ${lifecycleFailureReason(failure)}` };
|
|
584
600
|
}
|
|
585
|
-
|
|
586
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
587
|
-
return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, failureCategory: kind === "source" ? "source" : "opaque", reason: `Exact-gas validated replay unavailable: ${message}` };
|
|
588
|
-
}
|
|
589
|
-
validatedReplayError = message;
|
|
590
|
-
verified = [];
|
|
601
|
+
replayError = lifecycleFailureReason(failure);
|
|
591
602
|
}
|
|
592
603
|
assertCurrent();
|
|
593
|
-
let verifiedSteps = verified;
|
|
594
|
-
let admittedGasLimits = gasLimits;
|
|
595
604
|
let finalFailure = verifiedSteps.find((step) => !step.success);
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
// explicit envelope-discovery replay at the exact execution ceiling on the
|
|
604
|
-
// same pinned block; headroom is never silently raised — the carried gas is
|
|
605
|
-
// the validated envelope itself.
|
|
605
|
+
const failedIndex = verifiedSteps.findIndex((step) => !step.success);
|
|
606
|
+
const rescue = nextEnvelope === undefined && backend === "eth_simulateV1" &&
|
|
607
|
+
(replayError !== undefined && discoveryGas.some((gas, index) => index < transactions.length && gas > gasLimits[index]!) ||
|
|
608
|
+
finalFailure !== undefined && looksGasExhausted(finalFailure, gasLimits[failedIndex]!) && discoveryGas[failedIndex]! > gasLimits[failedIndex]!);
|
|
609
|
+
// One legal-envelope rescue covers thrown/returned genuine OOG. Never retry
|
|
610
|
+
// semantic errors, switch replay backends, or enlarge a held immediate envelope.
|
|
611
|
+
if (rescue) {
|
|
606
612
|
assertCurrent();
|
|
607
613
|
try {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
const retryFailureIndex = retried?.findIndex((step) => !step.success) ?? -1;
|
|
613
|
-
const retryFailure = retried?.[retryFailureIndex];
|
|
614
|
-
if (retried === undefined) {
|
|
615
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
616
|
-
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${validatedReplayError}` };
|
|
617
|
-
}
|
|
618
|
-
admittedGasLimits = ceilingLimits;
|
|
619
|
-
verifiedSteps = retried;
|
|
620
|
-
finalFailure = retryFailure;
|
|
621
|
-
validatedReplayError = undefined;
|
|
622
|
-
if (retryFailure !== undefined) {
|
|
623
|
-
const kind = transactions[retryFailureIndex]?.kind;
|
|
624
|
-
const indivisible = kind === "activate" || kind === "atomic";
|
|
625
|
-
finalFailureReason = `Validated execution at the exact current gas ceiling still failed: ${retryFailure.error ?? "Execution reverted"}${indivisible ? "; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned" : ""}`;
|
|
626
|
-
}
|
|
614
|
+
admittedGasLimits = discoveryGas.slice(0, transactions.length);
|
|
615
|
+
verifiedSteps = await lifecycleStage(client, "simulation.envelope", () => replay(admittedGasLimits));
|
|
616
|
+
finalFailure = verifiedSteps.find((step) => !step.success);
|
|
617
|
+
replayError = undefined;
|
|
627
618
|
} catch (failure) {
|
|
628
619
|
const kind = simulationFailureKind(failure);
|
|
629
620
|
if (kind === "abort") throw failure;
|
|
630
621
|
assertCurrent();
|
|
631
622
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
632
|
-
if (kind !== "out-of-gas") return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, failureCategory: kind === "source" ? "source" : "opaque", reason: `Legal-envelope validated replay unavailable
|
|
633
|
-
return { ...base, backend, admitted: false, steps: measured, reason: `Validated
|
|
634
|
-
}
|
|
635
|
-
} else if (finalFailure !== undefined) {
|
|
636
|
-
const failedIndex = verifiedSteps.findIndex((step) => !step.success);
|
|
637
|
-
const bufferedLimit = gasLimits[failedIndex];
|
|
638
|
-
const failedTransaction = transactions[failedIndex];
|
|
639
|
-
if (bufferedLimit !== undefined && failedTransaction !== undefined && (nextEnvelope === undefined || failedIndex !== 0) &&
|
|
640
|
-
looksGasExhausted(finalFailure, bufferedLimit) && (envelopeDiscoveryGas[failedIndex] ?? 0n) > bufferedLimit) {
|
|
641
|
-
// Buffered replay returned an exhaustion-shaped failed step; learn the true
|
|
642
|
-
// validated envelope with one explicit replay at the exact execution ceiling.
|
|
643
|
-
assertCurrent();
|
|
644
|
-
try {
|
|
645
|
-
const ceilingLimits = envelopeDiscoveryGas;
|
|
646
|
-
const retried = backend === "eth_simulateV1"
|
|
647
|
-
? await lifecycleStage(client, "simulation.envelope", () => simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true))
|
|
648
|
-
: options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
|
|
649
|
-
if (retried === undefined) {
|
|
650
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
651
|
-
return { ...base, backend, admitted: false, steps: measured, reason: "Validated execution at the exact current gas ceiling omitted transaction results" };
|
|
652
|
-
}
|
|
653
|
-
const retryFailureIndex = retried.findIndex((step) => !step.success);
|
|
654
|
-
const retryFailure = retried[retryFailureIndex];
|
|
655
|
-
admittedGasLimits = ceilingLimits;
|
|
656
|
-
verifiedSteps = retried;
|
|
657
|
-
finalFailure = retryFailure;
|
|
658
|
-
if (retryFailure !== undefined) {
|
|
659
|
-
const kind = transactions[retryFailureIndex]?.kind;
|
|
660
|
-
const indivisible = kind === "activate" || kind === "atomic";
|
|
661
|
-
finalFailureReason = `Validated execution at the exact current gas ceiling still failed: ${retryFailure.error ?? "Execution reverted"}${indivisible ? "; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned" : ""}`;
|
|
662
|
-
}
|
|
663
|
-
} catch (failure) {
|
|
664
|
-
const kind = simulationFailureKind(failure);
|
|
665
|
-
if (kind === "abort") throw failure;
|
|
666
|
-
assertCurrent();
|
|
667
|
-
if (kind !== "out-of-gas") {
|
|
668
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
669
|
-
return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, reason: `Legal-envelope validated replay unavailable: ${lifecycleFailureReason(failure)}` };
|
|
670
|
-
}
|
|
671
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
672
|
-
return { ...base, backend, admitted: false, steps: measured, reason: `Validated buffered replay failed (${finalFailure?.error ?? "Execution reverted"}) and execution at the exact gas ceiling still reverted: ${lifecycleFailureReason(failure)}` };
|
|
673
|
-
}
|
|
623
|
+
if (kind !== "out-of-gas") return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: measured, failureCategory: kind === "source" ? "source" : "opaque", reason: `Legal-envelope validated replay unavailable: ${lifecycleFailureReason(failure)}` };
|
|
624
|
+
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${lifecycleFailureReason(failure)}` };
|
|
674
625
|
}
|
|
675
626
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
if (nextEnvelope.gas < currentMinimum) {
|
|
680
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
681
|
-
return { ...base, backend, admitted: false, executionProof: "proved", protocolFit: "failed", steps: verifiedSteps,
|
|
682
|
-
failureCategory: "capacity", capacityConstraint: "gas-envelope", failedTransactionId: transactions[0]?.id,
|
|
683
|
-
reason: "Exact reviewed replay exceeds the held gas envelope" };
|
|
684
|
-
}
|
|
627
|
+
if (replayError !== undefined) {
|
|
628
|
+
await assertLifecycleBlock(client, block, planned.chainId);
|
|
629
|
+
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling failed: ${replayError}` };
|
|
685
630
|
}
|
|
631
|
+
assertCurrent();
|
|
686
632
|
const estimates: LifecycleSimulationStep[] = [];
|
|
687
633
|
// A definitive returned replay failure already refuses execution. Retain its
|
|
688
634
|
// fee observations without inventing a balance or doing redundant payer work.
|
|
@@ -708,6 +654,6 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
708
654
|
estimates.push({ ...step, estimate: { gasUsed: step.gasUsed ?? 0n, gasLimit, gasPrice, executionFee, dataFee, posterGas: poster, posterFee: poster === undefined ? undefined : poster * gasPrice, dataFeeIncludedInGas: limits.protocol === "nitro", totalFee: dataFee === undefined ? undefined : executionFee + dataFee, feeConfidence: dataFee === undefined ? "execution-only" : "execution-and-data" } });
|
|
709
655
|
}
|
|
710
656
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
711
|
-
return { ...base, backend, executionProof: finalFailure === undefined ? "proved" : "failed", protocolFit: finalFailure === undefined ? "proved" : "unknown", admitted: finalFailure === undefined && affordabilityFailure === undefined, steps: estimates, reason:
|
|
657
|
+
return { ...base, backend, executionProof: finalFailure === undefined ? "proved" : "failed", protocolFit: finalFailure === undefined ? "proved" : "unknown", admitted: finalFailure === undefined && affordabilityFailure === undefined, steps: estimates, reason: finalFailure?.error ?? affordabilityFailure, failedTransactionId: finalFailure?.transactionId, failureCategory: finalFailure?.failureCategory ?? (affordabilityFailure === undefined ? undefined : "affordability"), capacityConstraint: finalFailure?.failureCategory === "capacity" ? "compute" : undefined };
|
|
712
658
|
}
|
|
713
659
|
|