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