@black-market/sdk 0.10.0 → 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 +4 -2
- 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/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 +5 -0
- package/dist/lifecycle/presets.d.ts.map +1 -1
- package/dist/lifecycle/presets.js +15 -1
- 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 +77 -32
- package/examples/.env.example +4 -0
- package/examples/README.md +21 -2
- package/examples/launch-example-support.ts +1 -0
- package/examples/launch-example.ts +1 -0
- package/examples/launch.ts +1 -0
- package/package.json +1 -1
- package/src/lifecycle/calibration.ts +1 -0
- package/src/lifecycle/planner.ts +41 -32
- package/src/lifecycle/presets.ts +16 -2
- 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
|
@@ -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
|
|
|
@@ -1,8 +1,45 @@
|
|
|
1
1
|
import { encodeFunctionData } from "viem";
|
|
2
2
|
import { launchLifecycleAbi } from "./abi.js";
|
|
3
|
-
import { LifecycleFundingKind, LifecycleMode } from "./schema.js";
|
|
3
|
+
import { LifecycleFundingKind, LifecycleMode, type LaunchPlanV1 } from "./schema.js";
|
|
4
4
|
import { LifecyclePlanningError, type BuildLaunchTransactionsOptions, type LifecycleTransaction } from "./types.js";
|
|
5
5
|
import { assertLifecycleTokenAllocation } from "./allocations.js";
|
|
6
|
+
import { HMM_V2_PREPARE_GAS, hmmV2ForwardingReserve } from "./presets.js";
|
|
7
|
+
import { isHmmProfile } from "./hmm.js";
|
|
8
|
+
|
|
9
|
+
const OTHER_MARKET_PREPARE_GAS = 16_000_000n;
|
|
10
|
+
const BEGIN_GAS = 9_500_000n;
|
|
11
|
+
const ACTIVATE_MARKET_GAS = 6_000_000n;
|
|
12
|
+
|
|
13
|
+
export function preparationGasEstimate(plan: LaunchPlanV1, first: number, count: number): bigint {
|
|
14
|
+
let gas = 0n;
|
|
15
|
+
for (let index = first; index < first + count; index += 1) gas += isHmmProfile(plan.markets[index]!.profileId) ? HMM_V2_PREPARE_GAS : OTHER_MARKET_PREPARE_GAS;
|
|
16
|
+
return gas;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Contiguous cost packing preserves committed market order and recovery progress. */
|
|
20
|
+
export function packPreparationBatches(plan: LaunchPlanV1, prepared: number, computeCap: bigint): number[] {
|
|
21
|
+
const batches = prepared > 0 ? [prepared] : [];
|
|
22
|
+
let count = 0, gas = 0n;
|
|
23
|
+
for (let index = prepared; index < plan.markets.length; index += 1) {
|
|
24
|
+
const estimate = preparationGasEstimate(plan, index, 1);
|
|
25
|
+
if (count > 0 && gas + estimate > computeCap) { batches.push(count); count = 0; gas = 0n; }
|
|
26
|
+
count += 1;
|
|
27
|
+
gas += estimate;
|
|
28
|
+
}
|
|
29
|
+
if (count > 0) batches.push(count);
|
|
30
|
+
return batches;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Estimates select RPC proof coverage only; measurement and exact replay size gas. */
|
|
34
|
+
export function transactionGasEstimate(plan: LaunchPlanV1, transaction: LifecycleTransaction): bigint {
|
|
35
|
+
switch (transaction.kind) {
|
|
36
|
+
case "begin": return BEGIN_GAS;
|
|
37
|
+
case "prepare": return preparationGasEstimate(plan, transaction.marketStart!, transaction.marketCount!);
|
|
38
|
+
case "activate": return ACTIVATE_MARKET_GAS * BigInt(plan.markets.length) + hmmV2ForwardingReserve(plan);
|
|
39
|
+
case "atomic": return BEGIN_GAS + preparationGasEstimate(plan, 0, plan.markets.length) + ACTIVATE_MARKET_GAS * BigInt(plan.markets.length) + hmmV2ForwardingReserve(plan);
|
|
40
|
+
default: return 100_000n;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
6
43
|
|
|
7
44
|
/** Pure unsigned launch commands. Every command carries the complete commitment;
|
|
8
45
|
* preparation splits only empty markets, never positions, buys or public opening.
|
package/src/lifecycle/types.ts
CHANGED
|
@@ -44,7 +44,9 @@ export type LifecycleLimitContext = { client: LifecycleRpcClient; account: Addre
|
|
|
44
44
|
export type LifecycleLimits = {
|
|
45
45
|
/** Optional tightening caps on the complete transaction gas envelope, including Nitro poster gas. */
|
|
46
46
|
headroomBps?: number; chainGasLimit?: bigint; rpcGasLimit?: bigint; accountGasLimit?: bigint;
|
|
47
|
-
maxCalldataBytes?: number;
|
|
47
|
+
maxCalldataBytes?: number;
|
|
48
|
+
/** Aggregate gas-used budget for one simulation request; defaults to 50M on chain 4663. */
|
|
49
|
+
maxSimulationGas?: bigint;
|
|
48
50
|
chainId?: bigint; orchestrator?: Address; account?: Address; observedBlockNumber?: bigint; observedBlockHash?: Hash;
|
|
49
51
|
/** External data fee not already charged in gas, called with exact calldata and the pinned block.
|
|
50
52
|
* Nitro poster fees are included in gas and use the native NodeInterface instead. */
|
|
@@ -112,9 +114,12 @@ export type LifecycleSimulationStep = {
|
|
|
112
114
|
};
|
|
113
115
|
export type LifecycleSimulation = LifecycleProofOutcomes & {
|
|
114
116
|
backend: "eth_simulateV1" | "controlled-fork" | "unavailable";
|
|
117
|
+
/** Admission/proved outcomes cover the successful prefix, not the deferred suffix. */
|
|
115
118
|
confidence: "stateful" | "provisional"; admitted: boolean;
|
|
116
119
|
blockNumber: bigint; blockHash: Hash; account: Address; chainId: bigint;
|
|
117
120
|
steps: readonly LifecycleSimulationStep[]; limits: ResolvedLifecycleLimits;
|
|
121
|
+
/** Unproved suffix. These transactions receive no gas, price, estimate or admission. */
|
|
122
|
+
deferredTransactionIds: readonly string[];
|
|
118
123
|
reason?: string; failedTransactionId?: string;
|
|
119
124
|
failureCategory?: LifecycleFailureCategory; capacityConstraint?: LifecycleCapacityConstraint;
|
|
120
125
|
};
|
|
@@ -163,7 +168,7 @@ export type PlannedLaunch = {
|
|
|
163
168
|
/** Exact CREATE2 metadata captured from the final committed bound markets. */
|
|
164
169
|
hookDeployments: readonly LifecyclePoolBoundHookDeployment[];
|
|
165
170
|
progress: CanonicalLaunchProgress;
|
|
166
|
-
|
|
171
|
+
limits?: LifecycleLimitSource;
|
|
167
172
|
/** Unstarted-launch admission choices only; never part of the committed plan or calldata. */
|
|
168
173
|
allowTopologyGaps?: boolean; allowMissingPermanentTail?: boolean;
|
|
169
174
|
};
|
|
@@ -204,8 +209,8 @@ export type BuildNextTransactionOptions = ReadLaunchProgressOptions & {
|
|
|
204
209
|
planned: PlannedLaunch; action?: "continue" | "cancel"; limits?: LifecycleLimitSource; fork?: ControlledLifecycleFork;
|
|
205
210
|
/** Optional active submission RPC. Only the immediate next exact transaction is estimated, read-only. */
|
|
206
211
|
submissionClient?: LifecycleRpcClient;
|
|
207
|
-
/** Untrusted reviewed next envelope.
|
|
208
|
-
*
|
|
212
|
+
/** Untrusted reviewed next envelope. Canonical identity and direct exact replay
|
|
213
|
+
* prove only this immediate transaction at its held gas and gasPrice. */
|
|
209
214
|
reviewedTransaction?: LifecycleTransaction;
|
|
210
215
|
};
|
|
211
216
|
export class LifecyclePlanningError extends Error {
|