@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 } from "viem";
|
|
2
2
|
import { fixedFeePoolHookV1Abi, launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
|
|
3
3
|
import { hashLaunchIdentity, hashLaunchPlan, LifecyclePhase } 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 } from "./rpc.js";
|
|
5
5
|
import { LifecyclePlanningError } from "./types.js";
|
|
6
6
|
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion } from "./markets.js";
|
|
7
7
|
import { hmmV2ForwardingReserve } from "./presets.js";
|
|
@@ -61,7 +61,6 @@ function executionFailure(returnData, error) {
|
|
|
61
61
|
return { ...diagnostic, failureCategory: nativeGasCapped ? "source" : "capacity", error: nativeGasCapped ? "Native execution ran out of gas under the reported RPC gas cap" : "Native execution ran out of gas" };
|
|
62
62
|
return { ...diagnostic, failureCategory: "opaque", error: nativeErrorKind === "execution-reverted" ? "Native execution reverted without a reviewed error" : "Execution failed without a reviewed error" };
|
|
63
63
|
}
|
|
64
|
-
const nitroProofsByInvocation = new WeakMap();
|
|
65
64
|
function conditionRequest(condition, planned) {
|
|
66
65
|
if (condition.kind === "allowance")
|
|
67
66
|
return {
|
|
@@ -118,8 +117,6 @@ async function proveNitroBackend(client, planned, block, limits, gasPrice) {
|
|
|
118
117
|
throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Simulated ArbOS version/compute getters differ from the exact pinned chain context");
|
|
119
118
|
}
|
|
120
119
|
}
|
|
121
|
-
// The enclosing simulation rechecks chain and canonical hash after its proofs
|
|
122
|
-
// (including every refusal path); a separate probe-only recheck is redundant.
|
|
123
120
|
}
|
|
124
121
|
async function simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, validation) {
|
|
125
122
|
// Natural child-block fees must govern fee-bearing admission; overriding
|
|
@@ -129,7 +126,7 @@ async function simulateRpcPass(client, planned, block, transactions, gasLimits,
|
|
|
129
126
|
// parent gasLimit are set.
|
|
130
127
|
const blockStateCalls = transactions.map((transaction, index) => ({
|
|
131
128
|
blockOverrides: { number: toHex(block.number + BigInt(index + 1)), time: toHex(block.timestamp + BigInt(index + 1)), gasLimit: toHex(block.gasLimit) },
|
|
132
|
-
calls: [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gasLimits[index] ?? limits.executionGasCeiling), gasPrice: toHex(gasPrice) }],
|
|
129
|
+
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) }) }],
|
|
133
130
|
}));
|
|
134
131
|
const result = await lifecycleRpc(client, "eth_simulateV1", [{ blockStateCalls, validation, traceTransfers: false, returnFullTransactions: false }, toHex(block.number)]);
|
|
135
132
|
if (!Array.isArray(result) || result.length !== transactions.length)
|
|
@@ -199,10 +196,6 @@ async function simulateRpcPass(client, planned, block, transactions, gasLimits,
|
|
|
199
196
|
}
|
|
200
197
|
return steps;
|
|
201
198
|
}
|
|
202
|
-
/** A decoded semantic refusal or opaque deployment wrapper is never gas evidence. */
|
|
203
|
-
function looksGasExhausted(step, gasLimit) {
|
|
204
|
-
return !step.success && step.failureCategory === "capacity" && step.gasUsed !== undefined && step.gasUsed >= gasLimit - gasLimit / 64n;
|
|
205
|
-
}
|
|
206
199
|
function simulationFailureKind(failure) {
|
|
207
200
|
for (let depth = 0; depth < 8 && failure !== null && typeof failure === "object"; depth += 1) {
|
|
208
201
|
const error = failure;
|
|
@@ -223,6 +216,19 @@ function simulationFailureKind(failure) {
|
|
|
223
216
|
}
|
|
224
217
|
return "source";
|
|
225
218
|
}
|
|
219
|
+
function upfrontFundsFailure(failure) {
|
|
220
|
+
for (let depth = 0; depth < 8 && failure !== null && typeof failure === "object"; depth += 1) {
|
|
221
|
+
const error = failure;
|
|
222
|
+
const status = error.status ?? error.statusCode;
|
|
223
|
+
if (error.name === "AbortError" || typeof status === "number" && status >= 400 && status <= 599 || error.code === 3)
|
|
224
|
+
return false;
|
|
225
|
+
const message = typeof error.details === "string" ? error.details : typeof error.message === "string" ? error.message : "";
|
|
226
|
+
if (error.code === -38014 || (error.code === undefined || error.code === -32000) && /^insufficient funds\b/i.test(message))
|
|
227
|
+
return true;
|
|
228
|
+
failure = error.cause;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
226
232
|
async function waitForkReceipt(client, transactionHash, timeoutMs) {
|
|
227
233
|
const deadline = Date.now() + timeoutMs;
|
|
228
234
|
while (Date.now() <= deadline) {
|
|
@@ -269,13 +275,13 @@ async function simulateForkPass(fork, planned, block, transactions, gasLimits, g
|
|
|
269
275
|
throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing fork transaction/gas context");
|
|
270
276
|
try {
|
|
271
277
|
if (measure) {
|
|
272
|
-
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");
|
|
278
|
+
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");
|
|
273
279
|
gasRequired = estimated;
|
|
274
280
|
const buffered = (estimated * BigInt(10000 + headroomBps) + 9999n) / 10000n;
|
|
275
281
|
if (buffered < gas)
|
|
276
282
|
gas = buffered;
|
|
277
283
|
}
|
|
278
|
-
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");
|
|
284
|
+
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");
|
|
279
285
|
const receipt = await waitForkReceipt(fork.client, hash, fork.receiptTimeoutMs ?? 10_000);
|
|
280
286
|
const gasUsed = rpcQuantity(receipt.gasUsed, "fork gas used");
|
|
281
287
|
if (rpcQuantity(receipt.status, "fork transaction status") !== 1n) {
|
|
@@ -358,7 +364,7 @@ function observedFields(value) {
|
|
|
358
364
|
}
|
|
359
365
|
/** Internal continuation owned by one SDK read invocation. Only its positive
|
|
360
366
|
* capability proof is reused; every candidate executes its entire economic sequence. */
|
|
361
|
-
export function prepareLaunchSimulation(options, block, hasTransactions = true, reviewedEnvelope) {
|
|
367
|
+
export function prepareLaunchSimulation(options, block, hasTransactions = true, reviewedEnvelope, selectedGasPrice, sizing) {
|
|
362
368
|
const { client, planned, fork } = options;
|
|
363
369
|
const signal = options.signal;
|
|
364
370
|
if (signal?.aborted)
|
|
@@ -401,8 +407,8 @@ export function prepareLaunchSimulation(options, block, hasTransactions = true,
|
|
|
401
407
|
return lifecycleStage(client, "simulation.context", () => Promise.all([
|
|
402
408
|
resolveLifecycleLimits(context, policy),
|
|
403
409
|
readLifecycleChainId(client),
|
|
404
|
-
lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block),
|
|
405
|
-
hasTransactions && reviewedEnvelope === undefined ?
|
|
410
|
+
lifecycleSessionRead(client, `${planned.chainId}:account-code:${context.account.toLowerCase()}`, () => lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block)),
|
|
411
|
+
hasTransactions && reviewedEnvelope === undefined ? selectedGasPrice ?? readLifecycleGasPrice(client) : undefined,
|
|
406
412
|
]));
|
|
407
413
|
})().then((observations) => ({ observations }), (failure) => ({ failure }));
|
|
408
414
|
// Refused inputs may discard this read-only preparation before it settles.
|
|
@@ -442,21 +448,8 @@ export function prepareLaunchSimulation(options, block, hasTransactions = true,
|
|
|
442
448
|
if (capabilityProved)
|
|
443
449
|
return;
|
|
444
450
|
if (capabilityPending === undefined) {
|
|
445
|
-
const key =
|
|
446
|
-
|
|
447
|
-
if (reusable && proofs === undefined) {
|
|
448
|
-
proofs = new Map();
|
|
449
|
-
nitroProofsByInvocation.set(client, proofs);
|
|
450
|
-
}
|
|
451
|
-
const ownedProofs = proofs;
|
|
452
|
-
let nativeProof = ownedProofs?.get(key);
|
|
453
|
-
if (nativeProof === undefined) {
|
|
454
|
-
nativeProof = lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, context.block, limits, gasPrice));
|
|
455
|
-
ownedProofs?.set(key, nativeProof);
|
|
456
|
-
const pending = nativeProof;
|
|
457
|
-
void pending.catch(() => { if (ownedProofs?.get(key) === pending)
|
|
458
|
-
ownedProofs?.delete(key); });
|
|
459
|
-
}
|
|
451
|
+
const key = `${context.chainId}:nitro-backend`;
|
|
452
|
+
const nativeProof = lifecycleSessionRead(client, key, () => lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, context.block, limits, gasPrice)));
|
|
460
453
|
const proof = nativeProof.then(() => {
|
|
461
454
|
assertContext();
|
|
462
455
|
result.observations[0].assertCurrent();
|
|
@@ -470,8 +463,9 @@ export function prepareLaunchSimulation(options, block, hasTransactions = true,
|
|
|
470
463
|
await capabilityPending;
|
|
471
464
|
assertCurrent();
|
|
472
465
|
};
|
|
473
|
-
const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope);
|
|
466
|
+
const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, selectedGasPrice, sizing);
|
|
474
467
|
assertCurrent();
|
|
468
|
+
registerLifecycleFinalCheck(client, result.observations[0].assertCurrent);
|
|
475
469
|
return simulation;
|
|
476
470
|
}
|
|
477
471
|
catch (failure) {
|
|
@@ -488,12 +482,12 @@ export function prepareLaunchSimulation(options, block, hasTransactions = true,
|
|
|
488
482
|
close() { closed = true; capabilityProved = false; capabilityPending = undefined; } });
|
|
489
483
|
return simulate;
|
|
490
484
|
}
|
|
491
|
-
export async function simulateLaunchTransactions(options, transactions, pinnedBlock, nextEnvelope) {
|
|
485
|
+
export async function simulateLaunchTransactions(options, transactions, pinnedBlock, nextEnvelope, sizing) {
|
|
492
486
|
// Standalone calls always produce fresh context, including live chain and fees.
|
|
493
487
|
if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === lifecycleSourceClient(options.client))
|
|
494
488
|
throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
|
|
495
489
|
const block = pinnedBlock ?? await readLifecycleBlock(options.client);
|
|
496
|
-
const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope);
|
|
490
|
+
const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope, undefined, sizing);
|
|
497
491
|
try {
|
|
498
492
|
return await prepared(transactions, nextEnvelope);
|
|
499
493
|
}
|
|
@@ -501,7 +495,7 @@ export async function simulateLaunchTransactions(options, transactions, pinnedBl
|
|
|
501
495
|
prepared.close();
|
|
502
496
|
}
|
|
503
497
|
}
|
|
504
|
-
async function simulateLaunchTransactionsWithContext(options, transactions, context, observations, proveBackend, assertCurrent, diagnostic, nextEnvelope) {
|
|
498
|
+
async function simulateLaunchTransactionsWithContext(options, transactions, context, observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, selectedGasPrice, sizing) {
|
|
505
499
|
const { client, planned } = options;
|
|
506
500
|
const sourceClient = lifecycleSourceClient(client);
|
|
507
501
|
const { block } = context;
|
|
@@ -511,19 +505,16 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
|
|
|
511
505
|
throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
|
|
512
506
|
const accountCode = rpcHex(accountCodeValue, "account code");
|
|
513
507
|
if (accountCode !== "0x") {
|
|
514
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
515
508
|
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
|
|
516
509
|
}
|
|
517
510
|
for (const transaction of transactions) {
|
|
518
511
|
if (transaction.from.toLowerCase() !== planned.account.toLowerCase() || BigInt(transaction.chainId) !== planned.chainId)
|
|
519
512
|
throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Simulation transaction differs from the committed chain/account");
|
|
520
513
|
if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) {
|
|
521
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
522
514
|
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" };
|
|
523
515
|
}
|
|
524
516
|
}
|
|
525
517
|
if (transactions.length === 0) {
|
|
526
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
527
518
|
return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] };
|
|
528
519
|
}
|
|
529
520
|
if (nextEnvelope !== undefined && (transactions.length !== 1 || typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas <= 0n || nextEnvelope.gas >= 1n << 64n ||
|
|
@@ -552,15 +543,14 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
|
|
|
552
543
|
const reason = exceedsCompute
|
|
553
544
|
? `${atomic ? "Atomic launch" : "Immediate transaction"} needs about ${gasInMillions(estimatedGas)}M gas, above the ${gasInMillions(computeCap)}M per-transaction limit${atomic ? "; use staged" : ""}`
|
|
554
545
|
: "The immediate transaction estimate exceeds the RPC aggregate simulation gas cap; no prefix can be proved";
|
|
555
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
556
546
|
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: exceedsCompute ? "compute" : "simulation-gas", reason };
|
|
557
547
|
}
|
|
558
548
|
}
|
|
559
|
-
const recommendedGasPrice = nextEnvelope === undefined ? rpcQuantity(gasPriceValue, "gas price") : nextEnvelope.gasPrice;
|
|
549
|
+
const recommendedGasPrice = nextEnvelope === undefined ? typeof gasPriceValue === "bigint" ? gasPriceValue : rpcQuantity(gasPriceValue, "gas price") : nextEnvelope.gasPrice;
|
|
560
550
|
// New recommendations cover lagging quotes; reviewed fees are replayed unchanged
|
|
561
551
|
// under natural child fees, never refreshed or silently increased.
|
|
562
552
|
const baseFeeEnvelope = (block.baseFeePerGas ?? 0n) * 2n;
|
|
563
|
-
const gasPrice = nextEnvelope?.gasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
|
|
553
|
+
const gasPrice = nextEnvelope?.gasPrice ?? selectedGasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
|
|
564
554
|
if (gasPrice >= 1n << 256n)
|
|
565
555
|
throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
|
|
566
556
|
const buffer = (gas) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
|
|
@@ -569,7 +559,7 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
|
|
|
569
559
|
try {
|
|
570
560
|
const [, estimates] = await Promise.all([
|
|
571
561
|
proveBackend(limits, gasPrice),
|
|
572
|
-
nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster",
|
|
562
|
+
!diagnostic && nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster", async () => [buffer((await readNitroPosterGas(transactions[0], context)).posterGas)]) : [],
|
|
573
563
|
]);
|
|
574
564
|
posterGas.push(...estimates);
|
|
575
565
|
}
|
|
@@ -577,37 +567,81 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
|
|
|
577
567
|
if (simulationFailureKind(failure) === "abort")
|
|
578
568
|
throw failure;
|
|
579
569
|
assertCurrent();
|
|
580
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
581
570
|
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.` };
|
|
582
571
|
}
|
|
583
572
|
}
|
|
584
573
|
assertCurrent();
|
|
585
|
-
const discoveryGas = transactions.map((
|
|
586
|
-
const total = limits.
|
|
574
|
+
const discoveryGas = transactions.map(() => {
|
|
575
|
+
const total = limits.protocol === "nitro" ? limits.maxSimulationGas ?? block.gasLimit : limits.executionGasCeiling;
|
|
587
576
|
const capped = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < total ? limits.transactionGasCeiling : total;
|
|
588
577
|
if (capped <= 0n || capped >= 1n << 64n)
|
|
589
578
|
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Compute/poster envelope exceeds uint64 gas");
|
|
590
579
|
return capped;
|
|
591
580
|
});
|
|
592
|
-
|
|
581
|
+
if (discoveryGas[0] * gasPrice + transactions[0].value >= 1n << 256n)
|
|
582
|
+
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
|
|
583
|
+
const balance = rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance");
|
|
593
584
|
let measured = [];
|
|
594
|
-
|
|
585
|
+
const hasSizing = !diagnostic && transactions.every((transaction) => sizing?.has(transaction.id));
|
|
586
|
+
let backend = (nextEnvelope !== undefined || hasSizing) && limits.protocol === "evm" && options.fork !== undefined ? "controlled-fork" : "eth_simulateV1";
|
|
587
|
+
if (nextEnvelope === undefined && !hasSizing) {
|
|
588
|
+
// Nitro checks upfront funds even with validation:false. Bound discovery
|
|
589
|
+
// budgets before dispatch, without overriding the payer's real balance.
|
|
590
|
+
const value = transactions.reduce((total, transaction) => total + transaction.value, 0n);
|
|
591
|
+
let available = balance - value;
|
|
592
|
+
if (gasPrice !== 0n)
|
|
593
|
+
available /= gasPrice;
|
|
594
|
+
const sizingGas = discoveryGas.map((ceiling, index) => {
|
|
595
|
+
// Upfront reservation is per call; unused gas is refunded before the next.
|
|
596
|
+
const computeAndPoster = limits.executionGasCeiling + (posterGas[index] ?? 0n);
|
|
597
|
+
const capped = computeAndPoster < ceiling ? computeAndPoster : ceiling;
|
|
598
|
+
return gasPrice === 0n || capped < available ? capped : available;
|
|
599
|
+
});
|
|
600
|
+
let retriedForFunds = false;
|
|
601
|
+
if (balance < value || sizingGas.some((gas) => gas <= 0n))
|
|
602
|
+
return { ...base, executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "affordability", reason: "Creator native balance cannot fund transaction value and sizing fee budgets" };
|
|
603
|
+
assertCurrent();
|
|
595
604
|
try {
|
|
596
|
-
|
|
605
|
+
try {
|
|
606
|
+
measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, sizingGas, limits, gasPrice, false));
|
|
607
|
+
}
|
|
608
|
+
catch (failure) {
|
|
609
|
+
if (!upfrontFundsFailure(failure))
|
|
610
|
+
throw failure;
|
|
611
|
+
assertCurrent();
|
|
612
|
+
retriedForFunds = true;
|
|
613
|
+
let remaining = available;
|
|
614
|
+
const sharedGas = sizingGas.map((ceiling, index) => {
|
|
615
|
+
if (gasPrice === 0n)
|
|
616
|
+
return ceiling;
|
|
617
|
+
const share = remaining / BigInt(transactions.length - index);
|
|
618
|
+
const gas = ceiling < share ? ceiling : share;
|
|
619
|
+
remaining -= gas;
|
|
620
|
+
return gas;
|
|
621
|
+
});
|
|
622
|
+
measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, sharedGas, limits, gasPrice, false));
|
|
623
|
+
}
|
|
597
624
|
}
|
|
598
625
|
catch (failure) {
|
|
599
|
-
if (
|
|
626
|
+
if (upfrontFundsFailure(failure))
|
|
627
|
+
return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "affordability", reason: "Native balance cannot cover the sequential sizing fee envelopes" };
|
|
628
|
+
const kind = simulationFailureKind(failure);
|
|
629
|
+
if (kind === "abort")
|
|
600
630
|
throw failure;
|
|
601
631
|
assertCurrent();
|
|
602
|
-
if (options.fork === undefined || limits.protocol === "nitro") {
|
|
603
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
632
|
+
if (options.fork === undefined || limits.protocol === "nitro" || kind !== "source") {
|
|
604
633
|
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."}` };
|
|
605
634
|
}
|
|
606
635
|
backend = "controlled-fork";
|
|
607
|
-
measured = await simulateForkPass(options.fork, planned, block, transactions,
|
|
636
|
+
measured = await simulateForkPass(options.fork, planned, block, transactions, sizingGas, gasPrice, true, limits.headroomBps);
|
|
608
637
|
}
|
|
609
638
|
assertCurrent();
|
|
610
639
|
const failedIndex = measured.findIndex((step) => !step.success);
|
|
640
|
+
const refusal = measured[failedIndex];
|
|
641
|
+
if (retriedForFunds && refusal?.failureCategory === "capacity" && refusal.gasLimit !== undefined && refusal.gasLimit < limits.executionGasCeiling) {
|
|
642
|
+
return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: measured,
|
|
643
|
+
failedTransactionId: refusal.transactionId, failureCategory: "affordability", reason: "Native balance does not provide the sequential sizing margin; an exact transaction envelope was not proved" };
|
|
644
|
+
}
|
|
611
645
|
if (failedIndex >= 0 && limits.maxSimulationGas !== undefined &&
|
|
612
646
|
measured.reduce((gas, step) => gas + (step.gasUsed ?? 0n), 0n) * 100n >= limits.maxSimulationGas * 95n) {
|
|
613
647
|
// The provider exhausted its aggregate used-gas budget, not the contract.
|
|
@@ -616,100 +650,89 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
|
|
|
616
650
|
transactions = transactions.slice(0, failedIndex);
|
|
617
651
|
base.deferredTransactionIds = allTransactions.slice(failedIndex).map((transaction) => transaction.id);
|
|
618
652
|
if (transactions.length === 0) {
|
|
619
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
620
653
|
return { ...base, confidence: "provisional", executionProof: "unavailable", backend, admitted: false, steps: [], failureCategory: "source", reason: "RPC aggregate simulation gas cap left no successful prefix to prove" };
|
|
621
654
|
}
|
|
622
655
|
}
|
|
623
656
|
const failed = measured.find((step) => !step.success);
|
|
624
|
-
if (failed !== undefined)
|
|
625
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
657
|
+
if (diagnostic && failed !== undefined)
|
|
626
658
|
return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId, failureCategory: failed.failureCategory ?? "opaque", capacityConstraint: failed.failureCategory === "capacity" ? "compute" : undefined };
|
|
627
|
-
}
|
|
628
659
|
if (diagnostic) {
|
|
629
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
630
660
|
return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
|
|
631
661
|
}
|
|
632
662
|
}
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
663
|
+
const sizedTransactions = measured.length === 0 ? transactions : transactions.slice(0, measured.length);
|
|
664
|
+
const minimumGas = sizedTransactions.map((transaction, index) => {
|
|
665
|
+
return nextEnvelope !== undefined ? 0n : hasSizing ? sizing.get(transaction.id) : measured[index].gasRequired ?? measured[index].gasUsed;
|
|
666
|
+
});
|
|
667
|
+
const gasLimits = nextEnvelope === undefined ? sizedTransactions.map((transaction, index) => {
|
|
668
|
+
// Suffix budgets prove feasibility, not a future transaction's fee estimate.
|
|
669
|
+
if (index > 0)
|
|
670
|
+
return discoveryGas[index];
|
|
671
|
+
const gross = minimumGas[index];
|
|
672
|
+
const reserve = transaction.kind === "activate" || transaction.kind === "atomic" ? hmmV2ForwardingReserve(planned.plan) : 0n;
|
|
673
|
+
const buffered = gross + reserve > buffer(gross) ? gross + reserve : buffer(gross);
|
|
674
|
+
const desired = buffered + (posterGas[0] ?? 0n);
|
|
675
|
+
return desired < discoveryGas[index] ? desired : discoveryGas[index];
|
|
639
676
|
}) : [nextEnvelope.gas];
|
|
677
|
+
if (gasLimits[0] * gasPrice + transactions[0].value >= 1n << 256n)
|
|
678
|
+
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
|
|
679
|
+
const validate = nextEnvelope !== undefined || measured.every((step) => step.success);
|
|
680
|
+
if (nextEnvelope === undefined) {
|
|
681
|
+
const value = transactions.reduce((total, transaction) => total + transaction.value, 0n);
|
|
682
|
+
let available = balance - value;
|
|
683
|
+
let suffixMinimum = minimumGas.slice(1).reduce((total, gas) => total + gas, 0n);
|
|
684
|
+
if (gasPrice !== 0n)
|
|
685
|
+
available /= gasPrice;
|
|
686
|
+
for (let index = 0; index < gasLimits.length && gasPrice !== 0n; index += 1) {
|
|
687
|
+
const minimum = minimumGas[index];
|
|
688
|
+
if (index > 0)
|
|
689
|
+
suffixMinimum -= minimum;
|
|
690
|
+
const budget = index === 0 ? available - suffixMinimum : minimum + (available - minimum - suffixMinimum) / BigInt(gasLimits.length - index);
|
|
691
|
+
if (gasLimits[index] > budget)
|
|
692
|
+
gasLimits[index] = budget;
|
|
693
|
+
available -= gasLimits[index];
|
|
694
|
+
}
|
|
695
|
+
if (balance < value || gasLimits.some((gas, index) => gas <= 0n && minimumGas[index] > 0n))
|
|
696
|
+
return { ...base, executionProof: "unavailable", backend, admitted: false, steps: measured, failureCategory: "affordability", reason: "Creator native balance cannot fund transaction value and execution fee envelopes" };
|
|
697
|
+
}
|
|
640
698
|
const exceeds = gasLimits.findIndex((gas, index) => gas <= 0n || gas >= 1n << 64n || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling ||
|
|
641
|
-
nextEnvelope === undefined && (measured[index].gasRequired ?? measured[index].gasUsed) > gas
|
|
699
|
+
nextEnvelope === undefined && !hasSizing && (measured[index].gasRequired ?? measured[index].gasUsed) > gas ||
|
|
642
700
|
limits.protocol === "evm" && gas > limits.executionGasCeiling);
|
|
643
701
|
if (exceeds >= 0) {
|
|
644
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
645
702
|
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 };
|
|
646
703
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
let
|
|
650
|
-
|
|
651
|
-
? simulateRpcPass(client, planned, block, transactions, gas, limits, gasPrice, true)
|
|
652
|
-
: simulateForkPass(options.fork, planned, block, transactions, gas, gasPrice, false, limits.headroomBps);
|
|
653
|
-
try {
|
|
654
|
-
verifiedSteps = await lifecycleStage(client, "simulation.replay", () => replay(gasLimits));
|
|
655
|
-
}
|
|
656
|
-
catch (failure) {
|
|
657
|
-
const kind = simulationFailureKind(failure);
|
|
658
|
-
if (kind === "abort")
|
|
659
|
-
throw failure;
|
|
660
|
-
assertCurrent();
|
|
661
|
-
if (kind !== "out-of-gas" || backend !== "eth_simulateV1" || nextEnvelope !== undefined) {
|
|
662
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
663
|
-
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)}` };
|
|
664
|
-
}
|
|
665
|
-
replayError = lifecycleFailureReason(failure);
|
|
666
|
-
}
|
|
667
|
-
assertCurrent();
|
|
668
|
-
let finalFailure = verifiedSteps.find((step) => !step.success);
|
|
669
|
-
const failedIndex = verifiedSteps.findIndex((step) => !step.success);
|
|
670
|
-
const rescue = nextEnvelope === undefined && backend === "eth_simulateV1" &&
|
|
671
|
-
(replayError !== undefined && discoveryGas.some((gas, index) => index < transactions.length && gas > gasLimits[index]) ||
|
|
672
|
-
finalFailure !== undefined && looksGasExhausted(finalFailure, gasLimits[failedIndex]) && discoveryGas[failedIndex] > gasLimits[failedIndex]);
|
|
673
|
-
// One legal-envelope rescue covers thrown/returned genuine OOG. Never retry
|
|
674
|
-
// semantic errors, switch replay backends, or enlarge a held immediate envelope.
|
|
675
|
-
if (rescue) {
|
|
676
|
-
assertCurrent();
|
|
704
|
+
// Size first, then prove and return this exact executable envelope. Diagnostic
|
|
705
|
+
// gas budgets and caller-held proposals are never execution admission.
|
|
706
|
+
let verifiedSteps = measured;
|
|
707
|
+
if (validate) {
|
|
677
708
|
try {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
replayError = undefined;
|
|
709
|
+
verifiedSteps = await lifecycleStage(client, "simulation.measure", () => backend === "eth_simulateV1"
|
|
710
|
+
? simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true)
|
|
711
|
+
: simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice, false, limits.headroomBps));
|
|
682
712
|
}
|
|
683
713
|
catch (failure) {
|
|
684
|
-
|
|
685
|
-
if (kind === "abort")
|
|
714
|
+
if (simulationFailureKind(failure) === "abort")
|
|
686
715
|
throw failure;
|
|
687
716
|
assertCurrent();
|
|
688
|
-
|
|
689
|
-
if (kind !== "out-of-gas")
|
|
690
|
-
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)}` };
|
|
691
|
-
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${lifecycleFailureReason(failure)}` };
|
|
717
|
+
return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [], reason: `Exact transaction simulation unavailable: ${lifecycleFailureReason(failure)}` };
|
|
692
718
|
}
|
|
693
719
|
}
|
|
694
|
-
if (replayError !== undefined) {
|
|
695
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
696
|
-
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling failed: ${replayError}` };
|
|
697
|
-
}
|
|
698
720
|
assertCurrent();
|
|
721
|
+
const finalFailure = verifiedSteps.find((step) => !step.success);
|
|
699
722
|
const estimates = [];
|
|
700
|
-
|
|
701
|
-
// fee observations without inventing a balance or doing redundant payer work.
|
|
702
|
-
let remainingBalance = finalFailure === undefined
|
|
703
|
-
? rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance")
|
|
704
|
-
: undefined;
|
|
723
|
+
let remainingBalance = finalFailure === undefined ? balance : undefined;
|
|
705
724
|
let affordabilityFailure;
|
|
706
725
|
const feeContext = configuration.estimateDataFee === undefined || sourceClient === client ? context : { ...context, client: sourceClient };
|
|
707
726
|
for (let index = 0; index < verifiedSteps.length; index += 1) {
|
|
708
727
|
const step = verifiedSteps[index];
|
|
709
728
|
const transaction = transactions[index];
|
|
710
|
-
const gasLimit =
|
|
729
|
+
const gasLimit = gasLimits[index];
|
|
711
730
|
if (step === undefined || transaction === undefined || gasLimit === undefined)
|
|
712
731
|
throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing verified fee context");
|
|
732
|
+
if (index > 0) {
|
|
733
|
+
estimates.push({ ...step, computeGasRequired: minimumGas[index], provisional: true });
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
713
736
|
const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit, gasPrice }, feeContext);
|
|
714
737
|
if (dataFee !== undefined && (typeof dataFee !== "bigint" || dataFee < 0n || dataFee >= 1n << 256n))
|
|
715
738
|
throw new LifecyclePlanningError("INVALID_DATA_FEE", "Data-fee oracle must return a nonnegative uint256 bigint amount");
|
|
@@ -724,9 +747,8 @@ async function simulateLaunchTransactionsWithContext(options, transactions, cont
|
|
|
724
747
|
remainingBalance -= required;
|
|
725
748
|
}
|
|
726
749
|
const poster = posterGas[index];
|
|
727
|
-
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" } });
|
|
750
|
+
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" } });
|
|
728
751
|
}
|
|
729
|
-
await assertLifecycleBlock(client, block, planned.chainId);
|
|
730
752
|
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 };
|
|
731
753
|
}
|
|
732
754
|
//# sourceMappingURL=simulation.js.map
|