@black-market/sdk 0.6.1 → 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.
@@ -3,7 +3,7 @@ import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi, lifecycleFu
3
3
  import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecycleMode, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LifecycleVenue, LIFECYCLE_ERC404_CAPABILITY, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, LIFECYCLE_MAX_ERC404_SUPPLY, LIFECYCLE_MULTI_POSITION_CAPABILITY, LIFECYCLE_REQUIRED_CAPABILITIES, marketIdentityV1Components, parseLaunchPlan, serializeLaunchPlan, type LaunchPlanV1, type MarketIdentityV1 } from "./schema.js";
4
4
  import { ABYSS_LIFECYCLE_CONFIG_SCHEMA, readLaunchProgress, readLifecycleProfiles, readPoolBoundHookDeployment, validateLifecycleMarketIdentity, validateV4LifecycleOracle } from "./progress.js";
5
5
  import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, hasLifecycleV4HookPermissions, minePoolBoundHookSalt, predictPoolBoundHookAddress, validateV4LifecycleMarket, V4_LIFECYCLE_CONFIG_SCHEMA, V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA, type V4LifecycleMarketConfig, type V4PoolBoundLifecycleMarketConfig } from "./markets.js";
6
- import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, readLifecycleBlock, readLifecycleContract, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
6
+ import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleContract, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
7
7
  import { simulateLaunchTransactions } from "./simulation.js";
8
8
  import { LifecyclePlanningError, type BuildNextTransactionOptions, type CanonicalLaunchProgress, type LifecycleBlock, type LifecycleFundingPrerequisite, type LifecyclePoolBoundHookDeployment, type LifecycleProfile, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleTransaction, type PlannedLaunch, type PlanLaunchOptions, type PoolBoundLifecyclePreparationProgress, type SimulateLaunchPlanOptions } from "./types.js";
9
9
 
@@ -324,7 +324,7 @@ export async function preparePoolBoundLifecyclePlan(options: {
324
324
  /** Exact economic commitment with explicit execution mode and current-state admission. No broadcasts. */
325
325
  export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLaunch> {
326
326
  return withLifecycleReadClient(options.client, async (client) => {
327
- const planned = await planLaunchWithReads(options, client);
327
+ const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
328
328
  if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous planning; previous reads and simulation are invalid");
329
329
  return planned;
330
330
  });
@@ -337,22 +337,26 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
337
337
  if (plan.chainId > BigInt(Number.MAX_SAFE_INTEGER) || plan.chainId <= 0n) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Chain ID must be a positive exact JavaScript integer");
338
338
  validatePlanShape(plan);
339
339
  const planHash = hashLaunchPlan(plan); const launchId = hashLaunchIdentity(plan);
340
- const [block, chainIdValue] = await Promise.all([
340
+ const [block, chainIdValue] = await lifecycleStage(client, "plan.context", () => Promise.all([
341
341
  readLifecycleBlock(client),
342
342
  lifecycleRpc(client, "eth_chainId"),
343
- ]);
343
+ ]));
344
344
  if (rpcQuantity(chainIdValue, "chain ID") !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
345
- const [remoteHash, remoteIdentity, predictedToken, { tokenFactory, tokenFactoryCodeHash }, { limits }] = await Promise.all([
345
+ const prediction = readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block);
346
+ const [remoteHash, remoteIdentity, { tokenFactory, tokenFactoryCodeHash }, { limits }, { identity, progress }] = await lifecycleStage(client, "plan.domain", () => Promise.all([
346
347
  readLifecycleContract<Hex>(client, plan.orchestrator, launchLifecycleAbi, "hashPlan", [plan], block),
347
348
  readLifecycleContract<Hex>(client, plan.orchestrator, launchLifecycleAbi, "launchIdOf", [plan], block),
348
- readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block),
349
349
  readTokenFactoryBinding(client, plan.orchestrator, block),
350
350
  resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
351
- ]);
351
+ lifecycleStage(client, "plan.progress", async () => {
352
+ const predictedToken = await prediction;
353
+ const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1 };
354
+ const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts }, block);
355
+ return { identity, progress };
356
+ }),
357
+ ]));
352
358
  if (remoteHash.toLowerCase() !== planHash.toLowerCase() || remoteIdentity.toLowerCase() !== launchId.toLowerCase()) throw new LifecyclePlanningError("ABI_DOMAIN_MISMATCH", "Selected contract does not implement the exact lifecycle V1 plan domain/schema");
