@black-market/sdk 0.12.0 → 0.13.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/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +59 -24
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/progress.d.ts +3 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +120 -55
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +8 -2
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +165 -9
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts +1 -1
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +11 -10
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/types.d.ts +12 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +605 -1495
- package/docs/reference.md +332 -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/planner.ts +58 -23
- package/src/lifecycle/progress.ts +110 -47
- package/src/lifecycle/rpc.ts +158 -10
- package/src/lifecycle/simulation.ts +11 -9
- package/src/lifecycle/types.ts +9 -1
|
@@ -607,9 +607,7 @@ export function createLocalLaunchWallet({ privateKey, creator, chainId, orchestr
|
|
|
607
607
|
if (request[field] < 0n) throw exampleError("INVALID_ENVELOPE", `${field} must be unsigned`);
|
|
608
608
|
}
|
|
609
609
|
}
|
|
610
|
-
request.nonce = envelope.nonce
|
|
611
|
-
? exactNumber(await client.request({ method: "eth_getTransactionCount", params: [creator, "pending"] }), "nonce")
|
|
612
|
-
: exactNumber(envelope.nonce, "nonce");
|
|
610
|
+
if (envelope.nonce !== undefined) request.nonce = exactNumber(envelope.nonce, "nonce");
|
|
613
611
|
if (request.type !== undefined) {
|
|
614
612
|
request.type = transactionTypes[request.type];
|
|
615
613
|
if (!request.type) throw exampleError("INVALID_ENVELOPE", "Only legacy/EIP-2930/EIP-1559 envelopes are supported");
|
|
@@ -622,6 +620,16 @@ export function createLocalLaunchWallet({ privateKey, creator, chainId, orchestr
|
|
|
622
620
|
request.maxFeePerGas ??= fees.maxFeePerGas;
|
|
623
621
|
request.maxPriorityFeePerGas ??= fees.maxPriorityFeePerGas;
|
|
624
622
|
}
|
|
623
|
+
const [nonceValue, balanceValue] = await Promise.all([
|
|
624
|
+
client.request({ method: "eth_getTransactionCount", params: [creator, "pending"] }),
|
|
625
|
+
client.request({ method: "eth_getBalance", params: [creator, "pending"] }),
|
|
626
|
+
]);
|
|
627
|
+
const pendingNonce = exactNumber(nonceValue, "pending nonce");
|
|
628
|
+
if (request.nonce !== undefined && request.nonce !== pendingNonce) throw exampleError("PENDING_NONCE_CHANGED", "Pending account nonce differs from the exact reviewed envelope");
|
|
629
|
+
request.nonce = pendingNonce;
|
|
630
|
+
const maximumFee = request.gas * (request.gasPrice ?? request.maxFeePerGas);
|
|
631
|
+
if (BigInt(balanceValue) < request.value + maximumFee) throw exampleError("INSUFFICIENT_PENDING_BALANCE", "Pending balance cannot fund the exact transaction value and maximum gas envelope");
|
|
632
|
+
await assertChain();
|
|
625
633
|
const rawTransaction = await account.signTransaction(request);
|
|
626
634
|
redactor.addSecret(rawTransaction);
|
|
627
635
|
const wire = { ...envelope, chainId: toHex(id), nonce: toHex(request.nonce) };
|
|
@@ -494,7 +494,7 @@ async function executePlan({ client, fixture, planned, sdk, artifacts, signal, l
|
|
|
494
494
|
const next = await sdk.buildNextTransaction({ client, planned, receipts: references, limits, submissionClient: client, reviewedTransaction, signal });
|
|
495
495
|
if (!next) return { references, activation };
|
|
496
496
|
assert.ok(reviewedTransaction, "No transaction outside the saved review may be submitted");
|
|
497
|
-
for (const field of ["id", "kind", "chainId", "from", "to", "data", "value", "gas", "gasPrice"]) assert.equal(next[field], reviewedTransaction[field], `Fresh ${field} must match the saved review`);
|
|
497
|
+
for (const field of ["id", "kind", "chainId", "from", "to", "data", "value", "nonce", "gas", "gasPrice"]) assert.equal(next[field], reviewedTransaction[field], `Fresh ${field} must match the saved review`);
|
|
498
498
|
if (retainedReferences.length) {
|
|
499
499
|
const progress = await sdk.readLaunchProgress({ client, planned, receipts: references, confirmations: planned.confirmations, signal });
|
|
500
500
|
assertRetainedExampleReceipts({ progress, references, artifacts });
|
|
@@ -46,7 +46,7 @@ async function send(transaction) {
|
|
|
46
46
|
const gasPrice = transaction.gasPrice ?? BigInt(await client.request({ method: "eth_gasPrice" }));
|
|
47
47
|
const head = await client.request({ method: "eth_getBlockByNumber", params: ["latest", false] });
|
|
48
48
|
const ceiling = [BigInt(head.gasLimit), BigInt(policy.chainTransactionGasLimit), BigInt(policy.rpcTransactionGasLimit), BigInt(policy.accountTransactionGasLimit)].reduce((a, b) => a < b ? a : b);
|
|
49
|
-
const wire = { from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gasPrice: toHex(gasPrice) };
|
|
49
|
+
const wire = { from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gasPrice: toHex(gasPrice), ...(transaction.nonce === undefined ? {} : { nonce: toHex(transaction.nonce) }) };
|
|
50
50
|
const estimated = transaction.gas ?? ((BigInt(await client.request({ method: "eth_estimateGas", params: [{ ...wire, gas: toHex(ceiling) }] })) * BigInt(10000 + Number(policy.headroomBps)) + 9999n) / 10000n);
|
|
51
51
|
assert.ok(estimated <= ceiling, "Unsigned helper must still fit explicit local execution admission");
|
|
52
52
|
const transactionHash = await client.request({ method: "eth_sendTransaction", params: [{ ...wire, gas: toHex(estimated) }] });
|
package/package.json
CHANGED
package/src/lifecycle/planner.ts
CHANGED
|
@@ -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, type LaunchPlanV1 } from "./schema.js";
|
|
6
|
-
import { readLaunchProgress, readLifecycleProfiles, readLifecycleTokenAtBlock, readPoolBoundHookDeployment } from "./progress.js";
|
|
6
|
+
import { readLaunchProgress, readLifecycleHeadProgress, readLifecycleProfiles, readLifecycleTokenAtBlock, readPoolBoundHookDeployment } from "./progress.js";
|
|
7
7
|
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion, minePoolBoundHookSalt, type AbyssLifecycleMarketConfig, type V4LifecycleMarketConfig, type V4PoolBoundLifecycleMarketConfig } from "./markets.js";
|
|
8
|
-
import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleChainId, readLifecycleContract, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
|
|
8
|
+
import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleChainId, readLifecycleContract, readLifecycleGasPrice, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
|
|
9
9
|
import { prepareLaunchSimulation, simulateLaunchTransactions, 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";
|
|
@@ -144,7 +144,7 @@ function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", prog
|
|
|
144
144
|
(transaction.kind !== "begin" && transaction.kind !== "atomic" || progress.canonical.phase === LifecyclePhase.None) &&
|
|
145
145
|
(transaction.kind !== "prepare" || (transaction.marketStart ?? 0) >= prepared));
|
|
146
146
|
const transactions = progress.canonical.phase === LifecyclePhase.None ? [...approvals, ...commands] : commands;
|
|
147
|
-
return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1]!.id] }));
|
|
147
|
+
return transactions.map((transaction, index) => ({ ...transaction, nonce: progress.pendingAccountNonce + BigInt(index), calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1]!.id] }));
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
function attachEstimates(transactions: readonly LifecycleTransaction[], simulation: LifecycleSimulation): LifecycleTransaction[] {
|
|
@@ -154,7 +154,7 @@ function attachEstimates(transactions: readonly LifecycleTransaction[], simulati
|
|
|
154
154
|
admission: estimate === undefined ? undefined : { admitted: simulation.admitted, confidence: simulation.confidence, executionProof: simulation.executionProof, protocolFit: simulation.protocolFit, transportPreflight: simulation.transportPreflight, blockNumber: simulation.blockNumber, blockHash: simulation.blockHash, account: simulation.account, chainId: simulation.chainId, reason: simulation.reason, limits: simulation.limits } };
|
|
155
155
|
});
|
|
156
156
|
}
|
|
157
|
-
type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value"> & NextTransactionEnvelope;
|
|
157
|
+
type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value" | "nonce"> & NextTransactionEnvelope;
|
|
158
158
|
const reviewedTransactionKinds = ["approve-reset", "approve", "atomic", "begin", "prepare", "activate", "cancel"] as const;
|
|
159
159
|
|
|
160
160
|
function readReviewedNextTransaction(transaction: LifecycleTransaction | undefined): ReviewedNextTransaction | undefined {
|
|
@@ -167,7 +167,8 @@ function readReviewedNextTransaction(transaction: LifecycleTransaction | undefin
|
|
|
167
167
|
typeof transaction.data !== "string" || !/^0x(?:[\da-fA-F]{2})*$/.test(transaction.data) ||
|
|
168
168
|
typeof transaction.value !== "bigint" || transaction.value < 0n || transaction.value >= 1n << 256n ||
|
|
169
169
|
typeof transaction.gas !== "bigint" || transaction.gas <= 0n || transaction.gas >= 1n << 64n ||
|
|
170
|
-
typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n
|
|
170
|
+
typeof transaction.gasPrice !== "bigint" || transaction.gasPrice < 0n || transaction.gasPrice >= 1n << 256n ||
|
|
171
|
+
transaction.nonce !== undefined && (typeof transaction.nonce !== "bigint" || transaction.nonce < 0n || transaction.nonce >= 1n << 64n)) {
|
|
171
172
|
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");
|
|
172
173
|
}
|
|
173
174
|
return transaction as ReviewedNextTransaction;
|
|
@@ -176,7 +177,8 @@ function readReviewedNextTransaction(transaction: LifecycleTransaction | undefin
|
|
|
176
177
|
function matchesReviewedNextTransaction(next: LifecycleTransaction | ReviewedNextTransaction | undefined, reviewed: ReviewedNextTransaction): boolean {
|
|
177
178
|
return next !== undefined && next.id === reviewed.id && next.kind === reviewed.kind && next.chainId === reviewed.chainId &&
|
|
178
179
|
next.from.toLowerCase() === reviewed.from.toLowerCase() && next.to.toLowerCase() === reviewed.to.toLowerCase() &&
|
|
179
|
-
next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value
|
|
180
|
+
next.data.toLowerCase() === reviewed.data.toLowerCase() && next.value === reviewed.value &&
|
|
181
|
+
(reviewed.nonce === undefined || next.nonce === reviewed.nonce);
|
|
180
182
|
}
|
|
181
183
|
|
|
182
184
|
function assertReviewedTransactionUnchanged(options: BuildNextTransactionOptions, reviewed: ReviewedNextTransaction | undefined, next: LifecycleTransaction): void {
|
|
@@ -273,16 +275,24 @@ function assertFinalPlan(planned: PlannedLaunch, signal?: AbortSignal): void {
|
|
|
273
275
|
export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifecycleLaunchOptions): Promise<PlannedLaunch> {
|
|
274
276
|
const { commitment, ufgConfigHash } = validatePlanningIntent(options);
|
|
275
277
|
if (options.buySlippageBps !== undefined) validateBuySlippageBps(options.buySlippageBps);
|
|
278
|
+
const fixed = options.fixedFinalEnvelope;
|
|
279
|
+
if (fixed !== undefined && (fixed === null || typeof fixed !== "object" || options.mode !== "atomic" || typeof fixed.gas !== "bigint" || fixed.gas <= 0n || fixed.gas >= 1n << 64n ||
|
|
280
|
+
fixed.gasPrice !== undefined && (typeof fixed.gasPrice !== "bigint" || fixed.gasPrice < 0n || fixed.gasPrice >= 1n << 256n))) {
|
|
281
|
+
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires atomic mode, exact positive uint64 gas and optional uint256 price");
|
|
282
|
+
}
|
|
283
|
+
const heldEnvelope = fixed === undefined ? undefined : Object.freeze({ ...fixed });
|
|
276
284
|
const needsMining = options.plan.markets.some((market) => isPoolBoundV4ConfigVersion(market.configVersion));
|
|
277
285
|
const needsProtection = options.buySlippageBps !== undefined && options.plan.buys.length !== 0;
|
|
278
|
-
if (!needsMining && !needsProtection) return planLaunch(options);
|
|
286
|
+
if (!needsMining && !needsProtection && fixed === undefined) return planLaunch(options);
|
|
279
287
|
const draft = parseLaunchPlan(serializeLaunchPlan(options.plan));
|
|
280
288
|
const assertDraft = () => {
|
|
281
289
|
assertPlanningActive(options.signal);
|
|
282
290
|
if (hashLaunchPlan(options.plan).toLowerCase() !== commitment.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous preparation; previous construction is invalid");
|
|
283
291
|
if ((options.ufg === undefined ? undefined : hashUfgLaunchConfig(options.ufg.config)) !== ufgConfigHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG config changed during asynchronous preparation");
|
|
292
|
+
if (fixed === undefined ? options.fixedFinalEnvelope !== undefined : options.fixedFinalEnvelope !== fixed ||
|
|
293
|
+
fixed.gas !== heldEnvelope!.gas || fixed.gasPrice !== heldEnvelope!.gasPrice) throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope changed during preparation");
|
|
284
294
|
};
|
|
285
|
-
|
|
295
|
+
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
286
296
|
const [block, chainId] = await Promise.all([readLifecycleBlock(client), readLifecycleChainId(client)]);
|
|
287
297
|
if (chainId !== draft.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the economic plan being finalized");
|
|
288
298
|
const metadata = needsMining ? await Promise.all(draft.markets.map((market, marketIndex) => isPoolBoundV4ConfigVersion(market.configVersion)
|
|
@@ -307,13 +317,19 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
|
|
|
307
317
|
// start at a fresh post-mining snapshot and share it through final protection.
|
|
308
318
|
const executionBlock = needsMining ? await readLifecycleBlock(client) : block;
|
|
309
319
|
assertDraft();
|
|
320
|
+
const price = heldEnvelope === undefined ? undefined : heldEnvelope.gasPrice ?? rpcQuantity(await readLifecycleGasPrice(client), "gas price");
|
|
321
|
+
const baseFeeEnvelope = (executionBlock.baseFeePerGas ?? 0n) * 2n;
|
|
322
|
+
const finalEnvelope = heldEnvelope === undefined ? undefined : Object.freeze({
|
|
323
|
+
gas: heldEnvelope.gas,
|
|
324
|
+
gasPrice: heldEnvelope.gasPrice ?? (price! > baseFeeEnvelope ? price! : baseFeeEnvelope),
|
|
325
|
+
});
|
|
310
326
|
let plan: LaunchPlanV1 = { ...draft, markets };
|
|
311
327
|
let ownedProgress: CanonicalLaunchProgress | undefined;
|
|
312
328
|
if (needsProtection) {
|
|
313
329
|
// A diagnostic minimum is never returned as an admitted or executable plan.
|
|
314
330
|
// Price guards, funding minima, recipients and all ordered budgets stay exact.
|
|
315
331
|
const diagnosticPlan = { ...plan, buys: plan.buys.map((buy) => ({ ...buy, minTokenOut: 1n })) };
|
|
316
|
-
const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, executionBlock, undefined, true));
|
|
332
|
+
const diagnostic = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan: diagnosticPlan }, client, executionBlock, undefined, true, undefined, finalEnvelope));
|
|
317
333
|
assertDraft();
|
|
318
334
|
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");
|
|
319
335
|
plan = protectOrderedBuyMinimums(diagnostic, options.buySlippageBps!);
|
|
@@ -321,7 +337,7 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
|
|
|
321
337
|
// state. No onchain commitment existed at this exact snapshot to replace.
|
|
322
338
|
ownedProgress = { ...diagnostic.progress, planHash: hashLaunchPlan(plan) };
|
|
323
339
|
}
|
|
324
|
-
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress));
|
|
340
|
+
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads({ ...options, plan }, client, executionBlock, ownedProgress, false, undefined, finalEnvelope));
|
|
325
341
|
assertDraft();
|
|
326
342
|
assertFinalPlan(planned, options.signal);
|
|
327
343
|
if (planned.progress.canonical.phase !== LifecyclePhase.Active && planned.progress.canonical.phase !== LifecyclePhase.Cancelled) {
|
|
@@ -333,18 +349,23 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
|
|
|
333
349
|
if (needsProtection && !planned.simulation.admitted) throw new LifecyclePlanningError("PROTECTED_PLAN_NOT_ADMITTED", planned.simulation.reason ?? "The final protected launch did not establish exact execution proof", planned.simulation);
|
|
334
350
|
return planned;
|
|
335
351
|
});
|
|
352
|
+
assertDraft();
|
|
353
|
+
assertFinalPlan(result, options.signal);
|
|
354
|
+
return result;
|
|
336
355
|
}
|
|
337
356
|
|
|
338
357
|
/** Independently fresh current-state admission of an already finalized commitment. No broadcasts. */
|
|
339
358
|
export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLaunch> {
|
|
340
|
-
|
|
359
|
+
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
341
360
|
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
|
|
342
361
|
assertFinalPlan(planned, options.signal);
|
|
343
362
|
return planned;
|
|
344
363
|
});
|
|
364
|
+
assertFinalPlan(result, options.signal);
|
|
365
|
+
return result;
|
|
345
366
|
}
|
|
346
367
|
|
|
347
|
-
async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient, pinnedBlock?: LifecycleBlock, ownedProgress?: CanonicalLaunchProgress, diagnostic = false, reviewed?: ReviewedNextTransaction): Promise<PlannedLaunch> {
|
|
368
|
+
async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient, pinnedBlock?: LifecycleBlock, ownedProgress?: CanonicalLaunchProgress, diagnostic = false, reviewed?: ReviewedNextTransaction, fixedFinalEnvelope?: NextTransactionEnvelope): Promise<PlannedLaunch> {
|
|
348
369
|
const { plan, account, mode, ufg } = options;
|
|
349
370
|
const { commitment: planHash, marketConfigs, ufgConfigHash } = validatePlanningIntent(options);
|
|
350
371
|
const launchId = hashLaunchIdentity(plan);
|
|
@@ -366,7 +387,10 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
|
|
|
366
387
|
preset ?? readTokenFactoryBinding(client, plan.orchestrator, block),
|
|
367
388
|
resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
|
|
368
389
|
lifecycleStage(client, "plan.progress", async () => {
|
|
369
|
-
const predictedToken = await
|
|
390
|
+
const [predictedToken] = await Promise.all([
|
|
391
|
+
readLifecycleTokenAtBlock(client, plan, block),
|
|
392
|
+
ownedProgress === undefined ? readLifecycleHeadProgress(client, plan, account, block) : undefined,
|
|
393
|
+
]);
|
|
370
394
|
const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1,
|
|
371
395
|
...(ufg === undefined ? {} : { ufg: { config: ufg.config, configHash: ufgConfigHash! } }) };
|
|
372
396
|
if (ufg !== undefined) validateUfgLaunchPlan(plan, ufg.config, predictedToken);
|
|
@@ -419,6 +443,9 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
|
|
|
419
443
|
batches = [...(prepared > 0 ? [prepared] : []), count, ...(start + count < plan.markets.length ? [plan.markets.length - start - count] : [])];
|
|
420
444
|
}
|
|
421
445
|
let transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg);
|
|
446
|
+
if (fixedFinalEnvelope !== undefined && (transactions.length !== 1 || transactions[0]!.kind !== "atomic")) {
|
|
447
|
+
throw new LifecyclePlanningError("INVALID_GAS_ENVELOPE", "Fixed final envelope requires one immediate atomic launch with no preceding approvals");
|
|
448
|
+
}
|
|
422
449
|
if (reviewed !== undefined) {
|
|
423
450
|
if (!matchesReviewedNextTransaction(transactions[0], reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
|
|
424
451
|
transactions = transactions.slice(0, 1);
|
|
@@ -427,9 +454,10 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
|
|
|
427
454
|
await assertLifecycleBlock(client, block, plan.chainId);
|
|
428
455
|
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) };
|
|
429
456
|
}
|
|
430
|
-
const
|
|
457
|
+
const envelope = diagnostic ? undefined : reviewed ?? fixedFinalEnvelope;
|
|
458
|
+
const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block, true, envelope, fixedFinalEnvelope?.gasPrice);
|
|
431
459
|
try {
|
|
432
|
-
const execute = diagnostic ? preparedSimulation.measure : (candidate: readonly LifecycleTransaction[]) => preparedSimulation(candidate,
|
|
460
|
+
const execute = diagnostic ? preparedSimulation.measure : (candidate: readonly LifecycleTransaction[]) => preparedSimulation(candidate, envelope);
|
|
433
461
|
let simulation = await execute(transactions);
|
|
434
462
|
assertFinalPlan(planned, options.signal);
|
|
435
463
|
if (mode === "staged") {
|
|
@@ -475,6 +503,7 @@ async function preflightNextTransaction(options: BuildNextTransactionOptions, ne
|
|
|
475
503
|
if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId) throw new Error("Active submission RPC chain differs from the reviewed transaction");
|
|
476
504
|
const estimated = rpcQuantity(await lifecycleRpc(submissionClient, "eth_estimateGas", [{
|
|
477
505
|
from: next.from, to: next.to, data: next.data, value: toHex(next.value), gas: toHex(next.gas), gasPrice: toHex(next.gasPrice),
|
|
506
|
+
...(next.nonce === undefined ? {} : { nonce: toHex(next.nonce) }),
|
|
478
507
|
}, "latest"]), "submission gas estimate");
|
|
479
508
|
if (estimated <= 0n || estimated > next.gas) throw new Error("Submission RPC gas estimate exceeds the exact reviewed envelope or is zero");
|
|
480
509
|
if (rpcQuantity(await lifecycleRpc(submissionClient, "eth_chainId"), "submission chain ID") !== simulation.chainId) throw new Error("Active submission RPC chain changed during preflight");
|
|
@@ -484,12 +513,6 @@ async function preflightNextTransaction(options: BuildNextTransactionOptions, ne
|
|
|
484
513
|
const reason = `Exact next-transaction submission preflight failed: ${lifecycleFailureReason(failure)}`;
|
|
485
514
|
throw new LifecyclePlanningError("SUBMISSION_PREFLIGHT_FAILED", reason, { ...simulation, transportPreflight: "failed", reason });
|
|
486
515
|
}
|
|
487
|
-
const [canonical, chainId] = await Promise.all([
|
|
488
|
-
readLifecycleBlock(options.client, toHex(simulation.blockNumber)),
|
|
489
|
-
lifecycleRpc(options.client, "eth_chainId"),
|
|
490
|
-
]);
|
|
491
|
-
if (canonical.hash.toLowerCase() !== simulation.blockHash.toLowerCase()) throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch proof changed during submission preflight; rebuild from canonical progress");
|
|
492
|
-
if (rpcQuantity(chainId, "chain ID") !== simulation.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during submission preflight");
|
|
493
516
|
if (next.admission === undefined) throw new LifecyclePlanningError("MISSING_GAS_PROOF", "Next transaction omitted its exact simulation admission", simulation);
|
|
494
517
|
assertPlanningActive(options.signal);
|
|
495
518
|
assertReviewedTransactionUnchanged(options, reviewed, next);
|
|
@@ -503,9 +526,17 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
|
|
|
503
526
|
// Snapshot only the reviewed wire fields; caller proofs and commands are never
|
|
504
527
|
// reused, and final checks compare the mutable input with this one local copy.
|
|
505
528
|
const reviewed = supplied === undefined ? undefined : Object.freeze({ id: supplied.id, kind: supplied.kind, chainId: supplied.chainId,
|
|
506
|
-
from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice });
|
|
507
|
-
|
|
529
|
+
from: supplied.from, to: supplied.to, data: supplied.data, value: supplied.value, gas: supplied.gas, gasPrice: supplied.gasPrice, nonce: supplied.nonce });
|
|
530
|
+
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
508
531
|
const block = await readLifecycleBlock(client);
|
|
532
|
+
const limitSource = options.limits === undefined ? options.planned.limits : options.limits;
|
|
533
|
+
if (getKnownLifecycleDeployment({ chainId: options.planned.chainId, orchestrator: options.planned.plan.orchestrator }) !== undefined && typeof limitSource !== "function") {
|
|
534
|
+
await Promise.all([
|
|
535
|
+
readLifecycleTokenAtBlock(client, options.planned.plan, block),
|
|
536
|
+
readLifecycleHeadProgress(client, options.planned.plan, options.planned.account, block),
|
|
537
|
+
resolveLifecycleLimits({ client, account: options.planned.account, orchestrator: options.planned.plan.orchestrator, block, chainId: options.planned.chainId }, limitSource),
|
|
538
|
+
]);
|
|
539
|
+
}
|
|
509
540
|
const progress = await readLaunchProgress({ ...options, client }, block);
|
|
510
541
|
if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) {
|
|
511
542
|
// Only the confirmed canonical terminal phase is terminal; a head-only
|
|
@@ -523,6 +554,7 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
|
|
|
523
554
|
assertFinalPlan(planned, options.signal);
|
|
524
555
|
const transaction: LifecycleTransaction = planned.ufg !== undefined ? buildUfgCancelTransaction({ plan: planned.plan, preparedMarkets: progress.canonical.preparedMarkets })
|
|
525
556
|
: { 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 }] };
|
|
557
|
+
transaction.nonce = progress.pendingAccountNonce;
|
|
526
558
|
if (reviewed !== undefined && !matchesReviewedNextTransaction(transaction, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical cancellation");
|
|
527
559
|
const simulation = await simulateLaunchTransactions({ client, planned, limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork, signal: options.signal }, [transaction], block, reviewed);
|
|
528
560
|
if (!simulation.admitted) throw new LifecyclePlanningError("CANCEL_NOT_ADMITTED", simulation.reason ?? "Cancellation could not be proved against current canonical state", simulation);
|
|
@@ -539,6 +571,9 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
|
|
|
539
571
|
if (next === undefined) return undefined;
|
|
540
572
|
return preflightNextTransaction(options, next, refreshed.simulation, reviewed);
|
|
541
573
|
});
|
|
574
|
+
assertFinalPlan(options.planned, options.signal);
|
|
575
|
+
if (result !== undefined) assertReviewedTransactionUnchanged(options, reviewed, result);
|
|
576
|
+
return result;
|
|
542
577
|
}
|
|
543
578
|
|
|
544
579
|
/** Re-simulates only canonical unfinished work with the same submitted receipt
|
|
@@ -2,7 +2,7 @@ import { concatHex, decodeFunctionData, encodeAbiParameters, encodeFunctionData,
|
|
|
2
2
|
import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleDirectoryAbi, lifecycleErc20Abi, lifecycleOracleFactoryAbi, lifecycleRegistryAbi, poolFeeCollectorFactoryV1Abi, poolFeeCollectorFactoryConfigV6Abi, lifecycleV4HookAbi, lifecycleV4LockerAbi, poolHookDeployerV1Abi, poolHookDeployerConfigV6Abi, sharedHookDeployerV1Abi, fixedFeePoolHookV1Abi, fixedFeePoolHookConfigV6Abi, poolMarketAdapterV1Abi, sharedMarketAdapterV1Abi } from "./abi.js";
|
|
3
3
|
import { buildPoolBoundHookParameters, decodePoolBoundV4LifecycleMarketConfig, encodePoolBoundHookParameters, hasLifecycleV4HookPermissions, isPoolBoundV4ConfigVersion, poolBoundHookInitCodeHash, predictPoolBoundHookAddress, validateV4LifecycleMarket, V4_LIFECYCLE_CONFIG_SCHEMA, V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA_V5, V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA_V6, type PoolBoundHookParameters, type PoolBoundLifecycleConfigVersion } from "./markets.js";
|
|
4
4
|
import { hashLaunchIdentity, hashLaunchPlan, hashLaunchBounds, hashLaunchDependencies, hashLifecycleProfile, lifecycleTokenComponents, launchProgressV1Components, LifecycleMode, LifecyclePhase, LifecycleVenue, LIFECYCLE_REQUIRED_CAPABILITIES, LIFECYCLE_ERC404_CAPABILITY, LIFECYCLE_MULTI_POSITION_CAPABILITY, type AdapterRegistrationV1, type LaunchPlanV1, type LaunchProgressV1, type MarketIdentityV1, type MarketLiveStateV1, type PreparedMarketV1, type ProfileRegistrationV1, type ProfileTopologyV1, type LaunchEnvelopeV2, type LifecycleDeveloperTerms } from "./schema.js";
|
|
5
|
-
import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleChainId, readLifecycleContract, readLifecycleStaticContract, readLifecycleProfileTopology, rpcHex, rpcObject, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
|
|
5
|
+
import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleChainId, readLifecycleContract, readLifecycleImmutable, readLifecycleStaticContract, readLifecycleProfileTopology, rpcHex, rpcObject, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
|
|
6
6
|
import { LifecyclePlanningError, type CanonicalLaunchProgress, type LifecycleBlock, type LifecycleMarketProgress, type LifecycleProfile, type LifecycleReceiptReference, type LifecycleReceiptStatus, type LifecycleRpcClient, type PoolBoundHookDeployment, type ReadLaunchProgressOptions } from "./types.js";
|
|
7
7
|
import { deriveKnownPoolBoundHookDeployment } from "./calibration.js";
|
|
8
8
|
import { getKnownLifecycleDeployment, getKnownLifecycleProfile } from "./presets.js";
|
|
@@ -21,9 +21,18 @@ const lifecycleTokenPredictionAbi = [{
|
|
|
21
21
|
* avoiding full-plan prediction reads after salts or buy minima change. */
|
|
22
22
|
export async function readLifecycleTokenAtBlock(client: LifecycleRpcClient, plan: LaunchPlanV1, block: LifecycleBlock): Promise<Address> {
|
|
23
23
|
const deployment = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator });
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
if (deployment === undefined) return readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block);
|
|
25
|
+
const launchId = hashLaunchIdentity(plan);
|
|
26
|
+
const data = encodeFunctionData({ abi: lifecycleTokenPredictionAbi, functionName: "predictToken", args: [launchId, plan.token] });
|
|
27
|
+
const key = `${plan.chainId}:${plan.orchestrator.toLowerCase()}:${deployment.tokenFactory.toLowerCase()}:${deployment.tokenFactoryCodeHash}:${data}`;
|
|
28
|
+
return readLifecycleImmutable(client, key, () =>
|
|
29
|
+
readLifecycleContract<Address>(client, deployment.tokenFactory, lifecycleTokenPredictionAbi, "predictToken", [launchId, plan.token], block, undefined, false, true));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Internal pinned head observation. Known cores do not inspect msg.sender. */
|
|
33
|
+
export function readLifecycleHeadProgress(client: LifecycleRpcClient, plan: LaunchPlanV1, account: Address, block: LifecycleBlock): Promise<LaunchProgressV1> {
|
|
34
|
+
const known = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator }) !== undefined;
|
|
35
|
+
return readLifecycleContract<LaunchProgressV1>(client, plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [hashLaunchIdentity(plan)], block, account, false, known);
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
export async function predictLifecycleToken(options: { client: LifecycleRpcClient; plan: LaunchPlanV1 }): Promise<Address> {
|
|
@@ -224,7 +233,7 @@ export async function validateV4LifecycleOracle(options: {
|
|
|
224
233
|
|
|
225
234
|
export async function readLifecycleProfiles(options: { client: LifecycleRpcClient; orchestrator: Address; profileIds?: readonly Hex[]; offset?: bigint; limit?: bigint }, pinnedBlock?: LifecycleBlock): Promise<LifecycleProfile[]> {
|
|
226
235
|
return withLifecycleReadClient(options.client, (client) => lifecycleStage(client, "profile.certification", async () => {
|
|
227
|
-
const chainIdPromise =
|
|
236
|
+
const chainIdPromise = readLifecycleChainId(client);
|
|
228
237
|
// Block-bound discovery does not depend on chain identity. Join both branches
|
|
229
238
|
// immediately so a failed chain read cannot become detached or a profile refusal.
|
|
230
239
|
const [chainId, { block, profiles }] = await Promise.all([
|
|
@@ -303,7 +312,7 @@ async function readPoolBoundHookDeploymentDetails(options: { client: LifecycleRp
|
|
|
303
312
|
const market = plan.markets[marketIndex];
|
|
304
313
|
if (market === undefined || !isPoolBoundV4ConfigVersion(market.configVersion)) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Market index must select explicit pool-bound config version 5 or 6");
|
|
305
314
|
let chainId: bigint;
|
|
306
|
-
try { chainId =
|
|
315
|
+
try { chainId = await readLifecycleChainId(client); }
|
|
307
316
|
finally { assertDerivedReadClientOpen(client); }
|
|
308
317
|
if (hashLaunchPlan(plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while observing the hook deployment chain");
|
|
309
318
|
if (chainId !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
|
|
@@ -419,19 +428,21 @@ async function readPoolBoundHookDeploymentDetailsFresh(options: { client: Lifecy
|
|
|
419
428
|
export async function readPoolBoundHookDeployment(options: { client: LifecycleRpcClient; plan: LaunchPlanV1; marketIndex: number }, pinnedBlock?: LifecycleBlock): Promise<PoolBoundHookDeployment> {
|
|
420
429
|
const commitment = hashLaunchPlan(options.plan);
|
|
421
430
|
const block = pinnedBlock ?? await readLifecycleBlock(options.client);
|
|
422
|
-
|
|
431
|
+
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
423
432
|
const { deployment } = await readPoolBoundHookDeploymentDetails({ ...options, client }, block, commitment);
|
|
424
433
|
if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while reading hook deployment metadata");
|
|
425
434
|
if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, options.plan.chainId);
|
|
426
435
|
return { ...deployment };
|
|
427
436
|
});
|
|
437
|
+
if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during the final hook deployment guard");
|
|
438
|
+
return result;
|
|
428
439
|
}
|
|
429
440
|
|
|
430
441
|
/** Optional permissionless predeployment calldata; no token deployment, pool registration or wallet broadcast. */
|
|
431
442
|
export async function buildPoolBoundHookDeploymentTransaction(options: { client: LifecycleRpcClient; plan: LaunchPlanV1; marketIndex: number }): Promise<{ to: Address; data: Hex; value: bigint; deployment: PoolBoundHookDeployment }> {
|
|
432
443
|
const commitment = hashLaunchPlan(options.plan);
|
|
433
444
|
const block = await readLifecycleBlock(options.client);
|
|
434
|
-
|
|
445
|
+
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
435
446
|
const { deployment, parameters, configVersion } = await readPoolBoundHookDeploymentDetails({ ...options, client }, block, commitment);
|
|
436
447
|
if (!hasLifecycleV4HookPermissions(deployment.predictedHook)) throw new LifecyclePlanningError("INVALID_HOOK_BITS", "Finalize a salt with the exact lifecycle hook permissions before predeployment");
|
|
437
448
|
if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while constructing hook deployment calldata");
|
|
@@ -439,6 +450,8 @@ export async function buildPoolBoundHookDeploymentTransaction(options: { client:
|
|
|
439
450
|
const abi = configVersion === 5 ? poolHookDeployerV1Abi : poolHookDeployerConfigV6Abi;
|
|
440
451
|
return { to: deployment.deployer, data: encodeFunctionData({ abi, functionName: "deploy", args: [parameters, deployment.salt] }), value: 0n, deployment: { ...deployment } };
|
|
441
452
|
});
|
|
453
|
+
if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during the final hook transaction guard");
|
|
454
|
+
return result;
|
|
442
455
|
}
|
|
443
456
|
|
|
444
457
|
export function validateLifecycleMarketIdentity(identity: MarketIdentityV1, plan: LaunchPlanV1, token: Address, marketIndex: number): void {
|
|
@@ -455,7 +468,7 @@ export function validateLifecycleMarketIdentity(identity: MarketIdentityV1, plan
|
|
|
455
468
|
} else if (identity.venue !== LifecycleVenue.Abyss || identity.pool === zeroAddress || identity.factory === zeroAddress) throw new LifecyclePlanningError("MARKET_IDENTITY", "Abyss canonical factory/pool identity is missing");
|
|
456
469
|
}
|
|
457
470
|
|
|
458
|
-
async function transactionMatchesLaunch(transaction: Record<string, unknown>, options: ReadLaunchProgressOptions,
|
|
471
|
+
async function transactionMatchesLaunch(transaction: Record<string, unknown>, options: ReadLaunchProgressOptions, readBlock: () => Promise<LifecycleBlock>): Promise<boolean> {
|
|
459
472
|
const { planned } = options;
|
|
460
473
|
const from = rpcHex(transaction.from, "receipt transaction account");
|
|
461
474
|
if (from.toLowerCase() !== planned.account.toLowerCase()) throw new LifecyclePlanningError("RECEIPT_ACCOUNT", "Receipt transaction belongs to a different account");
|
|
@@ -484,53 +497,103 @@ async function transactionMatchesLaunch(transaction: Record<string, unknown>, op
|
|
|
484
497
|
if (decoded.functionName !== "approve" || rpcQuantity(transaction.value ?? "0x0", "approval native value") !== 0n) return false;
|
|
485
498
|
spender = decoded.args[0];
|
|
486
499
|
} catch { return false; }
|
|
487
|
-
const
|
|
500
|
+
const known = getKnownLifecycleDeployment({ chainId: planned.plan.chainId, orchestrator: planned.plan.orchestrator });
|
|
501
|
+
const escrow = planned.ufg !== undefined ? UFG_LAUNCH_PROXY_ADDRESS : known?.fundingEscrow ??
|
|
502
|
+
await readLifecycleContract<Address>(options.client, planned.plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], await readBlock());
|
|
488
503
|
return spender.toLowerCase() === escrow.toLowerCase();
|
|
489
504
|
}
|
|
490
505
|
|
|
491
|
-
|
|
506
|
+
type ReceiptObservation = {
|
|
507
|
+
pins: readonly { number: bigint; hash: Hex }[];
|
|
508
|
+
finish(headers: ReadonlyMap<bigint, Hex | undefined>): LifecycleReceiptStatus;
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
async function readReceipt(reference: LifecycleReceiptReference, options: ReadLaunchProgressOptions, block: LifecycleBlock, readIdentityBlock: (number: bigint) => Promise<LifecycleBlock>): Promise<ReceiptObservation> {
|
|
492
512
|
const effectiveHash = reference.replacementHash ?? reference.transactionHash;
|
|
493
513
|
const base = { transactionHash: reference.transactionHash, effectiveHash, replaced: reference.replacementHash !== undefined && reference.replacementHash.toLowerCase() !== reference.transactionHash.toLowerCase() };
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
514
|
+
const pins: { number: bigint; hash: Hex }[] = [];
|
|
515
|
+
const observedNumber = reference.observedBlockNumber, observedHash = reference.observedBlockHash;
|
|
516
|
+
if (observedNumber !== undefined && observedHash !== undefined) pins.push({ number: observedNumber, hash: observedHash });
|
|
517
|
+
const finish = (result: LifecycleReceiptStatus, failure?: { error: unknown }): ReceiptObservation => ({
|
|
518
|
+
pins,
|
|
519
|
+
finish(headers) {
|
|
520
|
+
for (const pin of pins) {
|
|
521
|
+
if (failure !== undefined && (pin.number !== observedNumber || pin.hash !== observedHash)) continue;
|
|
522
|
+
if (headers.get(pin.number)?.toLowerCase() !== pin.hash.toLowerCase()) return { ...base, status: "reorged", blockNumber: pin.number, blockHash: pin.hash };
|
|
523
|
+
}
|
|
524
|
+
if (failure !== undefined) throw failure.error;
|
|
525
|
+
return result;
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
try {
|
|
529
|
+
const [receiptValue, transactionValue] = await Promise.all([
|
|
530
|
+
lifecycleRpc(options.client, "eth_getTransactionReceipt", [effectiveHash]),
|
|
531
|
+
lifecycleRpc(options.client, "eth_getTransactionByHash", [effectiveHash]),
|
|
532
|
+
]);
|
|
533
|
+
if (receiptValue === null) return finish({ ...base, status: "pending", blockNumber: observedNumber, blockHash: observedHash });
|
|
534
|
+
if (transactionValue === null) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Receipt exists without its canonical transaction");
|
|
535
|
+
const receipt = rpcObject(receiptValue, "receipt");
|
|
536
|
+
const blockNumber = rpcQuantity(receipt.blockNumber, "receipt block number");
|
|
537
|
+
const blockHash = rpcHex(receipt.blockHash, "receipt block hash");
|
|
538
|
+
pins.push({ number: blockNumber, hash: blockHash });
|
|
539
|
+
const matches = await transactionMatchesLaunch(rpcObject(transactionValue, "receipt transaction"), options,
|
|
540
|
+
() => readIdentityBlock(blockNumber));
|
|
541
|
+
const confirmations = block.number >= blockNumber ? block.number - blockNumber + 1n : 0n;
|
|
542
|
+
const required = reference.confirmations ?? 1;
|
|
543
|
+
if (!Number.isSafeInteger(required) || required < 1) throw new LifecyclePlanningError("INVALID_CONFIRMATIONS", "Receipt confirmations must be a positive integer");
|
|
544
|
+
const observation = finish({
|
|
545
|
+
...base, blockNumber, blockHash, confirmations,
|
|
546
|
+
status: confirmations < BigInt(required) ? "unconfirmed" : !matches ? "replacement-cancelled" :
|
|
547
|
+
rpcQuantity(receipt.status, "receipt status") !== 1n ? "reverted" : "confirmed",
|
|
548
|
+
});
|
|
549
|
+
return {
|
|
550
|
+
pins,
|
|
551
|
+
finish(headers) {
|
|
552
|
+
const result = observation.finish(headers);
|
|
553
|
+
if (result.status !== "reorged" && !matches && !base.replaced) throw new LifecyclePlanningError("RECEIPT_IDENTITY", "Receipt calldata does not belong to the exact committed launch");
|
|
554
|
+
return result;
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (error instanceof Error && error.name === "AbortError") throw error;
|
|
559
|
+
// A changed retained observation is a reorg even if the orphan's transaction
|
|
560
|
+
// or identity read is now unavailable. Otherwise preserve the original error.
|
|
561
|
+
return finish({ ...base, status: "pending" }, { error });
|
|
509
562
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function readReceipts(options: ReadLaunchProgressOptions, block: LifecycleBlock, confirmationDepth: number): Promise<LifecycleReceiptStatus[]> {
|
|
566
|
+
if (options.receipts === undefined || options.receipts.length === 0) return [];
|
|
567
|
+
const identityBlocks = new Map<bigint, Promise<LifecycleBlock>>();
|
|
568
|
+
const readIdentityBlock = (number: bigint) => {
|
|
569
|
+
let pending = identityBlocks.get(number);
|
|
570
|
+
if (pending === undefined) { pending = readLifecycleBlock(options.client, toHex(number)); identityBlocks.set(number, pending); }
|
|
571
|
+
return pending;
|
|
572
|
+
};
|
|
573
|
+
const observations = await Promise.all((options.receipts ?? []).map((reference) =>
|
|
574
|
+
readReceipt({ ...reference, confirmations: Math.max(reference.confirmations ?? 1, confirmationDepth) }, options, block, readIdentityBlock)));
|
|
575
|
+
const headers = new Map<bigint, Hex | undefined>();
|
|
576
|
+
for (const observation of observations) for (const pin of observation.pins) headers.set(pin.number, undefined);
|
|
577
|
+
// All asynchronous identity reads have settled. Equal observed/inclusion blocks
|
|
578
|
+
// and multiple receipts now share one fresh canonical header, never a stale cache.
|
|
579
|
+
await Promise.all([...headers.keys()].map(async (number) => {
|
|
580
|
+
const value = await lifecycleRpc(options.client, "eth_getBlockByNumber", [toHex(number), false]);
|
|
581
|
+
const hash = value === null ? undefined : rpcHex(rpcObject(value, "receipt canonical block").hash, "receipt canonical hash");
|
|
582
|
+
if (hash !== undefined && hash.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Receipt canonical block hash must have 32 bytes");
|
|
583
|
+
headers.set(number, hash);
|
|
584
|
+
}));
|
|
585
|
+
return observations.map((observation) => observation.finish(headers));
|
|
527
586
|
}
|
|
528
587
|
|
|
529
588
|
/** Reads confirmation-bound canonical state and receipt provenance; local step counters are never used. */
|
|
530
589
|
export async function readLaunchProgress(options: ReadLaunchProgressOptions, pinnedBlock?: LifecycleBlock): Promise<CanonicalLaunchProgress> {
|
|
531
590
|
if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
|
|
532
|
-
const
|
|
533
|
-
|
|
591
|
+
const planHash = options.planned.planHash, launchId = options.planned.launchId;
|
|
592
|
+
const result = await withLifecycleReadClient(options.client, async (client) =>
|
|
593
|
+
readLaunchProgressAtBlock(options, client, pinnedBlock ?? await readLifecycleBlock(client), pinnedBlock === undefined));
|
|
594
|
+
if (hashLaunchPlan(options.planned.plan) !== planHash || hashLaunchIdentity(options.planned.plan) !== launchId) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during the final canonical progress guard");
|
|
595
|
+
if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
|
|
596
|
+
return result;
|
|
534
597
|
}
|
|
535
598
|
|
|
536
599
|
async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, client: LifecycleRpcClient, block: LifecycleBlock, checkBlock: boolean): Promise<CanonicalLaunchProgress> {
|
|
@@ -555,7 +618,7 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
|
|
|
555
618
|
} else if (planned.account.toLowerCase() !== planned.plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
|
|
556
619
|
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");
|
|
557
620
|
const [head, confirmedProgress, token, confirmedNonce, headNonce, pendingNonce] = await Promise.all([
|
|
558
|
-
|
|
621
|
+
readLifecycleHeadProgress(client, planned.plan, planned.account, block),
|
|
559
622
|
confirmedNumber === block.number ? undefined : readLifecycleContract<LaunchProgressV1>(client, planned.plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [planned.launchId], confirmedBlock, planned.account),
|
|
560
623
|
readLifecycleTokenAtBlock(client, planned.plan, block),
|
|
561
624
|
lifecycleRpc(client, "eth_getTransactionCount", [planned.account, toHex(confirmedBlock.number)]),
|
|
@@ -582,7 +645,7 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
|
|
|
582
645
|
const pendingAccountNonce = rpcQuantity(pendingNonce, "pending account nonce");
|
|
583
646
|
const progressParameters = [{ type: "tuple", components: launchProgressV1Components }] as const;
|
|
584
647
|
const confirmationSafe = confirmedAccountNonce === headAccountNonce && pendingAccountNonce === headAccountNonce && keccak256(encodeAbiParameters(progressParameters, [canonical])) === keccak256(encodeAbiParameters(progressParameters, [head]));
|
|
585
|
-
const receipts = await
|
|
648
|
+
const receipts = await readReceipts({ ...options, client }, block, confirmationDepth);
|
|
586
649
|
assertActive();
|
|
587
650
|
let markets: LifecycleMarketProgress[] = [];
|
|
588
651
|
if (canonical.preparedMarkets > 0) {
|