@black-market/sdk 0.12.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 +96 -96
- 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 +121 -78
- 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 +51 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +149 -73
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +14 -4
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +171 -12
- 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 +137 -115
- 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 +27 -5
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +627 -1495
- package/docs/reference.md +365 -0
- package/examples/.env.example +3 -2
- package/examples/README.md +171 -552
- package/examples/launch-example-support.ts +11 -3
- package/examples/launch-example.ts +1 -1
- package/examples/launch-lifecycle-smoke.ts +1 -1
- 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 +122 -73
- package/src/lifecycle/presets.ts +4 -0
- package/src/lifecycle/progress.ts +132 -59
- package/src/lifecycle/rpc.ts +163 -13
- package/src/lifecycle/simulation.ts +134 -106
- package/src/lifecycle/snapshot.ts +72 -0
- package/src/lifecycle/types.ts +24 -6
|
@@ -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[]> {
|
|
@@ -121,7 +118,7 @@ async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunc
|
|
|
121
118
|
// parent gasLimit are set.
|
|
122
119
|
const blockStateCalls = transactions.map((transaction, index) => ({
|
|
123
120
|
blockOverrides: { number: toHex(block.number + BigInt(index + 1)), time: toHex(block.timestamp + BigInt(index + 1)), gasLimit: toHex(block.gasLimit) },
|
|
124
|
-
calls: [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gasLimits[index] ?? limits.executionGasCeiling), gasPrice: toHex(gasPrice) }],
|
|
121
|
+
calls: [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gasLimits[index] ?? limits.executionGasCeiling), gasPrice: toHex(gasPrice), ...(transaction.nonce === undefined ? {} : { nonce: toHex(transaction.nonce) }) }],
|
|
125
122
|
}));
|
|
126
123
|
const result = await lifecycleRpc(client, "eth_simulateV1", [{ blockStateCalls, validation, traceTransfers: false, returnFullTransactions: false }, toHex(block.number)]);
|
|
127
124
|
if (!Array.isArray(result) || result.length !== transactions.length) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Sequential simulation omitted blocks");
|
|
@@ -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) {
|
|
@@ -234,12 +239,12 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
|
|
|
234
239
|
if (transaction === undefined || gas === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing fork transaction/gas context");
|
|
235
240
|
try {
|
|
236
241
|
if (measure) {
|
|
237
|
-
const estimated = rpcQuantity(await lifecycleRpc(fork.client, "eth_estimateGas", [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gasPrice: toHex(gasPrice) }]), "fork sequential gas estimate");
|
|
242
|
+
const estimated = rpcQuantity(await lifecycleRpc(fork.client, "eth_estimateGas", [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gasPrice: toHex(gasPrice), ...(transaction.nonce === undefined ? {} : { nonce: toHex(transaction.nonce) }) }]), "fork sequential gas estimate");
|
|
238
243
|
gasRequired = estimated;
|
|
239
244
|
const buffered = (estimated * BigInt(10000 + headroomBps) + 9999n) / 10000n;
|
|
240
245
|
if (buffered < gas) gas = buffered;
|
|
241
246
|
}
|
|
242
|
-
const hash = rpcHex(await lifecycleRpc(fork.client, "eth_sendTransaction", [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gas), gasPrice: toHex(gasPrice) }]), "fork transaction hash");
|
|
247
|
+
const hash = rpcHex(await lifecycleRpc(fork.client, "eth_sendTransaction", [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gas), gasPrice: toHex(gasPrice), ...(transaction.nonce === undefined ? {} : { nonce: toHex(transaction.nonce) }) }]), "fork transaction hash");
|
|
243
248
|
const receipt = await waitForkReceipt(fork.client, hash, fork.receiptTimeoutMs ?? 10_000);
|
|
244
249
|
const gasUsed = rpcQuantity(receipt.gasUsed, "fork gas used");
|
|
245
250
|
if (rpcQuantity(receipt.status, "fork transaction status") !== 1n) {
|
|
@@ -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): 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,8 +361,8 @@ 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),
|
|
358
|
-
hasTransactions && reviewedEnvelope === undefined ?
|
|
364
|
+
lifecycleSessionRead(client, `${planned.chainId}:account-code:${context.account.toLowerCase()}`, () => lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block)),
|
|
365
|
+
hasTransactions && reviewedEnvelope === undefined ? selectedGasPrice ?? readLifecycleGasPrice(client) : undefined,
|
|
359
366
|
]));
|
|
360
367
|
})().then((observations) => ({ observations }), (failure: unknown) => ({ failure }));
|
|
361
368
|
// Refused inputs may discard this read-only preparation before it settles.
|
|
@@ -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,8 +418,9 @@ 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);
|
|
421
|
+
const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, selectedGasPrice, sizing);
|
|
423
422
|
assertCurrent();
|
|
423
|
+
registerLifecycleFinalCheck(client, result.observations[0].assertCurrent);
|
|
424
424
|
return simulation;
|
|
425
425
|
} catch (failure) {
|
|
426
426
|
closed = true;
|
|
@@ -437,11 +437,11 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
|
|
|
437
437
|
return simulate;
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
-
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> {
|
|
441
441
|
// Standalone calls always produce fresh context, including live chain and fees.
|
|
442
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");
|
|
443
443
|
const block = pinnedBlock ?? await readLifecycleBlock(options.client);
|
|
444
|
-
const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope);
|
|
444
|
+
const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope, undefined, sizing);
|
|
445
445
|
try { return await prepared(transactions, nextEnvelope); }
|
|
446
446
|
finally { prepared.close(); }
|
|
447
447
|
}
|
|
@@ -455,6 +455,8 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
455
455
|
assertCurrent: () => void,
|
|
456
456
|
diagnostic: boolean,
|
|
457
457
|
nextEnvelope?: NextTransactionEnvelope,
|
|
458
|
+
selectedGasPrice?: bigint,
|
|
459
|
+
sizing?: GasSizingHints,
|
|
458
460
|
): Promise<LifecycleSimulation> {
|
|
459
461
|
const { client, planned } = options;
|
|
460
462
|
const sourceClient = lifecycleSourceClient(client);
|
|
@@ -464,17 +466,17 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
464
466
|
if (chainId !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
|
|
465
467
|
const accountCode = rpcHex(accountCodeValue, "account code");
|
|
466
468
|
if (accountCode !== "0x") {
|
|
467
|
-
|
|
469
|
+
|
|
468
470
|
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
|
|
469
471
|
}
|
|
470
472
|
for (const transaction of transactions) {
|
|
471
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");
|
|
472
474
|
if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) {
|
|
473
|
-
|
|
475
|
+
|
|
474
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" };
|
|
475
477
|
}
|
|
476
478
|
}
|
|
477
|
-
if (transactions.length === 0) {
|
|
479
|
+
if (transactions.length === 0) { return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] }; }
|
|
478
480
|
if (nextEnvelope !== undefined && (transactions.length !== 1 || typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas <= 0n || nextEnvelope.gas >= 1n << 64n ||
|
|
479
481
|
typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
|
|
480
482
|
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Reviewed replay requires one immediate transaction with exact uint64 gas and uint256 price");
|
|
@@ -500,15 +502,15 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
500
502
|
const reason = exceedsCompute
|
|
501
503
|
? `${atomic ? "Atomic launch" : "Immediate transaction"} needs about ${gasInMillions(estimatedGas)}M gas, above the ${gasInMillions(computeCap)}M per-transaction limit${atomic ? "; use staged" : ""}`
|
|
502
504
|
: "The immediate transaction estimate exceeds the RPC aggregate simulation gas cap; no prefix can be proved";
|
|
503
|
-
|
|
505
|
+
|
|
504
506
|
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: exceedsCompute ? "compute" : "simulation-gas", reason };
|
|
505
507
|
}
|
|
506
508
|
}
|
|
507
|
-
const recommendedGasPrice = nextEnvelope === undefined ? rpcQuantity(gasPriceValue, "gas price") : nextEnvelope.gasPrice;
|
|
509
|
+
const recommendedGasPrice = nextEnvelope === undefined ? typeof gasPriceValue === "bigint" ? gasPriceValue : rpcQuantity(gasPriceValue, "gas price") : nextEnvelope.gasPrice;
|
|
508
510
|
// New recommendations cover lagging quotes; reviewed fees are replayed unchanged
|
|
509
511
|
// under natural child fees, never refreshed or silently increased.
|
|
510
512
|
const baseFeeEnvelope = (block.baseFeePerGas ?? 0n) * 2n;
|
|
511
|
-
const gasPrice = nextEnvelope?.gasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
|
|
513
|
+
const gasPrice = nextEnvelope?.gasPrice ?? selectedGasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
|
|
512
514
|
if (gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
|
|
513
515
|
const buffer = (gas: bigint) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
|
|
514
516
|
const posterGas: bigint[] = [];
|
|
@@ -516,40 +518,78 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
516
518
|
try {
|
|
517
519
|
const [, estimates] = await Promise.all([
|
|
518
520
|
proveBackend(limits, gasPrice),
|
|
519
|
-
nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster",
|
|
521
|
+
!diagnostic && nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster", async () => [buffer((await readNitroPosterGas(transactions[0]!, context)).posterGas)]) : [],
|
|
520
522
|
]);
|
|
521
523
|
posterGas.push(...estimates);
|
|
522
524
|
} catch (failure) {
|
|
523
525
|
if (simulationFailureKind(failure) === "abort") throw failure;
|
|
524
526
|
assertCurrent();
|
|
525
|
-
|
|
527
|
+
|
|
526
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.` };
|
|
527
529
|
}
|
|
528
530
|
}
|
|
529
531
|
assertCurrent();
|
|
530
|
-
const discoveryGas = transactions.map((
|
|
531
|
-
const total = limits.
|
|
532
|
+
const discoveryGas = transactions.map(() => {
|
|
533
|
+
const total = limits.protocol === "nitro" ? limits.maxSimulationGas ?? block.gasLimit : limits.executionGasCeiling;
|
|
532
534
|
const capped = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < total ? limits.transactionGasCeiling : total;
|
|
533
535
|
if (capped <= 0n || capped >= 1n << 64n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Compute/poster envelope exceeds uint64 gas");
|
|
534
536
|
return capped;
|
|
535
537
|
});
|
|
536
|
-
|
|
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");
|
|
537
540
|
let measured: LifecycleSimulationStep[] = [];
|
|
538
|
-
|
|
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();
|
|
539
558
|
try {
|
|
540
|
-
|
|
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
|
+
}
|
|
541
575
|
} catch (failure) {
|
|
542
|
-
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;
|
|
543
579
|
assertCurrent();
|
|
544
|
-
if (options.fork === undefined || limits.protocol === "nitro") {
|
|
545
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
580
|
+
if (options.fork === undefined || limits.protocol === "nitro" || kind !== "source") {
|
|
546
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."}` };
|
|
547
582
|
}
|
|
548
583
|
backend = "controlled-fork";
|
|
549
|
-
measured = await simulateForkPass(options.fork, planned, block, transactions,
|
|
584
|
+
measured = await simulateForkPass(options.fork, planned, block, transactions, sizingGas, gasPrice, true, limits.headroomBps);
|
|
550
585
|
}
|
|
551
586
|
assertCurrent();
|
|
552
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
|
+
}
|
|
553
593
|
if (failedIndex >= 0 && limits.maxSimulationGas !== undefined &&
|
|
554
594
|
measured.reduce((gas, step) => gas + (step.gasUsed ?? 0n), 0n) * 100n >= limits.maxSimulationGas * 95n) {
|
|
555
595
|
// The provider exhausted its aggregate used-gas budget, not the contract.
|
|
@@ -558,90 +598,78 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
558
598
|
transactions = transactions.slice(0, failedIndex);
|
|
559
599
|
base.deferredTransactionIds = allTransactions.slice(failedIndex).map((transaction) => transaction.id);
|
|
560
600
|
if (transactions.length === 0) {
|
|
561
|
-
|
|
601
|
+
|
|
562
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" };
|
|
563
603
|
}
|
|
564
604
|
}
|
|
565
605
|
const failed = measured.find((step) => !step.success);
|
|
566
|
-
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 };
|
|
567
607
|
if (diagnostic) {
|
|
568
|
-
|
|
608
|
+
|
|
569
609
|
return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
|
|
570
610
|
}
|
|
571
611
|
}
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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]!;
|
|
578
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
|
+
}
|
|
579
641
|
const exceeds = gasLimits.findIndex((gas, index) =>
|
|
580
642
|
gas <= 0n || gas >= 1n << 64n || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling ||
|
|
581
|
-
nextEnvelope === undefined && (measured[index]!.gasRequired ?? measured[index]!.gasUsed!) > gas
|
|
643
|
+
nextEnvelope === undefined && !hasSizing && (measured[index]!.gasRequired ?? measured[index]!.gasUsed!) > gas ||
|
|
582
644
|
limits.protocol === "evm" && gas > limits.executionGasCeiling);
|
|
583
645
|
if (exceeds >= 0) {
|
|
584
|
-
|
|
646
|
+
|
|
585
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 };
|
|
586
648
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
let
|
|
590
|
-
|
|
591
|
-
? simulateRpcPass(client, planned, block, transactions, gas, limits, gasPrice, true)
|
|
592
|
-
: simulateForkPass(options.fork!, planned, block, transactions, gas, gasPrice, false, limits.headroomBps);
|
|
593
|
-
try {
|
|
594
|
-
verifiedSteps = await lifecycleStage(client, "simulation.replay", () => replay(gasLimits));
|
|
595
|
-
} catch (failure) {
|
|
596
|
-
const kind = simulationFailureKind(failure);
|
|
597
|
-
if (kind === "abort") throw failure;
|
|
598
|
-
assertCurrent();
|
|
599
|
-
if (kind !== "out-of-gas" || backend !== "eth_simulateV1" || nextEnvelope !== undefined) {
|
|
600
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
601
|
-
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)}` };
|
|
602
|
-
}
|
|
603
|
-
replayError = lifecycleFailureReason(failure);
|
|
604
|
-
}
|
|
605
|
-
assertCurrent();
|
|
606
|
-
let finalFailure = verifiedSteps.find((step) => !step.success);
|
|
607
|
-
const failedIndex = verifiedSteps.findIndex((step) => !step.success);
|
|
608
|
-
const rescue = nextEnvelope === undefined && backend === "eth_simulateV1" &&
|
|
609
|
-
(replayError !== undefined && discoveryGas.some((gas, index) => index < transactions.length && gas > gasLimits[index]!) ||
|
|
610
|
-
finalFailure !== undefined && looksGasExhausted(finalFailure, gasLimits[failedIndex]!) && discoveryGas[failedIndex]! > gasLimits[failedIndex]!);
|
|
611
|
-
// One legal-envelope rescue covers thrown/returned genuine OOG. Never retry
|
|
612
|
-
// semantic errors, switch replay backends, or enlarge a held immediate envelope.
|
|
613
|
-
if (rescue) {
|
|
614
|
-
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) {
|
|
615
653
|
try {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
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));
|
|
620
657
|
} catch (failure) {
|
|
621
|
-
|
|
622
|
-
if (kind === "abort") throw failure;
|
|
658
|
+
if (simulationFailureKind(failure) === "abort") throw failure;
|
|
623
659
|
assertCurrent();
|
|
624
|
-
|
|
625
|
-
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)}` };
|
|
626
|
-
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)}` };
|
|
627
661
|
}
|
|
628
662
|
}
|
|
629
|
-
if (replayError !== undefined) {
|
|
630
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
631
|
-
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling failed: ${replayError}` };
|
|
632
|
-
}
|
|
633
663
|
assertCurrent();
|
|
664
|
+
const finalFailure = verifiedSteps.find((step) => !step.success);
|
|
634
665
|
const estimates: LifecycleSimulationStep[] = [];
|
|
635
|
-
|
|
636
|
-
// fee observations without inventing a balance or doing redundant payer work.
|
|
637
|
-
let remainingBalance = finalFailure === undefined
|
|
638
|
-
? rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance")
|
|
639
|
-
: undefined;
|
|
666
|
+
let remainingBalance = finalFailure === undefined ? balance : undefined;
|
|
640
667
|
let affordabilityFailure: string | undefined;
|
|
641
668
|
const feeContext = configuration.estimateDataFee === undefined || sourceClient === client ? context : { ...context, client: sourceClient };
|
|
642
669
|
for (let index = 0; index < verifiedSteps.length; index += 1) {
|
|
643
|
-
const step = verifiedSteps[index]; const transaction = transactions[index]; const gasLimit =
|
|
670
|
+
const step = verifiedSteps[index]; const transaction = transactions[index]; const gasLimit = gasLimits[index];
|
|
644
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; }
|
|
645
673
|
const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit, gasPrice }, feeContext);
|
|
646
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");
|
|
647
675
|
// Nitro gas includes poster charges. Never add the quoted poster fee again.
|
|
@@ -653,9 +681,9 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
653
681
|
remainingBalance -= required;
|
|
654
682
|
}
|
|
655
683
|
const poster = posterGas[index];
|
|
656
|
-
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" } });
|
|
657
685
|
}
|
|
658
|
-
|
|
686
|
+
|
|
659
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 };
|
|
660
688
|
}
|
|
661
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
|
+
}
|