@black-market/sdk 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -96
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +59 -24
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/progress.d.ts +3 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +120 -55
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +8 -2
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +165 -9
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts +1 -1
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +11 -10
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/types.d.ts +12 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +605 -1495
- package/docs/reference.md +332 -0
- package/examples/.env.example +3 -2
- package/examples/README.md +171 -552
- package/examples/launch-example-support.ts +11 -3
- package/examples/launch-example.ts +1 -1
- package/examples/launch-lifecycle-smoke.ts +1 -1
- package/package.json +1 -1
- package/src/lifecycle/planner.ts +58 -23
- package/src/lifecycle/progress.ts +110 -47
- package/src/lifecycle/rpc.ts +158 -10
- package/src/lifecycle/simulation.ts +11 -9
- package/src/lifecycle/types.ts +9 -1
package/src/lifecycle/rpc.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { decodeFunctionResult, encodeFunctionData, isAddress, isHex, parseAbi, toHex, type Abi, type Address, type Hex } from "viem";
|
|
1
|
+
import { decodeFunctionResult, encodeFunctionData, isAddress, isHex, keccak256, multicall3Abi, parseAbi, toHex, type Abi, type Address, type Hex } from "viem";
|
|
2
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
|
+
import { robinhoodMainnet } from "../addresses.js";
|
|
6
|
+
|
|
7
|
+
// Reviewed chain4663 deployment, independently observed at block 98,136,655.
|
|
8
|
+
const multicall3CodeHash = "0xd5c15df687b16f2ff992fc8d767b4216323184a2bbc6ee2f9c398c318e770891";
|
|
9
|
+
type IndependentRead = { params: readonly unknown[]; resolve: (value: unknown) => void; reject: (failure: unknown) => void };
|
|
5
10
|
|
|
6
11
|
const lifecycleReadConcurrency = 8;
|
|
7
12
|
const diagnosticRpcMethods: Record<string, true> = {
|
|
@@ -67,6 +72,11 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
67
72
|
private readonly concurrency: number;
|
|
68
73
|
private diagnosticSequence = 0;
|
|
69
74
|
private chainId?: Promise<bigint>;
|
|
75
|
+
private gasPrice?: Promise<unknown>;
|
|
76
|
+
private readonly immutableReads = new Map<string, Promise<unknown>>();
|
|
77
|
+
private readonly canonicalPins = new Map<string, { block: Pick<LifecycleBlock, "number" | "hash">; chainId?: bigint }>();
|
|
78
|
+
private readonly independentReads = new Map<string, { block: LifecycleBlock; reads: IndependentRead[] }>();
|
|
79
|
+
readonly finalChecks = new Set<() => void>();
|
|
70
80
|
private readonly limitObservations = new Map<LifecycleLimitSource | undefined, Map<string, Promise<LifecycleLimitObservation>>>();
|
|
71
81
|
private readonly session?: SessionLifecycleCache;
|
|
72
82
|
private sessionGroup?: SessionReadGroup;
|
|
@@ -145,6 +155,103 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
145
155
|
return this.chainId ??= this.run("eth_chainId", []).then((value) => rpcQuantity(value, "chain ID"));
|
|
146
156
|
}
|
|
147
157
|
|
|
158
|
+
observeGasPrice(): Promise<unknown> {
|
|
159
|
+
this.assertOpen();
|
|
160
|
+
return this.gasPrice ??= this.run("eth_gasPrice", []);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
immutableRead(key: string, work: () => Promise<unknown>): Promise<unknown> {
|
|
164
|
+
this.assertOpen();
|
|
165
|
+
let pending = this.immutableReads.get(key);
|
|
166
|
+
if (pending === undefined) {
|
|
167
|
+
pending = work();
|
|
168
|
+
this.immutableReads.set(key, pending);
|
|
169
|
+
const owned = pending;
|
|
170
|
+
void pending.catch(() => { if (this.immutableReads.get(key) === owned) this.immutableReads.delete(key); });
|
|
171
|
+
}
|
|
172
|
+
return pending.then((value) => { this.assertOpen(); return value; });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
deferCanonicalCheck(block: Pick<LifecycleBlock, "number" | "hash">, chainId?: bigint): void {
|
|
176
|
+
this.assertOpen();
|
|
177
|
+
const key = block.number.toString();
|
|
178
|
+
const existing = this.canonicalPins.get(key);
|
|
179
|
+
if (existing !== undefined && (existing.block.hash.toLowerCase() !== block.hash.toLowerCase() ||
|
|
180
|
+
existing.chainId !== undefined && chainId !== undefined && existing.chainId !== chainId)) {
|
|
181
|
+
this.invalidateSession();
|
|
182
|
+
throw new LifecyclePlanningError("STATE_REORGED", "One lifecycle invocation observed incompatible canonical pins");
|
|
183
|
+
}
|
|
184
|
+
this.canonicalPins.set(key, { block: { ...block }, chainId: chainId ?? existing?.chainId });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async finalize(): Promise<void> {
|
|
188
|
+
this.assertOpen();
|
|
189
|
+
const pins = [...this.canonicalPins.values()];
|
|
190
|
+
if (pins.length === 0) return;
|
|
191
|
+
const [chainId, current] = await Promise.all([
|
|
192
|
+
pins.some((pin) => pin.chainId !== undefined) ? this.run("eth_chainId", []).then((value) => rpcQuantity(value, "chain ID")) : undefined,
|
|
193
|
+
Promise.all(pins.map((pin) => readLifecycleBlock(this, toHex(pin.block.number)))),
|
|
194
|
+
]);
|
|
195
|
+
for (let index = 0; index < pins.length; index += 1) {
|
|
196
|
+
const pin = pins[index]!;
|
|
197
|
+
if (pin.chainId !== undefined && chainId !== pin.chainId) {
|
|
198
|
+
this.invalidateSession();
|
|
199
|
+
throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during the pinned operation");
|
|
200
|
+
}
|
|
201
|
+
if (current[index]!.hash.toLowerCase() !== pin.block.hash.toLowerCase()) {
|
|
202
|
+
this.invalidateSession();
|
|
203
|
+
throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch state changed during the operation; read canonical progress again");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const check of this.finalChecks) check();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
independentRead(params: readonly unknown[], block: LifecycleBlock, staticRead: boolean): Promise<unknown> {
|
|
210
|
+
return this.read("eth_call", params, block, staticRead, () => new Promise((resolve, reject) => {
|
|
211
|
+
const key = `${block.number}:${block.hash.toLowerCase()}`;
|
|
212
|
+
let group = this.independentReads.get(key);
|
|
213
|
+
if (group === undefined) {
|
|
214
|
+
group = { block, reads: [] };
|
|
215
|
+
this.independentReads.set(key, group);
|
|
216
|
+
queueMicrotask(() => {
|
|
217
|
+
this.independentReads.delete(key);
|
|
218
|
+
void this.flushIndependentReads(group!);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
group.reads.push({ params, resolve, reject });
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private async flushIndependentReads(group: { block: LifecycleBlock; reads: IndependentRead[] }): Promise<void> {
|
|
226
|
+
const { block, reads } = group;
|
|
227
|
+
try {
|
|
228
|
+
// Two cold reads cannot beat two direct calls after code attestation.
|
|
229
|
+
if (reads.length < 3 || await this.observeChainId() !== 4663n) {
|
|
230
|
+
await Promise.all(reads.map(async (read) => {
|
|
231
|
+
try { read.resolve(await this.run("eth_call", read.params)); }
|
|
232
|
+
catch (failure) { read.reject(failure); }
|
|
233
|
+
}));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const to = robinhoodMainnet.contracts.multicall3.address;
|
|
237
|
+
const code = rpcHex(await this.read("eth_getCode", [to, toHex(block.number)], block, true), "Multicall3 code");
|
|
238
|
+
if (keccak256(code) !== multicall3CodeHash) throw new LifecyclePlanningError("MULTICALL_CODE_MISMATCH", "Pinned Multicall3 runtime differs from the reviewed chain4663 deployment");
|
|
239
|
+
const calls = reads.map((read) => {
|
|
240
|
+
const request = read.params[0] as { to: Address; data: Hex };
|
|
241
|
+
return { target: request.to, allowFailure: true, callData: request.data };
|
|
242
|
+
});
|
|
243
|
+
const data = encodeFunctionData({ abi: multicall3Abi, functionName: "aggregate3", args: [calls] });
|
|
244
|
+
const raw = rpcHex(await this.run("eth_call", [{ to, data }, toHex(block.number)]), "Multicall3 result");
|
|
245
|
+
const results = decodeFunctionResult({ abi: multicall3Abi, functionName: "aggregate3", data: raw });
|
|
246
|
+
if (results.length !== reads.length) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Multicall3 omitted independent pinned results");
|
|
247
|
+
for (let index = 0; index < reads.length; index += 1) {
|
|
248
|
+
const result = results[index]!;
|
|
249
|
+
if (result.success) reads[index]!.resolve(result.returnData);
|
|
250
|
+
else reads[index]!.reject(new LifecyclePlanningError("MULTICALL_READ_FAILED", "An independent pinned getter reverted"));
|
|
251
|
+
}
|
|
252
|
+
} catch (failure) { for (const read of reads) read.reject(failure); }
|
|
253
|
+
}
|
|
254
|
+
|
|
148
255
|
observeLimits(context: LifecycleLimitContext, source?: LifecycleLimitSource): Promise<LifecycleLimitObservation> {
|
|
149
256
|
this.assertOpen();
|
|
150
257
|
// A caller callback is live policy, not an immutable pinned observation.
|
|
@@ -168,7 +275,7 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
168
275
|
return observation.then((result) => { this.assertOpen(); result.assertCurrent(); return result; });
|
|
169
276
|
}
|
|
170
277
|
|
|
171
|
-
read(method: string, params: readonly unknown[], block: LifecycleBlock, staticRead = false): Promise<unknown> {
|
|
278
|
+
read(method: string, params: readonly unknown[], block: LifecycleBlock, staticRead = false, load?: () => Promise<unknown>): Promise<unknown> {
|
|
172
279
|
this.assertOpen();
|
|
173
280
|
// Source identity is this scope's original client. The complete wire call includes
|
|
174
281
|
// caller, value, gas and any other call fields; no selector-only/ABI-name aliasing.
|
|
@@ -189,7 +296,7 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
189
296
|
pending = Promise.resolve(cached);
|
|
190
297
|
} else {
|
|
191
298
|
if (staticRead && this.session !== undefined) this.sessionMisses += 1;
|
|
192
|
-
pending = this.run(method, params);
|
|
299
|
+
pending = load === undefined ? this.run(method, params) : load();
|
|
193
300
|
if (group !== undefined) pending = pending.then((value) => {
|
|
194
301
|
// Never share in-flight work or install late results after invalidation/closure.
|
|
195
302
|
// Only wire hex is retained, so public decoded DTOs cannot mutate authority.
|
|
@@ -207,6 +314,11 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
207
314
|
this.reads.clear();
|
|
208
315
|
this.chainId = undefined;
|
|
209
316
|
this.limitObservations.clear();
|
|
317
|
+
this.gasPrice = undefined;
|
|
318
|
+
this.immutableReads.clear();
|
|
319
|
+
this.canonicalPins.clear();
|
|
320
|
+
this.independentReads.clear();
|
|
321
|
+
this.finalChecks.clear();
|
|
210
322
|
const listener = this.source.onDiagnostic;
|
|
211
323
|
if (this.session !== undefined && listener !== undefined) emitLifecycleDiagnostic(listener, {
|
|
212
324
|
stage: "session-cache", phase: success ? "success" : "failure",
|
|
@@ -221,7 +333,15 @@ export async function withLifecycleReadClient<T>(source: LifecycleRpcClient, wor
|
|
|
221
333
|
const client = new InvocationLifecycleClient(source);
|
|
222
334
|
let success = false;
|
|
223
335
|
try {
|
|
224
|
-
|
|
336
|
+
let result: T;
|
|
337
|
+
try { result = await work(client); }
|
|
338
|
+
catch (failure) {
|
|
339
|
+
// Refused simulations still report a changed canonical source, not a
|
|
340
|
+
// misleading economic refusal derived from an orphaned observation.
|
|
341
|
+
await client.finalize();
|
|
342
|
+
throw failure;
|
|
343
|
+
}
|
|
344
|
+
await client.finalize();
|
|
225
345
|
success = true;
|
|
226
346
|
return result;
|
|
227
347
|
} finally { client.close(success); }
|
|
@@ -285,6 +405,28 @@ export function readLifecycleChainId(client: LifecycleRpcClient): Promise<bigint
|
|
|
285
405
|
? client.observeChainId()
|
|
286
406
|
: lifecycleRpc(client, "eth_chainId").then((value) => rpcQuantity(value, "chain ID"));
|
|
287
407
|
}
|
|
408
|
+
|
|
409
|
+
/** A single operation-local recommendation, never a session or cross-head quote. */
|
|
410
|
+
export function readLifecycleGasPrice(client: LifecycleRpcClient): Promise<unknown> {
|
|
411
|
+
return client instanceof InvocationLifecycleClient ? client.observeGasPrice() : lifecycleRpc(client, "eth_gasPrice");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Only the reviewed immutable factory identity may use an invocation-wide key. */
|
|
415
|
+
export function readLifecycleImmutable<T>(client: LifecycleRpcClient, key: string, work: () => Promise<T>): Promise<T> {
|
|
416
|
+
return (client instanceof InvocationLifecycleClient ? client.immutableRead(key, work) : work()) as Promise<T>;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Recheck live policy after the owner's final asynchronous canonical guards. */
|
|
420
|
+
export function registerLifecycleFinalCheck(client: LifecycleRpcClient, check: () => void): void {
|
|
421
|
+
if (client instanceof InvocationLifecycleClient) { client.assertOpen(); client.finalChecks.add(check); }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** SDK-selected caller-independent getters only; never poster quotes or execution. */
|
|
425
|
+
function lifecycleIndependentRpc(client: LifecycleRpcClient, params: readonly unknown[], block: LifecycleBlock, staticRead: boolean): Promise<unknown> {
|
|
426
|
+
return client instanceof InvocationLifecycleClient
|
|
427
|
+
? client.independentRead(params, block, staticRead)
|
|
428
|
+
: lifecyclePinnedRpc(client, "eth_call", params, block, staticRead);
|
|
429
|
+
}
|
|
288
430
|
export function rpcObject(value: unknown, label: string): Record<string, unknown> {
|
|
289
431
|
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", `${label} must be an object`);
|
|
290
432
|
// The object guard establishes the RPC record boundary; fields remain untrusted unknowns.
|
|
@@ -325,7 +467,11 @@ export async function readLifecycleBlock(client: LifecycleRpcClient, tag: Hex |
|
|
|
325
467
|
if (client instanceof InvocationLifecycleClient) client.observeBlock(block, tag === "latest");
|
|
326
468
|
return block;
|
|
327
469
|
}
|
|
328
|
-
export async function assertLifecycleBlock(client: LifecycleRpcClient, block: LifecycleBlock, expectedChainId?: bigint): Promise<void> {
|
|
470
|
+
export async function assertLifecycleBlock(client: LifecycleRpcClient, block: Pick<LifecycleBlock, "number" | "hash">, expectedChainId?: bigint): Promise<void> {
|
|
471
|
+
if (client instanceof InvocationLifecycleClient) {
|
|
472
|
+
client.deferCanonicalCheck(block, expectedChainId);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
329
475
|
const [chainId, current] = await Promise.all([
|
|
330
476
|
expectedChainId === undefined ? undefined : lifecycleRpc(client, "eth_chainId"),
|
|
331
477
|
readLifecycleBlock(client, toHex(block.number)),
|
|
@@ -339,13 +485,15 @@ export async function assertLifecycleBlock(client: LifecycleRpcClient, block: Li
|
|
|
339
485
|
throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch state changed during the operation; read canonical progress again");
|
|
340
486
|
}
|
|
341
487
|
}
|
|
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> {
|
|
488
|
+
export async function readLifecycleContract<T>(client: LifecycleRpcClient, to: Address, abi: Abi, functionName: string, args: readonly unknown[], block: LifecycleBlock, from?: Address, staticRead = false, independent = false): Promise<T> {
|
|
343
489
|
const data = encodeFunctionData({ abi, functionName, args });
|
|
344
490
|
const request = [{ to, from, data }, toHex(block.number)];
|
|
345
491
|
const getter = abi.some((item) => item.type === "function" && item.name === functionName && (item.stateMutability === "view" || item.stateMutability === "pure"));
|
|
346
|
-
const result = rpcHex(await (
|
|
347
|
-
?
|
|
348
|
-
:
|
|
492
|
+
const result = rpcHex(await (independent
|
|
493
|
+
? lifecycleIndependentRpc(client, request, block, staticRead)
|
|
494
|
+
: getter
|
|
495
|
+
? staticRead ? lifecycleStaticRpc(client, "eth_call", request, block) : lifecyclePinnedRpc(client, "eth_call", request, block)
|
|
496
|
+
: lifecycleRpc(client, "eth_call", request)), functionName);
|
|
349
497
|
// ABI decoding validates scalar widths and tuple layout; T names that exact frozen ABI result.
|
|
350
498
|
const decoded = decodeFunctionResult({ abi, functionName, data: result }) as T;
|
|
351
499
|
return decoded;
|
|
@@ -382,7 +530,7 @@ export async function readNitroPosterGas(transaction: LifecycleTransaction, cont
|
|
|
382
530
|
|
|
383
531
|
async function readNitroScalar(context: LifecycleLimitContext, to: Address, abi: Abi, functionName: string): Promise<bigint> {
|
|
384
532
|
const data = encodeFunctionData({ abi, functionName });
|
|
385
|
-
const result = rpcHex(await
|
|
533
|
+
const result = rpcHex(await lifecycleIndependentRpc(context.client, [{ to, data }, toHex(context.block.number)], context.block, true), functionName);
|
|
386
534
|
if (result.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", `${functionName} must return exactly one ABI word`);
|
|
387
535
|
return decodeFunctionResult({ abi, functionName, data: result }) as bigint;
|
|
388
536
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decodeErrorResult, decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctionResult, parseAbi, toHex, type Hex } from "viem";
|
|
2
2
|
import { fixedFeePoolHookV1Abi, launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
|
|
3
3
|
import { hashLaunchIdentity, hashLaunchPlan, LifecyclePhase, type LaunchPlanV1, type LaunchProgressV1, type LaunchReceiptV1 } from "./schema.js";
|
|
4
|
-
import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readLifecycleChainId, readNitroPosterGas, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity, type LifecycleLimitObservation } from "./rpc.js";
|
|
4
|
+
import { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readLifecycleChainId, readLifecycleGasPrice, readNitroPosterGas, registerLifecycleFinalCheck, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity, type LifecycleLimitObservation } from "./rpc.js";
|
|
5
5
|
import { LifecyclePlanningError, type ControlledLifecycleFork, type LifecycleBlock, type LifecycleLimitContext, type LifecyclePostcondition, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleSimulationStep, type LifecycleTransaction, type PlannedLaunch, type ResolvedLifecycleLimits, type SimulateLaunchPlanOptions } from "./types.js";
|
|
6
6
|
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion } from "./markets.js";
|
|
7
7
|
import { hmmV2ForwardingReserve } from "./presets.js";
|
|
@@ -121,7 +121,7 @@ async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunc
|
|
|
121
121
|
// parent gasLimit are set.
|
|
122
122
|
const blockStateCalls = transactions.map((transaction, index) => ({
|
|
123
123
|
blockOverrides: { number: toHex(block.number + BigInt(index + 1)), time: toHex(block.timestamp + BigInt(index + 1)), gasLimit: toHex(block.gasLimit) },
|
|
124
|
-
calls: [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gasLimits[index] ?? limits.executionGasCeiling), gasPrice: toHex(gasPrice) }],
|
|
124
|
+
calls: [{ from: transaction.from, to: transaction.to, data: transaction.data, value: toHex(transaction.value), gas: toHex(gasLimits[index] ?? limits.executionGasCeiling), gasPrice: toHex(gasPrice), ...(transaction.nonce === undefined ? {} : { nonce: toHex(transaction.nonce) }) }],
|
|
125
125
|
}));
|
|
126
126
|
const result = await lifecycleRpc(client, "eth_simulateV1", [{ blockStateCalls, validation, traceTransfers: false, returnFullTransactions: false }, toHex(block.number)]);
|
|
127
127
|
if (!Array.isArray(result) || result.length !== transactions.length) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Sequential simulation omitted blocks");
|
|
@@ -234,12 +234,12 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
|
|
|
234
234
|
if (transaction === undefined || gas === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing fork transaction/gas context");
|
|
235
235
|
try {
|
|
236
236
|
if (measure) {
|
|
237
|
-
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");
|
|
237
|
+
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), ...(transaction.nonce === undefined ? {} : { nonce: toHex(transaction.nonce) }) }]), "fork sequential gas estimate");
|
|
238
238
|
gasRequired = estimated;
|
|
239
239
|
const buffered = (estimated * BigInt(10000 + headroomBps) + 9999n) / 10000n;
|
|
240
240
|
if (buffered < gas) gas = buffered;
|
|
241
241
|
}
|
|
242
|
-
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");
|
|
242
|
+
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), ...(transaction.nonce === undefined ? {} : { nonce: toHex(transaction.nonce) }) }]), "fork transaction hash");
|
|
243
243
|
const receipt = await waitForkReceipt(fork.client, hash, fork.receiptTimeoutMs ?? 10_000);
|
|
244
244
|
const gasUsed = rpcQuantity(receipt.gasUsed, "fork gas used");
|
|
245
245
|
if (rpcQuantity(receipt.status, "fork transaction status") !== 1n) {
|
|
@@ -316,7 +316,7 @@ function observedFields<T extends object>(value: T): () => boolean {
|
|
|
316
316
|
|
|
317
317
|
/** Internal continuation owned by one SDK read invocation. Only its positive
|
|
318
318
|
* capability proof is reused; every candidate executes its entire economic sequence. */
|
|
319
|
-
export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true, reviewedEnvelope?: NextTransactionEnvelope): PreparedLaunchSimulation {
|
|
319
|
+
export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true, reviewedEnvelope?: NextTransactionEnvelope, selectedGasPrice?: bigint): PreparedLaunchSimulation {
|
|
320
320
|
const { client, planned, fork } = options;
|
|
321
321
|
const signal = options.signal;
|
|
322
322
|
if (signal?.aborted) throw Object.assign(new Error("Lifecycle simulation cancelled"), { name: "AbortError" });
|
|
@@ -355,7 +355,7 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
|
|
|
355
355
|
resolveLifecycleLimits(context, policy),
|
|
356
356
|
readLifecycleChainId(client),
|
|
357
357
|
lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(context.block.number)], context.block),
|
|
358
|
-
hasTransactions && reviewedEnvelope === undefined ?
|
|
358
|
+
hasTransactions && reviewedEnvelope === undefined ? selectedGasPrice ?? readLifecycleGasPrice(client) : undefined,
|
|
359
359
|
]));
|
|
360
360
|
})().then((observations) => ({ observations }), (failure: unknown) => ({ failure }));
|
|
361
361
|
// Refused inputs may discard this read-only preparation before it settles.
|
|
@@ -419,8 +419,9 @@ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, bloc
|
|
|
419
419
|
await capabilityPending;
|
|
420
420
|
assertCurrent();
|
|
421
421
|
};
|
|
422
|
-
const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope);
|
|
422
|
+
const simulation = await simulateLaunchTransactionsWithContext(options, transactions, context, result.observations, proveBackend, assertCurrent, diagnostic, nextEnvelope, selectedGasPrice);
|
|
423
423
|
assertCurrent();
|
|
424
|
+
registerLifecycleFinalCheck(client, result.observations[0].assertCurrent);
|
|
424
425
|
return simulation;
|
|
425
426
|
} catch (failure) {
|
|
426
427
|
closed = true;
|
|
@@ -455,6 +456,7 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
455
456
|
assertCurrent: () => void,
|
|
456
457
|
diagnostic: boolean,
|
|
457
458
|
nextEnvelope?: NextTransactionEnvelope,
|
|
459
|
+
selectedGasPrice?: bigint,
|
|
458
460
|
): Promise<LifecycleSimulation> {
|
|
459
461
|
const { client, planned } = options;
|
|
460
462
|
const sourceClient = lifecycleSourceClient(client);
|
|
@@ -504,11 +506,11 @@ async function simulateLaunchTransactionsWithContext(
|
|
|
504
506
|
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], failureCategory: "capacity", capacityConstraint: exceedsCompute ? "compute" : "simulation-gas", reason };
|
|
505
507
|
}
|
|
506
508
|
}
|
|
507
|
-
const recommendedGasPrice = nextEnvelope === undefined ? rpcQuantity(gasPriceValue, "gas price") : nextEnvelope.gasPrice;
|
|
509
|
+
const recommendedGasPrice = nextEnvelope === undefined ? typeof gasPriceValue === "bigint" ? gasPriceValue : rpcQuantity(gasPriceValue, "gas price") : nextEnvelope.gasPrice;
|
|
508
510
|
// New recommendations cover lagging quotes; reviewed fees are replayed unchanged
|
|
509
511
|
// under natural child fees, never refreshed or silently increased.
|
|
510
512
|
const baseFeeEnvelope = (block.baseFeePerGas ?? 0n) * 2n;
|
|
511
|
-
const gasPrice = nextEnvelope?.gasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
|
|
513
|
+
const gasPrice = nextEnvelope?.gasPrice ?? selectedGasPrice ?? (recommendedGasPrice > baseFeeEnvelope ? recommendedGasPrice : baseFeeEnvelope);
|
|
512
514
|
if (gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
|
|
513
515
|
const buffer = (gas: bigint) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
|
|
514
516
|
const posterGas: bigint[] = [];
|
package/src/lifecycle/types.ts
CHANGED
|
@@ -76,7 +76,7 @@ export type LifecyclePostcondition =
|
|
|
76
76
|
export type LifecycleTransaction = {
|
|
77
77
|
id: string; kind: LifecycleTransactionKind; chainId: number; from: Address; to: Address; data: Hex; value: bigint;
|
|
78
78
|
dependencies: readonly string[]; postconditions: readonly LifecyclePostcondition[];
|
|
79
|
-
marketStart?: number; marketCount?: number; gas?: bigint; gasPrice?: bigint;
|
|
79
|
+
marketStart?: number; marketCount?: number; gas?: bigint; gasPrice?: bigint; nonce?: bigint;
|
|
80
80
|
/** Exact encoded byte count; populated on SDK-built commands, including approvals. */
|
|
81
81
|
calldataBytes?: number;
|
|
82
82
|
/** Exact current-state execution admission, not a wallet batch or future guarantee. */
|
|
@@ -164,6 +164,8 @@ export type PlannedLaunch = {
|
|
|
164
164
|
account: Address; chainId: bigint; mode: LaunchExecutionMode; confirmations: number;
|
|
165
165
|
/** Opt-in proxy routing; the committed creator remains the proxy, account is its principal. */
|
|
166
166
|
ufg?: { config: UfgLaunchConfig; configHash: Hash };
|
|
167
|
+
/** With admitted exact proof, transactions[0] is the immediate reviewed envelope,
|
|
168
|
+
* including nonce, gas and gasPrice. Deferred commands have no execution admission. */
|
|
167
169
|
transactions: readonly LifecycleTransaction[]; prerequisites: readonly LifecycleFundingPrerequisite[];
|
|
168
170
|
/** Sum of exact calldata bytes across the returned remaining commands, including approvals. */
|
|
169
171
|
totalCalldataBytes: number;
|
|
@@ -186,11 +188,17 @@ export type PlanLaunchOptions = {
|
|
|
186
188
|
/** Unstarted only: waive the funded boundary tail; all other topology checks remain required. */
|
|
187
189
|
allowMissingPermanentTail?: boolean;
|
|
188
190
|
};
|
|
191
|
+
/** Explicit atomic final gas. Omitted price freezes this invocation's recommendation;
|
|
192
|
+
* a supplied price is replayed exactly, never refreshed or silently increased. */
|
|
193
|
+
export type LifecycleFinalEnvelope = Readonly<{ gas: bigint; gasPrice?: bigint }>;
|
|
189
194
|
export type PrepareAndPlanLifecycleLaunchOptions = PlanLaunchOptions & {
|
|
190
195
|
onProgress?: (progress: PoolBoundLifecyclePreparationProgress) => void;
|
|
191
196
|
/** Protect every ordered buy from its actual diagnostic receipt output, rounded down.
|
|
192
197
|
* Omitted preserves the caller's committed minima. Only an unstarted launch may be protected. */
|
|
193
198
|
buySlippageBps?: number;
|
|
199
|
+
/** Atomic only, with no preceding approvals. Final calldata receives one exact
|
|
200
|
+
* validated replay, not a new gas recommendation or a larger rescue envelope. */
|
|
201
|
+
fixedFinalEnvelope?: LifecycleFinalEnvelope;
|
|
194
202
|
};
|
|
195
203
|
export type BuildLaunchTransactionsOptions = {
|
|
196
204
|
plan: LaunchPlanV1; mode: LaunchExecutionMode;
|