@black-market/sdk 0.14.1 → 0.15.1
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 +9 -7
- package/dist/lifecycle/authors.js +2 -2
- package/dist/lifecycle/authors.js.map +1 -1
- package/dist/lifecycle/auto-compound.d.ts +21 -0
- package/dist/lifecycle/auto-compound.d.ts.map +1 -0
- package/dist/lifecycle/auto-compound.js +34 -0
- package/dist/lifecycle/auto-compound.js.map +1 -0
- package/dist/lifecycle/calibration.d.ts +2 -4
- package/dist/lifecycle/calibration.d.ts.map +1 -1
- package/dist/lifecycle/calibration.js +1 -26
- package/dist/lifecycle/calibration.js.map +1 -1
- package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.d.ts +73 -0
- package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.d.ts.map +1 -0
- package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.js +75 -0
- package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.js.map +1 -0
- package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.d.ts +73 -0
- package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.d.ts.map +1 -0
- package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.js +75 -0
- package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.js.map +1 -0
- package/dist/lifecycle/gas-profile.d.ts +39 -0
- package/dist/lifecycle/gas-profile.d.ts.map +1 -0
- package/dist/lifecycle/gas-profile.js +193 -0
- package/dist/lifecycle/gas-profile.js.map +1 -0
- package/dist/lifecycle/index.d.ts +6 -4
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +6 -4
- package/dist/lifecycle/index.js.map +1 -1
- package/dist/lifecycle/markets.d.ts +2 -0
- package/dist/lifecycle/markets.d.ts.map +1 -1
- package/dist/lifecycle/markets.js +30 -3
- package/dist/lifecycle/markets.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 +6 -9
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +175 -265
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/presets.d.ts +10 -3
- package/dist/lifecycle/presets.d.ts.map +1 -1
- package/dist/lifecycle/presets.js +13 -1
- 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 +25 -30
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +63 -81
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.js +2 -4
- package/dist/lifecycle/snapshot.js.map +1 -1
- package/dist/lifecycle/transactions.d.ts +29 -6
- package/dist/lifecycle/transactions.d.ts.map +1 -1
- package/dist/lifecycle/transactions.js +62 -27
- package/dist/lifecycle/transactions.js.map +1 -1
- package/dist/lifecycle/types.d.ts +55 -122
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js +3 -3
- package/dist/lifecycle/types.js.map +1 -1
- package/dist/lifecycle/ufg.d.ts +1 -1
- package/dist/lifecycle/ufg.d.ts.map +1 -1
- package/dist/lifecycle/ufg.js +4 -8
- package/dist/lifecycle/ufg.js.map +1 -1
- package/docs/launch-lifecycle.md +110 -78
- package/docs/lifecycle-gas-profiles.md +90 -0
- package/docs/reference.md +99 -47
- package/examples/.env.example +0 -3
- package/examples/README.md +14 -11
- package/examples/launch-example-support.ts +4 -9
- package/examples/launch-example.ts +30 -36
- package/examples/launch-lifecycle-smoke.ts +11 -21
- package/examples/launch.ts +8 -11
- package/package.json +1 -1
- package/src/lifecycle/authors.ts +2 -2
- package/src/lifecycle/auto-compound.ts +38 -0
- package/src/lifecycle/calibration.ts +3 -23
- package/src/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.ts +77 -0
- package/src/lifecycle/deployments/auto-compound-static-pool-v1-20261028.ts +77 -0
- package/src/lifecycle/gas-profile.ts +186 -0
- package/src/lifecycle/index.ts +6 -4
- package/src/lifecycle/markets.ts +25 -3
- package/src/lifecycle/opening-buys.ts +157 -0
- package/src/lifecycle/planner.ts +168 -247
- package/src/lifecycle/presets.ts +15 -1
- package/src/lifecycle/progress.ts +19 -20
- package/src/lifecycle/rpc.ts +58 -68
- package/src/lifecycle/snapshot.ts +2 -4
- package/src/lifecycle/transactions.ts +69 -27
- package/src/lifecycle/types.ts +60 -99
- package/src/lifecycle/ufg.ts +4 -8
- package/dist/lifecycle/fork.d.ts +0 -15
- package/dist/lifecycle/fork.d.ts.map +0 -1
- package/dist/lifecycle/fork.js +0 -36
- package/dist/lifecycle/fork.js.map +0 -1
- package/dist/lifecycle/simulation.d.ts +0 -18
- package/dist/lifecycle/simulation.d.ts.map +0 -1
- package/dist/lifecycle/simulation.js +0 -754
- package/dist/lifecycle/simulation.js.map +0 -1
- package/src/lifecycle/fork.ts +0 -36
- package/src/lifecycle/simulation.ts +0 -689
|
@@ -1,754 +0,0 @@
|
|
|
1
|
-
import { decodeErrorResult, decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctionResult, parseAbi, toHex } from "viem";
|
|
2
|
-
import { fixedFeePoolHookV1Abi, launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
|
|
3
|
-
import { hashLaunchIdentity, hashLaunchPlan, LifecyclePhase } from "./schema.js";
|
|
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
|
-
import { LifecyclePlanningError } from "./types.js";
|
|
6
|
-
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion } from "./markets.js";
|
|
7
|
-
import { hmmV2ForwardingReserve } from "./presets.js";
|
|
8
|
-
import { transactionGasEstimate } from "./transactions.js";
|
|
9
|
-
import { hashUfgLaunchConfig } from "./ufg-config.js";
|
|
10
|
-
function committedPositionCount(plan) {
|
|
11
|
-
let count = 0;
|
|
12
|
-
for (const market of plan.markets) {
|
|
13
|
-
if (market.configVersion === 1)
|
|
14
|
-
count += decodeAbyssLifecycleMarketConfig(market.config).positions.length;
|
|
15
|
-
else if (market.configVersion === 4)
|
|
16
|
-
count += decodeV4LifecycleMarketConfig(market.config).positions.length;
|
|
17
|
-
else if (isPoolBoundV4ConfigVersion(market.configVersion))
|
|
18
|
-
count += decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion).positions.length;
|
|
19
|
-
else
|
|
20
|
-
throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", "Activation requires a supported committed position schema");
|
|
21
|
-
}
|
|
22
|
-
return count;
|
|
23
|
-
}
|
|
24
|
-
const executionGuardErrors = [
|
|
25
|
-
...fixedFeePoolHookV1Abi.filter((item) => item.type === "error"),
|
|
26
|
-
...parseAbi([
|
|
27
|
-
"error WrongDomain()", "error PlanMismatch()", "error LaunchAlreadyExists()", "error InvalidMode()",
|
|
28
|
-
"error InvalidPhase()", "error InvalidPreparationOrder()", "error DeadlineExpired()", "error InvalidBinding()",
|
|
29
|
-
"error InvalidMarket()", "error InsufficientEscrow()", "error InvalidPositions()", "error InvalidPlan()",
|
|
30
|
-
"error InvalidFunding()", "error InvalidFeePolicy()", "error InvalidBuy()", "error DuplicateMarket()",
|
|
31
|
-
"error IneligibleImplementation()", "error QuoteDebtForbidden()", "error SlippageExceeded()", "error FeeBelowMinimum()",
|
|
32
|
-
"error DeploymentFailed()", "error Error(string reason)", "error Panic(uint256 code)",
|
|
33
|
-
]),
|
|
34
|
-
];
|
|
35
|
-
function executionFailure(returnData, error) {
|
|
36
|
-
const native = error !== null && typeof error === "object" && !Array.isArray(error) ? error : undefined;
|
|
37
|
-
let nativeData = native?.data;
|
|
38
|
-
for (let depth = 0; depth < 3 && nativeData !== null && typeof nativeData === "object" && !Array.isArray(nativeData); depth += 1)
|
|
39
|
-
nativeData = nativeData.data;
|
|
40
|
-
const nativeErrorDataBytes = typeof nativeData === "string" && /^0x(?:[0-9a-fA-F]{2})*$/.test(nativeData) ? (nativeData.length - 2) / 2 : undefined;
|
|
41
|
-
const nativeErrorCode = typeof native?.code === "number" && Number.isSafeInteger(native.code) ? native.code : undefined;
|
|
42
|
-
const message = typeof native?.message === "string" ? native.message : "";
|
|
43
|
-
const nativeGasCapped = message.endsWith(" (gas limit was capped by the RPC server's global gas cap)");
|
|
44
|
-
// Native eth_simulateV1 uses -32015 for VM errors; -32000 is also used by RPC clients.
|
|
45
|
-
// Empty data and an explicit OOG report are required. Malformed data or proximity is not proof.
|
|
46
|
-
const outOfGas = (returnData === undefined || returnData === "0x") && (native?.data === undefined || nativeErrorDataBytes === 0) &&
|
|
47
|
-
(nativeErrorCode === -32015 || nativeErrorCode === -32000) && /^out of gas\b/i.test(message);
|
|
48
|
-
const nativeErrorKind = native === undefined ? "missing" : outOfGas ? "out-of-gas" : nativeErrorCode === 3 ? "execution-reverted" : nativeErrorCode === -32015 ? "vm-error" : "other";
|
|
49
|
-
const diagnostic = { nativeErrorCode, nativeErrorKind, nativeErrorDataBytes, nativeGasCapped };
|
|
50
|
-
const data = returnData === undefined || returnData === "0x" ? nativeData : returnData;
|
|
51
|
-
if (typeof data === "string" && /^0x(?:[0-9a-fA-F]{2}){4,}$/.test(data)) {
|
|
52
|
-
try {
|
|
53
|
-
const { errorName } = decodeErrorResult({ abi: executionGuardErrors, data: data });
|
|
54
|
-
return { ...diagnostic, failureCategory: errorName === "DeploymentFailed" ? "opaque" : "semantic", decodedError: errorName, error: `Execution reverted (${errorName})` };
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return { ...diagnostic, failureCategory: "opaque", error: "Execution reverted without a reviewed error" };
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
if (outOfGas)
|
|
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
|
-
return { ...diagnostic, failureCategory: "opaque", error: nativeErrorKind === "execution-reverted" ? "Native execution reverted without a reviewed error" : "Execution failed without a reviewed error" };
|
|
63
|
-
}
|
|
64
|
-
function conditionRequest(condition, planned) {
|
|
65
|
-
if (condition.kind === "allowance")
|
|
66
|
-
return {
|
|
67
|
-
to: condition.asset, data: encodeFunctionData({ abi: lifecycleErc20Abi, functionName: "allowance", args: [planned.account, condition.spender] }),
|
|
68
|
-
};
|
|
69
|
-
return { to: planned.plan.orchestrator, data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "readLaunchProgress", args: [planned.launchId] }) };
|
|
70
|
-
}
|
|
71
|
-
function checkCondition(condition, data, planned) {
|
|
72
|
-
if (condition.kind === "allowance") {
|
|
73
|
-
const allowance = decodeFunctionResult({ abi: lifecycleErc20Abi, functionName: "allowance", data });
|
|
74
|
-
if (allowance < condition.minimum || (condition.exact !== undefined && allowance !== condition.exact))
|
|
75
|
-
throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Funding allowance was not established");
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
// The frozen ABI decoder produces the precise progress tuple, including all identity fields.
|
|
79
|
-
const progress = decodeFunctionResult({ abi: launchLifecycleAbi, functionName: "readLaunchProgress", data });
|
|
80
|
-
const phase = LifecyclePhase[condition.phase];
|
|
81
|
-
if (progress.phase !== phase || progress.preparedMarkets !== condition.preparedMarkets || progress.planHash.toLowerCase() !== planned.planHash.toLowerCase() || progress.launchId.toLowerCase() !== planned.launchId.toLowerCase() || progress.token.toLowerCase() !== planned.predictedToken.toLowerCase()) {
|
|
82
|
-
throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not establish the committed launch identity, phase and ordered progress");
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
/** Native Nitro eth_simulateV1 executes commit-mode ArbOS charging, including its
|
|
86
|
-
* compute hold after the actual poster charge. A generic EVM/fork is not this proof. */
|
|
87
|
-
async function proveNitroBackend(client, planned, block, limits, gasPrice) {
|
|
88
|
-
if (limits.arbOSVersion === undefined || limits.maxTxComputeGas === undefined || limits.maxBlockComputeGas === undefined)
|
|
89
|
-
throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Pinned Nitro metering context is incomplete");
|
|
90
|
-
const probes = [
|
|
91
|
-
{ 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
|
-
];
|
|
95
|
-
const gas = limits.executionGasCeiling < 100000n ? limits.executionGasCeiling : 100000n;
|
|
96
|
-
if (limits.maxSimulationGas !== undefined && gas * BigInt(probes.length) > limits.maxSimulationGas)
|
|
97
|
-
throw new LifecyclePlanningError("SIMULATION_RPC_CAP", "Nitro metering probe exceeds the supplied RPC aggregate simulation cap");
|
|
98
|
-
// This isolated read-only request does not add synthetic verification transactions to the launch sequence.
|
|
99
|
-
const result = await lifecycleRpc(client, "eth_simulateV1", [{
|
|
100
|
-
validation: true, traceTransfers: false, returnFullTransactions: false,
|
|
101
|
-
blockStateCalls: [{
|
|
102
|
-
// Capability reads are isolated from payer affordability. Real launch replay never overrides balances.
|
|
103
|
-
stateOverrides: { [planned.account]: { balance: toHex((1n << 256n) - 1n) } },
|
|
104
|
-
blockOverrides: { number: toHex(block.number + 1n), time: toHex(block.timestamp + 1n), gasLimit: toHex(block.gasLimit) },
|
|
105
|
-
calls: probes.map(({ to, data }) => ({ from: planned.account, to, data, value: "0x0", gas: toHex(gas), gasPrice: toHex(gasPrice) })),
|
|
106
|
-
}],
|
|
107
|
-
}, toHex(block.number)]);
|
|
108
|
-
if (!Array.isArray(result) || result.length !== 1)
|
|
109
|
-
throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Native simulation omitted the pinned ArbOS metering probe");
|
|
110
|
-
const simulated = rpcObject(result[0], "Nitro metering probe block");
|
|
111
|
-
if (!Array.isArray(simulated.calls) || simulated.calls.length !== probes.length)
|
|
112
|
-
throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Native simulation omitted ArbOS version or compute getters");
|
|
113
|
-
for (let index = 0; index < probes.length; index += 1) {
|
|
114
|
-
const call = rpcObject(simulated.calls[index], "Nitro metering probe");
|
|
115
|
-
const data = rpcHex(call.returnData, "simulated Nitro metering result");
|
|
116
|
-
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/compute getters differ from the exact pinned chain context");
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
async function simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, validation) {
|
|
122
|
-
// Natural child-block fees must govern fee-bearing admission; overriding
|
|
123
|
-
// baseFeePerGas with the parent's would fabricate a lower envelope and admit
|
|
124
|
-
// calls that the actual chain would reject. eth_simulateV1 derives the child
|
|
125
|
-
// base fee itself; only structural child fields (number/time) and the
|
|
126
|
-
// parent gasLimit are set.
|
|
127
|
-
const blockStateCalls = transactions.map((transaction, index) => ({
|
|
128
|
-
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), ...(transaction.nonce === undefined ? {} : { nonce: toHex(transaction.nonce) }) }],
|
|
130
|
-
}));
|
|
131
|
-
const result = await lifecycleRpc(client, "eth_simulateV1", [{ blockStateCalls, validation, traceTransfers: false, returnFullTransactions: false }, toHex(block.number)]);
|
|
132
|
-
if (!Array.isArray(result) || result.length !== transactions.length)
|
|
133
|
-
throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Sequential simulation omitted blocks");
|
|
134
|
-
const steps = [];
|
|
135
|
-
for (let index = 0; index < transactions.length; index += 1) {
|
|
136
|
-
const transaction = transactions[index];
|
|
137
|
-
if (transaction === undefined)
|
|
138
|
-
throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing simulation transaction");
|
|
139
|
-
const simulatedBlock = rpcObject(result[index], "simulated block");
|
|
140
|
-
if (!Array.isArray(simulatedBlock.calls) || simulatedBlock.calls.length !== 1)
|
|
141
|
-
throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Sequential simulation omitted transaction results");
|
|
142
|
-
const call = rpcObject(simulatedBlock.calls[0], "simulated transaction");
|
|
143
|
-
const gasUsed = rpcQuantity(call.gasUsed, "simulated gas used");
|
|
144
|
-
const gasRequired = call.maxUsedGas === undefined ? gasUsed : rpcQuantity(call.maxUsedGas, "simulated gross gas");
|
|
145
|
-
if (gasRequired < gasUsed || gasRequired > (gasLimits[index] ?? 0n))
|
|
146
|
-
throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Simulated gas use is outside the exact requested envelope");
|
|
147
|
-
const returnData = rpcHex(call.returnData, "simulated return data");
|
|
148
|
-
const success = rpcQuantity(call.status, "simulated status") === 1n;
|
|
149
|
-
const failureDiagnostic = success ? undefined : executionFailure(returnData, call.error);
|
|
150
|
-
let error = failureDiagnostic?.error;
|
|
151
|
-
if (success) {
|
|
152
|
-
try {
|
|
153
|
-
// Verification reads must not become synthetic transactions that consume creator nonce/fees.
|
|
154
|
-
// Core return tuples/events and subsequent dependent commands prove persisted transitions.
|
|
155
|
-
const events = (Array.isArray(call.logs) ? call.logs : []).flatMap((raw) => {
|
|
156
|
-
const log = rpcObject(raw, "simulation event");
|
|
157
|
-
if (rpcHex(log.address, "simulation event address").toLowerCase() !== planned.plan.orchestrator.toLowerCase() || !Array.isArray(log.topics))
|
|
158
|
-
return [];
|
|
159
|
-
const topics = log.topics.map((topic) => rpcHex(topic, "event topic"));
|
|
160
|
-
if (topics.length === 0)
|
|
161
|
-
return [];
|
|
162
|
-
const event = decodeEventLog({ abi: launchLifecycleAbi, topics: topics, data: rpcHex(log.data, "simulation event data") });
|
|
163
|
-
return [event];
|
|
164
|
-
});
|
|
165
|
-
if (transaction.kind === "begin")
|
|
166
|
-
checkCondition({ kind: "launch", phase: "Preparing", preparedMarkets: 0 }, returnData, planned);
|
|
167
|
-
else if (transaction.kind === "prepare") {
|
|
168
|
-
const prepared = events.filter((event) => event.eventName === "MarketPrepared");
|
|
169
|
-
if (prepared.length !== transaction.marketCount || prepared.some((event, ordinal) => event.eventName !== "MarketPrepared" || event.args.launchId.toLowerCase() !== planned.launchId.toLowerCase() || event.args.marketIndex !== (transaction.marketStart ?? 0) + ordinal))
|
|
170
|
-
throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not prepare each exact ordered market");
|
|
171
|
-
}
|
|
172
|
-
else if (transaction.kind === "atomic" || transaction.kind === "activate") {
|
|
173
|
-
const functionName = transaction.kind === "atomic" ? "launchAtomic" : "activateLaunch";
|
|
174
|
-
const receipt = decodeFunctionResult({ abi: launchLifecycleAbi, functionName, data: returnData });
|
|
175
|
-
const positionCount = committedPositionCount(planned.plan);
|
|
176
|
-
if (receipt.planHash.toLowerCase() !== planned.planHash.toLowerCase() || receipt.launchId.toLowerCase() !== planned.launchId.toLowerCase() || receipt.token.toLowerCase() !== planned.predictedToken.toLowerCase() || receipt.marketCount !== planned.plan.markets.length || receipt.positionCount !== positionCount || receipt.quoteSpent.length !== planned.plan.buys.length || receipt.tokenOut.length !== planned.plan.buys.length || !events.some((event) => event.eventName === "LaunchActivated" && event.args.launchId.toLowerCase() === planned.launchId.toLowerCase() && event.args.planHash.toLowerCase() === planned.planHash.toLowerCase() && event.args.token.toLowerCase() === planned.predictedToken.toLowerCase() && event.args.marketCount === planned.plan.markets.length && event.args.positionCount === positionCount))
|
|
177
|
-
throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not atomically complete every committed market, position and ordered buy");
|
|
178
|
-
for (let buyIndex = 0; buyIndex < planned.plan.buys.length; buyIndex += 1) {
|
|
179
|
-
const buy = planned.plan.buys[buyIndex];
|
|
180
|
-
if (buy === undefined || (receipt.quoteSpent[buyIndex] ?? buy.quoteAmountIn + 1n) > buy.quoteAmountIn || (receipt.tokenOut[buyIndex] ?? 0n) < buy.minTokenOut)
|
|
181
|
-
throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not satisfy every ordered buy's budget and output minimum");
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
else if (transaction.kind === "cancel" && !events.some((event) => event.eventName === "LaunchCancelled" && event.args.launchId.toLowerCase() === planned.launchId.toLowerCase() && event.args.creator.toLowerCase() === planned.plan.creator.toLowerCase()))
|
|
185
|
-
throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not cancel the exact committed launch");
|
|
186
|
-
else if ((transaction.kind === "approve" || transaction.kind === "approve-reset") && returnData !== "0x" && decodeFunctionResult({ abi: lifecycleErc20Abi, functionName: "approve", data: returnData }) !== true)
|
|
187
|
-
throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Funding approval returned failure");
|
|
188
|
-
}
|
|
189
|
-
catch (failure) {
|
|
190
|
-
error = failure instanceof LifecyclePlanningError ? failure.message : "Simulation returned an invalid committed postcondition";
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
steps.push({ transactionId: transaction.id, success: success && error === undefined, gasUsed, gasRequired, gasLimit: gasLimits[index], returnData, error, ...failureDiagnostic, ...(success && error !== undefined ? { failureCategory: "postcondition" } : {}) });
|
|
194
|
-
if (!success || error !== undefined)
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
return steps;
|
|
198
|
-
}
|
|
199
|
-
function simulationFailureKind(failure) {
|
|
200
|
-
for (let depth = 0; depth < 8 && failure !== null && typeof failure === "object"; depth += 1) {
|
|
201
|
-
const error = failure;
|
|
202
|
-
if (error.name === "AbortError")
|
|
203
|
-
return "abort";
|
|
204
|
-
const status = error.status ?? error.statusCode;
|
|
205
|
-
if (typeof status === "number" && status >= 400 && status <= 599)
|
|
206
|
-
return "source";
|
|
207
|
-
const message = typeof error.details === "string" ? error.details : typeof error.message === "string" ? error.message : "";
|
|
208
|
-
const execution = executionFailure(undefined, { ...error, message });
|
|
209
|
-
if (execution.nativeErrorKind === "out-of-gas" && execution.nativeGasCapped)
|
|
210
|
-
return "source";
|
|
211
|
-
if (execution.failureCategory === "capacity")
|
|
212
|
-
return "out-of-gas";
|
|
213
|
-
if (error.code === 3 || error.code === -32000 && /^execution (?:failed|reverted)\b/i.test(message))
|
|
214
|
-
return "execution";
|
|
215
|
-
failure = error.cause;
|
|
216
|
-
}
|
|
217
|
-
return "source";
|
|
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
|
-
}
|
|
232
|
-
async function waitForkReceipt(client, transactionHash, timeoutMs) {
|
|
233
|
-
const deadline = Date.now() + timeoutMs;
|
|
234
|
-
while (Date.now() <= deadline) {
|
|
235
|
-
const receipt = await lifecycleRpc(client, "eth_getTransactionReceipt", [transactionHash]);
|
|
236
|
-
if (receipt !== null)
|
|
237
|
-
return rpcObject(receipt, "fork receipt");
|
|
238
|
-
await new Promise((resolve) => globalThis.setTimeout(resolve, 25));
|
|
239
|
-
}
|
|
240
|
-
throw new LifecyclePlanningError("FORK_RECEIPT_TIMEOUT", "The disposable fork did not mine its simulation transaction");
|
|
241
|
-
}
|
|
242
|
-
async function simulateForkPass(fork, planned, block, transactions, gasLimits, gasPrice, measure, headroomBps) {
|
|
243
|
-
if (fork.isolation !== "disposable" || fork.allowTransactions !== true || !fork.sourceRpcUrl || !fork.forkRpcUrl)
|
|
244
|
-
throw new LifecyclePlanningError("UNSAFE_FORK", "Actual simulation transactions require an explicitly distinct disposable controlled fork");
|
|
245
|
-
const sourceUrl = new URL(fork.sourceRpcUrl);
|
|
246
|
-
const forkUrl = new URL(fork.forkRpcUrl);
|
|
247
|
-
const loopback = ["localhost", "127.0.0.1", "[::1]", "::1"];
|
|
248
|
-
const sourcePort = sourceUrl.port || (sourceUrl.protocol === "https:" ? "443" : "80");
|
|
249
|
-
const forkPort = forkUrl.port || (forkUrl.protocol === "https:" ? "443" : "80");
|
|
250
|
-
if (!loopback.includes(forkUrl.hostname) || sourceUrl.href === forkUrl.href || (loopback.includes(sourceUrl.hostname) && sourcePort === forkPort))
|
|
251
|
-
throw new LifecyclePlanningError("UNSAFE_FORK", "Disposable fork must not alias or share the loopback source endpoint");
|
|
252
|
-
let forkBlock = await readLifecycleBlock(fork.client);
|
|
253
|
-
if (fork.sourceRpcUrl !== undefined && forkBlock.hash.toLowerCase() !== block.hash.toLowerCase()) {
|
|
254
|
-
if (block.number > BigInt(Number.MAX_SAFE_INTEGER))
|
|
255
|
-
throw new LifecyclePlanningError("FORK_STATE_MISMATCH", "Pinned block is not exactly representable by the fork reset API");
|
|
256
|
-
await lifecycleRpc(fork.client, fork.resetMethod ?? "anvil_reset", [{ forking: { jsonRpcUrl: fork.sourceRpcUrl, blockNumber: Number(block.number) } }]);
|
|
257
|
-
forkBlock = await readLifecycleBlock(fork.client);
|
|
258
|
-
}
|
|
259
|
-
const forkChain = rpcQuantity(await lifecycleRpc(fork.client, "eth_chainId"), "fork chain ID");
|
|
260
|
-
if (forkChain !== planned.chainId || forkChain !== (fork.expectedChainId ?? planned.chainId) || forkBlock.hash.toLowerCase() !== block.hash.toLowerCase() || forkBlock.hash.toLowerCase() !== (fork.expectedBlockHash ?? block.hash).toLowerCase())
|
|
261
|
-
throw new LifecyclePlanningError("FORK_STATE_MISMATCH", "Controlled fork must match the exact source chain and pinned latest block before simulation");
|
|
262
|
-
const snapshot = rpcHex(await lifecycleRpc(fork.client, "evm_snapshot"), "fork snapshot");
|
|
263
|
-
const steps = [];
|
|
264
|
-
let impersonated = false;
|
|
265
|
-
try {
|
|
266
|
-
if (fork.impersonation !== undefined && fork.impersonation !== "none") {
|
|
267
|
-
await lifecycleRpc(fork.client, `${fork.impersonation}_impersonateAccount`, [planned.account]);
|
|
268
|
-
impersonated = true;
|
|
269
|
-
}
|
|
270
|
-
for (let index = 0; index < transactions.length; index += 1) {
|
|
271
|
-
const transaction = transactions[index];
|
|
272
|
-
let gas = gasLimits[index];
|
|
273
|
-
let gasRequired;
|
|
274
|
-
if (transaction === undefined || gas === undefined)
|
|
275
|
-
throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing fork transaction/gas context");
|
|
276
|
-
try {
|
|
277
|
-
if (measure) {
|
|
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");
|
|
285
|
-
const receipt = await waitForkReceipt(fork.client, hash, fork.receiptTimeoutMs ?? 10_000);
|
|
286
|
-
const gasUsed = rpcQuantity(receipt.gasUsed, "fork gas used");
|
|
287
|
-
if (rpcQuantity(receipt.status, "fork transaction status") !== 1n) {
|
|
288
|
-
steps.push({ transactionId: transaction.id, success: false, gasUsed, gasLimit: gas, error: "Actual fork transaction reverted" });
|
|
289
|
-
break;
|
|
290
|
-
}
|
|
291
|
-
const currentBlock = await readLifecycleBlock(fork.client);
|
|
292
|
-
let returnData;
|
|
293
|
-
if (transaction.kind === "atomic" || transaction.kind === "activate") {
|
|
294
|
-
const progressData = rpcHex(await lifecycleRpc(fork.client, "eth_call", [{ to: planned.plan.orchestrator, data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "readLaunchProgress", args: [planned.launchId] }) }, toHex(currentBlock.number)]), "fork activation progress");
|
|
295
|
-
const progress = decodeFunctionResult({ abi: launchLifecycleAbi, functionName: "readLaunchProgress", data: progressData });
|
|
296
|
-
if (!Array.isArray(receipt.logs))
|
|
297
|
-
throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Actual fork receipt does not contain canonical launch logs");
|
|
298
|
-
const events = receipt.logs.flatMap((raw) => {
|
|
299
|
-
const log = rpcObject(raw, "fork activation log");
|
|
300
|
-
if (rpcHex(log.address, "fork log emitter").toLowerCase() !== planned.plan.orchestrator.toLowerCase())
|
|
301
|
-
return [];
|
|
302
|
-
if (!Array.isArray(log.topics))
|
|
303
|
-
throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Actual fork log topics are malformed");
|
|
304
|
-
try {
|
|
305
|
-
return [decodeEventLog({ abi: launchLifecycleAbi, data: rpcHex(log.data, "fork log data"), topics: log.topics.map((topic) => rpcHex(topic, "fork log topic")) })];
|
|
306
|
-
}
|
|
307
|
-
catch {
|
|
308
|
-
return [];
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
const activated = events.filter((event) => event.eventName === "LaunchActivated");
|
|
312
|
-
const buys = events.filter((event) => event.eventName === "InitialBuyExecuted");
|
|
313
|
-
if (activated.length !== 1 || !activated.some((event) => event.eventName === "LaunchActivated" && event.args.launchId.toLowerCase() === planned.launchId.toLowerCase() && event.args.planHash.toLowerCase() === planned.planHash.toLowerCase() && event.args.token.toLowerCase() === planned.predictedToken.toLowerCase()) ||
|
|
314
|
-
progress.phase !== LifecyclePhase.Active || progress.planHash.toLowerCase() !== planned.planHash.toLowerCase() || progress.token.toLowerCase() !== planned.predictedToken.toLowerCase() || progress.marketCount !== planned.plan.markets.length || progress.positionCount !== committedPositionCount(planned.plan) ||
|
|
315
|
-
buys.length !== planned.plan.buys.length)
|
|
316
|
-
throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Actual fork activation differs from the exact committed launch");
|
|
317
|
-
const quoteSpent = [];
|
|
318
|
-
const tokenOut = [];
|
|
319
|
-
for (let buyIndex = 0; buyIndex < buys.length; buyIndex += 1) {
|
|
320
|
-
const event = buys[buyIndex];
|
|
321
|
-
const buy = planned.plan.buys[buyIndex];
|
|
322
|
-
if (event?.eventName !== "InitialBuyExecuted" || buy === undefined || event.args.launchId.toLowerCase() !== planned.launchId.toLowerCase() ||
|
|
323
|
-
event.args.buyIndex !== buyIndex || event.args.marketIndex !== buy.marketIndex || event.args.recipient.toLowerCase() !== buy.recipient.toLowerCase() ||
|
|
324
|
-
event.args.quoteAsset.toLowerCase() !== planned.plan.markets[buy.marketIndex]?.quoteAsset.toLowerCase() ||
|
|
325
|
-
event.args.quoteSpent > buy.quoteAmountIn || event.args.tokenOut < buy.minTokenOut)
|
|
326
|
-
throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Actual fork initial buy differs from its committed index, budget, destination or minimum");
|
|
327
|
-
quoteSpent.push(event.args.quoteSpent);
|
|
328
|
-
tokenOut.push(event.args.tokenOut);
|
|
329
|
-
}
|
|
330
|
-
returnData = encodeFunctionResult({ abi: launchLifecycleAbi, functionName: transaction.kind === "atomic" ? "launchAtomic" : "activateLaunch", result: {
|
|
331
|
-
launchId: planned.launchId, planHash: progress.planHash, token: progress.token, feeHub: progress.feeHub, rewards: progress.rewards,
|
|
332
|
-
marketCount: progress.marketCount, positionCount: progress.positionCount, quoteSpent, tokenOut,
|
|
333
|
-
} });
|
|
334
|
-
}
|
|
335
|
-
for (const condition of transaction.postconditions) {
|
|
336
|
-
const request = conditionRequest(condition, planned);
|
|
337
|
-
const data = rpcHex(await lifecycleRpc(fork.client, "eth_call", [{ ...request, from: planned.account }, toHex(currentBlock.number)]), "fork postcondition");
|
|
338
|
-
checkCondition(condition, data, planned);
|
|
339
|
-
}
|
|
340
|
-
steps.push({ transactionId: transaction.id, success: true, gasUsed, gasLimit: gas, gasRequired, returnData });
|
|
341
|
-
}
|
|
342
|
-
catch (failure) {
|
|
343
|
-
steps.push({ transactionId: transaction.id, success: false, gasLimit: gas, error: lifecycleFailureReason(failure) });
|
|
344
|
-
break;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
return steps;
|
|
348
|
-
}
|
|
349
|
-
finally {
|
|
350
|
-
try {
|
|
351
|
-
if (impersonated && fork.impersonation !== undefined)
|
|
352
|
-
await lifecycleRpc(fork.client, `${fork.impersonation}_stopImpersonatingAccount`, [planned.account]);
|
|
353
|
-
}
|
|
354
|
-
finally {
|
|
355
|
-
if (await lifecycleRpc(fork.client, "evm_revert", [snapshot]) !== true)
|
|
356
|
-
throw new LifecyclePlanningError("FORK_CLEANUP_FAILED", "Disposable fork snapshot could not be reverted; do not reuse this RPC");
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
function observedFields(value) {
|
|
361
|
-
const keys = Object.keys(value);
|
|
362
|
-
const values = keys.map((key) => value[key]);
|
|
363
|
-
return () => keys.length === Object.keys(value).length && keys.every((key, index) => value[key] === values[index]);
|
|
364
|
-
}
|
|
365
|
-
/** Internal continuation owned by one SDK read invocation. Only its positive
|
|
366
|
-
* capability proof is reused; every candidate executes its entire economic sequence. */
|
|
367
|
-
export function prepareLaunchSimulation(options, block, hasTransactions = true, reviewedEnvelope, selectedGasPrice, sizing) {
|
|
368
|
-
const { client, planned, fork } = options;
|
|
369
|
-
const signal = options.signal;
|
|
370
|
-
if (signal?.aborted)
|
|
371
|
-
throw Object.assign(new Error("Lifecycle simulation cancelled"), { name: "AbortError" });
|
|
372
|
-
const sourceClient = lifecycleSourceClient(client), sourceRequest = sourceClient.request;
|
|
373
|
-
const sourceBatching = sourceClient.supportsReadBatching;
|
|
374
|
-
const reusable = assertLifecycleReadClientOpen(client);
|
|
375
|
-
const plan = planned.plan, planHash = planned.planHash, launchId = planned.launchId;
|
|
376
|
-
const optionsLimits = options.limits, plannedLimits = planned.limits;
|
|
377
|
-
const policy = optionsLimits === undefined ? plannedLimits : optionsLimits;
|
|
378
|
-
const blockMatches = observedFields(block);
|
|
379
|
-
const forkMatches = fork === undefined ? undefined : observedFields(fork);
|
|
380
|
-
const forkRequest = fork?.client.request;
|
|
381
|
-
const plannedMatches = observedFields(planned);
|
|
382
|
-
const context = Object.freeze({ client, account: planned.account, orchestrator: plan.orchestrator, block: Object.freeze({ ...block }), chainId: planned.chainId });
|
|
383
|
-
let closed = false, active = false, consumed = false, capabilityProved = false;
|
|
384
|
-
let capabilityPending;
|
|
385
|
-
const assertContext = () => {
|
|
386
|
-
if (closed)
|
|
387
|
-
throw new LifecyclePlanningError("INVALID_SIMULATION", "Prepared simulation continuation has closed");
|
|
388
|
-
if (signal?.aborted)
|
|
389
|
-
throw Object.assign(new Error("Lifecycle simulation cancelled"), { name: "AbortError" });
|
|
390
|
-
assertLifecycleReadClientOpen(client);
|
|
391
|
-
if (options.client !== client || options.planned !== planned || options.limits !== optionsLimits || options.fork !== fork || options.signal !== signal ||
|
|
392
|
-
lifecycleSourceClient(client) !== sourceClient || sourceClient.request !== sourceRequest || !plannedMatches() || !blockMatches() ||
|
|
393
|
-
sourceClient.supportsReadBatching !== sourceBatching ||
|
|
394
|
-
planned.plan !== plan || planned.account !== context.account || plan.orchestrator !== context.orchestrator || planned.chainId !== context.chainId ||
|
|
395
|
-
planned.limits !== plannedLimits || forkMatches !== undefined && !forkMatches() || fork?.client.request !== forkRequest) {
|
|
396
|
-
throw new LifecyclePlanningError("LIMIT_CONTEXT_MISMATCH", "Prepared simulation differs from its source/account/core/plan/header/policy context");
|
|
397
|
-
}
|
|
398
|
-
if (hashLaunchPlan(plan).toLowerCase() !== planHash.toLowerCase() || hashLaunchIdentity(plan).toLowerCase() !== launchId.toLowerCase()) {
|
|
399
|
-
throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed after its simulation context was bound");
|
|
400
|
-
}
|
|
401
|
-
if (planned.ufg !== undefined && hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash)
|
|
402
|
-
throw new LifecyclePlanningError("PLAN_MUTATED", "UFG treasury config changed after its simulation context was bound");
|
|
403
|
-
};
|
|
404
|
-
const pending = (async () => {
|
|
405
|
-
if (fork !== undefined && lifecycleSourceClient(fork.client) === sourceClient)
|
|
406
|
-
throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
|
|
407
|
-
return lifecycleStage(client, "simulation.context", () => Promise.all([
|
|
408
|
-
resolveLifecycleLimits(context, policy),
|
|
409
|
-
readLifecycleChainId(client),
|
|
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,
|
|
412
|
-
]));
|
|
413
|
-
})().then((observations) => ({ observations }), (failure) => ({ failure }));
|
|
414
|
-
// Refused inputs may discard this read-only preparation before it settles.
|
|
415
|
-
// Its rejection is handled immediately; no capability or economic work starts.
|
|
416
|
-
const run = async (transactions, diagnostic, nextEnvelope) => {
|
|
417
|
-
if (active || consumed && !reusable || !hasTransactions && transactions.length !== 0) {
|
|
418
|
-
closed = true;
|
|
419
|
-
throw new LifecyclePlanningError("INVALID_SIMULATION", "Prepared candidates require sequential consumption by their SDK invocation owner");
|
|
420
|
-
}
|
|
421
|
-
active = true;
|
|
422
|
-
consumed = true;
|
|
423
|
-
try {
|
|
424
|
-
assertContext();
|
|
425
|
-
const envelopeMatches = nextEnvelope === undefined ? undefined : observedFields(nextEnvelope);
|
|
426
|
-
const candidates = transactions.map((transaction) => ({
|
|
427
|
-
transaction, matches: observedFields(transaction),
|
|
428
|
-
dependencies: [...transaction.dependencies],
|
|
429
|
-
conditions: transaction.postconditions.map((condition) => ({ condition, matches: observedFields(condition) })),
|
|
430
|
-
}));
|
|
431
|
-
const result = await pending;
|
|
432
|
-
if ("failure" in result)
|
|
433
|
-
throw result.failure;
|
|
434
|
-
const assertCurrent = () => {
|
|
435
|
-
assertContext();
|
|
436
|
-
result.observations[0].assertCurrent();
|
|
437
|
-
if (envelopeMatches !== undefined && !envelopeMatches())
|
|
438
|
-
throw new LifecyclePlanningError("INVALID_SIMULATION", "Reviewed gas envelope changed during its complete economic proof");
|
|
439
|
-
if (transactions.length !== candidates.length || candidates.some(({ transaction, matches, dependencies, conditions }, index) => transactions[index] !== transaction || !matches() || transaction.dependencies.length !== dependencies.length ||
|
|
440
|
-
dependencies.some((dependency, ordinal) => transaction.dependencies[ordinal] !== dependency) ||
|
|
441
|
-
transaction.postconditions.length !== conditions.length || conditions.some(({ condition, matches: conditionMatches }, ordinal) => transaction.postconditions[ordinal] !== condition || !conditionMatches()))) {
|
|
442
|
-
throw new LifecyclePlanningError("INVALID_SIMULATION", "Candidate transactions changed during their complete economic proof");
|
|
443
|
-
}
|
|
444
|
-
};
|
|
445
|
-
assertCurrent();
|
|
446
|
-
const proveBackend = async (limits, gasPrice) => {
|
|
447
|
-
assertCurrent();
|
|
448
|
-
if (capabilityProved)
|
|
449
|
-
return;
|
|
450
|
-
if (capabilityPending === undefined) {
|
|
451
|
-
const key = `${context.chainId}:nitro-backend`;
|
|
452
|
-
const nativeProof = lifecycleSessionRead(client, key, () => lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, context.block, limits, gasPrice)));
|
|
453
|
-
const proof = nativeProof.then(() => {
|
|
454
|
-
assertContext();
|
|
455
|
-
result.observations[0].assertCurrent();
|
|
456
|
-
capabilityProved = true;
|
|
457
|
-
});
|
|
458
|
-
capabilityPending = proof;
|
|
459
|
-
void proof.then(() => { if (capabilityPending === proof)
|
|
460
|
-
capabilityPending = undefined; }, () => { if (capabilityPending === proof)
|
|
461
|
-
capabilityPending = undefined; });
|
|
462
|
-
}
|
|
463
|
-
await capabilityPending;
|
|
464
|
-
assertCurrent();
|
|
465
|
-
};
|
|
466
|
-
const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, selectedGasPrice, sizing);
|
|
467
|
-
assertCurrent();
|
|
468
|
-
registerLifecycleFinalCheck(client, result.observations[0].assertCurrent);
|
|
469
|
-
return simulation;
|
|
470
|
-
}
|
|
471
|
-
catch (failure) {
|
|
472
|
-
closed = true;
|
|
473
|
-
capabilityProved = false;
|
|
474
|
-
capabilityPending = undefined;
|
|
475
|
-
throw failure;
|
|
476
|
-
}
|
|
477
|
-
finally {
|
|
478
|
-
active = false;
|
|
479
|
-
}
|
|
480
|
-
};
|
|
481
|
-
const simulate = Object.assign((transactions, nextEnvelope) => run(transactions, false, nextEnvelope), { measure: (transactions) => run(transactions, true),
|
|
482
|
-
close() { closed = true; capabilityProved = false; capabilityPending = undefined; } });
|
|
483
|
-
return simulate;
|
|
484
|
-
}
|
|
485
|
-
export async function simulateLaunchTransactions(options, transactions, pinnedBlock, nextEnvelope, sizing) {
|
|
486
|
-
// Standalone calls always produce fresh context, including live chain and fees.
|
|
487
|
-
if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === lifecycleSourceClient(options.client))
|
|
488
|
-
throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
|
|
489
|
-
const block = pinnedBlock ?? await readLifecycleBlock(options.client);
|
|
490
|
-
const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, nextEnvelope, undefined, sizing);
|
|
491
|
-
try {
|
|
492
|
-
return await prepared(transactions, nextEnvelope);
|
|
493
|
-
}
|
|
494
|
-
finally {
|
|
495
|
-
prepared.close();
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
async function simulateLaunchTransactionsWithContext(options, transactions, context, observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, selectedGasPrice, sizing) {
|
|
499
|
-
const { client, planned } = options;
|
|
500
|
-
const sourceClient = lifecycleSourceClient(client);
|
|
501
|
-
const { block } = context;
|
|
502
|
-
const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = observations;
|
|
503
|
-
const base = { confidence: "stateful", executionProof: "failed", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits, deferredTransactionIds: [] };
|
|
504
|
-
if (chainId !== planned.chainId)
|
|
505
|
-
throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
|
|
506
|
-
const accountCode = rpcHex(accountCodeValue, "account code");
|
|
507
|
-
if (accountCode !== "0x") {
|
|
508
|
-
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
|
-
for (const transaction of transactions) {
|
|
511
|
-
if (transaction.from.toLowerCase() !== planned.account.toLowerCase() || BigInt(transaction.chainId) !== planned.chainId)
|
|
512
|
-
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) {
|
|
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" };
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
if (transactions.length === 0) {
|
|
518
|
-
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 <= 0n || nextEnvelope.gas >= 1n << 64n ||
|
|
521
|
-
typeof nextEnvelope.gasPrice !== "bigint" || nextEnvelope.gasPrice < 0n || nextEnvelope.gasPrice >= 1n << 256n)) {
|
|
522
|
-
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Reviewed replay requires one immediate transaction with exact uint64 gas and uint256 price");
|
|
523
|
-
}
|
|
524
|
-
const allTransactions = transactions;
|
|
525
|
-
if (limits.maxSimulationGas !== undefined && nextEnvelope === undefined) {
|
|
526
|
-
let estimatedGas = 0n, count = 0;
|
|
527
|
-
for (const transaction of transactions) {
|
|
528
|
-
estimatedGas += transactionGasEstimate(planned.plan, transaction);
|
|
529
|
-
if (estimatedGas > limits.maxSimulationGas)
|
|
530
|
-
break;
|
|
531
|
-
count += 1;
|
|
532
|
-
}
|
|
533
|
-
transactions = transactions.slice(0, count);
|
|
534
|
-
base.deferredTransactionIds = allTransactions.slice(count).map((transaction) => transaction.id);
|
|
535
|
-
if (count === 0) {
|
|
536
|
-
const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
|
|
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;
|
|
557
|
-
const posterGas = [];
|
|
558
|
-
if (limits.protocol === "nitro") {
|
|
559
|
-
try {
|
|
560
|
-
const [, estimates] = await Promise.all([
|
|
561
|
-
proveBackend(limits, gasPrice),
|
|
562
|
-
!diagnostic && nextEnvelope === undefined ? lifecycleStage(client, "simulation.poster", async () => [buffer((await readNitroPosterGas(transactions[0], context)).posterGas)]) : [],
|
|
563
|
-
]);
|
|
564
|
-
posterGas.push(...estimates);
|
|
565
|
-
}
|
|
566
|
-
catch (failure) {
|
|
567
|
-
if (simulationFailureKind(failure) === "abort")
|
|
568
|
-
throw failure;
|
|
569
|
-
assertCurrent();
|
|
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.` };
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
assertCurrent();
|
|
574
|
-
const discoveryGas = transactions.map(() => {
|
|
575
|
-
const total = limits.protocol === "nitro" ? limits.maxSimulationGas ?? block.gasLimit : limits.executionGasCeiling;
|
|
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 {
|
|
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
|
-
}
|
|
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 };
|
|
703
|
-
}
|
|
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) {
|
|
708
|
-
try {
|
|
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));
|
|
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
|
-
}
|
|
719
|
-
}
|
|
720
|
-
assertCurrent();
|
|
721
|
-
const finalFailure = verifiedSteps.find((step) => !step.success);
|
|
722
|
-
const estimates = [];
|
|
723
|
-
let remainingBalance = finalFailure === undefined ? balance : undefined;
|
|
724
|
-
let affordabilityFailure;
|
|
725
|
-
const feeContext = configuration.estimateDataFee === undefined || sourceClient === client ? context : { ...context, client: sourceClient };
|
|
726
|
-
for (let index = 0; index < verifiedSteps.length; index += 1) {
|
|
727
|
-
const step = verifiedSteps[index];
|
|
728
|
-
const transaction = transactions[index];
|
|
729
|
-
const gasLimit = gasLimits[index];
|
|
730
|
-
if (step === undefined || transaction === undefined || gasLimit === undefined)
|
|
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
|
-
}
|
|
736
|
-
const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit, gasPrice }, feeContext);
|
|
737
|
-
if (dataFee !== undefined && (typeof dataFee !== "bigint" || dataFee < 0n || dataFee >= 1n << 256n))
|
|
738
|
-
throw new LifecyclePlanningError("INVALID_DATA_FEE", "Data-fee oracle must return a nonnegative uint256 bigint amount");
|
|
739
|
-
// Nitro gas includes poster charges. Never add the quoted poster fee again.
|
|
740
|
-
const executionFee = gasLimit * gasPrice;
|
|
741
|
-
const required = transaction.value + executionFee + (dataFee ?? 0n);
|
|
742
|
-
if (required >= 1n << 256n)
|
|
743
|
-
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Transaction value and fee envelope exceed uint256");
|
|
744
|
-
if (remainingBalance !== undefined) {
|
|
745
|
-
if (required > remainingBalance && affordabilityFailure === undefined)
|
|
746
|
-
affordabilityFailure = "Creator native balance cannot fund the remaining transaction value and execution/data fee envelopes";
|
|
747
|
-
remainingBalance -= required;
|
|
748
|
-
}
|
|
749
|
-
const poster = posterGas[index];
|
|
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" } });
|
|
751
|
-
}
|
|
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 };
|
|
753
|
-
}
|
|
754
|
-
//# sourceMappingURL=simulation.js.map
|