@black-market/sdk 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +96 -79
  2. package/dist/lifecycle/abi.d.ts +1 -0
  3. package/dist/lifecycle/abi.d.ts.map +1 -1
  4. package/dist/lifecycle/abi.js +1 -0
  5. package/dist/lifecycle/abi.js.map +1 -1
  6. package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.d.ts +71 -0
  7. package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.d.ts.map +1 -0
  8. package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.js +73 -0
  9. package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.js.map +1 -0
  10. package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.d.ts +71 -0
  11. package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.d.ts.map +1 -0
  12. package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.js +73 -0
  13. package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.js.map +1 -0
  14. package/dist/lifecycle/index.d.ts +1 -0
  15. package/dist/lifecycle/index.d.ts.map +1 -1
  16. package/dist/lifecycle/index.js +1 -0
  17. package/dist/lifecycle/index.js.map +1 -1
  18. package/dist/lifecycle/markets.d.ts +3 -1
  19. package/dist/lifecycle/markets.d.ts.map +1 -1
  20. package/dist/lifecycle/markets.js +20 -3
  21. package/dist/lifecycle/markets.js.map +1 -1
  22. package/dist/lifecycle/planner.d.ts.map +1 -1
  23. package/dist/lifecycle/planner.js +101 -46
  24. package/dist/lifecycle/planner.js.map +1 -1
  25. package/dist/lifecycle/presets.d.ts +4 -2
  26. package/dist/lifecycle/presets.d.ts.map +1 -1
  27. package/dist/lifecycle/presets.js +11 -4
  28. package/dist/lifecycle/presets.js.map +1 -1
  29. package/dist/lifecycle/progress.d.ts +3 -1
  30. package/dist/lifecycle/progress.d.ts.map +1 -1
  31. package/dist/lifecycle/progress.js +145 -59
  32. package/dist/lifecycle/progress.js.map +1 -1
  33. package/dist/lifecycle/rpc.d.ts +8 -2
  34. package/dist/lifecycle/rpc.d.ts.map +1 -1
  35. package/dist/lifecycle/rpc.js +165 -9
  36. package/dist/lifecycle/rpc.js.map +1 -1
  37. package/dist/lifecycle/simulation.d.ts +1 -1
  38. package/dist/lifecycle/simulation.d.ts.map +1 -1
  39. package/dist/lifecycle/simulation.js +15 -11
  40. package/dist/lifecycle/simulation.js.map +1 -1
  41. package/dist/lifecycle/types.d.ts +23 -1
  42. package/dist/lifecycle/types.d.ts.map +1 -1
  43. package/dist/lifecycle/types.js.map +1 -1
  44. package/dist/lifecycle/ufg-abi.d.ts +2386 -0
  45. package/dist/lifecycle/ufg-abi.d.ts.map +1 -0
  46. package/dist/lifecycle/ufg-abi.js +171 -0
  47. package/dist/lifecycle/ufg-abi.js.map +1 -0
  48. package/dist/lifecycle/ufg-config.d.ts +38 -0
  49. package/dist/lifecycle/ufg-config.d.ts.map +1 -0
  50. package/dist/lifecycle/ufg-config.js +75 -0
  51. package/dist/lifecycle/ufg-config.js.map +1 -0
  52. package/dist/lifecycle/ufg.d.ts +21 -0
  53. package/dist/lifecycle/ufg.d.ts.map +1 -0
  54. package/dist/lifecycle/ufg.js +98 -0
  55. package/dist/lifecycle/ufg.js.map +1 -0
  56. package/docs/launch-lifecycle.md +611 -1363
  57. package/docs/reference.md +332 -0
  58. package/examples/.env.example +3 -2
  59. package/examples/README.md +172 -539
  60. package/examples/launch-example-support.ts +11 -3
  61. package/examples/launch-example.ts +1 -1
  62. package/examples/launch-lifecycle-smoke.ts +1 -1
  63. package/examples/ufg-launch-config.ts +31 -0
  64. package/package.json +1 -1
  65. package/src/lifecycle/abi.ts +1 -0
  66. package/src/lifecycle/deployments/ufg-dynamic-pool-20261021.ts +74 -0
  67. package/src/lifecycle/deployments/ufg-fixed-pool-20261021.ts +74 -0
  68. package/src/lifecycle/index.ts +1 -0
  69. package/src/lifecycle/markets.ts +16 -3
  70. package/src/lifecycle/planner.ts +95 -45
  71. package/src/lifecycle/presets.ts +11 -4
  72. package/src/lifecycle/progress.ts +131 -51
  73. package/src/lifecycle/rpc.ts +158 -10
  74. package/src/lifecycle/simulation.ts +14 -10
  75. package/src/lifecycle/types.ts +15 -2
  76. package/src/lifecycle/ufg-abi.ts +173 -0
  77. package/src/lifecycle/ufg-config.ts +77 -0
  78. package/src/lifecycle/ufg.ts +86 -0