353
359
  const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [] };
354
- const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1 };
355
- const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts }, block);
356
360
  let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, atomicAttempt: placeholder, preparationBatchSize: plan.markets.length, limits: options.limits };
357
361
  const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
358
362
  if (!progress.confirmationSafe || receiptsPending) {
@@ -369,10 +373,10 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
369
373
  await assertLifecycleBlock(client, block, plan.chainId);
370
374
  return { ...planned, simulation: { ...placeholder, confidence: "stateful", executionProof: "proved", protocolFit: "proved", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
371
375
  }
372
- const [prerequisites, validation] = await Promise.all([
376
+ const [prerequisites, validation] = await lifecycleStage(client, "plan.inputs", () => Promise.all([
373
377
  progress.canonical.phase === LifecyclePhase.None ? readFunding(client, plan, block) : { prerequisites: [], approvals: [], nativeValue: 0n, reasons: [] },
374
378
  validatePendingInputs(client, planned, progress, block),
375
- ]);
379
+ ]));
376
380
  const reasons = [...prerequisites.reasons, ...validation.reasons];
377
381
  planned = { ...planned, prerequisites: prerequisites.prerequisites, profiles: validation.profiles, hookDeployments: validation.hookDeployments };
378
382
  if (reasons.length > 0) {
@@ -2,7 +2,7 @@ import { concatHex, decodeFunctionData, encodeAbiParameters, encodeFunctionData,
2
2
  import { abyssLifecycleAdapterAbi, launchLifecycleAbi, lifecycleAdapterAbi, lifecycleDirectoryAbi, lifecycleErc20Abi, lifecycleOracleFactoryAbi, lifecycleRegistryAbi, poolFeeCollectorFactoryV1Abi, lifecycleV4HookAbi, lifecycleV4LockerAbi, poolHookDeployerV1Abi, sharedHookDeployerV1Abi, fixedFeePoolHookV1Abi, poolMarketAdapterV1Abi, sharedMarketAdapterV1Abi } from "./abi.js";
3
3
  import { decodePoolBoundV4LifecycleMarketConfig, encodePoolBoundHookParameters, hasLifecycleV4HookPermissions, hashPoolBoundV4MarketCommitment, poolBoundHookInitCodeHash, predictPoolBoundHookAddress, validateV4LifecycleMarket, V4_LIFECYCLE_CONFIG_SCHEMA, V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA, type PoolBoundHookParametersV1 } from "./markets.js";
4
4
  import { hashLaunchIdentity, hashLaunchPlan, hashLaunchBounds, hashLaunchDependencies, hashLifecycleProfile, launchProgressV1Components, LifecycleMode, LifecyclePhase, LifecycleVenue, LIFECYCLE_REQUIRED_CAPABILITIES, LIFECYCLE_ERC404_CAPABILITY, LIFECYCLE_MULTI_POSITION_CAPABILITY, type AdapterRegistrationV1, type LaunchPlanV1, type LaunchProgressV1, type MarketIdentityV1, type MarketLiveStateV1, type PreparedMarketV1, type ProfileRegistrationV1, type ProfileTopologyV1, type LaunchEnvelopeV2, type LifecycleDeveloperTerms } from "./schema.js";
5
- import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, readLifecycleBlock, readLifecycleContract, readLifecycleProfileTopology, rpcHex, rpcObject, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
5
+ import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleContract, readLifecycleProfileTopology, rpcHex, rpcObject, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
6
6
  import { LifecyclePlanningError, type CanonicalLaunchProgress, type LifecycleBlock, type LifecycleMarketProgress, type LifecycleProfile, type LifecycleReceiptReference, type LifecycleReceiptStatus, type LifecycleRpcClient, type PoolBoundHookDeployment, type ReadLaunchProgressOptions } from "./types.js";
7
7
 
8
8
  export const ABYSS_LIFECYCLE_CONFIG_SCHEMA = keccak256(stringToHex("(uint8,uint24,bytes32,uint160,(int24,int24,uint128,uint256)[])"));
@@ -144,7 +144,7 @@ export async function validateV4LifecycleOracle(options: {
144
144
  }
145
145
 
146
146
  export async function readLifecycleProfiles(options: { client: LifecycleRpcClient; orchestrator: Address; profileIds?: readonly Hex[]; offset?: bigint; limit?: bigint }, pinnedBlock?: LifecycleBlock): Promise<LifecycleProfile[]> {
147
- return withLifecycleReadClient(options.client, async (client) => {
147
+ return withLifecycleReadClient(options.client, (client) => lifecycleStage(client, "profile.certification", async () => {
148
148
  const [block, chainIdValue] = await Promise.all([
149
149
  pinnedBlock ?? readLifecycleBlock(client),
150
150
  lifecycleRpc(client, "eth_chainId"),
@@ -201,7 +201,7 @@ export async function readLifecycleProfiles(options: { client: LifecycleRpcClien
201
201
  if (protocolMaximumDeveloperFeeBpsPromise !== undefined) await protocolMaximumDeveloperFeeBpsPromise;
202
202
  if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, chainId);
203
203
  return profiles;
204
- });
204
+ }));
205
205
  }
206
206
 
207
207
  async function readPoolBoundHookDeploymentDetails(options: { client: LifecycleRpcClient; plan: LaunchPlanV1; marketIndex: number }, block: LifecycleBlock): Promise<{ deployment: PoolBoundHookDeployment; parameters: PoolBoundHookParametersV1 }> {
@@ -1,9 +1,14 @@
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
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
+ };
7
12
 
8
13
  /** Owns only one invocation's immutable RPC observations, never live checks or writes. */
9
14
  class InvocationLifecycleClient implements LifecycleRpcClient {
@@ -11,6 +16,7 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
11
16
  private readonly active = new Set<Promise<void>>();
12
17
  private closed = false;
13
18
  private readonly concurrency: number;
19
+ private diagnosticSequence = 0;
14
20
  constructor(readonly source: LifecycleRpcClient) {
15
21
  this.concurrency = source.supportsReadBatching === true ? lifecycleReadConcurrency : 1;
16
22
  }
@@ -28,15 +34,29 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
28
34
 
29
35
  async run(method: string, params: readonly unknown[]): Promise<unknown> {
30
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 });
31
42
  while (this.active.size >= this.concurrency) await Promise.race(this.active);
32
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 });
33
46
  const pending = this.request({ method, params });
34
47
  const settled = pending.then(
35
48
  () => { this.active.delete(settled); },
36
49
  () => { this.active.delete(settled); },
37
50
  );
38
51
  this.active.add(settled);
39
- return pending;
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
+ }
40
60
  }
41
61
 
42
62
  read(method: string, params: readonly unknown[], block: LifecycleBlock): Promise<unknown> {
@@ -45,7 +65,11 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
45
65
  // caller, value, gas and any other call fields; no selector-only/ABI-name aliasing.
46
66
  const key = JSON.stringify([block.number.toString(), block.hash.toLowerCase(), method, params]);
47
67
  const existing = this.reads.get(key);
48
- if (existing !== undefined) return existing;
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
+ }
49
73
  const pending = this.run(method, params);
50
74
  this.reads.set(key, pending);
51
75
  void pending.catch(() => { if (this.reads.get(key) === pending) this.reads.delete(key); });
@@ -70,6 +94,32 @@ export function lifecycleSourceClient(client: LifecycleRpcClient): LifecycleRpcC
70
94
  return client instanceof InvocationLifecycleClient ? client.source : client;
71
95
  }
72
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
+
73
123
  /** Only explicitly pinned fixed-state reads may participate in invocation reuse. */
74
124
  export async function lifecyclePinnedRpc(client: LifecycleRpcClient, method: "eth_call" | "eth_getCode" | "eth_getBalance", params: readonly unknown[], block: LifecycleBlock): Promise<unknown> {
75
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");
@@ -105,8 +155,11 @@ export async function readLifecycleBlock(client: LifecycleRpcClient, tag: Hex |
105
155
  };
106
156
  }
107
157
  export async function assertLifecycleBlock(client: LifecycleRpcClient, block: LifecycleBlock, expectedChainId?: bigint): Promise<void> {
108
- if (expectedChainId !== undefined && rpcQuantity(await lifecycleRpc(client, "eth_chainId"), "chain ID") !== expectedChainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during the pinned operation");
109
- const current = await readLifecycleBlock(client, toHex(block.number));
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");
110
163
  if (current.hash.toLowerCase() !== block.hash.toLowerCase()) throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch state changed during the operation; read canonical progress again");
111
164
  }
112
165
  export async function readLifecycleContract<T>(client: LifecycleRpcClient, to: Address, abi: Abi, functionName: string, args: readonly unknown[], block: LifecycleBlock, from?: Address): Promise<T> {
@@ -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, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, 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 } {
@@ -238,24 +238,32 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
238
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");
239
239
  const block = pinnedBlock ?? await readLifecycleBlock(client);
240
240
  const context = { client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId };
241
- const { limits, configuration } = await resolveLifecycleLimits(context, options.limits === undefined ? planned.limits : options.limits);
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
+ ]));
242
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 };
243
- if (rpcQuantity(await lifecycleRpc(client, "eth_chainId"), "chain ID") !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
244
- const accountCode = rpcHex(await lifecyclePinnedRpc(client, "eth_getCode", [planned.account, toHex(block.number)], block), "account code");
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");
245
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" };
246
251
  for (const transaction of transactions) {
247
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");
248
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" };
249
254
  }
250
255
  if (transactions.length === 0) { await assertLifecycleBlock(client, block, planned.chainId); return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] }; }
