@black-market/sdk 0.6.0 → 0.6.2
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/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +198 -133
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +204 -143
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +8 -1
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +149 -10
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +23 -14
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/types.d.ts +15 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +107 -0
- package/package.json +1 -1
- package/src/lifecycle/planner.ts +164 -109
- package/src/lifecycle/progress.ts +187 -126
- package/src/lifecycle/rpc.ts +146 -11
- package/src/lifecycle/simulation.ts +23 -13
- package/src/lifecycle/types.ts +19 -0
package/src/lifecycle/rpc.ts
CHANGED
|
@@ -1,10 +1,133 @@
|
|
|
1
1
|
import { decodeFunctionResult, encodeFunctionData, isAddress, isHex, parseAbi, toHex, type Abi, type Address, type Hex } from "viem";
|
|
2
|
-
import { LifecyclePlanningError, type LifecycleBlock, 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 LifecycleTransaction, type ResolvedLifecycleLimits } from "./types.js";
|
|
3
3
|
import { lifecycleRegistryAbi } from "./abi.js";
|
|
4
4
|
import type { ProfileTopologyV1 } from "./schema.js";
|
|
5
5
|
|
|
6
|
+
const lifecycleReadConcurrency = 8;
|
|
7
|
+
const diagnosticRpcMethods: Record<string, true> = {
|
|
8
|
+
eth_chainId: true, eth_getBlockByNumber: true, eth_getBlockByHash: true, eth_getCode: true,
|
|
9
|
+
eth_getBalance: true, eth_call: true, eth_estimateGas: true, eth_gasPrice: true,
|
|
10
|
+
eth_simulateV1: true, eth_getTransactionCount: true, eth_getTransactionReceipt: true, eth_getTransactionByHash: true,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Owns only one invocation's immutable RPC observations, never live checks or writes. */
|
|
14
|
+
class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
15
|
+
private readonly reads = new Map<string, Promise<unknown>>();
|
|
16
|
+
private readonly active = new Set<Promise<void>>();
|
|
17
|
+
private closed = false;
|
|
18
|
+
private readonly concurrency: number;
|
|
19
|
+
private diagnosticSequence = 0;
|
|
20
|
+
constructor(readonly source: LifecycleRpcClient) {
|
|
21
|
+
this.concurrency = source.supportsReadBatching === true ? lifecycleReadConcurrency : 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async request(args: { method: string; params?: readonly unknown[] }): Promise<unknown> {
|
|
25
|
+
this.assertOpen();
|
|
26
|
+
const result = await this.source.request(args);
|
|
27
|
+
this.assertOpen();
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private assertOpen(): void {
|
|
32
|
+
if (this.closed) throw new LifecyclePlanningError("READ_SCOPE_CLOSED", "Lifecycle read invocation has already settled");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async run(method: string, params: readonly unknown[]): Promise<unknown> {
|
|
36
|
+
this.assertOpen();
|
|
37
|
+
const listener = this.source.onDiagnostic;
|
|
38
|
+
const observedMethod = listener === undefined ? undefined : diagnosticRpcMethods[method] === true ? method : "other";
|
|
39
|
+
const queuedAt = listener === undefined ? 0 : performance.now();
|
|
40
|
+
const requestId = listener === undefined ? 0 : ++this.diagnosticSequence;
|
|
41
|
+
if (listener !== undefined) emitLifecycleDiagnostic(listener, { stage: "rpc", phase: "queued", requestId, method: observedMethod });
|
|
42
|
+
while (this.active.size >= this.concurrency) await Promise.race(this.active);
|
|
43
|
+
this.assertOpen();
|
|
44
|
+
const started = listener === undefined ? 0 : performance.now();
|
|
45
|
+
if (listener !== undefined) emitLifecycleDiagnostic(listener, { stage: "rpc", phase: "start", requestId, method: observedMethod, queueMs: started - queuedAt });
|
|
46
|
+
const pending = this.request({ method, params });
|
|
47
|
+
const settled = pending.then(
|
|
48
|
+
() => { this.active.delete(settled); },
|
|
49
|
+
() => { this.active.delete(settled); },
|
|
50
|
+
);
|
|
51
|
+
this.active.add(settled);
|
|
52
|
+
try {
|
|
53
|
+
const result = await pending;
|
|
54
|
+
if (listener !== undefined) emitLifecycleDiagnostic(listener, { stage: "rpc", phase: "success", requestId, method: observedMethod, durationMs: performance.now() - started });
|
|
55
|
+
return result;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (listener !== undefined) emitLifecycleDiagnostic(listener, { stage: "rpc", phase: "failure", requestId, method: observedMethod, durationMs: performance.now() - started });
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
read(method: string, params: readonly unknown[], block: LifecycleBlock): Promise<unknown> {
|
|
63
|
+
this.assertOpen();
|
|
64
|
+
// Source identity is this scope's original client. The complete wire call includes
|
|
65
|
+
// caller, value, gas and any other call fields; no selector-only/ABI-name aliasing.
|
|
66
|
+
const key = JSON.stringify([block.number.toString(), block.hash.toLowerCase(), method, params]);
|
|
67
|
+
const existing = this.reads.get(key);
|
|
68
|
+
if (existing !== undefined) {
|
|
69
|
+
const listener = this.source.onDiagnostic;
|
|
70
|
+
if (listener !== undefined) emitLifecycleDiagnostic(listener, { stage: "rpc", phase: "reuse", method: diagnosticRpcMethods[method] === true ? method : "other" });
|
|
71
|
+
return existing;
|
|
72
|
+
}
|
|
73
|
+
const pending = this.run(method, params);
|
|
74
|
+
this.reads.set(key, pending);
|
|
75
|
+
void pending.catch(() => { if (this.reads.get(key) === pending) this.reads.delete(key); });
|
|
76
|
+
return pending;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
close(): void {
|
|
80
|
+
this.closed = true;
|
|
81
|
+
this.reads.clear();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Internal callers join the same invocation; no process-wide/source-wide read cache. */
|
|
86
|
+
export async function withLifecycleReadClient<T>(source: LifecycleRpcClient, work: (client: LifecycleRpcClient) => Promise<T>): Promise<T> {
|
|
87
|
+
if (source instanceof InvocationLifecycleClient) return work(source);
|
|
88
|
+
const client = new InvocationLifecycleClient(source);
|
|
89
|
+
try { return await work(client); }
|
|
90
|
+
finally { client.close(); }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function lifecycleSourceClient(client: LifecycleRpcClient): LifecycleRpcClient {
|
|
94
|
+
return client instanceof InvocationLifecycleClient ? client.source : client;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function emitLifecycleDiagnostic(listener: LifecycleDiagnosticListener, event: LifecycleDiagnosticEvent): void {
|
|
98
|
+
try {
|
|
99
|
+
const pending = listener(event);
|
|
100
|
+
if (pending !== undefined) void pending.catch(() => {});
|
|
101
|
+
} catch { /* Observers cannot change financial behavior. */ }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** No clock, event allocation or extra promise when diagnostics are disabled. */
|
|
105
|
+
export function lifecycleStage<T>(client: LifecycleRpcClient, stage: LifecycleDiagnosticStage, work: () => Promise<T>): Promise<T> {
|
|
106
|
+
const listener = lifecycleSourceClient(client).onDiagnostic;
|
|
107
|
+
return listener === undefined ? work() : observedLifecycleStage(listener, stage, work);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function observedLifecycleStage<T>(listener: LifecycleDiagnosticListener, stage: LifecycleDiagnosticStage, work: () => Promise<T>): Promise<T> {
|
|
111
|
+
const started = performance.now();
|
|
112
|
+
emitLifecycleDiagnostic(listener, { stage, phase: "start" });
|
|
113
|
+
try {
|
|
114
|
+
const result = await work();
|
|
115
|
+
emitLifecycleDiagnostic(listener, { stage, phase: "success", durationMs: performance.now() - started });
|
|
116
|
+
return result;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
emitLifecycleDiagnostic(listener, { stage, phase: "failure", durationMs: performance.now() - started });
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Only explicitly pinned fixed-state reads may participate in invocation reuse. */
|
|
124
|
+
export async function lifecyclePinnedRpc(client: LifecycleRpcClient, method: "eth_call" | "eth_getCode" | "eth_getBalance", params: readonly unknown[], block: LifecycleBlock): Promise<unknown> {
|
|
125
|
+
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");
|
|
126
|
+
return client instanceof InvocationLifecycleClient ? client.read(method, params, block) : lifecycleRpc(client, method, params);
|
|
127
|
+
}
|
|
128
|
+
|
|
6
129
|
export async function lifecycleRpc(client: LifecycleRpcClient, method: string, params: readonly unknown[] = []): Promise<unknown> {
|
|
7
|
-
return client.request({ method, params });
|
|
130
|
+
return client instanceof InvocationLifecycleClient ? client.run(method, params) : client.request({ method, params });
|
|
8
131
|
}
|
|
9
132
|
export function rpcObject(value: unknown, label: string): Record<string, unknown> {
|
|
10
133
|
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", `${label} must be an object`);
|
|
@@ -32,13 +155,20 @@ export async function readLifecycleBlock(client: LifecycleRpcClient, tag: Hex |
|
|
|
32
155
|
};
|
|
33
156
|
}
|
|
34
157
|
export async function assertLifecycleBlock(client: LifecycleRpcClient, block: LifecycleBlock, expectedChainId?: bigint): Promise<void> {
|
|
35
|
-
|
|
36
|
-
|
|
158
|
+
const [chainId, current] = await Promise.all([
|
|
159
|
+
expectedChainId === undefined ? undefined : lifecycleRpc(client, "eth_chainId"),
|
|
160
|
+
readLifecycleBlock(client, toHex(block.number)),
|
|
161
|
+
]);
|
|
162
|
+
if (expectedChainId !== undefined && rpcQuantity(chainId, "chain ID") !== expectedChainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during the pinned operation");
|
|
37
163
|
if (current.hash.toLowerCase() !== block.hash.toLowerCase()) throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch state changed during the operation; read canonical progress again");
|
|
38
164
|
}
|
|
39
165
|
export async function readLifecycleContract<T>(client: LifecycleRpcClient, to: Address, abi: Abi, functionName: string, args: readonly unknown[], block: LifecycleBlock, from?: Address): Promise<T> {
|
|
40
166
|
const data = encodeFunctionData({ abi, functionName, args });
|
|
41
|
-
const
|
|
167
|
+
const request = [{ to, from, data }, toHex(block.number)];
|
|
168
|
+
const getter = abi.some((item) => item.type === "function" && item.name === functionName && (item.stateMutability === "view" || item.stateMutability === "pure"));
|
|
169
|
+
const result = rpcHex(await (getter && functionName !== "readLaunchProgress"
|
|
170
|
+
? lifecyclePinnedRpc(client, "eth_call", request, block)
|
|
171
|
+
: lifecycleRpc(client, "eth_call", request)), functionName);
|
|
42
172
|
// ABI decoding validates scalar widths and tuple layout; T names that exact frozen ABI result.
|
|
43
173
|
const decoded = decodeFunctionResult({ abi, functionName, data: result }) as T;
|
|
44
174
|
return decoded;
|
|
@@ -61,7 +191,7 @@ const maxUint64 = (1n << 64n) - 1n;
|
|
|
61
191
|
/** A poster quote is an envelope budget, never an execution/compute proof. */
|
|
62
192
|
export async function readNitroPosterGas(transaction: LifecycleTransaction, context: LifecycleLimitContext): Promise<{ posterGas: bigint; baseFee: bigint; l1BaseFeeEstimate: bigint }> {
|
|
63
193
|
const data = encodeFunctionData({ abi: nitroNodeInterfaceAbi, functionName: "gasEstimateL1Component", args: [transaction.to, false, transaction.data] });
|
|
64
|
-
const result = rpcHex(await
|
|
194
|
+
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");
|
|
65
195
|
if (result.length !== 194) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Nitro poster estimate must contain exactly three ABI words");
|
|
66
196
|
const [posterGas, baseFee, l1BaseFeeEstimate] = decodeFunctionResult({ abi: nitroNodeInterfaceAbi, functionName: "gasEstimateL1Component", data: result });
|
|
67
197
|
if (posterGas < 0n || posterGas > maxUint64 || baseFee <= 0n || l1BaseFeeEstimate < 0n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Nitro poster estimate has invalid gas or fee values");
|
|
@@ -70,14 +200,15 @@ export async function readNitroPosterGas(transaction: LifecycleTransaction, cont
|
|
|
70
200
|
|
|
71
201
|
async function readNitroScalar(context: LifecycleLimitContext, to: Address, abi: Abi, functionName: string): Promise<bigint> {
|
|
72
202
|
const data = encodeFunctionData({ abi, functionName });
|
|
73
|
-
const result = rpcHex(await
|
|
203
|
+
const result = rpcHex(await lifecyclePinnedRpc(context.client, "eth_call", [{ to, data }, toHex(context.block.number)], context.block), functionName);
|
|
74
204
|
if (result.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", `${functionName} must return exactly one ABI word`);
|
|
75
205
|
return decodeFunctionResult({ abi, functionName, data: result }) as bigint;
|
|
76
206
|
}
|
|
77
207
|
|
|
78
208
|
export async function resolveLifecycleLimits(context: LifecycleLimitContext, source?: LifecycleLimitSource): Promise<{ limits: ResolvedLifecycleLimits; configuration: LifecycleLimits }> {
|
|
79
209
|
let configuration: LifecycleLimits;
|
|
80
|
-
|
|
210
|
+
const policyContext = typeof source === "function" && context.client instanceof InvocationLifecycleClient ? { ...context, client: context.client.source } : context;
|
|
211
|
+
try { configuration = source === undefined ? {} : typeof source === "function" ? await source(policyContext) : source; }
|
|
81
212
|
catch (failure) { throw new LifecyclePlanningError("LIMIT_SOURCE_FAILED", `Supplied execution policy could not be resolved: ${failure instanceof Error ? failure.message : String(failure)}`); }
|
|
82
213
|
if (configuration === null || typeof configuration !== "object" || Array.isArray(configuration)) throw new LifecyclePlanningError("INVALID_LIMITS", "Supplied execution policy must be an object");
|
|
83
214
|
const headroomBps = configuration.headroomBps === undefined ? 1500 : configuration.headroomBps;
|
|
@@ -98,12 +229,16 @@ export async function resolveLifecycleLimits(context: LifecycleLimitContext, sou
|
|
|
98
229
|
let arbOSVersion: bigint | undefined; let maxTxComputeGas: bigint | undefined; let maxBlockComputeGas: bigint | undefined;
|
|
99
230
|
if (protocol === "nitro") {
|
|
100
231
|
try {
|
|
101
|
-
const rawVersion = await
|
|
232
|
+
const [rawVersion, txComputeGas, blockComputeGas] = await Promise.all([
|
|
233
|
+
readNitroScalar(context, nitroArbSys, nitroArbSysAbi, "arbOSVersion"),
|
|
234
|
+
readNitroScalar(context, nitroGasInfo, nitroGasInfoAbi, "getMaxTxGasLimit"),
|
|
235
|
+
readNitroScalar(context, nitroGasInfo, nitroGasInfoAbi, "getMaxBlockGasLimit"),
|
|
236
|
+
]);
|
|
102
237
|
// ArbSys alone adds 55 to the logical Nitro ArbOS version.
|
|
103
238
|
if (typeof rawVersion !== "bigint" || rawVersion < 105n) throw new Error("ArbOS version 50 or newer is required (ArbSys raw version >= 105)");
|
|
104
239
|
arbOSVersion = rawVersion - 55n;
|
|
105
|
-
maxTxComputeGas =
|
|
106
|
-
maxBlockComputeGas =
|
|
240
|
+
maxTxComputeGas = txComputeGas;
|
|
241
|
+
maxBlockComputeGas = blockComputeGas;
|
|
107
242
|
if ([maxTxComputeGas, maxBlockComputeGas].some((limit) => typeof limit !== "bigint" || limit <= 0n || limit > maxUint64)) throw new Error("Nitro compute getters must be positive uint64 values");
|
|
108
243
|
} catch (failure) { throw new LifecyclePlanningError("NITRO_LIMITS_UNAVAILABLE", `Pinned Nitro compute limits could not be established: ${failure instanceof Error ? failure.message : String(failure)}; header gasLimit is not a fallback`); }
|
|
109
244
|
if (configuration.estimateDataFee !== undefined) throw new LifecyclePlanningError("INVALID_LIMITS", "Nitro poster fees are already included in gas; an external data-fee estimator would double-charge them");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctionResult, toHex, type Hex } from "viem";
|
|
2
2
|
import { launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
|
|
3
3
|
import { LifecyclePhase, type LaunchProgressV1, type LaunchReceiptV1 } from "./schema.js";
|
|
4
|
-
import { assertLifecycleBlock, lifecycleRpc, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readNitroPosterGas, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity } from "./rpc.js";
|
|
4
|
+
import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readNitroPosterGas, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity } from "./rpc.js";
|
|
5
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
6
|
|
|
7
7
|
function conditionRequest(condition: LifecyclePostcondition, planned: PlannedLaunch): { to: Hex; data: Hex } {
|
|
@@ -234,27 +234,36 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
|
|
|
234
234
|
|
|
235
235
|
export async function simulateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], pinnedBlock?: LifecycleBlock): Promise<LifecycleSimulation> {
|
|
236
236
|
const { client, planned } = options;
|
|
237
|
-
|
|
237
|
+
const sourceClient = lifecycleSourceClient(client);
|
|
238
|
+
if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === sourceClient) throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
|
|
238
239
|
const block = pinnedBlock ?? await readLifecycleBlock(client);
|
|
239
240
|
const context = { client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId };
|
|
240
|
-
const { limits, configuration } = await
|
|
241
|
+
const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = await lifecycleStage(client, "simulation.context", () => Promise.all([
|
|
242
|
+
resolveLifecycleLimits(context, options.limits === undefined ? planned.limits : options.limits),
|
|
243
|
+
lifecycleRpc(client, "eth_chainId"),
|
|
244
|
+
lifecyclePinnedRpc(client, "eth_getCode", [planned.account, toHex(block.number)], block),
|
|
245
|
+
transactions.length === 0 ? undefined : lifecycleRpc(client, "eth_gasPrice"),
|
|
246
|
+
]));
|
|
241
247
|
const base = { confidence: "stateful" as const, executionProof: "failed" as const, protocolFit: "unknown" as const, transportPreflight: "not-requested" as const, blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits };
|
|
242
|
-
if (rpcQuantity(
|
|
243
|
-
const accountCode = rpcHex(
|
|
248
|
+
if (rpcQuantity(chainId, "chain ID") !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
|
|
249
|
+
const accountCode = rpcHex(accountCodeValue, "account code");
|
|
244
250
|
if (accountCode !== "0x") return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
|
|
245
251
|
for (const transaction of transactions) {
|
|
246
252
|
if (transaction.from.toLowerCase() !== planned.account.toLowerCase() || BigInt(transaction.chainId) !== planned.chainId) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Simulation transaction differs from the committed chain/account");
|
|
247
253
|
if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) return { ...base, executionProof: "unavailable", protocolFit: "failed", backend: "unavailable", admitted: false, steps: [], failedTransactionId: transaction.id, reason: "Exact calldata exceeds the supplied chain/account/RPC byte cap" };
|
|
248
254
|
}
|
|
249
255
|
if (transactions.length === 0) { await assertLifecycleBlock(client, block, planned.chainId); return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] }; }
|
|
250
|
-
const gasPrice = rpcQuantity(
|
|
256
|
+
const gasPrice = rpcQuantity(gasPriceValue, "gas price");
|
|
251
257
|
if (gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
|
|
252
258
|
const buffer = (gas: bigint) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
|
|
253
259
|
const posterGas: bigint[] = [];
|
|
254
260
|
if (limits.protocol === "nitro") {
|
|
255
261
|
try {
|
|
256
|
-
|
|
257
|
-
|
|
262
|
+
const [, estimates] = await Promise.all([
|
|
263
|
+
lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, block, limits, gasPrice)),
|
|
264
|
+
lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
|
|
265
|
+
]);
|
|
266
|
+
posterGas.push(...estimates);
|
|
258
267
|
} catch (failure) {
|
|
259
268
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
260
269
|
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Native Nitro compute/poster proof unavailable: ${failure instanceof Error ? failure.message : String(failure)}. Generic Anvil/Hardhat forks cannot prove ArbOS metering.` };
|
|
@@ -273,7 +282,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
273
282
|
let backend: "eth_simulateV1" | "controlled-fork" = "eth_simulateV1";
|
|
274
283
|
let measured: LifecycleSimulationStep[];
|
|
275
284
|
// Permissive cap measurement is not proof; only the exact-gas validated replay admits a plan.
|
|
276
|
-
try { measured = await simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false); }
|
|
285
|
+
try { measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false)); }
|
|
277
286
|
catch (failure) {
|
|
278
287
|
if (options.fork === undefined || limits.protocol === "nitro") {
|
|
279
288
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
@@ -300,7 +309,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
300
309
|
let verified: LifecycleSimulationStep[];
|
|
301
310
|
let validatedReplayError: string | undefined;
|
|
302
311
|
try {
|
|
303
|
-
if (backend === "eth_simulateV1") verified = await simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true);
|
|
312
|
+
if (backend === "eth_simulateV1") verified = await lifecycleStage(client, "simulation.replay", () => simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true));
|
|
304
313
|
else {
|
|
305
314
|
if (options.fork === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Controlled fork is missing for the exact-gas replay");
|
|
306
315
|
verified = await simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice, false, limits.headroomBps);
|
|
@@ -325,7 +334,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
325
334
|
try {
|
|
326
335
|
const ceilingLimits = discoveryGas;
|
|
327
336
|
const retried = backend === "eth_simulateV1"
|
|
328
|
-
? await simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true)
|
|
337
|
+
? await lifecycleStage(client, "simulation.envelope", () => simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true))
|
|
329
338
|
: options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
|
|
330
339
|
if (retried === undefined || retried.some((step) => !step.success)) {
|
|
331
340
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
@@ -368,13 +377,14 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
368
377
|
}
|
|
369
378
|
}
|
|
370
379
|
const estimates: LifecycleSimulationStep[] = [];
|
|
371
|
-
const balance = rpcQuantity(await
|
|
380
|
+
const balance = rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance");
|
|
372
381
|
let remainingBalance = balance;
|
|
373
382
|
let affordabilityFailure: string | undefined;
|
|
383
|
+
const feeContext = configuration.estimateDataFee === undefined || sourceClient === client ? context : { ...context, client: sourceClient };
|
|
374
384
|
for (let index = 0; index < verifiedSteps.length; index += 1) {
|
|
375
385
|
const step = verifiedSteps[index]; const transaction = transactions[index]; const gasLimit = admittedGasLimits[index];
|
|
376
386
|
if (step === undefined || transaction === undefined || gasLimit === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing verified fee context");
|
|
377
|
-
const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit },
|
|
387
|
+
const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit }, feeContext);
|
|
378
388
|
if (dataFee !== undefined && (typeof dataFee !== "bigint" || dataFee < 0n || dataFee >= 1n << 256n)) throw new LifecyclePlanningError("INVALID_DATA_FEE", "Data-fee oracle must return a nonnegative uint256 bigint amount");
|
|
379
389
|
// Nitro gas includes poster charges. Never add the quoted poster fee again.
|
|
380
390
|
const executionFee = gasLimit * gasPrice;
|
package/src/lifecycle/types.ts
CHANGED
|
@@ -4,9 +4,28 @@ import type {
|
|
|
4
4
|
MarketLiveStateV1, PreparedMarketV1, ProfileRegistrationV1, ProfileTopologyV1, LaunchEnvelopeV2, LifecycleDeveloperTerms,
|
|
5
5
|
} from "./schema.js";
|
|
6
6
|
|
|
7
|
+
/** Local opt-in timing only: never contains request arguments, addresses, results or errors. */
|
|
8
|
+
export type LifecycleDiagnosticStage =
|
|
9
|
+
| "plan" | "plan.context" | "plan.domain" | "plan.progress" | "plan.inputs" | "rpc"
|
|
10
|
+
| "profile.certification" | "simulation.context" | "simulation.nitro"
|
|
11
|
+
| "simulation.poster" | "simulation.measure" | "simulation.replay" | "simulation.envelope";
|
|
12
|
+
export type LifecycleDiagnosticEvent = {
|
|
13
|
+
stage: LifecycleDiagnosticStage;
|
|
14
|
+
phase: "queued" | "start" | "success" | "failure" | "reuse";
|
|
15
|
+
durationMs?: number;
|
|
16
|
+
queueMs?: number;
|
|
17
|
+
requestId?: number;
|
|
18
|
+
method?: string;
|
|
19
|
+
};
|
|
20
|
+
export type LifecycleDiagnosticListener = (event: LifecycleDiagnosticEvent) => void | Promise<void>;
|
|
21
|
+
|
|
7
22
|
/** viem public clients or a raw JSON-RPC client; neither selects a transport. */
|
|
8
23
|
export type LifecycleRpcClient = {
|
|
9
24
|
request(args: { method: string; params?: readonly unknown[] }): Promise<unknown>;
|
|
25
|
+
/** Opt in to bounded parallel reads only when the supplied transport batches concurrent requests. */
|
|
26
|
+
supportsReadBatching?: boolean;
|
|
27
|
+
/** Optional local observer. Listener failures cannot change execution or admission. */
|
|
28
|
+
onDiagnostic?: LifecycleDiagnosticListener;
|
|
10
29
|
};
|
|
11
30
|
export type LifecycleBlock = { number: bigint; hash: Hash; timestamp: bigint; gasLimit: bigint; baseFeePerGas?: bigint };
|
|
12
31
|
export type LifecycleLimitContext = { client: LifecycleRpcClient; account: Address; orchestrator: Address; block: LifecycleBlock; chainId: bigint };
|