@black-market/sdk 0.6.1 → 0.6.3
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 +78 -75
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +65 -65
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +3 -1
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +62 -4
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +39 -13
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/types.d.ts +13 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +102 -0
- package/package.json +1 -1
- package/src/lifecycle/planner.ts +27 -23
- package/src/lifecycle/progress.ts +58 -59
- package/src/lifecycle/rpc.ts +58 -5
- package/src/lifecycle/simulation.ts +39 -13
- package/src/lifecycle/types.ts +17 -0
package/src/lifecycle/planner.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleErc20Abi, lifecycleFu
|
|
|
3
3
|
import { hashLaunchIdentity, hashLaunchPlan, LifecycleFundingKind, LifecycleMode, LifecyclePhase, LifecycleRewardMode, LifecycleTokenKind, LifecycleVenue, LIFECYCLE_ERC404_CAPABILITY, LIFECYCLE_MAX_REWARD_ERC20_SUPPLY, LIFECYCLE_MAX_ERC404_SUPPLY, LIFECYCLE_MULTI_POSITION_CAPABILITY, LIFECYCLE_REQUIRED_CAPABILITIES, marketIdentityV1Components, parseLaunchPlan, serializeLaunchPlan, type LaunchPlanV1, type MarketIdentityV1 } from "./schema.js";
|
|
4
4
|
import { ABYSS_LIFECYCLE_CONFIG_SCHEMA, readLaunchProgress, readLifecycleProfiles, readPoolBoundHookDeployment, validateLifecycleMarketIdentity, validateV4LifecycleOracle } from "./progress.js";
|
|
5
5
|
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, encodePoolBoundV4LifecycleMarketConfig, hasLifecycleV4HookPermissions, minePoolBoundHookSalt, predictPoolBoundHookAddress, validateV4LifecycleMarket, V4_LIFECYCLE_CONFIG_SCHEMA, V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA, type V4LifecycleMarketConfig, type V4PoolBoundLifecycleMarketConfig } from "./markets.js";
|
|
6
|
-
import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, readLifecycleBlock, readLifecycleContract, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
|
|
6
|
+
import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleContract, resolveLifecycleLimits, rpcHex, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
|
|
7
7
|
import { simulateLaunchTransactions } from "./simulation.js";
|
|
8
8
|
import { LifecyclePlanningError, type BuildNextTransactionOptions, type CanonicalLaunchProgress, type LifecycleBlock, type LifecycleFundingPrerequisite, type LifecyclePoolBoundHookDeployment, type LifecycleProfile, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleTransaction, type PlannedLaunch, type PlanLaunchOptions, type PoolBoundLifecyclePreparationProgress, type SimulateLaunchPlanOptions } from "./types.js";
|
|
9
9
|
|
|
@@ -123,10 +123,14 @@ async function validatePendingInputs(client: LifecycleRpcClient, planned: Planne
|
|
|
123
123
|
for (const policy of plan.feeAssets) if (policy.asset.toLowerCase() !== planned.predictedToken.toLowerCase()) assets.add(policy.asset);
|
|
124
124
|
for (const market of plan.markets) assets.add(market.quoteAsset);
|
|
125
125
|
for (const funding of plan.funding) { assets.add(funding.asset); if (funding.kind !== LifecycleFundingKind.NativeWrap && funding.inputAsset !== zeroAddress) assets.add(funding.inputAsset); }
|
|
126
|
-
const
|
|
126
|
+
const registryRead = readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "registry", [], block);
|
|
127
|
+
const [profiles, registry, assetCodes, allowedInputs, escrowBalances] = await Promise.all([
|
|
127
128
|
readLifecycleProfiles({ client, orchestrator: plan.orchestrator, profileIds: [...new Set(plan.markets.map((market) => market.profileId))] }, block),
|
|
128
|
-
|
|
129
|
+
registryRead,
|
|
129
130
|
Promise.all([...assets].map(async (asset) => ({ asset, code: rpcHex(await lifecyclePinnedRpc(client, "eth_getCode", [asset, toHex(block.number)], block), "asset code") }))),
|
|
131
|
+
registryRead.then((registry) => Promise.all(plan.funding.map((funding) => funding.kind === LifecycleFundingKind.Swap && funding.inputAsset !== zeroAddress
|
|
132
|
+
? readLifecycleContract<boolean>(client, registry, lifecycleRegistryAbi, "fundingInputAllowed", [funding.inputAsset], block) : true))),
|
|
133
|
+
progress.canonical.phase === LifecyclePhase.None ? [] : Promise.all(plan.funding.map((funding) => readLifecycleContract<bigint>(client, plan.orchestrator, launchLifecycleAbi, "escrowBalance", [planned.launchId, funding.asset], block))),
|
|
130
134
|
]);
|
|
131
135
|
const reasons = profiles.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Implementation/profile is unavailable");
|
|
132
136
|
if (block.timestamp > plan.deadline) reasons.push("The immutable launch deadline has expired; cancellation remains available");
|
|
@@ -134,10 +138,7 @@ async function validatePendingInputs(client: LifecycleRpcClient, planned: Planne
|
|
|
134
138
|
if (!plan.feeAssets.some((policy) => policy.asset.toLowerCase() === planned.predictedToken.toLowerCase())) reasons.push("The launch token must have a committed fee policy");
|
|
135
139
|
for (const { asset, code } of assetCodes) if (code === "0x") reasons.push(`Asset ${asset} has no contract code`);
|
|
136
140
|
const required = LIFECYCLE_REQUIRED_CAPABILITIES | (plan.token.kind === LifecycleTokenKind.ERC404 ? LIFECYCLE_ERC404_CAPABILITY : 0n);
|
|
137
|
-
const
|
|
138
|
-
Promise.all(plan.funding.map((funding) => funding.kind === LifecycleFundingKind.Swap && funding.inputAsset !== zeroAddress
|
|
139
|
-
? readLifecycleContract<boolean>(client, registry, lifecycleRegistryAbi, "fundingInputAllowed", [funding.inputAsset], block) : true)),
|
|
140
|
-
Promise.all(plan.markets.map(async (market, index) => {
|
|
141
|
+
const markets = await Promise.all(plan.markets.map(async (market, index) => {
|
|
141
142
|
const profile = profiles.find((item) => item.id.toLowerCase() === market.profileId.toLowerCase());
|
|
142
143
|
let positions = 0;
|
|
143
144
|
let deployment: LifecyclePoolBoundHookDeployment | undefined;
|
|
@@ -152,23 +153,24 @@ async function validatePendingInputs(client: LifecycleRpcClient, planned: Planne
|
|
|
152
153
|
if (config === undefined) throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${profile.registration.configSchema}`);
|
|
153
154
|
if ("developerFeeBps" in config) {
|
|
154
155
|
validateV4LifecycleMarket({ market, config, profile, token: planned.predictedToken });
|
|
155
|
-
if (config.version === 4) await validateV4LifecycleOracle({ client, envelope: profile.envelope!, oracleConfigId: config.oracleConfigId, block });
|
|
156
156
|
}
|
|
157
157
|
if (config.positions.length === 0 || config.positions.length > 32) throw new LifecyclePlanningError("INVALID_POSITION_COUNT", "A launch commits at most 32 positive positions across every market/venue");
|
|
158
158
|
positions = config.positions.length;
|
|
159
159
|
const rewards = progress.canonical.rewards === zeroAddress && plan.token.rewardMode === LifecycleRewardMode.Dividends ? planned.predictedToken : progress.canonical.rewards;
|
|
160
160
|
if ("treasury" in config && rewards !== zeroAddress && config.treasury.toLowerCase() === rewards.toLowerCase() && config.hookFeePips !== 0 && config.protocolFeeDenominator !== 0) throw new LifecyclePlanningError("INVALID_FEE_POLICY", "Hook protocol treasury cannot be the launch rewards contract");
|
|
161
161
|
const capabilities = required | (positions > 1 ? LIFECYCLE_MULTI_POSITION_CAPABILITY : 0n);
|
|
162
|
-
const [adapter, boundDeployment] = await Promise.all([
|
|
162
|
+
const [adapter, boundDeployment, resolvedIdentity] = await Promise.all([
|
|
163
163
|
readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "requireEligible", [market.adapterId, market.profileId, market.configVersion, capabilities], block),
|
|
164
164
|
profile.topology.hookTopology === 2 ? readPoolBoundHookDeployment({ client, plan, marketIndex: index }, block) : undefined,
|
|
165
|
+
readLifecycleContract<MarketIdentityV1>(client, profile.adapter.implementation, lifecycleAdapterAbi, "resolve", [planned.launchId, planned.predictedToken, market], block),
|
|
166
|
+
"developerFeeBps" in config && config.version === 4 ? validateV4LifecycleOracle({ client, envelope: profile.envelope!, oracleConfigId: config.oracleConfigId, block }) : undefined,
|
|
165
167
|
]);
|
|
166
168
|
if (adapter.toLowerCase() !== profile.adapter.implementation.toLowerCase()) throw new LifecyclePlanningError("MARKET_IDENTITY", "Eligible adapter differs from the certified profile implementation");
|
|
167
169
|
if (boundDeployment !== undefined) {
|
|
168
170
|
if (!hasLifecycleV4HookPermissions(boundDeployment.predictedHook)) throw new LifecyclePlanningError("INVALID_HOOK_BITS", "Finalize the pool-bound salt with the exact lifecycle hook permission bits before planning wallet execution");
|
|
169
171
|
deployment = { marketIndex: index, ...boundDeployment };
|
|
170
172
|
}
|
|
171
|
-
identity =
|
|
173
|
+
identity = resolvedIdentity;
|
|
172
174
|
validateLifecycleMarketIdentity(identity, plan, planned.predictedToken, index);
|
|
173
175
|
const expectedHook = deployment?.predictedHook ?? profile.registration.hook;
|
|
174
176
|
if (identity.factory.toLowerCase() !== profile.registration.factory.toLowerCase() || identity.hook.toLowerCase() !== expectedHook.toLowerCase() || (identity.venue === LifecycleVenue.UniswapV4 && (identity.manager.toLowerCase() !== profile.registration.venue.toLowerCase() || profile.topology.hookTopology === 0)) || (identity.venue === LifecycleVenue.Abyss && (identity.factory.toLowerCase() !== profile.registration.venue.toLowerCase() || profile.topology.hookTopology !== 0))) throw new LifecyclePlanningError("MARKET_IDENTITY", "Canonical venue dependencies differ from exact immutable profile/topology approval");
|
|
@@ -181,9 +183,7 @@ async function validatePendingInputs(client: LifecycleRpcClient, planned: Planne
|
|
|
181
183
|
}
|
|
182
184
|
} catch (failure) { reason = failure instanceof Error ? failure.message : String(failure); }
|
|
183
185
|
return { positions, deployment, identity, reason };
|
|
184
|
-
|
|
185
|
-
progress.canonical.phase === LifecyclePhase.None ? [] : Promise.all(plan.funding.map((funding) => readLifecycleContract<bigint>(client, plan.orchestrator, launchLifecycleAbi, "escrowBalance", [planned.launchId, funding.asset], block))),
|
|
186
|
-
]);
|
|
186
|
+
}));
|
|
187
187
|
for (const [index, funding] of plan.funding.entries()) {
|
|
188
188
|
if (funding.asset.toLowerCase() === planned.predictedToken.toLowerCase() || (funding.kind === LifecycleFundingKind.Swap && funding.inputAsset.toLowerCase() === planned.predictedToken.toLowerCase())) reasons.push("Launch-token inventory cannot be an external funding output or conversion input");
|
|
189
189
|
if (!allowedInputs[index]) reasons.push(`Conversion input ${funding.inputAsset} is not currently admitted`);
|
|
@@ -324,7 +324,7 @@ export async function preparePoolBoundLifecyclePlan(options: {
|
|
|
324
324
|
/** Exact economic commitment with explicit execution mode and current-state admission. No broadcasts. */
|
|
325
325
|
export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLaunch> {
|
|
326
326
|
return withLifecycleReadClient(options.client, async (client) => {
|
|
327
|
-
const planned = await planLaunchWithReads(options, client);
|
|
327
|
+
const planned = await lifecycleStage(client, "plan", () => planLaunchWithReads(options, client));
|
|
328
328
|
if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous planning; previous reads and simulation are invalid");
|
|
329
329
|
return planned;
|
|
330
330
|
});
|
|
@@ -337,22 +337,26 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
|
|
|
337
337
|
if (plan.chainId > BigInt(Number.MAX_SAFE_INTEGER) || plan.chainId <= 0n) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Chain ID must be a positive exact JavaScript integer");
|
|
338
338
|
validatePlanShape(plan);
|
|
339
339
|
const planHash = hashLaunchPlan(plan); const launchId = hashLaunchIdentity(plan);
|
|
340
|
-
const [block, chainIdValue] = await Promise.all([
|
|
340
|
+
const [block, chainIdValue] = await lifecycleStage(client, "plan.context", () => Promise.all([
|
|
341
341
|
readLifecycleBlock(client),
|
|
342
342
|
lifecycleRpc(client, "eth_chainId"),
|
|
343
|
-
]);
|
|
343
|
+
]));
|
|
344
344
|
if (rpcQuantity(chainIdValue, "chain ID") !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
|
|
345
|
-
const
|
|
345
|
+
const prediction = readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block);
|
|
346
|
+
const [remoteHash, remoteIdentity, { tokenFactory, tokenFactoryCodeHash }, { limits }, { identity, progress }] = await lifecycleStage(client, "plan.domain", () => Promise.all([
|
|
346
347
|
readLifecycleContract<Hex>(client, plan.orchestrator, launchLifecycleAbi, "hashPlan", [plan], block),
|
|
347
348
|
readLifecycleContract<Hex>(client, plan.orchestrator, launchLifecycleAbi, "launchIdOf", [plan], block),
|
|
348
|
-
readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block),
|
|
349
349
|
readTokenFactoryBinding(client, plan.orchestrator, block),
|
|
350
350
|
resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
|
|
351
|
-
|
|
351
|
+
lifecycleStage(client, "plan.progress", async () => {
|
|
352
|
+
const predictedToken = await prediction;
|
|
353
|
+
const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1 };
|
|
354
|
+
const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts }, block);
|
|
355
|
+
return { identity, progress };
|
|
356
|
+
}),
|
|
357
|
+
]));
|
|
352
358
|
if (remoteHash.toLowerCase() !== planHash.toLowerCase() || remoteIdentity.toLowerCase() !== launchId.toLowerCase()) throw new LifecyclePlanningError("ABI_DOMAIN_MISMATCH", "Selected contract does not implement the exact lifecycle V1 plan domain/schema");
|
|
353
359
|
const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [] };
|
|
354
|
-
const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1 };
|
|
355
|
-
const progress = await readLaunchProgress({ client, planned: identity, receipts: options.receipts }, block);
|
|
356
360
|
let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, atomicAttempt: placeholder, preparationBatchSize: plan.markets.length, limits: options.limits };
|
|
357
361
|
const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
|
|
358
362
|
if (!progress.confirmationSafe || receiptsPending) {
|
|
@@ -369,10 +373,10 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
|
|
|
369
373
|
await assertLifecycleBlock(client, block, plan.chainId);
|
|
370
374
|
return { ...planned, simulation: { ...placeholder, confidence: "stateful", executionProof: "proved", protocolFit: "proved", admitted: true, reason: "Canonical launch is terminal; no transaction remains" } };
|
|
371
375
|
}
|
|
372
|
-
const [prerequisites, validation] = await Promise.all([
|
|
376
|
+
const [prerequisites, validation] = await lifecycleStage(client, "plan.inputs", () => Promise.all([
|
|
373
377
|
progress.canonical.phase === LifecyclePhase.None ? readFunding(client, plan, block) : { prerequisites: [], approvals: [], nativeValue: 0n, reasons: [] },
|
|
374
378
|
validatePendingInputs(client, planned, progress, block),
|
|
375
|
-
]);
|
|
379
|
+
]));
|
|
376
380
|
const reasons = [...prerequisites.reasons, ...validation.reasons];
|
|
377
381
|
planned = { ...planned, prerequisites: prerequisites.prerequisites, profiles: validation.profiles, hookDeployments: validation.hookDeployments };
|
|
378
382
|
if (reasons.length > 0) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { concatHex, decodeFunctionData, encodeAbiParameters, encodeFunctionData, keccak256, stringToHex, toHex, zeroAddress, zeroHash, type Address, type Hash, type Hex } from "viem";
|
|
2
|
-
import {
|
|
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
|
-
import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, readLifecycleBlock, readLifecycleContract, readLifecycleProfileTopology, rpcHex, rpcObject, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
|
|
5
|
+
import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleContract, 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
|
|
|
8
8
|
export const ABYSS_LIFECYCLE_CONFIG_SCHEMA = keccak256(stringToHex("(uint8,uint24,bytes32,uint160,(int24,int24,uint128,uint256)[])"));
|
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 ||
|
|
49
|
-
await
|
|
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
|
-
|
|
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] =
|
|
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() !==
|
|
112
|
-
if (
|
|
113
|
-
const [rootCode, recordedHash, remotePrediction, dependencies, requiredFlags, allFlags] =
|
|
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() ||
|
|
@@ -144,7 +140,7 @@ export async function validateV4LifecycleOracle(options: {
|
|
|
144
140
|
}
|
|
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
|
-
return withLifecycleReadClient(options.client, async (
|
|
143
|
+
return withLifecycleReadClient(options.client, (client) => lifecycleStage(client, "profile.certification", async () => {
|
|
148
144
|
const [block, chainIdValue] = await Promise.all([
|
|
149
145
|
pinnedBlock ?? readLifecycleBlock(client),
|
|
150
146
|
lifecycleRpc(client, "eth_chainId"),
|
|
@@ -153,33 +149,32 @@ export async function readLifecycleProfiles(options: { client: LifecycleRpcClien
|
|
|
153
149
|
const registry = await readLifecycleContract<Address>(client, options.orchestrator, launchLifecycleAbi, "registry", [], block);
|
|
154
150
|
const offset = options.offset ?? 0n; const limit = options.limit ?? 100n;
|
|
155
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");
|
|
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
152
|
let protocolMaximumDeveloperFeeBpsPromise: Promise<number> | undefined;
|
|
162
153
|
const readProtocolMaximumDeveloperFeeBps = () => protocolMaximumDeveloperFeeBpsPromise ??=
|
|
163
154
|
readLifecycleContract<number>(client, registry, lifecycleRegistryAbi, "protocolMaximumDeveloperFeeBps", [], block);
|
|
164
|
-
const
|
|
155
|
+
const readProfile = async (id: Hex): Promise<LifecycleProfile> => {
|
|
165
156
|
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
157
|
const schema = registration.configSchema.toLowerCase();
|
|
168
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";
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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([
|
|
159
|
+
const [adapter, certification] = await Promise.all([
|
|
160
|
+
readLifecycleContract<AdapterRegistrationV1>(client, registry, lifecycleRegistryAbi, "adapter", [registration.adapterId], block),
|
|
161
|
+
venueKind === "unknown" ? undefined : Promise.all([
|
|
176
162
|
readLifecycleProfileTopology(client, registry, id, block),
|
|
177
163
|
venueKind === "uniswap-v4" ? Promise.all([
|
|
178
164
|
readLifecycleContract<LaunchEnvelopeV2>(client, registry, lifecycleRegistryAbi, "profileEnvelope", [id], block),
|
|
179
165
|
readLifecycleContract<readonly [Address, Address, number, Hex, boolean]>(client, registry, lifecycleRegistryAbi, "developerTerms", [id], block),
|
|
180
166
|
readProtocolMaximumDeveloperFeeBps(),
|
|
181
167
|
]) : undefined,
|
|
182
|
-
])
|
|
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;
|
|
183
178
|
topology = recordedTopology;
|
|
184
179
|
if (reviewed !== undefined) {
|
|
185
180
|
envelope = reviewed[0];
|
|
@@ -191,17 +186,21 @@ export async function readLifecycleProfiles(options: { client: LifecycleRpcClien
|
|
|
191
186
|
maximumDeveloperFeeBps > reviewed[2]) throw new LifecyclePlanningError("PROFILE_TERMS", "Registry developer terms differ from the frozen reviewed envelope");
|
|
192
187
|
}
|
|
193
188
|
await certifyLifecycleProfile(client, options.orchestrator, registry, id, registration, adapter, topology, envelope, block, chainId);
|
|
194
|
-
|
|
195
|
-
if (eligible.toLowerCase() !== adapter.implementation.toLowerCase() || (developerTerms !== undefined && !developerTerms.enabled)) throw new LifecyclePlanningError("INELIGIBLE_PROFILE", "Registry refuses this profile for pending source admission");
|
|
189
|
+
if (developerTerms !== undefined && !developerTerms.enabled) throw new LifecyclePlanningError("INELIGIBLE_PROFILE", "Registry refuses this profile for pending source admission");
|
|
196
190
|
admitted = true;
|
|
197
191
|
} catch (failure) { reason = failure instanceof Error ? failure.message : String(failure); }
|
|
198
192
|
return { id, registration, adapter, topology, envelope, developerTerms, protocolMaximumDeveloperFeeBps: venueKind === "uniswap-v4" ? protocolMaximumDeveloperFeeBps : undefined, admitted, reason, venueKind };
|
|
199
|
-
}
|
|
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");
|
|
200
199
|
// A required V4 cap read still rejects the invocation on failure; Abyss has no developer-fee terms.
|
|
201
200
|
if (protocolMaximumDeveloperFeeBpsPromise !== undefined) await protocolMaximumDeveloperFeeBpsPromise;
|
|
202
201
|
if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, chainId);
|
|
203
202
|
return profiles;
|
|
204
|
-
});
|
|
203
|
+
}));
|
|
205
204
|
}
|
|
206
205
|
|
|
207
206
|
async function readPoolBoundHookDeploymentDetails(options: { client: LifecycleRpcClient; plan: LaunchPlanV1; marketIndex: number }, block: LifecycleBlock): Promise<{ deployment: PoolBoundHookDeployment; parameters: PoolBoundHookParametersV1 }> {
|
package/src/lifecycle/rpc.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { decodeFunctionResult, encodeFunctionData, isAddress, isHex, parseAbi, toHex, type Abi, type Address, type Hex } from "viem";
|
|
2
|
-
import { LifecyclePlanningError, type LifecycleBlock, type LifecycleLimitContext, type LifecycleLimitSource, type LifecycleLimits, type LifecycleRpcClient, type LifecycleTransaction, type ResolvedLifecycleLimits } from "./types.js";
|
|
2
|
+
import { LifecyclePlanningError, type LifecycleBlock, type LifecycleDiagnosticEvent, type LifecycleDiagnosticListener, type LifecycleDiagnosticStage, type LifecycleLimitContext, type LifecycleLimitSource, type LifecycleLimits, type LifecycleRpcClient, type LifecycleTransaction, type ResolvedLifecycleLimits } from "./types.js";
|
|
3
3
|
import { lifecycleRegistryAbi } from "./abi.js";
|
|
4
4
|
import type { ProfileTopologyV1 } from "./schema.js";
|
|
5
5
|
|
|
6
6
|
const lifecycleReadConcurrency = 8;
|
|
7
|
+
const diagnosticRpcMethods: Record<string, true> = {
|
|
8
|
+
eth_chainId: true, eth_getBlockByNumber: true, eth_getBlockByHash: true, eth_getCode: true,
|
|
9
|
+
eth_getBalance: true, eth_call: true, eth_estimateGas: true, eth_gasPrice: true,
|
|
10
|
+
eth_simulateV1: true, eth_getTransactionCount: true, eth_getTransactionReceipt: true, eth_getTransactionByHash: true,
|
|
11
|
+
};
|
|
7
12
|
|
|
8
13
|
/** Owns only one invocation's immutable RPC observations, never live checks or writes. */
|
|
9
14
|
class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
@@ -11,6 +16,7 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
11
16
|
private readonly active = new Set<Promise<void>>();
|
|
12
17
|
private closed = false;
|
|
13
18
|
private readonly concurrency: number;
|
|
19
|
+
private diagnosticSequence = 0;
|
|
14
20
|
constructor(readonly source: LifecycleRpcClient) {
|
|
15
21
|
this.concurrency = source.supportsReadBatching === true ? lifecycleReadConcurrency : 1;
|
|
16
22
|
}
|
|
@@ -28,15 +34,29 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
28
34
|
|
|
29
35
|
async run(method: string, params: readonly unknown[]): Promise<unknown> {
|
|
30
36
|
this.assertOpen();
|
|
37
|
+
const listener = this.source.onDiagnostic;
|
|
38
|
+
const observedMethod = listener === undefined ? undefined : diagnosticRpcMethods[method] === true ? method : "other";
|
|
39
|
+
const queuedAt = listener === undefined ? 0 : performance.now();
|
|
40
|
+
const requestId = listener === undefined ? 0 : ++this.diagnosticSequence;
|
|
41
|
+
if (listener !== undefined) emitLifecycleDiagnostic(listener, { stage: "rpc", phase: "queued", requestId, method: observedMethod });
|
|
31
42
|
while (this.active.size >= this.concurrency) await Promise.race(this.active);
|
|
32
43
|
this.assertOpen();
|
|
44
|
+
const started = listener === undefined ? 0 : performance.now();
|
|
45
|
+
if (listener !== undefined) emitLifecycleDiagnostic(listener, { stage: "rpc", phase: "start", requestId, method: observedMethod, queueMs: started - queuedAt });
|
|
33
46
|
const pending = this.request({ method, params });
|
|
34
47
|
const settled = pending.then(
|
|
35
48
|
() => { this.active.delete(settled); },
|
|
36
49
|
() => { this.active.delete(settled); },
|
|
37
50
|
);
|
|
38
51
|
this.active.add(settled);
|
|
39
|
-
|
|
52
|
+
try {
|
|
53
|
+
const result = await pending;
|
|
54
|
+
if (listener !== undefined) emitLifecycleDiagnostic(listener, { stage: "rpc", phase: "success", requestId, method: observedMethod, durationMs: performance.now() - started });
|
|
55
|
+
return result;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (listener !== undefined) emitLifecycleDiagnostic(listener, { stage: "rpc", phase: "failure", requestId, method: observedMethod, durationMs: performance.now() - started });
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
40
60
|
}
|
|
41
61
|
|
|
42
62
|
read(method: string, params: readonly unknown[], block: LifecycleBlock): Promise<unknown> {
|
|
@@ -45,7 +65,11 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
45
65
|
// caller, value, gas and any other call fields; no selector-only/ABI-name aliasing.
|
|
46
66
|
const key = JSON.stringify([block.number.toString(), block.hash.toLowerCase(), method, params]);
|
|
47
67
|
const existing = this.reads.get(key);
|
|
48
|
-
if (existing !== undefined)
|
|
68
|
+
if (existing !== undefined) {
|
|
69
|
+
const listener = this.source.onDiagnostic;
|
|
70
|
+
if (listener !== undefined) emitLifecycleDiagnostic(listener, { stage: "rpc", phase: "reuse", method: diagnosticRpcMethods[method] === true ? method : "other" });
|
|
71
|
+
return existing;
|
|
72
|
+
}
|
|
49
73
|
const pending = this.run(method, params);
|
|
50
74
|
this.reads.set(key, pending);
|
|
51
75
|
void pending.catch(() => { if (this.reads.get(key) === pending) this.reads.delete(key); });
|
|
@@ -70,6 +94,32 @@ export function lifecycleSourceClient(client: LifecycleRpcClient): LifecycleRpcC
|
|
|
70
94
|
return client instanceof InvocationLifecycleClient ? client.source : client;
|
|
71
95
|
}
|
|
72
96
|
|
|
97
|
+
function emitLifecycleDiagnostic(listener: LifecycleDiagnosticListener, event: LifecycleDiagnosticEvent): void {
|
|
98
|
+
try {
|
|
99
|
+
const pending = listener(event);
|
|
100
|
+
if (pending !== undefined) void pending.catch(() => {});
|
|
101
|
+
} catch { /* Observers cannot change financial behavior. */ }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** No clock, event allocation or extra promise when diagnostics are disabled. */
|
|
105
|
+
export function lifecycleStage<T>(client: LifecycleRpcClient, stage: LifecycleDiagnosticStage, work: () => Promise<T>): Promise<T> {
|
|
106
|
+
const listener = lifecycleSourceClient(client).onDiagnostic;
|
|
107
|
+
return listener === undefined ? work() : observedLifecycleStage(listener, stage, work);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function observedLifecycleStage<T>(listener: LifecycleDiagnosticListener, stage: LifecycleDiagnosticStage, work: () => Promise<T>): Promise<T> {
|
|
111
|
+
const started = performance.now();
|
|
112
|
+
emitLifecycleDiagnostic(listener, { stage, phase: "start" });
|
|
113
|
+
try {
|
|
114
|
+
const result = await work();
|
|
115
|
+
emitLifecycleDiagnostic(listener, { stage, phase: "success", durationMs: performance.now() - started });
|
|
116
|
+
return result;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
emitLifecycleDiagnostic(listener, { stage, phase: "failure", durationMs: performance.now() - started });
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
73
123
|
/** Only explicitly pinned fixed-state reads may participate in invocation reuse. */
|
|
74
124
|
export async function lifecyclePinnedRpc(client: LifecycleRpcClient, method: "eth_call" | "eth_getCode" | "eth_getBalance", params: readonly unknown[], block: LifecycleBlock): Promise<unknown> {
|
|
75
125
|
if (params.length !== 2 || params[1] !== toHex(block.number)) throw new LifecyclePlanningError("INVALID_READ_CONTEXT", "Reusable lifecycle reads require the exact pinned block and no state overrides");
|
|
@@ -105,8 +155,11 @@ export async function readLifecycleBlock(client: LifecycleRpcClient, tag: Hex |
|
|
|
105
155
|
};
|
|
106
156
|
}
|
|
107
157
|
export async function assertLifecycleBlock(client: LifecycleRpcClient, block: LifecycleBlock, expectedChainId?: bigint): Promise<void> {
|
|
108
|
-
|
|
109
|
-
|
|
158
|
+
const [chainId, current] = await Promise.all([
|
|
159
|
+
expectedChainId === undefined ? undefined : lifecycleRpc(client, "eth_chainId"),
|
|
160
|
+
readLifecycleBlock(client, toHex(block.number)),
|
|
161
|
+
]);
|
|
162
|
+
if (expectedChainId !== undefined && rpcQuantity(chainId, "chain ID") !== expectedChainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during the pinned operation");
|
|
110
163
|
if (current.hash.toLowerCase() !== block.hash.toLowerCase()) throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch state changed during the operation; read canonical progress again");
|
|
111
164
|
}
|
|
112
165
|
export async function readLifecycleContract<T>(client: LifecycleRpcClient, to: Address, abi: Abi, functionName: string, args: readonly unknown[], block: LifecycleBlock, from?: Address): Promise<T> {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctionResult, toHex, type Hex } from "viem";
|
|
2
2
|
import { launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
|
|
3
3
|
import { LifecyclePhase, type LaunchProgressV1, type LaunchReceiptV1 } from "./schema.js";
|
|
4
|
-
import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readNitroPosterGas, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity } from "./rpc.js";
|
|
4
|
+
import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readNitroPosterGas, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity } from "./rpc.js";
|
|
5
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";
|
|
6
6
|
|
|
7
7
|
function conditionRequest(condition: LifecyclePostcondition, planned: PlannedLaunch): { to: Hex; data: Hex } {
|
|
@@ -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
|
-
|
|
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[]> {
|
|
@@ -238,31 +239,49 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
238
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");
|
|
239
240
|
const block = pinnedBlock ?? await readLifecycleBlock(client);
|
|
240
241
|
const context = { client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId };
|
|
241
|
-
const { limits, configuration } = await
|
|
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
|
+
]));
|
|
242
248
|
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 };
|
|
243
|
-
if (rpcQuantity(
|
|
244
|
-
const accountCode = rpcHex(
|
|
249
|
+
if (rpcQuantity(chainId, "chain ID") !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
|
|
250
|
+
const accountCode = rpcHex(accountCodeValue, "account code");
|
|
245
251
|
if (accountCode !== "0x") return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: "Direct EOA transaction simulation cannot prove smart-account signature/execution behavior" };
|
|
246
252
|
for (const transaction of transactions) {
|
|
247
253
|
if (transaction.from.toLowerCase() !== planned.account.toLowerCase() || BigInt(transaction.chainId) !== planned.chainId) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Simulation transaction differs from the committed chain/account");
|
|
248
254
|
if (limits.maxCalldataBytes !== undefined && (transaction.data.length - 2) / 2 > limits.maxCalldataBytes) return { ...base, executionProof: "unavailable", protocolFit: "failed", backend: "unavailable", admitted: false, steps: [], failedTransactionId: transaction.id, reason: "Exact calldata exceeds the supplied chain/account/RPC byte cap" };
|
|
249
255
|
}
|
|
250
256
|
if (transactions.length === 0) { await assertLifecycleBlock(client, block, planned.chainId); return { ...base, executionProof: "proved", protocolFit: "proved", backend: "unavailable", admitted: true, steps: [] }; }
|
|
251
|
-
const gasPrice = rpcQuantity(
|
|
257
|
+
const gasPrice = rpcQuantity(gasPriceValue, "gas price");
|
|
252
258
|
if (gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
|
|
253
259
|
const buffer = (gas: bigint) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
|
|
260
|
+
const computeCeiling = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
|
|
261
|
+
const capGas = transactions.map(() => computeCeiling);
|
|
262
|
+
let parallelMeasurement: Promise<{ steps: LifecycleSimulationStep[] } | { failure: unknown }> | undefined;
|
|
254
263
|
const posterGas: bigint[] = [];
|
|
255
264
|
if (limits.protocol === "nitro") {
|
|
265
|
+
// The isolated capability probe, poster quote and permissive cap measurement
|
|
266
|
+
// have no data dependency. Only an actually batching source overlaps all three.
|
|
267
|
+
const nitroEvidence = Promise.all([
|
|
268
|
+
lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, block, limits, gasPrice)),
|
|
269
|
+
lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
|
|
270
|
+
]);
|
|
271
|
+
if (sourceClient.supportsReadBatching === true) {
|
|
272
|
+
// Handle settlement immediately so a failed measurement cannot escape while
|
|
273
|
+
// the independent metering evidence is still pending. It is never admission.
|
|
274
|
+
parallelMeasurement = lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false))
|
|
275
|
+
.then((steps) => ({ steps }), (failure: unknown) => ({ failure }));
|
|
276
|
+
}
|
|
256
277
|
try {
|
|
257
|
-
|
|
258
|
-
posterGas.push(...
|
|
278
|
+
const [, estimates] = await nitroEvidence;
|
|
279
|
+
posterGas.push(...estimates);
|
|
259
280
|
} catch (failure) {
|
|
260
281
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
261
282
|
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.` };
|
|
262
283
|
}
|
|
263
284
|
}
|
|
264
|
-
const computeCeiling = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
|
|
265
|
-
const capGas = transactions.map(() => computeCeiling);
|
|
266
285
|
// The poster quote allocates gas only. The native exact replay enforces actual
|
|
267
286
|
// compute hold; subtracting this approximate budget from gasUsed would not.
|
|
268
287
|
const discoveryGas = transactions.map((_, index) => {
|
|
@@ -274,7 +293,14 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
274
293
|
let backend: "eth_simulateV1" | "controlled-fork" = "eth_simulateV1";
|
|
275
294
|
let measured: LifecycleSimulationStep[];
|
|
276
295
|
// Permissive cap measurement is not proof; only the exact-gas validated replay admits a plan.
|
|
277
|
-
try {
|
|
296
|
+
try {
|
|
297
|
+
if (parallelMeasurement === undefined) measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false));
|
|
298
|
+
else {
|
|
299
|
+
const observation = await parallelMeasurement;
|
|
300
|
+
if ("failure" in observation) throw observation.failure;
|
|
301
|
+
measured = observation.steps;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
278
304
|
catch (failure) {
|
|
279
305
|
if (options.fork === undefined || limits.protocol === "nitro") {
|
|
280
306
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
@@ -301,7 +327,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
301
327
|
let verified: LifecycleSimulationStep[];
|
|
302
328
|
let validatedReplayError: string | undefined;
|
|
303
329
|
try {
|
|
304
|
-
if (backend === "eth_simulateV1") verified = await simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true);
|
|
330
|
+
if (backend === "eth_simulateV1") verified = await lifecycleStage(client, "simulation.replay", () => simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true));
|
|
305
331
|
else {
|
|
306
332
|
if (options.fork === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Controlled fork is missing for the exact-gas replay");
|
|
307
333
|
verified = await simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice, false, limits.headroomBps);
|
|
@@ -326,7 +352,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
326
352
|
try {
|
|
327
353
|
const ceilingLimits = discoveryGas;
|
|
328
354
|
const retried = backend === "eth_simulateV1"
|
|
329
|
-
? await simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true)
|
|
355
|
+
? await lifecycleStage(client, "simulation.envelope", () => simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true))
|
|
330
356
|
: options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
|
|
331
357
|
if (retried === undefined || retried.some((step) => !step.success)) {
|
|
332
358
|
await assertLifecycleBlock(client, block, planned.chainId);
|