@black-market/sdk 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +96 -96
  2. package/dist/lifecycle/index.d.ts +3 -1
  3. package/dist/lifecycle/index.d.ts.map +1 -1
  4. package/dist/lifecycle/index.js +3 -1
  5. package/dist/lifecycle/index.js.map +1 -1
  6. package/dist/lifecycle/lens.d.ts +409 -0
  7. package/dist/lifecycle/lens.d.ts.map +1 -0
  8. package/dist/lifecycle/lens.js +30 -0
  9. package/dist/lifecycle/lens.js.map +1 -0
  10. package/dist/lifecycle/mining.d.ts +47 -0
  11. package/dist/lifecycle/mining.d.ts.map +1 -0
  12. package/dist/lifecycle/mining.js +89 -0
  13. package/dist/lifecycle/mining.js.map +1 -0
  14. package/dist/lifecycle/planner.d.ts.map +1 -1
  15. package/dist/lifecycle/planner.js +121 -78
  16. package/dist/lifecycle/planner.js.map +1 -1
  17. package/dist/lifecycle/presets.d.ts +3 -0
  18. package/dist/lifecycle/presets.d.ts.map +1 -1
  19. package/dist/lifecycle/presets.js +1 -0
  20. package/dist/lifecycle/presets.js.map +1 -1
  21. package/dist/lifecycle/progress.d.ts +51 -1
  22. package/dist/lifecycle/progress.d.ts.map +1 -1
  23. package/dist/lifecycle/progress.js +149 -73
  24. package/dist/lifecycle/progress.js.map +1 -1
  25. package/dist/lifecycle/rpc.d.ts +14 -4
  26. package/dist/lifecycle/rpc.d.ts.map +1 -1
  27. package/dist/lifecycle/rpc.js +171 -12
  28. package/dist/lifecycle/rpc.js.map +1 -1
  29. package/dist/lifecycle/simulation.d.ts +5 -3
  30. package/dist/lifecycle/simulation.d.ts.map +1 -1
  31. package/dist/lifecycle/simulation.js +137 -115
  32. package/dist/lifecycle/simulation.js.map +1 -1
  33. package/dist/lifecycle/snapshot.d.ts +8 -0
  34. package/dist/lifecycle/snapshot.d.ts.map +1 -0
  35. package/dist/lifecycle/snapshot.js +79 -0
  36. package/dist/lifecycle/snapshot.js.map +1 -0
  37. package/dist/lifecycle/types.d.ts +27 -5
  38. package/dist/lifecycle/types.d.ts.map +1 -1
  39. package/dist/lifecycle/types.js.map +1 -1
  40. package/docs/launch-lifecycle.md +627 -1495
  41. package/docs/reference.md +365 -0
  42. package/examples/.env.example +3 -2
  43. package/examples/README.md +171 -552
  44. package/examples/launch-example-support.ts +11 -3
  45. package/examples/launch-example.ts +1 -1
  46. package/examples/launch-lifecycle-smoke.ts +1 -1
  47. package/package.json +1 -1
  48. package/src/lifecycle/index.ts +5 -1
  49. package/src/lifecycle/lens.ts +42 -0
  50. package/src/lifecycle/mining.ts +80 -0
  51. package/src/lifecycle/planner.ts +122 -73
  52. package/src/lifecycle/presets.ts +4 -0
  53. package/src/lifecycle/progress.ts +132 -59
  54. package/src/lifecycle/rpc.ts +163 -13
  55. package/src/lifecycle/simulation.ts +134 -106
  56. package/src/lifecycle/snapshot.ts +72 -0
  57. package/src/lifecycle/types.ts +24 -6
