@black-market/sdk 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -96
- package/dist/lifecycle/index.d.ts +3 -1
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +3 -1
- package/dist/lifecycle/index.js.map +1 -1
- package/dist/lifecycle/lens.d.ts +409 -0
- package/dist/lifecycle/lens.d.ts.map +1 -0
- package/dist/lifecycle/lens.js +30 -0
- package/dist/lifecycle/lens.js.map +1 -0
- package/dist/lifecycle/mining.d.ts +47 -0
- package/dist/lifecycle/mining.d.ts.map +1 -0
- package/dist/lifecycle/mining.js +89 -0
- package/dist/lifecycle/mining.js.map +1 -0
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +121 -78
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/presets.d.ts +3 -0
- package/dist/lifecycle/presets.d.ts.map +1 -1
- package/dist/lifecycle/presets.js +1 -0
- package/dist/lifecycle/presets.js.map +1 -1
- package/dist/lifecycle/progress.d.ts +51 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +149 -73
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +14 -4
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +171 -12
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts +5 -3
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +137 -115
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/snapshot.d.ts +8 -0
- package/dist/lifecycle/snapshot.d.ts.map +1 -0
- package/dist/lifecycle/snapshot.js +79 -0
- package/dist/lifecycle/snapshot.js.map +1 -0
- package/dist/lifecycle/types.d.ts +27 -5
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +627 -1495
- package/docs/reference.md +365 -0
- package/examples/.env.example +3 -2
- package/examples/README.md +171 -552
- package/examples/launch-example-support.ts +11 -3
- package/examples/launch-example.ts +1 -1
- package/examples/launch-lifecycle-smoke.ts +1 -1
- package/package.json +1 -1
- package/src/lifecycle/index.ts +5 -1
- package/src/lifecycle/lens.ts +42 -0
- package/src/lifecycle/mining.ts +80 -0
- package/src/lifecycle/planner.ts +122 -73
- package/src/lifecycle/presets.ts +4 -0
- package/src/lifecycle/progress.ts +132 -59
- package/src/lifecycle/rpc.ts +163 -13
- package/src/lifecycle/simulation.ts +134 -106
- package/src/lifecycle/snapshot.ts +72 -0
- package/src/lifecycle/types.ts +24 -6
|
@@ -3,9 +3,9 @@ import { ABYSS_FEE_TIERS } from "../abyss.js";
|
|
|
3
3
|
import { assertLaunchTopologyCoverage, getTickAtSqrtRatio } from "../launch.js";
|
|
4
4
|
import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi } from "./abi.js";
|
|
5
5
|
import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, LIFECYCLE_MAX_ERC404_SUPPLY, parseLaunchPlan, serializeLaunchPlan } from "./schema.js";
|
|
6
|
-
import { readLaunchProgress, readLifecycleProfiles, readLifecycleTokenAtBlock, readPoolBoundHookDeployment } from "./progress.js";
|
|
7
|
-
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion
|
|
8
|
-
import {
|
|
6
|
+
import { readLaunchProgress, readLifecycleExecutionProgress, readLifecycleHeadProgress, readLifecycleProfiles, readLifecycleTokenAtBlock, readPoolBoundHookDeployment } from "./progress.js";
|
|
7
|
+
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion } from "./markets.js";
|
|
8
|
+
import { assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleRpc, lifecycleStage, readLifecycleChainId, readLifecycleContract, readLifecycleGasPrice, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
|
|
9
9
|
import { prepareLaunchSimulation, simulateLaunchTransactions } from "./simulation.js";
|
|
10
10
|
import { LifecyclePlanningError } from "./types.js";
|
|
11
11
|
import { deriveKnownPoolBoundHookDeployment, protectOrderedBuyMinimums, validateBuySlippageBps } from "./calibration.js";
|
|
@@ -14,6 +14,8 @@ import { buildLaunchTransactions, packPreparationBatches } from "./transactions.
|
|
|
14
14
|
import { assertLifecycleMarketAllocation, assertLifecycleSupplyAllocation } from "./allocations.js";
|
|
15
15
|
import { assertUfgLaunchAccount, hashUfgLaunchConfig, UFG_LAUNCH_PROXY_ADDRESS } from "./ufg-config.js";
|
|
16
16
|
import { buildUfgLaunchTransactions, buildUfgCancelTransaction, validateUfgLaunchPlan } from "./ufg.js";
|
|
17
|
+
import { readLifecyclePlanningBlock } from "./snapshot.js";
|
|
18
|
+
import { mineLifecycleHooks } from "./mining.js";
|
|
17
19
|
function validatePlanShape(plan) {
|
|
18
20
|
// Encoding checks all exact integer widths, addresses, byte strings and tuple fields.
|
|
19
21
|
const commitment = hashLaunchPlan(plan);
|
|
@@ -159,7 +161,7 @@ function orderedTransactions(plan, mode, progress, approvals, batches, ufg) {
|
|
|
159
161
|
const commands = compiled.filter((transaction) => (transaction.kind !== "begin" && transaction.kind !== "atomic" || progress.canonical.phase === LifecyclePhase.None) &&
|
|
160
162
|
(transaction.kind !== "prepare" || (transaction.marketStart ?? 0) >= prepared));
|
|
161
163
|
const transactions = progress.canonical.phase === LifecyclePhase.None ? [...approvals, ...commands] : commands;
|
|
162
|
-
return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1].id] }));
|
|
164
|
+
return transactions.map((transaction, index) => ({ ...transaction, nonce: progress.pendingAccountNonce + BigInt(index), calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1].id] }));
|
|
163
165
|
}
|
|
164
166
|
function attachEstimates(transactions, simulation) {
|
|
165
167
|
return transactions.map((transaction) => {
|
|
@@ -180,7 +182,8 @@ function readReviewedNextTransaction(transaction) {
|
|
|
180
182
|
typeof transaction.data !== "string" || !/^0x(?:[\da-fA-F]{2})*$/.test(transaction.data) ||
|
|
181
183
|
typeof transaction.value !== "bigint" || transaction.value < 0n || transaction.value >= 1n << 256n ||
|
|
182
184
|
typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
|
|
183
|
-
typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n
|
|
185
|
+
typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n ||
|
|
186
|
+
transaction.nonce !== undefined && (typeof transaction.nonce !== "bigint" || transaction.nonce < 0n || transaction.nonce >= 1n << 64n)) {
|
|
184
187
|
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");
|
|
185
188
|
}
|
|
186
189
|
return transaction;
|
|
@@ -188,7 +191,8 @@ function readReviewedNextTransaction(transaction) {
|
|
|
188
191
|
function matchesReviewedNextTransaction(next, reviewed) {
|
|
189
192
|
return next !== undefined && next.id === reviewed.id && next.kind === reviewed.kind && next.chainId === reviewed.chainId &&
|
|
190
193
|
next.from.toLowerCase() === reviewed.from.toLowerCase() && next.to.toLowerCase() === reviewed.to.toLowerCase() &&
|
|
191
|
-
next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value
|
|
194
|
+
next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value &&
|
|
195
|
+
(reviewed.nonce === undefined || next.nonce === reviewed.nonce);
|
|
192
196
|
}
|
|
193
197
|
function assertReviewedTransactionUnchanged(options, reviewed, next) {
|
|
194
198
|
if (options.planned.ufg !== undefined)
|
|
@@ -297,9 +301,15 @@ export async function prepareAndPlanLifecycleLaunch(options) {
|
|
|
297
301
|
const { commitment, ufgConfigHash } = validatePlanningIntent(options);
|
|
298
302
|
if (options.buySlippageBps !== undefined)
|
|
299
303
|
validateBuySlippageBps(options.buySlippageBps);
|
|
304
|
+
const fixed = options.fixedFinalEnvelope;
|
|
305
|
+
if (fixed !== undefined && (fixed === null || typeof fixed !== "object" || options.mode !== "atomic" || typeof fixed.gas !== "bigint" || fixed.gas <= 0n || fixed.gas >= 1n << 64n ||
|
|
306
|
+
fixed.gasPrice !== undefined && (typeof fixed.gasPrice !== "bigint" || fixed.gasPrice < 0n || fixed.gasPrice >= 1n << 256n))) {
|
|
307
|
+
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires atomic mode, exact positive uint64 gas and optional uint256 price");
|
|
308
|
+
}
|
|
309
|
+
const heldEnvelope = fixed === undefined ? undefined : Object.freeze({ ...fixed });
|
|
300
310
|
const needsMining = options.plan.markets.some((market) => isPoolBoundV4ConfigVersion(market.configVersion));
|
|
301
311
|
const needsProtection = options.buySlippageBps !== undefined && options.plan.buys.length !== 0;
|
|
302
|
-
if (!needsMining && !needsProtection)
|
|
312
|
+
if (!needsMining && !needsProtection && fixed === undefined)
|
|
303
313
|
return planLaunch(options);
|
|
304
314
|
const draft = parseLaunchPlan(serializeLaunchPlan(options.plan));
|
|
305
315
|
const assertDraft = () => {
|
|
@@ -308,42 +318,48 @@ export async function prepareAndPlanLifecycleLaunch(options) {
|
|
|
308
318
|
throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous preparation; previous construction is invalid");
|
|
309
319
|
if ((options.ufg === undefined ? undefined : hashUfgLaunchConfig(options.ufg.config)) !== ufgConfigHash)
|
|
310
320
|
throw new LifecyclePlanningError("PLAN_MUTATED", "UFG config changed during asynchronous preparation");
|
|
321
|
+
if (fixed === undefined ? options.fixedFinalEnvelope !== undefined : options.fixedFinalEnvelope !== fixed ||
|
|
322
|
+
fixed.gas !== heldEnvelope.gas || fixed.gasPrice !== heldEnvelope.gasPrice)
|
|
323
|
+
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope changed during preparation");
|
|
311
324
|
};
|
|
312
|
-
|
|
313
|
-
const
|
|
325
|
+
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
326
|
+
const block = await readLifecyclePlanningBlock(client, draft, options.account, options.ufg !== undefined, true);
|
|
327
|
+
const chainId = await readLifecycleChainId(client);
|
|
314
328
|
if (chainId !== draft.chainId)
|
|
315
329
|
throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the economic plan being finalized");
|
|
316
330
|
const metadata = needsMining ? await Promise.all(draft.markets.map((market, marketIndex) => isPoolBoundV4ConfigVersion(market.configVersion)
|
|
317
331
|
? readPoolBoundHookDeployment({ client, plan: draft, marketIndex }, block) : undefined)) : [];
|
|
318
332
|
assertDraft();
|
|
319
333
|
const markets = [...draft.markets];
|
|
320
|
-
const minedDeployments =
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
334
|
+
const minedDeployments = await mineLifecycleHooks({ client, block,
|
|
335
|
+
miner: getKnownLifecycleDeployment({ chainId, orchestrator: draft.orchestrator })?.hookSaltMiner,
|
|
336
|
+
deployments: metadata.flatMap((deployment, marketIndex) => deployment === undefined ? [] : [{ marketIndex, ...deployment }]),
|
|
337
|
+
signal: options.signal, onProgress: options.onProgress });
|
|
338
|
+
for (const mined of minedDeployments) {
|
|
324
339
|
assertDraft();
|
|
325
|
-
const
|
|
326
|
-
deployer: deployment.deployer, initCodeHash: deployment.initCodeHash, startSalt: BigInt(deployment.salt), signal: options.signal,
|
|
327
|
-
onProgress: options.onProgress === undefined ? undefined : (progress) => options.onProgress?.({ marketIndex, ...progress }),
|
|
328
|
-
});
|
|
340
|
+
const marketIndex = mined.marketIndex;
|
|
329
341
|
const market = markets[marketIndex];
|
|
330
342
|
if (!isPoolBoundV4ConfigVersion(market.configVersion))
|
|
331
343
|
throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Mined market lost its explicit pool-bound version");
|
|
332
344
|
markets[marketIndex] = { ...market, config: encodePoolBoundV4LifecycleMarketConfig({ ...decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion), hookSalt: mined.salt }) };
|
|
333
|
-
minedDeployments.push({ marketIndex, deployer: deployment.deployer, initCodeHash: deployment.initCodeHash, ...mined });
|
|
334
345
|
}
|
|
335
346
|
assertDraft();
|
|
336
|
-
|
|
337
|
-
// start at a fresh post-mining snapshot and share it through final protection.
|
|
338
|
-
const executionBlock = needsMining ? await readLifecycleBlock(client) : block;
|
|
347
|
+
const executionBlock = block;
|
|
339
348
|
assertDraft();
|
|
349
|
+
const price = heldEnvelope === undefined ? undefined : heldEnvelope.gasPrice ?? rpcQuantity(await readLifecycleGasPrice(client), "gas price");
|
|
350
|
+
const baseFeeEnvelope = (executionBlock.baseFeePerGas ?? 0n) * 2n;
|
|
351
|
+
const finalEnvelope = heldEnvelope === undefined ? undefined : Object.freeze({
|
|
352
|
+
gas: heldEnvelope.gas,
|
|
353
|
+
gasPrice: heldEnvelope.gasPrice ?? (price > baseFeeEnvelope ? price : baseFeeEnvelope),
|
|
354
|
+
});
|
|
340
355
|
let plan = { ...draft, markets };
|
|
341
356
|
let ownedProgress;
|
|
357
|
+
let sizing;
|
|
342
358
|
if (needsProtection) {
|
|
343
359
|
// A diagnostic minimum is never returned as an admitted or executable plan.
|
|
344
360
|
// Price guards, funding minima, recipients and all ordered budgets stay exact.
|
|
345
361
|
const diagnosticPlan = { ...plan, buys: plan.buys.map((buy) => ({ ...buy, minTokenOut: 1n })) };
|
|
346
|
-
const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, executionBlock, undefined, true));
|
|
362
|
+
const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, executionBlock, undefined, true, finalEnvelope));
|
|
347
363
|
assertDraft();
|
|
348
364
|
if (diagnostic.progress.canonical.phase !== LifecyclePhase.None || diagnostic.progress.head.phase !== LifecyclePhase.None)
|
|
349
365
|
throw new LifecyclePlanningError("LAUNCH_ALREADY_STARTED", "Buy protection cannot replace an already committed launch plan");
|
|
@@ -351,8 +367,9 @@ export async function prepareAndPlanLifecycleLaunch(options) {
|
|
|
351
367
|
// This is the real SDK-owned None observation, not an assumed new-launch
|
|
352
368
|
// state. No onchain commitment existed at this exact snapshot to replace.
|
|
353
369
|
ownedProgress = { ...diagnostic.progress, planHash: hashLaunchPlan(plan) };
|
|
370
|
+
sizing = new Map(diagnostic.simulation.steps.map((step) => [step.transactionId, step.gasRequired ?? step.gasUsed]));
|
|
354
371
|
}
|
|
355
|
-
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress));
|
|
372
|
+
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress, false, finalEnvelope, sizing));
|
|
356
373
|
assertDraft();
|
|
357
374
|
assertFinalPlan(planned, options.signal);
|
|
358
375
|
if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
|
|
@@ -366,21 +383,26 @@ export async function prepareAndPlanLifecycleLaunch(options) {
|
|
|
366
383
|
throw new LifecyclePlanningError("PROTECTED_PLAN_NOT_ADMITTED", planned.simulation.reason ?? "The final protected launch did not establish exact execution proof", planned.simulation);
|
|
367
384
|
return planned;
|
|
368
385
|
});
|
|
386
|
+
assertDraft();
|
|
387
|
+
assertFinalPlan(result, options.signal);
|
|
388
|
+
return result;
|
|
369
389
|
}
|
|
370
390
|
/** Independently fresh current-state admission of an already finalized commitment. No broadcasts. */
|
|
371
391
|
export async function planLaunch(options) {
|
|
372
|
-
|
|
392
|
+
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
373
393
|
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
|
|
374
394
|
assertFinalPlan(planned, options.signal);
|
|
375
395
|
return planned;
|
|
376
396
|
});
|
|
397
|
+
assertFinalPlan(result, options.signal);
|
|
398
|
+
return result;
|
|
377
399
|
}
|
|
378
|
-
async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress, diagnostic = false,
|
|
400
|
+
async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress, diagnostic = false, fixedFinalEnvelope, sizing) {
|
|
379
401
|
const { plan, account, mode, ufg } = options;
|
|
380
402
|
const { commitment: planHash, marketConfigs, ufgConfigHash } = validatePlanningIntent(options);
|
|
381
403
|
const launchId = hashLaunchIdentity(plan);
|
|
382
404
|
const [block, chainId] = await lifecycleStage(client, "plan.context", () => Promise.all([
|
|
383
|
-
pinnedBlock ??
|
|
405
|
+
pinnedBlock ?? readLifecyclePlanningBlock(client, plan, account, ufg !== undefined), readLifecycleChainId(client),
|
|
384
406
|
]));
|
|
385
407
|
if (chainId !== plan.chainId)
|
|
386
408
|
throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
|
|
@@ -400,7 +422,10 @@ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress,
|
|
|
400
422
|
preset ?? readTokenFactoryBinding(client, plan.orchestrator, block),
|
|
401
423
|
resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
|
|
402
424
|
lifecycleStage(client, "plan.progress", async () => {
|
|
403
|
-
const predictedToken = await
|
|
425
|
+
const [predictedToken] = await Promise.all([
|
|
426
|
+
readLifecycleTokenAtBlock(client, plan, block),
|
|
427
|
+
ownedProgress === undefined ? readLifecycleHeadProgress(client, plan, account, block) : undefined,
|
|
428
|
+
]);
|
|
404
429
|
const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1,
|
|
405
430
|
...(ufg === undefined ? {} : { ufg: { config: ufg.config, configHash: ufgConfigHash } }) };
|
|
406
431
|
if (ufg !== undefined)
|
|
@@ -426,11 +451,9 @@ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress,
|
|
|
426
451
|
hookDeployments.push({ marketIndex, ...(known?.deployment ?? await readPoolBoundHookDeployment({ client, plan, marketIndex }, block)) });
|
|
427
452
|
}
|
|
428
453
|
const waiting = { ...placeholder, confidence: "stateful", reason: !progress.confirmationSafe ? "Canonical confirmed progress/account nonce has not caught up with head or pending transactions; wait for the selected confirmation depth" : "Wait for canonical receipt confirmations or resolve replacement before requesting another transaction" };
|
|
429
|
-
await assertLifecycleBlock(client, block, plan.chainId);
|
|
430
454
|
return { ...planned, hookDeployments, simulation: waiting };
|
|
431
455
|
}
|
|
432
456
|
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
|
|
433
|
-
await assertLifecycleBlock(client, block, plan.chainId);
|
|
434
457
|
return { ...planned, simulation: { ...placeholder, confidence: "stateful", executionProof: "proved", protocolFit: "proved", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
|
|
435
458
|
}
|
|
436
459
|
// Topology is a creation policy, not a retroactive obstacle to recovery of
|
|
@@ -447,41 +470,29 @@ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress,
|
|
|
447
470
|
const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
|
|
448
471
|
let batches = [];
|
|
449
472
|
if (mode === "staged")
|
|
450
|
-
batches =
|
|
451
|
-
: [...(prepared > 0 ? [prepared] : []), ...(prepared < plan.markets.length ? [plan.markets.length - prepared] : [])];
|
|
452
|
-
if (reviewed !== undefined && reviewed.kind === "prepare") {
|
|
453
|
-
// Canonical progress fixes the start; the exact held calldata fixes its span.
|
|
454
|
-
// Never rediscover a batch or enlarge a reviewed transaction.
|
|
455
|
-
const match = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
|
|
456
|
-
const start = Number(match?.[1]), count = Number(match?.[2]);
|
|
457
|
-
if (start !== prepared || !Number.isSafeInteger(count) || count <= 0 || start + count > plan.markets.length)
|
|
458
|
-
throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation differs from canonical unfinished progress");
|
|
459
|
-
batches = [...(prepared > 0 ? [prepared] : []), count, ...(start + count < plan.markets.length ? [plan.markets.length - start - count] : [])];
|
|
460
|
-
}
|
|
473
|
+
batches = packPreparationBatches(plan, prepared, computeCap);
|
|
461
474
|
let transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg);
|
|
462
|
-
if (
|
|
463
|
-
|
|
464
|
-
throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
|
|
465
|
-
transactions = transactions.slice(0, 1);
|
|
475
|
+
if (fixedFinalEnvelope !== undefined && (transactions.length !== 1 || transactions[0].kind !== "atomic")) {
|
|
476
|
+
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires one immediate atomic launch with no preceding approvals");
|
|
466
477
|
}
|
|
467
478
|
if (metadata.reasons.length > 0) {
|
|
468
|
-
await assertLifecycleBlock(client, block, plan.chainId);
|
|
469
479
|
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) };
|
|
470
480
|
}
|
|
471
|
-
const
|
|
481
|
+
const envelope = diagnostic ? undefined : fixedFinalEnvelope;
|
|
482
|
+
const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block, true, envelope, fixedFinalEnvelope?.gasPrice, sizing);
|
|
472
483
|
try {
|
|
473
|
-
const execute = diagnostic ? preparedSimulation.measure : (candidate) => preparedSimulation(candidate,
|
|
484
|
+
const execute = diagnostic ? preparedSimulation.measure : (candidate) => preparedSimulation(candidate, envelope);
|
|
474
485
|
let simulation = await execute(transactions);
|
|
475
486
|
assertFinalPlan(planned, options.signal);
|
|
476
487
|
if (mode === "staged") {
|
|
477
|
-
// Only
|
|
478
|
-
//
|
|
479
|
-
while (
|
|
488
|
+
// Only pressure at the native ceiling can invalidate packing. A smaller
|
|
489
|
+
// affordability/RPC/account envelope is not evidence for another wallet write.
|
|
490
|
+
while (!simulation.admitted && simulation.failedTransactionId?.startsWith("prepare:")) {
|
|
480
491
|
const failedIndex = transactions.findIndex((transaction) => transaction.id === simulation.failedTransactionId);
|
|
481
492
|
const failedTransaction = transactions[failedIndex];
|
|
482
493
|
const step = simulation.steps.find((candidate) => candidate.transactionId === simulation.failedTransactionId);
|
|
483
494
|
const count = failedTransaction?.marketCount ?? 0;
|
|
484
|
-
if (count <= 1 || step?.
|
|
495
|
+
if (count <= 1 || step?.gasUsed === undefined || step.gasLimit === undefined || step.gasLimit < limits.executionGasCeiling || step.gasUsed * 100n < limits.executionGasCeiling * 95n)
|
|
485
496
|
break;
|
|
486
497
|
let first = 0;
|
|
487
498
|
const batchIndex = batches.findIndex((batch) => { const matches = first === failedTransaction?.marketStart; first += batch; return matches; });
|
|
@@ -497,9 +508,6 @@ async function planLaunchWithReads(options, client, pinnedBlock, ownedProgress,
|
|
|
497
508
|
else if (!diagnostic && !simulation.admitted && !(simulation.capacityConstraint === "compute" && simulation.confidence === "provisional")) {
|
|
498
509
|
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` };
|
|
499
510
|
}
|
|
500
|
-
if (reviewed !== undefined && !matchesReviewedNextTransaction(transactions[0], reviewed)) {
|
|
501
|
-
throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
|
|
502
|
-
}
|
|
503
511
|
return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
|
|
504
512
|
}
|
|
505
513
|
finally {
|
|
@@ -523,11 +531,10 @@ async function preflightNextTransaction(options, next, simulation, reviewed) {
|
|
|
523
531
|
throw new Error("Active submission RPC chain differs from the reviewed transaction");
|
|
524
532
|
const estimated = rpcQuantity(await lifecycleRpc(submissionClient, "eth_estimateGas", [{
|
|
525
533
|
from: next.from, to: next.to, data: next.data, value: toHex(next.value), gas: toHex(next.gas), gasPrice: toHex(next.gasPrice),
|
|
534
|
+
...(next.nonce === undefined ? {} : { nonce: toHex(next.nonce) }),
|
|
526
535
|
}, "latest"]), "submission gas estimate");
|
|
527
536
|
if (estimated <= 0n || estimated > next.gas)
|
|
528
537
|
throw new Error("Submission RPC gas estimate exceeds the exact reviewed envelope or is zero");
|
|
529
|
-
if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId)
|
|
530
|
-
throw new Error("Active submission RPC chain changed during preflight");
|
|
531
538
|
assertPlanningActive(options.signal);
|
|
532
539
|
}
|
|
533
540
|
catch (failure) {
|
|
@@ -536,14 +543,6 @@ async function preflightNextTransaction(options, next, simulation, reviewed) {
|
|
|
536
543
|
const reason = `Exact next-transaction submission preflight failed: ${lifecycleFailureReason(failure)}`;
|
|
537
544
|
throw new LifecyclePlanningError("SUBMISSION_PREFLIGHT_FAILED", reason, { ...simulation, transportPreflight: "failed", reason });
|
|
538
545
|
}
|
|
539
|
-
const [canonical, chainId] = await Promise.all([
|
|
540
|
-
readLifecycleBlock(options.client, toHex(simulation.blockNumber)),
|
|
541
|
-
lifecycleRpc(options.client, "eth_chainId"),
|
|
542
|
-
]);
|
|
543
|
-
if (canonical.hash.toLowerCase() !== simulation.blockHash.toLowerCase())
|
|
544
|
-
throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch proof changed during submission preflight; rebuild from canonical progress");
|
|
545
|
-
if (rpcQuantity(chainId, "chain ID") !== simulation.chainId)
|
|
546
|
-
throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during submission preflight");
|
|
547
546
|
if (next.admission === undefined)
|
|
548
547
|
throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction omitted its exact simulation admission", simulation);
|
|
549
548
|
assertPlanningActive(options.signal);
|
|
@@ -557,15 +556,22 @@ export async function buildNextTransaction(options) {
|
|
|
557
556
|
// Snapshot only the reviewed wire fields; caller proofs and commands are never
|
|
558
557
|
// reused, and final checks compare the mutable input with this one local copy.
|
|
559
558
|
const reviewed = supplied === undefined ? undefined : Object.freeze({ id: supplied.id, kind: supplied.kind, chainId: supplied.chainId,
|
|
560
|
-
from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice });
|
|
561
|
-
|
|
562
|
-
const block = await
|
|
563
|
-
const
|
|
559
|
+
from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice, nonce: supplied.nonce });
|
|
560
|
+
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
561
|
+
const block = await readLifecyclePlanningBlock(client, options.planned.plan, options.planned.account, options.planned.ufg !== undefined);
|
|
562
|
+
const limitSource = options.limits === undefined ? options.planned.limits : options.limits;
|
|
563
|
+
if (getKnownLifecycleDeployment({ chainId: options.planned.chainId, orchestrator: options.planned.plan.orchestrator }) !== undefined && typeof limitSource !== "function") {
|
|
564
|
+
await Promise.all([
|
|
565
|
+
readLifecycleTokenAtBlock(client, options.planned.plan, block),
|
|
566
|
+
readLifecycleHeadProgress(client, options.planned.plan, options.planned.account, block),
|
|
567
|
+
resolveLifecycleLimits({ client, account: options.planned.account, orchestrator: options.planned.plan.orchestrator, block, chainId: options.planned.chainId }, limitSource),
|
|
568
|
+
]);
|
|
569
|
+
}
|
|
570
|
+
const progress = await readLifecycleExecutionProgress({ ...options, client }, block);
|
|
564
571
|
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
|
|
565
572
|
// Only the confirmed canonical terminal phase is terminal; a head-only
|
|
566
573
|
// Active observation is not, and the confirmation/nonce/receipt gates below
|
|
567
574
|
// remain in force for every nonterminal case.
|
|
568
|
-
await assertLifecycleBlock(client, block, options.planned.chainId);
|
|
569
575
|
return undefined;
|
|
570
576
|
}
|
|
571
577
|
if (!progress.confirmationSafe)
|
|
@@ -580,6 +586,7 @@ export async function buildNextTransaction(options) {
|
|
|
580
586
|
assertFinalPlan(planned, options.signal);
|
|
581
587
|
const transaction = planned.ufg !== undefined ? buildUfgCancelTransaction({ plan: planned.plan, preparedMarkets: progress.canonical.preparedMarkets })
|
|
582
588
|
: { 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 }] };
|
|
589
|
+
transaction.nonce = progress.pendingAccountNonce;
|
|
583
590
|
if (reviewed !== undefined && !matchesReviewedNextTransaction(transaction, reviewed))
|
|
584
591
|
throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical cancellation");
|
|
585
592
|
const simulation = await simulateLaunchTransactions({ client, planned, limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork, signal: options.signal }, [transaction], block, reviewed);
|
|
@@ -590,17 +597,53 @@ export async function buildNextTransaction(options) {
|
|
|
590
597
|
throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Cancellation omitted its exact simulation admission", simulation);
|
|
591
598
|
return preflightNextTransaction(options, next, simulation, reviewed);
|
|
592
599
|
}
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
if (
|
|
600
|
+
const { planned } = options;
|
|
601
|
+
assertFinalPlan(planned, options.signal);
|
|
602
|
+
const funding = progress.canonical.phase === LifecyclePhase.None
|
|
603
|
+
? await readFunding(client, planned.plan, block, planned.account, planned.ufg !== undefined)
|
|
604
|
+
: { prerequisites: [], approvals: [] };
|
|
605
|
+
const prepared = progress.canonical.preparedMarkets;
|
|
606
|
+
let batches = [];
|
|
607
|
+
if (planned.mode === "staged") {
|
|
608
|
+
let suffixStart = prepared;
|
|
609
|
+
const prefix = prepared > 0 ? [prepared] : [];
|
|
610
|
+
if (reviewed?.kind === "prepare") {
|
|
611
|
+
const span = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
|
|
612
|
+
const first = Number(span?.[1]), count = Number(span?.[2]);
|
|
613
|
+
if (first !== prepared || !Number.isSafeInteger(count) || count <= 0 || first + count > planned.plan.markets.length)
|
|
614
|
+
throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation does not start at the canonical unfinished market");
|
|
615
|
+
prefix.push(count);
|
|
616
|
+
suffixStart += count;
|
|
617
|
+
}
|
|
618
|
+
const suffix = planned.transactions
|
|
619
|
+
.filter((transaction) => transaction.kind === "prepare" && transaction.marketStart + transaction.marketCount > suffixStart)
|
|
620
|
+
.map((transaction) => transaction.marketStart + transaction.marketCount - Math.max(transaction.marketStart, suffixStart));
|
|
621
|
+
if (suffix.length === 0 && suffixStart < planned.plan.markets.length) {
|
|
622
|
+
const { limits } = await resolveLifecycleLimits({ client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId }, limitSource);
|
|
623
|
+
const cap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
|
|
624
|
+
suffix.push(...packPreparationBatches(planned.plan, suffixStart, cap).slice(suffixStart > 0 ? 1 : 0));
|
|
625
|
+
}
|
|
626
|
+
batches = [...prefix, ...suffix];
|
|
627
|
+
}
|
|
628
|
+
const candidate = orderedTransactions(planned.plan, planned.mode, progress, funding.approvals, batches, planned.ufg)[0];
|
|
629
|
+
if (candidate === undefined)
|
|
601
630
|
return undefined;
|
|
602
|
-
|
|
631
|
+
if (reviewed !== undefined && !matchesReviewedNextTransaction(candidate, reviewed))
|
|
632
|
+
throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
|
|
633
|
+
const observed = planned.simulation.executionProof === "proved" ? planned.simulation.steps.find((step) => step.transactionId === candidate.id && step.success) : undefined;
|
|
634
|
+
const compute = observed?.computeGasRequired;
|
|
635
|
+
const sizing = reviewed !== undefined || compute === undefined ? undefined : new Map([[candidate.id, compute]]);
|
|
636
|
+
const simulation = await simulateLaunchTransactions({ client, planned, limits: limitSource, fork: options.fork, signal: options.signal }, [candidate], block, reviewed, sizing);
|
|
637
|
+
assertFinalPlan(planned, options.signal);
|
|
638
|
+
if (!simulation.admitted)
|
|
639
|
+
throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", simulation.reason ?? "Next exact launch transaction is not admitted", simulation);
|
|
640
|
+
const next = attachEstimates([candidate], simulation)[0];
|
|
641
|
+
return preflightNextTransaction(options, next, simulation, reviewed);
|
|
603
642
|
});
|
|
643
|
+
assertFinalPlan(options.planned, options.signal);
|
|
644
|
+
if (result !== undefined)
|
|
645
|
+
assertReviewedTransactionUnchanged(options, reviewed, result);
|
|
646
|
+
return result;
|
|
604
647
|
}
|
|
605
648
|
/** Re-simulates only canonical unfinished work with the same submitted receipt
|
|
606
649
|
* evidence and confirmation depth as build-next, never silently dropping context. */
|