@black-market/sdk 0.14.0 → 0.15.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 -3
- package/dist/lifecycle/authors.js +2 -2
- package/dist/lifecycle/authors.js.map +1 -1
- package/dist/lifecycle/gas-profile.d.ts +35 -0
- package/dist/lifecycle/gas-profile.d.ts.map +1 -0
- package/dist/lifecycle/gas-profile.js +183 -0
- package/dist/lifecycle/gas-profile.js.map +1 -0
- 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 +1 -1
- package/dist/lifecycle/lens.d.ts.map +1 -1
- package/dist/lifecycle/lens.js +1 -1
- package/dist/lifecycle/lens.js.map +1 -1
- package/dist/lifecycle/opening-buys.d.ts +13 -0
- package/dist/lifecycle/opening-buys.d.ts.map +1 -0
- package/dist/lifecycle/opening-buys.js +171 -0
- package/dist/lifecycle/opening-buys.js.map +1 -0
- package/dist/lifecycle/planner.d.ts +2 -2
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +178 -163
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/presets.d.ts +6 -1
- package/dist/lifecycle/presets.d.ts.map +1 -1
- package/dist/lifecycle/presets.js +7 -0
- package/dist/lifecycle/presets.js.map +1 -1
- package/dist/lifecycle/progress.d.ts +1 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +21 -20
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +24 -19
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +58 -50
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts +3 -4
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +98 -247
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.js +1 -2
- package/dist/lifecycle/snapshot.js.map +1 -1
- package/dist/lifecycle/transactions.d.ts +19 -3
- package/dist/lifecycle/transactions.d.ts.map +1 -1
- package/dist/lifecycle/transactions.js +25 -20
- package/dist/lifecycle/transactions.js.map +1 -1
- package/dist/lifecycle/types.d.ts +53 -45
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/dist/lifecycle/ufg.d.ts.map +1 -1
- package/dist/lifecycle/ufg.js +3 -7
- package/dist/lifecycle/ufg.js.map +1 -1
- package/docs/launch-lifecycle.md +71 -82
- package/docs/lifecycle-gas-profiles.md +90 -0
- package/docs/reference.md +53 -51
- package/examples/README.md +13 -10
- package/examples/launch-example.ts +35 -25
- package/examples/launch-lifecycle-smoke.ts +5 -3
- package/package.json +1 -1
- package/src/lifecycle/authors.ts +2 -2
- package/src/lifecycle/gas-profile.ts +175 -0
- package/src/lifecycle/index.ts +3 -1
- package/src/lifecycle/lens.ts +1 -1
- package/src/lifecycle/opening-buys.ts +157 -0
- package/src/lifecycle/planner.ts +164 -151
- package/src/lifecycle/presets.ts +12 -1
- package/src/lifecycle/progress.ts +19 -20
- package/src/lifecycle/rpc.ts +56 -44
- package/src/lifecycle/simulation.ts +85 -231
- package/src/lifecycle/snapshot.ts +1 -2
- package/src/lifecycle/transactions.ts +33 -21
- package/src/lifecycle/types.ts +52 -42
- package/src/lifecycle/ufg.ts +3 -7
|
@@ -1,11 +1,9 @@
|
|
|
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 { assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleRpc, lifecycleSessionRead, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi,
|
|
4
|
+
import { assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleRpc, lifecycleSessionRead, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroPosterGasBound, readLifecycleBlock, readLifecycleChainId, readLifecycleGasPrice, readNitroPrices, registerLifecycleFinalCheck, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity, withLifecycleReadClient, type LifecycleLimitObservation } from "./rpc.js";
|
|
5
5
|
import { LifecyclePlanningError, type ControlledLifecycleFork, type LifecycleBlock, type LifecycleLimitContext, type LifecyclePostcondition, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleSimulationStep, type LifecycleTransaction, type PlannedLaunch, type ResolvedLifecycleLimits, type SimulateLaunchPlanOptions } from "./types.js";
|
|
6
6
|
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion } from "./markets.js";
|
|
7
|
-
import { hmmV2ForwardingReserve } from "./presets.js";
|
|
8
|
-
import { transactionGasEstimate } from "./transactions.js";
|
|
9
7
|
import { hashUfgLaunchConfig } from "./ufg-config.js";
|
|
10
8
|
|
|
11
9
|
function committedPositionCount(plan: LaunchPlanV1): number {
|
|
@@ -80,11 +78,9 @@ function checkCondition(condition: LifecyclePostcondition, data: Hex, planned: P
|
|
|
80
78
|
/** Native Nitro eth_simulateV1 executes commit-mode ArbOS charging, including its
|
|
81
79
|
* compute hold after the actual poster charge. A generic EVM/fork is not this proof. */
|
|
82
80
|
async function proveNitroBackend(client: LifecycleRpcClient, planned: PlannedLaunch, block: LifecycleBlock, limits: ResolvedLifecycleLimits, gasPrice: bigint): Promise<void> {
|
|
83
|
-
if (limits.arbOSVersion === undefined
|
|
81
|
+
if (limits.arbOSVersion === undefined) throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Pinned Nitro version is unavailable");
|
|
84
82
|
const probes = [
|
|
85
83
|
{ to: nitroArbSys, data: encodeFunctionData({ abi: nitroArbSysAbi, functionName: "arbOSVersion" }), expected: limits.arbOSVersion + 55n },
|
|
86
|
-
{ to: nitroGasInfo, data: encodeFunctionData({ abi: nitroGasInfoAbi, functionName: "getMaxTxGasLimit" }), expected: limits.maxTxComputeGas },
|
|
87
|
-
{ to: nitroGasInfo, data: encodeFunctionData({ abi: nitroGasInfoAbi, functionName: "getMaxBlockGasLimit" }), expected: limits.maxBlockComputeGas },
|
|
88
84
|
];
|
|
89
85
|
const gas = limits.executionGasCeiling < 100_000n ? limits.executionGasCeiling : 100_000n;
|
|
90
86
|
if (limits.maxSimulationGas !== undefined && gas * BigInt(probes.length) > limits.maxSimulationGas) throw new LifecyclePlanningError("SIMULATION_RPC_CAP", "Nitro metering probe exceeds the supplied RPC aggregate simulation cap");
|
|
@@ -100,12 +96,12 @@ async function proveNitroBackend(client: LifecycleRpcClient, planned: PlannedLau
|
|
|
100
96
|
}, toHex(block.number)]);
|
|
101
97
|
if (!Array.isArray(result) || result.length !== 1) throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Native simulation omitted the pinned ArbOS metering probe");
|
|
102
98
|
const simulated = rpcObject(result[0], "Nitro metering probe block");
|
|
103
|
-
if (!Array.isArray(simulated.calls) || simulated.calls.length !== probes.length) throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Native simulation omitted ArbOS version
|
|
99
|
+
if (!Array.isArray(simulated.calls) || simulated.calls.length !== probes.length) throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Native simulation omitted the ArbOS version");
|
|
104
100
|
for (let index = 0; index < probes.length; index += 1) {
|
|
105
101
|
const call = rpcObject(simulated.calls[index], "Nitro metering probe");
|
|
106
102
|
const data = rpcHex(call.returnData, "simulated Nitro metering result");
|
|
107
103
|
if (rpcQuantity(call.status, "Nitro metering probe status") !== 1n || rpcQuantity(call.gasUsed, "Nitro metering probe gas") > gas || data.length !== 66 || BigInt(data) !== probes[index]?.expected) {
|
|
108
|
-
throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Simulated ArbOS version
|
|
104
|
+
throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Simulated ArbOS version differs from the exact pinned chain context");
|
|
109
105
|
}
|
|
110
106
|
}
|
|
111
107
|
}
|
|
@@ -118,7 +114,7 @@ async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunc
|
|
|
118
114
|
// parent gasLimit are set.
|
|
119
115
|
const blockStateCalls = transactions.map((transaction, index) => ({
|
|
120
116
|
blockOverrides: { number: toHex(block.number + BigInt(index + 1)), time: toHex(block.timestamp + BigInt(index + 1)), gasLimit: toHex(block.gasLimit) },
|
|
121
|
-
calls: [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gasLimits[index] ?? limits.executionGasCeiling), gasPrice: toHex(gasPrice)
|
|
117
|
+
calls: [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gasLimits[index] ?? limits.executionGasCeiling), gasPrice: toHex(gasPrice) }],
|
|
122
118
|
}));
|
|
123
119
|
const result = await lifecycleRpc(client, "eth_simulateV1", [{ blockStateCalls, validation, traceTransfers: false, returnFullTransactions: false }, toHex(block.number)]);
|
|
124
120
|
if (!Array.isArray(result) || result.length !== transactions.length) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Sequential simulation omitted blocks");
|
|
@@ -208,7 +204,7 @@ async function waitForkReceipt(client: LifecycleRpcClient, transactionHash: Hex,
|
|
|
208
204
|
}
|
|
209
205
|
throw new LifecyclePlanningError("FORK_RECEIPT_TIMEOUT", "The disposable fork did not mine its simulation transaction");
|
|
210
206
|
}
|
|
211
|
-
async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedLaunch, block: LifecycleBlock, transactions: readonly LifecycleTransaction[], gasLimits: readonly bigint[], gasPrice: bigint
|
|
207
|
+
async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedLaunch, block: LifecycleBlock, transactions: readonly LifecycleTransaction[], gasLimits: readonly bigint[], gasPrice: bigint): Promise<LifecycleSimulationStep[]> {
|
|
212
208
|
if (fork.isolation !== "disposable" || fork.allowTransactions !== true || !fork.sourceRpcUrl || !fork.forkRpcUrl) throw new LifecyclePlanningError("UNSAFE_FORK", "Actual simulation transactions require an explicitly distinct disposable controlled fork");
|
|
213
209
|
const sourceUrl = new URL(fork.sourceRpcUrl);
|
|
214
210
|
const forkUrl = new URL(fork.forkRpcUrl);
|
|
@@ -222,7 +218,7 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
|
|
|
222
218
|
await lifecycleRpc(fork.client, fork.resetMethod ?? "anvil_reset", [{ forking: { jsonRpcUrl: fork.sourceRpcUrl, blockNumber: Number(block.number) } }]);
|
|
223
219
|
forkBlock = await readLifecycleBlock(fork.client);
|
|
224
220
|
}
|
|
225
|
-
const forkChain =
|
|
221
|
+
const forkChain = await readLifecycleChainId(fork.client);
|
|
226
222
|
if (forkChain !== planned.chainId || forkChain !== (fork.expectedChainId ?? planned.chainId) || forkBlock.hash.toLowerCase() !== block.hash.toLowerCase() || forkBlock.hash.toLowerCase() !== (fork.expectedBlockHash ?? block.hash).toLowerCase()) throw new LifecyclePlanningError("FORK_STATE_MISMATCH", "Controlled fork must match the exact source chain and pinned latest block before simulation");
|
|
227
223
|
const snapshot = rpcHex(await lifecycleRpc(fork.client, "evm_snapshot"), "fork snapshot");
|
|
228
224
|
const steps: LifecycleSimulationStep[] = [];
|
|
@@ -234,17 +230,10 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
|
|
|
234
230
|
}
|
|
235
231
|
for (let index = 0; index < transactions.length; index += 1) {
|
|
236
232
|
const transaction = transactions[index];
|
|
237
|
-
|
|
238
|
-
let gasRequired: bigint | undefined;
|
|
233
|
+
const gas = gasLimits[index];
|
|
239
234
|
if (transaction === undefined || gas === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing fork transaction/gas context");
|
|
240
235
|
try {
|
|
241
|
-
|
|
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");
|
|
243
|
-
gasRequired = estimated;
|
|
244
|
-
const buffered = (estimated * BigInt(10000 + headroomBps) + 9999n) / 10000n;
|
|
245
|
-
if (buffered < gas) gas = buffered;
|
|
246
|
-
}
|
|
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");
|
|
236
|
+
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");
|
|
248
237
|
const receipt = await waitForkReceipt(fork.client, hash, fork.receiptTimeoutMs ?? 10_000);
|
|
249
238
|
const gasUsed = rpcQuantity(receipt.gasUsed, "fork gas used");
|
|
250
239
|
if (rpcQuantity(receipt.status, "fork transaction status") !== 1n) {
|
|
@@ -288,7 +277,7 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
|
|
|
288
277
|
const data = rpcHex(await lifecycleRpc(fork.client, "eth_call", [{ ...request, from: planned.account }, toHex(currentBlock.number)]), "fork postcondition");
|
|
289
278
|
checkCondition(condition, data, planned);
|
|
290
279
|
}
|
|
291
|
-
steps.push({ transactionId: transaction.id, success: true, gasUsed, gasLimit: gas,
|
|
280
|
+
steps.push({ transactionId: transaction.id, success: true, gasUsed, gasLimit: gas, returnData });
|
|
292
281
|
} catch (failure) {
|
|
293
282
|
steps.push({ transactionId: transaction.id, success: false, gasLimit: gas, error: lifecycleFailureReason(failure) });
|
|
294
283
|
break;
|
|
@@ -307,15 +296,13 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
|
|
|
307
296
|
type SimulationObservations = [LifecycleLimitObservation, unknown, unknown, unknown];
|
|
308
297
|
/** Internal, operation-local fee envelope for the already matched immediate step. */
|
|
309
298
|
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>;
|
|
312
299
|
export type PreparedLaunchSimulation = ((transactions: readonly LifecycleTransaction[], nextEnvelope?: NextTransactionEnvelope) => Promise<LifecycleSimulation>) & {
|
|
313
300
|
/** Unadmitted diagnostic results only; final measurement validates exact calldata. */
|
|
314
301
|
measure(transactions: readonly LifecycleTransaction[]): Promise<LifecycleSimulation>;
|
|
315
302
|
close(): void;
|
|
316
303
|
};
|
|
317
304
|
|
|
318
|
-
function observedFields<T extends object>(value: T): () => boolean {
|
|
305
|
+
export function observedFields<T extends object>(value: T): () => boolean {
|
|
319
306
|
const keys = Object.keys(value) as (keyof T)[];
|
|
320
307
|
const values = keys.map((key) => value[key]);
|
|
321
308
|
return () => keys.length === Object.keys(value).length && keys.every((key, index) => value[key] === values[index]);
|
|
@@ -323,7 +310,7 @@ function observedFields<T extends object>(value: T): () => boolean {
|
|
|
323
310
|
|
|
324
311
|
/** Internal continuation owned by one SDK read invocation. Only its positive
|
|
325
312
|
* capability proof is reused; every candidate executes its entire economic sequence. */
|
|
326
|
-
export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true,
|
|
313
|
+
export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true, immediate = false): PreparedLaunchSimulation {
|
|
327
314
|
const { client, planned, fork } = options;
|
|
328
315
|
const signal = options.signal;
|
|
329
316
|
if (signal?.aborted) throw Object.assign(new Error("Lifecycle simulation cancelled"), { name: "AbortError" });
|
|
@@ -362,7 +349,7 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
|
|
|
362
349
|
resolveLifecycleLimits(context, policy),
|
|
363
350
|
readLifecycleChainId(client),
|
|
364
351
|
lifecycleSessionRead(client, `${planned.chainId}:account-code:${context.account.toLowerCase()}`, () => lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block)),
|
|
365
|
-
hasTransactions
|
|
352
|
+
hasTransactions ? readLifecycleGasPrice(client, block, planned.chainId) : undefined,
|
|
366
353
|
]));
|
|
367
354
|
})().then((observations) => ({ observations }), (failure: unknown) => ({ failure }));
|
|
368
355
|
// Refused inputs may discard this read-only preparation before it settles.
|
|
@@ -418,7 +405,7 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
|
|
|
418
405
|
await capabilityPending;
|
|
419
406
|
assertCurrent();
|
|
420
407
|
};
|
|
421
|
-
const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope,
|
|
408
|
+
const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, immediate);
|
|
422
409
|
assertCurrent();
|
|
423
410
|
registerLifecycleFinalCheck(client, result.observations[0].assertCurrent);
|
|
424
411
|
return simulation;
|
|
@@ -437,13 +424,15 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
|
|
|
437
424
|
return simulate;
|
|
438
425
|
}
|
|
439
426
|
|
|
440
|
-
export async function simulateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], pinnedBlock?: LifecycleBlock, nextEnvelope?: NextTransactionEnvelope
|
|
441
|
-
// Standalone calls
|
|
427
|
+
export async function simulateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], pinnedBlock?: LifecycleBlock, nextEnvelope?: NextTransactionEnvelope): Promise<LifecycleSimulation> {
|
|
428
|
+
// Standalone calls own one fresh block/pricing context, not fresh chain guards.
|
|
442
429
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
430
|
+
return withLifecycleReadClient(options.client, async (client) => {
|
|
431
|
+
const [block] = await Promise.all([pinnedBlock ?? readLifecycleBlock(client), readLifecycleChainId(client)]);
|
|
432
|
+
const prepared = prepareLaunchSimulation({ ...options, client }, block, transactions.length !== 0);
|
|
433
|
+
try { return await prepared(transactions, nextEnvelope); }
|
|
434
|
+
finally { prepared.close(); }
|
|
435
|
+
});
|
|
447
436
|
}
|
|
448
437
|
|
|
449
438
|
async function simulateLaunchTransactionsWithContext(
|
|
@@ -455,235 +444,100 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
455
444
|
assertCurrent: () => void,
|
|
456
445
|
diagnostic: boolean,
|
|
457
446
|
nextEnvelope?: NextTransactionEnvelope,
|
|
458
|
-
|
|
459
|
-
sizing?: GasSizingHints,
|
|
447
|
+
immediate = false,
|
|
460
448
|
): Promise<LifecycleSimulation> {
|
|
461
|
-
const { client, planned } = options;
|
|
449
|
+
const { client, planned } = options, { block } = context;
|
|
462
450
|
const sourceClient = lifecycleSourceClient(client);
|
|
463
|
-
const { block } = context;
|
|
464
451
|
const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = observations;
|
|
465
|
-
const base = { confidence: "stateful" as const, executionProof: "failed" as const, protocolFit: "unknown" as const,
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (
|
|
469
|
-
|
|
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
|
-
}
|
|
452
|
+
const base = { confidence: "stateful" as const, executionProof: "failed" as const, protocolFit: "unknown" as const,
|
|
453
|
+
blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits, deferredTransactionIds: [] as string[] };
|
|
454
|
+
if (chainId !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Bound RPC chain differs from the committed launch");
|
|
455
|
+
if (rpcHex(accountCodeValue, "account code") !== "0x") return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
|
|
472
456
|
for (const transaction of transactions) {
|
|
473
457
|
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
|
-
if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) {
|
|
475
|
-
|
|
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
|
-
}
|
|
458
|
+
if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) 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" };
|
|
478
459
|
}
|
|
479
|
-
if (transactions.length === 0)
|
|
480
|
-
|
|
460
|
+
if (transactions.length === 0) return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] };
|
|
461
|
+
const gasLimit = limits.executionGasCeiling;
|
|
462
|
+
if (nextEnvelope !== undefined && (transactions.length !== 1 || typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas !== gasLimit ||
|
|
481
463
|
typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
|
|
482
|
-
throw new LifecyclePlanningError("
|
|
483
|
-
}
|
|
484
|
-
const allTransactions = transactions;
|
|
485
|
-
if (limits.maxSimulationGas !== undefined && nextEnvelope === undefined) {
|
|
486
|
-
let estimatedGas = 0n, count = 0;
|
|
487
|
-
for (const transaction of transactions) {
|
|
488
|
-
estimatedGas += transactionGasEstimate(planned.plan, transaction);
|
|
489
|
-
if (estimatedGas > limits.maxSimulationGas) break;
|
|
490
|
-
count += 1;
|
|
491
|
-
}
|
|
492
|
-
transactions = transactions.slice(0, count);
|
|
493
|
-
base.deferredTransactionIds = allTransactions.slice(count).map((transaction) => transaction.id);
|
|
494
|
-
if (count === 0) {
|
|
495
|
-
const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
|
|
496
|
-
const exceedsCompute = estimatedGas > computeCap;
|
|
497
|
-
const gasInMillions = (gas: bigint) => {
|
|
498
|
-
const tenths = (gas + 50_000n) / 100_000n;
|
|
499
|
-
return `${tenths / 10n}.${tenths % 10n}`;
|
|
500
|
-
};
|
|
501
|
-
const atomic = allTransactions[0]!.kind === "atomic";
|
|
502
|
-
const reason = exceedsCompute
|
|
503
|
-
? `${atomic ? "Atomic launch" : "Immediate transaction"} needs about ${gasInMillions(estimatedGas)}M gas, above the ${gasInMillions(computeCap)}M per-transaction limit${atomic ? "; use staged" : ""}`
|
|
504
|
-
: "The immediate transaction estimate exceeds the RPC aggregate simulation gas cap; no prefix can be proved";
|
|
505
|
-
|
|
506
|
-
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: exceedsCompute ? "compute" : "simulation-gas", reason };
|
|
507
|
-
}
|
|
464
|
+
throw new LifecyclePlanningError("STALE_REVIEWED_GAS", "Reviewed gas differs from the native transaction ceiling; review the launch again");
|
|
508
465
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
const
|
|
513
|
-
const gasPrice = nextEnvelope?.gasPrice ??
|
|
514
|
-
if (gasPrice >= 1n << 256n) throw new LifecyclePlanningError("
|
|
515
|
-
|
|
466
|
+
if (limits.transactionGasCeiling !== undefined && gasLimit > limits.transactionGasCeiling) return { ...base, executionProof: "unavailable", protocolFit: "failed", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: "gas-envelope", reason: "Native transaction ceiling exceeds the supplied complete gas policy", failedTransactionId: transactions[0]!.id };
|
|
467
|
+
const recommendation = typeof gasPriceValue === "bigint" ? gasPriceValue : rpcQuantity(gasPriceValue, "gas price");
|
|
468
|
+
const baseFee = block.baseFeePerGas ?? 0n;
|
|
469
|
+
const requiredPrice = limits.protocol === "nitro" && recommendation > baseFee ? recommendation : baseFee;
|
|
470
|
+
const gasPrice = nextEnvelope?.gasPrice ?? (recommendation > baseFee * 2n ? recommendation : baseFee * 2n);
|
|
471
|
+
if (gasPrice >= 1n << 256n || transactions[0]!.value + gasLimit * gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and native-ceiling fee must fit uint256");
|
|
472
|
+
if (gasPrice < requiredPrice) return { ...base, executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failedTransactionId: transactions[0]!.id, reason: "Reviewed gas price is below the pinned required fee" };
|
|
473
|
+
const balance = rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance");
|
|
474
|
+
if (transactions[0]!.value + gasLimit * gasPrice > balance) return { ...base, executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "affordability", reason: "Creator native balance cannot reserve transaction value and the native gas ceiling" };
|
|
516
475
|
const posterGas: bigint[] = [];
|
|
517
476
|
if (limits.protocol === "nitro") {
|
|
518
477
|
try {
|
|
519
|
-
const [,
|
|
520
|
-
|
|
521
|
-
!diagnostic && nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster", async () => [buffer((await readNitroPosterGas(transactions[0]!, context)).posterGas)]) : [],
|
|
522
|
-
]);
|
|
523
|
-
posterGas.push(...estimates);
|
|
478
|
+
const [, prices] = await Promise.all([proveBackend(limits, gasPrice), readNitroPrices(client, block)]);
|
|
479
|
+
for (const transaction of transactions) posterGas.push(nitroPosterGasBound(transaction, prices).posterGas);
|
|
524
480
|
} catch (failure) {
|
|
525
481
|
if (simulationFailureKind(failure) === "abort") throw failure;
|
|
526
482
|
assertCurrent();
|
|
527
|
-
|
|
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.` };
|
|
483
|
+
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Native Nitro execution unavailable: ${lifecycleFailureReason(failure)}. Generic Anvil/Hardhat forks cannot prove ArbOS metering.` };
|
|
529
484
|
}
|
|
530
485
|
}
|
|
486
|
+
const gasLimits = transactions.map(() => gasLimit);
|
|
487
|
+
let backend: "eth_simulateV1" | "controlled-fork" = (immediate || nextEnvelope !== undefined) && limits.protocol === "evm" && options.fork !== undefined ? "controlled-fork" : "eth_simulateV1";
|
|
488
|
+
let verified: LifecycleSimulationStep[];
|
|
531
489
|
assertCurrent();
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const capped = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < total ? limits.transactionGasCeiling : total;
|
|
535
|
-
if (capped <= 0n || capped >= 1n << 64n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Compute/poster envelope exceeds uint64 gas");
|
|
536
|
-
return capped;
|
|
537
|
-
});
|
|
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");
|
|
540
|
-
let measured: LifecycleSimulationStep[] = [];
|
|
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();
|
|
558
|
-
try {
|
|
490
|
+
try {
|
|
491
|
+
verified = await lifecycleStage(client, "simulation.measure", async () => {
|
|
559
492
|
try {
|
|
560
|
-
|
|
493
|
+
return backend === "controlled-fork"
|
|
494
|
+
? await simulateForkPass(options.fork!, planned, block, transactions, gasLimits, gasPrice)
|
|
495
|
+
: await simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, !diagnostic);
|
|
561
496
|
} catch (failure) {
|
|
562
|
-
if (
|
|
497
|
+
if (backend !== "eth_simulateV1" || limits.protocol !== "evm" || options.fork === undefined ||
|
|
498
|
+
upfrontFundsFailure(failure) || simulationFailureKind(failure) !== "source") throw failure;
|
|
563
499
|
assertCurrent();
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
}
|
|
575
|
-
} catch (failure) {
|
|
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;
|
|
579
|
-
assertCurrent();
|
|
580
|
-
if (options.fork === undefined || limits.protocol === "nitro" || kind !== "source") {
|
|
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."}` };
|
|
500
|
+
backend = "controlled-fork";
|
|
501
|
+
return simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice);
|
|
582
502
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
503
|
+
});
|
|
504
|
+
} catch (failure) {
|
|
505
|
+
if (simulationFailureKind(failure) === "abort") throw failure;
|
|
586
506
|
assertCurrent();
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
}
|
|
593
|
-
if (failedIndex >= 0 && limits.maxSimulationGas !== undefined &&
|
|
594
|
-
measured.reduce((gas, step) => gas + (step.gasUsed ?? 0n), 0n) * 100n >= limits.maxSimulationGas * 95n) {
|
|
595
|
-
// The provider exhausted its aggregate used-gas budget, not the contract.
|
|
596
|
-
// Preserve only actual successful results; the failed call is deferred too.
|
|
597
|
-
measured = measured.slice(0, failedIndex);
|
|
598
|
-
transactions = transactions.slice(0, failedIndex);
|
|
599
|
-
base.deferredTransactionIds = allTransactions.slice(failedIndex).map((transaction) => transaction.id);
|
|
600
|
-
if (transactions.length === 0) {
|
|
601
|
-
|
|
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" };
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
const failed = measured.find((step) => !step.success);
|
|
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 };
|
|
607
|
-
if (diagnostic) {
|
|
608
|
-
|
|
609
|
-
return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
|
|
610
|
-
}
|
|
611
|
-
}
|
|
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]!;
|
|
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
|
-
}
|
|
641
|
-
const exceeds = gasLimits.findIndex((gas, index) =>
|
|
642
|
-
gas <= 0n || gas >= 1n << 64n || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling ||
|
|
643
|
-
nextEnvelope === undefined && !hasSizing && (measured[index]!.gasRequired ?? measured[index]!.gasUsed!) > gas ||
|
|
644
|
-
limits.protocol === "evm" && gas > limits.executionGasCeiling);
|
|
645
|
-
if (exceeds >= 0) {
|
|
646
|
-
|
|
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 };
|
|
648
|
-
}
|
|
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) {
|
|
653
|
-
try {
|
|
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));
|
|
657
|
-
} catch (failure) {
|
|
658
|
-
if (simulationFailureKind(failure) === "abort") throw failure;
|
|
659
|
-
assertCurrent();
|
|
660
|
-
return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [], reason: `Exact transaction simulation unavailable: ${lifecycleFailureReason(failure)}` };
|
|
661
|
-
}
|
|
507
|
+
if (upfrontFundsFailure(failure)) return { ...base, backend, confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [], failureCategory: "affordability", reason: "Native balance cannot reserve the fixed-ceiling sequence fee envelopes" };
|
|
508
|
+
return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [], reason: `Exact transaction simulation unavailable: ${lifecycleFailureReason(failure)}` };
|
|
662
509
|
}
|
|
663
510
|
assertCurrent();
|
|
664
|
-
|
|
511
|
+
if (limits.maxSimulationGas !== undefined && verified.reduce((total, step) => total + (step.gasUsed ?? 0n), 0n) > limits.maxSimulationGas) {
|
|
512
|
+
return { ...base, backend, confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [],
|
|
513
|
+
failureCategory: "source", capacityConstraint: "simulation-gas", deferredTransactionIds: transactions.map((transaction) => transaction.id),
|
|
514
|
+
reason: "Complete launch execution exceeds the supplied aggregate simulation gas budget" };
|
|
515
|
+
}
|
|
516
|
+
const failure = verified.find((step) => !step.success);
|
|
517
|
+
if (diagnostic) return { ...base, backend, executionProof: "unavailable", admitted: false, steps: verified, reason: failure?.error, failedTransactionId: failure?.transactionId, failureCategory: failure?.failureCategory };
|
|
665
518
|
const estimates: LifecycleSimulationStep[] = [];
|
|
666
|
-
let remainingBalance = finalFailure === undefined ? balance : undefined;
|
|
667
519
|
let affordabilityFailure: string | undefined;
|
|
668
520
|
const feeContext = configuration.estimateDataFee === undefined || sourceClient === client ? context : { ...context, client: sourceClient };
|
|
669
|
-
for (
|
|
670
|
-
const
|
|
671
|
-
if (
|
|
672
|
-
|
|
521
|
+
for (const [index, step] of verified.entries()) {
|
|
522
|
+
const transaction = transactions[index]!;
|
|
523
|
+
if (index > 0 || transaction.kind === "approve" || transaction.kind === "approve-reset") {
|
|
524
|
+
estimates.push({ ...step, ...(index > 0 ? { provisional: true } : {}) }); continue;
|
|
525
|
+
}
|
|
673
526
|
const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit, gasPrice }, feeContext);
|
|
674
527
|
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");
|
|
675
|
-
|
|
676
|
-
const executionFee = gasLimit * gasPrice;
|
|
677
|
-
const required = transaction.value + executionFee + (dataFee ?? 0n);
|
|
528
|
+
const executionFee = gasLimit * gasPrice, required = transaction.value + executionFee + (dataFee ?? 0n);
|
|
678
529
|
if (required >= 1n << 256n) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
|
|
679
|
-
if (
|
|
680
|
-
if (required > remainingBalance && affordabilityFailure === undefined) affordabilityFailure = "Creator native balance cannot fund the remaining transaction value and execution/data fee envelopes";
|
|
681
|
-
remainingBalance -= required;
|
|
682
|
-
}
|
|
530
|
+
if (required > balance) affordabilityFailure = "Creator native balance cannot reserve transaction value and the native-ceiling/data fee envelope";
|
|
683
531
|
const poster = posterGas[index];
|
|
684
|
-
estimates.push({ ...step,
|
|
532
|
+
estimates.push({ ...step, estimate: { gasUsed: step.gasUsed, gasLimit, gasPrice, executionFee, dataFee, posterGas: poster,
|
|
533
|
+
posterFee: poster === undefined ? undefined : poster * gasPrice, dataFeeIncludedInGas: limits.protocol === "nitro",
|
|
534
|
+
totalFee: dataFee === undefined ? undefined : executionFee + dataFee, feeConfidence: dataFee === undefined ? "execution-only" : "execution-and-data" } });
|
|
685
535
|
}
|
|
686
|
-
|
|
687
|
-
return { ...base, backend, executionProof:
|
|
536
|
+
const activationFailure = failure?.transactionId === "activate" && failure.failureCategory === "capacity";
|
|
537
|
+
return { ...base, backend, executionProof: failure === undefined ? "proved" : "failed", protocolFit: failure === undefined ? "proved" : "unknown",
|
|
538
|
+
admitted: failure === undefined && affordabilityFailure === undefined, steps: estimates,
|
|
539
|
+
reason: activationFailure ? "Too many positions or pools for one activation. Reduce them to fit the native transaction ceiling." : failure?.error ?? affordabilityFailure,
|
|
540
|
+
failedTransactionId: failure?.transactionId, failureCategory: failure?.failureCategory ?? (affordabilityFailure === undefined ? undefined : "affordability"),
|
|
541
|
+
capacityConstraint: activationFailure ? "activation" : failure?.failureCategory === "capacity" ? "compute" : undefined };
|
|
688
542
|
}
|
|
689
543
|
|
|
@@ -37,8 +37,7 @@ export async function readLifecyclePlanningBlock(client: LifecycleRpcClient, pla
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
add(nitroArbSys, nitroArbSysAbi, "arbOSVersion", []);
|
|
40
|
-
add(nitroGasInfo, nitroGasInfoAbi, "
|
|
41
|
-
add(nitroGasInfo, nitroGasInfoAbi, "getMaxBlockGasLimit", []);
|
|
40
|
+
add(nitroGasInfo, nitroGasInfoAbi, "getPricesInWei", []);
|
|
42
41
|
await attestLifecycleMulticall(client, block);
|
|
43
42
|
const calls = reads.map((read) => ({ target: read.to, allowFailure: true, callData: encodeFunctionData({ abi: read.abi, functionName: read.functionName, args: read.args }) }));
|
|
44
43
|
const data = encodeFunctionData({ abi: multicall3Abi, functionName: "aggregate3", args: [calls] });
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { encodeFunctionData } from "viem";
|
|
1
|
+
import { encodeFunctionData, type Address } from "viem";
|
|
2
2
|
import { launchLifecycleAbi } from "./abi.js";
|
|
3
3
|
import { LifecycleFundingKind, LifecycleMode, type LaunchPlanV1 } from "./schema.js";
|
|
4
4
|
import { LifecyclePlanningError, type BuildLaunchTransactionsOptions, type LifecycleTransaction } from "./types.js";
|
|
5
5
|
import { assertLifecycleTokenAllocation } from "./allocations.js";
|
|
6
|
-
import { HMM_V2_PREPARE_GAS
|
|
6
|
+
import { HMM_V2_PREPARE_GAS } from "./presets.js";
|
|
7
7
|
import { isHmmProfile } from "./hmm.js";
|
|
8
|
+
import { ufgLaunchProxyV1Abi } from "./ufg-abi.js";
|
|
9
|
+
import { UFG_LAUNCH_PROXY_ADDRESS } from "./ufg-config.js";
|
|
10
|
+
import type { UfgLaunchConfig } from "./ufg-config.js";
|
|
8
11
|
|
|
9
12
|
const OTHER_MARKET_PREPARE_GAS = 16_000_000n;
|
|
10
|
-
const BEGIN_GAS = 9_500_000n;
|
|
11
|
-
const ACTIVATE_MARKET_GAS = 6_000_000n;
|
|
12
13
|
|
|
13
14
|
export function preparationGasEstimate(plan: LaunchPlanV1, first: number, count: number): bigint {
|
|
14
15
|
let gas = 0n;
|
|
@@ -30,42 +31,53 @@ export function packPreparationBatches(plan: LaunchPlanV1, prepared: number, com
|
|
|
30
31
|
return batches;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
export type LifecycleCommand =
|
|
35
|
+
| { kind: "atomic" | "begin" | "activate" }
|
|
36
|
+
| { kind: "prepare"; first: number; count: number };
|
|
37
|
+
type LifecycleUfgRoute = { account: Address; config: UfgLaunchConfig };
|
|
38
|
+
|
|
39
|
+
/** Internal encoder shared by complete plans and the one-command continuation. */
|
|
40
|
+
export function buildLifecycleCommand(plan: LaunchPlanV1, command: LifecycleCommand, ufg?: LifecycleUfgRoute): LifecycleTransaction {
|
|
41
|
+
const abi = ufg === undefined ? launchLifecycleAbi : ufgLaunchProxyV1Abi;
|
|
42
|
+
const base = { chainId: Number(plan.chainId), from: ufg?.account ?? plan.creator, to: ufg === undefined ? plan.orchestrator : UFG_LAUNCH_PROXY_ADDRESS };
|
|
43
|
+
const value = command.kind === "atomic" || command.kind === "begin"
|
|
44
|
+
? plan.funding.reduce((total, funding) => total + (funding.kind === LifecycleFundingKind.NativeWrap || funding.kind === LifecycleFundingKind.Swap && BigInt(funding.inputAsset) === 0n ? funding.inputAmount : 0n), 0n) : 0n;
|
|
45
|
+
switch (command.kind) {
|
|
46
|
+
case "atomic": return { ...base, id: "atomic", kind: "atomic", data: encodeFunctionData({ abi, functionName: "launchAtomic", args: ufg === undefined ? [plan] : [plan, ufg.config] }), value, dependencies: [], postconditions: [{ kind: "launch", phase: "Active", preparedMarkets: plan.markets.length }] };
|
|
47
|
+
case "begin": return { ...base, id: "begin", kind: "begin", data: encodeFunctionData({ abi, functionName: "beginLaunch", args: ufg === undefined ? [plan, LifecycleMode.Staged] : [plan, LifecycleMode.Staged, ufg.config] }), value, dependencies: [], postconditions: [{ kind: "launch", phase: "Preparing", preparedMarkets: 0 }] };
|
|
48
|
+
case "activate": return { ...base, id: "activate", kind: "activate", data: encodeFunctionData({ abi, functionName: "activateLaunch", args: [plan] }), value, dependencies: [], postconditions: [{ kind: "launch", phase: "Active", preparedMarkets: plan.markets.length }] };
|
|
49
|
+
case "prepare": return { ...base, id: `prepare:${command.first}:${command.count}`, kind: "prepare", data: encodeFunctionData({ abi, functionName: "prepareMarkets", args: [plan, command.first, command.count] }), value, dependencies: [], marketStart: command.first, marketCount: command.count,
|
|
50
|
+
postconditions: [{ kind: "launch", phase: command.first + command.count === plan.markets.length ? "Ready" : "Preparing", preparedMarkets: command.first + command.count }] };
|
|
41
51
|
}
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
/** Pure unsigned launch commands. Every command carries the complete commitment;
|
|
45
55
|
* preparation splits only empty markets, never positions, buys or public opening.
|
|
46
56
|
* Allowance discovery, recovery and execution proof belong to the planner. */
|
|
47
|
-
export function
|
|
57
|
+
export function buildLifecycleCommands({ plan, mode, preparationBatches }: BuildLaunchTransactionsOptions, ufg?: LifecycleUfgRoute): LifecycleTransaction[] {
|
|
48
58
|
if (mode !== "atomic" && mode !== "staged") throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose atomic or staged explicitly; staged is never silently substituted");
|
|
49
59
|
if (plan.chainId <= 0n || plan.chainId > BigInt(Number.MAX_SAFE_INTEGER)) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Chain ID must be a positive exact JavaScript integer");
|
|
50
60
|
if (plan.markets.length === 0 || plan.markets.length > 16) throw new LifecyclePlanningError("INVALID_MARKET_COUNT", "A launch requires between one and sixteen markets");
|
|
51
61
|
if (mode === "atomic" && preparationBatches !== undefined) throw new LifecyclePlanningError("INVALID_PREPARATION_BATCHES", "Atomic execution has no separate preparation batches");
|
|
52
62
|
assertLifecycleTokenAllocation(plan);
|
|
53
|
-
const nativeValue = plan.funding.reduce((total, funding) => total + (funding.kind === LifecycleFundingKind.NativeWrap || funding.kind === LifecycleFundingKind.Swap && BigInt(funding.inputAsset) === 0n ? funding.inputAmount : 0n), 0n);
|
|
54
|
-
const base = { chainId: Number(plan.chainId), from: plan.creator, to: plan.orchestrator };
|
|
55
63
|
const transactions: LifecycleTransaction[] = [];
|
|
56
64
|
if (mode === "atomic") {
|
|
57
|
-
transactions.push(
|
|
65
|
+
transactions.push(buildLifecycleCommand(plan, { kind: "atomic" }, ufg));
|
|
58
66
|
} else {
|
|
59
67
|
const batches = preparationBatches ?? [plan.markets.length];
|
|
60
68
|
if (batches.length === 0 || batches.some((count) => !Number.isSafeInteger(count) || count <= 0) || batches.reduce((total, count) => total + count, 0) !== plan.markets.length) throw new LifecyclePlanningError("INVALID_PREPARATION_BATCHES", "Positive ordered preparation counts must cover every market exactly");
|
|
61
|
-
transactions.push(
|
|
69
|
+
transactions.push(buildLifecycleCommand(plan, { kind: "begin" }, ufg));
|
|
62
70
|
let first = 0;
|
|
63
71
|
for (const count of batches) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
first = completed;
|
|
72
|
+
transactions.push(buildLifecycleCommand(plan, { kind: "prepare", first, count }, ufg));
|
|
73
|
+
first += count;
|
|
67
74
|
}
|
|
68
|
-
transactions.push(
|
|
75
|
+
transactions.push(buildLifecycleCommand(plan, { kind: "activate" }, ufg));
|
|
69
76
|
}
|
|
70
77
|
return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1]!.id] }));
|
|
71
78
|
}
|
|
79
|
+
|
|
80
|
+
/** Pure unsigned core commands, with no account nonce assigned by the SDK. */
|
|
81
|
+
export function buildLaunchTransactions(options: BuildLaunchTransactionsOptions): LifecycleTransaction[] {
|
|
82
|
+
return buildLifecycleCommands(options);
|
|
83
|
+
}
|