@@ -61,6 +61,9 @@ export type LifecycleDeploymentPreset = {
61
61
  readonly tokenFactory: Address;
62
62
  readonly tokenFactoryCodeHash: Hex;
63
63
  readonly fundingEscrow: Address;
64
+ /** Optional read-only acceleration contracts. An unset lens is not a deployed feature. */
65
+ readonly hookSaltMiner?: Address;
66
+ readonly launchLens?: Address;
64
67
  readonly profiles: readonly LifecycleProfilePreset[];
65
68
  readonly provenance: {
66
69
  readonly sourceCommit: string;
@@ -325,6 +328,7 @@ const knownDeployment: LifecycleDeploymentPreset = Object.freeze({
325
328
  tokenFactory: "0xd3cE64E49224a9a96075633f63761FE6BE22FE30",
326
329
  tokenFactoryCodeHash: "0x6bbbdfe93ec615b7f1e70bac5e69aa890eba5e5f248f25e7022e866b4064d541",
327
330
  fundingEscrow: "0x7bc77946CeF52A178583FCe1EEB7751983cEC703",
331
+ hookSaltMiner: "0x2f06176ba739b609b6a7334e2e6647558ba78ad5",
328
332
  profiles: Object.freeze([
329
333
  abyssPreset(KnownLifecycleProfile.Abyss0, "0x49b2ed65e2021247951193dd2c671b40867b6e064af65a2d98a70f57556412fc"),
330
334
  abyssPreset(KnownLifecycleProfile.Abyss1, "0x2f6f494f8810473b3c0ac6b03b1a1353d067ffa5c86d3cba16b04c6b61f73c05"),
@@ -2,16 +2,17 @@ import { concatHex, decodeFunctionData, encodeAbiParameters, encodeFunctionData,
2
2
  import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleDirectoryAbi, lifecycleErc20Abi, lifecycleOracleFactoryAbi, lifecycleRegistryAbi, poolFeeCollectorFactoryV1Abi, poolFeeCollectorFactoryConfigV6Abi, lifecycleV4HookAbi, lifecycleV4LockerAbi, poolHookDeployerV1Abi, poolHookDeployerConfigV6Abi, sharedHookDeployerV1Abi, fixedFeePoolHookV1Abi, fixedFeePoolHookConfigV6Abi, poolMarketAdapterV1Abi, sharedMarketAdapterV1Abi } from "./abi.js";
3
3
  import { buildPoolBoundHookParameters, decodePoolBoundV4LifecycleMarketConfig, encodePoolBoundHookParameters, hasLifecycleV4HookPermissions, isPoolBoundV4ConfigVersion, poolBoundHookInitCodeHash, predictPoolBoundHookAddress, validateV4LifecycleMarket, V4_LIFECYCLE_CONFIG_SCHEMA, V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA_V5, V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA_V6, type PoolBoundHookParameters, type PoolBoundLifecycleConfigVersion } from "./markets.js";
4
4
  import { hashLaunchIdentity, hashLaunchPlan, hashLaunchBounds, hashLaunchDependencies, hashLifecycleProfile, lifecycleTokenComponents, 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, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleChainId, readLifecycleContract, readLifecycleStaticContract, readLifecycleProfileTopology, rpcHex, rpcObject, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
5
+ import { assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleChainId, readLifecycleContract, readLifecycleImmutable, readLifecycleStaticContract, 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
  import { deriveKnownPoolBoundHookDeployment } from "./calibration.js";
8
8
  import { getKnownLifecycleDeployment, getKnownLifecycleProfile } from "./presets.js";
9
9
  import { ufgLaunchProxyV1Abi } from "./ufg-abi.js";
10
10
  import { assertUfgLaunchAccount, hashUfgLaunchConfig, UFG_LAUNCH_PROXY_ADDRESS, type UfgLaunchConfig } from "./ufg-config.js";
11
+ import { lifecycleLensSnapshot } from "./lens.js";
11
12
 
12
13
  export const ABYSS_LIFECYCLE_CONFIG_SCHEMA = keccak256(stringToHex("(uint8,uint24,bytes32,uint160,(int24,int24,uint128,uint256)[])"));
13
14
 
14
- const lifecycleTokenPredictionAbi = [{
15
+ export const lifecycleTokenPredictionAbi = [{
15
16
  type: "function", name: "predictToken", stateMutability: "view",
16
17
  inputs: [{ name: "launchId", type: "bytes32" }, { name: "config", type: "tuple", components: lifecycleTokenComponents }],
17
18
  outputs: [{ type: "address" }],
@@ -21,16 +22,24 @@ const lifecycleTokenPredictionAbi = [{
21
22
  * avoiding full-plan prediction reads after salts or buy minima change. */
22
23
  export async function readLifecycleTokenAtBlock(client: LifecycleRpcClient, plan: LaunchPlanV1, block: LifecycleBlock): Promise<Address> {
23
24
  const deployment = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator });
24
- return deployment === undefined
25
- ? readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block)
26
- : readLifecycleContract<Address>(client, deployment.tokenFactory, lifecycleTokenPredictionAbi, "predictToken", [hashLaunchIdentity(plan), plan.token], block);
25
+ if (deployment === undefined) return readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block);
26
+ const launchId = hashLaunchIdentity(plan);
27
+ const data = encodeFunctionData({ abi: lifecycleTokenPredictionAbi, functionName: "predictToken", args: [launchId, plan.token] });
28
+ const key = `${plan.chainId}:${plan.orchestrator.toLowerCase()}:${deployment.tokenFactory.toLowerCase()}:${deployment.tokenFactoryCodeHash}:${data}`;
29
+ return readLifecycleImmutable(client, key, () =>
30
+ readLifecycleContract<Address>(client, deployment.tokenFactory, lifecycleTokenPredictionAbi, "predictToken", [launchId, plan.token], block, undefined, false, true));
31
+ }
32
+
33
+ /** Internal pinned head observation. Known cores do not inspect msg.sender. */
34
+ export function readLifecycleHeadProgress(client: LifecycleRpcClient, plan: LaunchPlanV1, account: Address, block: LifecycleBlock): Promise<LaunchProgressV1> {
35
+ const known = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator }) !== undefined;
36
+ return readLifecycleContract<LaunchProgressV1>(client, plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [hashLaunchIdentity(plan)], block, account, false, known);
27
37
  }
28
38
 
29
39
  export async function predictLifecycleToken(options: { client: LifecycleRpcClient; plan: LaunchPlanV1 }): Promise<Address> {
30
40
  return withLifecycleReadClient(options.client, async (client) => {
31
41
  const block = await readLifecycleBlock(client);
32
42
  const token = await readLifecycleTokenAtBlock(client, options.plan, block);
33
- await assertLifecycleBlock(client, block, options.plan.chainId);
34
43
  return token;
35
44
  });
36
45
  }
@@ -224,10 +233,10 @@ export async function validateV4LifecycleOracle(options: {
224
233
 
225
234
  export async function readLifecycleProfiles(options: { client: LifecycleRpcClient; orchestrator: Address; profileIds?: readonly Hex[]; offset?: bigint; limit?: bigint }, pinnedBlock?: LifecycleBlock): Promise<LifecycleProfile[]> {
226
235
  return withLifecycleReadClient(options.client, (client) => lifecycleStage(client, "profile.certification", async () => {
227
- const chainIdPromise = lifecycleRpc(client, "eth_chainId").then((value) => rpcQuantity(value, "chain ID"));
236
+ const chainIdPromise = readLifecycleChainId(client);
228
237
  // Block-bound discovery does not depend on chain identity. Join both branches
229
238
  // immediately so a failed chain read cannot become detached or a profile refusal.
230
- const [chainId, { block, profiles }] = await Promise.all([
239
+ const [, { profiles }] = await Promise.all([
231
240
  chainIdPromise,
232
241
  (async () => {
233
242
  const block = pinnedBlock ?? await readLifecycleBlock(client);
@@ -293,7 +302,6 @@ export async function readLifecycleProfiles(options: { client: LifecycleRpcClien
293
302
  return { block, profiles };
294
303
  })(),
295
304
  ]);
296
- if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, chainId);
297
305
  return profiles;
298
306
  }));
299
307
  }
@@ -303,7 +311,7 @@ async function readPoolBoundHookDeploymentDetails(options: { client: LifecycleRp
303
311
  const market = plan.markets[marketIndex];
304
312
  if (market === undefined || !isPoolBoundV4ConfigVersion(market.configVersion)) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Market index must select explicit pool-bound config version 5 or 6");
305
313
  let chainId: bigint;
306
- try { chainId = rpcQuantity(await lifecycleRpc(client, "eth_chainId"), "chain ID"); }
314
+ try { chainId = await readLifecycleChainId(client); }
307
315
  finally { assertDerivedReadClientOpen(client); }
308
316
  if (hashLaunchPlan(plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while observing the hook deployment chain");
309
317
  if (chainId !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
@@ -419,26 +427,28 @@ async function readPoolBoundHookDeploymentDetailsFresh(options: { client: Lifecy
419
427
  export async function readPoolBoundHookDeployment(options: { client: LifecycleRpcClient; plan: LaunchPlanV1; marketIndex: number }, pinnedBlock?: LifecycleBlock): Promise<PoolBoundHookDeployment> {
420
428
  const commitment = hashLaunchPlan(options.plan);
421
429
  const block = pinnedBlock ?? await readLifecycleBlock(options.client);
422
- return withLifecycleReadClient(options.client, async (client) => {
430
+ const result = await withLifecycleReadClient(options.client, async (client) => {
423
431
  const { deployment } = await readPoolBoundHookDeploymentDetails({ ...options, client }, block, commitment);
424
432
  if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while reading hook deployment metadata");
425
- if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, options.plan.chainId);
426
433
  return { ...deployment };
427
434
  });
435
+ if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during the final hook deployment guard");
436
+ return result;
428
437
  }
429
438
 
430
439
  /** Optional permissionless predeployment calldata; no token deployment, pool registration or wallet broadcast. */
431
440
  export async function buildPoolBoundHookDeploymentTransaction(options: { client: LifecycleRpcClient; plan: LaunchPlanV1; marketIndex: number }): Promise<{ to: Address; data: Hex; value: bigint; deployment: PoolBoundHookDeployment }> {
432
441
  const commitment = hashLaunchPlan(options.plan);
433
442
  const block = await readLifecycleBlock(options.client);
434
- return withLifecycleReadClient(options.client, async (client) => {
443
+ const result = await withLifecycleReadClient(options.client, async (client) => {
435
444
  const { deployment, parameters, configVersion } = await readPoolBoundHookDeploymentDetails({ ...options, client }, block, commitment);
436
445
  if (!hasLifecycleV4HookPermissions(deployment.predictedHook)) throw new LifecyclePlanningError("INVALID_HOOK_BITS", "Finalize a salt with the exact lifecycle hook permissions before predeployment");
437
446
  if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while constructing hook deployment calldata");
438
- await assertLifecycleBlock(client, block, options.plan.chainId);
439
447
  const abi = configVersion === 5 ? poolHookDeployerV1Abi : poolHookDeployerConfigV6Abi;
440
448
  return { to: deployment.deployer, data: encodeFunctionData({ abi, functionName: "deploy", args: [parameters, deployment.salt] }), value: 0n, deployment: { ...deployment } };
441
449
  });
450
+ if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during the final hook transaction guard");
451
+ return result;
442
452
  }
443
453
 
444
454
  export function validateLifecycleMarketIdentity(identity: MarketIdentityV1, plan: LaunchPlanV1, token: Address, marketIndex: number): void {
@@ -455,7 +465,7 @@ export function validateLifecycleMarketIdentity(identity: MarketIdentityV1, plan
455
465
  } else if (identity.venue !== LifecycleVenue.Abyss || identity.pool === zeroAddress || identity.factory === zeroAddress) throw new LifecyclePlanningError("MARKET_IDENTITY", "Abyss canonical factory/pool identity is missing");
456
466
  }
457
467
 
458
- async function transactionMatchesLaunch(transaction: Record<string, unknown>, options: ReadLaunchProgressOptions, block: LifecycleBlock): Promise<boolean> {
468
+ async function transactionMatchesLaunch(transaction: Record<string, unknown>, options: ReadLaunchProgressOptions, readBlock: () => Promise<LifecycleBlock>): Promise<boolean> {
459
469
  const { planned } = options;
460
470
  const from = rpcHex(transaction.from, "receipt transaction account");
461
471
  if (from.toLowerCase() !== planned.account.toLowerCase()) throw new LifecyclePlanningError("RECEIPT_ACCOUNT", "Receipt transaction belongs to a different account");
@@ -484,56 +494,114 @@ async function transactionMatchesLaunch(transaction: Record<string, unknown>, op
484
494
  if (decoded.functionName !== "approve" || rpcQuantity(transaction.value ?? "0x0", "approval native value") !== 0n) return false;
485
495
  spender = decoded.args[0];
486
496
  } catch { return false; }
487
- const escrow = planned.ufg !== undefined ? UFG_LAUNCH_PROXY_ADDRESS : await readLifecycleContract<Address>(options.client, planned.plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], block);
497
+ const known = getKnownLifecycleDeployment({ chainId: planned.plan.chainId, orchestrator: planned.plan.orchestrator });
498
+ const escrow = planned.ufg !== undefined ? UFG_LAUNCH_PROXY_ADDRESS : known?.fundingEscrow ??
499
+ await readLifecycleContract<Address>(options.client, planned.plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], await readBlock());
488
500
  return spender.toLowerCase() === escrow.toLowerCase();
489
501
  }
490
502
 
491
- async function readReceipt(reference: LifecycleReceiptReference, options: ReadLaunchProgressOptions, block: LifecycleBlock): Promise<LifecycleReceiptStatus> {
503
+ type ReceiptObservation = {
504
+ pins: readonly { number: bigint; hash: Hex }[];
505
+ finish(headers: ReadonlyMap<bigint, Hex | undefined>): LifecycleReceiptStatus;
506
+ };
507
+
508
+ async function readReceipt(reference: LifecycleReceiptReference, options: ReadLaunchProgressOptions, block: LifecycleBlock, readIdentityBlock: (number: bigint) => Promise<LifecycleBlock>): Promise<ReceiptObservation> {
492
509
  const effectiveHash = reference.replacementHash ?? reference.transactionHash;
493
510
  const base = { transactionHash: reference.transactionHash, effectiveHash, replaced: reference.replacementHash !== undefined && reference.replacementHash.toLowerCase() !== reference.transactionHash.toLowerCase() };
494
- const observedNumber = reference.observedBlockNumber;
495
- if (observedNumber !== undefined && reference.observedBlockHash !== undefined) {
496
- const observed = await lifecycleRpc(options.client, "eth_getBlockByNumber", [toHex(observedNumber), false]);
497
- if (observed === null || rpcHex(rpcObject(observed, "observed receipt block").hash, "observed receipt block hash").toLowerCase() !== reference.observedBlockHash.toLowerCase()) return { ...base, status: "reorged", blockNumber: observedNumber, blockHash: reference.observedBlockHash };
498
- }
499
- const [receiptValue, transactionValue] = await Promise.all([
500
- lifecycleRpc(options.client, "eth_getTransactionReceipt", [effectiveHash]),
501
- lifecycleRpc(options.client, "eth_getTransactionByHash", [effectiveHash]),
502
- ]);
503
- if (receiptValue === null) {
504
- // An absent receipt with an unchanged canonical observed block is not a
505
- // reorg: the transaction is simply not (yet) mined. A missing/changed
506
- // canonical block above already reported the real reorg.
507
- if (reference.observedBlockHash !== undefined) return { ...base, status: "pending", blockNumber: observedNumber, blockHash: reference.observedBlockHash };
508
- return { ...base, status: "pending" };
511
+ const pins: { number: bigint; hash: Hex }[] = [];
512
+ const observedNumber = reference.observedBlockNumber, observedHash = reference.observedBlockHash;
513
+ if (observedNumber !== undefined && observedHash !== undefined) pins.push({ number: observedNumber, hash: observedHash });
514
+ const finish = (result: LifecycleReceiptStatus, failure?: { error: unknown }): ReceiptObservation => ({
515
+ pins,
516
+ finish(headers) {
517
+ for (const pin of pins) {
518
+ if (failure !== undefined && (pin.number !== observedNumber || pin.hash !== observedHash)) continue;
519
+ if (headers.get(pin.number)?.toLowerCase() !== pin.hash.toLowerCase()) return { ...base, status: "reorged", blockNumber: pin.number, blockHash: pin.hash };
520
+ }
521
+ if (failure !== undefined) throw failure.error;
522
+ return result;
523
+ },
524
+ });
525
+ try {
526
+ const [receiptValue, transactionValue] = await Promise.all([
527
+ lifecycleRpc(options.client, "eth_getTransactionReceipt", [effectiveHash]),
528
+ lifecycleRpc(options.client, "eth_getTransactionByHash", [effectiveHash]),
529
+ ]);
530
+ if (receiptValue === null) return finish({ ...base, status: "pending", blockNumber: observedNumber, blockHash: observedHash });
531
+ if (transactionValue === null) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Receipt exists without its canonical transaction");
532
+ const receipt = rpcObject(receiptValue, "receipt");
533
+ const blockNumber = rpcQuantity(receipt.blockNumber, "receipt block number");
534
+ const blockHash = rpcHex(receipt.blockHash, "receipt block hash");
535
+ pins.push({ number: blockNumber, hash: blockHash });
536
+ const matches = await transactionMatchesLaunch(rpcObject(transactionValue, "receipt transaction"), options,
537
+ () => readIdentityBlock(blockNumber));
538
+ const confirmations = block.number >= blockNumber ? block.number - blockNumber + 1n : 0n;
539
+ const required = reference.confirmations ?? 1;
540
+ if (!Number.isSafeInteger(required) || required < 1) throw new LifecyclePlanningError("INVALID_CONFIRMATIONS", "Receipt confirmations must be a positive integer");
541
+ const observation = finish({
542
+ ...base, blockNumber, blockHash, confirmations,
543
+ status: confirmations < BigInt(required) ? "unconfirmed" : !matches ? "replacement-cancelled" :
544
+ rpcQuantity(receipt.status, "receipt status") !== 1n ? "reverted" : "confirmed",
545
+ });
546
+ return {
547
+ pins,
548
+ finish(headers) {
549
+ const result = observation.finish(headers);
550
+ if (result.status !== "reorged" && !matches && !base.replaced) throw new LifecyclePlanningError("RECEIPT_IDENTITY", "Receipt calldata does not belong to the exact committed launch");
551
+ return result;
552
+ },
553
+ };
554
+ } catch (error) {
555
+ if (error instanceof Error && error.name === "AbortError") throw error;
556
+ // A changed retained observation is a reorg even if the orphan's transaction
557
+ // or identity read is now unavailable. Otherwise preserve the original error.
558
+ return finish({ ...base, status: "pending" }, { error });
509
559
  }
510
- if (transactionValue === null) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Receipt exists without its canonical transaction");
511
- const transaction = rpcObject(transactionValue, "receipt transaction");
512
- const receiptBlock = await readLifecycleBlock(options.client, toHex(rpcQuantity(rpcObject(receiptValue, "receipt").blockNumber, "receipt block number")));
513
- const matches = await transactionMatchesLaunch(transaction, options, receiptBlock);
514
- if (!matches && !base.replaced) throw new LifecyclePlanningError("RECEIPT_IDENTITY", "Receipt calldata does not belong to the exact committed launch");
515
- const receipt = rpcObject(receiptValue, "receipt");
516
- const blockNumber = rpcQuantity(receipt.blockNumber, "receipt block number");
517
- const blockHash = rpcHex(receipt.blockHash, "receipt block hash");
518
- const canonicalBlockValue = await lifecycleRpc(options.client, "eth_getBlockByNumber", [toHex(blockNumber), false]);
519
- if (canonicalBlockValue === null || rpcHex(rpcObject(canonicalBlockValue, "receipt canonical block").hash, "receipt canonical hash").toLowerCase() !== blockHash.toLowerCase()) return { ...base, status: "reorged", blockNumber, blockHash };
520
- const confirmations = block.number >= blockNumber ? block.number - blockNumber + 1n : 0n;
521
- const required = reference.confirmations ?? 1;
522
- if (!Number.isSafeInteger(required) || required < 1) throw new LifecyclePlanningError("INVALID_CONFIRMATIONS", "Receipt confirmations must be a positive integer");
523
- if (confirmations < BigInt(required)) return { ...base, status: "unconfirmed", blockNumber, blockHash, confirmations };
524
- if (!matches) return { ...base, status: "replacement-cancelled", blockNumber, blockHash, confirmations };
525
- if (rpcQuantity(receipt.status, "receipt status") !== 1n) return { ...base, status: "reverted", blockNumber, blockHash, confirmations };
526
- return { ...base, status: "confirmed", blockNumber, blockHash, confirmations };
560
+ }
561
+
562
+ async function readReceipts(options: ReadLaunchProgressOptions, block: LifecycleBlock, confirmationDepth: number): Promise<LifecycleReceiptStatus[]> {
563
+ if (options.receipts === undefined || options.receipts.length === 0) return [];
564
+ const identityBlocks = new Map<bigint, Promise<LifecycleBlock>>();
565
+ const readIdentityBlock = (number: bigint) => {
566
+ if (number === block.number) return Promise.resolve(block);
567
+ let pending = identityBlocks.get(number);
568
+ if (pending === undefined) { pending = readLifecycleBlock(options.client, toHex(number)); identityBlocks.set(number, pending); }
569
+ return pending;
570
+ };
571
+ const observations = await Promise.all((options.receipts ?? []).map((reference) =>
572
+ readReceipt({ ...reference, confirmations: Math.max(reference.confirmations ?? 1, confirmationDepth) }, options, block, readIdentityBlock)));
573
+ const headers = new Map<bigint, Hex | undefined>();
574
+ for (const observation of observations) for (const pin of observation.pins) headers.set(pin.number, undefined);
575
+ // Reuse this invocation's authoritative L2 head header. Older receipt blocks
576
+ // share one canonical read after their asynchronous identity observations.
577
+ await Promise.all([...headers.keys()].map(async (number) => {
578
+ if (number === block.number) { headers.set(number, block.hash); return; }
579
+ const value = await lifecycleRpc(options.client, "eth_getBlockByNumber", [toHex(number), false]);
580
+ const hash = value === null ? undefined : rpcHex(rpcObject(value, "receipt canonical block").hash, "receipt canonical hash");
581
+ if (hash !== undefined && hash.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Receipt canonical block hash must have 32 bytes");
582
+ headers.set(number, hash);
583
+ }));
584
+ return observations.map((observation) => observation.finish(headers));
527
585
  }
528
586
 
529
587
  /** Reads confirmation-bound canonical state and receipt provenance; local step counters are never used. */
530
588
  export async function readLaunchProgress(options: ReadLaunchProgressOptions, pinnedBlock?: LifecycleBlock): Promise<CanonicalLaunchProgress> {
531
589
  if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
532
- const block = pinnedBlock ?? await readLifecycleBlock(options.client);
533
- return withLifecycleReadClient(options.client, (client) => readLaunchProgressAtBlock(options, client, block, pinnedBlock === undefined));
590
+ const planHash = options.planned.planHash, launchId = options.planned.launchId;
591
+ const result = await withLifecycleReadClient(options.client, async (client) =>
592
+ readLaunchProgressAtBlock(options, client, pinnedBlock ?? await readLifecycleBlock(client)));
593
+ if (hashLaunchPlan(options.planned.plan) !== planHash || hashLaunchIdentity(options.planned.plan) !== launchId) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during the final canonical progress guard");
594
+ if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
595
+ return result;
534
596
  }
535
597
 
536
- async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, client: LifecycleRpcClient, block: LifecycleBlock, checkBlock: boolean): Promise<CanonicalLaunchProgress> {
598
+ /** Build-next needs the state machine, receipts and nonce, not display-only live markets. */
599
+ export async function readLifecycleExecutionProgress(options: ReadLaunchProgressOptions, block: LifecycleBlock): Promise<CanonicalLaunchProgress> {
600
+ if (!assertLifecycleReadClientOpen(options.client)) throw new LifecyclePlanningError("INVALID_READ_CONTEXT", "Execution progress requires an invocation owner");
601
+ return readLaunchProgressAtBlock(options, options.client, block, false);
602
+ }
603
+
604
+ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, client: LifecycleRpcClient, block: LifecycleBlock, readMarkets = true): Promise<CanonicalLaunchProgress> {
537
605
  const { planned } = options;
538
606
  const assertActive = () => {
539
607
  if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
@@ -555,7 +623,7 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
555
623
  } else if (planned.account.toLowerCase() !== planned.plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
556
624
  if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan no longer matches its commitment");
557
625
  const [head, confirmedProgress, token, confirmedNonce, headNonce, pendingNonce] = await Promise.all([
558
- readLifecycleContract<LaunchProgressV1>(client, planned.plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [planned.launchId], block, planned.account),
626
+ readLifecycleHeadProgress(client, planned.plan, planned.account, block),
559
627
  confirmedNumber === block.number ? undefined : readLifecycleContract<LaunchProgressV1>(client, planned.plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [planned.launchId], confirmedBlock, planned.account),
560
628
  readLifecycleTokenAtBlock(client, planned.plan, block),
561
629
  lifecycleRpc(client, "eth_getTransactionCount", [planned.account, toHex(confirmedBlock.number)]),
@@ -582,16 +650,23 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
582
650
  const pendingAccountNonce = rpcQuantity(pendingNonce, "pending account nonce");
583
651
  const progressParameters = [{ type: "tuple", components: launchProgressV1Components }] as const;
584
652
  const confirmationSafe = confirmedAccountNonce === headAccountNonce && pendingAccountNonce === headAccountNonce && keccak256(encodeAbiParameters(progressParameters, [canonical])) === keccak256(encodeAbiParameters(progressParameters, [head]));
585
- const receipts = await Promise.all((options.receipts ?? []).map((reference) => readReceipt({ ...reference, confirmations: Math.max(reference.confirmations ?? 1, confirmationDepth) }, { ...options, client }, block)));
653
+ const receipts = await readReceipts({ ...options, client }, block, confirmationDepth);
586
654
  assertActive();
587
655
  let markets: LifecycleMarketProgress[] = [];
588
- if (canonical.preparedMarkets > 0) {
656
+ const lens = lifecycleLensSnapshot(client, confirmedBlock);
657
+ if (readMarkets && lens !== undefined) {
658
+ markets = lens.preparedMarkets.map((market) => {
659
+ validateLifecycleMarketIdentity(market.prepared.identity, planned.plan, token, market.index);
660
+ return { index: market.index, prepared: market.prepared,
661
+ ...(market.liveStateSuccess ? { live: market.live } : { error: "Prepared market live-state read failed" }) };
662
+ });
663
+ } else if (readMarkets && canonical.preparedMarkets > 0) {
589
664
  const directory = await readLifecycleContract<Address>(client, planned.plan.orchestrator, launchLifecycleAbi, "directory", [], confirmedBlock);
590
665
  markets = await Promise.all(Array.from({ length: canonical.preparedMarkets }, async (_, index): Promise<LifecycleMarketProgress> => {
591
- const [adapter, prepared] = await readLifecycleContract<readonly [Address, PreparedMarketV1]>(client, directory, lifecycleDirectoryAbi, "market", [planned.launchId, index], confirmedBlock);
666
+ const [adapter, prepared] = await readLifecycleContract<readonly [Address, PreparedMarketV1]>(client, directory, lifecycleDirectoryAbi, "market", [planned.launchId, index], confirmedBlock, undefined, false, true);
592
667
  validateLifecycleMarketIdentity(prepared.identity, planned.plan, token, index);
593
668
  try {
594
- const live = await readLifecycleContract<MarketLiveStateV1>(client, adapter, lifecycleAdapterAbi, "readMarket", [planned.launchId, index], confirmedBlock);
669
+ const live = await readLifecycleContract<MarketLiveStateV1>(client, adapter, lifecycleAdapterAbi, "readMarket", [planned.launchId, index], confirmedBlock, undefined, false, true);
595
670
  return { index, prepared, live };
596
671
  } catch (failure) {
597
672
  if (failure instanceof Error && failure.name === "AbortError") throw failure;
@@ -599,8 +674,6 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
599
674
  }
600
675
  }));
601
676
  }
602
- if (checkBlock) await assertLifecycleBlock(client, block, chainId);
603
- if (confirmedNumber !== block.number) await assertLifecycleBlock(client, confirmedBlock, chainId);
604
677
  if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan changed during asynchronous progress reads");
605
678
  assertActive();
606
679
  return { canonical, head, confirmationDepth, confirmedBlockNumber: confirmedBlock.number, confirmedBlockHash: confirmedBlock.hash, confirmedAccountNonce, headAccountNonce, pendingAccountNonce, confirmationSafe, token, planHash: planned.planHash, launchId: planned.launchId, chainId, blockNumber: block.number, blockHash: block.hash, markets, receipts };
@@ -1,7 +1,14 @@
1
- import { decodeFunctionResult, encodeFunctionData, isAddress, isHex, parseAbi, toHex, type Abi, type Address, type Hex } from "viem";
1
+ import { decodeFunctionResult, encodeFunctionData, isAddress, isHex, keccak256, multicall3Abi, parseAbi, toHex, type Abi, type Address, type Hex } from "viem";
2
2
  import { LifecyclePlanningError, type LifecycleBlock, type LifecycleDiagnosticEvent, type LifecycleDiagnosticListener, type LifecycleDiagnosticStage, type LifecycleLimitContext, type LifecycleLimitSource, type LifecycleLimits, type LifecycleRpcClient, type LifecycleSessionCache, type LifecycleTransaction, type ResolvedLifecycleLimits } from "./types.js";
3
3
  import { lifecycleRegistryAbi } from "./abi.js";
4
4
  import type { ProfileTopologyV1 } from "./schema.js";
5
+ import { robinhoodMainnet } from "../addresses.js";
6
+
7
+ // Reviewed chain4663 deployment, independently observed at block 98,136,655.
8
+ const multicall3CodeHash = "0xd5c15df687b16f2ff992fc8d767b4216323184a2bbc6ee2f9c398c318e770891";
9
+ const sessionObservations = new WeakMap<object, Map<string, Promise<unknown>>>();
10
+ const canonicalWireValue = (_key: string, value: unknown): unknown => typeof value === "string" && value.startsWith("0x") ? value.toLowerCase() : value;
11
+ type IndependentRead = { params: readonly unknown[]; resolve: (value: unknown) => void; reject: (failure: unknown) => void };
5
12
 
6
13
  const lifecycleReadConcurrency = 8;
7
14
  const diagnosticRpcMethods: Record<string, true> = {
@@ -67,6 +74,10 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
67
74
  private readonly concurrency: number;
68
75
  private diagnosticSequence = 0;
69
76
  private chainId?: Promise<bigint>;
77
+ private gasPrice?: Promise<unknown>;
78
+ private readonly immutableReads = new Map<string, Promise<unknown>>();
79
+ private readonly independentReads = new Map<string, { block: LifecycleBlock; reads: IndependentRead[] }>();
80
+ readonly finalChecks = new Set<() => void>();
70
81
  private readonly limitObservations = new Map<LifecycleLimitSource | undefined, Map<string, Promise<LifecycleLimitObservation>>>();
71
82
  private readonly session?: SessionLifecycleCache;
72
83
  private sessionGroup?: SessionReadGroup;
@@ -145,6 +156,73 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
145
156
  return this.chainId ??= this.run("eth_chainId", []).then((value) => rpcQuantity(value, "chain ID"));
146
157
  }
147
158
 
159
+ observeGasPrice(): Promise<unknown> {
160
+ this.assertOpen();
161
+ return this.gasPrice ??= this.run("eth_gasPrice", []);
162
+ }
163
+
164
+ immutableRead(key: string, work: () => Promise<unknown>): Promise<unknown> {
165
+ this.assertOpen();
166
+ let pending = this.immutableReads.get(key);
167
+ if (pending === undefined) {
168
+ pending = work();
169
+ this.immutableReads.set(key, pending);
170
+ const owned = pending;
171
+ void pending.catch(() => { if (this.immutableReads.get(key) === owned) this.immutableReads.delete(key); });
172
+ }
173
+ return pending.then((value) => { this.assertOpen(); return value; });
174
+ }
175
+
176
+
177
+ async finalize(): Promise<void> {
178
+ this.assertOpen();
179
+ for (const check of this.finalChecks) check();
180
+ }
181
+
182
+ independentRead(params: readonly unknown[], block: LifecycleBlock, staticRead: boolean): Promise<unknown> {
183
+ return this.read("eth_call", params, block, staticRead, () => new Promise((resolve, reject) => {
184
+ const key = `${block.number}:${block.hash.toLowerCase()}`;
185
+ let group = this.independentReads.get(key);
186
+ if (group === undefined) {
187
+ group = { block, reads: [] };
188
+ this.independentReads.set(key, group);
189
+ queueMicrotask(() => {
190
+ this.independentReads.delete(key);
191
+ void this.flushIndependentReads(group!);
192
+ });
193
+ }
194
+ group.reads.push({ params, resolve, reject });
195
+ }));
196
+ }
197
+
198
+ private async flushIndependentReads(group: { block: LifecycleBlock; reads: IndependentRead[] }): Promise<void> {
199
+ const { block, reads } = group;
200
+ try {
201
+ if (await this.observeChainId() !== 4663n) {
202
+ await Promise.all(reads.map(async (read) => {
203
+ try { read.resolve(await this.run("eth_call", read.params)); }
204
+ catch (failure) { read.reject(failure); }
205
+ }));
206
+ return;
207
+ }
208
+ const to = robinhoodMainnet.contracts.multicall3.address;
209
+ await attestLifecycleMulticall(this, block);
210
+ const calls = reads.map((read) => {
211
+ const request = read.params[0] as { to: Address; data: Hex };
212
+ return { target: request.to, allowFailure: true, callData: request.data };
213
+ });
214
+ const data = encodeFunctionData({ abi: multicall3Abi, functionName: "aggregate3", args: [calls] });
215
+ const raw = rpcHex(await this.run("eth_call", [{ to, data }, toHex(block.number)]), "Multicall3 result");
216
+ const results = decodeFunctionResult({ abi: multicall3Abi, functionName: "aggregate3", data: raw });
217
+ if (results.length !== reads.length) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Multicall3 omitted independent pinned results");
218
+ for (let index = 0; index < reads.length; index += 1) {
219
+ const result = results[index]!;
220
+ if (result.success) reads[index]!.resolve(result.returnData);
221
+ else reads[index]!.reject(new LifecyclePlanningError("MULTICALL_READ_FAILED", "An independent pinned getter reverted"));
222
+ }
223
+ } catch (failure) { for (const read of reads) read.reject(failure); }
224
+ }
225
+
148
226
  observeLimits(context: LifecycleLimitContext, source?: LifecycleLimitSource): Promise<LifecycleLimitObservation> {
149
227
  this.assertOpen();
150
228
  // A caller callback is live policy, not an immutable pinned observation.
@@ -168,11 +246,11 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
168
246
  return observation.then((result) => { this.assertOpen(); result.assertCurrent(); return result; });
169
247
  }
170
248
 
171
- read(method: string, params: readonly unknown[], block: LifecycleBlock, staticRead = false): Promise<unknown> {
249
+ read(method: string, params: readonly unknown[], block: LifecycleBlock, staticRead = false, load?: () => Promise<unknown>): Promise<unknown> {
172
250
  this.assertOpen();
173
251
  // Source identity is this scope's original client. The complete wire call includes
174
252
  // caller, value, gas and any other call fields; no selector-only/ABI-name aliasing.
175
- const key = JSON.stringify([block.number.toString(), block.hash.toLowerCase(), method, params]);
253
+ const key = JSON.stringify([block.number.toString(), block.hash, method, params], canonicalWireValue);
176
254
  const existing = this.reads.get(key);
177
255
  if (existing !== undefined) {
178
256
  if (staticRead && this.session !== undefined) this.sessionReuses += 1;
@@ -189,7 +267,7 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
189
267
  pending = Promise.resolve(cached);
190
268
  } else {
191
269
  if (staticRead && this.session !== undefined) this.sessionMisses += 1;
192
- pending = this.run(method, params);
270
+ pending = load === undefined ? this.run(method, params) : load();
193
271
  if (group !== undefined) pending = pending.then((value) => {
194
272
  // Never share in-flight work or install late results after invalidation/closure.
195
273
  // Only wire hex is retained, so public decoded DTOs cannot mutate authority.
@@ -202,11 +280,21 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
202
280
  return pending;
203
281
  }
204
282
 
283
+ seed(method: string, params: readonly unknown[], block: LifecycleBlock, value: unknown): void {
284
+ this.assertOpen();
285
+ const key = JSON.stringify([block.number.toString(), block.hash, method, params], canonicalWireValue);
286
+ this.reads.set(key, Promise.resolve(value));
287
+ }
288
+
205
289
  close(success: boolean): void {
206
290
  this.closed = true;
207
291
  this.reads.clear();
208
292
  this.chainId = undefined;
209
293
  this.limitObservations.clear();
294
+ this.gasPrice = undefined;
295
+ this.immutableReads.clear();
296
+ this.independentReads.clear();
297
+ this.finalChecks.clear();
210
298
  const listener = this.source.onDiagnostic;
211
299
  if (this.session !== undefined && listener !== undefined) emitLifecycleDiagnostic(listener, {
212
300
  stage: "session-cache", phase: success ? "success" : "failure",
@@ -221,7 +309,14 @@ export async function withLifecycleReadClient<T>(source: LifecycleRpcClient, wor
221
309
  const client = new InvocationLifecycleClient(source);
222
310
  let success = false;
223
311
  try {
224
- const result = await work(client);
312
+ let result: T;
313
+ try { result = await work(client); }
314
+ catch (failure) {
315
+ // Live policy is rechecked even when exact execution or a source read refuses.
316
+ await client.finalize();
317
+ throw failure;
318
+ }
319
+ await client.finalize();
225
320
  success = true;
226
321
  return result;
227
322
  } finally { client.close(success); }
@@ -278,13 +373,65 @@ export function lifecycleStaticRpc(client: LifecycleRpcClient, method: "eth_call
278
373
  export async function lifecycleRpc(client: LifecycleRpcClient, method: string, params: readonly unknown[] = []): Promise<unknown> {
279
374
  return client instanceof InvocationLifecycleClient ? client.run(method, params) : client.request({ method, params });
280
375
  }
281
- /** One invocation's observed identity. End-of-operation and submission checks
282
- * deliberately use uncached eth_chainId so a switched provider cannot retain proof. */
376
+ /** One invocation's observed identity; wallet submission owns its final chain check. */
283
377
  export function readLifecycleChainId(client: LifecycleRpcClient): Promise<bigint> {
284
378
  return client instanceof InvocationLifecycleClient
285
379
  ? client.observeChainId()
286
380
  : lifecycleRpc(client, "eth_chainId").then((value) => rpcQuantity(value, "chain ID"));
287
381
  }
382
+
383
+ /** A single operation-local recommendation, never a session or cross-head quote. */
384
+ export function readLifecycleGasPrice(client: LifecycleRpcClient): Promise<unknown> {
385
+ return client instanceof InvocationLifecycleClient ? client.observeGasPrice() : lifecycleRpc(client, "eth_gasPrice");
386
+ }
387
+
388
+ /** Only the reviewed immutable factory identity may use an invocation-wide key. */
389
+ export function readLifecycleImmutable<T>(client: LifecycleRpcClient, key: string, work: () => Promise<T>): Promise<T> {
390
+ return (client instanceof InvocationLifecycleClient ? client.immutableRead(key, work) : work()) as Promise<T>;
391
+ }
392
+
393
+ /** Successful deployment/backend observations live for the caller's session, not one block. */
394
+ export function lifecycleSessionRead<T>(client: LifecycleRpcClient, key: string, work: () => Promise<T>): Promise<T> {
395
+ const source = lifecycleSourceClient(client);
396
+ const owner = source.sessionCache ?? source;
397
+ let values = sessionObservations.get(owner);
398
+ if (values === undefined) { values = new Map(); sessionObservations.set(owner, values); }
399
+ let pending = values.get(key);
400
+ if (pending === undefined) {
401
+ pending = work();
402
+ values.set(key, pending);
403
+ const owned = pending;
404
+ void pending.catch(() => { if (values.get(key) === owned) values.delete(key); });
405
+ }
406
+ return pending as Promise<T>;
407
+ }
408
+
409
+ export async function attestLifecycleMulticall(client: LifecycleRpcClient, block?: LifecycleBlock): Promise<void> {
410
+ const to = robinhoodMainnet.contracts.multicall3.address;
411
+ await lifecycleSessionRead(client, `4663:multicall:${to}`, async () => {
412
+ const code = rpcHex(await lifecycleRpc(client, "eth_getCode", [to, block === undefined ? "latest" : toHex(block.number)]), "Multicall3 code");
413
+ if (keccak256(code) !== multicall3CodeHash) throw new LifecyclePlanningError("MULTICALL_CODE_MISMATCH", "Multicall3 runtime differs from the reviewed chain4663 deployment");
414
+ });
415
+ }
416
+
417
+ /** Installs only SDK-observed wire results from this invocation's aggregate/lens. */
418
+ export function seedLifecycleRead(client: LifecycleRpcClient, method: string, params: readonly unknown[], block: LifecycleBlock, value: unknown): void {
419
+ if (!(client instanceof InvocationLifecycleClient)) throw new LifecyclePlanningError("INVALID_READ_CONTEXT", "Snapshot reads require an invocation owner");
420
+ client.observeBlock(block, true);
421
+ client.seed(method, params, block, value);
422
+ }
423
+
424
+ /** Recheck live policy after the owner's final asynchronous canonical guards. */
425
+ export function registerLifecycleFinalCheck(client: LifecycleRpcClient, check: () => void): void {
426
+ if (client instanceof InvocationLifecycleClient) { client.assertOpen(); client.finalChecks.add(check); }
427
+ }
428
+
429
+ /** SDK-selected caller-independent getters only; never poster quotes or execution. */
430
+ function lifecycleIndependentRpc(client: LifecycleRpcClient, params: readonly unknown[], block: LifecycleBlock, staticRead: boolean): Promise<unknown> {
431
+ return client instanceof InvocationLifecycleClient
432
+ ? client.independentRead(params, block, staticRead)
433
+ : lifecyclePinnedRpc(client, "eth_call", params, block, staticRead);
434
+ }
288
435
  export function rpcObject(value: unknown, label: string): Record<string, unknown> {
289
436
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", `${label} must be an object`);
290
437
  // The object guard establishes the RPC record boundary; fields remain untrusted unknowns.
@@ -325,7 +472,8 @@ export async function readLifecycleBlock(client: LifecycleRpcClient, tag: Hex |
325
472
  if (client instanceof InvocationLifecycleClient) client.observeBlock(block, tag === "latest");
326
473
  return block;
327
474
  }
328
- export async function assertLifecycleBlock(client: LifecycleRpcClient, block: LifecycleBlock, expectedChainId?: bigint): Promise<void> {
475
+ export async function assertLifecycleBlock(client: LifecycleRpcClient, block: Pick<LifecycleBlock, "number" | "hash">, expectedChainId?: bigint): Promise<void> {
476
+ if (client instanceof InvocationLifecycleClient) return;
329
477
  const [chainId, current] = await Promise.all([
330
478
  expectedChainId === undefined ? undefined : lifecycleRpc(client, "eth_chainId"),
331
479
  readLifecycleBlock(client, toHex(block.number)),
@@ -339,13 +487,15 @@ export async function assertLifecycleBlock(client: LifecycleRpcClient, block: Li
339
487
  throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch state changed during the operation; read canonical progress again");
340
488
  }
341
489
  }
342
- export async function readLifecycleContract<T>(client: LifecycleRpcClient, to: Address, abi: Abi, functionName: string, args: readonly unknown[], block: LifecycleBlock, from?: Address, staticRead = false): Promise<T> {
490
+ export async function readLifecycleContract<T>(client: LifecycleRpcClient, to: Address, abi: Abi, functionName: string, args: readonly unknown[], block: LifecycleBlock, from?: Address, staticRead = false, independent = false): Promise<T> {
343
491
  const data = encodeFunctionData({ abi, functionName, args });
344
492
  const request = [{ to, from, data }, toHex(block.number)];
345
493
  const getter = abi.some((item) => item.type === "function" && item.name === functionName && (item.stateMutability === "view" || item.stateMutability === "pure"));
346
- const result = rpcHex(await (getter && functionName !== "readLaunchProgress"
347
- ? staticRead ? lifecycleStaticRpc(client, "eth_call", request, block) : lifecyclePinnedRpc(client, "eth_call", request, block)
348
- : lifecycleRpc(client, "eth_call", request)), functionName);
494
+ const result = rpcHex(await (independent
495
+ ? lifecycleIndependentRpc(client, request, block, staticRead)
496
+ : getter
497
+ ? staticRead ? lifecycleStaticRpc(client, "eth_call", request, block) : lifecyclePinnedRpc(client, "eth_call", request, block)
498
+ : lifecycleRpc(client, "eth_call", request)), functionName);
349
499
  // ABI decoding validates scalar widths and tuple layout; T names that exact frozen ABI result.
350
500
  const decoded = decodeFunctionResult({ abi, functionName, data: result }) as T;
351
501
  return decoded;
@@ -382,7 +532,7 @@ export async function readNitroPosterGas(transaction: LifecycleTransaction, cont
382
532
 
383
533
  async function readNitroScalar(context: LifecycleLimitContext, to: Address, abi: Abi, functionName: string): Promise<bigint> {
384
534
  const data = encodeFunctionData({ abi, functionName });
385
- const result = rpcHex(await lifecycleStaticRpc(context.client, "eth_call", [{ to, data }, toHex(context.block.number)], context.block), functionName);
535
+ const result = rpcHex(await lifecycleIndependentRpc(context.client, [{ to, data }, toHex(context.block.number)], context.block, true), functionName);
386
536
  if (result.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", `${functionName} must return exactly one ABI word`);
387
537
  return decodeFunctionResult({ abi, functionName, data: result }) as bigint;
388
538
  }