@black-market/sdk 0.1.0 → 0.4.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 +92 -20
- package/dist/abyss.d.ts +4 -2748
- package/dist/abyss.d.ts.map +1 -1
- package/dist/abyss.js +3 -1029
- package/dist/abyss.js.map +1 -1
- package/dist/addresses.d.ts +3 -7
- package/dist/addresses.d.ts.map +1 -1
- package/dist/addresses.js +6 -21
- package/dist/addresses.js.map +1 -1
- package/dist/auction.d.ts +2 -2
- package/dist/auction.d.ts.map +1 -1
- package/dist/auction.js +2 -2
- package/dist/auction.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/launch-api.d.ts +151 -0
- package/dist/launch-api.d.ts.map +1 -0
- package/dist/launch-api.js +117 -0
- package/dist/launch-api.js.map +1 -0
- package/dist/launch.d.ts +23 -185
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js +35 -272
- package/dist/launch.js.map +1 -1
- package/dist/lifecycle/abi.d.ts +6391 -0
- package/dist/lifecycle/abi.d.ts.map +1 -0
- package/dist/lifecycle/abi.js +335 -0
- package/dist/lifecycle/abi.js.map +1 -0
- package/dist/lifecycle/authors.d.ts +133 -0
- package/dist/lifecycle/authors.d.ts.map +1 -0
- package/dist/lifecycle/authors.js +217 -0
- package/dist/lifecycle/authors.js.map +1 -0
- package/dist/lifecycle/fork.d.ts +15 -0
- package/dist/lifecycle/fork.d.ts.map +1 -0
- package/dist/lifecycle/fork.js +36 -0
- package/dist/lifecycle/fork.js.map +1 -0
- package/dist/lifecycle/index.d.ts +10 -0
- package/dist/lifecycle/index.d.ts.map +1 -0
- package/dist/lifecycle/index.js +10 -0
- package/dist/lifecycle/index.js.map +1 -0
- package/dist/lifecycle/markets.d.ts +349 -0
- package/dist/lifecycle/markets.d.ts.map +1 -0
- package/dist/lifecycle/markets.js +186 -0
- package/dist/lifecycle/markets.js.map +1 -0
- package/dist/lifecycle/planner.d.ts +20 -0
- package/dist/lifecycle/planner.d.ts.map +1 -0
- package/dist/lifecycle/planner.js +481 -0
- package/dist/lifecycle/planner.js.map +1 -0
- package/dist/lifecycle/progress.d.ts +43 -0
- package/dist/lifecycle/progress.d.ts.map +1 -0
- package/dist/lifecycle/progress.js +449 -0
- package/dist/lifecycle/progress.js.map +1 -0
- package/dist/lifecycle/rpc.d.ts +17 -0
- package/dist/lifecycle/rpc.d.ts.map +1 -0
- package/dist/lifecycle/rpc.js +100 -0
- package/dist/lifecycle/rpc.js.map +1 -0
- package/dist/lifecycle/schema.d.ts +1068 -0
- package/dist/lifecycle/schema.d.ts.map +1 -0
- package/dist/lifecycle/schema.js +335 -0
- package/dist/lifecycle/schema.js.map +1 -0
- package/dist/lifecycle/simulation.d.ts +3 -0
- package/dist/lifecycle/simulation.d.ts.map +1 -0
- package/dist/lifecycle/simulation.js +393 -0
- package/dist/lifecycle/simulation.js.map +1 -0
- package/dist/lifecycle/types.d.ts +273 -0
- package/dist/lifecycle/types.d.ts.map +1 -0
- package/dist/lifecycle/types.js +11 -0
- package/dist/lifecycle/types.js.map +1 -0
- package/docs/launch-lifecycle.md +243 -0
- package/package.json +12 -3
- package/src/abyss.ts +3 -1059
- package/src/addresses.ts +9 -26
- package/src/auction.ts +2 -2
- package/src/index.ts +2 -0
- package/src/launch-api.ts +110 -0
- package/src/launch.ts +43 -411
- package/src/lifecycle/abi.ts +343 -0
- package/src/lifecycle/authors.ts +203 -0
- package/src/lifecycle/fork.ts +36 -0
- package/src/lifecycle/index.ts +9 -0
- package/src/lifecycle/markets.ts +205 -0
- package/src/lifecycle/planner.ts +396 -0
- package/src/lifecycle/progress.ts +378 -0
- package/src/lifecycle/rpc.ts +88 -0
- package/src/lifecycle/schema.ts +387 -0
- package/src/lifecycle/simulation.ts +329 -0
- package/src/lifecycle/types.ts +132 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctionResult, toHex, type Hex } from "viem";
|
|
2
|
+
import { launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
|
|
3
|
+
import { LifecyclePhase, type LaunchProgressV1, type LaunchReceiptV1 } from "./schema.js";
|
|
4
|
+
import { assertLifecycleBlock, lifecycleRpc, readLifecycleBlock, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity } from "./rpc.js";
|
|
5
|
+
import { LifecyclePlanningError, type ControlledLifecycleFork, type LifecycleBlock, type LifecyclePostcondition, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleSimulationStep, type LifecycleTransaction, type PlannedLaunch, type ResolvedLifecycleLimits, type SimulateLaunchPlanOptions } from "./types.js";
|
|
6
|
+
|
|
7
|
+
function conditionRequest(condition: LifecyclePostcondition, planned: PlannedLaunch): { to: Hex; data: Hex } {
|
|
8
|
+
if (condition.kind === "allowance") return {
|
|
9
|
+
to: condition.asset, data: encodeFunctionData({ abi: lifecycleErc20Abi, functionName: "allowance", args: [planned.account, condition.spender] }),
|
|
10
|
+
};
|
|
11
|
+
return { to: planned.plan.orchestrator, data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "readLaunchProgress", args: [planned.launchId] }) };
|
|
12
|
+
}
|
|
13
|
+
function checkCondition(condition: LifecyclePostcondition, data: Hex, planned: PlannedLaunch): void {
|
|
14
|
+
if (condition.kind === "allowance") {
|
|
15
|
+
const allowance = decodeFunctionResult({ abi: lifecycleErc20Abi, functionName: "allowance", data });
|
|
16
|
+
if (allowance < condition.minimum || (condition.exact !== undefined && allowance !== condition.exact)) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Funding allowance was not established");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// The frozen ABI decoder produces the precise progress tuple, including all identity fields.
|
|
20
|
+
const progress = decodeFunctionResult({ abi: launchLifecycleAbi, functionName: "readLaunchProgress", data }) as LaunchProgressV1;
|
|
21
|
+
const phase = LifecyclePhase[condition.phase];
|
|
22
|
+
if (progress.phase !== phase || progress.preparedMarkets !== condition.preparedMarkets || progress.planHash.toLowerCase() !== planned.planHash.toLowerCase() || progress.launchId.toLowerCase() !== planned.launchId.toLowerCase() || progress.token.toLowerCase() !== planned.predictedToken.toLowerCase()) {
|
|
23
|
+
throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not establish the committed launch identity, phase and ordered progress");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunch, block: LifecycleBlock, transactions: readonly LifecycleTransaction[], gasLimits: readonly bigint[], limits: ResolvedLifecycleLimits, gasPrice: bigint, validation: boolean): Promise<LifecycleSimulationStep[]> {
|
|
28
|
+
const totalRequestedGas = gasLimits.reduce((total, gas) => total + gas, 0n);
|
|
29
|
+
if (limits.maxSimulationGas !== undefined && totalRequestedGas > limits.maxSimulationGas) throw new LifecyclePlanningError("SIMULATION_RPC_CAP", "Sequential request exceeds the current RPC aggregate simulation gas cap; use an isolated controlled fork");
|
|
30
|
+
// Natural child-block fees must govern fee-bearing admission; overriding
|
|
31
|
+
// baseFeePerGas with the parent's would fabricate a lower envelope and admit
|
|
32
|
+
// calls that the actual chain would reject. eth_simulateV1 derives the child
|
|
33
|
+
// base fee itself; only structural child fields (number/time) and the
|
|
34
|
+
// parent gasLimit are set.
|
|
35
|
+
const blockStateCalls = transactions.map((transaction, index) => ({
|
|
36
|
+
blockOverrides: { number: toHex(block.number + BigInt(index + 1)), time: toHex(block.timestamp + BigInt(index + 1)), gasLimit: toHex(block.gasLimit) },
|
|
37
|
+
calls: [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gasLimits[index] ?? limits.executionGasCeiling), gasPrice: toHex(gasPrice) }],
|
|
38
|
+
}));
|
|
39
|
+
const result = await lifecycleRpc(client, "eth_simulateV1", [{ blockStateCalls, validation, traceTransfers: false, returnFullTransactions: false }, toHex(block.number)]);
|
|
40
|
+
if (!Array.isArray(result) || result.length !== transactions.length) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Sequential simulation omitted blocks");
|
|
41
|
+
const steps: LifecycleSimulationStep[] = [];
|
|
42
|
+
for (let index = 0; index < transactions.length; index += 1) {
|
|
43
|
+
const transaction = transactions[index];
|
|
44
|
+
if (transaction === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing simulation transaction");
|
|
45
|
+
const simulatedBlock = rpcObject(result[index], "simulated block");
|
|
46
|
+
if (!Array.isArray(simulatedBlock.calls) || simulatedBlock.calls.length !== 1) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Sequential simulation omitted transaction results");
|
|
47
|
+
const call = rpcObject(simulatedBlock.calls[0], "simulated transaction");
|
|
48
|
+
const gasUsed = rpcQuantity(call.gasUsed, "simulated gas used");
|
|
49
|
+
const returnData = rpcHex(call.returnData, "simulated return data");
|
|
50
|
+
const success = rpcQuantity(call.status, "simulated status") === 1n;
|
|
51
|
+
let error: string | undefined;
|
|
52
|
+
if (!success) error = call.error === undefined ? `Execution reverted: ${returnData}` : JSON.stringify(call.error);
|
|
53
|
+
if (success) {
|
|
54
|
+
try {
|
|
55
|
+
// Verification reads must not become synthetic transactions that consume creator nonce/fees.
|
|
56
|
+
// Core return tuples/events and subsequent dependent commands prove persisted transitions.
|
|
57
|
+
const events = (Array.isArray(call.logs) ? call.logs : []).flatMap((raw) => {
|
|
58
|
+
const log = rpcObject(raw, "simulation event");
|
|
59
|
+
if (rpcHex(log.address, "simulation event address").toLowerCase() !== planned.plan.orchestrator.toLowerCase() || !Array.isArray(log.topics)) return [];
|
|
60
|
+
const topics = log.topics.map((topic) => rpcHex(topic, "event topic"));
|
|
61
|
+
if (topics.length === 0) return [];
|
|
62
|
+
const event = decodeEventLog({ abi: launchLifecycleAbi, topics: topics as [Hex, ...Hex[]], data: rpcHex(log.data, "simulation event data") });
|
|
63
|
+
return [event];
|
|
64
|
+
});
|
|
65
|
+
if (transaction.kind === "begin") checkCondition({ kind: "launch", phase: "Preparing", preparedMarkets: 0 }, returnData, planned);
|
|
66
|
+
else if (transaction.kind === "prepare") {
|
|
67
|
+
const prepared = events.filter((event) => event.eventName === "MarketPrepared");
|
|
68
|
+
if (prepared.length !== transaction.marketCount || prepared.some((event, ordinal) => event.eventName !== "MarketPrepared" || event.args.launchId.toLowerCase() !== planned.launchId.toLowerCase() || event.args.marketIndex !== (transaction.marketStart ?? 0) + ordinal)) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not prepare each exact ordered market");
|
|
69
|
+
} else if (transaction.kind === "atomic" || transaction.kind === "activate") {
|
|
70
|
+
const functionName = transaction.kind === "atomic" ? "launchAtomic" : "activateLaunch";
|
|
71
|
+
const receipt = decodeFunctionResult({ abi: launchLifecycleAbi, functionName, data: returnData }) as LaunchReceiptV1;
|
|
72
|
+
if (receipt.planHash.toLowerCase() !== planned.planHash.toLowerCase() || receipt.launchId.toLowerCase() !== planned.launchId.toLowerCase() || receipt.token.toLowerCase() !== planned.predictedToken.toLowerCase() || receipt.marketCount !== planned.plan.markets.length || receipt.quoteSpent.length !== planned.plan.buys.length || receipt.tokenOut.length !== planned.plan.buys.length || !events.some((event) => event.eventName === "LaunchActivated" && event.args.planHash.toLowerCase() === planned.planHash.toLowerCase() && event.args.token.toLowerCase() === planned.predictedToken.toLowerCase())) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not atomically complete the committed launch");
|
|
73
|
+
for (let buyIndex = 0; buyIndex < planned.plan.buys.length; buyIndex += 1) {
|
|
74
|
+
const buy = planned.plan.buys[buyIndex];
|
|
75
|
+
if (buy === undefined || (receipt.quoteSpent[buyIndex] ?? buy.quoteAmountIn + 1n) > buy.quoteAmountIn || (receipt.tokenOut[buyIndex] ?? 0n) < buy.minTokenOut) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not satisfy every ordered buy's budget and output minimum");
|
|
76
|
+
}
|
|
77
|
+
} else if (transaction.kind === "cancel" && !events.some((event) => event.eventName === "LaunchCancelled" && event.args.launchId.toLowerCase() === planned.launchId.toLowerCase() && event.args.creator.toLowerCase() === planned.account.toLowerCase())) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Simulation did not cancel the exact committed launch");
|
|
78
|
+
else if ((transaction.kind === "approve" || transaction.kind === "approve-reset") && returnData !== "0x" && decodeFunctionResult({ abi: lifecycleErc20Abi, functionName: "approve", data: returnData }) !== true) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Funding approval returned failure");
|
|
79
|
+
} catch (failure) { error = failure instanceof Error ? failure.message : String(failure); }
|
|
80
|
+
}
|
|
81
|
+
steps.push({ transactionId: transaction.id, success: success && error === undefined, gasUsed, gasLimit: gasLimits[index], returnData, error });
|
|
82
|
+
if (!success || error !== undefined) break;
|
|
83
|
+
}
|
|
84
|
+
return steps;
|
|
85
|
+
}
|
|
86
|
+
/** A failure that consumed nearly the whole limit is treated as possible gas-envelope
|
|
87
|
+
* exhaustion (EIP-150 retention starves forwarded sub-calls; the surfaced revert bytes
|
|
88
|
+
* may be a wrapped inner bubble rather than empty). Discovery is safe: a genuine
|
|
89
|
+
* business revert also fails at the exact ceiling, so it can never be masked. */
|
|
90
|
+
function looksGasExhausted(step: LifecycleSimulationStep, gasLimit: bigint): boolean {
|
|
91
|
+
return !step.success && step.gasUsed !== undefined && step.gasUsed >= gasLimit - gasLimit / 64n;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function waitForkReceipt(client: LifecycleRpcClient, transactionHash: Hex, timeoutMs: number): Promise<Record<string, unknown>> {
|
|
95
|
+
const deadline = Date.now() + timeoutMs;
|
|
96
|
+
while (Date.now() <= deadline) {
|
|
97
|
+
const receipt = await lifecycleRpc(client, "eth_getTransactionReceipt", [transactionHash]);
|
|
98
|
+
if (receipt !== null) return rpcObject(receipt, "fork receipt");
|
|
99
|
+
await new Promise<void>((resolve) => globalThis.setTimeout(resolve, 25));
|
|
100
|
+
}
|
|
101
|
+
throw new LifecyclePlanningError("FORK_RECEIPT_TIMEOUT", "The disposable fork did not mine its simulation transaction");
|
|
102
|
+
}
|
|
103
|
+
async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedLaunch, block: LifecycleBlock, transactions: readonly LifecycleTransaction[], gasLimits: readonly bigint[], gasPrice: bigint, measure: boolean, headroomBps: number): Promise<LifecycleSimulationStep[]> {
|
|
104
|
+
if (fork.isolation !== "disposable" || fork.allowTransactions !== true || !fork.sourceRpcUrl || !fork.forkRpcUrl) throw new LifecyclePlanningError("UNSAFE_FORK", "Actual simulation transactions require an explicitly distinct disposable controlled fork");
|
|
105
|
+
const sourceUrl = new URL(fork.sourceRpcUrl);
|
|
106
|
+
const forkUrl = new URL(fork.forkRpcUrl);
|
|
107
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]", "::1"];
|
|
108
|
+
const sourcePort = sourceUrl.port || (sourceUrl.protocol === "https:" ? "443" : "80");
|
|
109
|
+
const forkPort = forkUrl.port || (forkUrl.protocol === "https:" ? "443" : "80");
|
|
110
|
+
if (!loopback.includes(forkUrl.hostname) || sourceUrl.href === forkUrl.href || (loopback.includes(sourceUrl.hostname) && sourcePort === forkPort)) throw new LifecyclePlanningError("UNSAFE_FORK", "Disposable fork must not alias or share the loopback source endpoint");
|
|
111
|
+
let forkBlock = await readLifecycleBlock(fork.client);
|
|
112
|
+
if (fork.sourceRpcUrl !== undefined && forkBlock.hash.toLowerCase() !== block.hash.toLowerCase()) {
|
|
113
|
+
if (block.number > BigInt(Number.MAX_SAFE_INTEGER)) throw new LifecyclePlanningError("FORK_STATE_MISMATCH", "Pinned block is not exactly representable by the fork reset API");
|
|
114
|
+
await lifecycleRpc(fork.client, fork.resetMethod ?? "anvil_reset", [{ forking: { jsonRpcUrl: fork.sourceRpcUrl, blockNumber: Number(block.number) } }]);
|
|
115
|
+
forkBlock = await readLifecycleBlock(fork.client);
|
|
116
|
+
}
|
|
117
|
+
const forkChain = rpcQuantity(await lifecycleRpc(fork.client, "eth_chainId"), "fork chain ID");
|
|
118
|
+
if (forkChain !== planned.chainId || forkChain !== (fork.expectedChainId ?? planned.chainId) || forkBlock.hash.toLowerCase() !== block.hash.toLowerCase() || forkBlock.hash.toLowerCase() !== (fork.expectedBlockHash ?? block.hash).toLowerCase()) throw new LifecyclePlanningError("FORK_STATE_MISMATCH", "Controlled fork must match the exact source chain and pinned latest block before simulation");
|
|
119
|
+
const snapshot = rpcHex(await lifecycleRpc(fork.client, "evm_snapshot"), "fork snapshot");
|
|
120
|
+
const steps: LifecycleSimulationStep[] = [];
|
|
121
|
+
let impersonated = false;
|
|
122
|
+
try {
|
|
123
|
+
if (fork.impersonation !== undefined && fork.impersonation !== "none") {
|
|
124
|
+
await lifecycleRpc(fork.client, `${fork.impersonation}_impersonateAccount`, [planned.account]);
|
|
125
|
+
impersonated = true;
|
|
126
|
+
}
|
|
127
|
+
for (let index = 0; index < transactions.length; index += 1) {
|
|
128
|
+
const transaction = transactions[index];
|
|
129
|
+
let gas = gasLimits[index];
|
|
130
|
+
let gasRequired: bigint | undefined;
|
|
131
|
+
if (transaction === undefined || gas === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing fork transaction/gas context");
|
|
132
|
+
try {
|
|
133
|
+
if (measure) {
|
|
134
|
+
const estimated = rpcQuantity(await lifecycleRpc(fork.client, "eth_estimateGas", [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gasPrice: toHex(gasPrice) }]), "fork sequential gas estimate");
|
|
135
|
+
gasRequired = estimated;
|
|
136
|
+
const buffered = (estimated * BigInt(10000 + headroomBps) + 9999n) / 10000n;
|
|
137
|
+
if (buffered < gas) gas = buffered;
|
|
138
|
+
}
|
|
139
|
+
const hash = rpcHex(await lifecycleRpc(fork.client, "eth_sendTransaction", [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gas), gasPrice: toHex(gasPrice) }]), "fork transaction hash");
|
|
140
|
+
const receipt = await waitForkReceipt(fork.client, hash, fork.receiptTimeoutMs ?? 10_000);
|
|
141
|
+
const gasUsed = rpcQuantity(receipt.gasUsed, "fork gas used");
|
|
142
|
+
if (rpcQuantity(receipt.status, "fork transaction status") !== 1n) {
|
|
143
|
+
steps.push({ transactionId: transaction.id, success: false, gasUsed, gasLimit: gas, error: "Actual fork transaction reverted" });
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
const currentBlock = await readLifecycleBlock(fork.client);
|
|
147
|
+
let returnData: Hex | undefined;
|
|
148
|
+
if (transaction.kind === "atomic" || transaction.kind === "activate") {
|
|
149
|
+
const progressData = rpcHex(await lifecycleRpc(fork.client, "eth_call", [{ to: planned.plan.orchestrator, data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "readLaunchProgress", args: [planned.launchId] }) }, toHex(currentBlock.number)]), "fork activation progress");
|
|
150
|
+
const progress = decodeFunctionResult({ abi: launchLifecycleAbi, functionName: "readLaunchProgress", data: progressData });
|
|
151
|
+
if (!Array.isArray(receipt.logs)) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Actual fork receipt does not contain canonical launch logs");
|
|
152
|
+
const events = receipt.logs.flatMap((raw) => {
|
|
153
|
+
const log = rpcObject(raw, "fork activation log");
|
|
154
|
+
if (rpcHex(log.address, "fork log emitter").toLowerCase() !== planned.plan.orchestrator.toLowerCase()) return [];
|
|
155
|
+
if (!Array.isArray(log.topics)) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Actual fork log topics are malformed");
|
|
156
|
+
try { return [decodeEventLog({ abi: launchLifecycleAbi, data: rpcHex(log.data, "fork log data"), topics: log.topics.map((topic) => rpcHex(topic, "fork log topic")) as [Hex, ...Hex[]] })]; }
|
|
157
|
+
catch { return []; }
|
|
158
|
+
});
|
|
159
|
+
const activated = events.filter((event) => event.eventName === "LaunchActivated");
|
|
160
|
+
const buys = events.filter((event) => event.eventName === "InitialBuyExecuted");
|
|
161
|
+
if (activated.length !== 1 || !activated.some((event) => event.eventName === "LaunchActivated" && event.args.launchId.toLowerCase() === planned.launchId.toLowerCase() && event.args.planHash.toLowerCase() === planned.planHash.toLowerCase() && event.args.token.toLowerCase() === planned.predictedToken.toLowerCase()) ||
|
|
162
|
+
progress.phase !== LifecyclePhase.Active || progress.planHash.toLowerCase() !== planned.planHash.toLowerCase() || progress.token.toLowerCase() !== planned.predictedToken.toLowerCase() ||
|
|
163
|
+
buys.length !== planned.plan.buys.length) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Actual fork activation differs from the exact committed launch");
|
|
164
|
+
const quoteSpent: bigint[] = []; const tokenOut: bigint[] = [];
|
|
165
|
+
for (let buyIndex = 0; buyIndex < buys.length; buyIndex += 1) {
|
|
166
|
+
const event = buys[buyIndex]; const buy = planned.plan.buys[buyIndex];
|
|
167
|
+
if (event?.eventName !== "InitialBuyExecuted" || buy === undefined || event.args.launchId.toLowerCase() !== planned.launchId.toLowerCase() ||
|
|
168
|
+
event.args.buyIndex !== buyIndex || event.args.marketIndex !== buy.marketIndex || event.args.recipient.toLowerCase() !== buy.recipient.toLowerCase() ||
|
|
169
|
+
event.args.quoteAsset.toLowerCase() !== planned.plan.markets[buy.marketIndex]?.quoteAsset.toLowerCase() ||
|
|
170
|
+
event.args.quoteSpent > buy.quoteAmountIn || event.args.tokenOut < buy.minTokenOut) throw new LifecyclePlanningError("POSTCONDITION_FAILED", "Actual fork initial buy differs from its committed index, budget, destination or minimum");
|
|
171
|
+
quoteSpent.push(event.args.quoteSpent); tokenOut.push(event.args.tokenOut);
|
|
172
|
+
}
|
|
173
|
+
returnData = encodeFunctionResult({ abi: launchLifecycleAbi, functionName: transaction.kind === "atomic" ? "launchAtomic" : "activateLaunch", result: {
|
|
174
|
+
launchId: planned.launchId, planHash: progress.planHash, token: progress.token, feeHub: progress.feeHub, rewards: progress.rewards,
|
|
175
|
+
marketCount: progress.marketCount, positionCount: progress.positionCount, quoteSpent, tokenOut,
|
|
176
|
+
} });
|
|
177
|
+
}
|
|
178
|
+
for (const condition of transaction.postconditions) {
|
|
179
|
+
const request = conditionRequest(condition, planned);
|
|
180
|
+
const data = rpcHex(await lifecycleRpc(fork.client, "eth_call", [{ ...request, from: planned.account }, toHex(currentBlock.number)]), "fork postcondition");
|
|
181
|
+
checkCondition(condition, data, planned);
|
|
182
|
+
}
|
|
183
|
+
steps.push({ transactionId: transaction.id, success: true, gasUsed, gasLimit: gas, gasRequired, returnData });
|
|
184
|
+
} catch (failure) {
|
|
185
|
+
steps.push({ transactionId: transaction.id, success: false, gasLimit: gas, error: failure instanceof Error ? failure.message : String(failure) });
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return steps;
|
|
190
|
+
} finally {
|
|
191
|
+
try {
|
|
192
|
+
if (impersonated && fork.impersonation !== undefined) await lifecycleRpc(fork.client, `${fork.impersonation}_stopImpersonatingAccount`, [planned.account]);
|
|
193
|
+
} finally {
|
|
194
|
+
if (await lifecycleRpc(fork.client, "evm_revert", [snapshot]) !== true) throw new LifecyclePlanningError("FORK_CLEANUP_FAILED", "Disposable fork snapshot could not be reverted; do not reuse this RPC");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function simulateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], pinnedBlock?: LifecycleBlock): Promise<LifecycleSimulation> {
|
|
200
|
+
const { client, planned } = options;
|
|
201
|
+
if (options.fork?.client === client) throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
|
|
202
|
+
const block = pinnedBlock ?? await readLifecycleBlock(client);
|
|
203
|
+
const context = { client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId };
|
|
204
|
+
const { limits, configuration } = await resolveLifecycleLimits(context, options.limits ?? planned.limits);
|
|
205
|
+
const base = { confidence: "stateful" as const, blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits };
|
|
206
|
+
if (rpcQuantity(await lifecycleRpc(client, "eth_chainId"), "chain ID") !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
|
|
207
|
+
const accountCode = rpcHex(await lifecycleRpc(client, "eth_getCode", [planned.account, toHex(block.number)]), "account code");
|
|
208
|
+
if (accountCode !== "0x") return { ...base, confidence: "provisional", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
|
|
209
|
+
for (const transaction of transactions) {
|
|
210
|
+
if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) return { ...base, backend: "unavailable", admitted: false, steps: [], failedTransactionId: transaction.id, reason: "Exact calldata exceeds the current chain/account/RPC byte cap" };
|
|
211
|
+
}
|
|
212
|
+
if (transactions.length === 0) { await assertLifecycleBlock(client, block); return { ...base, backend: "unavailable", admitted: true, steps: [] }; }
|
|
213
|
+
const gasPrice = rpcQuantity(await lifecycleRpc(client, "eth_gasPrice"), "gas price");
|
|
214
|
+
const capGas = transactions.map(() => limits.executionGasCeiling);
|
|
215
|
+
let backend: "eth_simulateV1" | "controlled-fork" = "eth_simulateV1";
|
|
216
|
+
let measured: LifecycleSimulationStep[];
|
|
217
|
+
// Permissive cap measurement is not proof; only the exact-gas validated replay admits a plan.
|
|
218
|
+
try { measured = await simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false); }
|
|
219
|
+
catch (failure) {
|
|
220
|
+
if (options.fork === undefined) {
|
|
221
|
+
await assertLifecycleBlock(client, block);
|
|
222
|
+
return { ...base, confidence: "provisional", backend: "unavailable", admitted: false, steps: [], reason: `Stateful sequential RPC simulation unavailable: ${failure instanceof Error ? failure.message : String(failure)}. Dependent eth_call estimates are not proof.` };
|
|
223
|
+
}
|
|
224
|
+
backend = "controlled-fork";
|
|
225
|
+
measured = await simulateForkPass(options.fork, planned, block, transactions, capGas, gasPrice, true, limits.headroomBps);
|
|
226
|
+
}
|
|
227
|
+
const failed = measured.find((step) => !step.success);
|
|
228
|
+
if (failed !== undefined) { await assertLifecycleBlock(client, block); return { ...base, backend, admitted: false, steps: measured, reason: failed.error, failedTransactionId: failed.transactionId }; }
|
|
229
|
+
const gasLimits = measured.map((step) => ((step.gasRequired ?? step.gasUsed ?? limits.executionGasCeiling) * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n);
|
|
230
|
+
const exceeds = gasLimits.findIndex((gas) => gas > limits.executionGasCeiling);
|
|
231
|
+
if (exceeds >= 0) {
|
|
232
|
+
await assertLifecycleBlock(client, block);
|
|
233
|
+
return { ...base, backend, admitted: false, steps: measured, reason: "Execution gas plus conservative headroom exceeds current transaction limits", failedTransactionId: transactions[exceeds]?.id };
|
|
234
|
+
}
|
|
235
|
+
// Replay from the same initial state with exact headroom limits: gasUsed alone is not an EIP-150/gasleft proof.
|
|
236
|
+
// eth_simulateV1 with validation raises an RPC error on any revert (a generic
|
|
237
|
+
// -32000 "execution failed" carries no gasUsed/returnData), so a validated
|
|
238
|
+
// replay failure surfaces as a thrown error here. Route it through envelope
|
|
239
|
+
// discovery too: EIP-150 retention can starve a forwarded sub-call below the
|
|
240
|
+
// buffered limit while the true envelope still fits the execution ceiling.
|
|
241
|
+
let verified: LifecycleSimulationStep[];
|
|
242
|
+
let validatedReplayError: string | undefined;
|
|
243
|
+
try {
|
|
244
|
+
if (backend === "eth_simulateV1") verified = await simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true);
|
|
245
|
+
else {
|
|
246
|
+
if (options.fork === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Controlled fork is missing for the exact-gas replay");
|
|
247
|
+
verified = await simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice, false, limits.headroomBps);
|
|
248
|
+
}
|
|
249
|
+
} catch (failure) {
|
|
250
|
+
const message = failure instanceof Error ? failure.message : String(failure);
|
|
251
|
+
if (backend !== "eth_simulateV1") {
|
|
252
|
+
await assertLifecycleBlock(client, block);
|
|
253
|
+
return { ...base, backend, admitted: false, steps: [], reason: `Exact-gas validated replay failed: ${message}` };
|
|
254
|
+
}
|
|
255
|
+
validatedReplayError = message;
|
|
256
|
+
verified = [];
|
|
257
|
+
}
|
|
258
|
+
let verifiedSteps = verified;
|
|
259
|
+
let admittedGasLimits = gasLimits;
|
|
260
|
+
let finalFailure = verifiedSteps.find((step) => !step.success);
|
|
261
|
+
if (finalFailure === undefined && validatedReplayError !== undefined) {
|
|
262
|
+
// The buffered validated replay threw before yielding steps. Attempt one
|
|
263
|
+
// explicit envelope-discovery replay at the exact execution ceiling on the
|
|
264
|
+
// same pinned block; headroom is never silently raised — the carried gas is
|
|
265
|
+
// the validated envelope itself.
|
|
266
|
+
try {
|
|
267
|
+
const ceilingLimits = transactions.map(() => limits.executionGasCeiling);
|
|
268
|
+
const retried = backend === "eth_simulateV1"
|
|
269
|
+
? await simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true)
|
|
270
|
+
: options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
|
|
271
|
+
if (retried === undefined || retried.some((step) => !step.success)) {
|
|
272
|
+
await assertLifecycleBlock(client, block);
|
|
273
|
+
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${retried?.find((step) => !step.success)?.error ?? validatedReplayError}; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned`, failedTransactionId: transactions[0]?.id };
|
|
274
|
+
}
|
|
275
|
+
admittedGasLimits = ceilingLimits;
|
|
276
|
+
verifiedSteps = retried;
|
|
277
|
+
finalFailure = undefined;
|
|
278
|
+
validatedReplayError = undefined;
|
|
279
|
+
} catch {
|
|
280
|
+
await assertLifecycleBlock(client, block);
|
|
281
|
+
return { ...base, backend, admitted: false, steps: measured, reason: `Validated buffered replay failed (${validatedReplayError}) and envelope discovery at the exact gas ceiling was rejected by the simulation backend; final mint/lock/all-buys/public-opening is indivisible`, failedTransactionId: transactions[0]?.id };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (finalFailure !== undefined) {
|
|
285
|
+
const failedIndex = verifiedSteps.findIndex((step) => !step.success);
|
|
286
|
+
const bufferedLimit = gasLimits[failedIndex];
|
|
287
|
+
const failedTransaction = transactions[failedIndex];
|
|
288
|
+
if (bufferedLimit !== undefined && failedTransaction !== undefined && looksGasExhausted(finalFailure, bufferedLimit) && limits.executionGasCeiling > bufferedLimit) {
|
|
289
|
+
// Buffered replay returned an exhaustion-shaped failed step; learn the true
|
|
290
|
+
// validated envelope with one explicit replay at the exact execution ceiling.
|
|
291
|
+
try {
|
|
292
|
+
const ceilingLimits = transactions.map(() => limits.executionGasCeiling);
|
|
293
|
+
const retried = backend === "eth_simulateV1"
|
|
294
|
+
? await simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true)
|
|
295
|
+
: options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
|
|
296
|
+
if (retried !== undefined) {
|
|
297
|
+
const retryFailure = retried.find((step) => !step.success);
|
|
298
|
+
if (retryFailure === undefined) {
|
|
299
|
+
admittedGasLimits = ceilingLimits;
|
|
300
|
+
verifiedSteps = retried;
|
|
301
|
+
finalFailure = undefined;
|
|
302
|
+
} else {
|
|
303
|
+
const indivisible = failedTransaction.kind === "activate" || failedTransaction.kind === "atomic";
|
|
304
|
+
await assertLifecycleBlock(client, block);
|
|
305
|
+
return { ...base, backend, admitted: false, steps: measured, reason: `Validated execution at the exact current gas ceiling still failed: ${retryFailure.error ?? "the true gas envelope exceeds the current execution limit"}${indivisible ? "; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned" : ""}`, failedTransactionId: failedTransaction.id };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} catch { /* Envelope discovery unavailable; report the original buffered-limit failure unchanged. */ }
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const estimates: LifecycleSimulationStep[] = [];
|
|
312
|
+
const balance = rpcQuantity(await lifecycleRpc(client, "eth_getBalance", [planned.account, toHex(block.number)]), "creator native balance");
|
|
313
|
+
let remainingBalance = balance;
|
|
314
|
+
let affordabilityFailure: string | undefined;
|
|
315
|
+
for (let index = 0; index < verifiedSteps.length; index += 1) {
|
|
316
|
+
const step = verifiedSteps[index]; const transaction = transactions[index]; const gasLimit = admittedGasLimits[index];
|
|
317
|
+
if (step === undefined || transaction === undefined || gasLimit === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing verified fee context");
|
|
318
|
+
const dataFee = configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit }, context);
|
|
319
|
+
if (dataFee !== undefined && dataFee < 0n) throw new LifecyclePlanningError("INVALID_DATA_FEE", "Data-fee oracle returned a negative amount");
|
|
320
|
+
const executionFee = gasLimit * gasPrice;
|
|
321
|
+
const required = transaction.value + executionFee + (dataFee ?? 0n);
|
|
322
|
+
if (required > remainingBalance && affordabilityFailure === undefined) affordabilityFailure = "Creator native balance cannot fund the remaining transaction value and execution/data fee envelopes";
|
|
323
|
+
remainingBalance -= required;
|
|
324
|
+
estimates.push({ ...step, estimate: { gasUsed: step.gasUsed ?? 0n, gasLimit, gasPrice, executionFee, dataFee, totalFee: dataFee === undefined ? undefined : executionFee + dataFee, feeConfidence: dataFee === undefined ? "execution-only" : "execution-and-data" } });
|
|
325
|
+
}
|
|
326
|
+
await assertLifecycleBlock(client, block);
|
|
327
|
+
return { ...base, backend, admitted: finalFailure === undefined && affordabilityFailure === undefined && limits.admissionKnown, steps: estimates, reason: finalFailure?.error ?? affordabilityFailure ?? (limits.admissionKnown ? undefined : `Stateful execution was proved, but current execution admission constraints are incomplete: ${limits.unknownExecutionConstraints.join("; ")}`), failedTransactionId: finalFailure?.transactionId };
|
|
328
|
+
}
|
|
329
|
+
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { Address, Hash, Hex } from "viem";
|
|
2
|
+
import type {
|
|
3
|
+
AdapterRegistrationV1, AssetFundingV1, LaunchExecutionMode, LaunchPlanV1, LaunchProgressV1,
|
|
4
|
+
MarketLiveStateV1, PreparedMarketV1, ProfileRegistrationV1, ProfileTopologyV1, LaunchEnvelopeV2, LifecycleDeveloperTerms,
|
|
5
|
+
} from "./schema.js";
|
|
6
|
+
|
|
7
|
+
/** viem public clients or a raw JSON-RPC client; neither selects a transport. */
|
|
8
|
+
export type LifecycleRpcClient = {
|
|
9
|
+
request(args: { method: string; params?: readonly unknown[] }): Promise<unknown>;
|
|
10
|
+
};
|
|
11
|
+
export type LifecycleBlock = { number: bigint; hash: Hash; timestamp: bigint; gasLimit: bigint; baseFeePerGas?: bigint };
|
|
12
|
+
export type LifecycleLimitContext = { client: LifecycleRpcClient; account: Address; orchestrator: Address; block: LifecycleBlock; chainId: bigint };
|
|
13
|
+
export type LifecycleLimits = {
|
|
14
|
+
headroomBps?: number; chainGasLimit?: bigint; rpcGasLimit?: bigint; accountGasLimit?: bigint;
|
|
15
|
+
maxCalldataBytes?: number; maxSimulationGas?: bigint;
|
|
16
|
+
chainId?: bigint; orchestrator?: Address; account?: Address; observedBlockNumber?: bigint; observedBlockHash?: Hash;
|
|
17
|
+
/** Chain-specific L1/data-fee oracle, called with exact calldata and the pinned block. */
|
|
18
|
+
estimateDataFee?: (transaction: LifecycleTransaction, context: LifecycleLimitContext) => Promise<bigint>;
|
|
19
|
+
};
|
|
20
|
+
export type LifecycleLimitSource = LifecycleLimits | ((context: LifecycleLimitContext) => Promise<LifecycleLimits>);
|
|
21
|
+
export type ResolvedLifecycleLimits = {
|
|
22
|
+
executionGasCeiling: bigint; headroomBps: number; maxCalldataBytes?: number; maxSimulationGas?: bigint;
|
|
23
|
+
blockGasLimit: bigint; chainGasLimit?: bigint; rpcGasLimit?: bigint; accountGasLimit?: bigint;
|
|
24
|
+
admissionKnown: boolean; unknownExecutionConstraints: readonly string[]; unknownConstraints: readonly string[];
|
|
25
|
+
};
|
|
26
|
+
/** Only explicitly disposable RPCs may execute snapshot-isolated simulation transactions. */
|
|
27
|
+
export type ControlledLifecycleFork = {
|
|
28
|
+
client: LifecycleRpcClient; isolation: "disposable"; allowTransactions: true;
|
|
29
|
+
expectedChainId?: bigint; expectedBlockHash?: Hash; impersonation?: "anvil" | "hardhat" | "none";
|
|
30
|
+
receiptTimeoutMs?: number;
|
|
31
|
+
sourceRpcUrl: string; forkRpcUrl: string; resetMethod?: "anvil_reset" | "hardhat_reset";
|
|
32
|
+
};
|
|
33
|
+
export type LifecycleTransactionKind = "approve-reset" | "approve" | "atomic" | "begin" | "prepare" | "activate" | "cancel";
|
|
34
|
+
export type LifecyclePostcondition =
|
|
35
|
+
| { kind: "allowance"; asset: Address; spender: Address; minimum: bigint; exact?: bigint }
|
|
36
|
+
| { kind: "launch"; phase: "Preparing" | "Ready" | "Active" | "Cancelled"; preparedMarkets: number };
|
|
37
|
+
export type LifecycleTransaction = {
|
|
38
|
+
id: string; kind: LifecycleTransactionKind; chainId: number; from: Address; to: Address; data: Hex; value: bigint;
|
|
39
|
+
dependencies: readonly string[]; postconditions: readonly LifecyclePostcondition[];
|
|
40
|
+
marketStart?: number; marketCount?: number; gas?: bigint; gasPrice?: bigint;
|
|
41
|
+
/** Exact current-state execution admission, not a wallet batch or future guarantee. */
|
|
42
|
+
admission?: LifecycleAdmission;
|
|
43
|
+
estimate?: LifecycleTransactionEstimate;
|
|
44
|
+
};
|
|
45
|
+
export type LifecycleTransactionEstimate = {
|
|
46
|
+
gasUsed: bigint; gasLimit: bigint; gasPrice: bigint; executionFee: bigint; dataFee?: bigint; totalFee?: bigint;
|
|
47
|
+
feeConfidence: "execution-and-data" | "execution-only";
|
|
48
|
+
};
|
|
49
|
+
export type LifecycleAdmission = {
|
|
50
|
+
admitted: boolean; confidence: "stateful" | "provisional";
|
|
51
|
+
reason?: string; limits: ResolvedLifecycleLimits;
|
|
52
|
+
};
|
|
53
|
+
export type LifecycleSimulationStep = {
|
|
54
|
+
transactionId: string; success: boolean; gasUsed?: bigint; gasLimit?: bigint; gasRequired?: bigint;
|
|
55
|
+
returnData?: Hex; error?: string; estimate?: LifecycleTransactionEstimate;
|
|
56
|
+
};
|
|
57
|
+
export type LifecycleSimulation = {
|
|
58
|
+
backend: "eth_simulateV1" | "controlled-fork" | "unavailable";
|
|
59
|
+
confidence: "stateful" | "provisional"; admitted: boolean;
|
|
60
|
+
blockNumber: bigint; blockHash: Hash; account: Address; chainId: bigint;
|
|
61
|
+
steps: readonly LifecycleSimulationStep[]; limits: ResolvedLifecycleLimits;
|
|
62
|
+
reason?: string; failedTransactionId?: string;
|
|
63
|
+
};
|
|
64
|
+
export type LifecycleFundingPrerequisite = {
|
|
65
|
+
funding: AssetFundingV1; inputBalance: bigint; requiredInput: bigint;
|
|
66
|
+
allowance?: bigint; spender: Address; nativeValue: bigint; conversion: "none" | "native-wrap" | "allowlisted-swap";
|
|
67
|
+
};
|
|
68
|
+
export type LifecycleProfile = {
|
|
69
|
+
id: Hex; registration: ProfileRegistrationV1; adapter: AdapterRegistrationV1; topology: ProfileTopologyV1;
|
|
70
|
+
venueKind: "uniswap-v4" | "abyss" | "unknown";
|
|
71
|
+
envelope?: LaunchEnvelopeV2; developerTerms?: LifecycleDeveloperTerms; protocolMaximumDeveloperFeeBps?: number;
|
|
72
|
+
admitted: boolean; reason?: string;
|
|
73
|
+
};
|
|
74
|
+
export type LifecycleMarketProgress = {
|
|
75
|
+
index: number; prepared: PreparedMarketV1; live?: MarketLiveStateV1; error?: string;
|
|
76
|
+
};
|
|
77
|
+
export type LifecycleReceiptReference = {
|
|
78
|
+
transactionHash: Hash; replacementHash?: Hash; confirmations?: number;
|
|
79
|
+
observedBlockNumber?: bigint; observedBlockHash?: Hash;
|
|
80
|
+
};
|
|
81
|
+
export type LifecycleReceiptStatus = {
|
|
82
|
+
transactionHash: Hash; effectiveHash: Hash; status: "pending" | "confirmed" | "reverted" | "reorged" | "unconfirmed" | "replacement-cancelled";
|
|
83
|
+
blockNumber?: bigint; blockHash?: Hash; confirmations?: bigint; replaced: boolean;
|
|
84
|
+
};
|
|
85
|
+
export type CanonicalLaunchProgress = {
|
|
86
|
+
canonical: LaunchProgressV1; blockNumber: bigint; blockHash: Hash; chainId: bigint;
|
|
87
|
+
head: LaunchProgressV1; confirmationDepth: number; confirmedBlockNumber: bigint; confirmedBlockHash: Hash;
|
|
88
|
+
confirmedAccountNonce: bigint; headAccountNonce: bigint; pendingAccountNonce: bigint; confirmationSafe: boolean;
|
|
89
|
+
markets: readonly LifecycleMarketProgress[]; receipts: readonly LifecycleReceiptStatus[];
|
|
90
|
+
token: Address; planHash: Hash; launchId: Hash;
|
|
91
|
+
};
|
|
92
|
+
export type PoolBoundHookDeployment = { deployer: Address; initCodeHash: Hex; salt: Hex; predictedHook: Address };
|
|
93
|
+
export type LifecyclePoolBoundHookDeployment = PoolBoundHookDeployment & { marketIndex: number };
|
|
94
|
+
export type PoolBoundHookSaltProgress = { attempts: bigint; salt: Hex; predictedHook: Address };
|
|
95
|
+
export type PoolBoundLifecyclePreparationProgress = PoolBoundHookSaltProgress & { marketIndex: number };
|
|
96
|
+
export type PlannedLaunch = {
|
|
97
|
+
plan: LaunchPlanV1; planHash: Hash; launchId: Hash; predictedToken: Address;
|
|
98
|
+
tokenFactory: Address; tokenFactoryCodeHash: Hex;
|
|
99
|
+
account: Address; chainId: bigint; mode: LaunchExecutionMode; confirmations: number;
|
|
100
|
+
transactions: readonly LifecycleTransaction[]; prerequisites: readonly LifecycleFundingPrerequisite[];
|
|
101
|
+
profiles: readonly LifecycleProfile[]; simulation: LifecycleSimulation;
|
|
102
|
+
/** Exact CREATE2 metadata captured from the final committed bound markets. */
|
|
103
|
+
hookDeployments: readonly LifecyclePoolBoundHookDeployment[];
|
|
104
|
+
atomicAttempt: LifecycleSimulation; progress: CanonicalLaunchProgress;
|
|
105
|
+
preparationBatchSize: number; limits?: LifecycleLimitSource;
|
|
106
|
+
};
|
|
107
|
+
export type PlanLaunchOptions = {
|
|
108
|
+
client: LifecycleRpcClient; account: Address; plan: LaunchPlanV1; mode: LaunchExecutionMode;
|
|
109
|
+
limits?: LifecycleLimitSource; fork?: ControlledLifecycleFork; receipts?: readonly LifecycleReceiptReference[];
|
|
110
|
+
confirmations?: number;
|
|
111
|
+
};
|
|
112
|
+
export type SimulateLaunchPlanOptions = {
|
|
113
|
+
client: LifecycleRpcClient; planned: PlannedLaunch; limits?: LifecycleLimitSource; fork?: ControlledLifecycleFork;
|
|
114
|
+
/** Submitted receipt evidence and confirmation depth are inherited from the
|
|
115
|
+
* stored plan when omitted, exactly as build-next consumes them; resimulation
|
|
116
|
+
* never silently drops submitted-transaction context. */
|
|
117
|
+
receipts?: readonly LifecycleReceiptReference[]; confirmations?: number;
|
|
118
|
+
};
|
|
119
|
+
export type ReadLaunchProgressOptions = {
|
|
120
|
+
client: LifecycleRpcClient; planned: Pick<PlannedLaunch, "plan" | "planHash" | "launchId" | "predictedToken" | "account" | "mode" | "confirmations">;
|
|
121
|
+
receipts?: readonly LifecycleReceiptReference[]; confirmations?: number;
|
|
122
|
+
};
|
|
123
|
+
export type BuildNextTransactionOptions = ReadLaunchProgressOptions & {
|
|
124
|
+
planned: PlannedLaunch; action?: "continue" | "cancel"; limits?: LifecycleLimitSource; fork?: ControlledLifecycleFork;
|
|
125
|
+
};
|
|
126
|
+
export class LifecyclePlanningError extends Error {
|
|
127
|
+
readonly code: string;
|
|
128
|
+
readonly simulation?: LifecycleSimulation;
|
|
129
|
+
constructor(code: string, message: string, simulation?: LifecycleSimulation) {
|
|
130
|
+
super(message); this.name = "LifecyclePlanningError"; this.code = code; this.simulation = simulation;
|
|
131
|
+
}
|
|
132
|
+
}
|