@black-market/sdk 0.14.1 → 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/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 +3 -0
- package/dist/lifecycle/presets.d.ts.map +1 -1
- package/dist/lifecycle/presets.js +3 -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 +67 -78
- package/docs/lifecycle-gas-profiles.md +90 -0
- package/docs/reference.md +43 -45
- package/examples/README.md +11 -8
- 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/opening-buys.ts +157 -0
- package/src/lifecycle/planner.ts +164 -151
- package/src/lifecycle/presets.ts +5 -0
- 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 } from "viem";
|
|
2
2
|
import { fixedFeePoolHookV1Abi, launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
|
|
3
3
|
import { hashLaunchIdentity, hashLaunchPlan, LifecyclePhase } 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 } from "./rpc.js";
|
|
5
5
|
import { LifecyclePlanningError } 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
|
function committedPositionCount(plan) {
|
|
11
9
|
let count = 0;
|
|
@@ -85,12 +83,10 @@ function checkCondition(condition, data, planned) {
|
|
|
85
83
|
/** Native Nitro eth_simulateV1 executes commit-mode ArbOS charging, including its
|
|
86
84
|
* compute hold after the actual poster charge. A generic EVM/fork is not this proof. */
|
|
87
85
|
async function proveNitroBackend(client, planned, block, limits, gasPrice) {
|
|
88
|
-
if (limits.arbOSVersion === undefined
|
|
89
|
-
throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Pinned Nitro
|
|
86
|
+
if (limits.arbOSVersion === undefined)
|
|
87
|
+
throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Pinned Nitro version is unavailable");
|
|
90
88
|
const probes = [
|
|
91
89
|
{ to: nitroArbSys, data: encodeFunctionData({ abi: nitroArbSysAbi, functionName: "arbOSVersion" }), expected: limits.arbOSVersion + 55n },
|
|
92
|
-
{ to: nitroGasInfo, data: encodeFunctionData({ abi: nitroGasInfoAbi, functionName: "getMaxTxGasLimit" }), expected: limits.maxTxComputeGas },
|
|
93
|
-
{ to: nitroGasInfo, data: encodeFunctionData({ abi: nitroGasInfoAbi, functionName: "getMaxBlockGasLimit" }), expected: limits.maxBlockComputeGas },
|
|
94
90
|
];
|
|
95
91
|
const gas = limits.executionGasCeiling < 100000n ? limits.executionGasCeiling : 100000n;
|
|
96
92
|
if (limits.maxSimulationGas !== undefined && gas * BigInt(probes.length) > limits.maxSimulationGas)
|
|
@@ -109,12 +105,12 @@ async function proveNitroBackend(client, planned, block, limits, gasPrice) {
|
|
|
109
105
|
throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Native simulation omitted the pinned ArbOS metering probe");
|
|
110
106
|
const simulated = rpcObject(result[0], "Nitro metering probe block");
|
|
111
107
|
if (!Array.isArray(simulated.calls) || simulated.calls.length !== probes.length)
|
|
112
|
-
throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Native simulation omitted ArbOS version
|
|
108
|
+
throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Native simulation omitted the ArbOS version");
|
|
113
109
|
for (let index = 0; index < probes.length; index += 1) {
|
|
114
110
|
const call = rpcObject(simulated.calls[index], "Nitro metering probe");
|
|
115
111
|
const data = rpcHex(call.returnData, "simulated Nitro metering result");
|
|
116
112
|
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) {
|
|
117
|
-
throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Simulated ArbOS version
|
|
113
|
+
throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Simulated ArbOS version differs from the exact pinned chain context");
|
|
118
114
|
}
|
|
119
115
|
}
|
|
120
116
|
}
|
|
@@ -126,7 +122,7 @@ async function simulateRpcPass(client, planned, block, transactions, gasLimits,
|
|
|
126
122
|
// parent gasLimit are set.
|
|
127
123
|
const blockStateCalls = transactions.map((transaction, index) => ({
|
|
128
124
|
blockOverrides: { number: toHex(block.number + BigInt(index + 1)), time: toHex(block.timestamp + BigInt(index + 1)), gasLimit: toHex(block.gasLimit) },
|
|
129
|
-
calls: [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gasLimits[index] ?? limits.executionGasCeiling), gasPrice: toHex(gasPrice)
|
|
125
|
+
calls: [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gasLimits[index] ?? limits.executionGasCeiling), gasPrice: toHex(gasPrice) }],
|
|
130
126
|
}));
|
|
131
127
|
const result = await lifecycleRpc(client, "eth_simulateV1", [{ blockStateCalls, validation, traceTransfers: false, returnFullTransactions: false }, toHex(block.number)]);
|
|
132
128
|
if (!Array.isArray(result) || result.length !== transactions.length)
|
|
@@ -239,7 +235,7 @@ async function waitForkReceipt(client, transactionHash, timeoutMs) {
|
|
|
239
235
|
}
|
|
240
236
|
throw new LifecyclePlanningError("FORK_RECEIPT_TIMEOUT", "The disposable fork did not mine its simulation transaction");
|
|
241
237
|
}
|
|
242
|
-
async function simulateForkPass(fork, planned, block, transactions, gasLimits, gasPrice
|
|
238
|
+
async function simulateForkPass(fork, planned, block, transactions, gasLimits, gasPrice) {
|
|
243
239
|
if (fork.isolation !== "disposable" || fork.allowTransactions !== true || !fork.sourceRpcUrl || !fork.forkRpcUrl)
|
|
244
240
|
throw new LifecyclePlanningError("UNSAFE_FORK", "Actual simulation transactions require an explicitly distinct disposable controlled fork");
|
|
245
241
|
const sourceUrl = new URL(fork.sourceRpcUrl);
|
|
@@ -256,7 +252,7 @@ async function simulateForkPass(fork, planned, block, transactions, gasLimits, g
|
|
|
256
252
|
await lifecycleRpc(fork.client, fork.resetMethod ?? "anvil_reset", [{ forking: { jsonRpcUrl: fork.sourceRpcUrl, blockNumber: Number(block.number) } }]);
|
|
257
253
|
forkBlock = await readLifecycleBlock(fork.client);
|
|
258
254
|
}
|
|
259
|
-
const forkChain =
|
|
255
|
+
const forkChain = await readLifecycleChainId(fork.client);
|
|
260
256
|
if (forkChain !== planned.chainId || forkChain !== (fork.expectedChainId ?? planned.chainId) || forkBlock.hash.toLowerCase() !== block.hash.toLowerCase() || forkBlock.hash.toLowerCase() !== (fork.expectedBlockHash ?? block.hash).toLowerCase())
|
|
261
257
|
throw new LifecyclePlanningError("FORK_STATE_MISMATCH", "Controlled fork must match the exact source chain and pinned latest block before simulation");
|
|
262
258
|
const snapshot = rpcHex(await lifecycleRpc(fork.client, "evm_snapshot"), "fork snapshot");
|
|
@@ -269,19 +265,11 @@ async function simulateForkPass(fork, planned, block, transactions, gasLimits, g
|
|
|
269
265
|
}
|
|
270
266
|
for (let index = 0; index < transactions.length; index += 1) {
|
|
271
267
|
const transaction = transactions[index];
|
|
272
|
-
|
|
273
|
-
let gasRequired;
|
|
268
|
+
const gas = gasLimits[index];
|
|
274
269
|
if (transaction === undefined || gas === undefined)
|
|
275
270
|
throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing fork transaction/gas context");
|
|
276
271
|
try {
|
|
277
|
-
|
|
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");
|
|
279
|
-
gasRequired = estimated;
|
|
280
|
-
const buffered = (estimated * BigInt(10000 + headroomBps) + 9999n) / 10000n;
|
|
281
|
-
if (buffered < gas)
|
|
282
|
-
gas = buffered;
|
|
283
|
-
}
|
|
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");
|
|
272
|
+
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");
|
|
285
273
|
const receipt = await waitForkReceipt(fork.client, hash, fork.receiptTimeoutMs ?? 10_000);
|
|
286
274
|
const gasUsed = rpcQuantity(receipt.gasUsed, "fork gas used");
|
|
287
275
|
if (rpcQuantity(receipt.status, "fork transaction status") !== 1n) {
|
|
@@ -337,7 +325,7 @@ async function simulateForkPass(fork, planned, block, transactions, gasLimits, g
|
|
|
337
325
|
const data = rpcHex(await lifecycleRpc(fork.client, "eth_call", [{ ...request, from: planned.account }, toHex(currentBlock.number)]), "fork postcondition");
|
|
338
326
|
checkCondition(condition, data, planned);
|
|
339
327
|
}
|
|
340
|
-
steps.push({ transactionId: transaction.id, success: true, gasUsed, gasLimit: gas,
|
|
328
|
+
steps.push({ transactionId: transaction.id, success: true, gasUsed, gasLimit: gas, returnData });
|
|
341
329
|
}
|
|
342
330
|
catch (failure) {
|
|
343
331
|
steps.push({ transactionId: transaction.id, success: false, gasLimit: gas, error: lifecycleFailureReason(failure) });
|
|
@@ -357,14 +345,14 @@ async function simulateForkPass(fork, planned, block, transactions, gasLimits, g
|
|
|
357
345
|
}
|
|
358
346
|
}
|
|
359
347
|
}
|
|
360
|
-
function observedFields(value) {
|
|
348
|
+
export function observedFields(value) {
|
|
361
349
|
const keys = Object.keys(value);
|
|
362
350
|
const values = keys.map((key) => value[key]);
|
|
363
351
|
return () => keys.length === Object.keys(value).length && keys.every((key, index) => value[key] === values[index]);
|
|
364
352
|
}
|
|
365
353
|
/** Internal continuation owned by one SDK read invocation. Only its positive
|
|
366
354
|
* capability proof is reused; every candidate executes its entire economic sequence. */
|
|
367
|
-
export function prepareLaunchSimulation(options, block, hasTransactions = true,
|
|
355
|
+
export function prepareLaunchSimulation(options, block, hasTransactions = true, immediate = false) {
|
|
368
356
|
const { client, planned, fork } = options;
|
|
369
357
|
const signal = options.signal;
|
|
370
358
|
if (signal?.aborted)
|
|
@@ -408,7 +396,7 @@ export function prepareLaunchSimulation(options, block, hasTransactions = true,
|
|
|
408
396
|
resolveLifecycleLimits(context, policy),
|
|
409
397
|
readLifecycleChainId(client),
|
|
410
398
|
lifecycleSessionRead(client, `${planned.chainId}:account-code:${context.account.toLowerCase()}`, () => lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block)),
|
|
411
|
-
hasTransactions
|
|
399
|
+
hasTransactions ? readLifecycleGasPrice(client, block, planned.chainId) : undefined,
|
|
412
400
|
]));
|
|
413
401
|
})().then((observations) => ({ observations }), (failure) => ({ failure }));
|
|
414
402
|
// Refused inputs may discard this read-only preparation before it settles.
|
|
@@ -463,7 +451,7 @@ export function prepareLaunchSimulation(options, block, hasTransactions = true,
|
|
|
463
451
|
await capabilityPending;
|
|
464
452
|
assertCurrent();
|
|
465
453
|
};
|
|
466
|
-
const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope,
|
|
454
|
+
const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, immediate);
|
|
467
455
|
assertCurrent();
|
|
468
456
|
registerLifecycleFinalCheck(client, result.observations[0].assertCurrent);
|
|
469
457
|
return simulation;
|
|
@@ -482,273 +470,136 @@ export function prepareLaunchSimulation(options, block, hasTransactions = true,
|
|
|
482
470
|
close() { closed = true; capabilityProved = false; capabilityPending = undefined; } });
|
|
483
471
|
return simulate;
|
|
484
472
|
}
|
|
485
|
-
export async function simulateLaunchTransactions(options, transactions, pinnedBlock, nextEnvelope
|
|
486
|
-
// Standalone calls
|
|
473
|
+
export async function simulateLaunchTransactions(options, transactions, pinnedBlock, nextEnvelope) {
|
|
474
|
+
// Standalone calls own one fresh block/pricing context, not fresh chain guards.
|
|
487
475
|
if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === lifecycleSourceClient(options.client))
|
|
488
476
|
throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
477
|
+
return withLifecycleReadClient(options.client, async (client) => {
|
|
478
|
+
const [block] = await Promise.all([pinnedBlock ?? readLifecycleBlock(client), readLifecycleChainId(client)]);
|
|
479
|
+
const prepared = prepareLaunchSimulation({ ...options, client }, block, transactions.length !== 0);
|
|
480
|
+
try {
|
|
481
|
+
return await prepared(transactions, nextEnvelope);
|
|
482
|
+
}
|
|
483
|
+
finally {
|
|
484
|
+
prepared.close();
|
|
485
|
+
}
|
|
486
|
+
});
|
|
497
487
|
}
|
|
498
|
-
async function simulateLaunchTransactionsWithContext(options, transactions, context, observations, proveBackend, assertCurrent, diagnostic, nextEnvelope,
|
|
499
|
-
const { client, planned } = options;
|
|
488
|
+
async function simulateLaunchTransactionsWithContext(options, transactions, context, observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, immediate = false) {
|
|
489
|
+
const { client, planned } = options, { block } = context;
|
|
500
490
|
const sourceClient = lifecycleSourceClient(client);
|
|
501
|
-
const { block } = context;
|
|
502
491
|
const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = observations;
|
|
503
|
-
const base = { confidence: "stateful", executionProof: "failed", protocolFit: "unknown",
|
|
492
|
+
const base = { confidence: "stateful", executionProof: "failed", protocolFit: "unknown",
|
|
493
|
+
blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits, deferredTransactionIds: [] };
|
|
504
494
|
if (chainId !== planned.chainId)
|
|
505
|
-
throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain
|
|
506
|
-
|
|
507
|
-
if (accountCode !== "0x") {
|
|
495
|
+
throw new LifecyclePlanningError("CHAIN_MISMATCH", "Bound RPC chain differs from the committed launch");
|
|
496
|
+
if (rpcHex(accountCodeValue, "account code") !== "0x")
|
|
508
497
|
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
|
|
509
|
-
}
|
|
510
498
|
for (const transaction of transactions) {
|
|
511
499
|
if (transaction.from.toLowerCase() !== planned.account.toLowerCase() || BigInt(transaction.chainId) !== planned.chainId)
|
|
512
500
|
throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Simulation transaction differs from the committed chain/account");
|
|
513
|
-
if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes)
|
|
501
|
+
if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes)
|
|
514
502
|
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" };
|
|
515
|
-
}
|
|
516
503
|
}
|
|
517
|
-
if (transactions.length === 0)
|
|
504
|
+
if (transactions.length === 0)
|
|
518
505
|
return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] };
|
|
519
|
-
|
|
520
|
-
if (nextEnvelope !== undefined && (transactions.length !== 1 || typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas
|
|
506
|
+
const gasLimit = limits.executionGasCeiling;
|
|
507
|
+
if (nextEnvelope !== undefined && (transactions.length !== 1 || typeof nextEnvelope.gas !== "bigint" || nextEnvelope.gas !== gasLimit ||
|
|
521
508
|
typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
|
|
522
|
-
throw new LifecyclePlanningError("
|
|
509
|
+
throw new LifecyclePlanningError("STALE_REVIEWED_GAS", "Reviewed gas differs from the native transaction ceiling; review the launch again");
|
|
523
510
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const exceedsCompute = estimatedGas > computeCap;
|
|
538
|
-
const gasInMillions = (gas) => {
|
|
539
|
-
const tenths = (gas + 50000n) / 100000n;
|
|
540
|
-
return `${tenths / 10n}.${tenths % 10n}`;
|
|
541
|
-
};
|
|
542
|
-
const atomic = allTransactions[0].kind === "atomic";
|
|
543
|
-
const reason = exceedsCompute
|
|
544
|
-
? `${atomic ? "Atomic launch" : "Immediate transaction"} needs about ${gasInMillions(estimatedGas)}M gas, above the ${gasInMillions(computeCap)}M per-transaction limit${atomic ? "; use staged" : ""}`
|
|
545
|
-
: "The immediate transaction estimate exceeds the RPC aggregate simulation gas cap; no prefix can be proved";
|
|
546
|
-
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: exceedsCompute ? "compute" : "simulation-gas", reason };
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
const recommendedGasPrice = nextEnvelope === undefined ? typeof gasPriceValue === "bigint" ? gasPriceValue : rpcQuantity(gasPriceValue, "gas price") : nextEnvelope.gasPrice;
|
|
550
|
-
// New recommendations cover lagging quotes; reviewed fees are replayed unchanged
|
|
551
|
-
// under natural child fees, never refreshed or silently increased.
|
|
552
|
-
const baseFeeEnvelope = (block.baseFeePerGas ?? 0n) * 2n;
|
|
553
|
-
const gasPrice = nextEnvelope?.gasPrice ?? selectedGasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
|
|
554
|
-
if (gasPrice >= 1n << 256n)
|
|
555
|
-
throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
|
|
556
|
-
const buffer = (gas) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
|
|
511
|
+
if (limits.transactionGasCeiling !== undefined && gasLimit > limits.transactionGasCeiling)
|
|
512
|
+
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 };
|
|
513
|
+
const recommendation = typeof gasPriceValue === "bigint" ? gasPriceValue : rpcQuantity(gasPriceValue, "gas price");
|
|
514
|
+
const baseFee = block.baseFeePerGas ?? 0n;
|
|
515
|
+
const requiredPrice = limits.protocol === "nitro" && recommendation > baseFee ? recommendation : baseFee;
|
|
516
|
+
const gasPrice = nextEnvelope?.gasPrice ?? (recommendation > baseFee * 2n ? recommendation : baseFee * 2n);
|
|
517
|
+
if (gasPrice >= 1n << 256n || transactions[0].value + gasLimit * gasPrice >= 1n << 256n)
|
|
518
|
+
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and native-ceiling fee must fit uint256");
|
|
519
|
+
if (gasPrice < requiredPrice)
|
|
520
|
+
return { ...base, executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failedTransactionId: transactions[0].id, reason: "Reviewed gas price is below the pinned required fee" };
|
|
521
|
+
const balance = rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance");
|
|
522
|
+
if (transactions[0].value + gasLimit * gasPrice > balance)
|
|
523
|
+
return { ...base, executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "affordability", reason: "Creator native balance cannot reserve transaction value and the native gas ceiling" };
|
|
557
524
|
const posterGas = [];
|
|
558
525
|
if (limits.protocol === "nitro") {
|
|
559
526
|
try {
|
|
560
|
-
const [,
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
]);
|
|
564
|
-
posterGas.push(...estimates);
|
|
527
|
+
const [, prices] = await Promise.all([proveBackend(limits, gasPrice), readNitroPrices(client, block)]);
|
|
528
|
+
for (const transaction of transactions)
|
|
529
|
+
posterGas.push(nitroPosterGasBound(transaction, prices).posterGas);
|
|
565
530
|
}
|
|
566
531
|
catch (failure) {
|
|
567
532
|
if (simulationFailureKind(failure) === "abort")
|
|
568
533
|
throw failure;
|
|
569
534
|
assertCurrent();
|
|
570
|
-
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Native Nitro
|
|
535
|
+
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.` };
|
|
571
536
|
}
|
|
572
537
|
}
|
|
538
|
+
const gasLimits = transactions.map(() => gasLimit);
|
|
539
|
+
let backend = (immediate || nextEnvelope !== undefined) && limits.protocol === "evm" && options.fork !== undefined ? "controlled-fork" : "eth_simulateV1";
|
|
540
|
+
let verified;
|
|
573
541
|
assertCurrent();
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const capped = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < total ? limits.transactionGasCeiling : total;
|
|
577
|
-
if (capped <= 0n || capped >= 1n << 64n)
|
|
578
|
-
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Compute/poster envelope exceeds uint64 gas");
|
|
579
|
-
return capped;
|
|
580
|
-
});
|
|
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");
|
|
584
|
-
let measured = [];
|
|
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();
|
|
604
|
-
try {
|
|
542
|
+
try {
|
|
543
|
+
verified = await lifecycleStage(client, "simulation.measure", async () => {
|
|
605
544
|
try {
|
|
606
|
-
|
|
545
|
+
return backend === "controlled-fork"
|
|
546
|
+
? await simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice)
|
|
547
|
+
: await simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, !diagnostic);
|
|
607
548
|
}
|
|
608
549
|
catch (failure) {
|
|
609
|
-
if (
|
|
550
|
+
if (backend !== "eth_simulateV1" || limits.protocol !== "evm" || options.fork === undefined ||
|
|
551
|
+
upfrontFundsFailure(failure) || simulationFailureKind(failure) !== "source")
|
|
610
552
|
throw failure;
|
|
611
553
|
assertCurrent();
|
|
612
|
-
|
|
613
|
-
|
|
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));
|
|
554
|
+
backend = "controlled-fork";
|
|
555
|
+
return simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice);
|
|
623
556
|
}
|
|
624
|
-
}
|
|
625
|
-
catch (failure) {
|
|
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")
|
|
630
|
-
throw failure;
|
|
631
|
-
assertCurrent();
|
|
632
|
-
if (options.fork === undefined || limits.protocol === "nitro" || kind !== "source") {
|
|
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."}` };
|
|
634
|
-
}
|
|
635
|
-
backend = "controlled-fork";
|
|
636
|
-
measured = await simulateForkPass(options.fork, planned, block, transactions, sizingGas, gasPrice, true, limits.headroomBps);
|
|
637
|
-
}
|
|
638
|
-
assertCurrent();
|
|
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
|
-
}
|
|
645
|
-
if (failedIndex >= 0 && limits.maxSimulationGas !== undefined &&
|
|
646
|
-
measured.reduce((gas, step) => gas + (step.gasUsed ?? 0n), 0n) * 100n >= limits.maxSimulationGas * 95n) {
|
|
647
|
-
// The provider exhausted its aggregate used-gas budget, not the contract.
|
|
648
|
-
// Preserve only actual successful results; the failed call is deferred too.
|
|
649
|
-
measured = measured.slice(0, failedIndex);
|
|
650
|
-
transactions = transactions.slice(0, failedIndex);
|
|
651
|
-
base.deferredTransactionIds = allTransactions.slice(failedIndex).map((transaction) => transaction.id);
|
|
652
|
-
if (transactions.length === 0) {
|
|
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" };
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
const failed = measured.find((step) => !step.success);
|
|
657
|
-
if (diagnostic && failed !== undefined)
|
|
658
|
-
return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId, failureCategory: failed.failureCategory ?? "opaque", capacityConstraint: failed.failureCategory === "capacity" ? "compute" : undefined };
|
|
659
|
-
if (diagnostic) {
|
|
660
|
-
return { ...base, backend, admitted: false, executionProof: "unavailable", steps: measured };
|
|
661
|
-
}
|
|
662
|
-
}
|
|
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];
|
|
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
|
-
}
|
|
698
|
-
const exceeds = gasLimits.findIndex((gas, index) => gas <= 0n || gas >= 1n << 64n || limits.transactionGasCeiling !== undefined && gas > limits.transactionGasCeiling ||
|
|
699
|
-
nextEnvelope === undefined && !hasSizing && (measured[index].gasRequired ?? measured[index].gasUsed) > gas ||
|
|
700
|
-
limits.protocol === "evm" && gas > limits.executionGasCeiling);
|
|
701
|
-
if (exceeds >= 0) {
|
|
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 };
|
|
557
|
+
});
|
|
703
558
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
: simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice, false, limits.headroomBps));
|
|
712
|
-
}
|
|
713
|
-
catch (failure) {
|
|
714
|
-
if (simulationFailureKind(failure) === "abort")
|
|
715
|
-
throw failure;
|
|
716
|
-
assertCurrent();
|
|
717
|
-
return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [], reason: `Exact transaction simulation unavailable: ${lifecycleFailureReason(failure)}` };
|
|
718
|
-
}
|
|
559
|
+
catch (failure) {
|
|
560
|
+
if (simulationFailureKind(failure) === "abort")
|
|
561
|
+
throw failure;
|
|
562
|
+
assertCurrent();
|
|
563
|
+
if (upfrontFundsFailure(failure))
|
|
564
|
+
return { ...base, backend, confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [], failureCategory: "affordability", reason: "Native balance cannot reserve the fixed-ceiling sequence fee envelopes" };
|
|
565
|
+
return { ...base, backend: "unavailable", confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [], reason: `Exact transaction simulation unavailable: ${lifecycleFailureReason(failure)}` };
|
|
719
566
|
}
|
|
720
567
|
assertCurrent();
|
|
721
|
-
|
|
568
|
+
if (limits.maxSimulationGas !== undefined && verified.reduce((total, step) => total + (step.gasUsed ?? 0n), 0n) > limits.maxSimulationGas) {
|
|
569
|
+
return { ...base, backend, confidence: "provisional", executionProof: "unavailable", admitted: false, steps: [],
|
|
570
|
+
failureCategory: "source", capacityConstraint: "simulation-gas", deferredTransactionIds: transactions.map((transaction) => transaction.id),
|
|
571
|
+
reason: "Complete launch execution exceeds the supplied aggregate simulation gas budget" };
|
|
572
|
+
}
|
|
573
|
+
const failure = verified.find((step) => !step.success);
|
|
574
|
+
if (diagnostic)
|
|
575
|
+
return { ...base, backend, executionProof: "unavailable", admitted: false, steps: verified, reason: failure?.error, failedTransactionId: failure?.transactionId, failureCategory: failure?.failureCategory };
|
|
722
576
|
const estimates = [];
|
|
723
|
-
let remainingBalance = finalFailure === undefined ? balance : undefined;
|
|
724
577
|
let affordabilityFailure;
|
|
725
578
|
const feeContext = configuration.estimateDataFee === undefined || sourceClient === client ? context : { ...context, client: sourceClient };
|
|
726
|
-
for (
|
|
727
|
-
const step = verifiedSteps[index];
|
|
579
|
+
for (const [index, step] of verified.entries()) {
|
|
728
580
|
const transaction = transactions[index];
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing verified fee context");
|
|
732
|
-
if (index > 0) {
|
|
733
|
-
estimates.push({ ...step, computeGasRequired: minimumGas[index], provisional: true });
|
|
581
|
+
if (index > 0 || transaction.kind === "approve" || transaction.kind === "approve-reset") {
|
|
582
|
+
estimates.push({ ...step, ...(index > 0 ? { provisional: true } : {}) });
|
|
734
583
|
continue;
|
|
735
584
|
}
|
|
736
585
|
const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit, gasPrice }, feeContext);
|
|
737
586
|
if (dataFee !== undefined && (typeof dataFee !== "bigint" || dataFee < 0n || dataFee >= 1n << 256n))
|
|
738
587
|
throw new LifecyclePlanningError("INVALID_DATA_FEE", "Data-fee oracle must return a nonnegative uint256 bigint amount");
|
|
739
|
-
|
|
740
|
-
const executionFee = gasLimit * gasPrice;
|
|
741
|
-
const required = transaction.value + executionFee + (dataFee ?? 0n);
|
|
588
|
+
const executionFee = gasLimit * gasPrice, required = transaction.value + executionFee + (dataFee ?? 0n);
|
|
742
589
|
if (required >= 1n << 256n)
|
|
743
590
|
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
|
|
744
|
-
if (
|
|
745
|
-
|
|
746
|
-
affordabilityFailure = "Creator native balance cannot fund the remaining transaction value and execution/data fee envelopes";
|
|
747
|
-
remainingBalance -= required;
|
|
748
|
-
}
|
|
591
|
+
if (required > balance)
|
|
592
|
+
affordabilityFailure = "Creator native balance cannot reserve transaction value and the native-ceiling/data fee envelope";
|
|
749
593
|
const poster = posterGas[index];
|
|
750
|
-
estimates.push({ ...step,
|
|
594
|
+
estimates.push({ ...step, estimate: { gasUsed: step.gasUsed, gasLimit, gasPrice, executionFee, dataFee, posterGas: poster,
|
|
595
|
+
posterFee: poster === undefined ? undefined : poster * gasPrice, dataFeeIncludedInGas: limits.protocol === "nitro",
|
|
596
|
+
totalFee: dataFee === undefined ? undefined : executionFee + dataFee, feeConfidence: dataFee === undefined ? "execution-only" : "execution-and-data" } });
|
|
751
597
|
}
|
|
752
|
-
|
|
598
|
+
const activationFailure = failure?.transactionId === "activate" && failure.failureCategory === "capacity";
|
|
599
|
+
return { ...base, backend, executionProof: failure === undefined ? "proved" : "failed", protocolFit: failure === undefined ? "proved" : "unknown",
|
|
600
|
+
admitted: failure === undefined && affordabilityFailure === undefined, steps: estimates,
|
|
601
|
+
reason: activationFailure ? "Too many positions or pools for one activation. Reduce them to fit the native transaction ceiling." : failure?.error ?? affordabilityFailure,
|
|
602
|
+
failedTransactionId: failure?.transactionId, failureCategory: failure?.failureCategory ?? (affordabilityFailure === undefined ? undefined : "affordability"),
|
|
603
|
+
capacityConstraint: activationFailure ? "activation" : failure?.failureCategory === "capacity" ? "compute" : undefined };
|
|
753
604
|
}
|
|
754
605
|
//# sourceMappingURL=simulation.js.map
|