@@ -2,10 +2,12 @@ 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 { assertLifecycleBlock, 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
+ import { ufgLaunchProxyV1Abi } from "./ufg-abi.js";
10
+ import { assertUfgLaunchAccount, hashUfgLaunchConfig, UFG_LAUNCH_PROXY_ADDRESS, type UfgLaunchConfig } from "./ufg-config.js";
9
11
 
10
12
  export const ABYSS_LIFECYCLE_CONFIG_SCHEMA = keccak256(stringToHex("(uint8,uint24,bytes32,uint160,(int24,int24,uint128,uint256)[])"));
11
13
 
@@ -19,9 +21,18 @@ const lifecycleTokenPredictionAbi = [{
19
21
  * avoiding full-plan prediction reads after salts or buy minima change. */
20
22
  export async function readLifecycleTokenAtBlock(client: LifecycleRpcClient, plan: LaunchPlanV1, block: LifecycleBlock): Promise<Address> {
21
23
  const deployment = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator });
22
- return deployment === undefined
23
- ? readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block)
24
- : readLifecycleContract<Address>(client, deployment.tokenFactory, lifecycleTokenPredictionAbi, "predictToken", [hashLaunchIdentity(plan), plan.token], block);
24
+ if (deployment === undefined) return readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block);
25
+ const launchId = hashLaunchIdentity(plan);
26
+ const data = encodeFunctionData({ abi: lifecycleTokenPredictionAbi, functionName: "predictToken", args: [launchId, plan.token] });
27
+ const key = `${plan.chainId}:${plan.orchestrator.toLowerCase()}:${deployment.tokenFactory.toLowerCase()}:${deployment.tokenFactoryCodeHash}:${data}`;
28
+ return readLifecycleImmutable(client, key, () =>
29
+ readLifecycleContract<Address>(client, deployment.tokenFactory, lifecycleTokenPredictionAbi, "predictToken", [launchId, plan.token], block, undefined, false, true));
30
+ }
31
+
32
+ /** Internal pinned head observation. Known cores do not inspect msg.sender. */
33
+ export function readLifecycleHeadProgress(client: LifecycleRpcClient, plan: LaunchPlanV1, account: Address, block: LifecycleBlock): Promise<LaunchProgressV1> {
34
+ const known = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator }) !== undefined;
35
+ return readLifecycleContract<LaunchProgressV1>(client, plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [hashLaunchIdentity(plan)], block, account, false, known);
25
36
  }
26
37
 
