@black-market/sdk 0.6.1 → 0.6.3

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
 
@@ -123,10 +123,14 @@ async function validatePendingInputs(client: LifecycleRpcClient, planned: Planne
123
123
  for (const policy of plan.feeAssets) if (policy.asset.toLowerCase() !== planned.predictedToken.toLowerCase()) assets.add(policy.asset);
124
124
  for (const market of plan.markets) assets.add(market.quoteAsset);
125
125
  for (const funding of plan.funding) { assets.add(funding.asset); if (funding.kind !== LifecycleFundingKind.NativeWrap && funding.inputAsset !== zeroAddress) assets.add(funding.inputAsset); }
126
- const [profiles, registry, assetCodes] = await Promise.all([
126
+ const registryRead = readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "registry", [], block);
127
+ const [profiles, registry, assetCodes, allowedInputs, escrowBalances] = await Promise.all([
127
128
  readLifecycleProfiles({ client, orchestrator: plan.orchestrator, profileIds: [...new Set(plan.markets.map((market) => market.profileId))] }, block),
128
- readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "registry", [], block),
129
+ registryRead,
129
130
  Promise.all([...assets].map(async (asset) => ({ asset, code: rpcHex(await lifecyclePinnedRpc(client, "eth_getCode", [asset, toHex(block.number)], block), "asset code") }))),
131
+ registryRead.then((registry) => Promise.all(plan.funding.map((funding) => funding.kind === LifecycleFundingKind.Swap && funding.inputAsset !== zeroAddress
132
+ ? readLifecycleContract<boolean>(client, registry, lifecycleRegistryAbi, "fundingInputAllowed", [funding.inputAsset], block) : true))),
133
+ progress.canonical.phase === LifecyclePhase.None ? [] : Promise.all(plan.funding.map((funding) => readLifecycleContract<bigint>(client, plan.orchestrator, launchLifecycleAbi, "escrowBalance", [planned.launchId, funding.asset], block))),
130
134
  ]);
131
135
  const reasons = profiles.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Implementation/profile is unavailable");
132
136
  if (block.timestamp > plan.deadline) reasons.push("The immutable launch deadline has expired; cancellation remains available");
@@ -134,10 +138,7 @@ async function validatePendingInputs(client: LifecycleRpcClient, planned: Planne
134
138
  if (!plan.feeAssets.some((policy) => policy.asset.toLowerCase() === planned.predictedToken.toLowerCase())) reasons.push("The launch token must have a committed fee policy");
135
139
  for (const { asset, code } of assetCodes) if (code === "0x") reasons.push(`Asset ${asset} has no contract code`);
136
140
  const required = LIFECYCLE_REQUIRED_CAPABILITIES | (plan.token.kind === LifecycleTokenKind.ERC404 ? LIFECYCLE_ERC404_CAPABILITY : 0n);
137
- const [allowedInputs, markets, escrowBalances] = await Promise.all([
138
- Promise.all(plan.funding.map((funding) => funding.kind === LifecycleFundingKind.Swap && funding.inputAsset !== zeroAddress
139
- ? readLifecycleContract<boolean>(client, registry, lifecycleRegistryAbi, "fundingInputAllowed", [funding.inputAsset], block) : true)),
140
- Promise.all(plan.markets.map(async (market, index) => {
141
+ const markets = await Promise.all(plan.markets.map(async (market, index) => {
141
142
  const profile = profiles.find((item) => item.id.toLowerCase() === market.profileId.toLowerCase());
142
143
  let positions = 0;
143
144
  let deployment: LifecyclePoolBoundHookDeployment | undefined;
@@ -152,23 +153,24 @@ async function validatePendingInputs(client: LifecycleRpcClient, planned: Planne
152
153
  if (config === undefined) throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${profile.registration.configSchema}`);
153
154
  if ("developerFeeBps" in config) {
154
155
  validateV4LifecycleMarket({ market, config, profile, token: planned.predictedToken });
155
- if (config.version === 4) await validateV4LifecycleOracle({ client, envelope: profile.envelope!, oracleConfigId: config.oracleConfigId, block });
156
156
  }
157
157
  if (config.positions.length === 0 || config.positions.length > 32) throw new LifecyclePlanningError("INVALID_POSITION_COUNT", "A launch commits at most 32 positive positions across every market/venue");
158
158
  positions = config.positions.length;
159
159
  const rewards = progress.canonical.rewards === zeroAddress && plan.token.rewardMode === LifecycleRewardMode.Dividends ? planned.predictedToken : progress.canonical.rewards;
160
160
  if ("treasury" in config && rewards !== zeroAddress && config.treasury.toLowerCase() === rewards.toLowerCase() && config.hookFeePips !== 0 && config.protocolFeeDenominator !== 0) throw new LifecyclePlanningError("INVALID_FEE_POLICY", "Hook protocol treasury cannot be the launch rewards contract");
161
161
  const capabilities = required | (positions > 1 ? LIFECYCLE_MULTI_POSITION_CAPABILITY : 0n);
162
- const [adapter, boundDeployment] = await Promise.all([
162
+ const [adapter, boundDeployment, resolvedIdentity] = await Promise.all([
163
163
  readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "requireEligible", [market.adapterId, market.profileId, market.configVersion, capabilities], block),
164
164
  profile.topology.hookTopology === 2 ? readPoolBoundHookDeployment({ client, plan, marketIndex: index }, block) : undefined,
165
+ readLifecycleContract<MarketIdentityV1>(client, profile.adapter.implementation, lifecycleAdapterAbi, "resolve", [planned.launchId, planned.predictedToken, market], block),
166
+ "developerFeeBps" in config && config.version === 4 ? validateV4LifecycleOracle({ client, envelope: profile.envelope!, oracleConfigId: config.oracleConfigId, block }) : undefined,
165
167
  ]);
166
168
  if (adapter.toLowerCase() !== profile.adapter.implementation.toLowerCase()) throw new LifecyclePlanningError("MARKET_IDENTITY", "Eligible adapter differs from the certified profile implementation");
167
169
  if (boundDeployment !== undefined) {
168
170
  if (!hasLifecycleV4HookPermissions(boundDeployment.predictedHook)) throw new LifecyclePlanningError("INVALID_HOOK_BITS", "Finalize the pool-bound salt with the exact lifecycle hook permission bits before planning wallet execution");
169
171
  deployment = { marketIndex: index, ...boundDeployment };
170
172
  }
171
- identity = await readLifecycleContract<MarketIdentityV1>(client, adapter, lifecycleAdapterAbi, "resolve", [planned.launchId, planned.predictedToken, market], block);
173
+ identity = resolvedIdentity;
172
174
  validateLifecycleMarketIdentity(identity, plan, planned.predictedToken, index);
173
175
  const expectedHook = deployment?.predictedHook ?? profile.registration.hook;
174
176
  if (identity.factory.toLowerCase() !== profile.registration.factory.toLowerCase() || identity.hook.toLowerCase() !== expectedHook.toLowerCase() || (identity.venue === LifecycleVenue.UniswapV4 && (identity.manager.toLowerCase() !== profile.registration.venue.toLowerCase() || profile.topology.hookTopology === 0)) || (identity.venue === LifecycleVenue.Abyss && (identity.factory.toLowerCase() !== profile.registration.venue.toLowerCase() || profile.topology.hookTopology !== 0))) throw new LifecyclePlanningError("MARKET_IDENTITY", "Canonical venue dependencies differ from exact immutable profile/topology approval");
@@ -181,9 +183,7 @@ async function validatePendingInputs(client: LifecycleRpcClient, planned: Planne
181
183
  }
182
184
  } catch (failure) { reason = failure instanceof Error ? failure.message : String(failure); }
183
185
  return { positions, deployment, identity, reason };
184
- })),
185
- progress.canonical.phase === LifecyclePhase.None ? [] : Promise.all(plan.funding.map((funding) => readLifecycleContract<bigint>(client, plan.orchestrator, launchLifecycleAbi, "escrowBalance", [planned.launchId, funding.asset], block))),
186
- ]);
186
+ }));
187
187
  for (const [index, funding] of plan.funding.entries()) {
188
188
  if (funding.asset.toLowerCase() === planned.predictedToken.toLowerCase() || (funding.kind === LifecycleFundingKind.Swap && funding.inputAsset.toLowerCase() === planned.predictedToken.toLowerCase())) reasons.push("Launch-token inventory cannot be an external funding output or conversion input");
189
189
  if (!allowedInputs[index]) reasons.push(`Conversion input ${funding.inputAsset} is not currently admitted`);
@@ -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) {
@@ -1,8 +1,8 @@
1
1
  import { concatHex, decodeFunctionData, encodeAbiParameters, encodeFunctionData, keccak256, stringToHex, toHex, zeroAddress, zeroHash, type Address, type Hash, type Hex } from "viem";
2
- import { abyssLifecycleAdapterAbi, launchLifecycleAbi, lifecycleAdapterAbi, lifecycleDirectoryAbi, lifecycleErc20Abi, lifecycleOracleFactoryAbi, lifecycleRegistryAbi, poolFeeCollectorFactoryV1Abi, lifecycleV4HookAbi, lifecycleV4LockerAbi, poolHookDeployerV1Abi, sharedHookDeployerV1Abi, fixedFeePoolHookV1Abi, poolMarketAdapterV1Abi, sharedMarketAdapterV1Abi } from "./abi.js";
2
+ import { 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)[])"));
@@ -29,24 +29,16 @@ async function certifyLifecycleProfile(client: LifecycleRpcClient, orchestrator:
29
29
  if (!shared && !bound && !abyss) throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${registration.configSchema}`);
30
30
  const version = shared ? 4 : bound ? 5 : 1;
31
31
  if (adapter.configVersion !== version || topology.configVersion !== version || topology.hookTopology !== (shared ? 1 : bound ? 2 : 0)) throw new LifecyclePlanningError("UNCERTIFIED_TOPOLOGY", "Registry topology/version differs from the supported config schema");
32
- const [implementationCode, authority, digest] = await Promise.all([
33
- readDependencyCode(client, adapter.implementation, block),
34
- readLifecycleContract<Address>(client, adapter.implementation, lifecycleAdapterAbi, "core", [], block),
35
- readLifecycleContract<Hex>(client, adapter.implementation, lifecycleAdapterAbi, "dependencyDigest", [], block),
36
- ]);
37
- if (keccak256(implementationCode).toLowerCase() !== adapter.codeHash.toLowerCase()) throw new LifecyclePlanningError("PROFILE_CODE_HASH", "Adapter code hash differs from immutable registry approval");
38
- if (authority.toLowerCase() !== orchestrator.toLowerCase() || digest.toLowerCase() !== registration.dependencyDigest.toLowerCase()) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Adapter authority or dependency digest differs from immutable registry approval");
39
32
  if (abyss) {
40
33
  if (topology.hookDeployer !== zeroAddress || topology.hookCreationCodeHash !== zeroHash) throw new LifecyclePlanningError("UNCERTIFIED_TOPOLOGY", "Abyss does not admit a V4 hook deployment graph");
41
- const [factory, schema, actualVersion] = await Promise.all([
42
- readLifecycleContract<Address>(client, adapter.implementation, abyssLifecycleAdapterAbi, "factory", [], block),
43
- readLifecycleContract<Hex>(client, adapter.implementation, abyssLifecycleAdapterAbi, "CONFIG_SCHEMA", [], block),
44
- readLifecycleContract<number>(client, adapter.implementation, abyssLifecycleAdapterAbi, "CONFIG_VERSION", [], block),
45
- ]);
34
+ // certifyAbyss fixes schema/version/factory at registration; requireEligible
35
+ // rechecks that approved runtime and the live graph digest at this block.
36
+ const factory = registration.factory;
46
37
  const domain = keccak256(stringToHex("BLACK_MARKET_ABYSS_CANONICAL_PROFILE_V1"));
47
38
  const known = [0, 1, 2, 3].some((variant) => keccak256(encodeAbiParameters([{ type: "bytes32" }, { type: "uint256" }, { type: "address" }, { type: "uint8" }], [domain, chainId, factory, variant])).toLowerCase() === id.toLowerCase());
48
- if (!known || schema.toLowerCase() !== ABYSS_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() || actualVersion !== 1 || registration.factory.toLowerCase() !== factory.toLowerCase() || registration.venue.toLowerCase() !== factory.toLowerCase() || registration.hook !== zeroAddress) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Abyss profile is not the exact canonical factory/variant/schema binding");
49
- await readDependencyCode(client, factory, block);
39
+ if (!known || registration.venue.toLowerCase() !== factory.toLowerCase() || registration.hook !== zeroAddress) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Abyss profile is not the exact canonical factory/variant/schema binding");
40
+ const eligible = await readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "requireEligible", [registration.adapterId, id, version, LIFECYCLE_REQUIRED_CAPABILITIES], block);
41
+ if (eligible.toLowerCase() !== adapter.implementation.toLowerCase()) throw new LifecyclePlanningError("INELIGIBLE_PROFILE", "Registry refuses this profile for pending source admission");
50
42
  return { topology };
51
43
  }
52
44
  if (envelope === undefined || envelope.topology !== topology.hookTopology || envelope.configVersion !== version || envelope.economicVersion !== 3 ||
@@ -64,14 +56,6 @@ async function certifyLifecycleProfile(client: LifecycleRpcClient, orchestrator:
64
56
  b.maximumOracleCardinality < 2 || b.maximumOracleCardinality > 4096 || b.feeModeFlags === 0 || (b.feeModeFlags & ~3) !== 0 ||
65
57
  envelope.protocolTreasury === zeroAddress || (envelope.protocolFeeDenominator !== 0 && (envelope.protocolFeeDenominator < 4 || envelope.protocolFeeDenominator > 10))) throw new LifecyclePlanningError("PROFILE_BOUNDS", "Registry envelope has unsupported executable bounds");
66
58
  const abi = bound ? poolMarketAdapterV1Abi : sharedMarketAdapterV1Abi;
67
- const [metadata, actualVersion] = await Promise.all([
68
- Promise.all((["PROFILE_ID", "CONFIG_SCHEMA", "implementationRegistry"] as const).map((getter) => readLifecycleContract<Hex>(client, adapter.implementation, abi, getter, [], block))),
69
- readLifecycleContract<number>(client, adapter.implementation, abi, "CONFIG_VERSION", [], block),
70
- ]);
71
- for (const [index, expected] of [id, registration.configSchema, registry].entries()) {
72
- if (metadata[index]?.toLowerCase() !== expected.toLowerCase()) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Reviewed adapter metadata differs from registry certification");
73
- }
74
- if (actualVersion !== version) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Reviewed adapter config version differs from certification");
75
59
  const g = envelope.graph;
76
60
  if (registration.factory !== zeroAddress || registration.venue.toLowerCase() !== g.manager.toLowerCase() || registration.hook.toLowerCase() !== g.hookRoot.toLowerCase() ||
77
61
  topology.hookDeployer.toLowerCase() !== g.hookDeployer.toLowerCase() || topology.hookCreationCodeHash.toLowerCase() !== g.hookCreationCodeHash.toLowerCase()) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Reviewed venue/topology differs from the frozen envelope graph");
@@ -79,7 +63,12 @@ async function certifyLifecycleProfile(client: LifecycleRpcClient, orchestrator:
79
63
  const codeBindings = [[orchestrator, g.coreCodeHash], [g.manager, g.managerCodeHash], [g.oracleFactory, g.oracleFactoryCodeHash],
80
64
  [g.locker, g.lockerCodeHash], [g.collectorFactory, g.collectorFactoryCodeHash], [g.collectorDeployer, g.collectorDeployerCodeHash],
81
65
  [g.hookDeployer, g.hookDeployerCodeHash]] as const;
82
- const [dependencies, dependencyCodes, collectorDeployer, launcher, lockerManager, creationCodeHash, chunk0, chunk1] = await Promise.all([
66
+ // Frozen graph addresses permit independent evidence reads. Getter results
67
+ // still have to match that graph; none of these observations is caller authority.
68
+ const [eligible, metadata, actualVersion, dependencies, dependencyCodes, collectorDeployer, launcher, lockerManager, creationCodeHash, chunk0, chunk1, chunkCodes, sharedRoot] = await Promise.all([
69
+ readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "requireEligible", [registration.adapterId, id, version, LIFECYCLE_REQUIRED_CAPABILITIES], block),
70
+ Promise.all((["PROFILE_ID", "CONFIG_SCHEMA", "implementationRegistry"] as const).map((getter) => readLifecycleContract<Hex>(client, adapter.implementation, abi, getter, [], block))),
71
+ readLifecycleContract<number>(client, adapter.implementation, abi, "CONFIG_VERSION", [], block),
83
72
  Promise.all((["poolManager", "hookRoot", "oracleFactory", "locker", "collectorFactory", "hookDeployer"] as const).map((getter) => readLifecycleContract<Address>(client, adapter.implementation, abi, getter, [], block))),
84
73
  Promise.all(codeBindings.map(([address]) => readDependencyCode(client, address, block))),
85
74
  readLifecycleContract<Address>(client, g.collectorFactory, poolFeeCollectorFactoryV1Abi, "collectorDeployer", [], block),
@@ -88,7 +77,24 @@ async function certifyLifecycleProfile(client: LifecycleRpcClient, orchestrator:
88
77
  readLifecycleContract<Hex>(client, g.hookDeployer, deployerAbi, "creationCodeHash", [], block),
89
78
  readLifecycleContract<Address>(client, g.hookDeployer, deployerAbi, "codeChunk0", [], block),
90
79
  readLifecycleContract<Address>(client, g.hookDeployer, deployerAbi, "codeChunk1", [], block),
80
+ Promise.all([
81
+ readDependencyCode(client, g.codeChunk0, block),
82
+ g.codeChunk1 === zeroAddress ? Promise.resolve<Hex>("0x") : readDependencyCode(client, g.codeChunk1, block),
83
+ ]),
84
+ shared ? Promise.all([
85
+ readDependencyCode(client, g.hookRoot, block),
86
+ readLifecycleContract<Hex>(client, g.hookDeployer, deployerAbi, "deployedCodeHash", [g.hookRoot], block),
87
+ readLifecycleContract<Address>(client, g.hookDeployer, sharedHookDeployerV1Abi, "predict", [g.manager, adapter.implementation, g.oracleFactory, g.sharedHookSalt], block),
88
+ Promise.all((["registrar", "poolManager", "oracleFactory"] as const).map((getter) => readLifecycleContract<Address>(client, g.hookRoot, lifecycleV4HookAbi, getter, [], block))),
89
+ readLifecycleContract<bigint>(client, g.hookRoot, lifecycleV4HookAbi, "REQUIRED_HOOK_FLAGS", [], block),
90
+ readLifecycleContract<bigint>(client, g.hookRoot, lifecycleV4HookAbi, "ALL_HOOK_MASK", [], block),
91
+ ]) : undefined,
91
92
  ]);
93
+ if (eligible.toLowerCase() !== adapter.implementation.toLowerCase()) throw new LifecyclePlanningError("INELIGIBLE_PROFILE", "Registry refuses this profile for pending source admission");
94
+ for (const [index, expected] of [id, registration.configSchema, registry].entries()) {
95
+ if (metadata[index]?.toLowerCase() !== expected.toLowerCase()) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Reviewed adapter metadata differs from registry certification");
96
+ }
97
+ if (actualVersion !== version) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Reviewed adapter config version differs from certification");
92
98
  for (const [index, expected] of [g.manager, g.hookRoot, g.oracleFactory, g.locker, g.collectorFactory, g.hookDeployer].entries()) {
93
99
  if (dependencies[index]?.toLowerCase() !== expected.toLowerCase()) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Live adapter dependency differs from the frozen reviewed graph");
94
100
  }
@@ -97,10 +103,7 @@ async function certifyLifecycleProfile(client: LifecycleRpcClient, orchestrator:
97
103
  }
98
104
  if (collectorDeployer.toLowerCase() !== g.collectorDeployer.toLowerCase() || launcher.toLowerCase() !== adapter.implementation.toLowerCase() ||
99
105
  lockerManager.toLowerCase() !== g.manager.toLowerCase() || g.locker.toLowerCase() === g.manager.toLowerCase() || g.locker.toLowerCase() === g.hookRoot.toLowerCase()) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Reviewed collector or locker authority differs from the frozen graph");
100
- const [chunk0Code, chunk1Code] = await Promise.all([
101
- readDependencyCode(client, chunk0, block),
102
- chunk1 === zeroAddress ? Promise.resolve<Hex>("0x") : readDependencyCode(client, chunk1, block),
103
- ]);
106
+ const [chunk0Code, chunk1Code] = chunkCodes;
104
107
  if (chunk0.toLowerCase() !== g.codeChunk0.toLowerCase() || chunk1.toLowerCase() !== g.codeChunk1.toLowerCase() || chunk0.toLowerCase() === chunk1.toLowerCase() ||
105
108
  !chunk0Code.startsWith("0x00") || chunk0Code.length <= 4 || (chunk0Code.length - 2) / 2 > 24576 ||
106
109
  (chunk1 !== zeroAddress && (!chunk1Code.startsWith("0x00") || chunk1Code.length <= 4 || (chunk1Code.length - 2) / 2 > 24576 || (chunk0Code.length - 2) / 2 !== 24576)) ||
@@ -108,16 +111,9 @@ async function certifyLifecycleProfile(client: LifecycleRpcClient, orchestrator:
108
111
  const creationCode = concatHex([`0x${chunk0Code.slice(4)}`, chunk1 === zeroAddress ? "0x" : `0x${chunk1Code.slice(4)}`]);
109
112
  if ((creationCode.length - 2) / 2 + (shared ? 96 : 576) > 49152 || creationCodeHash === zeroHash ||
110
113
  creationCodeHash.toLowerCase() !== g.hookCreationCodeHash.toLowerCase() || keccak256(creationCode).toLowerCase() !== creationCodeHash.toLowerCase() ||
111
- hashLaunchDependencies({ chainId, core: orchestrator, registry, registrar: adapter.implementation, graph: g }).toLowerCase() !== digest.toLowerCase()) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Reviewed creation artifact, constructor bound or graph digest differs from admission");
112
- if (shared) {
113
- const [rootCode, recordedHash, remotePrediction, dependencies, requiredFlags, allFlags] = await Promise.all([
114
- readDependencyCode(client, g.hookRoot, block),
115
- readLifecycleContract<Hex>(client, g.hookDeployer, deployerAbi, "deployedCodeHash", [g.hookRoot], block),
116
- readLifecycleContract<Address>(client, g.hookDeployer, sharedHookDeployerV1Abi, "predict", [g.manager, adapter.implementation, g.oracleFactory, g.sharedHookSalt], block),
117
- Promise.all((["registrar", "poolManager", "oracleFactory"] as const).map((getter) => readLifecycleContract<Address>(client, g.hookRoot, lifecycleV4HookAbi, getter, [], block))),
118
- readLifecycleContract<bigint>(client, g.hookRoot, lifecycleV4HookAbi, "REQUIRED_HOOK_FLAGS", [], block),
119
- readLifecycleContract<bigint>(client, g.hookRoot, lifecycleV4HookAbi, "ALL_HOOK_MASK", [], block),
120
- ]);
114
+ hashLaunchDependencies({ chainId, core: orchestrator, registry, registrar: adapter.implementation, graph: g }).toLowerCase() !== registration.dependencyDigest.toLowerCase()) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Reviewed creation artifact, constructor bound or graph digest differs from admission");
115
+ if (sharedRoot !== undefined) {
116
+ const [rootCode, recordedHash, remotePrediction, dependencies, requiredFlags, allFlags] = sharedRoot;
121
117
  const initCodeHash = keccak256(concatHex([creationCode, encodeAbiParameters([{ type: "address" }, { type: "address" }, { type: "address" }], [g.manager, adapter.implementation, g.oracleFactory])]));
122
118
  const prediction = predictPoolBoundHookAddress({ deployer: g.hookDeployer, initCodeHash, salt: g.sharedHookSalt });
123
119
  if (recordedHash === zeroHash || recordedHash.toLowerCase() !== g.hookRuntimeCodeHash.toLowerCase() || keccak256(rootCode).toLowerCase() !== recordedHash.toLowerCase() ||
@@ -144,7 +140,7 @@ export async function validateV4LifecycleOracle(options: {
144
140
  }
145
141
 
146
142
  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) => {
143
+ return withLifecycleReadClient(options.client, (client) => lifecycleStage(client, "profile.certification", async () => {
148
144
  const [block, chainIdValue] = await Promise.all([
149
145
  pinnedBlock ?? readLifecycleBlock(client),
150
146
  lifecycleRpc(client, "eth_chainId"),
@@ -153,33 +149,32 @@ export async function readLifecycleProfiles(options: { client: LifecycleRpcClien
153
149
  const registry = await readLifecycleContract<Address>(client, options.orchestrator, launchLifecycleAbi, "registry", [], block);
154
150
  const offset = options.offset ?? 0n; const limit = options.limit ?? 100n;
155
151
  if (offset < 0n || limit < 0n || limit > 100n) throw new LifecyclePlanningError("INVALID_PROFILE_PAGE", "Registry profile pages require unsigned offset and limit at most 100");
156
- const [authority, ids] = await Promise.all([
157
- readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "core", [], block),
158
- options.profileIds ?? readLifecycleContract<readonly Hex[]>(client, registry, lifecycleRegistryAbi, "profileIds", [offset, limit], block),
159
- ]);
160
- if (authority.toLowerCase() !== options.orchestrator.toLowerCase()) throw new LifecyclePlanningError("REGISTRY_BINDING", "Registry is not bound to the selected lifecycle orchestrator");
161
152
  let protocolMaximumDeveloperFeeBpsPromise: Promise<number> | undefined;
162
153
  const readProtocolMaximumDeveloperFeeBps = () => protocolMaximumDeveloperFeeBpsPromise ??=
163
154
  readLifecycleContract<number>(client, registry, lifecycleRegistryAbi, "protocolMaximumDeveloperFeeBps", [], block);
164
- const profiles = await Promise.all(ids.map(async (id): Promise<LifecycleProfile> => {
155
+ const readProfile = async (id: Hex): Promise<LifecycleProfile> => {
165
156
  const registration = await readLifecycleContract<ProfileRegistrationV1>(client, registry, lifecycleRegistryAbi, "profile", [id], block);
166
- const adapter = await readLifecycleContract<AdapterRegistrationV1>(client, registry, lifecycleRegistryAbi, "adapter", [registration.adapterId], block);
167
157
  const schema = registration.configSchema.toLowerCase();
168
158
  const venueKind = schema === V4_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() || schema === V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? "uniswap-v4" : schema === ABYSS_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? "abyss" : "unknown";
169
- let topology: ProfileTopologyV1 = { hookTopology: 0, configVersion: adapter.configVersion, hookDeployer: zeroAddress, hookCreationCodeHash: zeroHash };
170
- let envelope: LaunchEnvelopeV2 | undefined; let developerTerms: LifecycleDeveloperTerms | undefined;
171
- let protocolMaximumDeveloperFeeBps: number | undefined;
172
- let admitted = false; let reason: string | undefined;
173
- try {
174
- if (venueKind === "unknown") throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${registration.configSchema}`);
175
- const [recordedTopology, reviewed] = await Promise.all([
159
+ const [adapter, certification] = await Promise.all([
160
+ readLifecycleContract<AdapterRegistrationV1>(client, registry, lifecycleRegistryAbi, "adapter", [registration.adapterId], block),
161
+ venueKind === "unknown" ? undefined : Promise.all([
176
162
  readLifecycleProfileTopology(client, registry, id, block),
177
163
  venueKind === "uniswap-v4" ? Promise.all([
178
164
  readLifecycleContract<LaunchEnvelopeV2>(client, registry, lifecycleRegistryAbi, "profileEnvelope", [id], block),
179
165
  readLifecycleContract<readonly [Address, Address, number, Hex, boolean]>(client, registry, lifecycleRegistryAbi, "developerTerms", [id], block),
180
166
  readProtocolMaximumDeveloperFeeBps(),
181
167
  ]) : undefined,
182
- ]);
168
+ ]).then((values) => ({ values }), (failure: unknown) => ({ failure })),
169
+ ]);
170
+ let topology: ProfileTopologyV1 = { hookTopology: 0, configVersion: adapter.configVersion, hookDeployer: zeroAddress, hookCreationCodeHash: zeroHash };
171
+ let envelope: LaunchEnvelopeV2 | undefined; let developerTerms: LifecycleDeveloperTerms | undefined;
172
+ let protocolMaximumDeveloperFeeBps: number | undefined;
173
+ let admitted = false; let reason: string | undefined;
174
+ try {
175
+ if (certification === undefined) throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${registration.configSchema}`);
176
+ if ("failure" in certification) throw certification.failure;
177
+ const [recordedTopology, reviewed] = certification.values;
183
178
  topology = recordedTopology;
184
179
  if (reviewed !== undefined) {
185
180
  envelope = reviewed[0];
@@ -191,17 +186,21 @@ export async function readLifecycleProfiles(options: { client: LifecycleRpcClien
191
186
  maximumDeveloperFeeBps > reviewed[2]) throw new LifecyclePlanningError("PROFILE_TERMS", "Registry developer terms differ from the frozen reviewed envelope");
192
187
  }
193
188
  await certifyLifecycleProfile(client, options.orchestrator, registry, id, registration, adapter, topology, envelope, block, chainId);
194
- const eligible = await readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "requireEligible", [registration.adapterId, id, topology.configVersion, LIFECYCLE_REQUIRED_CAPABILITIES], block);
195
- if (eligible.toLowerCase() !== adapter.implementation.toLowerCase() || (developerTerms !== undefined && !developerTerms.enabled)) throw new LifecyclePlanningError("INELIGIBLE_PROFILE", "Registry refuses this profile for pending source admission");
189
+ if (developerTerms !== undefined && !developerTerms.enabled) throw new LifecyclePlanningError("INELIGIBLE_PROFILE", "Registry refuses this profile for pending source admission");
196
190
  admitted = true;
197
191
  } catch (failure) { reason = failure instanceof Error ? failure.message : String(failure); }
198
192
  return { id, registration, adapter, topology, envelope, developerTerms, protocolMaximumDeveloperFeeBps: venueKind === "uniswap-v4" ? protocolMaximumDeveloperFeeBps : undefined, admitted, reason, venueKind };
199
- }));
193
+ };
194
+ const [authority, profiles] = await Promise.all([
195
+ readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "core", [], block),
196
+ Promise.resolve(options.profileIds ?? readLifecycleContract<readonly Hex[]>(client, registry, lifecycleRegistryAbi, "profileIds", [offset, limit], block)).then((ids) => Promise.all(ids.map(readProfile))),
197
+ ]);
198
+ if (authority.toLowerCase() !== options.orchestrator.toLowerCase()) throw new LifecyclePlanningError("REGISTRY_BINDING", "Registry is not bound to the selected lifecycle orchestrator");
200
199
  // A required V4 cap read still rejects the invocation on failure; Abyss has no developer-fee terms.
201
200
  if (protocolMaximumDeveloperFeeBpsPromise !== undefined) await protocolMaximumDeveloperFeeBpsPromise;
202
201
  if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, chainId);
203
202
  return profiles;
204
- });
203
+ }));
205
204
  }
206
205
 
207
206
  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 } {
@@ -55,7 +55,8 @@ async function proveNitroBackend(client: LifecycleRpcClient, planned: PlannedLau
55
55
  throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Simulated ArbOS version/compute getters differ from the exact pinned chain context");
56
56
  }
57
57
  }
58
- await assertLifecycleBlock(client, block, planned.chainId);
58
+ // The enclosing simulation rechecks chain and canonical hash after its proofs
59
+ // (including every refusal path); a separate probe-only recheck is redundant.
59
60
  }
60
61
 
61
62
  async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunch, block: LifecycleBlock, transactions: readonly LifecycleTransaction[], gasLimits: readonly bigint[], limits: ResolvedLifecycleLimits, gasPrice: bigint, validation: boolean): Promise<LifecycleSimulationStep[]> {
@@ -238,31 +239,49 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
238
239
  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
240
  const block = pinnedBlock ?? await readLifecycleBlock(client);
240
241
  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);
242
+ const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = await lifecycleStage(client, "simulation.context", () => Promise.all([
243
+ resolveLifecycleLimits(context, options.limits === undefined ? planned.limits : options.limits),
244
+ lifecycleRpc(client, "eth_chainId"),
245
+ lifecyclePinnedRpc(client, "eth_getCode", [planned.account, toHex(block.number)], block),
246
+ transactions.length === 0 ? undefined : lifecycleRpc(client, "eth_gasPrice"),
247
+ ]));
242
248
  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");
249
+ if (rpcQuantity(chainId, "chain ID") !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
250
+ const accountCode = rpcHex(accountCodeValue, "account code");
245
251
  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
252
  for (const transaction of transactions) {
247
253
  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
254
  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
255
  }
250
256
  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");
257
+ const gasPrice = rpcQuantity(gasPriceValue, "gas price");
252
258
  if (gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
253
259
  const buffer = (gas: bigint) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
260
+ const computeCeiling = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
261
+ const capGas = transactions.map(() => computeCeiling);
262
+ let parallelMeasurement: Promise<{ steps: LifecycleSimulationStep[] } | { failure: unknown }> | undefined;
254
263
  const posterGas: bigint[] = [];
255
264
  if (limits.protocol === "nitro") {
265
+ // The isolated capability probe, poster quote and permissive cap measurement
266
+ // have no data dependency. Only an actually batching source overlaps all three.
267
+ const nitroEvidence = Promise.all([
268
+ lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, block, limits, gasPrice)),
269
+ lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
270
+ ]);
271
+ if (sourceClient.supportsReadBatching === true) {
272
+ // Handle settlement immediately so a failed measurement cannot escape while
273
+ // the independent metering evidence is still pending. It is never admission.
274
+ parallelMeasurement = lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false))
275
+ .then((steps) => ({ steps }), (failure: unknown) => ({ failure }));
276
+ }
256
277
  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))));
278
+ const [, estimates] = await nitroEvidence;
279
+ posterGas.push(...estimates);
259
280
  } catch (failure) {
260
281
  await assertLifecycleBlock(client, block, planned.chainId);
261
282
  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.` };
262
283
  }
263
284
  }
264
- const computeCeiling = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
265
- const capGas = transactions.map(() => computeCeiling);
266
285
  // The poster quote allocates gas only. The native exact replay enforces actual
267
286
  // compute hold; subtracting this approximate budget from gasUsed would not.
268
287
  const discoveryGas = transactions.map((_, index) => {
@@ -274,7 +293,14 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
274
293
  let backend: "eth_simulateV1" | "controlled-fork" = "eth_simulateV1";
275
294
  let measured: LifecycleSimulationStep[];
276
295
  // 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); }
296
+ try {
297
+ if (parallelMeasurement === undefined) measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false));
298
+ else {
299
+ const observation = await parallelMeasurement;
300
+ if ("failure" in observation) throw observation.failure;
301
+ measured = observation.steps;
302
+ }
303
+ }
278
304
  catch (failure) {
279
305
  if (options.fork === undefined || limits.protocol === "nitro") {
280
306
  await assertLifecycleBlock(client, block, planned.chainId);
@@ -301,7 +327,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
301
327
  let verified: LifecycleSimulationStep[];
302
328
  let validatedReplayError: string | undefined;
303
329
  try {
304
- if (backend === "eth_simulateV1") verified = await simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true);
330
+ if (backend === "eth_simulateV1") verified = await lifecycleStage(client, "simulation.replay", () => simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true));
305
331
  else {
306
332
  if (options.fork === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Controlled fork is missing for the exact-gas replay");
307
333
  verified = await simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice, false, limits.headroomBps);
@@ -326,7 +352,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
326
352
  try {
327
353
  const ceilingLimits = discoveryGas;
328
354
  const retried = backend === "eth_simulateV1"
329
- ? await simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true)
355
+ ? await lifecycleStage(client, "simulation.envelope", () => simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true))
330
356
  : options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
331
357
  if (retried === undefined || retried.some((step) => !step.success)) {
332
358
  await assertLifecycleBlock(client, block, planned.chainId);