@black-market/sdk 0.6.2 → 0.6.4

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.
@@ -1,5 +1,5 @@
1
1
  import { concatHex, decodeFunctionData, encodeAbiParameters, encodeFunctionData, keccak256, stringToHex, toHex, zeroAddress, zeroHash, type Address, type Hash, type Hex } from "viem";
2
- import { abyssLifecycleAdapterAbi, launchLifecycleAbi, lifecycleAdapterAbi, lifecycleDirectoryAbi, lifecycleErc20Abi, lifecycleOracleFactoryAbi, lifecycleRegistryAbi, poolFeeCollectorFactoryV1Abi, lifecycleV4HookAbi, lifecycleV4LockerAbi, poolHookDeployerV1Abi, sharedHookDeployerV1Abi, fixedFeePoolHookV1Abi, poolMarketAdapterV1Abi, sharedMarketAdapterV1Abi } from "./abi.js";
2
+ import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleDirectoryAbi, lifecycleErc20Abi, lifecycleOracleFactoryAbi, lifecycleRegistryAbi, poolFeeCollectorFactoryV1Abi, lifecycleV4HookAbi, lifecycleV4LockerAbi, poolHookDeployerV1Abi, sharedHookDeployerV1Abi, fixedFeePoolHookV1Abi, poolMarketAdapterV1Abi, sharedMarketAdapterV1Abi } from "./abi.js";
3
3
  import { decodePoolBoundV4LifecycleMarketConfig, encodePoolBoundHookParameters, hasLifecycleV4HookPermissions, hashPoolBoundV4MarketCommitment, poolBoundHookInitCodeHash, predictPoolBoundHookAddress, validateV4LifecycleMarket, V4_LIFECYCLE_CONFIG_SCHEMA, V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA, type PoolBoundHookParametersV1 } from "./markets.js";
4
4
  import { hashLaunchIdentity, hashLaunchPlan, hashLaunchBounds, hashLaunchDependencies, hashLifecycleProfile, launchProgressV1Components, LifecycleMode, LifecyclePhase, LifecycleVenue, LIFECYCLE_REQUIRED_CAPABILITIES, LIFECYCLE_ERC404_CAPABILITY, LIFECYCLE_MULTI_POSITION_CAPABILITY, type AdapterRegistrationV1, type LaunchPlanV1, type LaunchProgressV1, type MarketIdentityV1, type MarketLiveStateV1, type PreparedMarketV1, type ProfileRegistrationV1, type ProfileTopologyV1, type LaunchEnvelopeV2, type LifecycleDeveloperTerms } from "./schema.js";
5
5
  import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleContract, readLifecycleProfileTopology, rpcHex, rpcObject, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
