@black-market/sdk 0.13.0 → 0.14.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 -4
- package/dist/lifecycle/index.d.ts +3 -1
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +3 -1
- package/dist/lifecycle/index.js.map +1 -1
- package/dist/lifecycle/lens.d.ts +409 -0
- package/dist/lifecycle/lens.d.ts.map +1 -0
- package/dist/lifecycle/lens.js +30 -0
- package/dist/lifecycle/lens.js.map +1 -0
- package/dist/lifecycle/mining.d.ts +47 -0
- package/dist/lifecycle/mining.d.ts.map +1 -0
- package/dist/lifecycle/mining.js +89 -0
- package/dist/lifecycle/mining.js.map +1 -0
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +70 -62
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/presets.d.ts +3 -0
- package/dist/lifecycle/presets.d.ts.map +1 -1
- package/dist/lifecycle/presets.js +1 -0
- package/dist/lifecycle/presets.js.map +1 -1
- package/dist/lifecycle/progress.d.ts +48 -0
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +31 -20
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +6 -2
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +47 -44
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts +5 -3
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +130 -109
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/snapshot.d.ts +8 -0
- package/dist/lifecycle/snapshot.d.ts.map +1 -0
- package/dist/lifecycle/snapshot.js +79 -0
- package/dist/lifecycle/snapshot.js.map +1 -0
- package/dist/lifecycle/types.d.ts +15 -5
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +42 -20
- package/docs/reference.md +39 -6
- package/examples/README.md +2 -2
- package/package.json +1 -1
- package/src/lifecycle/index.ts +5 -1
- package/src/lifecycle/lens.ts +42 -0
- package/src/lifecycle/mining.ts +80 -0
- package/src/lifecycle/planner.ts +72 -58
- package/src/lifecycle/presets.ts +4 -0
- package/src/lifecycle/progress.ts +26 -16
- package/src/lifecycle/rpc.ts +45 -43
- package/src/lifecycle/simulation.ts +126 -100
- package/src/lifecycle/snapshot.ts +72 -0
- package/src/lifecycle/types.ts +15 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decodeErrorResult, decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctionResult, parseAbi, toHex, type Hex } from "viem";
|
|
2
2
|
import { fixedFeePoolHookV1Abi, launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
|
|
3
3
|
import { hashLaunchIdentity, hashLaunchPlan, LifecyclePhase, type LaunchPlanV1, type LaunchProgressV1, type LaunchReceiptV1 } from "./schema.js";
|
|
4
|
-
import {
|
|
4
|
+
import { assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleRpc, lifecycleSessionRead, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readLifecycleChainId, readLifecycleGasPrice, readNitroPosterGas, registerLifecycleFinalCheck, 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
7
|
import { hmmV2ForwardingReserve } from "./presets.js";
|
|
@@ -56,7 +56,6 @@ function executionFailure(returnData: Hex | undefined, error: unknown): Pick<Lif
|
|
|
56
56
|
return { ...diagnostic, failureCategory: "opaque", error: nativeErrorKind === "execution-reverted" ? "Native execution reverted without a reviewed error" : "Execution failed without a reviewed error" };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const nitroProofsByInvocation = new WeakMap<LifecycleRpcClient, Map<string, Promise<void>>>();
|
|
60
59
|
|
|
61
60
|
function conditionRequest(condition: LifecyclePostcondition, planned: PlannedLaunch): { to: Hex; data: Hex } {
|
|
62
61
|
if (condition.kind === "allowance") return {
|
|
@@ -109,8 +108,6 @@ async function proveNitroBackend(client: LifecycleRpcClient, planned: PlannedLau
|
|
|
109
108
|
throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Simulated ArbOS version/compute getters differ from the exact pinned chain context");
|
|
110
109
|
}
|
|
111
110
|
}
|
|
112
|
-
// The enclosing simulation rechecks chain and canonical hash after its proofs
|
|
113
|
-
// (including every refusal path); a separate probe-only recheck is redundant.
|
|
114
111
|
}
|
|
115
112
|
|
|
116
113
|
async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunch, block: LifecycleBlock, transactions: readonly LifecycleTransaction[], gasLimits: readonly bigint[], limits: ResolvedLifecycleLimits, gasPrice: bigint, validation: boolean): Promise<LifecycleSimulationStep[]> {
|
|
@@ -173,10 +170,6 @@ async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunc
|
|
|
173
170
|
}
|
|
174
171
|
return steps;
|
|
175
172
|
}
|
|
176
|
-
/** A decoded semantic refusal or opaque deployment wrapper is never gas evidence. */
|
|
177
|
-
function looksGasExhausted(step: LifecycleSimulationStep, gasLimit: bigint): boolean {
|
|
178
|
-
return !step.success && step.failureCategory === "capacity" && step.gasUsed !== undefined && step.gasUsed >= gasLimit - gasLimit / 64n;
|
|
179
|
-
}
|
|
180
173
|
|
|
181
174
|
function simulationFailureKind(failure: unknown): "abort" | "out-of-gas" | "execution" | "source" {
|
|
182
175
|
for (let depth = 0; depth < 8 && failure !== null && typeof failure === "object"; depth += 1) {
|
|
@@ -194,6 +187,18 @@ function simulationFailureKind(failure: unknown): "abort" | "out-of-gas" | "exec
|
|
|
194
187
|
return "source";
|
|
195
188
|
}
|
|
196
189
|
|
|
190
|
+
function upfrontFundsFailure(failure: unknown): boolean {
|
|
191
|
+
for (let depth = 0; depth < 8 && failure !== null && typeof failure === "object"; depth += 1) {
|
|
192
|
+
const error = failure as { name?: unknown; code?: unknown; status?: unknown; statusCode?: unknown; message?: unknown; details?: unknown; cause?: unknown };
|
|
193
|
+
const status = error.status ?? error.statusCode;
|
|
194
|
+
if (error.name === "AbortError" || typeof status === "number" && status >= 400 && status <= 599 || error.code === 3) return false;
|
|
195
|
+
const message = typeof error.details === "string" ? error.details : typeof error.message === "string" ? error.message : "";
|
|
196
|
+
if (error.code === -38014 || (error.code === undefined || error.code === -32000) && /^insufficient funds\b/i.test(message)) return true;
|
|
197
|
+
failure = error.cause;
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
197
202
|
async function waitForkReceipt(client: LifecycleRpcClient, transactionHash: Hex, timeoutMs: number): Promise<Record<string, unknown>> {
|
|
198
203
|
const deadline = Date.now() + timeoutMs;
|
|
199
204
|
while (Date.now() <= deadline) {
|
|
@@ -302,8 +307,10 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
|
|
|
302
307
|
type SimulationObservations = [LifecycleLimitObservation, unknown, unknown, unknown];
|
|
303
308
|
/** Internal, operation-local fee envelope for the already matched immediate step. */
|
|
304
309
|
export type NextTransactionEnvelope = Readonly<{ gas: bigint; gasPrice: bigint }>;
|
|
310
|
+
/** Compute-only sizing observations; native poster charges are never included. */
|
|
311
|
+
export type GasSizingHints = ReadonlyMap<string, bigint>;
|
|
305
312
|
export type PreparedLaunchSimulation = ((transactions: readonly LifecycleTransaction[], nextEnvelope?: NextTransactionEnvelope) => Promise<LifecycleSimulation>) & {
|
|
306
|
-
/** Unadmitted diagnostic results only; final
|
|
313
|
+
/** Unadmitted diagnostic results only; final measurement validates exact calldata. */
|
|
307
314
|
measure(transactions: readonly LifecycleTransaction[]): Promise<LifecycleSimulation>;
|
|
308
315
|
close(): void;
|
|
309
316
|
};
|
|
@@ -316,7 +323,7 @@ function observedFields<T extends object>(value: T): () => boolean {
|
|
|
316
323
|
|
|
317
324
|
/** Internal continuation owned by one SDK read invocation. Only its positive
|
|
318
325
|
* capability proof is reused; every candidate executes its entire economic sequence. */
|
|
319
|
-
export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true, reviewedEnvelope?: NextTransactionEnvelope, selectedGasPrice?: bigint): PreparedLaunchSimulation {
|
|
326
|
+
export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true, reviewedEnvelope?: NextTransactionEnvelope, selectedGasPrice?: bigint, sizing?: GasSizingHints): PreparedLaunchSimulation {
|
|
320
327
|
const { client, planned, fork } = options;
|
|
321
328
|
const signal = options.signal;
|
|
322
329
|
if (signal?.aborted) throw Object.assign(new Error("Lifecycle simulation cancelled"), { name: "AbortError" });
|
|
@@ -354,7 +361,7 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
|
|
|
354
361
|
return lifecycleStage(client, "simulation.context", () => Promise.all([
|
|
355
362
|
resolveLifecycleLimits(context, policy),
|
|
356
363
|
readLifecycleChainId(client),
|
|
357
|
-
lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block),
|
|
364
|
+
lifecycleSessionRead(client, `${planned.chainId}:account-code:${context.account.toLowerCase()}`, () => lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block)),
|
|
358
365
|
hasTransactions && reviewedEnvelope === undefined ? selectedGasPrice ?? readLifecycleGasPrice(client) : undefined,
|
|
359
366
|
]));
|
|
360
367
|
})().then((observations) => ({ observations }), (failure: unknown) => ({ failure }));
|
|
@@ -394,17 +401,9 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
|
|
|
394
401
|
assertCurrent();
|
|
395
402
|
if (capabilityProved) return;
|
|
396
403
|
if (capabilityPending === undefined) {
|
|
397
|
-
const key =
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const ownedProofs = proofs;
|
|
401
|
-
let nativeProof = ownedProofs?.get(key);
|
|
402
|
-
if (nativeProof === undefined) {
|
|
403
|
-
nativeProof = lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, context.block, limits, gasPrice));
|
|
404
|
-
ownedProofs?.set(key, nativeProof);
|
|
405
|
-
const pending = nativeProof;
|
|
406
|
-
void pending.catch(() => { if (ownedProofs?.get(key) === pending) ownedProofs?.delete(key); });
|
|
407
|
-
}
|
|
404
|
+
const key = `${context.chainId}:nitro-backend`;
|
|
405
|
+
const nativeProof = lifecycleSessionRead(client, key, () =>
|
|
406
|
+
lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, context.block, limits, gasPrice)));
|
|
408
407
|
const proof = nativeProof.then(() => {
|
|
409
408
|
assertContext();
|
|
410
409
|
result.observations[0].assertCurrent();
|
|
@@ -419,7 +418,7 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
|
|
|
419
418
|
await capabilityPending;
|
|
420
419
|
assertCurrent();
|
|
421
420
|
};
|
|
422
|
-
const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, selectedGasPrice);
|
|
421
|
+
const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, selectedGasPrice, sizing);
|
|
423
422
|
assertCurrent();
|
|
424
423
|
registerLifecycleFinalCheck(client, result.observations[0].assertCurrent);
|
|
425
424
|
return simulation;
|
|
@@ -438,11 +437,11 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
|
|
|
438
437
|
return simulate;
|
|
439
438
|
}
|
|
440
439
|
|
|
441
|
-
export async function simulateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], pinnedBlock?: LifecycleBlock, nextEnvelope?: NextTransactionEnvelope): Promise<LifecycleSimulation> {
|
|
440
|
+
export async function simulateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], pinnedBlock?: LifecycleBlock, nextEnvelope?: NextTransactionEnvelope, sizing?: GasSizingHints): Promise<LifecycleSimulation> {
|
|
442
441
|
// Standalone calls always produce fresh context, including live chain and fees.
|
|
443
442
|
if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === lifecycleSourceClient(options.client)) throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
|
|
444
443
|
const block = pinnedBlock ?? await readLifecycleBlock(options.client);
|
|
445
|
-
const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope);
|
|
444
|
+
const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope, undefined, sizing);
|
|
446
445
|
try { return await prepared(transactions, nextEnvelope); }
|
|
447
446
|
finally { prepared.close(); }
|
|
448
447
|
}
|
|
@@ -457,6 +456,7 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
457
456
|
diagnostic: boolean,
|
|
458
457
|
nextEnvelope?: NextTransactionEnvelope,
|
|
459
458
|
selectedGasPrice?: bigint,
|
|
459
|
+
sizing?: GasSizingHints,
|
|
460
460
|
): Promise<LifecycleSimulation> {
|
|
461
461
|
const { client, planned } = options;
|
|
462
462
|
const sourceClient = lifecycleSourceClient(client);
|
|
@@ -466,17 +466,17 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
466
466
|
if (chainId !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
|
|
467
467
|
const accountCode = rpcHex(accountCodeValue, "account code");
|
|
468
468
|
if (accountCode !== "0x") {
|
|
469
|
-
|
|
469
|
+
|
|
470
470
|
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
|
|
471
471
|
}
|
|
472
472
|
for (const transaction of transactions) {
|
|
473
473
|
if (transaction.from.toLowerCase() !== planned.account.toLowerCase() || BigInt(transaction.chainId) !== planned.chainId) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Simulation transaction differs from the committed chain/account");
|
|
474
474
|
if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) {
|
|
475
|
-
|
|
475
|
+
|
|
476
476
|
return { ...base, executionProof: "unavailable", protocolFit: "failed", backend: "unavailable", admitted: false, steps: [], failedTransactionId: transaction.id, failureCategory: "capacity", capacityConstraint: "calldata", reason: "Exact calldata exceeds the supplied chain/account/RPC byte cap" };
|
|
477
477
|
}
|
|
478
478
|
}
|
|
479
|
-
if (transactions.length === 0) {
|
|
479
|
+
if (transactions.length === 0) { return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] }; }
|
|
480
480
|
if (nextEnvelope !== undefined && (transactions.length !== 1 || typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas <= 0n || nextEnvelope.gas >= 1n << 64n ||
|
|
481
481
|
typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
|
|
482
482
|
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Reviewed replay requires one immediate transaction with exact uint64 gas and uint256 price");
|
|
@@ -502,7 +502,7 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
502
502
|
const reason = exceedsCompute
|
|
503
503
|
? `${atomic ? "Atomic launch" : "Immediate transaction"} needs about ${gasInMillions(estimatedGas)}M gas, above the ${gasInMillions(computeCap)}M per-transaction limit${atomic ? "; use staged" : ""}`
|
|
504
504
|
: "The immediate transaction estimate exceeds the RPC aggregate simulation gas cap; no prefix can be proved";
|
|
505
|
-
|
|
505
|
+
|
|
506
506
|
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: exceedsCompute ? "compute" : "simulation-gas", reason };
|
|
507
507
|
}
|
|
508
508
|
}
|
|
@@ -518,40 +518,78 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
518
518
|
try {
|
|
519
519
|
const [, estimates] = await Promise.all([
|
|
520
520
|
proveBackend(limits, gasPrice),
|
|
521
|
-
nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster",
|
|
521
|
+
!diagnostic && nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster", async () => [buffer((await readNitroPosterGas(transactions[0]!, context)).posterGas)]) : [],
|
|
522
522
|
]);
|
|
523
523
|
posterGas.push(...estimates);
|
|
524
524
|
} catch (failure) {
|
|
525
525
|
if (simulationFailureKind(failure) === "abort") throw failure;
|
|
526
526
|
assertCurrent();
|
|
527
|
-
|
|
527
|
+
|
|
528
528
|
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Native Nitro compute/poster proof unavailable: ${lifecycleFailureReason(failure)}. Generic Anvil/Hardhat forks cannot prove ArbOS metering.` };
|
|
529
529
|
}
|
|
530
530
|
}
|
|
531
531
|
assertCurrent();
|
|
532
|
-
const discoveryGas = transactions.map((
|
|
533
|
-
const total = limits.
|
|
532
|
+
const discoveryGas = transactions.map(() => {
|
|
533
|
+
const total = limits.protocol === "nitro" ? limits.maxSimulationGas ?? block.gasLimit : limits.executionGasCeiling;
|
|
534
534
|
const capped = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < total ? limits.transactionGasCeiling : total;
|
|
535
535
|
if (capped <= 0n || capped >= 1n << 64n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Compute/poster envelope exceeds uint64 gas");
|
|
536
536
|
return capped;
|
|
537
537
|
});
|
|
538
|
-
|
|
538
|
+
if (discoveryGas[0]! * gasPrice + transactions[0]!.value >= 1n << 256n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
|
|
539
|
+
const balance = rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance");
|
|
539
540
|
let measured: LifecycleSimulationStep[] = [];
|
|
540
|
-
|
|
541
|
+
const hasSizing = !diagnostic && transactions.every((transaction) => sizing?.has(transaction.id));
|
|
542
|
+
let backend: "eth_simulateV1" | "controlled-fork" = (nextEnvelope !== undefined || hasSizing) && limits.protocol === "evm" && options.fork !== undefined ? "controlled-fork" : "eth_simulateV1";
|
|
543
|
+
if (nextEnvelope === undefined && !hasSizing) {
|
|
544
|
+
// Nitro checks upfront funds even with validation:false. Bound discovery
|
|
545
|
+
// budgets before dispatch, without overriding the payer's real balance.
|
|
546
|
+
const value = transactions.reduce((total, transaction) => total + transaction.value, 0n);
|
|
547
|
+
let available = balance - value;
|
|
548
|
+
if (gasPrice !== 0n) available /= gasPrice;
|
|
549
|
+
const sizingGas = discoveryGas.map((ceiling, index) => {
|
|
550
|
+
// Upfront reservation is per call; unused gas is refunded before the next.
|
|
551
|
+
const computeAndPoster = limits.executionGasCeiling + (posterGas[index] ?? 0n);
|
|
552
|
+
const capped = computeAndPoster < ceiling ? computeAndPoster : ceiling;
|
|
553
|
+
return gasPrice === 0n || capped < available ? capped : available;
|
|
554
|
+
});
|
|
555
|
+
let retriedForFunds = false;
|
|
556
|
+
if (balance < value || sizingGas.some((gas) => gas <= 0n)) return { ...base, executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "affordability", reason: "Creator native balance cannot fund transaction value and sizing fee budgets" };
|
|
557
|
+
assertCurrent();
|
|
541
558
|
try {
|
|
542
|
-
|
|
559
|
+
try {
|
|
560
|
+
measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, sizingGas, limits, gasPrice, false));
|
|
561
|
+
} catch (failure) {
|
|
562
|
+
if (!upfrontFundsFailure(failure)) throw failure;
|
|
563
|
+
assertCurrent();
|
|
564
|
+
retriedForFunds = true;
|
|
565
|
+
let remaining = available;
|
|
566
|
+
const sharedGas = sizingGas.map((ceiling, index) => {
|
|
567
|
+
if (gasPrice === 0n) return ceiling;
|
|
568
|
+
const share = remaining / BigInt(transactions.length - index);
|
|
569
|
+
const gas = ceiling < share ? ceiling : share;
|
|
570
|
+
remaining -= gas;
|
|
571
|
+
return gas;
|
|
572
|
+
});
|
|
573
|
+
measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, sharedGas, limits, gasPrice, false));
|
|
574
|
+
}
|
|
543
575
|
} catch (failure) {
|
|
544
|
-
if (
|
|
576
|
+
if (upfrontFundsFailure(failure)) return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "affordability", reason: "Native balance cannot cover the sequential sizing fee envelopes" };
|
|
577
|
+
const kind = simulationFailureKind(failure);
|
|
578
|
+
if (kind === "abort") throw failure;
|
|
545
579
|
assertCurrent();
|
|
546
|
-
if (options.fork === undefined || limits.protocol === "nitro") {
|
|
547
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
580
|
+
if (options.fork === undefined || limits.protocol === "nitro" || kind !== "source") {
|
|
548
581
|
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."}` };
|
|
549
582
|
}
|
|
550
583
|
backend = "controlled-fork";
|
|
551
|
-
measured = await simulateForkPass(options.fork, planned, block, transactions,
|
|
584
|
+
measured = await simulateForkPass(options.fork, planned, block, transactions, sizingGas, gasPrice, true, limits.headroomBps);
|
|
552
585
|
}
|
|
553
586
|
assertCurrent();
|
|
554
587
|
const failedIndex = measured.findIndex((step) => !step.success);
|
|
588
|
+
const refusal = measured[failedIndex];
|
|
589
|
+
if (retriedForFunds && refusal?.failureCategory === "capacity" && refusal.gasLimit !== undefined && refusal.gasLimit < limits.executionGasCeiling) {
|
|
590
|
+
return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: measured,
|
|
591
|
+
failedTransactionId: refusal.transactionId, failureCategory: "affordability", reason: "Native balance does not provide the sequential sizing margin; an exact transaction envelope was not proved" };
|
|
592
|
+
}
|
|
555
593
|
if (failedIndex >= 0 && limits.maxSimulationGas !== undefined &&
|
|
556
594
|
measured.reduce((gas, step) => gas + (step.gasUsed ?? 0n), 0n) * 100n >= limits.maxSimulationGas * 95n) {
|
|
557
595
|
// The provider exhausted its aggregate used-gas budget, not the contract.
|
|
@@ -560,90 +598,78 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
560
598
|
transactions = transactions.slice(0, failedIndex);
|
|
561
599
|
base.deferredTransactionIds = allTransactions.slice(failedIndex).map((transaction) => transaction.id);
|
|
562
600
|
if (transactions.length === 0) {
|
|
563
|
-
|
|
601
|
+
|
|
564
602
|
return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "source", reason: "RPC aggregate simulation gas cap left no successful prefix to prove" };
|
|
565
603
|
}
|
|
566
604
|
}
|
|
567
605
|
const failed = measured.find((step) => !step.success);
|
|
568
|
-
if (failed !== undefined)
|
|
606
|
+
if (diagnostic && failed !== undefined) return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId, failureCategory: failed.failureCategory ?? "opaque", capacityConstraint: failed.failureCategory === "capacity" ? "compute" : undefined };
|
|
569
607
|
if (diagnostic) {
|
|
570
|
-
|
|
608
|
+
|
|
571
609
|
return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
|
|
572
610
|
}
|
|
573
611
|
}
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
612
|
+
const sizedTransactions = measured.length === 0 ? transactions : transactions.slice(0, measured.length);
|
|
613
|
+
const minimumGas = sizedTransactions.map((transaction, index) => {
|
|
614
|
+
return nextEnvelope !== undefined ? 0n : hasSizing ? sizing!.get(transaction.id)! : measured[index]!.gasRequired ?? measured[index]!.gasUsed!;
|
|
615
|
+
});
|
|
616
|
+
const gasLimits = nextEnvelope === undefined ? sizedTransactions.map((transaction, index) => {
|
|
617
|
+
// Suffix budgets prove feasibility, not a future transaction's fee estimate.
|
|
618
|
+
if (index > 0) return discoveryGas[index]!;
|
|
619
|
+
const gross = minimumGas[index]!;
|
|
620
|
+
const reserve = transaction.kind === "activate" || transaction.kind === "atomic" ? hmmV2ForwardingReserve(planned.plan) : 0n;
|
|
621
|
+
const buffered = gross + reserve > buffer(gross) ? gross + reserve : buffer(gross);
|
|
622
|
+
const desired = buffered + (posterGas[0] ?? 0n);
|
|
623
|
+
return desired < discoveryGas[index]! ? desired : discoveryGas[index]!;
|
|
580
624
|
}) : [nextEnvelope.gas];
|
|
625
|
+
if (gasLimits[0]! * gasPrice + transactions[0]!.value >= 1n << 256n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
|
|
626
|
+
const validate = nextEnvelope !== undefined || measured.every((step) => step.success);
|
|
627
|
+
if (nextEnvelope === undefined) {
|
|
628
|
+
const value = transactions.reduce((total, transaction) => total + transaction.value, 0n);
|
|
629
|
+
let available = balance - value;
|
|
630
|
+
let suffixMinimum = minimumGas.slice(1).reduce((total, gas) => total + gas, 0n);
|
|
631
|
+
if (gasPrice !== 0n) available /= gasPrice;
|
|
632
|
+
for (let index = 0; index < gasLimits.length && gasPrice !== 0n; index += 1) {
|
|
633
|
+
const minimum = minimumGas[index]!;
|
|
634
|
+
if (index > 0) suffixMinimum -= minimum;
|
|
635
|
+
const budget = index === 0 ? available - suffixMinimum : minimum + (available - minimum - suffixMinimum) / BigInt(gasLimits.length - index);
|
|
636
|
+
if (gasLimits[index]! > budget) gasLimits[index] = budget;
|
|
637
|
+
available -= gasLimits[index]!;
|
|
638
|
+
}
|
|
639
|
+
if (balance < value || gasLimits.some((gas, index) => gas <= 0n && minimumGas[index]! > 0n)) return { ...base, executionProof: "unavailable", backend, admitted: false, steps: measured, failureCategory: "affordability", reason: "Creator native balance cannot fund transaction value and execution fee envelopes" };
|
|
640
|
+
}
|
|
581
641
|
const exceeds = gasLimits.findIndex((gas, index) =>
|
|
582
642
|
gas <= 0n || gas >= 1n << 64n || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling ||
|
|
583
|
-
nextEnvelope === undefined && (measured[index]!.gasRequired ?? measured[index]!.gasUsed!) > gas
|
|
643
|
+
nextEnvelope === undefined && !hasSizing && (measured[index]!.gasRequired ?? measured[index]!.gasUsed!) > gas ||
|
|
584
644
|
limits.protocol === "evm" && gas > limits.executionGasCeiling);
|
|
585
645
|
if (exceeds >= 0) {
|
|
586
|
-
|
|
646
|
+
|
|
587
647
|
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 };
|
|
588
648
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
let
|
|
592
|
-
|
|
593
|
-
? simulateRpcPass(client, planned, block, transactions, gas, limits, gasPrice, true)
|
|
594
|
-
: simulateForkPass(options.fork!, planned, block, transactions, gas, gasPrice, false, limits.headroomBps);
|
|
595
|
-
try {
|
|
596
|
-
verifiedSteps = await lifecycleStage(client, "simulation.replay", () => replay(gasLimits));
|
|
597
|
-
} catch (failure) {
|
|
598
|
-
const kind = simulationFailureKind(failure);
|
|
599
|
-
if (kind === "abort") throw failure;
|
|
600
|
-
assertCurrent();
|
|
601
|
-
if (kind !== "out-of-gas" || backend !== "eth_simulateV1" || nextEnvelope !== undefined) {
|
|
602
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
603
|
-
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)}` };
|
|
604
|
-
}
|
|
605
|
-
replayError = lifecycleFailureReason(failure);
|
|
606
|
-
}
|
|
607
|
-
assertCurrent();
|
|
608
|
-
let finalFailure = verifiedSteps.find((step) => !step.success);
|
|
609
|
-
const failedIndex = verifiedSteps.findIndex((step) => !step.success);
|
|
610
|
-
const rescue = nextEnvelope === undefined && backend === "eth_simulateV1" &&
|
|
611
|
-
(replayError !== undefined && discoveryGas.some((gas, index) => index < transactions.length && gas > gasLimits[index]!) ||
|
|
612
|
-
finalFailure !== undefined && looksGasExhausted(finalFailure, gasLimits[failedIndex]!) && discoveryGas[failedIndex]! > gasLimits[failedIndex]!);
|
|
613
|
-
// One legal-envelope rescue covers thrown/returned genuine OOG. Never retry
|
|
614
|
-
// semantic errors, switch replay backends, or enlarge a held immediate envelope.
|
|
615
|
-
if (rescue) {
|
|
616
|
-
assertCurrent();
|
|
649
|
+
// Size first, then prove and return this exact executable envelope. Diagnostic
|
|
650
|
+
// gas budgets and caller-held proposals are never execution admission.
|
|
651
|
+
let verifiedSteps = measured;
|
|
652
|
+
if (validate) {
|
|
617
653
|
try {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
replayError = undefined;
|
|
654
|
+
verifiedSteps = await lifecycleStage(client, "simulation.measure", () => backend === "eth_simulateV1"
|
|
655
|
+
? simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true)
|
|
656
|
+
: simulateForkPass(options.fork!, planned, block, transactions, gasLimits, gasPrice, false, limits.headroomBps));
|
|
622
657
|
} catch (failure) {
|
|
623
|
-
|
|
624
|
-
if (kind === "abort") throw failure;
|
|
658
|
+
if (simulationFailureKind(failure) === "abort") throw failure;
|
|
625
659
|
assertCurrent();
|
|
626
|
-
|
|
627
|
-
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)}` };
|
|
628
|
-
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${lifecycleFailureReason(failure)}` };
|
|
660
|
+
return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [], reason: `Exact transaction simulation unavailable: ${lifecycleFailureReason(failure)}` };
|
|
629
661
|
}
|
|
630
662
|
}
|
|
631
|
-
if (replayError !== undefined) {
|
|
632
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
633
|
-
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling failed: ${replayError}` };
|
|
634
|
-
}
|
|
635
663
|
assertCurrent();
|
|
664
|
+
const finalFailure = verifiedSteps.find((step) => !step.success);
|
|
636
665
|
const estimates: LifecycleSimulationStep[] = [];
|
|
637
|
-
|
|
638
|
-
// fee observations without inventing a balance or doing redundant payer work.
|
|
639
|
-
let remainingBalance = finalFailure === undefined
|
|
640
|
-
? rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance")
|
|
641
|
-
: undefined;
|
|
666
|
+
let remainingBalance = finalFailure === undefined ? balance : undefined;
|
|
642
667
|
let affordabilityFailure: string | undefined;
|
|
643
668
|
const feeContext = configuration.estimateDataFee === undefined || sourceClient === client ? context : { ...context, client: sourceClient };
|
|
644
669
|
for (let index = 0; index < verifiedSteps.length; index += 1) {
|
|
645
|
-
const step = verifiedSteps[index]; const transaction = transactions[index]; const gasLimit =
|
|
670
|
+
const step = verifiedSteps[index]; const transaction = transactions[index]; const gasLimit = gasLimits[index];
|
|
646
671
|
if (step === undefined || transaction === undefined || gasLimit === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing verified fee context");
|
|
672
|
+
if (index > 0) { estimates.push({ ...step, computeGasRequired: minimumGas[index], provisional: true }); continue; }
|
|
647
673
|
const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit, gasPrice }, feeContext);
|
|
648
674
|
if (dataFee !== undefined && (typeof dataFee !== "bigint" || dataFee < 0n || dataFee >= 1n << 256n)) throw new LifecyclePlanningError("INVALID_DATA_FEE", "Data-fee oracle must return a nonnegative uint256 bigint amount");
|
|
649
675
|
// Nitro gas includes poster charges. Never add the quoted poster fee again.
|
|
@@ -655,9 +681,9 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
655
681
|
remainingBalance -= required;
|
|
656
682
|
}
|
|
657
683
|
const poster = posterGas[index];
|
|
658
|
-
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" } });
|
|
684
|
+
estimates.push({ ...step, computeGasRequired: nextEnvelope === undefined ? minimumGas[index] : undefined, 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" } });
|
|
659
685
|
}
|
|
660
|
-
|
|
686
|
+
|
|
661
687
|
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 };
|
|
662
688
|
}
|
|
663
689
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { decodeFunctionResult, encodeFunctionData, encodeFunctionResult, multicall3Abi, toHex, zeroAddress, type Abi, type Address } from "viem";
|
|
2
|
+
import { robinhoodMainnet } from "../addresses.js";
|
|
3
|
+
import { launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
|
|
4
|
+
import { getKnownLifecycleDeployment, type LifecycleDeploymentPreset } from "./presets.js";
|
|
5
|
+
import { lifecycleTokenPredictionAbi } from "./progress.js";
|
|
6
|
+
import { launchLensAbi, observeLifecycleLens, type LifecycleLensSnapshot } from "./lens.js";
|
|
7
|
+
import { hashLaunchIdentity, LifecycleFundingKind, type LaunchPlanV1 } from "./schema.js";
|
|
8
|
+
import { attestLifecycleMulticall, lifecycleRpc, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readLifecycleChainId, rpcHex, seedLifecycleRead } from "./rpc.js";
|
|
9
|
+
import { LifecyclePlanningError, type LifecycleBlock, type LifecycleRpcClient } from "./types.js";
|
|
10
|
+
import { UFG_LAUNCH_PROXY_ADDRESS } from "./ufg-config.js";
|
|
11
|
+
|
|
12
|
+
type SnapshotRead = { to: Address; abi: Abi; functionName: string; args: readonly unknown[]; from?: Address };
|
|
13
|
+
|
|
14
|
+
/** Nitro's EVM BLOCKNUMBER/BLOCKHASH expose L1 values. One RPC header owns the L2
|
|
15
|
+
* pin; every planning getter is aggregated at that explicit L2 block number. */
|
|
16
|
+
export async function readLifecyclePlanningBlock(client: LifecycleRpcClient, plan: LaunchPlanV1, account: Address, ufg = false, mine = false,
|
|
17
|
+
preset: LifecycleDeploymentPreset | undefined = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator })): Promise<LifecycleBlock> {
|
|
18
|
+
const [block, chainId] = await Promise.all([readLifecycleBlock(client), readLifecycleChainId(client)]);
|
|
19
|
+
if (chainId !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
|
|
20
|
+
if (chainId !== 4663n) return block;
|
|
21
|
+
const to = robinhoodMainnet.contracts.multicall3.address;
|
|
22
|
+
const reads: SnapshotRead[] = [];
|
|
23
|
+
const add = (target: Address, abi: Abi, functionName: string, args: readonly unknown[], from?: Address) => reads.push({ to: target, abi, functionName, args, from });
|
|
24
|
+
const escrow = ufg ? UFG_LAUNCH_PROXY_ADDRESS : preset?.fundingEscrow;
|
|
25
|
+
const inputs = [...new Set(plan.funding.filter((funding) => funding.kind !== LifecycleFundingKind.NativeWrap && !(funding.kind === LifecycleFundingKind.Swap && funding.inputAsset === zeroAddress)).map((funding) => funding.inputAsset.toLowerCase() as Address))];
|
|
26
|
+
const hasLens = preset?.launchLens !== undefined;
|
|
27
|
+
if (hasLens) {
|
|
28
|
+
add(preset.launchLens!, launchLensAbi, "snapshot", [plan, account, escrow!, inputs, mine ? 150_000n : 0n]);
|
|
29
|
+
} else {
|
|
30
|
+
add(to, multicall3Abi, "getEthBalance", [account]);
|
|
31
|
+
if (preset === undefined) add(plan.orchestrator, launchLifecycleAbi, "predictToken", [plan]);
|
|
32
|
+
else add(preset.tokenFactory, lifecycleTokenPredictionAbi, "predictToken", [hashLaunchIdentity(plan), plan.token]);
|
|
33
|
+
add(plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [hashLaunchIdentity(plan)], account);
|
|
34
|
+
for (const asset of inputs) {
|
|
35
|
+
add(asset, lifecycleErc20Abi, "balanceOf", [account]);
|
|
36
|
+
if (escrow !== undefined) add(asset, lifecycleErc20Abi, "allowance", [account, escrow]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
add(nitroArbSys, nitroArbSysAbi, "arbOSVersion", []);
|
|
40
|
+
add(nitroGasInfo, nitroGasInfoAbi, "getMaxTxGasLimit", []);
|
|
41
|
+
add(nitroGasInfo, nitroGasInfoAbi, "getMaxBlockGasLimit", []);
|
|
42
|
+
await attestLifecycleMulticall(client, block);
|
|
43
|
+
const calls = reads.map((read) => ({ target: read.to, allowFailure: true, callData: encodeFunctionData({ abi: read.abi, functionName: read.functionName, args: read.args }) }));
|
|
44
|
+
const data = encodeFunctionData({ abi: multicall3Abi, functionName: "aggregate3", args: [calls] });
|
|
45
|
+
const raw = rpcHex(await lifecycleRpc(client, "eth_call", [{ to, data }, toHex(block.number)]), "planning snapshot");
|
|
46
|
+
const results = decodeFunctionResult({ abi: multicall3Abi, functionName: "aggregate3", data: raw });
|
|
47
|
+
if (results.length !== reads.length || !results[0]!.success) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Multicall3 omitted the planning balance snapshot");
|
|
48
|
+
const seed = (target: Address, abi: Abi, functionName: string, args: readonly unknown[], result: unknown, from?: Address) => {
|
|
49
|
+
seedLifecycleRead(client, "eth_call", [{ to: target, from, data: encodeFunctionData({ abi, functionName, args }) }, toHex(block.number)], block, encodeFunctionResult({ abi, functionName, result }));
|
|
50
|
+
};
|
|
51
|
+
if (hasLens) {
|
|
52
|
+
const snapshot = decodeFunctionResult({ abi: launchLensAbi, functionName: "snapshot", data: results[0]!.returnData }) as LifecycleLensSnapshot;
|
|
53
|
+
if (snapshot.chainId !== chainId || snapshot.blockNumber !== block.number) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "LaunchLens returned a different L2 planning domain");
|
|
54
|
+
observeLifecycleLens(client, block, snapshot);
|
|
55
|
+
seedLifecycleRead(client, "eth_getBalance", [account, toHex(block.number)], block, toHex(snapshot.nativeBalance));
|
|
56
|
+
seed(preset!.tokenFactory, lifecycleTokenPredictionAbi, "predictToken", [hashLaunchIdentity(plan), plan.token], snapshot.predictedToken);
|
|
57
|
+
seed(plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [hashLaunchIdentity(plan)], snapshot.progress, account);
|
|
58
|
+
for (const balance of snapshot.tokenBalances) {
|
|
59
|
+
seed(balance.token, lifecycleErc20Abi, "balanceOf", [account], balance.balance);
|
|
60
|
+
seed(balance.token, lifecycleErc20Abi, "allowance", [account, escrow!], balance.allowance);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
const balance = decodeFunctionResult({ abi: multicall3Abi, functionName: "getEthBalance", data: results[0]!.returnData });
|
|
64
|
+
seedLifecycleRead(client, "eth_getBalance", [account, toHex(block.number)], block, toHex(balance));
|
|
65
|
+
}
|
|
66
|
+
for (let index = 1; index < reads.length; index += 1) {
|
|
67
|
+
if (!results[index]!.success) continue;
|
|
68
|
+
const read = reads[index]!;
|
|
69
|
+
seedLifecycleRead(client, "eth_call", [{ to: read.to, from: read.from, data: calls[index]!.callData }, toHex(block.number)], block, results[index]!.returnData);
|
|
70
|
+
}
|
|
71
|
+
return block;
|
|
72
|
+
}
|
package/src/lifecycle/types.ts
CHANGED
|
@@ -5,11 +5,13 @@ import type {
|
|
|
5
5
|
MarketLiveStateV1, PreparedMarketV1, ProfileRegistrationV1, ProfileTopologyV1, LaunchEnvelopeV2, LifecycleDeveloperTerms,
|
|
6
6
|
} from "./schema.js";
|
|
7
7
|
|
|
8
|
-
/** Local opt-in timing only: never contains request arguments, addresses, results or errors.
|
|
8
|
+
/** Local opt-in timing only: never contains request arguments, addresses, results or errors.
|
|
9
|
+
* Since 0.14.0, removed replay/envelope stages are represented by simulation.measure,
|
|
10
|
+
* which includes the final validated execution at the returned gas envelope. */
|
|
9
11
|
export type LifecycleDiagnosticStage =
|
|
10
12
|
| "plan" | "plan.context" | "plan.domain" | "plan.progress" | "plan.inputs" | "rpc"
|
|
11
13
|
| "profile.certification" | "simulation.context" | "simulation.nitro"
|
|
12
|
-
| "simulation.poster" | "simulation.measure" | "
|
|
14
|
+
| "simulation.poster" | "simulation.measure" | "session-cache";
|
|
13
15
|
export type LifecycleDiagnosticEvent = {
|
|
14
16
|
stage: LifecycleDiagnosticStage;
|
|
15
17
|
phase: "queued" | "start" | "success" | "failure" | "reuse";
|
|
@@ -35,7 +37,7 @@ export type LifecycleRpcClient = {
|
|
|
35
37
|
sessionCache?: LifecycleSessionCache;
|
|
36
38
|
};
|
|
37
39
|
export type LifecycleBlock = { number: bigint; hash: Hash; timestamp: bigint; gasLimit: bigint; baseFeePerGas?: bigint };
|
|
38
|
-
/** A session retains
|
|
40
|
+
/** A session retains pinned static reads and deployment/backend capability observations. */
|
|
39
41
|
export type LifecycleSessionCache = {
|
|
40
42
|
readonly block: Readonly<Pick<LifecycleBlock, "number" | "hash">> | undefined;
|
|
41
43
|
/** Call on wallet/chain changes and after every confirmed transaction. */
|
|
@@ -79,7 +81,8 @@ export type LifecycleTransaction = {
|
|
|
79
81
|
marketStart?: number; marketCount?: number; gas?: bigint; gasPrice?: bigint; nonce?: bigint;
|
|
80
82
|
/** Exact encoded byte count; populated on SDK-built commands, including approvals. */
|
|
81
83
|
calldataBytes?: number;
|
|
82
|
-
/**
|
|
84
|
+
/** Only the immediate command receives exact gas/price/estimate/admission.
|
|
85
|
+
* Future commands must be re-sized and proved by buildNextTransaction. */
|
|
83
86
|
admission?: LifecycleAdmission;
|
|
84
87
|
estimate?: LifecycleTransactionEstimate;
|
|
85
88
|
};
|
|
@@ -104,7 +107,12 @@ export type LifecycleFailureCategory = "semantic" | "capacity" | "opaque" | "sou
|
|
|
104
107
|
export type LifecycleCapacityConstraint = "compute" | "gas-envelope" | "calldata" | "simulation-gas";
|
|
105
108
|
export type LifecycleSimulationStep = {
|
|
106
109
|
transactionId: string; success: boolean; gasUsed?: bigint; gasLimit?: bigint; gasRequired?: bigint;
|
|
110
|
+
/** Compute-only sizing observation, excluding Nitro poster gas; not admission. */
|
|
111
|
+
computeGasRequired?: bigint;
|
|
107
112
|
returnData?: Hex; error?: string; estimate?: LifecycleTransactionEstimate;
|
|
113
|
+
/** Suffix feasibility budget only, not a transaction estimate or execution admission.
|
|
114
|
+
* buildNextTransaction obtains a fresh poster quote and exact envelope at its turn. */
|
|
115
|
+
provisional?: boolean;
|
|
108
116
|
failureCategory?: LifecycleFailureCategory; decodedError?: string;
|
|
109
117
|
/** Safe failed-call facts only; native messages and error-data bytes are never copied here. */
|
|
110
118
|
nativeErrorCode?: number;
|
|
@@ -115,13 +123,15 @@ export type LifecycleSimulationStep = {
|
|
|
115
123
|
};
|
|
116
124
|
export type LifecycleSimulation = LifecycleProofOutcomes & {
|
|
117
125
|
backend: "eth_simulateV1" | "controlled-fork" | "unavailable";
|
|
118
|
-
/**
|
|
126
|
+
/** Full successful prefix feasibility; only its immediate envelope is final. */
|
|
119
127
|
confidence: "stateful" | "provisional"; admitted: boolean;
|
|
120
128
|
blockNumber: bigint; blockHash: Hash; account: Address; chainId: bigint;
|
|
121
129
|
steps: readonly LifecycleSimulationStep[]; limits: ResolvedLifecycleLimits;
|
|
122
130
|
/** Unproved suffix. These transactions receive no gas, price, estimate or admission. */
|
|
123
131
|
deferredTransactionIds: readonly string[];
|
|
124
132
|
reason?: string; failedTransactionId?: string;
|
|
133
|
+
/** Affordability can mean insufficient sizing margin, even if smaller exact
|
|
134
|
+
* envelopes could fit. A refused RPC exposes no prefix results to refine them. */
|
|
125
135
|
failureCategory?: LifecycleFailureCategory; capacityConstraint?: LifecycleCapacityConstraint;
|
|
126
136
|
};
|
|
127
137
|
export type LifecycleFundingPrerequisite = {
|