@black-market/sdk 0.14.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/lifecycle/authors.js +2 -2
- package/dist/lifecycle/authors.js.map +1 -1
- package/dist/lifecycle/gas-profile.d.ts +35 -0
- package/dist/lifecycle/gas-profile.d.ts.map +1 -0
- package/dist/lifecycle/gas-profile.js +183 -0
- package/dist/lifecycle/gas-profile.js.map +1 -0
- package/dist/lifecycle/index.d.ts +3 -1
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +3 -1
- package/dist/lifecycle/index.js.map +1 -1
- package/dist/lifecycle/opening-buys.d.ts +13 -0
- package/dist/lifecycle/opening-buys.d.ts.map +1 -0
- package/dist/lifecycle/opening-buys.js +171 -0
- package/dist/lifecycle/opening-buys.js.map +1 -0
- package/dist/lifecycle/planner.d.ts +2 -2
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +178 -163
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/presets.d.ts +3 -0
- package/dist/lifecycle/presets.d.ts.map +1 -1
- package/dist/lifecycle/presets.js +3 -0
- package/dist/lifecycle/presets.js.map +1 -1
- package/dist/lifecycle/progress.d.ts +1 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +21 -20
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +24 -19
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +58 -50
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts +3 -4
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +98 -247
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.js +1 -2
- package/dist/lifecycle/snapshot.js.map +1 -1
- package/dist/lifecycle/transactions.d.ts +19 -3
- package/dist/lifecycle/transactions.d.ts.map +1 -1
- package/dist/lifecycle/transactions.js +25 -20
- package/dist/lifecycle/transactions.js.map +1 -1
- package/dist/lifecycle/types.d.ts +53 -45
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/dist/lifecycle/ufg.d.ts.map +1 -1
- package/dist/lifecycle/ufg.js +3 -7
- package/dist/lifecycle/ufg.js.map +1 -1
- package/docs/launch-lifecycle.md +67 -78
- package/docs/lifecycle-gas-profiles.md +90 -0
- package/docs/reference.md +43 -45
- package/examples/README.md +11 -8
- package/examples/launch-example.ts +35 -25
- package/examples/launch-lifecycle-smoke.ts +5 -3
- package/package.json +1 -1
- package/src/lifecycle/authors.ts +2 -2
- package/src/lifecycle/gas-profile.ts +175 -0
- package/src/lifecycle/index.ts +3 -1
- package/src/lifecycle/opening-buys.ts +157 -0
- package/src/lifecycle/planner.ts +164 -151
- package/src/lifecycle/presets.ts +5 -0
- package/src/lifecycle/progress.ts +19 -20
- package/src/lifecycle/rpc.ts +56 -44
- package/src/lifecycle/simulation.ts +85 -231
- package/src/lifecycle/snapshot.ts +1 -2
- package/src/lifecycle/transactions.ts +33 -21
- package/src/lifecycle/types.ts +52 -42
- package/src/lifecycle/ufg.ts +3 -7
package/src/lifecycle/planner.ts
CHANGED
|
@@ -5,23 +5,32 @@ import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi } from "./ab
|
|
|
5
5
|
import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, LIFECYCLE_MAX_ERC404_SUPPLY, parseLaunchPlan, serializeLaunchPlan, type LaunchPlanV1 } from "./schema.js";
|
|
6
6
|
import { readLaunchProgress, readLifecycleExecutionProgress, readLifecycleHeadProgress, readLifecycleProfiles, readLifecycleTokenAtBlock, readPoolBoundHookDeployment } from "./progress.js";
|
|
7
7
|
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion, type AbyssLifecycleMarketConfig, type V4LifecycleMarketConfig, type V4PoolBoundLifecycleMarketConfig } from "./markets.js";
|
|
8
|
-
import { assertLifecycleReadClientOpen,
|
|
9
|
-
import {
|
|
8
|
+
import { assertLifecycleReadClientOpen, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleStage, nitroPosterGasBound, readLifecycleChainId, readLifecycleContract, readNitroPrices, registerLifecycleFinalCheck, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
|
|
9
|
+
import { observedFields, prepareLaunchSimulation, type NextTransactionEnvelope } from "./simulation.js";
|
|
10
10
|
import { LifecyclePlanningError, type BuildNextTransactionOptions, type CanonicalLaunchProgress, type LifecycleBlock, type LifecycleConstructionProfile, type LifecycleFundingPrerequisite, type LifecyclePoolBoundHookDeployment, type LifecycleProfile, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleTransaction, type PlannedLaunch, type PlanLaunchOptions, type PrepareAndPlanLifecycleLaunchOptions, type SimulateLaunchPlanOptions } from "./types.js";
|
|
11
11
|
import { deriveKnownPoolBoundHookDeployment, protectOrderedBuyMinimums, validateBuySlippageBps } from "./calibration.js";
|
|
12
12
|
import { getKnownLifecycleDeployment, getKnownLifecycleProfile } from "./presets.js";
|
|
13
|
-
import {
|
|
13
|
+
import { buildLifecycleCommand, packPreparationBatches, type LifecycleCommand } from "./transactions.js";
|
|
14
14
|
import { assertLifecycleMarketAllocation, assertLifecycleSupplyAllocation } from "./allocations.js";
|
|
15
|
-
import { assertUfgLaunchAccount, hashUfgLaunchConfig, UFG_LAUNCH_PROXY_ADDRESS } from "./ufg-config.js";
|
|
16
|
-
import {
|
|
15
|
+
import { assertUfgLaunchAccount, hashUfgLaunchConfig, ufgPlanPrincipal, UFG_LAUNCH_PROXY_ADDRESS } from "./ufg-config.js";
|
|
16
|
+
import { buildUfgCancelTransaction, validateUfgLaunchPlan } from "./ufg.js";
|
|
17
17
|
import { readLifecyclePlanningBlock } from "./snapshot.js";
|
|
18
18
|
import { mineLifecycleHooks } from "./mining.js";
|
|
19
|
+
import { computeLifecycleOpeningBuys, hasProfiledOpeningMath } from "./opening-buys.js";
|
|
20
|
+
import { deriveLifecycleLaunchCapacity, profileLifecycleTransactions, type ProfiledLaunchCapacity } from "./gas-profile.js";
|
|
21
|
+
import type { LifecycleOpeningBuyQuote } from "./types.js";
|
|
19
22
|
|
|
20
23
|
type ValidatedPlanShape = {
|
|
21
24
|
commitment: Hex;
|
|
22
25
|
ufgConfigHash?: Hex;
|
|
23
26
|
marketConfigs: (AbyssLifecycleMarketConfig | V4LifecycleMarketConfig | V4PoolBoundLifecycleMarketConfig)[];
|
|
24
27
|
};
|
|
28
|
+
type PlanReadContext = {
|
|
29
|
+
block?: LifecycleBlock; progress?: CanonicalLaunchProgress; diagnostic?: boolean;
|
|
30
|
+
finalEnvelope?: NextTransactionEnvelope; capacity?: ProfiledLaunchCapacity;
|
|
31
|
+
openingBuys?: readonly LifecycleOpeningBuyQuote[]; forceSimulation?: boolean; immediate?: boolean;
|
|
32
|
+
};
|
|
33
|
+
const defaultPlanReadContext: PlanReadContext = {};
|
|
25
34
|
|
|
26
35
|
function validatePlanShape(plan: LaunchPlanV1): ValidatedPlanShape {
|
|
27
36
|
// Encoding checks all exact integer widths, addresses, byte strings and tuple fields.
|
|
@@ -137,30 +146,40 @@ async function readPlanMetadata(client: LifecycleRpcClient, plan: LaunchPlanV1,
|
|
|
137
146
|
return { profiles, hookDeployments, reasons: live.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Unlisted lifecycle profile is unavailable") };
|
|
138
147
|
}
|
|
139
148
|
|
|
140
|
-
function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", progress: CanonicalLaunchProgress, approvals: readonly LifecycleTransaction[], batches: readonly number[], ufg?: PlanLaunchOptions["ufg"]): LifecycleTransaction[] {
|
|
149
|
+
function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", progress: CanonicalLaunchProgress, approvals: readonly LifecycleTransaction[], batches: readonly number[], ufg?: PlanLaunchOptions["ufg"], capacity?: ProfiledLaunchCapacity): LifecycleTransaction[] {
|
|
141
150
|
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) return [];
|
|
142
|
-
const prepared = progress.canonical.preparedMarkets;
|
|
143
|
-
const compiled
|
|
144
|
-
|
|
151
|
+
const prepared = progress.canonical.preparedMarkets, route = ufg === undefined ? undefined : { account: ufgPlanPrincipal(plan), config: ufg.config };
|
|
152
|
+
const compiled: LifecycleTransaction[] = [];
|
|
153
|
+
if (mode === "atomic") compiled.push(capacity?.atomic ?? buildLifecycleCommand(plan, { kind: "atomic" }, route));
|
|
154
|
+
else {
|
|
155
|
+
compiled.push(buildLifecycleCommand(plan, { kind: "begin" }, route));
|
|
156
|
+
let first = 0;
|
|
157
|
+
for (const count of batches) { compiled.push(buildLifecycleCommand(plan, { kind: "prepare", first, count }, route)); first += count; }
|
|
158
|
+
compiled.push(capacity?.activation ?? buildLifecycleCommand(plan, { kind: "activate" }, route));
|
|
159
|
+
}
|
|
145
160
|
const commands = compiled.filter((transaction) =>
|
|
146
161
|
(transaction.kind !== "begin" && transaction.kind !== "atomic" || progress.canonical.phase === LifecyclePhase.None) &&
|
|
147
162
|
(transaction.kind !== "prepare" || (transaction.marketStart ?? 0) >= prepared));
|
|
148
163
|
const transactions = progress.canonical.phase === LifecyclePhase.None ? [...approvals, ...commands] : commands;
|
|
149
|
-
return transactions.map((transaction, index) => ({ ...transaction,
|
|
164
|
+
return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1]!.id] }));
|
|
150
165
|
}
|
|
151
166
|
|
|
152
167
|
function attachEstimates(transactions: readonly LifecycleTransaction[], simulation: LifecycleSimulation): LifecycleTransaction[] {
|
|
153
|
-
return transactions.map((transaction) => {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
168
|
+
return transactions.map((transaction, index) => {
|
|
169
|
+
if (index !== 0) return transaction;
|
|
170
|
+
const admission = { admitted: simulation.admitted, confidence: simulation.confidence, executionProof: simulation.executionProof, protocolFit: simulation.protocolFit,
|
|
171
|
+
blockNumber: simulation.blockNumber, blockHash: simulation.blockHash, account: simulation.account, chainId: simulation.chainId, reason: simulation.reason, limits: simulation.limits };
|
|
172
|
+
if (transaction.kind === "approve" || transaction.kind === "approve-reset") return { ...transaction, admission };
|
|
173
|
+
const estimate = simulation.steps.find((step) => step.transactionId === transaction.id)?.estimate ?? simulation.estimate;
|
|
174
|
+
return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, admission };
|
|
157
175
|
});
|
|
158
176
|
}
|
|
159
|
-
type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value"
|
|
177
|
+
type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value"> & Partial<NextTransactionEnvelope>;
|
|
160
178
|
const reviewedTransactionKinds = ["approve-reset", "approve", "atomic", "begin", "prepare", "activate", "cancel"] as const;
|
|
161
179
|
|
|
162
180
|
function readReviewedNextTransaction(transaction: LifecycleTransaction | undefined): ReviewedNextTransaction | undefined {
|
|
163
181
|
if (transaction === undefined) return undefined;
|
|
182
|
+
const walletApproval = transaction?.kind === "approve" || transaction?.kind === "approve-reset";
|
|
164
183
|
if (transaction === null || typeof transaction !== "object" || typeof transaction.id !== "string" ||
|
|
165
184
|
!reviewedTransactionKinds.includes(transaction.kind) ||
|
|
166
185
|
!Number.isSafeInteger(transaction.chainId) || transaction.chainId <= 0 ||
|
|
@@ -168,9 +187,8 @@ function readReviewedNextTransaction(transaction: LifecycleTransaction | undefin
|
|
|
168
187
|
typeof transaction.to !== "string" || !/^0x[\da-fA-F]{40}$/.test(transaction.to) ||
|
|
169
188
|
typeof transaction.data !== "string" || !/^0x(?:[\da-fA-F]{2})*$/.test(transaction.data) ||
|
|
170
189
|
typeof transaction.value !== "bigint" || transaction.value < 0n || transaction.value >= 1n << 256n ||
|
|
171
|
-
typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
|
|
172
|
-
typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n
|
|
173
|
-
transaction.nonce !== undefined && (typeof transaction.nonce !== "bigint" || transaction.nonce < 0n || transaction.nonce >= 1n << 64n)) {
|
|
190
|
+
!walletApproval && (typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
|
|
191
|
+
typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n)) {
|
|
174
192
|
throw new LifecyclePlanningError("INVALID_REVIEWED_TRANSACTION", "Reviewed transaction must contain a well-formed exact chain/account/target/calldata/value and uint64 gas/uint256 price envelope");
|
|
175
193
|
}
|
|
176
194
|
return transaction as ReviewedNextTransaction;
|
|
@@ -179,8 +197,7 @@ function readReviewedNextTransaction(transaction: LifecycleTransaction | undefin
|
|
|
179
197
|
function matchesReviewedNextTransaction(next: LifecycleTransaction | ReviewedNextTransaction | undefined, reviewed: ReviewedNextTransaction): boolean {
|
|
180
198
|
return next !== undefined && next.id === reviewed.id && next.kind === reviewed.kind && next.chainId === reviewed.chainId &&
|
|
181
199
|
next.from.toLowerCase() === reviewed.from.toLowerCase() && next.to.toLowerCase() === reviewed.to.toLowerCase() &&
|
|
182
|
-
next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value
|
|
183
|
-
(reviewed.nonce === undefined || next.nonce === reviewed.nonce);
|
|
200
|
+
next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value;
|
|
184
201
|
}
|
|
185
202
|
|
|
186
203
|
function assertReviewedTransactionUnchanged(options: BuildNextTransactionOptions, reviewed: ReviewedNextTransaction | undefined, next: LifecycleTransaction): void {
|
|
@@ -192,9 +209,9 @@ function assertReviewedTransactionUnchanged(options: BuildNextTransactionOptions
|
|
|
192
209
|
throw new LifecyclePlanningError("CHAIN_MISMATCH", "Execution chain changed during fresh execution or submission proof");
|
|
193
210
|
}
|
|
194
211
|
const current = readReviewedNextTransaction(options.reviewedTransaction);
|
|
195
|
-
if (reviewed === undefined ? current !== undefined : current === undefined || !matchesReviewedNextTransaction(current, reviewed) ||
|
|
196
|
-
|
|
197
|
-
next.gas !== reviewed.gas || next.gasPrice !== reviewed.gasPrice) {
|
|
212
|
+
if (reviewed === undefined ? current !== undefined : current === undefined || !matchesReviewedNextTransaction(current, reviewed) || !matchesReviewedNextTransaction(next, reviewed) ||
|
|
213
|
+
reviewed.kind !== "approve" && reviewed.kind !== "approve-reset" &&
|
|
214
|
+
(current.gas !== reviewed.gas || current.gasPrice !== reviewed.gasPrice || next.gas !== reviewed.gas || next.gasPrice !== reviewed.gasPrice)) {
|
|
198
215
|
throw new LifecyclePlanningError("REVIEWED_TRANSACTION_CHANGED", "Reviewed transaction changed during fresh execution or submission proof");
|
|
199
216
|
}
|
|
200
217
|
}
|
|
@@ -228,7 +245,7 @@ function assertPlanningActive(signal?: AbortSignal): void {
|
|
|
228
245
|
function validatePlanningIntent(options: PlanLaunchOptions): ValidatedPlanShape {
|
|
229
246
|
const { plan, account, mode } = options;
|
|
230
247
|
assertPlanningActive(options.signal);
|
|
231
|
-
if (mode !== "atomic" && mode !== "staged") throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose atomic or
|
|
248
|
+
if (mode !== "atomic" && mode !== "staged") throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose an atomic preference or explicit staged mode");
|
|
232
249
|
if (options.ufg !== undefined) assertUfgLaunchAccount(plan, account);
|
|
233
250
|
else if (account.toLowerCase() !== plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
|
|
234
251
|
if (plan.chainId > BigInt(Number.MAX_SAFE_INTEGER) || plan.chainId <= 0n) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Chain ID must be a positive exact JavaScript integer");
|
|
@@ -272,27 +289,29 @@ function assertFinalPlan(planned: PlannedLaunch, signal?: AbortSignal): void {
|
|
|
272
289
|
if (planned.ufg !== undefined && hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG treasury config changed during asynchronous planning");
|
|
273
290
|
}
|
|
274
291
|
|
|
292
|
+
async function evaluateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], block: LifecycleBlock, controls: PlanReadContext = defaultPlanReadContext): Promise<LifecycleSimulation> {
|
|
293
|
+
const profile = controls.diagnostic ? undefined : await profileLifecycleTransactions(options, transactions, block, controls.finalEnvelope, controls.openingBuys, controls.capacity);
|
|
294
|
+
if (!controls.forceSimulation && profile?.profileOnly) return profile.simulation;
|
|
295
|
+
const prepared = prepareLaunchSimulation(options, block, transactions.length !== 0, controls.immediate);
|
|
296
|
+
try {
|
|
297
|
+
const simulation = controls.diagnostic ? await prepared.measure(transactions) : await prepared(transactions, controls.finalEnvelope);
|
|
298
|
+
return profile === undefined ? simulation : { ...simulation, gasProfile: profile.simulation.gasProfile };
|
|
299
|
+
} finally { prepared.close(); }
|
|
300
|
+
}
|
|
301
|
+
|
|
275
302
|
/** One owner constructs and mines, optionally measures actual ordered buy outputs,
|
|
276
303
|
* then proves only the final protected commitment. No cached execution certificates. */
|
|
277
304
|
export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifecycleLaunchOptions): Promise<PlannedLaunch> {
|
|
278
305
|
const { commitment, ufgConfigHash } = validatePlanningIntent(options);
|
|
279
306
|
if (options.buySlippageBps !== undefined) validateBuySlippageBps(options.buySlippageBps);
|
|
280
|
-
const fixed = options.fixedFinalEnvelope;
|
|
281
|
-
if (fixed !== undefined && (fixed === null || typeof fixed !== "object" || options.mode !== "atomic" || typeof fixed.gas !== "bigint" || fixed.gas <= 0n || fixed.gas >= 1n << 64n ||
|
|
282
|
-
fixed.gasPrice !== undefined && (typeof fixed.gasPrice !== "bigint" || fixed.gasPrice < 0n || fixed.gasPrice >= 1n << 256n))) {
|
|
283
|
-
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires atomic mode, exact positive uint64 gas and optional uint256 price");
|
|
284
|
-
}
|
|
285
|
-
const heldEnvelope = fixed === undefined ? undefined : Object.freeze({ ...fixed });
|
|
286
307
|
const needsMining = options.plan.markets.some((market) => isPoolBoundV4ConfigVersion(market.configVersion));
|
|
287
308
|
const needsProtection = options.buySlippageBps !== undefined && options.plan.buys.length !== 0;
|
|
288
|
-
if (!needsMining && !needsProtection
|
|
309
|
+
if (!needsMining && !needsProtection) return planLaunch(options);
|
|
289
310
|
const draft = parseLaunchPlan(serializeLaunchPlan(options.plan));
|
|
290
311
|
const assertDraft = () => {
|
|
291
312
|
assertPlanningActive(options.signal);
|
|
292
313
|
if (hashLaunchPlan(options.plan).toLowerCase() !== commitment.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous preparation; previous construction is invalid");
|
|
293
314
|
if ((options.ufg === undefined ? undefined : hashUfgLaunchConfig(options.ufg.config)) !== ufgConfigHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG config changed during asynchronous preparation");
|
|
294
|
-
if (fixed === undefined ? options.fixedFinalEnvelope !== undefined : options.fixedFinalEnvelope !== fixed ||
|
|
295
|
-
fixed.gas !== heldEnvelope!.gas || fixed.gasPrice !== heldEnvelope!.gasPrice) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope changed during preparation");
|
|
296
315
|
};
|
|
297
316
|
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
298
317
|
const block = await readLifecyclePlanningBlock(client, draft, options.account, options.ufg !== undefined, true);
|
|
@@ -314,31 +333,31 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
|
|
|
314
333
|
markets[marketIndex] = { ...market, config: encodePoolBoundV4LifecycleMarketConfig({ ...decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion), hookSalt: mined.salt }) };
|
|
315
334
|
}
|
|
316
335
|
assertDraft();
|
|
317
|
-
const executionBlock = block;
|
|
318
|
-
assertDraft();
|
|
319
|
-
const price = heldEnvelope === undefined ? undefined : heldEnvelope.gasPrice ?? rpcQuantity(await readLifecycleGasPrice(client), "gas price");
|
|
320
|
-
const baseFeeEnvelope = (executionBlock.baseFeePerGas ?? 0n) * 2n;
|
|
321
|
-
const finalEnvelope = heldEnvelope === undefined ? undefined : Object.freeze({
|
|
322
|
-
gas: heldEnvelope.gas,
|
|
323
|
-
gasPrice: heldEnvelope.gasPrice ?? (price! > baseFeeEnvelope ? price! : baseFeeEnvelope),
|
|
324
|
-
});
|
|
325
336
|
let plan: LaunchPlanV1 = { ...draft, markets };
|
|
326
337
|
let ownedProgress: CanonicalLaunchProgress | undefined;
|
|
327
|
-
let
|
|
328
|
-
if (needsProtection) {
|
|
338
|
+
let openingBuys: PlanReadContext["openingBuys"];
|
|
339
|
+
if (needsProtection && hasProfiledOpeningMath(plan)) {
|
|
340
|
+
const predictedToken = await readLifecycleTokenAtBlock(client, plan, block);
|
|
341
|
+
openingBuys = computeLifecycleOpeningBuys({ plan, predictedToken });
|
|
342
|
+
const factor = BigInt(10000 - options.buySlippageBps!);
|
|
343
|
+
plan = { ...plan, buys: plan.buys.map((buy, index) => {
|
|
344
|
+
const minTokenOut = openingBuys![index]!.tokenOut * factor / 10000n;
|
|
345
|
+
if (minTokenOut <= 0n) throw new LifecyclePlanningError("INVALID_BUY_OUTPUT", "Each opening buy must produce a positive protected minimum");
|
|
346
|
+
return { ...buy, minTokenOut };
|
|
347
|
+
}) };
|
|
348
|
+
} else if (needsProtection) {
|
|
329
349
|
// A diagnostic minimum is never returned as an admitted or executable plan.
|
|
330
350
|
// Price guards, funding minima, recipients and all ordered budgets stay exact.
|
|
331
351
|
const diagnosticPlan = { ...plan, buys: plan.buys.map((buy) => ({ ...buy, minTokenOut: 1n })) };
|
|
332
|
-
const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client,
|
|
352
|
+
const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, { block, diagnostic: true }));
|
|
333
353
|
assertDraft();
|
|
334
354
|
if (diagnostic.progress.canonical.phase !== LifecyclePhase.None || diagnostic.progress.head.phase !== LifecyclePhase.None) throw new LifecyclePlanningError("LAUNCH_ALREADY_STARTED", "Buy protection cannot replace an already committed launch plan");
|
|
335
355
|
plan = protectOrderedBuyMinimums(diagnostic, options.buySlippageBps!);
|
|
336
356
|
// This is the real SDK-owned None observation, not an assumed new-launch
|
|
337
357
|
// state. No onchain commitment existed at this exact snapshot to replace.
|
|
338
358
|
ownedProgress = { ...diagnostic.progress, planHash: hashLaunchPlan(plan) };
|
|
339
|
-
sizing = new Map(diagnostic.simulation.steps.map((step) => [step.transactionId, step.gasRequired ?? step.gasUsed!]));
|
|
340
359
|
}
|
|
341
|
-
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client,
|
|
360
|
+
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, { block, progress: ownedProgress, openingBuys }));
|
|
342
361
|
assertDraft();
|
|
343
362
|
assertFinalPlan(planned, options.signal);
|
|
344
363
|
if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
|
|
@@ -366,8 +385,10 @@ export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLau
|
|
|
366
385
|
return result;
|
|
367
386
|
}
|
|
368
387
|
|
|
369
|
-
async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient,
|
|
370
|
-
const {
|
|
388
|
+
async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient, readContext: PlanReadContext = defaultPlanReadContext): Promise<PlannedLaunch> {
|
|
389
|
+
const { block: pinnedBlock, progress: ownedProgress, diagnostic = false, openingBuys, forceSimulation = false } = readContext;
|
|
390
|
+
const { plan, account, mode: requestedMode, ufg } = options;
|
|
391
|
+
let mode = requestedMode;
|
|
371
392
|
const { commitment: planHash, marketConfigs, ufgConfigHash } = validatePlanningIntent(options);
|
|
372
393
|
const launchId = hashLaunchIdentity(plan);
|
|
373
394
|
const [block, chainId] = await lifecycleStage(client, "plan.context", () => Promise.all([
|
|
@@ -401,7 +422,7 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
|
|
|
401
422
|
}),
|
|
402
423
|
]));
|
|
403
424
|
assertPlanningActive(options.signal);
|
|
404
|
-
const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown",
|
|
425
|
+
const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [], deferredTransactionIds: [] };
|
|
405
426
|
let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, limits: options.limits,
|
|
406
427
|
allowTopologyGaps: options.allowTopologyGaps === true, allowMissingPermanentTail: options.allowMissingPermanentTail === true };
|
|
407
428
|
const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
|
|
@@ -413,7 +434,7 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
|
|
|
413
434
|
const known = deriveKnownPoolBoundHookDeployment({ plan, token: identity.predictedToken, marketIndex });
|
|
414
435
|
hookDeployments.push({ marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) });
|
|
415
436
|
}
|
|
416
|
-
const waiting = { ...placeholder, confidence: "stateful" as const, reason: !progress.confirmationSafe ? "Canonical
|
|
437
|
+
const waiting = { ...placeholder, confidence: "stateful" as const, reason: !progress.confirmationSafe ? "Canonical launch progress has not reached the selected confirmation depth" : "Wait for canonical receipt confirmations or resolve replacement before requesting another transaction" };
|
|
417
438
|
|
|
418
439
|
return { ...planned, hookDeployments, simulation: waiting };
|
|
419
440
|
}
|
|
@@ -429,89 +450,82 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
|
|
|
429
450
|
readPlanMetadata(client, plan, identity.predictedToken, block),
|
|
430
451
|
]));
|
|
431
452
|
assertFinalPlan(planned, options.signal);
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
let transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg);
|
|
438
|
-
if (fixedFinalEnvelope !== undefined && (transactions.length !== 1 || transactions[0]!.kind !== "atomic")) {
|
|
439
|
-
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires one immediate atomic launch with no preceding approvals");
|
|
453
|
+
const capacity = limits.protocol === "nitro" && limits.arbOSVersion === 61n
|
|
454
|
+
? deriveLifecycleLaunchCapacity({ plan, predictedToken: identity.predictedToken, ufg, headroomBps: limits.headroomBps }, openingBuys) : undefined;
|
|
455
|
+
if (!forceSimulation && progress.canonical.phase === LifecyclePhase.None && mode === "atomic" && capacity !== undefined) {
|
|
456
|
+
const prices = await readNitroPrices(client, block);
|
|
457
|
+
if (capacity.bounds.atomicComputeGasUpperBound + nitroPosterGasBound(capacity.atomic, prices).posterGas > limits.executionGasCeiling) mode = "staged";
|
|
440
458
|
}
|
|
459
|
+
planned = { ...planned, mode, prerequisites: funding.prerequisites, profiles: metadata.profiles, hookDeployments: metadata.hookDeployments };
|
|
460
|
+
const prepared = progress.canonical.preparedMarkets, computeCap = limits.executionGasCeiling;
|
|
461
|
+
let batches: number[] = mode === "staged" ? packPreparationBatches(plan, prepared, computeCap) : [];
|
|
462
|
+
let transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg, capacity);
|
|
441
463
|
if (metadata.reasons.length > 0) {
|
|
442
464
|
|
|
443
465
|
return { ...planned, simulation: { ...placeholder, confidence: "stateful", reason: metadata.reasons.join("; "), failureCategory: "semantic" }, transactions, totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
|
|
444
466
|
}
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
467
|
+
const execute = (candidate: readonly LifecycleTransaction[], quotes = openingBuys) => evaluateLaunchTransactions(
|
|
468
|
+
{ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, candidate, block,
|
|
469
|
+
{ diagnostic, openingBuys: quotes, capacity, forceSimulation });
|
|
470
|
+
let simulation = await execute(transactions);
|
|
471
|
+
assertFinalPlan(planned, options.signal);
|
|
472
|
+
if (!diagnostic && !forceSimulation && requestedMode === "atomic" && mode === "atomic" && progress.canonical.phase === LifecyclePhase.None &&
|
|
473
|
+
simulation.executionProof === "failed" && simulation.failureCategory === "capacity" && simulation.capacityConstraint === "compute") {
|
|
474
|
+
mode = "staged"; planned = { ...planned, mode };
|
|
475
|
+
batches = packPreparationBatches(plan, prepared, computeCap);
|
|
476
|
+
transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg, capacity);
|
|
477
|
+
simulation = await execute(transactions);
|
|
450
478
|
assertFinalPlan(planned, options.signal);
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (!simulation.admitted && simulation.failedTransactionId === "activate") simulation = { ...simulation, reason: `${simulation.reason ?? "Final activation failed"}; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned` };
|
|
469
|
-
} else if (!diagnostic && !simulation.admitted && !(simulation.capacityConstraint === "compute" && simulation.confidence === "provisional")) {
|
|
470
|
-
simulation = { ...simulation, reason: `${simulation.reason ?? "Exact atomic plan is not admitted"}; choose staged explicitly only if empty preparation can be partitioned and complete activation fits` };
|
|
479
|
+
}
|
|
480
|
+
if (mode === "staged") {
|
|
481
|
+
// Only pressure at the native ceiling can invalidate packing. A smaller
|
|
482
|
+
// affordability/RPC/account envelope is not evidence for another wallet write.
|
|
483
|
+
while (!simulation.admitted && simulation.failedTransactionId?.startsWith("prepare:")) {
|
|
484
|
+
const failedIndex = transactions.findIndex((transaction) => transaction.id === simulation.failedTransactionId);
|
|
485
|
+
const failedTransaction = transactions[failedIndex];
|
|
486
|
+
const step = simulation.steps.find((candidate) => candidate.transactionId === simulation.failedTransactionId);
|
|
487
|
+
const count = failedTransaction?.marketCount ?? 0;
|
|
488
|
+
if (count <= 1 || step?.gasUsed === undefined || step.gasLimit === undefined || step.gasLimit < limits.executionGasCeiling || step.gasUsed * 100n < limits.executionGasCeiling * 95n) break;
|
|
489
|
+
let first = 0;
|
|
490
|
+
const batchIndex = batches.findIndex((batch) => { const matches = first === failedTransaction?.marketStart; first += batch; return matches; });
|
|
491
|
+
const left = Math.ceil(count / 2);
|
|
492
|
+
batches.splice(batchIndex, 1, left, count - left);
|
|
493
|
+
transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg, capacity);
|
|
494
|
+
simulation = await execute(transactions, simulation.gasProfile?.initialBuys);
|
|
495
|
+
assertFinalPlan(planned, options.signal);
|
|
471
496
|
}
|
|
472
|
-
|
|
473
|
-
|
|
497
|
+
if (!simulation.admitted && simulation.failedTransactionId === "activate" && simulation.failureCategory === "capacity") simulation = {
|
|
498
|
+
...simulation, capacityConstraint: "activation", reason: "Too many positions or pools for one activation. Reduce them to fit the native transaction ceiling." };
|
|
499
|
+
} else if (!diagnostic && !simulation.admitted) {
|
|
500
|
+
simulation = { ...simulation, reason: simulation.reason ?? "The exact atomic launch is not admitted" };
|
|
501
|
+
}
|
|
502
|
+
return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
|
|
474
503
|
}
|
|
475
504
|
|
|
476
|
-
|
|
477
|
-
if (next.gas === undefined || next.gas <= 0n || next.gas >= 1n << 64n || next.gasPrice === undefined || next.gasPrice < 0n ||
|
|
478
|
-
simulation.executionProof !== "proved" || simulation.protocolFit !== "proved" || !simulation.admitted ||
|
|
479
|
-
next.estimate === undefined || next.estimate.gasLimit !== next.gas || next.estimate.gasPrice !== next.gasPrice ||
|
|
480
|
-
simulation.limits.transactionGasCeiling !== undefined && next.gas > simulation.limits.transactionGasCeiling) {
|
|
481
|
-
throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction has no current-state exact gas/headroom/fee-envelope proof", simulation);
|
|
482
|
-
}
|
|
483
|
-
assertReviewedTransactionUnchanged(options, reviewed, next);
|
|
484
|
-
const submissionClient = options.submissionClient;
|
|
485
|
-
assertPlanningActive(options.signal);
|
|
486
|
-
if (submissionClient === undefined) return { ...next, dependencies: [] };
|
|
487
|
-
try {
|
|
488
|
-
if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId) throw new Error("Active submission RPC chain differs from the reviewed transaction");
|
|
489
|
-
const estimated = rpcQuantity(await lifecycleRpc(submissionClient, "eth_estimateGas", [{
|
|
490
|
-
from: next.from, to: next.to, data: next.data, value: toHex(next.value), gas: toHex(next.gas), gasPrice: toHex(next.gasPrice),
|
|
491
|
-
...(next.nonce === undefined ? {} : { nonce: toHex(next.nonce) }),
|
|
492
|
-
}, "latest"]), "submission gas estimate");
|
|
493
|
-
if (estimated <= 0n || estimated > next.gas) throw new Error("Submission RPC gas estimate exceeds the exact reviewed envelope or is zero");
|
|
494
|
-
assertPlanningActive(options.signal);
|
|
495
|
-
} catch (failure) {
|
|
496
|
-
if (failure instanceof Error && failure.name === "AbortError") throw failure;
|
|
497
|
-
const reason = `Exact next-transaction submission preflight failed: ${lifecycleFailureReason(failure)}`;
|
|
498
|
-
throw new LifecyclePlanningError("SUBMISSION_PREFLIGHT_FAILED", reason, { ...simulation, transportPreflight: "failed", reason });
|
|
499
|
-
}
|
|
500
|
-
if (next.admission === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction omitted its exact simulation admission", simulation);
|
|
505
|
+
function finalizeNextTransaction(options: BuildNextTransactionOptions, next: LifecycleTransaction, reviewed?: ReviewedNextTransaction): LifecycleTransaction {
|
|
501
506
|
assertPlanningActive(options.signal);
|
|
502
507
|
assertReviewedTransactionUnchanged(options, reviewed, next);
|
|
503
|
-
return { ...next, dependencies: []
|
|
508
|
+
return { ...next, dependencies: [] };
|
|
504
509
|
}
|
|
505
510
|
|
|
506
|
-
/** Recovery observes real canonical receipts
|
|
507
|
-
* execution;
|
|
511
|
+
/** Recovery observes real canonical receipts, then evaluates only unfinished
|
|
512
|
+
* execution; the wallet owns its nonce and assignment at send time. */
|
|
508
513
|
export async function buildNextTransaction(options: BuildNextTransactionOptions): Promise<LifecycleTransaction | undefined> {
|
|
514
|
+
const original = options.planned, matches = observedFields(original);
|
|
515
|
+
const fullReviewAdmitted = original.simulation?.admitted === true;
|
|
516
|
+
const assertInput = () => {
|
|
517
|
+
if (options.planned !== original || !matches()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored launch context changed while constructing its next transaction");
|
|
518
|
+
assertFinalPlan(original, options.signal);
|
|
519
|
+
};
|
|
509
520
|
const supplied = readReviewedNextTransaction(options.reviewedTransaction);
|
|
510
521
|
// Snapshot only the reviewed wire fields; caller proofs and commands are never
|
|
511
522
|
// reused, and final checks compare the mutable input with this one local copy.
|
|
523
|
+
const walletApproval = supplied?.kind === "approve" || supplied?.kind === "approve-reset";
|
|
512
524
|
const reviewed = supplied === undefined ? undefined : Object.freeze({ id: supplied.id, kind: supplied.kind, chainId: supplied.chainId,
|
|
513
|
-
from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice
|
|
525
|
+
from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, ...(walletApproval ? {} : { gas: supplied.gas, gasPrice: supplied.gasPrice }) });
|
|
526
|
+
const envelope = reviewed === undefined || walletApproval ? undefined : reviewed as NextTransactionEnvelope;
|
|
514
527
|
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
528
|
+
registerLifecycleFinalCheck(client, assertInput);
|
|
515
529
|
const block = await readLifecyclePlanningBlock(client, options.planned.plan, options.planned.account, options.planned.ufg !== undefined);
|
|
516
530
|
const limitSource = options.limits === undefined ? options.planned.limits : options.limits;
|
|
517
531
|
if (getKnownLifecycleDeployment({ chainId: options.planned.chainId, orchestrator: options.planned.plan.orchestrator }) !== undefined && typeof limitSource !== "function") {
|
|
@@ -524,12 +538,12 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
|
|
|
524
538
|
const progress = await readLifecycleExecutionProgress({ ...options, client }, block);
|
|
525
539
|
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
|
|
526
540
|
// Only the confirmed canonical terminal phase is terminal; a head-only
|
|
527
|
-
// Active observation is not, and the
|
|
528
|
-
//
|
|
541
|
+
// Active observation is not, and the launch-state/receipt confirmation
|
|
542
|
+
// policy below remains in force for every nonterminal case.
|
|
529
543
|
|
|
530
544
|
return undefined;
|
|
531
545
|
}
|
|
532
|
-
if (!progress.confirmationSafe) throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for
|
|
546
|
+
if (!progress.confirmationSafe) throw new LifecyclePlanningError("UNCONFIRMED_STATE", "Wait for the selected canonical launch confirmation depth before constructing the next transaction");
|
|
533
547
|
if (progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed")) throw new LifecyclePlanningError("RECEIPT_PENDING", "Wait for canonical receipt confirmations or resolve replacement before requesting another transaction");
|
|
534
548
|
assertPlanningActive(options.signal);
|
|
535
549
|
if (options.action === "cancel") {
|
|
@@ -538,54 +552,53 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
|
|
|
538
552
|
assertFinalPlan(planned, options.signal);
|
|
539
553
|
const transaction: LifecycleTransaction = planned.ufg !== undefined ? buildUfgCancelTransaction({ plan: planned.plan, preparedMarkets: progress.canonical.preparedMarkets })
|
|
540
554
|
: { id: "cancel", kind: "cancel", chainId: Number(planned.chainId), from: planned.account, to: planned.plan.orchestrator, data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "cancelLaunch", args: [planned.plan] }), value: 0n, dependencies: [], postconditions: [{ kind: "launch", phase: "Cancelled", preparedMarkets: progress.canonical.preparedMarkets }] };
|
|
541
|
-
transaction.nonce = progress.pendingAccountNonce;
|
|
542
555
|
if (reviewed !== undefined && !matchesReviewedNextTransaction(transaction, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical cancellation");
|
|
543
|
-
const simulation = await
|
|
556
|
+
const simulation = await evaluateLaunchTransactions({ client, planned, limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork, signal: options.signal }, [transaction], block, { finalEnvelope: envelope, immediate: true });
|
|
544
557
|
if (!simulation.admitted) throw new LifecyclePlanningError("CANCEL_NOT_ADMITTED", simulation.reason ?? "Cancellation could not be proved against current canonical state", simulation);
|
|
545
558
|
const next = attachEstimates([transaction], simulation)[0];
|
|
546
559
|
if (next === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
|
|
547
|
-
return
|
|
560
|
+
return finalizeNextTransaction(options, next, reviewed);
|
|
548
561
|
}
|
|
549
562
|
const { planned } = options;
|
|
550
563
|
assertFinalPlan(planned, options.signal);
|
|
564
|
+
if (progress.canonical.phase === LifecyclePhase.None && !fullReviewAdmitted) throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", "Complete an admitted full launch review before starting or approving funding; next-only construction does not replan the remaining sequence");
|
|
551
565
|
const funding = progress.canonical.phase === LifecyclePhase.None
|
|
552
566
|
? await readFunding(client, planned.plan, block, planned.account, planned.ufg !== undefined)
|
|
553
567
|
: { prerequisites: [], approvals: [] };
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
let
|
|
558
|
-
|
|
559
|
-
if (
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
568
|
+
let candidate = funding.approvals[0];
|
|
569
|
+
if (candidate === undefined) {
|
|
570
|
+
const prepared = progress.canonical.preparedMarkets;
|
|
571
|
+
let command: LifecycleCommand;
|
|
572
|
+
if (progress.canonical.phase === LifecyclePhase.None) command = { kind: planned.mode === "atomic" ? "atomic" : "begin" };
|
|
573
|
+
else if (prepared < planned.plan.markets.length) {
|
|
574
|
+
let count: number;
|
|
575
|
+
if (reviewed?.kind === "prepare") {
|
|
576
|
+
const span = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
|
|
577
|
+
const first = Number(span?.[1]);
|
|
578
|
+
count = Number(span?.[2]);
|
|
579
|
+
if (first !== prepared) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation does not start at the canonical unfinished market");
|
|
580
|
+
} else {
|
|
581
|
+
const stored = planned.transactions.find((transaction) => transaction.kind === "prepare" && transaction.marketStart! <= prepared && transaction.marketStart! + transaction.marketCount! > prepared);
|
|
582
|
+
if (stored !== undefined) count = stored.marketStart! + stored.marketCount! - prepared;
|
|
583
|
+
else {
|
|
584
|
+
const { limits } = await resolveLifecycleLimits({ client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId }, limitSource);
|
|
585
|
+
const cap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
|
|
586
|
+
count = packPreparationBatches(planned.plan, prepared, cap)[prepared > 0 ? 1 : 0]!;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (!Number.isSafeInteger(count) || count <= 0 || prepared + count > planned.plan.markets.length) throw new LifecyclePlanningError("INVALID_PREPARATION_BATCHES", "Next preparation must be a positive contiguous canonical market span");
|
|
590
|
+
command = { kind: "prepare", first: prepared, count };
|
|
591
|
+
} else command = { kind: "activate" };
|
|
592
|
+
candidate = buildLifecycleCommand(planned.plan, command, planned.ufg === undefined ? undefined : { account: ufgPlanPrincipal(planned.plan), config: planned.ufg.config });
|
|
575
593
|
}
|
|
576
|
-
const candidate = orderedTransactions(planned.plan, planned.mode, progress, funding.approvals, batches, planned.ufg)[0];
|
|
577
|
-
if (candidate === undefined) return undefined;
|
|
578
594
|
if (reviewed !== undefined && !matchesReviewedNextTransaction(candidate, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
|
|
579
|
-
const
|
|
580
|
-
const compute = observed?.computeGasRequired;
|
|
581
|
-
const sizing: GasSizingHints | undefined = reviewed !== undefined || compute === undefined ? undefined : new Map([[candidate.id, compute]]);
|
|
582
|
-
const simulation = await simulateLaunchTransactions({ client, planned, limits: limitSource, fork: options.fork, signal: options.signal }, [candidate], block, reviewed, sizing);
|
|
595
|
+
const simulation = await evaluateLaunchTransactions({ client, planned, limits: limitSource, fork: options.fork, signal: options.signal }, [candidate], block, { finalEnvelope: envelope, immediate: true });
|
|
583
596
|
assertFinalPlan(planned, options.signal);
|
|
584
597
|
if (!simulation.admitted) throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", simulation.reason ?? "Next exact launch transaction is not admitted", simulation);
|
|
585
598
|
const next = attachEstimates([candidate], simulation)[0]!;
|
|
586
|
-
return
|
|
599
|
+
return finalizeNextTransaction(options, next, reviewed);
|
|
587
600
|
});
|
|
588
|
-
|
|
601
|
+
assertInput();
|
|
589
602
|
if (result !== undefined) assertReviewedTransactionUnchanged(options, reviewed, result);
|
|
590
603
|
return result;
|
|
591
604
|
}
|
|
@@ -595,12 +608,12 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
|
|
|
595
608
|
export async function simulateLaunchPlan(options: SimulateLaunchPlanOptions): Promise<LifecycleSimulation> {
|
|
596
609
|
const { client, planned } = options;
|
|
597
610
|
if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan no longer matches its commitment");
|
|
598
|
-
const current = await
|
|
611
|
+
const current = await withLifecycleReadClient(client, (readClient) => planLaunchWithReads({
|
|
599
612
|
client, plan: planned.plan, account: planned.account, mode: planned.mode,
|
|
600
613
|
limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork,
|
|
601
614
|
receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations, signal: options.signal,
|
|
602
615
|
ufg: planned.ufg, allowTopologyGaps: planned.allowTopologyGaps, allowMissingPermanentTail: planned.allowMissingPermanentTail,
|
|
603
|
-
});
|
|
616
|
+
}, readClient, { forceSimulation: true }));
|
|
604
617
|
assertPlannedDeploymentBindings(planned, current);
|
|
605
618
|
return current.simulation;
|
|
606
619
|
}
|
package/src/lifecycle/presets.ts
CHANGED
|
@@ -35,6 +35,9 @@ export enum KnownLifecycleHook {
|
|
|
35
35
|
Object.freeze(KnownLifecycleProfile);
|
|
36
36
|
Object.freeze(KnownLifecycleHook);
|
|
37
37
|
|
|
38
|
+
/** Total launch-transaction gas limit, including Nitro poster gas; unused gas is refunded. */
|
|
39
|
+
export const ROBINHOOD_NATIVE_TRANSACTION_GAS_LIMIT = 32_000_000n;
|
|
40
|
+
|
|
38
41
|
/** Frozen construction metadata, not a current eligibility or execution observation. */
|
|
39
42
|
export type LifecycleProfilePreset = {
|
|
40
43
|
readonly key: KnownLifecycleProfile;
|
|
@@ -61,6 +64,7 @@ export type LifecycleDeploymentPreset = {
|
|
|
61
64
|
readonly tokenFactory: Address;
|
|
62
65
|
readonly tokenFactoryCodeHash: Hex;
|
|
63
66
|
readonly fundingEscrow: Address;
|
|
67
|
+
readonly nativeTransactionGasLimit?: bigint;
|
|
64
68
|
/** Optional SDK-owned read-only acceleration contracts. */
|
|
65
69
|
readonly hookSaltMiner?: Address;
|
|
66
70
|
readonly launchLens?: Address;
|
|
@@ -330,6 +334,7 @@ const knownDeployment: LifecycleDeploymentPreset = Object.freeze({
|
|
|
330
334
|
tokenFactory: "0xd3cE64E49224a9a96075633f63761FE6BE22FE30",
|
|
331
335
|
tokenFactoryCodeHash: "0x6bbbdfe93ec615b7f1e70bac5e69aa890eba5e5f248f25e7022e866b4064d541",
|
|
332
336
|
fundingEscrow: "0x7bc77946CeF52A178583FCe1EEB7751983cEC703",
|
|
337
|
+
nativeTransactionGasLimit: ROBINHOOD_NATIVE_TRANSACTION_GAS_LIMIT,
|
|
333
338
|
hookSaltMiner: "0x2f06176ba739b609b6a7334e2e6647558ba78ad5",
|
|
334
339
|
// LaunchLens/871f9385, L2 deployment block 100095353.
|
|
335
340
|
// tx 0xafdf35fa50d2c3fe08f1ab231a4e5d5e4dd5629c57f14dfe0a3aff5b0b8404ff
|