@@ -29,24 +29,16 @@ async function certifyLifecycleProfile(client: LifecycleRpcClient, orchestrator:
29
29
  if (!shared && !bound && !abyss) throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${registration.configSchema}`);
30
30
  const version = shared ? 4 : bound ? 5 : 1;
31
31
  if (adapter.configVersion !== version || topology.configVersion !== version || topology.hookTopology !== (shared ? 1 : bound ? 2 : 0)) throw new LifecyclePlanningError("UNCERTIFIED_TOPOLOGY", "Registry topology/version differs from the supported config schema");
32
- const [implementationCode, authority, digest] = await Promise.all([
33
- readDependencyCode(client, adapter.implementation, block),
34
- readLifecycleContract<Address>(client, adapter.implementation, lifecycleAdapterAbi, "core", [], block),
35
- readLifecycleContract<Hex>(client, adapter.implementation, lifecycleAdapterAbi, "dependencyDigest", [], block),
36
- ]);
37
- if (keccak256(implementationCode).toLowerCase() !== adapter.codeHash.toLowerCase()) throw new LifecyclePlanningError("PROFILE_CODE_HASH", "Adapter code hash differs from immutable registry approval");
38
- if (authority.toLowerCase() !== orchestrator.toLowerCase() || digest.toLowerCase() !== registration.dependencyDigest.toLowerCase()) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Adapter authority or dependency digest differs from immutable registry approval");
39
32
  if (abyss) {
40
33
  if (topology.hookDeployer !== zeroAddress || topology.hookCreationCodeHash !== zeroHash) throw new LifecyclePlanningError("UNCERTIFIED_TOPOLOGY", "Abyss does not admit a V4 hook deployment graph");
41
- const [factory, schema, actualVersion] = await Promise.all([
42
- readLifecycleContract<Address>(client, adapter.implementation, abyssLifecycleAdapterAbi, "factory", [], block),
43
- readLifecycleContract<Hex>(client, adapter.implementation, abyssLifecycleAdapterAbi, "CONFIG_SCHEMA", [], block),
44
- readLifecycleContract<number>(client, adapter.implementation, abyssLifecycleAdapterAbi, "CONFIG_VERSION", [], block),
45
- ]);
34
+ // certifyAbyss fixes schema/version/factory at registration; requireEligible
35
+ // rechecks that approved runtime and the live graph digest at this block.
36
+ const factory = registration.factory;
46
37
  const domain = keccak256(stringToHex("BLACK_MARKET_ABYSS_CANONICAL_PROFILE_V1"));
47
38
  const known = [0, 1, 2, 3].some((variant) => keccak256(encodeAbiParameters([{ type: "bytes32" }, { type: "uint256" }, { type: "address" }, { type: "uint8" }], [domain, chainId, factory, variant])).toLowerCase() === id.toLowerCase());
48
- if (!known || schema.toLowerCase() !== ABYSS_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() || actualVersion !== 1 || registration.factory.toLowerCase() !== factory.toLowerCase() || registration.venue.toLowerCase() !== factory.toLowerCase() || registration.hook !== zeroAddress) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Abyss profile is not the exact canonical factory/variant/schema binding");
49
- await readDependencyCode(client, factory, block);
39
+ if (!known || registration.venue.toLowerCase() !== factory.toLowerCase() || registration.hook !== zeroAddress) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Abyss profile is not the exact canonical factory/variant/schema binding");
40
+ const eligible = await readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "requireEligible", [registration.adapterId, id, version, LIFECYCLE_REQUIRED_CAPABILITIES], block);
41
+ if (eligible.toLowerCase() !== adapter.implementation.toLowerCase()) throw new LifecyclePlanningError("INELIGIBLE_PROFILE", "Registry refuses this profile for pending source admission");
50
42
  return { topology };
51
43
  }
52
44
  if (envelope === undefined || envelope.topology !== topology.hookTopology || envelope.configVersion !== version || envelope.economicVersion !== 3 ||
@@ -64,14 +56,6 @@ async function certifyLifecycleProfile(client: LifecycleRpcClient, orchestrator:
64
56
  b.maximumOracleCardinality < 2 || b.maximumOracleCardinality > 4096 || b.feeModeFlags === 0 || (b.feeModeFlags & ~3) !== 0 ||
65
57
  envelope.protocolTreasury === zeroAddress || (envelope.protocolFeeDenominator !== 0 && (envelope.protocolFeeDenominator < 4 || envelope.protocolFeeDenominator > 10))) throw new LifecyclePlanningError("PROFILE_BOUNDS", "Registry envelope has unsupported executable bounds");
66
58
  const abi = bound ? poolMarketAdapterV1Abi : sharedMarketAdapterV1Abi;
67
- const [metadata, actualVersion] = await Promise.all([
68
- Promise.all((["PROFILE_ID", "CONFIG_SCHEMA", "implementationRegistry"] as const).map((getter) => readLifecycleContract<Hex>(client, adapter.implementation, abi, getter, [], block))),
69
- readLifecycleContract<number>(client, adapter.implementation, abi, "CONFIG_VERSION", [], block),
70
- ]);
71
- for (const [index, expected] of [id, registration.configSchema, registry].entries()) {
72
- if (metadata[index]?.toLowerCase() !== expected.toLowerCase()) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Reviewed adapter metadata differs from registry certification");
73
- }
74
- if (actualVersion !== version) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Reviewed adapter config version differs from certification");
75
59
  const g = envelope.graph;
76
60
  if (registration.factory !== zeroAddress || registration.venue.toLowerCase() !== g.manager.toLowerCase() || registration.hook.toLowerCase() !== g.hookRoot.toLowerCase() ||
77
61
  topology.hookDeployer.toLowerCase() !== g.hookDeployer.toLowerCase() || topology.hookCreationCodeHash.toLowerCase() !== g.hookCreationCodeHash.toLowerCase()) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Reviewed venue/topology differs from the frozen envelope graph");
@@ -79,7 +63,12 @@ async function certifyLifecycleProfile(client: LifecycleRpcClient, orchestrator:
79
63
  const codeBindings = [[orchestrator, g.coreCodeHash], [g.manager, g.managerCodeHash], [g.oracleFactory, g.oracleFactoryCodeHash],
80
64
  [g.locker, g.lockerCodeHash], [g.collectorFactory, g.collectorFactoryCodeHash], [g.collectorDeployer, g.collectorDeployerCodeHash],
81
65
  [g.hookDeployer, g.hookDeployerCodeHash]] as const;
82
- const [dependencies, dependencyCodes, collectorDeployer, launcher, lockerManager, creationCodeHash, chunk0, chunk1] = await Promise.all([
66
+ // Frozen graph addresses permit independent evidence reads. Getter results
67
+ // still have to match that graph; none of these observations is caller authority.
68
+ const [eligible, metadata, actualVersion, dependencies, dependencyCodes, collectorDeployer, launcher, lockerManager, creationCodeHash, chunk0, chunk1, chunkCodes, sharedRoot] = await Promise.all([
69
+ readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "requireEligible", [registration.adapterId, id, version, LIFECYCLE_REQUIRED_CAPABILITIES], block),
70
+ Promise.all((["PROFILE_ID", "CONFIG_SCHEMA", "implementationRegistry"] as const).map((getter) => readLifecycleContract<Hex>(client, adapter.implementation, abi, getter, [], block))),
71
+ readLifecycleContract<number>(client, adapter.implementation, abi, "CONFIG_VERSION", [], block),
83
72
  Promise.all((["poolManager", "hookRoot", "oracleFactory", "locker", "collectorFactory", "hookDeployer"] as const).map((getter) => readLifecycleContract<Address>(client, adapter.implementation, abi, getter, [], block))),
84
73
  Promise.all(codeBindings.map(([address]) => readDependencyCode(client, address, block))),
85
74
  readLifecycleContract<Address>(client, g.collectorFactory, poolFeeCollectorFactoryV1Abi, "collectorDeployer", [], block),
@@ -88,7 +77,24 @@ async function certifyLifecycleProfile(client: LifecycleRpcClient, orchestrator:
88
77
  readLifecycleContract<Hex>(client, g.hookDeployer, deployerAbi, "creationCodeHash", [], block),
89
78
  readLifecycleContract<Address>(client, g.hookDeployer, deployerAbi, "codeChunk0", [], block),
90
79
  readLifecycleContract<Address>(client, g.hookDeployer, deployerAbi, "codeChunk1", [], block),
80
+ Promise.all([
81
+ readDependencyCode(client, g.codeChunk0, block),
82
+ g.codeChunk1 === zeroAddress ? Promise.resolve<Hex>("0x") : readDependencyCode(client, g.codeChunk1, block),
83
+ ]),
84
+ shared ? Promise.all([
85
+ readDependencyCode(client, g.hookRoot, block),
86
+ readLifecycleContract<Hex>(client, g.hookDeployer, deployerAbi, "deployedCodeHash", [g.hookRoot], block),
87
+ readLifecycleContract<Address>(client, g.hookDeployer, sharedHookDeployerV1Abi, "predict", [g.manager, adapter.implementation, g.oracleFactory, g.sharedHookSalt], block),
88
+ Promise.all((["registrar", "poolManager", "oracleFactory"] as const).map((getter) => readLifecycleContract<Address>(client, g.hookRoot, lifecycleV4HookAbi, getter, [], block))),
89
+ readLifecycleContract<bigint>(client, g.hookRoot, lifecycleV4HookAbi, "REQUIRED_HOOK_FLAGS", [], block),
90
+ readLifecycleContract<bigint>(client, g.hookRoot, lifecycleV4HookAbi, "ALL_HOOK_MASK", [], block),
91
+ ]) : undefined,
91
92
  ]);
93
+ if (eligible.toLowerCase() !== adapter.implementation.toLowerCase()) throw new LifecyclePlanningError("INELIGIBLE_PROFILE", "Registry refuses this profile for pending source admission");
94
+ for (const [index, expected] of [id, registration.configSchema, registry].entries()) {
95
+ if (metadata[index]?.toLowerCase() !== expected.toLowerCase()) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Reviewed adapter metadata differs from registry certification");
96
+ }
97
+ if (actualVersion !== version) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Reviewed adapter config version differs from certification");
92
98
  for (const [index, expected] of [g.manager, g.hookRoot, g.oracleFactory, g.locker, g.collectorFactory, g.hookDeployer].entries()) {
93
99
  if (dependencies[index]?.toLowerCase() !== expected.toLowerCase()) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Live adapter dependency differs from the frozen reviewed graph");
94
100
  }
@@ -97,10 +103,7 @@ async function certifyLifecycleProfile(client: LifecycleRpcClient, orchestrator:
97
103
  }
98
104
  if (collectorDeployer.toLowerCase() !== g.collectorDeployer.toLowerCase() || launcher.toLowerCase() !== adapter.implementation.toLowerCase() ||
99
105
  lockerManager.toLowerCase() !== g.manager.toLowerCase() || g.locker.toLowerCase() === g.manager.toLowerCase() || g.locker.toLowerCase() === g.hookRoot.toLowerCase()) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Reviewed collector or locker authority differs from the frozen graph");
100
- const [chunk0Code, chunk1Code] = await Promise.all([
101
- readDependencyCode(client, chunk0, block),
102
- chunk1 === zeroAddress ? Promise.resolve<Hex>("0x") : readDependencyCode(client, chunk1, block),
103
- ]);
106
+ const [chunk0Code, chunk1Code] = chunkCodes;
104
107
  if (chunk0.toLowerCase() !== g.codeChunk0.toLowerCase() || chunk1.toLowerCase() !== g.codeChunk1.toLowerCase() || chunk0.toLowerCase() === chunk1.toLowerCase() ||
105
108
  !chunk0Code.startsWith("0x00") || chunk0Code.length <= 4 || (chunk0Code.length - 2) / 2 > 24576 ||
106
109
  (chunk1 !== zeroAddress && (!chunk1Code.startsWith("0x00") || chunk1Code.length <= 4 || (chunk1Code.length - 2) / 2 > 24576 || (chunk0Code.length - 2) / 2 !== 24576)) ||
@@ -108,16 +111,9 @@ async function certifyLifecycleProfile(client: LifecycleRpcClient, orchestrator:
108
111
  const creationCode = concatHex([`0x${chunk0Code.slice(4)}`, chunk1 === zeroAddress ? "0x" : `0x${chunk1Code.slice(4)}`]);
109
112
  if ((creationCode.length - 2) / 2 + (shared ? 96 : 576) > 49152 || creationCodeHash === zeroHash ||
110
113
  creationCodeHash.toLowerCase() !== g.hookCreationCodeHash.toLowerCase() || keccak256(creationCode).toLowerCase() !== creationCodeHash.toLowerCase() ||
111
- hashLaunchDependencies({ chainId, core: orchestrator, registry, registrar: adapter.implementation, graph: g }).toLowerCase() !== digest.toLowerCase()) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Reviewed creation artifact, constructor bound or graph digest differs from admission");
112
- if (shared) {
113
- const [rootCode, recordedHash, remotePrediction, dependencies, requiredFlags, allFlags] = await Promise.all([
114
- readDependencyCode(client, g.hookRoot, block),
115
- readLifecycleContract<Hex>(client, g.hookDeployer, deployerAbi, "deployedCodeHash", [g.hookRoot], block),
116
- readLifecycleContract<Address>(client, g.hookDeployer, sharedHookDeployerV1Abi, "predict", [g.manager, adapter.implementation, g.oracleFactory, g.sharedHookSalt], block),
117
- Promise.all((["registrar", "poolManager", "oracleFactory"] as const).map((getter) => readLifecycleContract<Address>(client, g.hookRoot, lifecycleV4HookAbi, getter, [], block))),
118
- readLifecycleContract<bigint>(client, g.hookRoot, lifecycleV4HookAbi, "REQUIRED_HOOK_FLAGS", [], block),
119
- readLifecycleContract<bigint>(client, g.hookRoot, lifecycleV4HookAbi, "ALL_HOOK_MASK", [], block),
120
- ]);
114
+ hashLaunchDependencies({ chainId, core: orchestrator, registry, registrar: adapter.implementation, graph: g }).toLowerCase() !== registration.dependencyDigest.toLowerCase()) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Reviewed creation artifact, constructor bound or graph digest differs from admission");
115
+ if (sharedRoot !== undefined) {
116
+ const [rootCode, recordedHash, remotePrediction, dependencies, requiredFlags, allFlags] = sharedRoot;
121
117
  const initCodeHash = keccak256(concatHex([creationCode, encodeAbiParameters([{ type: "address" }, { type: "address" }, { type: "address" }], [g.manager, adapter.implementation, g.oracleFactory])]));
122
118
  const prediction = predictPoolBoundHookAddress({ deployer: g.hookDeployer, initCodeHash, salt: g.sharedHookSalt });
123
119
  if (recordedHash === zeroHash || recordedHash.toLowerCase() !== g.hookRuntimeCodeHash.toLowerCase() || keccak256(rootCode).toLowerCase() !== recordedHash.toLowerCase() ||
@@ -145,60 +141,69 @@ export async function validateV4LifecycleOracle(options: {
145
141
 
146
142
  export async function readLifecycleProfiles(options: { client: LifecycleRpcClient; orchestrator: Address; profileIds?: readonly Hex[]; offset?: bigint; limit?: bigint }, pinnedBlock?: LifecycleBlock): Promise<LifecycleProfile[]> {
147
143
  return withLifecycleReadClient(options.client, (client) => lifecycleStage(client, "profile.certification", async () => {
148
- const [block, chainIdValue] = await Promise.all([
149
- pinnedBlock ?? readLifecycleBlock(client),
150
- lifecycleRpc(client, "eth_chainId"),
151
- ]);
152
- const chainId = rpcQuantity(chainIdValue, "chain ID");
153
- const registry = await readLifecycleContract<Address>(client, options.orchestrator, launchLifecycleAbi, "registry", [], block);
154
- const offset = options.offset ?? 0n; const limit = options.limit ?? 100n;
155
- if (offset < 0n || limit < 0n || limit > 100n) throw new LifecyclePlanningError("INVALID_PROFILE_PAGE", "Registry profile pages require unsigned offset and limit at most 100");
156
- const [authority, ids] = await Promise.all([
157
- readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "core", [], block),
158
- options.profileIds ?? readLifecycleContract<readonly Hex[]>(client, registry, lifecycleRegistryAbi, "profileIds", [offset, limit], block),
159
- ]);
160
- if (authority.toLowerCase() !== options.orchestrator.toLowerCase()) throw new LifecyclePlanningError("REGISTRY_BINDING", "Registry is not bound to the selected lifecycle orchestrator");
161
- let protocolMaximumDeveloperFeeBpsPromise: Promise<number> | undefined;
162
- const readProtocolMaximumDeveloperFeeBps = () => protocolMaximumDeveloperFeeBpsPromise ??=
163
- readLifecycleContract<number>(client, registry, lifecycleRegistryAbi, "protocolMaximumDeveloperFeeBps", [], block);
164
- const profiles = await Promise.all(ids.map(async (id): Promise<LifecycleProfile> => {
165
- const registration = await readLifecycleContract<ProfileRegistrationV1>(client, registry, lifecycleRegistryAbi, "profile", [id], block);
166
- const adapter = await readLifecycleContract<AdapterRegistrationV1>(client, registry, lifecycleRegistryAbi, "adapter", [registration.adapterId], block);
167
- const schema = registration.configSchema.toLowerCase();
168
- const venueKind = schema === V4_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() || schema === V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? "uniswap-v4" : schema === ABYSS_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? "abyss" : "unknown";
169
- let topology: ProfileTopologyV1 = { hookTopology: 0, configVersion: adapter.configVersion, hookDeployer: zeroAddress, hookCreationCodeHash: zeroHash };
170
- let envelope: LaunchEnvelopeV2 | undefined; let developerTerms: LifecycleDeveloperTerms | undefined;
171
- let protocolMaximumDeveloperFeeBps: number | undefined;
172
- let admitted = false; let reason: string | undefined;
173
- try {
174
- if (venueKind === "unknown") throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${registration.configSchema}`);
175
- const [recordedTopology, reviewed] = await Promise.all([
176
- readLifecycleProfileTopology(client, registry, id, block),
177
- venueKind === "uniswap-v4" ? Promise.all([
178
- readLifecycleContract<LaunchEnvelopeV2>(client, registry, lifecycleRegistryAbi, "profileEnvelope", [id], block),
179
- readLifecycleContract<readonly [Address, Address, number, Hex, boolean]>(client, registry, lifecycleRegistryAbi, "developerTerms", [id], block),
180
- readProtocolMaximumDeveloperFeeBps(),
181
- ]) : undefined,
144
+ const chainIdPromise = lifecycleRpc(client, "eth_chainId").then((value) => rpcQuantity(value, "chain ID"));
145
+ // Block-bound discovery does not depend on chain identity. Join both branches
146
+ // immediately so a failed chain read cannot become detached or a profile refusal.
147
+ const [chainId, { block, profiles }] = await Promise.all([
148
+ chainIdPromise,
149
+ (async () => {
150
+ const block = pinnedBlock ?? await readLifecycleBlock(client);
151
+ const registry = await readLifecycleContract<Address>(client, options.orchestrator, launchLifecycleAbi, "registry", [], block);
152
+ const offset = options.offset ?? 0n; const limit = options.limit ?? 100n;
153
+ if (offset < 0n || limit < 0n || limit > 100n) throw new LifecyclePlanningError("INVALID_PROFILE_PAGE", "Registry profile pages require unsigned offset and limit at most 100");
154
+ let protocolMaximumDeveloperFeeBpsPromise: Promise<number> | undefined;
155
+ const readProtocolMaximumDeveloperFeeBps = () => protocolMaximumDeveloperFeeBpsPromise ??=
156
+ readLifecycleContract<number>(client, registry, lifecycleRegistryAbi, "protocolMaximumDeveloperFeeBps", [], block);
157
+ const readProfile = async (id: Hex): Promise<LifecycleProfile> => {
158
+ const registration = await readLifecycleContract<ProfileRegistrationV1>(client, registry, lifecycleRegistryAbi, "profile", [id], block);
159
+ const schema = registration.configSchema.toLowerCase();
160
+ const venueKind = schema === V4_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() || schema === V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? "uniswap-v4" : schema === ABYSS_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? "abyss" : "unknown";
161
+ const [adapter, certification] = await Promise.all([
162
+ readLifecycleContract<AdapterRegistrationV1>(client, registry, lifecycleRegistryAbi, "adapter", [registration.adapterId], block),
163
+ venueKind === "unknown" ? undefined : Promise.all([
164
+ readLifecycleProfileTopology(client, registry, id, block),
165
+ venueKind === "uniswap-v4" ? Promise.all([
166
+ readLifecycleContract<LaunchEnvelopeV2>(client, registry, lifecycleRegistryAbi, "profileEnvelope", [id], block),
167
+ readLifecycleContract<readonly [Address, Address, number, Hex, boolean]>(client, registry, lifecycleRegistryAbi, "developerTerms", [id], block),
168
+ readProtocolMaximumDeveloperFeeBps(),
169
+ ]) : undefined,
170
+ ]).then((values) => ({ values }), (failure: unknown) => ({ failure })),
171
+ ]);
172
+ const chainId = await chainIdPromise;
173
+ let topology: ProfileTopologyV1 = { hookTopology: 0, configVersion: adapter.configVersion, hookDeployer: zeroAddress, hookCreationCodeHash: zeroHash };
174
+ let envelope: LaunchEnvelopeV2 | undefined; let developerTerms: LifecycleDeveloperTerms | undefined;
175
+ let protocolMaximumDeveloperFeeBps: number | undefined;
176
+ let admitted = false; let reason: string | undefined;
177
+ try {
178
+ if (certification === undefined) throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${registration.configSchema}`);
179
+ if ("failure" in certification) throw certification.failure;
180
+ const [recordedTopology, reviewed] = certification.values;
181
+ topology = recordedTopology;
182
+ if (reviewed !== undefined) {
183
+ envelope = reviewed[0];
184
+ protocolMaximumDeveloperFeeBps = reviewed[2];
185
+ const [implementation, beneficiary, maximumDeveloperFeeBps, termsDigest, enabled] = reviewed[1];
186
+ developerTerms = { adapter: implementation, beneficiary, maximumDeveloperFeeBps, termsDigest, enabled };
187
+ if (implementation.toLowerCase() !== adapter.implementation.toLowerCase() || beneficiary.toLowerCase() !== envelope.beneficiary.toLowerCase() ||
188
+ termsDigest.toLowerCase() !== envelope.termsDigest.toLowerCase() || maximumDeveloperFeeBps !== envelope.maximumDeveloperFeeBps ||
189
+ maximumDeveloperFeeBps > reviewed[2]) throw new LifecyclePlanningError("PROFILE_TERMS", "Registry developer terms differ from the frozen reviewed envelope");
190
+ }
191
+ await certifyLifecycleProfile(client, options.orchestrator, registry, id, registration, adapter, topology, envelope, block, chainId);
192
+ if (developerTerms !== undefined && !developerTerms.enabled) throw new LifecyclePlanningError("INELIGIBLE_PROFILE", "Registry refuses this profile for pending source admission");
193
+ admitted = true;
194
+ } catch (failure) { reason = failure instanceof Error ? failure.message : String(failure); }
195
+ return { id, registration, adapter, topology, envelope, developerTerms, protocolMaximumDeveloperFeeBps: venueKind === "uniswap-v4" ? protocolMaximumDeveloperFeeBps : undefined, admitted, reason, venueKind };
196
+ };
197
+ const [authority, profiles] = await Promise.all([
198
+ readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "core", [], block),
199
+ Promise.resolve(options.profileIds ?? readLifecycleContract<readonly Hex[]>(client, registry, lifecycleRegistryAbi, "profileIds", [offset, limit], block)).then((ids) => Promise.all(ids.map(readProfile))),
182
200
  ]);
183
- topology = recordedTopology;
184
- if (reviewed !== undefined) {
185
- envelope = reviewed[0];
186
- protocolMaximumDeveloperFeeBps = reviewed[2];
187
- const [implementation, beneficiary, maximumDeveloperFeeBps, termsDigest, enabled] = reviewed[1];
188
- developerTerms = { adapter: implementation, beneficiary, maximumDeveloperFeeBps, termsDigest, enabled };
189
- if (implementation.toLowerCase() !== adapter.implementation.toLowerCase() || beneficiary.toLowerCase() !== envelope.beneficiary.toLowerCase() ||
190
- termsDigest.toLowerCase() !== envelope.termsDigest.toLowerCase() || maximumDeveloperFeeBps !== envelope.maximumDeveloperFeeBps ||
191
- maximumDeveloperFeeBps > reviewed[2]) throw new LifecyclePlanningError("PROFILE_TERMS", "Registry developer terms differ from the frozen reviewed envelope");
192
- }
193
- await certifyLifecycleProfile(client, options.orchestrator, registry, id, registration, adapter, topology, envelope, block, chainId);
194
- const eligible = await readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "requireEligible", [registration.adapterId, id, topology.configVersion, LIFECYCLE_REQUIRED_CAPABILITIES], block);
195
- if (eligible.toLowerCase() !== adapter.implementation.toLowerCase() || (developerTerms !== undefined && !developerTerms.enabled)) throw new LifecyclePlanningError("INELIGIBLE_PROFILE", "Registry refuses this profile for pending source admission");
196
- admitted = true;
197
- } catch (failure) { reason = failure instanceof Error ? failure.message : String(failure); }
198
- return { id, registration, adapter, topology, envelope, developerTerms, protocolMaximumDeveloperFeeBps: venueKind === "uniswap-v4" ? protocolMaximumDeveloperFeeBps : undefined, admitted, reason, venueKind };
199
- }));
200
- // A required V4 cap read still rejects the invocation on failure; Abyss has no developer-fee terms.
201
- if (protocolMaximumDeveloperFeeBpsPromise !== undefined) await protocolMaximumDeveloperFeeBpsPromise;
201
+ if (authority.toLowerCase() !== options.orchestrator.toLowerCase()) throw new LifecyclePlanningError("REGISTRY_BINDING", "Registry is not bound to the selected lifecycle orchestrator");
202
+ // A required V4 cap read still rejects the invocation on failure; Abyss has no developer-fee terms.
203
+ if (protocolMaximumDeveloperFeeBpsPromise !== undefined) await protocolMaximumDeveloperFeeBpsPromise;
204
+ return { block, profiles };
205
+ })(),
206
+ ]);
202
207
  if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, chainId);
203
208
  return profiles;
204
209
  }));
@@ -392,8 +397,11 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
392
397
  const confirmationDepth = options.confirmations ?? planned.confirmations;
393
398
  if (!Number.isSafeInteger(confirmationDepth) || confirmationDepth < 1) throw new LifecyclePlanningError("INVALID_CONFIRMATIONS", "Confirmation depth must be a positive integer");
394
399
  const confirmedNumber = block.number >= BigInt(confirmationDepth - 1) ? block.number - BigInt(confirmationDepth - 1) : 0n;
395
- const confirmedBlock = confirmedNumber === block.number ? block : await readLifecycleBlock(client, toHex(confirmedNumber));
396
- const chainId = rpcQuantity(await lifecycleRpc(client, "eth_chainId"), "chain ID");
400
+ const [confirmedBlock, chainIdValue] = await Promise.all([
401
+ confirmedNumber === block.number ? block : readLifecycleBlock(client, toHex(confirmedNumber)),
402
+ lifecycleRpc(client, "eth_chainId"),
403
+ ]);
404
+ const chainId = rpcQuantity(chainIdValue, "chain ID");
397
405
  if (chainId !== planned.plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed launch");
398
406
  if (planned.account.toLowerCase() !== planned.plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
399
407
  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");
@@ -2,7 +2,7 @@ import { decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctio
2
2
  import { launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
3
3
  import { LifecyclePhase, type LaunchProgressV1, type LaunchReceiptV1 } from "./schema.js";
4
4
  import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readNitroPosterGas, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity } from "./rpc.js";
5
- import { LifecyclePlanningError, type ControlledLifecycleFork, type LifecycleBlock, type LifecyclePostcondition, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleSimulationStep, type LifecycleTransaction, type PlannedLaunch, type ResolvedLifecycleLimits, type SimulateLaunchPlanOptions } from "./types.js";
5
+ import { LifecyclePlanningError, type ControlledLifecycleFork, type LifecycleBlock, type LifecycleLimitContext, type LifecycleLimits, type LifecyclePostcondition, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleSimulationStep, type LifecycleTransaction, type PlannedLaunch, type ResolvedLifecycleLimits, type SimulateLaunchPlanOptions } from "./types.js";
6
6
 
7
7
  function conditionRequest(condition: LifecyclePostcondition, planned: PlannedLaunch): { to: Hex; data: Hex } {
8
8
  if (condition.kind === "allowance") return {
@@ -55,7 +55,8 @@ async function proveNitroBackend(client: LifecycleRpcClient, planned: PlannedLau
55
55
  throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Simulated ArbOS version/compute getters differ from the exact pinned chain context");
56
56
  }
57
57
  }
58
- await assertLifecycleBlock(client, block, planned.chainId);
58
+ // The enclosing simulation rechecks chain and canonical hash after its proofs
59
+ // (including every refusal path); a separate probe-only recheck is redundant.
59
60
  }
60
61
 
61
62
  async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunch, block: LifecycleBlock, transactions: readonly LifecycleTransaction[], gasLimits: readonly bigint[], limits: ResolvedLifecycleLimits, gasPrice: bigint, validation: boolean): Promise<LifecycleSimulationStep[]> {
@@ -232,18 +233,54 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
232
233
  }
233
234
  }
234
235
 
235
- export async function simulateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], pinnedBlock?: LifecycleBlock): Promise<LifecycleSimulation> {
236
+ /** Internal one-shot continuation. Only SDK reads can produce its context; callers
237
+ * cannot supply resolved policy or account/chain observations as authority. */
238
+ export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true): (transactions: readonly LifecycleTransaction[]) => Promise<LifecycleSimulation> {
236
239
  const { client, planned } = options;
237
240
  const sourceClient = lifecycleSourceClient(client);
238
- if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === sourceClient) throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
239
- const block = pinnedBlock ?? await readLifecycleBlock(client);
241
+ const blockNumber = block.number, blockHash = block.hash;
240
242
  const context = { client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId };
241
- const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = await lifecycleStage(client, "simulation.context", () => Promise.all([
242
- resolveLifecycleLimits(context, options.limits === undefined ? planned.limits : options.limits),
243
- lifecycleRpc(client, "eth_chainId"),
244
- lifecyclePinnedRpc(client, "eth_getCode", [planned.account, toHex(block.number)], block),
245
- transactions.length === 0 ? undefined : lifecycleRpc(client, "eth_gasPrice"),
246
- ]));
243
+ const pending = (async () => {
244
+ if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === sourceClient) throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
245
+ return lifecycleStage(client, "simulation.context", () => Promise.all([
246
+ resolveLifecycleLimits(context, options.limits === undefined ? planned.limits : options.limits),
247
+ lifecycleRpc(client, "eth_chainId"),
248
+ lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(block.number)], block),
249
+ hasTransactions ? lifecycleRpc(client, "eth_gasPrice") : undefined,
250
+ ]));
251
+ })().then((observations) => ({ observations }), (failure: unknown) => ({ failure }));
252
+ // Attach the failure handler before input validation can refuse or throw.
253
+ // Preparation is read-only; no probe, measurement, replay or fork work starts.
254
+ let consumed = false;
255
+ return async (transactions) => {
256
+ if (consumed) throw new LifecyclePlanningError("INVALID_SIMULATION", "Prepared simulation context is invocation-local and may only be consumed once");
257
+ consumed = true;
258
+ const result = await pending;
259
+ if ("failure" in result) throw result.failure;
260
+ if (options.client !== client || options.planned !== planned || context.client !== client || context.block !== block || lifecycleSourceClient(client) !== sourceClient ||
261
+ planned.account !== context.account || planned.plan.orchestrator !== context.orchestrator || planned.chainId !== context.chainId ||
262
+ block.number !== blockNumber || block.hash !== blockHash) throw new LifecyclePlanningError("LIMIT_CONTEXT_MISMATCH", "Prepared simulation context differs from the bound source/chain/account/core/block");
263
+ return simulateLaunchTransactionsWithContext(options, transactions, context, result.observations);
264
+ };
265
+ }
266
+
267
+ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], pinnedBlock?: LifecycleBlock): Promise<LifecycleSimulation> {
268
+ // Standalone calls always produce fresh context, including live chain and fees.
269
+ if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === lifecycleSourceClient(options.client)) throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
270
+ const block = pinnedBlock ?? await readLifecycleBlock(options.client);
271
+ return prepareLaunchSimulation(options, block, transactions.length !== 0)(transactions);
272
+ }
273
+
274
+ async function simulateLaunchTransactionsWithContext(
275
+ options: SimulateLaunchPlanOptions,
276
+ transactions: readonly LifecycleTransaction[],
277
+ context: LifecycleLimitContext,
278
+ observations: [{ limits: ResolvedLifecycleLimits; configuration: LifecycleLimits }, unknown, unknown, unknown],
279
+ ): Promise<LifecycleSimulation> {
280
+ const { client, planned } = options;
281
+ const sourceClient = lifecycleSourceClient(client);
282
+ const { block } = context;
283
+ const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = observations;
247
284
  const base = { confidence: "stateful" as const, executionProof: "failed" as const, protocolFit: "unknown" as const, transportPreflight: "not-requested" as const, blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits };
248
285
  if (rpcQuantity(chainId, "chain ID") !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
249
286
  const accountCode = rpcHex(accountCodeValue, "account code");
@@ -256,21 +293,31 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
256
293
  const gasPrice = rpcQuantity(gasPriceValue, "gas price");
257
294
  if (gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
258
295
  const buffer = (gas: bigint) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
296
+ const computeCeiling = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
297
+ const capGas = transactions.map(() => computeCeiling);
298
+ let parallelMeasurement: Promise<{ steps: LifecycleSimulationStep[] } | { failure: unknown }> | undefined;
259
299
  const posterGas: bigint[] = [];
260
300
  if (limits.protocol === "nitro") {
301
+ // The isolated capability probe, poster quote and permissive cap measurement
302
+ // have no data dependency. Only an actually batching source overlaps all three.
303
+ const nitroEvidence = Promise.all([
304
+ lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, block, limits, gasPrice)),
305
+ lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
306
+ ]);
307
+ if (sourceClient.supportsReadBatching === true) {
308
+ // Handle settlement immediately so a failed measurement cannot escape while
309
+ // the independent metering evidence is still pending. It is never admission.
310
+ parallelMeasurement = lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false))
311
+ .then((steps) => ({ steps }), (failure: unknown) => ({ failure }));
312
+ }
261
313
  try {
262
- const [, estimates] = await Promise.all([
263
- lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, block, limits, gasPrice)),
264
- lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
265
- ]);
314
+ const [, estimates] = await nitroEvidence;
266
315
  posterGas.push(...estimates);
267
316
  } catch (failure) {
268
317
  await assertLifecycleBlock(client, block, planned.chainId);
269
318
  return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Native Nitro compute/poster proof unavailable: ${failure instanceof Error ? failure.message : String(failure)}. Generic Anvil/Hardhat forks cannot prove ArbOS metering.` };
270
319
  }
271
320
  }
272
- const computeCeiling = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
273
- const capGas = transactions.map(() => computeCeiling);
274
321
  // The poster quote allocates gas only. The native exact replay enforces actual
275
322
  // compute hold; subtracting this approximate budget from gasUsed would not.
276
323
  const discoveryGas = transactions.map((_, index) => {
@@ -282,7 +329,14 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
282
329
  let backend: "eth_simulateV1" | "controlled-fork" = "eth_simulateV1";
283
330
  let measured: LifecycleSimulationStep[];
284
331
  // Permissive cap measurement is not proof; only the exact-gas validated replay admits a plan.
285
- try { measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false)); }
332
+ try {
333
+ if (parallelMeasurement === undefined) measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false));
334
+ else {
335
+ const observation = await parallelMeasurement;
336
+ if ("failure" in observation) throw observation.failure;
337
+ measured = observation.steps;
338
+ }
339
+ }
286
340
  catch (failure) {
287
341
  if (options.fork === undefined || limits.protocol === "nitro") {
288
342
  await assertLifecycleBlock(client, block, planned.chainId);