@black-market/sdk 0.13.0 → 0.14.1

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 (53) hide show
  1. package/README.md +4 -4
  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 +70 -62
  16. package/dist/lifecycle/planner.js.map +1 -1
  17. package/dist/lifecycle/presets.d.ts +5 -0
  18. package/dist/lifecycle/presets.d.ts.map +1 -1
  19. package/dist/lifecycle/presets.js +5 -0
  20. package/dist/lifecycle/presets.js.map +1 -1
  21. package/dist/lifecycle/progress.d.ts +48 -0
  22. package/dist/lifecycle/progress.d.ts.map +1 -1
  23. package/dist/lifecycle/progress.js +31 -20
  24. package/dist/lifecycle/progress.js.map +1 -1
  25. package/dist/lifecycle/rpc.d.ts +6 -2
  26. package/dist/lifecycle/rpc.d.ts.map +1 -1
  27. package/dist/lifecycle/rpc.js +47 -44
  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 +130 -109
  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 +15 -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 +42 -20
  41. package/docs/reference.md +43 -6
  42. package/examples/README.md +2 -2
  43. package/package.json +1 -1
  44. package/src/lifecycle/index.ts +5 -1
  45. package/src/lifecycle/lens.ts +42 -0
  46. package/src/lifecycle/mining.ts +80 -0
  47. package/src/lifecycle/planner.ts +72 -58
  48. package/src/lifecycle/presets.ts +10 -0
  49. package/src/lifecycle/progress.ts +26 -16
  50. package/src/lifecycle/rpc.ts +45 -43
  51. package/src/lifecycle/simulation.ts +126 -100
  52. package/src/lifecycle/snapshot.ts +72 -0
  53. package/src/lifecycle/types.ts +15 -5
