@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.
- package/README.md +58 -22
- package/dist/launch.d.ts +31 -5
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js +71 -11
- package/dist/launch.js.map +1 -1
- package/dist/lifecycle/abi.d.ts +18 -0
- package/dist/lifecycle/abi.d.ts.map +1 -1
- package/dist/lifecycle/abi.js +4 -0
- package/dist/lifecycle/abi.js.map +1 -1
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +2 -0
- package/dist/lifecycle/index.js.map +1 -1
- package/dist/lifecycle/planner.d.ts +1 -1
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +58 -10
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/progress.js +35 -35
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +10 -4
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +137 -16
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/types.d.ts +20 -1
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +396 -64
- package/examples/.env.example +22 -2
- package/examples/README.md +315 -87
- package/examples/{launch-erc20-abyss.mjs → launch-erc20-abyss.ts} +1 -1
- package/examples/{launch-erc20-burn-mixed.mjs → launch-erc20-burn-mixed.ts} +1 -1
- package/examples/launch-erc20-depth-v4.ts +7 -0
- package/examples/{launch-erc20-dividends-abyss.mjs → launch-erc20-dividends-abyss.ts} +1 -1
- package/examples/launch-erc20-dividends-multiquote-mixed.ts +8 -0
- package/examples/launch-erc20-multi-quote.ts +8 -0
- package/examples/{launch-erc20-staking-v4.mjs → launch-erc20-staking-v4.ts} +1 -1
- package/examples/{launch-erc20-v4.mjs → launch-erc20-v4.ts} +1 -1
- package/examples/{launch-erc404-abyss.mjs → launch-erc404-abyss.ts} +1 -1
- package/examples/{launch-erc404-dividends-mixed.mjs → launch-erc404-dividends-mixed.ts} +1 -1
- package/examples/{launch-erc404-v4.mjs → launch-erc404-v4.ts} +1 -1
- package/examples/launch-example-kyber.ts +204 -0
- package/examples/launch-example-support.ts +876 -0
- package/examples/launch-example.ts +1163 -0
- package/examples/{launch-lifecycle-smoke.mjs → launch-lifecycle-smoke.ts} +1 -1
- package/examples/{launch.mjs → launch.ts} +1 -1
- package/package.json +2 -2
- package/src/launch.ts +81 -14
- package/src/lifecycle/abi.ts +4 -0
- package/src/lifecycle/index.ts +2 -0
- package/src/lifecycle/planner.ts +61 -12
- package/src/lifecycle/progress.ts +35 -35
- package/src/lifecycle/rpc.ts +140 -17
- package/src/lifecycle/types.ts +19 -1
- package/examples/launch-example-support.mjs +0 -393
- package/examples/launch-example.mjs +0 -633
- /package/examples/{generate-lifecycle-commitment-fixture.mjs → generate-lifecycle-commitment-fixture.ts} +0 -0
- /package/examples/{quickstart.mjs → quickstart.ts} +0 -0
package/src/lifecycle/rpc.ts
CHANGED
|
@@ -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
|
|
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;
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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)
|
|
222
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/src/lifecycle/types.ts
CHANGED
|
@@ -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;
|