@black-market/sdk 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +58 -22
  2. package/dist/launch.d.ts +31 -5
  3. package/dist/launch.d.ts.map +1 -1
  4. package/dist/launch.js +71 -11
  5. package/dist/launch.js.map +1 -1
  6. package/dist/lifecycle/abi.d.ts +18 -0
  7. package/dist/lifecycle/abi.d.ts.map +1 -1
  8. package/dist/lifecycle/abi.js +4 -0
  9. package/dist/lifecycle/abi.js.map +1 -1
  10. package/dist/lifecycle/index.d.ts +2 -0
  11. package/dist/lifecycle/index.d.ts.map +1 -1
  12. package/dist/lifecycle/index.js +2 -0
  13. package/dist/lifecycle/index.js.map +1 -1
  14. package/dist/lifecycle/planner.d.ts +1 -1
  15. package/dist/lifecycle/planner.d.ts.map +1 -1
  16. package/dist/lifecycle/planner.js +58 -10
  17. package/dist/lifecycle/planner.js.map +1 -1
  18. package/dist/lifecycle/progress.js +35 -35
  19. package/dist/lifecycle/progress.js.map +1 -1
  20. package/dist/lifecycle/rpc.d.ts +10 -4
  21. package/dist/lifecycle/rpc.d.ts.map +1 -1
  22. package/dist/lifecycle/rpc.js +137 -16
  23. package/dist/lifecycle/rpc.js.map +1 -1
  24. package/dist/lifecycle/types.d.ts +20 -1
  25. package/dist/lifecycle/types.d.ts.map +1 -1
  26. package/dist/lifecycle/types.js.map +1 -1
  27. package/docs/launch-lifecycle.md +396 -64
  28. package/examples/.env.example +22 -2
  29. package/examples/README.md +315 -87
  30. package/examples/{launch-erc20-abyss.mjs → launch-erc20-abyss.ts} +1 -1
  31. package/examples/{launch-erc20-burn-mixed.mjs → launch-erc20-burn-mixed.ts} +1 -1
  32. package/examples/launch-erc20-depth-v4.ts +7 -0
  33. package/examples/{launch-erc20-dividends-abyss.mjs → launch-erc20-dividends-abyss.ts} +1 -1
  34. package/examples/launch-erc20-dividends-multiquote-mixed.ts +8 -0
  35. package/examples/launch-erc20-multi-quote.ts +8 -0
  36. package/examples/{launch-erc20-staking-v4.mjs → launch-erc20-staking-v4.ts} +1 -1
  37. package/examples/{launch-erc20-v4.mjs → launch-erc20-v4.ts} +1 -1
  38. package/examples/{launch-erc404-abyss.mjs → launch-erc404-abyss.ts} +1 -1
  39. package/examples/{launch-erc404-dividends-mixed.mjs → launch-erc404-dividends-mixed.ts} +1 -1
  40. package/examples/{launch-erc404-v4.mjs → launch-erc404-v4.ts} +1 -1
  41. package/examples/launch-example-kyber.ts +204 -0
  42. package/examples/launch-example-support.ts +876 -0
  43. package/examples/launch-example.ts +1163 -0
  44. package/examples/{launch-lifecycle-smoke.mjs → launch-lifecycle-smoke.ts} +1 -1
  45. package/examples/{launch.mjs → launch.ts} +1 -1
  46. package/package.json +2 -2
  47. package/src/launch.ts +81 -14
  48. package/src/lifecycle/abi.ts +4 -0
  49. package/src/lifecycle/index.ts +2 -0
  50. package/src/lifecycle/planner.ts +61 -12
  51. package/src/lifecycle/progress.ts +35 -35
  52. package/src/lifecycle/rpc.ts +140 -17
  53. package/src/lifecycle/types.ts +19 -1
  54. package/examples/launch-example-support.mjs +0 -393
  55. package/examples/launch-example.mjs +0 -633
  56. /package/examples/{generate-lifecycle-commitment-fixture.mjs → generate-lifecycle-commitment-fixture.ts} +0 -0
  57. /package/examples/{quickstart.mjs → quickstart.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { decodeFunctionResult, encodeFunctionData, isAddress, isHex, parseAbi, toHex, type Abi, type Address, type Hex } from "viem";
2
- import { LifecyclePlanningError, type LifecycleBlock, type LifecycleDiagnosticEvent, type LifecycleDiagnosticListener, type LifecycleDiagnosticStage, type LifecycleLimitContext, type LifecycleLimitSource, type LifecycleLimits, type LifecycleRpcClient, type LifecycleTransaction, type ResolvedLifecycleLimits } from "./types.js";
2
+ import { LifecyclePlanningError, type LifecycleBlock, type LifecycleDiagnosticEvent, type LifecycleDiagnosticListener, type LifecycleDiagnosticStage, type LifecycleLimitContext, type LifecycleLimitSource, type LifecycleLimits, type LifecycleRpcClient, type LifecycleSessionCache, type LifecycleTransaction, type ResolvedLifecycleLimits } from "./types.js";
3
3
  import { lifecycleRegistryAbi } from "./abi.js";
4
4
  import type { ProfileTopologyV1 } from "./schema.js";
5
5
 
@@ -10,6 +10,55 @@ const diagnosticRpcMethods: Record<string, true> = {
10
10
  eth_simulateV1: true, eth_getTransactionCount: true, eth_getTransactionReceipt: true, eth_getTransactionByHash: true,
11
11
  };
12
12
 
13
+ type SessionReadGroup = {
14
+ chainId?: bigint;
15
+ block: Readonly<Pick<LifecycleBlock, "number" | "hash">>;
16
+ values: Map<string, Hex>;
17
+ };
18
+
19
+ /** Private wire observations; callers can inspect the pin or discard it, never install values. */
20
+ class SessionLifecycleCache implements LifecycleSessionCache {
21
+ #group?: SessionReadGroup;
22
+
23
+ get block(): LifecycleSessionCache["block"] { return this.#group?.block; }
24
+
25
+ invalidate(): void {
26
+ this.#group?.values.clear();
27
+ this.#group = undefined;
28
+ }
29
+
30
+ pin(block: LifecycleBlock, chainId?: bigint): SessionReadGroup {
31
+ const group = this.#group;
32
+ if (group !== undefined && group.block.number === block.number && group.block.hash.toLowerCase() === block.hash.toLowerCase() &&
33
+ (chainId === undefined || group.chainId === undefined || group.chainId === chainId)) {
34
+ if (chainId !== undefined) group.chainId = chainId;
35
+ return group;
36
+ }
37
+ this.invalidate();
38
+ return this.#group = {
39
+ chainId,
40
+ block: Object.freeze({ number: block.number, hash: block.hash }),
41
+ values: new Map(),
42
+ };
43
+ }
44
+
45
+ isCurrent(group: SessionReadGroup, block: LifecycleBlock): boolean {
46
+ return this.#group === group &&
47
+ group.block.number === block.number && group.block.hash.toLowerCase() === block.hash.toLowerCase();
48
+ }
49
+
50
+ observeChain(group: SessionReadGroup, chainId: bigint): void {
51
+ if (this.#group !== group) return;
52
+ if (group.chainId !== undefined && group.chainId !== chainId) this.invalidate();
53
+ else group.chainId = chainId;
54
+ }
55
+ }
56
+
57
+ /** Opt in per caller/session; there is no global or transport-owned lifecycle cache. */
58
+ export function createLifecycleSessionCache(): LifecycleSessionCache {
59
+ return new SessionLifecycleCache();
60
+ }
61
+
13
62
  /** Owns only one invocation's immutable RPC observations, never live checks or writes. */
14
63
  class InvocationLifecycleClient implements LifecycleRpcClient {
15
64
  private readonly reads = new Map<string, Promise<unknown>>();
@@ -19,8 +68,19 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
19
68
  private diagnosticSequence = 0;
20
69
  private chainId?: Promise<bigint>;
21
70
  private readonly limitObservations = new Map<LifecycleLimitSource | undefined, Map<string, Promise<LifecycleLimitObservation>>>();
71
+ private readonly session?: SessionLifecycleCache;
72
+ private sessionGroup?: SessionReadGroup;
73
+ private observedChainId?: bigint;
74
+ private sessionHits = 0;
75
+ private sessionMisses = 0;
76
+ private sessionReuses = 0;
22
77
  constructor(readonly source: LifecycleRpcClient) {
23
78
  this.concurrency = source.supportsReadBatching === true ? lifecycleReadConcurrency : 1;
79
+ if (source.sessionCache !== undefined) {
80
+ if (!(source.sessionCache instanceof SessionLifecycleCache)) throw new LifecyclePlanningError("INVALID_SESSION_CACHE", "Lifecycle session cache must be created by createLifecycleSessionCache");
81
+ this.session = source.sessionCache;
82
+ if (source.onDiagnostic !== undefined) emitLifecycleDiagnostic(source.onDiagnostic, { stage: "session-cache", phase: "start" });
83
+ }
24
84
  }
25
85
 
26
86
  async request(args: { method: string; params?: readonly unknown[] }): Promise<unknown> {
@@ -54,6 +114,10 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
54
114
  try {
55
115
  const result = await pending;
56
116
  if (listener !== undefined) emitLifecycleDiagnostic(listener, { stage: "rpc", phase: "success", requestId, method: observedMethod, durationMs: performance.now() - started });
117
+ if (this.session !== undefined && method === "eth_chainId") {
118
+ this.observedChainId = rpcQuantity(result, "chain ID");
119
+ if (this.sessionGroup !== undefined) this.session.observeChain(this.sessionGroup, this.observedChainId);
120
+ }
57
121
  return result;
58
122
  } catch (error) {
59
123
  if (listener !== undefined) emitLifecycleDiagnostic(listener, { stage: "rpc", phase: "failure", requestId, method: observedMethod, durationMs: performance.now() - started });
@@ -61,6 +125,21 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
61
125
  }
62
126
  }
63
127
 
128
+ observeBlock(block: LifecycleBlock, latest: boolean): void {
129
+ if (this.session === undefined) return;
130
+ this.assertOpen();
131
+ if (latest) {
132
+ this.sessionGroup = this.session.pin(block, this.observedChainId);
133
+ } else if (this.sessionGroup?.block.number === block.number && this.sessionGroup.block.hash.toLowerCase() !== block.hash.toLowerCase()) {
134
+ this.invalidateSession();
135
+ }
136
+ }
137
+
138
+ invalidateSession(): void {
139
+ this.session?.invalidate();
140
+ this.sessionGroup = undefined;
141
+ }
142
+
64
143
  observeChainId(): Promise<bigint> {
65
144
  this.assertOpen();
66
145
  return this.chainId ??= this.run("eth_chainId", []).then((value) => rpcQuantity(value, "chain ID"));
@@ -89,37 +168,63 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
89
168
  return observation.then((result) => { this.assertOpen(); result.assertCurrent(); return result; });
90
169
  }
91
170
 
92
- read(method: string, params: readonly unknown[], block: LifecycleBlock): Promise<unknown> {
171
+ read(method: string, params: readonly unknown[], block: LifecycleBlock, staticRead = false): Promise<unknown> {
93
172
  this.assertOpen();
94
173
  // Source identity is this scope's original client. The complete wire call includes
95
174
  // caller, value, gas and any other call fields; no selector-only/ABI-name aliasing.
96
175
  const key = JSON.stringify([block.number.toString(), block.hash.toLowerCase(), method, params]);
97
176
  const existing = this.reads.get(key);
98
177
  if (existing !== undefined) {
178
+ if (staticRead && this.session !== undefined) this.sessionReuses += 1;
99
179
  const listener = this.source.onDiagnostic;
100
180
  if (listener !== undefined) emitLifecycleDiagnostic(listener, { stage: "rpc", phase: "reuse", method: diagnosticRpcMethods[method] === true ? method : "other" });
101
181
  return existing;
102
182
  }
103
- const pending = this.run(method, params);
183
+ const group = staticRead && this.observedChainId !== undefined && this.sessionGroup !== undefined &&
184
+ this.session?.isCurrent(this.sessionGroup, block) ? this.sessionGroup : undefined;
185
+ const cached = group?.values.get(key);
186
+ let pending: Promise<unknown>;
187
+ if (cached !== undefined) {
188
+ this.sessionHits += 1;
189
+ pending = Promise.resolve(cached);
190
+ } else {
191
+ if (staticRead && this.session !== undefined) this.sessionMisses += 1;
192
+ pending = this.run(method, params);
193
+ if (group !== undefined) pending = pending.then((value) => {
194
+ // Never share in-flight work or install late results after invalidation/closure.
195
+ // Only wire hex is retained, so public decoded DTOs cannot mutate authority.
196
+ if (!this.closed && this.session?.isCurrent(group, block) && typeof value === "string" && isHex(value)) group.values.set(key, value);
197
+ return value;
198
+ });
199
+ }
104
200
  this.reads.set(key, pending);
105
201
  void pending.catch(() => { if (this.reads.get(key) === pending) this.reads.delete(key); });
106
202
  return pending;
107
203
  }
108
204
 
109
- close(): void {
205
+ close(success: boolean): void {
110
206
  this.closed = true;
111
207
  this.reads.clear();
112
208
  this.chainId = undefined;
113
209
  this.limitObservations.clear();
210
+ const listener = this.source.onDiagnostic;
211
+ if (this.session !== undefined && listener !== undefined) emitLifecycleDiagnostic(listener, {
212
+ stage: "session-cache", phase: success ? "success" : "failure",
213
+ hits: this.sessionHits, misses: this.sessionMisses, reuses: this.sessionReuses,
214
+ });
114
215
  }
115
216
  }
116
217
 
117
- /** Internal callers join the same invocation; no process-wide/source-wide read cache. */
218
+ /** Internal callers join the same invocation; only explicit static reads opt in to a session. */
118
219
  export async function withLifecycleReadClient<T>(source: LifecycleRpcClient, work: (client: LifecycleRpcClient) => Promise<T>): Promise<T> {
119
220
  if (source instanceof InvocationLifecycleClient) return work(source);
120
221
  const client = new InvocationLifecycleClient(source);
121
- try { return await work(client); }
122
- finally { client.close(); }
222
+ let success = false;
223
+ try {
224
+ const result = await work(client);
225
+ success = true;
226
+ return result;
227
+ } finally { client.close(success); }
123
228
  }
124
229
 
125
230
  export function lifecycleSourceClient(client: LifecycleRpcClient): LifecycleRpcClient {
@@ -160,9 +265,14 @@ async function observedLifecycleStage<T>(listener: LifecycleDiagnosticListener,
160
265
  }
161
266
 
162
267
  /** Only explicitly pinned fixed-state reads may participate in invocation reuse. */
163
- export async function lifecyclePinnedRpc(client: LifecycleRpcClient, method: "eth_call" | "eth_getCode" | "eth_getBalance", params: readonly unknown[], block: LifecycleBlock): Promise<unknown> {
268
+ export async function lifecyclePinnedRpc(client: LifecycleRpcClient, method: "eth_call" | "eth_getCode" | "eth_getBalance", params: readonly unknown[], block: LifecycleBlock, staticRead = false): Promise<unknown> {
164
269
  if (params.length !== 2 || params[1] !== toHex(block.number)) throw new LifecyclePlanningError("INVALID_READ_CONTEXT", "Reusable lifecycle reads require the exact pinned block and no state overrides");
165
- return client instanceof InvocationLifecycleClient ? client.read(method, params, block) : lifecycleRpc(client, method, params);
270
+ return client instanceof InvocationLifecycleClient ? client.read(method, params, block, staticRead && method !== "eth_getBalance") : lifecycleRpc(client, method, params);
271
+ }
272
+
273
+ /** Explicit SDK-owned static class only; balances and execution calls never use this path. */
274
+ export function lifecycleStaticRpc(client: LifecycleRpcClient, method: "eth_call" | "eth_getCode", params: readonly unknown[], block: LifecycleBlock): Promise<unknown> {
275
+ return lifecyclePinnedRpc(client, method, params, block, true);
166
276
  }
167
277
 
168
278
  export async function lifecycleRpc(client: LifecycleRpcClient, method: string, params: readonly unknown[] = []): Promise<unknown> {
@@ -207,35 +317,48 @@ export async function readLifecycleBlock(client: LifecycleRpcClient, tag: Hex |
207
317
  const value = rpcObject(await lifecycleRpc(client, "eth_getBlockByNumber", [tag, false]), "block");
208
318
  const hash = rpcHex(value.hash, "block hash");
209
319
  if (hash.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Block hash must have 32 bytes");
210
- return {
320
+ const block = {
211
321
  number: rpcQuantity(value.number, "block number"), hash,
212
322
  timestamp: rpcQuantity(value.timestamp, "block timestamp"), gasLimit: rpcQuantity(value.gasLimit, "block gas limit"),
213
323
  baseFeePerGas: value.baseFeePerGas === undefined || value.baseFeePerGas === null ? undefined : rpcQuantity(value.baseFeePerGas, "base fee"),
214
324
  };
325
+ if (client instanceof InvocationLifecycleClient) client.observeBlock(block, tag === "latest");
326
+ return block;
215
327
  }
216
328
  export async function assertLifecycleBlock(client: LifecycleRpcClient, block: LifecycleBlock, expectedChainId?: bigint): Promise<void> {
217
329
  const [chainId, current] = await Promise.all([
218
330
  expectedChainId === undefined ? undefined : lifecycleRpc(client, "eth_chainId"),
219
331
  readLifecycleBlock(client, toHex(block.number)),
220
332
  ]);
221
- if (expectedChainId !== undefined && rpcQuantity(chainId, "chain ID") !== expectedChainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during the pinned operation");
222
- if (current.hash.toLowerCase() !== block.hash.toLowerCase()) throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch state changed during the operation; read canonical progress again");
333
+ if (expectedChainId !== undefined && rpcQuantity(chainId, "chain ID") !== expectedChainId) {
334
+ if (client instanceof InvocationLifecycleClient) client.invalidateSession();
335
+ throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during the pinned operation");
336
+ }
337
+ if (current.hash.toLowerCase() !== block.hash.toLowerCase()) {
338
+ if (client instanceof InvocationLifecycleClient) client.invalidateSession();
339
+ throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch state changed during the operation; read canonical progress again");
340
+ }
223
341
  }
224
- export async function readLifecycleContract<T>(client: LifecycleRpcClient, to: Address, abi: Abi, functionName: string, args: readonly unknown[], block: LifecycleBlock, from?: Address): Promise<T> {
342
+ export async function readLifecycleContract<T>(client: LifecycleRpcClient, to: Address, abi: Abi, functionName: string, args: readonly unknown[], block: LifecycleBlock, from?: Address, staticRead = false): Promise<T> {
225
343
  const data = encodeFunctionData({ abi, functionName, args });
226
344
  const request = [{ to, from, data }, toHex(block.number)];
227
345
  const getter = abi.some((item) => item.type === "function" && item.name === functionName && (item.stateMutability === "view" || item.stateMutability === "pure"));
228
346
  const result = rpcHex(await (getter && functionName !== "readLaunchProgress"
229
- ? lifecyclePinnedRpc(client, "eth_call", request, block)
347
+ ? staticRead ? lifecycleStaticRpc(client, "eth_call", request, block) : lifecyclePinnedRpc(client, "eth_call", request, block)
230
348
  : lifecycleRpc(client, "eth_call", request)), functionName);
231
349
  // ABI decoding validates scalar widths and tuple layout; T names that exact frozen ABI result.
232
350
  const decoded = decodeFunctionResult({ abi, functionName, data: result }) as T;
233
351
  return decoded;
234
352
  }
235
353
 
354
+ /** Certification/configuration getters, explicitly selected by their SDK callsite. */
355
+ export function readLifecycleStaticContract<T>(client: LifecycleRpcClient, to: Address, abi: Abi, functionName: string, args: readonly unknown[], block: LifecycleBlock, from?: Address): Promise<T> {
356
+ return readLifecycleContract<T>(client, to, abi, functionName, args, block, from, true);
357
+ }
358
+
236
359
  /** A reviewed registry must supply topology; missing selectors never certify a profile. */
237
360
  export async function readLifecycleProfileTopology(client: LifecycleRpcClient, registry: Address, profileId: Hex, block: LifecycleBlock): Promise<ProfileTopologyV1> {
238
- const topology = await readLifecycleContract<{ hookTopology: number; configVersion: number; hookDeployer: Address; hookCreationCodeHash: Hex }>(client, registry, lifecycleRegistryAbi, "profileTopology", [profileId], block);
361
+ const topology = await readLifecycleStaticContract<{ hookTopology: number; configVersion: number; hookDeployer: Address; hookCreationCodeHash: Hex }>(client, registry, lifecycleRegistryAbi, "profileTopology", [profileId], block);
239
362
  if (topology.hookTopology !== 0 && topology.hookTopology !== 1 && topology.hookTopology !== 2) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Registry returned an unsupported hook topology");
240
363
  return { ...topology, hookTopology: topology.hookTopology };
241
364
  }
@@ -250,7 +373,7 @@ const maxUint64 = (1n << 64n) - 1n;
250
373
  /** A poster quote is an envelope budget, never an execution/compute proof. */
251
374
  export async function readNitroPosterGas(transaction: LifecycleTransaction, context: LifecycleLimitContext): Promise<{ posterGas: bigint; baseFee: bigint; l1BaseFeeEstimate: bigint }> {
252
375
  const data = encodeFunctionData({ abi: nitroNodeInterfaceAbi, functionName: "gasEstimateL1Component", args: [transaction.to, false, transaction.data] });
253
- const result = rpcHex(await lifecyclePinnedRpc(context.client, "eth_call", [{ from: transaction.from, to: nitroNodeInterface, data, value: toHex(transaction.value) }, toHex(context.block.number)], context.block), "Nitro poster estimate");
376
+ const result = rpcHex(await lifecycleStaticRpc(context.client, "eth_call", [{ from: transaction.from, to: nitroNodeInterface, data, value: toHex(transaction.value) }, toHex(context.block.number)], context.block), "Nitro poster estimate");
254
377
  if (result.length !== 194) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Nitro poster estimate must contain exactly three ABI words");
255
378
  const [posterGas, baseFee, l1BaseFeeEstimate] = decodeFunctionResult({ abi: nitroNodeInterfaceAbi, functionName: "gasEstimateL1Component", data: result });
256
379
  if (posterGas < 0n || posterGas > maxUint64 || baseFee <= 0n || l1BaseFeeEstimate < 0n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Nitro poster estimate has invalid gas or fee values");
@@ -259,7 +382,7 @@ export async function readNitroPosterGas(transaction: LifecycleTransaction, cont
259
382
 
260
383
  async function readNitroScalar(context: LifecycleLimitContext, to: Address, abi: Abi, functionName: string): Promise<bigint> {
261
384
  const data = encodeFunctionData({ abi, functionName });
262
- const result = rpcHex(await lifecyclePinnedRpc(context.client, "eth_call", [{ to, data }, toHex(context.block.number)], context.block), functionName);
385
+ const result = rpcHex(await lifecycleStaticRpc(context.client, "eth_call", [{ to, data }, toHex(context.block.number)], context.block), functionName);
263
386
  if (result.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", `${functionName} must return exactly one ABI word`);
264
387
  return decodeFunctionResult({ abi, functionName, data: result }) as bigint;
265
388
  }
@@ -8,7 +8,7 @@ import type {
8
8
  export type LifecycleDiagnosticStage =
9
9
  | "plan" | "plan.context" | "plan.domain" | "plan.progress" | "plan.inputs" | "rpc"
10
10
  | "profile.certification" | "simulation.context" | "simulation.nitro"
11
- | "simulation.poster" | "simulation.measure" | "simulation.replay" | "simulation.envelope";
11
+ | "simulation.poster" | "simulation.measure" | "simulation.replay" | "simulation.envelope" | "session-cache";
12
12
  export type LifecycleDiagnosticEvent = {
13
13
  stage: LifecycleDiagnosticStage;
14
14
  phase: "queued" | "start" | "success" | "failure" | "reuse";
@@ -16,6 +16,10 @@ export type LifecycleDiagnosticEvent = {
16
16
  queueMs?: number;
17
17
  requestId?: number;
18
18
  method?: string;
19
+ /** Static session hits/misses and invocation-local static reuse; counts only. */
20
+ hits?: number;
21
+ misses?: number;
22
+ reuses?: number;
19
23
  };
20
24
  export type LifecycleDiagnosticListener = (event: LifecycleDiagnosticEvent) => void | Promise<void>;
21
25
 
@@ -26,8 +30,16 @@ export type LifecycleRpcClient = {
26
30
  supportsReadBatching?: boolean;
27
31
  /** Optional local observer. Listener failures cannot change execution or admission. */
28
32
  onDiagnostic?: LifecycleDiagnosticListener;
33
+ /** Caller-owned, opt-in static observations; create with createLifecycleSessionCache(). */
34
+ sessionCache?: LifecycleSessionCache;
29
35
  };
30
36
  export type LifecycleBlock = { number: bigint; hash: Hash; timestamp: bigint; gasLimit: bigint; baseFeePerGas?: bigint };
37
+ /** A session retains only static reads at one canonical block, never execution proof. */
38
+ export type LifecycleSessionCache = {
39
+ readonly block: Readonly<Pick<LifecycleBlock, "number" | "hash">> | undefined;
40
+ /** Call on wallet/chain changes and after every confirmed transaction. */
41
+ invalidate(): void;
42
+ };
31
43
  export type LifecycleLimitContext = { client: LifecycleRpcClient; account: Address; orchestrator: Address; block: LifecycleBlock; chainId: bigint };
32
44
  export type LifecycleLimits = {
33
45
  /** Optional tightening caps on the complete transaction gas envelope, including Nitro poster gas. */
@@ -152,11 +164,17 @@ export type PlannedLaunch = {
152
164
  hookDeployments: readonly LifecyclePoolBoundHookDeployment[];
153
165
  progress: CanonicalLaunchProgress;
154
166
  preparationBatchSize: number; limits?: LifecycleLimitSource;
167
+ /** Unstarted-launch admission choices only; never part of the committed plan or calldata. */
168
+ allowTopologyGaps?: boolean; allowMissingPermanentTail?: boolean;
155
169
  };
156
170
  export type PlanLaunchOptions = {
157
171
  client: LifecycleRpcClient; account: Address; plan: LaunchPlanV1; mode: LaunchExecutionMode;
158
172
  limits?: LifecycleLimitSource; fork?: ControlledLifecycleFork; receipts?: readonly LifecycleReceiptReference[];
159
173
  confirmations?: number; signal?: AbortSignal;
174
+ /** Unstarted only: permit interior gaps; opening containment, aligned ticks and the tail remain required. */
175
+ allowTopologyGaps?: boolean;
176
+ /** Unstarted only: waive the funded boundary tail; all other topology checks remain required. */
177
+ allowMissingPermanentTail?: boolean;
160
178
  };
161
179
  export type PrepareAndPlanLifecycleLaunchOptions = PlanLaunchOptions & {
162
180
  onProgress?: (progress: PoolBoundLifecyclePreparationProgress) => void;