@@ -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, readLifecycleImmutable, 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" }],
@@ -39,7 +40,6 @@ export async function predictLifecycleToken(options: { client: LifecycleRpcClien
39
40
  return withLifecycleReadClient(options.client, async (client) => {
40
41
  const block = await readLifecycleBlock(client);
41
42
  const token = await readLifecycleTokenAtBlock(client, options.plan, block);
42
- await assertLifecycleBlock(client, block, options.plan.chainId);
43
43
  return token;
44
44
  });
45
45
  }
@@ -236,7 +236,7 @@ export async function readLifecycleProfiles(options: { client: LifecycleRpcClien
236
236
  const chainIdPromise = readLifecycleChainId(client);
237
237
  // Block-bound discovery does not depend on chain identity. Join both branches
238
238
  // immediately so a failed chain read cannot become detached or a profile refusal.
239
- const [chainId, { block, profiles }] = await Promise.all([
239
+ const [, { profiles }] = await Promise.all([
240
240
  chainIdPromise,
241
241
  (async () => {
242
242
  const block = pinnedBlock ?? await readLifecycleBlock(client);
@@ -302,7 +302,6 @@ export async function readLifecycleProfiles(options: { client: LifecycleRpcClien
302
302
  return { block, profiles };
303
303
  })(),
304
304
  ]);
305
- if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, chainId);
306
305
  return profiles;
307
306
  }));
308
307
  }
@@ -431,7 +430,6 @@ export async function readPoolBoundHookDeployment(options: { client: LifecycleRp
431
430
  const result = await withLifecycleReadClient(options.client, async (client) => {
432
431
  const { deployment } = await readPoolBoundHookDeploymentDetails({ ...options, client }, block, commitment);
433
432
  if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while reading hook deployment metadata");
434
- if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, options.plan.chainId);
435
433
  return { ...deployment };
436
434
  });
437
435
  if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during the final hook deployment guard");
@@ -446,7 +444,6 @@ export async function buildPoolBoundHookDeploymentTransaction(options: { client:
446
444
  const { deployment, parameters, configVersion } = await readPoolBoundHookDeploymentDetails({ ...options, client }, block, commitment);
447
445
  if (!hasLifecycleV4HookPermissions(deployment.predictedHook)) throw new LifecyclePlanningError("INVALID_HOOK_BITS", "Finalize a salt with the exact lifecycle hook permissions before predeployment");
448
446
  if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while constructing hook deployment calldata");
449
- await assertLifecycleBlock(client, block, options.plan.chainId);
450
447
  const abi = configVersion === 5 ? poolHookDeployerV1Abi : poolHookDeployerConfigV6Abi;
451
448
  return { to: deployment.deployer, data: encodeFunctionData({ abi, functionName: "deploy", args: [parameters, deployment.salt] }), value: 0n, deployment: { ...deployment } };
452
449
  });
@@ -566,6 +563,7 @@ async function readReceipts(options: ReadLaunchProgressOptions, block: Lifecycle
566
563
  if (options.receipts === undefined || options.receipts.length === 0) return [];
567
564
  const identityBlocks = new Map<bigint, Promise<LifecycleBlock>>();
568
565
  const readIdentityBlock = (number: bigint) => {
566
+ if (number === block.number) return Promise.resolve(block);
569
567
  let pending = identityBlocks.get(number);
570
568
  if (pending === undefined) { pending = readLifecycleBlock(options.client, toHex(number)); identityBlocks.set(number, pending); }
571
569
  return pending;
@@ -574,9 +572,10 @@ async function readReceipts(options: ReadLaunchProgressOptions, block: Lifecycle
574
572
  readReceipt({ ...reference, confirmations: Math.max(reference.confirmations ?? 1, confirmationDepth) }, options, block, readIdentityBlock)));
575
573
  const headers = new Map<bigint, Hex | undefined>();
576
574
  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.
575
+ // Reuse this invocation's authoritative L2 head header. Older receipt blocks
576
+ // share one canonical read after their asynchronous identity observations.
579
577
  await Promise.all([...headers.keys()].map(async (number) => {
578
+ if (number === block.number) { headers.set(number, block.hash); return; }
580
579
  const value = await lifecycleRpc(options.client, "eth_getBlockByNumber", [toHex(number), false]);
581
580
  const hash = value === null ? undefined : rpcHex(rpcObject(value, "receipt canonical block").hash, "receipt canonical hash");
582
581
  if (hash !== undefined && hash.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Receipt canonical block hash must have 32 bytes");
@@ -590,13 +589,19 @@ export async function readLaunchProgress(options: ReadLaunchProgressOptions, pin
590
589
  if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
591
590
  const planHash = options.planned.planHash, launchId = options.planned.launchId;
592
591
  const result = await withLifecycleReadClient(options.client, async (client) =>
593
- readLaunchProgressAtBlock(options, client, pinnedBlock ?? await readLifecycleBlock(client), pinnedBlock === undefined));
592
+ readLaunchProgressAtBlock(options, client, pinnedBlock ?? await readLifecycleBlock(client)));
594
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");
595
594
  if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
596
595
  return result;
597
596
  }
598
597
 
599
- 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> {
600
605
  const { planned } = options;
601
606
  const assertActive = () => {
602
607
  if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
@@ -648,13 +653,20 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
648
653
  const receipts = await readReceipts({ ...options, client }, block, confirmationDepth);
649
654
  assertActive();
650
655
  let markets: LifecycleMarketProgress[] = [];
651
- 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) {
652
664
  const directory = await readLifecycleContract<Address>(client, planned.plan.orchestrator, launchLifecycleAbi, "directory", [], confirmedBlock);
653
665
  markets = await Promise.all(Array.from({ length: canonical.preparedMarkets }, async (_, index): Promise<LifecycleMarketProgress> => {
654
- 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);
655
667
  validateLifecycleMarketIdentity(prepared.identity, planned.plan, token, index);
656
668
  try {
657
- 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);
658
670
  return { index, prepared, live };
659
671
  } catch (failure) {
660
672
  if (failure instanceof Error && failure.name === "AbortError") throw failure;
@@ -662,8 +674,6 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
662
674
  }
663
675
  }));
664
676
  }
665
- if (checkBlock) await assertLifecycleBlock(client, block, chainId);
666
- if (confirmedNumber !== block.number) await assertLifecycleBlock(client, confirmedBlock, chainId);
667
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");
668
678
  assertActive();
669
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 };
@@ -6,6 +6,8 @@ import { robinhoodMainnet } from "../addresses.js";
6
6
 
7
7
  // Reviewed chain4663 deployment, independently observed at block 98,136,655.
8
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;
9
11
  type IndependentRead = { params: readonly unknown[]; resolve: (value: unknown) => void; reject: (failure: unknown) => void };
10
12
 
11
13
  const lifecycleReadConcurrency = 8;
@@ -74,7 +76,6 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
74
76
  private chainId?: Promise<bigint>;
75
77
  private gasPrice?: Promise<unknown>;
76
78
  private readonly immutableReads = new Map<string, Promise<unknown>>();
77
- private readonly canonicalPins = new Map<string, { block: Pick<LifecycleBlock, "number" | "hash">; chainId?: bigint }>();
78
79
  private readonly independentReads = new Map<string, { block: LifecycleBlock; reads: IndependentRead[] }>();
79
80
  readonly finalChecks = new Set<() => void>();
80
81
  private readonly limitObservations = new Map<LifecycleLimitSource | undefined, Map<string, Promise<LifecycleLimitObservation>>>();
@@ -172,37 +173,9 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
172
173
  return pending.then((value) => { this.assertOpen(); return value; });
173
174
  }
174
175
 
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
176
 
187
177
  async finalize(): Promise<void> {
188
178
  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
179
  for (const check of this.finalChecks) check();
207
180
  }
208
181
 
@@ -225,8 +198,7 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
225
198
  private async flushIndependentReads(group: { block: LifecycleBlock; reads: IndependentRead[] }): Promise<void> {
226
199
  const { block, reads } = group;
227
200
  try {
228
- // Two cold reads cannot beat two direct calls after code attestation.
229
- if (reads.length < 3 || await this.observeChainId() !== 4663n) {
201
+ if (await this.observeChainId() !== 4663n) {
230
202
  await Promise.all(reads.map(async (read) => {
231
203
  try { read.resolve(await this.run("eth_call", read.params)); }
232
204
  catch (failure) { read.reject(failure); }
@@ -234,8 +206,7 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
234
206
  return;
235
207
  }
236
208
  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");
209
+ await attestLifecycleMulticall(this, block);
239
210
  const calls = reads.map((read) => {
240
211
  const request = read.params[0] as { to: Address; data: Hex };
241
212
  return { target: request.to, allowFailure: true, callData: request.data };
@@ -279,7 +250,7 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
279
250
  this.assertOpen();
280
251
  // Source identity is this scope's original client. The complete wire call includes
281
252
  // caller, value, gas and any other call fields; no selector-only/ABI-name aliasing.
282
- 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);
283
254
  const existing = this.reads.get(key);
284
255
  if (existing !== undefined) {
285
256
  if (staticRead && this.session !== undefined) this.sessionReuses += 1;
@@ -309,6 +280,12 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
309
280
  return pending;
310
281
  }
311
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
+
312
289
  close(success: boolean): void {
313
290
  this.closed = true;
314
291
  this.reads.clear();
@@ -316,7 +293,6 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
316
293
  this.limitObservations.clear();
317
294
  this.gasPrice = undefined;
318
295
  this.immutableReads.clear();
319
- this.canonicalPins.clear();
320
296
  this.independentReads.clear();
321
297
  this.finalChecks.clear();
322
298
  const listener = this.source.onDiagnostic;
@@ -336,8 +312,7 @@ export async function withLifecycleReadClient<T>(source: LifecycleRpcClient, wor
336
312
  let result: T;
337
313
  try { result = await work(client); }
338
314
  catch (failure) {
339
- // Refused simulations still report a changed canonical source, not a
340
- // misleading economic refusal derived from an orphaned observation.
315
+ // Live policy is rechecked even when exact execution or a source read refuses.
341
316
  await client.finalize();
342
317
  throw failure;
343
318
  }
@@ -398,8 +373,7 @@ export function lifecycleStaticRpc(client: LifecycleRpcClient, method: "eth_call
398
373
  export async function lifecycleRpc(client: LifecycleRpcClient, method: string, params: readonly unknown[] = []): Promise<unknown> {
399
374
  return client instanceof InvocationLifecycleClient ? client.run(method, params) : client.request({ method, params });
400
375
  }
401
- /** One invocation's observed identity. End-of-operation and submission checks
402
- * 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. */
403
377
  export function readLifecycleChainId(client: LifecycleRpcClient): Promise<bigint> {
404
378
  return client instanceof InvocationLifecycleClient
405
379
  ? client.observeChainId()
@@ -416,6 +390,37 @@ export function readLifecycleImmutable<T>(client: LifecycleRpcClient, key: strin
416
390
  return (client instanceof InvocationLifecycleClient ? client.immutableRead(key, work) : work()) as Promise<T>;
417
391
  }
418
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
+
419
424
  /** Recheck live policy after the owner's final asynchronous canonical guards. */
420
425
  export function registerLifecycleFinalCheck(client: LifecycleRpcClient, check: () => void): void {
421
426
  if (client instanceof InvocationLifecycleClient) { client.assertOpen(); client.finalChecks.add(check); }
@@ -468,10 +473,7 @@ export async function readLifecycleBlock(client: LifecycleRpcClient, tag: Hex |
468
473
  return block;
469
474
  }
470
475
  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
- }
476
+ if (client instanceof InvocationLifecycleClient) return;
475
477
  const [chainId, current] = await Promise.all([
476
478
  expectedChainId === undefined ? undefined : lifecycleRpc(client, "eth_chainId"),
477
479
  readLifecycleBlock(client, toHex(block.number)),