251
- const gasPrice = rpcQuantity(await lifecycleRpc(client, "eth_gasPrice"), "gas price");
256
+ const gasPrice = rpcQuantity(gasPriceValue, "gas price");
252
257
  if (gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
253
258
  const buffer = (gas: bigint) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
254
259
  const posterGas: bigint[] = [];
255
260
  if (limits.protocol === "nitro") {
256
261
  try {
257
- await proveNitroBackend(client, planned, block, limits, gasPrice);
258
- posterGas.push(...await Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas))));
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);
259
267
  } catch (failure) {
260
268
  await assertLifecycleBlock(client, block, planned.chainId);
261
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.` };
@@ -274,7 +282,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
274
282
  let backend: "eth_simulateV1" | "controlled-fork" = "eth_simulateV1";
275
283
  let measured: LifecycleSimulationStep[];
276
284
  // Permissive cap measurement is not proof; only the exact-gas validated replay admits a plan.
277
- 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)); }
278
286
  catch (failure) {
279
287
  if (options.fork === undefined || limits.protocol === "nitro") {
280
288
  await assertLifecycleBlock(client, block, planned.chainId);
@@ -301,7 +309,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
301
309
  let verified: LifecycleSimulationStep[];
302
310
  let validatedReplayError: string | undefined;
303
311
  try {
304
- 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));
305
313
  else {
306
314
  if (options.fork === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Controlled fork is missing for the exact-gas replay");
307
315
  verified = await simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice, false, limits.headroomBps);
@@ -326,7 +334,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
326
334
  try {
327
335
  const ceilingLimits = discoveryGas;
328
336
  const retried = backend === "eth_simulateV1"
329
- ? 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))
330
338
  : options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
331
339
  if (retried === undefined || retried.some((step) => !step.success)) {
332
340
  await assertLifecycleBlock(client, block, planned.chainId);
@@ -4,11 +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>;
10
25
  /** Opt in to bounded parallel reads only when the supplied transport batches concurrent requests. */
11
26
  supportsReadBatching?: boolean;
27
+ /** Optional local observer. Listener failures cannot change execution or admission. */
28
+ onDiagnostic?: LifecycleDiagnosticListener;
12
29
  };
13
30
  export type LifecycleBlock = { number: bigint; hash: Hash; timestamp: bigint; gasLimit: bigint; baseFeePerGas?: bigint };
14
31
  export type LifecycleLimitContext = { client: LifecycleRpcClient; account: Address; orchestrator: Address; block: LifecycleBlock; chainId: bigint };