27
38
  export async function predictLifecycleToken(options: { client: LifecycleRpcClient; plan: LaunchPlanV1 }): Promise<Address> {
@@ -222,7 +233,7 @@ export async function validateV4LifecycleOracle(options: {
222
233
 
223
234
  export async function readLifecycleProfiles(options: { client: LifecycleRpcClient; orchestrator: Address; profileIds?: readonly Hex[]; offset?: bigint; limit?: bigint }, pinnedBlock?: LifecycleBlock): Promise<LifecycleProfile[]> {
224
235
  return withLifecycleReadClient(options.client, (client) => lifecycleStage(client, "profile.certification", async () => {
225
- const chainIdPromise = lifecycleRpc(client, "eth_chainId").then((value) => rpcQuantity(value, "chain ID"));
236
+ const chainIdPromise = readLifecycleChainId(client);
226
237
  // Block-bound discovery does not depend on chain identity. Join both branches
227
238
  // immediately so a failed chain read cannot become detached or a profile refusal.
228
239
  const [chainId, { block, profiles }] = await Promise.all([
@@ -301,7 +312,7 @@ async function readPoolBoundHookDeploymentDetails(options: { client: LifecycleRp
301
312
  const market = plan.markets[marketIndex];
302
313
  if (market === undefined || !isPoolBoundV4ConfigVersion(market.configVersion)) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Market index must select explicit pool-bound config version 5 or 6");
303
314
  let chainId: bigint;
304
- try { chainId = rpcQuantity(await lifecycleRpc(client, "eth_chainId"), "chain ID"); }
315
+ try { chainId = await readLifecycleChainId(client); }
305
316
  finally { assertDerivedReadClientOpen(client); }
306
317
  if (hashLaunchPlan(plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while observing the hook deployment chain");
307
318
  if (chainId !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
@@ -417,19 +428,21 @@ async function readPoolBoundHookDeploymentDetailsFresh(options: { client: Lifecy
417
428
  export async function readPoolBoundHookDeployment(options: { client: LifecycleRpcClient; plan: LaunchPlanV1; marketIndex: number }, pinnedBlock?: LifecycleBlock): Promise<PoolBoundHookDeployment> {
418
429
  const commitment = hashLaunchPlan(options.plan);
419
430
  const block = pinnedBlock ?? await readLifecycleBlock(options.client);
420
- return withLifecycleReadClient(options.client, async (client) => {
431
+ const result = await withLifecycleReadClient(options.client, async (client) => {
421
432
  const { deployment } = await readPoolBoundHookDeploymentDetails({ ...options, client }, block, commitment);
422
433
  if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while reading hook deployment metadata");
423
434
  if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, options.plan.chainId);
424
435
  return { ...deployment };
425
436
  });
437
+ if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during the final hook deployment guard");
438
+ return result;
426
439
  }
427
440
 
428
441
  /** Optional permissionless predeployment calldata; no token deployment, pool registration or wallet broadcast. */
429
442
  export async function buildPoolBoundHookDeploymentTransaction(options: { client: LifecycleRpcClient; plan: LaunchPlanV1; marketIndex: number }): Promise<{ to: Address; data: Hex; value: bigint; deployment: PoolBoundHookDeployment }> {
430
443
  const commitment = hashLaunchPlan(options.plan);
431
444
  const block = await readLifecycleBlock(options.client);
432
- return withLifecycleReadClient(options.client, async (client) => {
445
+ const result = await withLifecycleReadClient(options.client, async (client) => {
433
446
  const { deployment, parameters, configVersion } = await readPoolBoundHookDeploymentDetails({ ...options, client }, block, commitment);
434
447
  if (!hasLifecycleV4HookPermissions(deployment.predictedHook)) throw new LifecyclePlanningError("INVALID_HOOK_BITS", "Finalize a salt with the exact lifecycle hook permissions before predeployment");
435
448
  if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while constructing hook deployment calldata");
@@ -437,6 +450,8 @@ export async function buildPoolBoundHookDeploymentTransaction(options: { client:
437
450
  const abi = configVersion === 5 ? poolHookDeployerV1Abi : poolHookDeployerConfigV6Abi;
438
451
  return { to: deployment.deployer, data: encodeFunctionData({ abi, functionName: "deploy", args: [parameters, deployment.salt] }), value: 0n, deployment: { ...deployment } };
439
452
  });
453
+ if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during the final hook transaction guard");
454
+ return result;
440
455
  }
441
456
 
442
457
  export function validateLifecycleMarketIdentity(identity: MarketIdentityV1, plan: LaunchPlanV1, token: Address, marketIndex: number): void {
@@ -453,19 +468,24 @@ export function validateLifecycleMarketIdentity(identity: MarketIdentityV1, plan
453
468
  } else if (identity.venue !== LifecycleVenue.Abyss || identity.pool === zeroAddress || identity.factory === zeroAddress) throw new LifecyclePlanningError("MARKET_IDENTITY", "Abyss canonical factory/pool identity is missing");
454
469
  }
455
470
 
456
- async function transactionMatchesLaunch(transaction: Record<string, unknown>, options: ReadLaunchProgressOptions, block: LifecycleBlock): Promise<boolean> {
471
+ async function transactionMatchesLaunch(transaction: Record<string, unknown>, options: ReadLaunchProgressOptions, readBlock: () => Promise<LifecycleBlock>): Promise<boolean> {
457
472
  const { planned } = options;
458
473
  const from = rpcHex(transaction.from, "receipt transaction account");
459
474
  if (from.toLowerCase() !== planned.account.toLowerCase()) throw new LifecyclePlanningError("RECEIPT_ACCOUNT", "Receipt transaction belongs to a different account");
460
475
  if (transaction.chainId !== undefined && rpcQuantity(transaction.chainId, "receipt transaction chain") !== planned.plan.chainId) throw new LifecyclePlanningError("RECEIPT_CHAIN", "Receipt transaction belongs to a different chain");
461
476
  const to = transaction.to === null ? zeroAddress : rpcHex(transaction.to, "receipt transaction target");
462
477
  const input = rpcHex(transaction.input ?? transaction.data, "receipt transaction calldata");
463
- if (to.toLowerCase() === planned.plan.orchestrator.toLowerCase()) {
478
+ const target = planned.ufg === undefined ? planned.plan.orchestrator : UFG_LAUNCH_PROXY_ADDRESS;
479
+ if (to.toLowerCase() === target.toLowerCase()) {
464
480
  try {
465
- const decoded = decodeFunctionData({ abi: launchLifecycleAbi, data: input });
481
+ const decoded = decodeFunctionData({ abi: planned.ufg === undefined ? launchLifecycleAbi : ufgLaunchProxyV1Abi, data: input });
466
482
  if (!["launchAtomic", "beginLaunch", "prepareMarkets", "activateLaunch", "cancelLaunch"].includes(decoded.functionName) || !Array.isArray(decoded.args)) return false;
467
483
  // Decoded plan layout is validated by the exact ABI, not trusted raw RPC JSON.
468
484
  const plan = decoded.args[0] as LaunchPlanV1;
485
+ if (planned.ufg !== undefined && (decoded.functionName === "launchAtomic" || decoded.functionName === "beginLaunch")) {
486
+ const config = decoded.args[decoded.functionName === "launchAtomic" ? 1 : 2] as UfgLaunchConfig;
487
+ if (hashUfgLaunchConfig(config) !== planned.ufg.configHash) return false;
488
+ }
469
489
  return hashLaunchPlan(plan).toLowerCase() === planned.planHash.toLowerCase();
470
490
  } catch { return false; }
471
491
  }
@@ -477,53 +497,103 @@ async function transactionMatchesLaunch(transaction: Record<string, unknown>, op
477
497
  if (decoded.functionName !== "approve" || rpcQuantity(transaction.value ?? "0x0", "approval native value") !== 0n) return false;
478
498
  spender = decoded.args[0];
479
499
  } catch { return false; }
480
- const escrow = await readLifecycleContract<Address>(options.client, planned.plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], block);
500
+ const known = getKnownLifecycleDeployment({ chainId: planned.plan.chainId, orchestrator: planned.plan.orchestrator });
501
+ const escrow = planned.ufg !== undefined ? UFG_LAUNCH_PROXY_ADDRESS : known?.fundingEscrow ??
502
+ await readLifecycleContract<Address>(options.client, planned.plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], await readBlock());
481
503
  return spender.toLowerCase() === escrow.toLowerCase();
482
504
  }
483
505
 
484
- async function readReceipt(reference: LifecycleReceiptReference, options: ReadLaunchProgressOptions, block: LifecycleBlock): Promise<LifecycleReceiptStatus> {
506
+ type ReceiptObservation = {
507
+ pins: readonly { number: bigint; hash: Hex }[];
508
+ finish(headers: ReadonlyMap<bigint, Hex | undefined>): LifecycleReceiptStatus;
509
+ };
510
+
511
+ async function readReceipt(reference: LifecycleReceiptReference, options: ReadLaunchProgressOptions, block: LifecycleBlock, readIdentityBlock: (number: bigint) => Promise<LifecycleBlock>): Promise<ReceiptObservation> {
485
512
  const effectiveHash = reference.replacementHash ?? reference.transactionHash;
486
513
  const base = { transactionHash: reference.transactionHash, effectiveHash, replaced: reference.replacementHash !== undefined && reference.replacementHash.toLowerCase() !== reference.transactionHash.toLowerCase() };
487
- const observedNumber = reference.observedBlockNumber;
488
- if (observedNumber !== undefined && reference.observedBlockHash !== undefined) {
489
- const observed = await lifecycleRpc(options.client, "eth_getBlockByNumber", [toHex(observedNumber), false]);
490
- 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 };
491
- }
492
- const [receiptValue, transactionValue] = await Promise.all([
493
- lifecycleRpc(options.client, "eth_getTransactionReceipt", [effectiveHash]),
494
- lifecycleRpc(options.client, "eth_getTransactionByHash", [effectiveHash]),
495
- ]);
496
- if (receiptValue === null) {
497
- // An absent receipt with an unchanged canonical observed block is not a
498
- // reorg: the transaction is simply not (yet) mined. A missing/changed
499
- // canonical block above already reported the real reorg.
500
- if (reference.observedBlockHash !== undefined) return { ...base, status: "pending", blockNumber: observedNumber, blockHash: reference.observedBlockHash };
501
- return { ...base, status: "pending" };
514
+ const pins: { number: bigint; hash: Hex }[] = [];
515
+ const observedNumber = reference.observedBlockNumber, observedHash = reference.observedBlockHash;
516
+ if (observedNumber !== undefined && observedHash !== undefined) pins.push({ number: observedNumber, hash: observedHash });
517
+ const finish = (result: LifecycleReceiptStatus, failure?: { error: unknown }): ReceiptObservation => ({
518
+ pins,
519
+ finish(headers) {
520
+ for (const pin of pins) {
521
+ if (failure !== undefined && (pin.number !== observedNumber || pin.hash !== observedHash)) continue;
522
+ if (headers.get(pin.number)?.toLowerCase() !== pin.hash.toLowerCase()) return { ...base, status: "reorged", blockNumber: pin.number, blockHash: pin.hash };
523
+ }
524
+ if (failure !== undefined) throw failure.error;
525
+ return result;
526
+ },
527
+ });
528
+ try {
529
+ const [receiptValue, transactionValue] = await Promise.all([
530
+ lifecycleRpc(options.client, "eth_getTransactionReceipt", [effectiveHash]),
531
+ lifecycleRpc(options.client, "eth_getTransactionByHash", [effectiveHash]),
532
+ ]);
533
+ if (receiptValue === null) return finish({ ...base, status: "pending", blockNumber: observedNumber, blockHash: observedHash });
534
+ if (transactionValue === null) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Receipt exists without its canonical transaction");
535
+ const receipt = rpcObject(receiptValue, "receipt");
536
+ const blockNumber = rpcQuantity(receipt.blockNumber, "receipt block number");
537
+ const blockHash = rpcHex(receipt.blockHash, "receipt block hash");
538
+ pins.push({ number: blockNumber, hash: blockHash });
539
+ const matches = await transactionMatchesLaunch(rpcObject(transactionValue, "receipt transaction"), options,
540
+ () => readIdentityBlock(blockNumber));
541
+ const confirmations = block.number >= blockNumber ? block.number - blockNumber + 1n : 0n;
542
+ const required = reference.confirmations ?? 1;
543
+ if (!Number.isSafeInteger(required) || required < 1) throw new LifecyclePlanningError("INVALID_CONFIRMATIONS", "Receipt confirmations must be a positive integer");
544
+ const observation = finish({
545
+ ...base, blockNumber, blockHash, confirmations,
546
+ status: confirmations < BigInt(required) ? "unconfirmed" : !matches ? "replacement-cancelled" :
547
+ rpcQuantity(receipt.status, "receipt status") !== 1n ? "reverted" : "confirmed",
548
+ });
549
+ return {
550
+ pins,
551
+ finish(headers) {
552
+ const result = observation.finish(headers);
553
+ if (result.status !== "reorged" && !matches && !base.replaced) throw new LifecyclePlanningError("RECEIPT_IDENTITY", "Receipt calldata does not belong to the exact committed launch");
554
+ return result;
555
+ },
556
+ };
557
+ } catch (error) {
558
+ if (error instanceof Error && error.name === "AbortError") throw error;
559
+ // A changed retained observation is a reorg even if the orphan's transaction
560
+ // or identity read is now unavailable. Otherwise preserve the original error.
561
+ return finish({ ...base, status: "pending" }, { error });
502
562
  }
503
- if (transactionValue === null) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Receipt exists without its canonical transaction");
504
- const transaction = rpcObject(transactionValue, "receipt transaction");
505
- const receiptBlock = await readLifecycleBlock(options.client, toHex(rpcQuantity(rpcObject(receiptValue, "receipt").blockNumber, "receipt block number")));
506
- const matches = await transactionMatchesLaunch(transaction, options, receiptBlock);
507
- if (!matches && !base.replaced) throw new LifecyclePlanningError("RECEIPT_IDENTITY", "Receipt calldata does not belong to the exact committed launch");
508
- const receipt = rpcObject(receiptValue, "receipt");
509
- const blockNumber = rpcQuantity(receipt.blockNumber, "receipt block number");
510
- const blockHash = rpcHex(receipt.blockHash, "receipt block hash");
511
- const canonicalBlockValue = await lifecycleRpc(options.client, "eth_getBlockByNumber", [toHex(blockNumber), false]);
512
- if (canonicalBlockValue === null || rpcHex(rpcObject(canonicalBlockValue, "receipt canonical block").hash, "receipt canonical hash").toLowerCase() !== blockHash.toLowerCase()) return { ...base, status: "reorged", blockNumber, blockHash };
513
- const confirmations = block.number >= blockNumber ? block.number - blockNumber + 1n : 0n;
514
- const required = reference.confirmations ?? 1;
515
- if (!Number.isSafeInteger(required) || required < 1) throw new LifecyclePlanningError("INVALID_CONFIRMATIONS", "Receipt confirmations must be a positive integer");
516
- if (confirmations < BigInt(required)) return { ...base, status: "unconfirmed", blockNumber, blockHash, confirmations };
517
- if (!matches) return { ...base, status: "replacement-cancelled", blockNumber, blockHash, confirmations };
518
- if (rpcQuantity(receipt.status, "receipt status") !== 1n) return { ...base, status: "reverted", blockNumber, blockHash, confirmations };
519
- return { ...base, status: "confirmed", blockNumber, blockHash, confirmations };
563
+ }
564
+
565
+ async function readReceipts(options: ReadLaunchProgressOptions, block: LifecycleBlock, confirmationDepth: number): Promise<LifecycleReceiptStatus[]> {
566
+ if (options.receipts === undefined || options.receipts.length === 0) return [];
567
+ const identityBlocks = new Map<bigint, Promise<LifecycleBlock>>();
568
+ const readIdentityBlock = (number: bigint) => {
569
+ let pending = identityBlocks.get(number);
570
+ if (pending === undefined) { pending = readLifecycleBlock(options.client, toHex(number)); identityBlocks.set(number, pending); }
571
+ return pending;
572
+ };
573
+ const observations = await Promise.all((options.receipts ?? []).map((reference) =>
574
+ readReceipt({ ...reference, confirmations: Math.max(reference.confirmations ?? 1, confirmationDepth) }, options, block, readIdentityBlock)));
575
+ const headers = new Map<bigint, Hex | undefined>();
576
+ for (const observation of observations) for (const pin of observation.pins) headers.set(pin.number, undefined);
577
+ // All asynchronous identity reads have settled. Equal observed/inclusion blocks
578
+ // and multiple receipts now share one fresh canonical header, never a stale cache.
579
+ await Promise.all([...headers.keys()].map(async (number) => {
580
+ const value = await lifecycleRpc(options.client, "eth_getBlockByNumber", [toHex(number), false]);
581
+ const hash = value === null ? undefined : rpcHex(rpcObject(value, "receipt canonical block").hash, "receipt canonical hash");
582
+ if (hash !== undefined && hash.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Receipt canonical block hash must have 32 bytes");
583
+ headers.set(number, hash);
584
+ }));
585
+ return observations.map((observation) => observation.finish(headers));
520
586
  }
521
587
 
522
588
  /** Reads confirmation-bound canonical state and receipt provenance; local step counters are never used. */
523
589
  export async function readLaunchProgress(options: ReadLaunchProgressOptions, pinnedBlock?: LifecycleBlock): Promise<CanonicalLaunchProgress> {
524
590
  if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
525
- const block = pinnedBlock ?? await readLifecycleBlock(options.client);
526
- return withLifecycleReadClient(options.client, (client) => readLaunchProgressAtBlock(options, client, block, pinnedBlock === undefined));
591
+ const planHash = options.planned.planHash, launchId = options.planned.launchId;
592
+ const result = await withLifecycleReadClient(options.client, async (client) =>
593
+ readLaunchProgressAtBlock(options, client, pinnedBlock ?? await readLifecycleBlock(client), pinnedBlock === undefined));
594
+ 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");
595
+ if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
596
+ return result;
527
597
  }
528
598
 
529
599
  async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, client: LifecycleRpcClient, block: LifecycleBlock, checkBlock: boolean): Promise<CanonicalLaunchProgress> {
@@ -542,10 +612,13 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
542
612
  assertActive();
543
613
  const chainId = chainIdValue;
544
614
  if (chainId !== planned.plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed launch");
545
- if (planned.account.toLowerCase() !== planned.plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
615
+ if (planned.ufg !== undefined) {
616
+ assertUfgLaunchAccount(planned.plan, planned.account);
617
+ if (hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG config differs from its finalized commitment");
618
+ } else if (planned.account.toLowerCase() !== planned.plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
546
619
  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");
547
620
  const [head, confirmedProgress, token, confirmedNonce, headNonce, pendingNonce] = await Promise.all([
548
- readLifecycleContract<LaunchProgressV1>(client, planned.plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [planned.launchId], block, planned.account),
621
+ readLifecycleHeadProgress(client, planned.plan, planned.account, block),
549
622
  confirmedNumber === block.number ? undefined : readLifecycleContract<LaunchProgressV1>(client, planned.plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [planned.launchId], confirmedBlock, planned.account),
550
623
  readLifecycleTokenAtBlock(client, planned.plan, block),
551
624
  lifecycleRpc(client, "eth_getTransactionCount", [planned.account, toHex(confirmedBlock.number)]),
@@ -556,16 +629,23 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
556
629
  const canonical = confirmedProgress ?? head;
557
630
  if (token.toLowerCase() !== planned.predictedToken.toLowerCase()) throw new LifecyclePlanningError("TOKEN_IDENTITY", "Predicted token changed for the committed token configuration");
558
631
  for (const progress of [canonical, head]) if (progress.phase !== LifecyclePhase.None) {
559
- if (progress.planHash.toLowerCase() !== planned.planHash.toLowerCase() || progress.launchId.toLowerCase() !== planned.launchId.toLowerCase() || progress.creator.toLowerCase() !== planned.account.toLowerCase() || progress.nonce !== planned.plan.nonce || progress.token.toLowerCase() !== token.toLowerCase() || progress.marketCount !== planned.plan.markets.length || progress.buyCount !== planned.plan.buys.length || progress.deadline !== planned.plan.deadline) throw new LifecyclePlanningError("PLAN_REPLAY", "This creator nonce is already bound to a different economic launch");
632
+ if (progress.planHash.toLowerCase() !== planned.planHash.toLowerCase() || progress.launchId.toLowerCase() !== planned.launchId.toLowerCase() || progress.creator.toLowerCase() !== planned.plan.creator.toLowerCase() || progress.nonce !== planned.plan.nonce || progress.token.toLowerCase() !== token.toLowerCase() || progress.marketCount !== planned.plan.markets.length || progress.buyCount !== planned.plan.buys.length || progress.deadline !== planned.plan.deadline) throw new LifecyclePlanningError("PLAN_REPLAY", "This creator nonce is already bound to a different economic launch");
560
633
  if (progress.mode !== (planned.mode === "atomic" ? LifecycleMode.Atomic : LifecycleMode.Staged)) throw new LifecyclePlanningError("MODE_MISMATCH", "Execution mode differs from the already recorded launch");
561
634
  if (progress.preparedMarkets > progress.marketCount || progress.phase === LifecyclePhase.Activating || (progress.phase === LifecyclePhase.Ready && progress.preparedMarkets !== progress.marketCount)) throw new LifecyclePlanningError("INVALID_PROGRESS", "Canonical progress violates the committed ordered state machine");
562
635
  }
636
+ if (planned.ufg !== undefined && head.phase !== LifecyclePhase.None) {
637
+ const [principal, configHash] = await Promise.all([
638
+ readLifecycleContract<Address>(client, UFG_LAUNCH_PROXY_ADDRESS, ufgLaunchProxyV1Abi, "principalOf", [planned.launchId], block),
639
+ readLifecycleContract<Hash>(client, UFG_LAUNCH_PROXY_ADDRESS, ufgLaunchProxyV1Abi, "launchConfigHash", [planned.launchId], block),
640
+ ]);
641
+ if (principal.toLowerCase() !== planned.account.toLowerCase() || configHash.toLowerCase() !== planned.ufg.configHash.toLowerCase()) throw new LifecyclePlanningError("PLAN_REPLAY", "UFG proxy principal or launch config differs from the stored launch");
642
+ }
563
643
  const confirmedAccountNonce = rpcQuantity(confirmedNonce, "confirmed account nonce");
564
644
  const headAccountNonce = headNonce === undefined ? confirmedAccountNonce : rpcQuantity(headNonce, "head account nonce");
565
645
  const pendingAccountNonce = rpcQuantity(pendingNonce, "pending account nonce");
566
646
  const progressParameters = [{ type: "tuple", components: launchProgressV1Components }] as const;
567
647
  const confirmationSafe = confirmedAccountNonce === headAccountNonce && pendingAccountNonce === headAccountNonce && keccak256(encodeAbiParameters(progressParameters, [canonical])) === keccak256(encodeAbiParameters(progressParameters, [head]));
568
- const receipts = await Promise.all((options.receipts ?? []).map((reference) => readReceipt({ ...reference, confirmations: Math.max(reference.confirmations ?? 1, confirmationDepth) }, { ...options, client }, block)));
648
+ const receipts = await readReceipts({ ...options, client }, block, confirmationDepth);
569
649
  assertActive();
570
650
  let markets: LifecycleMarketProgress[] = [];
571
651
  if (canonical.preparedMarkets > 0) {
@@ -1,7 +1,12 @@
1
- import { decodeFunctionResult, encodeFunctionData, isAddress, isHex, parseAbi, toHex, type Abi, type Address, type Hex } from "viem";
1
+ import { decodeFunctionResult, encodeFunctionData, isAddress, isHex, keccak256, multicall3Abi, parseAbi, toHex, type Abi, type Address, type Hex } from "viem";
2
2
  import { LifecyclePlanningError, type LifecycleBlock, type LifecycleDiagnosticEvent, type LifecycleDiagnosticListener, type LifecycleDiagnosticStage, type LifecycleLimitContext, type LifecycleLimitSource, type LifecycleLimits, type LifecycleRpcClient, type LifecycleSessionCache, type LifecycleTransaction, type ResolvedLifecycleLimits } from "./types.js";
3
3
  import { lifecycleRegistryAbi } from "./abi.js";
4
4
  import type { ProfileTopologyV1 } from "./schema.js";
5
+ import { robinhoodMainnet } from "../addresses.js";
6
+
7
+ // Reviewed chain4663 deployment, independently observed at block 98,136,655.
8
+ const multicall3CodeHash = "0xd5c15df687b16f2ff992fc8d767b4216323184a2bbc6ee2f9c398c318e770891";
9
+ type IndependentRead = { params: readonly unknown[]; resolve: (value: unknown) => void; reject: (failure: unknown) => void };
5
10
 
6
11
  const lifecycleReadConcurrency = 8;
7
12
  const diagnosticRpcMethods: Record<string, true> = {
@@ -67,6 +72,11 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
67
72
  private readonly concurrency: number;
68
73
  private diagnosticSequence = 0;
69
74
  private chainId?: Promise<bigint>;
75
+ private gasPrice?: Promise<unknown>;
76
+ private readonly immutableReads = new Map<string, Promise<unknown>>();
77
+ private readonly canonicalPins = new Map<string, { block: Pick<LifecycleBlock, "number" | "hash">; chainId?: bigint }>();
78
+ private readonly independentReads = new Map<string, { block: LifecycleBlock; reads: IndependentRead[] }>();
79
+ readonly finalChecks = new Set<() => void>();
70
80
  private readonly limitObservations = new Map<LifecycleLimitSource | undefined, Map<string, Promise<LifecycleLimitObservation>>>();
71
81
  private readonly session?: SessionLifecycleCache;
72
82
  private sessionGroup?: SessionReadGroup;
@@ -145,6 +155,103 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
145
155
  return this.chainId ??= this.run("eth_chainId", []).then((value) => rpcQuantity(value, "chain ID"));
146
156
  }
147
157
 
158
+ observeGasPrice(): Promise<unknown> {
159
+ this.assertOpen();
160
+ return this.gasPrice ??= this.run("eth_gasPrice", []);
161
+ }
162
+
163
+ immutableRead(key: string, work: () => Promise<unknown>): Promise<unknown> {
164
+ this.assertOpen();
165
+ let pending = this.immutableReads.get(key);
166
+ if (pending === undefined) {
167
+ pending = work();
168
+ this.immutableReads.set(key, pending);
169
+ const owned = pending;
170
+ void pending.catch(() => { if (this.immutableReads.get(key) === owned) this.immutableReads.delete(key); });
171
+ }
172
+ return pending.then((value) => { this.assertOpen(); return value; });
173
+ }
174
+
175
+ deferCanonicalCheck(block: Pick<LifecycleBlock, "number" | "hash">, chainId?: bigint): void {
176
+ this.assertOpen();
177
+ const key = block.number.toString();
178
+ const existing = this.canonicalPins.get(key);
179
+ if (existing !== undefined && (existing.block.hash.toLowerCase() !== block.hash.toLowerCase() ||
180
+ existing.chainId !== undefined && chainId !== undefined && existing.chainId !== chainId)) {
181
+ this.invalidateSession();
182
+ throw new LifecyclePlanningError("STATE_REORGED", "One lifecycle invocation observed incompatible canonical pins");
183
+ }
184
+ this.canonicalPins.set(key, { block: { ...block }, chainId: chainId ?? existing?.chainId });
185
+ }
186
+
187
+ async finalize(): Promise<void> {
188
+ this.assertOpen();
189
+ const pins = [...this.canonicalPins.values()];
190
+ if (pins.length === 0) return;
191
+ const [chainId, current] = await Promise.all([
192
+ pins.some((pin) => pin.chainId !== undefined) ? this.run("eth_chainId", []).then((value) => rpcQuantity(value, "chain ID")) : undefined,
193
+ Promise.all(pins.map((pin) => readLifecycleBlock(this, toHex(pin.block.number)))),
194
+ ]);
195
+ for (let index = 0; index < pins.length; index += 1) {
196
+ const pin = pins[index]!;
197
+ if (pin.chainId !== undefined && chainId !== pin.chainId) {
198
+ this.invalidateSession();
199
+ throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during the pinned operation");
200
+ }
201
+ if (current[index]!.hash.toLowerCase() !== pin.block.hash.toLowerCase()) {
202
+ this.invalidateSession();
203
+ throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch state changed during the operation; read canonical progress again");
204
+ }
205
+ }
206
+ for (const check of this.finalChecks) check();
207
+ }
208
+
209
+ independentRead(params: readonly unknown[], block: LifecycleBlock, staticRead: boolean): Promise<unknown> {
210
+ return this.read("eth_call", params, block, staticRead, () => new Promise((resolve, reject) => {
211
+ const key = `${block.number}:${block.hash.toLowerCase()}`;
212
+ let group = this.independentReads.get(key);
213
+ if (group === undefined) {
214
+ group = { block, reads: [] };
215
+ this.independentReads.set(key, group);
216
+ queueMicrotask(() => {
217
+ this.independentReads.delete(key);
218
+ void this.flushIndependentReads(group!);
219
+ });
220
+ }
221
+ group.reads.push({ params, resolve, reject });
222
+ }));
223
+ }
224
+
225
+ private async flushIndependentReads(group: { block: LifecycleBlock; reads: IndependentRead[] }): Promise<void> {
226
+ const { block, reads } = group;
227
+ try {
228
+ // Two cold reads cannot beat two direct calls after code attestation.
229
+ if (reads.length < 3 || await this.observeChainId() !== 4663n) {
230
+ await Promise.all(reads.map(async (read) => {
231
+ try { read.resolve(await this.run("eth_call", read.params)); }
232
+ catch (failure) { read.reject(failure); }
233
+ }));
234
+ return;
235
+ }
236
+ const to = robinhoodMainnet.contracts.multicall3.address;
237
+ const code = rpcHex(await this.read("eth_getCode", [to, toHex(block.number)], block, true), "Multicall3 code");
238
+ if (keccak256(code) !== multicall3CodeHash) throw new LifecyclePlanningError("MULTICALL_CODE_MISMATCH", "Pinned Multicall3 runtime differs from the reviewed chain4663 deployment");
239
+ const calls = reads.map((read) => {
240
+ const request = read.params[0] as { to: Address; data: Hex };
241
+ return { target: request.to, allowFailure: true, callData: request.data };
242
+ });
243
+ const data = encodeFunctionData({ abi: multicall3Abi, functionName: "aggregate3", args: [calls] });
244
+ const raw = rpcHex(await this.run("eth_call", [{ to, data }, toHex(block.number)]), "Multicall3 result");
245
+ const results = decodeFunctionResult({ abi: multicall3Abi, functionName: "aggregate3", data: raw });
246
+ if (results.length !== reads.length) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Multicall3 omitted independent pinned results");
247
+ for (let index = 0; index < reads.length; index += 1) {
248
+ const result = results[index]!;
249
+ if (result.success) reads[index]!.resolve(result.returnData);
250
+ else reads[index]!.reject(new LifecyclePlanningError("MULTICALL_READ_FAILED", "An independent pinned getter reverted"));
251
+ }
252
+ } catch (failure) { for (const read of reads) read.reject(failure); }
253
+ }
254
+
148
255
  observeLimits(context: LifecycleLimitContext, source?: LifecycleLimitSource): Promise<LifecycleLimitObservation> {
149
256
  this.assertOpen();
150
257
  // A caller callback is live policy, not an immutable pinned observation.
@@ -168,7 +275,7 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
168
275
  return observation.then((result) => { this.assertOpen(); result.assertCurrent(); return result; });
169
276
  }
170
277
 
171
- read(method: string, params: readonly unknown[], block: LifecycleBlock, staticRead = false): Promise<unknown> {
278
+ read(method: string, params: readonly unknown[], block: LifecycleBlock, staticRead = false, load?: () => Promise<unknown>): Promise<unknown> {
172
279
  this.assertOpen();
173
280
  // Source identity is this scope's original client. The complete wire call includes
174
281
  // caller, value, gas and any other call fields; no selector-only/ABI-name aliasing.
@@ -189,7 +296,7 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
189
296
  pending = Promise.resolve(cached);
190
297
  } else {
191
298
  if (staticRead && this.session !== undefined) this.sessionMisses += 1;
192
- pending = this.run(method, params);
299
+ pending = load === undefined ? this.run(method, params) : load();
193
300
  if (group !== undefined) pending = pending.then((value) => {
194
301
  // Never share in-flight work or install late results after invalidation/closure.
195
302
  // Only wire hex is retained, so public decoded DTOs cannot mutate authority.
@@ -207,6 +314,11 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
207
314
  this.reads.clear();
208
315
  this.chainId = undefined;
209
316
  this.limitObservations.clear();
317
+ this.gasPrice = undefined;
318
+ this.immutableReads.clear();
319
+ this.canonicalPins.clear();
320
+ this.independentReads.clear();
321
+ this.finalChecks.clear();
210
322
  const listener = this.source.onDiagnostic;
211
323
  if (this.session !== undefined && listener !== undefined) emitLifecycleDiagnostic(listener, {
212
324
  stage: "session-cache", phase: success ? "success" : "failure",
@@ -221,7 +333,15 @@ export async function withLifecycleReadClient<T>(source: LifecycleRpcClient, wor
221
333
  const client = new InvocationLifecycleClient(source);
222
334
  let success = false;
223
335
  try {
224
- const result = await work(client);
336
+ let result: T;
337
+ try { result = await work(client); }
338
+ catch (failure) {
339
+ // Refused simulations still report a changed canonical source, not a
340
+ // misleading economic refusal derived from an orphaned observation.
341
+ await client.finalize();
342
+ throw failure;
343
+ }
344
+ await client.finalize();
225
345
  success = true;
226
346
  return result;
227
347
  } finally { client.close(success); }
@@ -285,6 +405,28 @@ export function readLifecycleChainId(client: LifecycleRpcClient): Promise<bigint
285
405
  ? client.observeChainId()
286
406
  : lifecycleRpc(client, "eth_chainId").then((value) => rpcQuantity(value, "chain ID"));
287
407
  }
408
+
409
+ /** A single operation-local recommendation, never a session or cross-head quote. */
410
+ export function readLifecycleGasPrice(client: LifecycleRpcClient): Promise<unknown> {
411
+ return client instanceof InvocationLifecycleClient ? client.observeGasPrice() : lifecycleRpc(client, "eth_gasPrice");
412
+ }
413
+
414
+ /** Only the reviewed immutable factory identity may use an invocation-wide key. */
415
+ export function readLifecycleImmutable<T>(client: LifecycleRpcClient, key: string, work: () => Promise<T>): Promise<T> {
416
+ return (client instanceof InvocationLifecycleClient ? client.immutableRead(key, work) : work()) as Promise<T>;
417
+ }
418
+
419
+ /** Recheck live policy after the owner's final asynchronous canonical guards. */
420
+ export function registerLifecycleFinalCheck(client: LifecycleRpcClient, check: () => void): void {
421
+ if (client instanceof InvocationLifecycleClient) { client.assertOpen(); client.finalChecks.add(check); }
422
+ }
423
+
424
+ /** SDK-selected caller-independent getters only; never poster quotes or execution. */
425
+ function lifecycleIndependentRpc(client: LifecycleRpcClient, params: readonly unknown[], block: LifecycleBlock, staticRead: boolean): Promise<unknown> {
426
+ return client instanceof InvocationLifecycleClient
427
+ ? client.independentRead(params, block, staticRead)
428
+ : lifecyclePinnedRpc(client, "eth_call", params, block, staticRead);
429
+ }
288
430
  export function rpcObject(value: unknown, label: string): Record<string, unknown> {
289
431
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", `${label} must be an object`);
290
432
  // The object guard establishes the RPC record boundary; fields remain untrusted unknowns.
@@ -325,7 +467,11 @@ export async function readLifecycleBlock(client: LifecycleRpcClient, tag: Hex |
325
467
  if (client instanceof InvocationLifecycleClient) client.observeBlock(block, tag === "latest");
326
468
  return block;
327
469
  }
328
- export async function assertLifecycleBlock(client: LifecycleRpcClient, block: LifecycleBlock, expectedChainId?: bigint): Promise<void> {
470
+ export async function assertLifecycleBlock(client: LifecycleRpcClient, block: Pick<LifecycleBlock, "number" | "hash">, expectedChainId?: bigint): Promise<void> {
471
+ if (client instanceof InvocationLifecycleClient) {
472
+ client.deferCanonicalCheck(block, expectedChainId);
473
+ return;
474
+ }
329
475
  const [chainId, current] = await Promise.all([
330
476
  expectedChainId === undefined ? undefined : lifecycleRpc(client, "eth_chainId"),
331
477
  readLifecycleBlock(client, toHex(block.number)),
@@ -339,13 +485,15 @@ export async function assertLifecycleBlock(client: LifecycleRpcClient, block: Li
339
485
  throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch state changed during the operation; read canonical progress again");
340
486
  }
341
487
  }
342
- export async function readLifecycleContract<T>(client: LifecycleRpcClient, to: Address, abi: Abi, functionName: string, args: readonly unknown[], block: LifecycleBlock, from?: Address, staticRead = false): Promise<T> {
488
+ export async function readLifecycleContract<T>(client: LifecycleRpcClient, to: Address, abi: Abi, functionName: string, args: readonly unknown[], block: LifecycleBlock, from?: Address, staticRead = false, independent = false): Promise<T> {
343
489
  const data = encodeFunctionData({ abi, functionName, args });
344
490
  const request = [{ to, from, data }, toHex(block.number)];
345
491
  const getter = abi.some((item) => item.type === "function" && item.name === functionName && (item.stateMutability === "view" || item.stateMutability === "pure"));
346
- const result = rpcHex(await (getter && functionName !== "readLaunchProgress"
347
- ? staticRead ? lifecycleStaticRpc(client, "eth_call", request, block) : lifecyclePinnedRpc(client, "eth_call", request, block)
348
- : lifecycleRpc(client, "eth_call", request)), functionName);
492
+ const result = rpcHex(await (independent
493
+ ? lifecycleIndependentRpc(client, request, block, staticRead)
494
+ : getter
495
+ ? staticRead ? lifecycleStaticRpc(client, "eth_call", request, block) : lifecyclePinnedRpc(client, "eth_call", request, block)
496
+ : lifecycleRpc(client, "eth_call", request)), functionName);
349
497
  // ABI decoding validates scalar widths and tuple layout; T names that exact frozen ABI result.
350
498
  const decoded = decodeFunctionResult({ abi, functionName, data: result }) as T;
351
499
  return decoded;
@@ -382,7 +530,7 @@ export async function readNitroPosterGas(transaction: LifecycleTransaction, cont
382
530
 
383
531
  async function readNitroScalar(context: LifecycleLimitContext, to: Address, abi: Abi, functionName: string): Promise<bigint> {
384
532
  const data = encodeFunctionData({ abi, functionName });
385
- const result = rpcHex(await lifecycleStaticRpc(context.client, "eth_call", [{ to, data }, toHex(context.block.number)], context.block), functionName);
533
+ const result = rpcHex(await lifecycleIndependentRpc(context.client, [{ to, data }, toHex(context.block.number)], context.block, true), functionName);
386
534
  if (result.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", `${functionName} must return exactly one ABI word`);
387
535
  return decodeFunctionResult({ abi, functionName, data: result }) as bigint;
388
536
  }