@black-market/sdk 0.6.3 → 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.
@@ -141,63 +141,69 @@ export async function validateV4LifecycleOracle(options: {
141
141
 
142
142
  export async function readLifecycleProfiles(options: { client: LifecycleRpcClient; orchestrator: Address; profileIds?: readonly Hex[]; offset?: bigint; limit?: bigint }, pinnedBlock?: LifecycleBlock): Promise<LifecycleProfile[]> {
143
143
  return withLifecycleReadClient(options.client, (client) => lifecycleStage(client, "profile.certification", async () => {
144
- const [block, chainIdValue] = await Promise.all([
145
- pinnedBlock ?? readLifecycleBlock(client),
146
- lifecycleRpc(client, "eth_chainId"),
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))),
200
+ ]);
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
+ })(),
147
206
  ]);
148
- const chainId = rpcQuantity(chainIdValue, "chain ID");
149
- const registry = await readLifecycleContract<Address>(client, options.orchestrator, launchLifecycleAbi, "registry", [], block);
150
- const offset = options.offset ?? 0n; const limit = options.limit ?? 100n;
151
- if (offset < 0n || limit < 0n || limit > 100n) throw new LifecyclePlanningError("INVALID_PROFILE_PAGE", "Registry profile pages require unsigned offset and limit at most 100");
152
- let protocolMaximumDeveloperFeeBpsPromise: Promise<number> | undefined;
153
- const readProtocolMaximumDeveloperFeeBps = () => protocolMaximumDeveloperFeeBpsPromise ??=
154
- readLifecycleContract<number>(client, registry, lifecycleRegistryAbi, "protocolMaximumDeveloperFeeBps", [], block);
155
- const readProfile = async (id: Hex): Promise<LifecycleProfile> => {
156
- const registration = await readLifecycleContract<ProfileRegistrationV1>(client, registry, lifecycleRegistryAbi, "profile", [id], block);
157
- const schema = registration.configSchema.toLowerCase();
158
- const venueKind = schema === V4_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() || schema === V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? "uniswap-v4" : schema === ABYSS_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? "abyss" : "unknown";
159
- const [adapter, certification] = await Promise.all([
160
- readLifecycleContract<AdapterRegistrationV1>(client, registry, lifecycleRegistryAbi, "adapter", [registration.adapterId], block),
161
- venueKind === "unknown" ? undefined : Promise.all([
162
- readLifecycleProfileTopology(client, registry, id, block),
163
- venueKind === "uniswap-v4" ? Promise.all([
164
- readLifecycleContract<LaunchEnvelopeV2>(client, registry, lifecycleRegistryAbi, "profileEnvelope", [id], block),
165
- readLifecycleContract<readonly [Address, Address, number, Hex, boolean]>(client, registry, lifecycleRegistryAbi, "developerTerms", [id], block),
166
- readProtocolMaximumDeveloperFeeBps(),
167
- ]) : undefined,
168
- ]).then((values) => ({ values }), (failure: unknown) => ({ failure })),
169
- ]);
170
- let topology: ProfileTopologyV1 = { hookTopology: 0, configVersion: adapter.configVersion, hookDeployer: zeroAddress, hookCreationCodeHash: zeroHash };
171
- let envelope: LaunchEnvelopeV2 | undefined; let developerTerms: LifecycleDeveloperTerms | undefined;
172
- let protocolMaximumDeveloperFeeBps: number | undefined;
173
- let admitted = false; let reason: string | undefined;
174
- try {
175
- if (certification === undefined) throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${registration.configSchema}`);
176
- if ("failure" in certification) throw certification.failure;
177
- const [recordedTopology, reviewed] = certification.values;
178
- topology = recordedTopology;
179
- if (reviewed !== undefined) {
180
- envelope = reviewed[0];
181
- protocolMaximumDeveloperFeeBps = reviewed[2];
182
- const [implementation, beneficiary, maximumDeveloperFeeBps, termsDigest, enabled] = reviewed[1];
183
- developerTerms = { adapter: implementation, beneficiary, maximumDeveloperFeeBps, termsDigest, enabled };
184
- if (implementation.toLowerCase() !== adapter.implementation.toLowerCase() || beneficiary.toLowerCase() !== envelope.beneficiary.toLowerCase() ||
185
- termsDigest.toLowerCase() !== envelope.termsDigest.toLowerCase() || maximumDeveloperFeeBps !== envelope.maximumDeveloperFeeBps ||
186
- maximumDeveloperFeeBps > reviewed[2]) throw new LifecyclePlanningError("PROFILE_TERMS", "Registry developer terms differ from the frozen reviewed envelope");
187
- }
188
- await certifyLifecycleProfile(client, options.orchestrator, registry, id, registration, adapter, topology, envelope, block, chainId);
189
- if (developerTerms !== undefined && !developerTerms.enabled) throw new LifecyclePlanningError("INELIGIBLE_PROFILE", "Registry refuses this profile for pending source admission");
190
- admitted = true;
191
- } catch (failure) { reason = failure instanceof Error ? failure.message : String(failure); }
192
- return { id, registration, adapter, topology, envelope, developerTerms, protocolMaximumDeveloperFeeBps: venueKind === "uniswap-v4" ? protocolMaximumDeveloperFeeBps : undefined, admitted, reason, venueKind };
193
- };
194
- const [authority, profiles] = await Promise.all([
195
- readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "core", [], block),
196
- Promise.resolve(options.profileIds ?? readLifecycleContract<readonly Hex[]>(client, registry, lifecycleRegistryAbi, "profileIds", [offset, limit], block)).then((ids) => Promise.all(ids.map(readProfile))),
197
- ]);
198
- if (authority.toLowerCase() !== options.orchestrator.toLowerCase()) throw new LifecyclePlanningError("REGISTRY_BINDING", "Registry is not bound to the selected lifecycle orchestrator");
199
- // A required V4 cap read still rejects the invocation on failure; Abyss has no developer-fee terms.
200
- if (protocolMaximumDeveloperFeeBpsPromise !== undefined) await protocolMaximumDeveloperFeeBpsPromise;
201
207
  if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, chainId);
202
208
  return profiles;
203
209
  }));
@@ -391,8 +397,11 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
391
397
  const confirmationDepth = options.confirmations ?? planned.confirmations;
392
398
  if (!Number.isSafeInteger(confirmationDepth) || confirmationDepth < 1) throw new LifecyclePlanningError("INVALID_CONFIRMATIONS", "Confirmation depth must be a positive integer");
393
399
  const confirmedNumber = block.number >= BigInt(confirmationDepth - 1) ? block.number - BigInt(confirmationDepth - 1) : 0n;
394
- const confirmedBlock = confirmedNumber === block.number ? block : await readLifecycleBlock(client, toHex(confirmedNumber));
395
- 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");
396
405
  if (chainId !== planned.plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed launch");
397
406
  if (planned.account.toLowerCase() !== planned.plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
398
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 {
@@ -233,18 +233,54 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
233
233
  }
234
234
  }
235
235
 
236
- 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> {
237
239
  const { client, planned } = options;
238
240
  const sourceClient = lifecycleSourceClient(client);
239
- if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === sourceClient) throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
240
- const block = pinnedBlock ?? await readLifecycleBlock(client);
241
+ const blockNumber = block.number, blockHash = block.hash;
241
242
  const context = { client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId };
242
- const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = await lifecycleStage(client, "simulation.context", () => Promise.all([
243
- resolveLifecycleLimits(context, options.limits === undefined ? planned.limits : options.limits),
244
- lifecycleRpc(client, "eth_chainId"),
245
- lifecyclePinnedRpc(client, "eth_getCode", [planned.account, toHex(block.number)], block),
246
- transactions.length === 0 ? undefined : lifecycleRpc(client, "eth_gasPrice"),
247
- ]));
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;
248
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 };
249
285
  if (rpcQuantity(chainId, "chain ID") !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
250
286
  const accountCode = rpcHex(accountCodeValue, "account code");