@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.
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +13 -4
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +80 -71
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts +3 -0
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +43 -10
- package/dist/lifecycle/simulation.js.map +1 -1
- package/docs/launch-lifecycle.md +27 -4
- package/package.json +1 -1
- package/src/lifecycle/planner.ts +13 -4
- package/src/lifecycle/progress.ts +67 -58
- package/src/lifecycle/simulation.ts +46 -10
|
@@ -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
|
|
145
|
-
|
|
146
|
-
|
|
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 =
|
|
395
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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");
|