@black-market/sdk 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +76 -68
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +118 -109
- 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 +69 -18
- package/dist/lifecycle/simulation.js.map +1 -1
- package/docs/launch-lifecycle.md +90 -0
- package/package.json +1 -1
- package/src/lifecycle/planner.ts +25 -16
- package/src/lifecycle/progress.ts +101 -93
- package/src/lifecycle/simulation.ts +72 -18
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { concatHex, decodeFunctionData, encodeAbiParameters, encodeFunctionData, keccak256, stringToHex, toHex, zeroAddress, zeroHash, type Address, type Hash, type Hex } from "viem";
|
|
2
|
-
import {
|
|
2
|
+
import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleDirectoryAbi, lifecycleErc20Abi, lifecycleOracleFactoryAbi, lifecycleRegistryAbi, poolFeeCollectorFactoryV1Abi, lifecycleV4HookAbi, lifecycleV4LockerAbi, poolHookDeployerV1Abi, sharedHookDeployerV1Abi, fixedFeePoolHookV1Abi, poolMarketAdapterV1Abi, sharedMarketAdapterV1Abi } from "./abi.js";
|
|
3
3
|
import { decodePoolBoundV4LifecycleMarketConfig, encodePoolBoundHookParameters, hasLifecycleV4HookPermissions, hashPoolBoundV4MarketCommitment, poolBoundHookInitCodeHash, predictPoolBoundHookAddress, validateV4LifecycleMarket, V4_LIFECYCLE_CONFIG_SCHEMA, V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA, type PoolBoundHookParametersV1 } from "./markets.js";
|
|
4
4
|
import { hashLaunchIdentity, hashLaunchPlan, hashLaunchBounds, hashLaunchDependencies, hashLifecycleProfile, launchProgressV1Components, LifecycleMode, LifecyclePhase, LifecycleVenue, LIFECYCLE_REQUIRED_CAPABILITIES, LIFECYCLE_ERC404_CAPABILITY, LIFECYCLE_MULTI_POSITION_CAPABILITY, type AdapterRegistrationV1, type LaunchPlanV1, type LaunchProgressV1, type MarketIdentityV1, type MarketLiveStateV1, type PreparedMarketV1, type ProfileRegistrationV1, type ProfileTopologyV1, type LaunchEnvelopeV2, type LifecycleDeveloperTerms } from "./schema.js";
|
|
5
5
|
import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleContract, readLifecycleProfileTopology, rpcHex, rpcObject, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
|
|
@@ -29,24 +29,16 @@ async function certifyLifecycleProfile(client: LifecycleRpcClient, orchestrator:
|
|
|
29
29
|
if (!shared && !bound && !abyss) throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${registration.configSchema}`);
|
|
30
30
|
const version = shared ? 4 : bound ? 5 : 1;
|
|
31
31
|
if (adapter.configVersion !== version || topology.configVersion !== version || topology.hookTopology !== (shared ? 1 : bound ? 2 : 0)) throw new LifecyclePlanningError("UNCERTIFIED_TOPOLOGY", "Registry topology/version differs from the supported config schema");
|
|
32
|
-
const [implementationCode, authority, digest] = await Promise.all([
|
|
33
|
-
readDependencyCode(client, adapter.implementation, block),
|
|
34
|
-
readLifecycleContract<Address>(client, adapter.implementation, lifecycleAdapterAbi, "core", [], block),
|
|
35
|
-
readLifecycleContract<Hex>(client, adapter.implementation, lifecycleAdapterAbi, "dependencyDigest", [], block),
|
|
36
|
-
]);
|
|
37
|
-
if (keccak256(implementationCode).toLowerCase() !== adapter.codeHash.toLowerCase()) throw new LifecyclePlanningError("PROFILE_CODE_HASH", "Adapter code hash differs from immutable registry approval");
|
|
38
|
-
if (authority.toLowerCase() !== orchestrator.toLowerCase() || digest.toLowerCase() !== registration.dependencyDigest.toLowerCase()) throw new LifecyclePlanningError("PROFILE_DEPENDENCY", "Adapter authority or dependency digest differs from immutable registry approval");
|
|
39
32
|
if (abyss) {
|
|
40
33
|
if (topology.hookDeployer !== zeroAddress || topology.hookCreationCodeHash !== zeroHash) throw new LifecyclePlanningError("UNCERTIFIED_TOPOLOGY", "Abyss does not admit a V4 hook deployment graph");
|
|
41
|
-
|
|
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() ||
|
|
@@ -145,60 +141,69 @@ export async function validateV4LifecycleOracle(options: {
|
|
|
145
141
|
|
|
146
142
|
export async function readLifecycleProfiles(options: { client: LifecycleRpcClient; orchestrator: Address; profileIds?: readonly Hex[]; offset?: bigint; limit?: bigint }, pinnedBlock?: LifecycleBlock): Promise<LifecycleProfile[]> {
|
|
147
143
|
return withLifecycleReadClient(options.client, (client) => lifecycleStage(client, "profile.certification", async () => {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
]
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
144
|
+
const chainIdPromise = lifecycleRpc(client, "eth_chainId").then((value) => rpcQuantity(value, "chain ID"));
|
|
145
|
+
// Block-bound discovery does not depend on chain identity. Join both branches
|
|
146
|
+
// immediately so a failed chain read cannot become detached or a profile refusal.
|
|
147
|
+
const [chainId, { block, profiles }] = await Promise.all([
|
|
148
|
+
chainIdPromise,
|
|
149
|
+
(async () => {
|
|
150
|
+
const block = pinnedBlock ?? await readLifecycleBlock(client);
|
|
151
|
+
const registry = await readLifecycleContract<Address>(client, options.orchestrator, launchLifecycleAbi, "registry", [], block);
|
|
152
|
+
const offset = options.offset ?? 0n; const limit = options.limit ?? 100n;
|
|
153
|
+
if (offset < 0n || limit < 0n || limit > 100n) throw new LifecyclePlanningError("INVALID_PROFILE_PAGE", "Registry profile pages require unsigned offset and limit at most 100");
|
|
154
|
+
let protocolMaximumDeveloperFeeBpsPromise: Promise<number> | undefined;
|
|
155
|
+
const readProtocolMaximumDeveloperFeeBps = () => protocolMaximumDeveloperFeeBpsPromise ??=
|
|
156
|
+
readLifecycleContract<number>(client, registry, lifecycleRegistryAbi, "protocolMaximumDeveloperFeeBps", [], block);
|
|
157
|
+
const readProfile = async (id: Hex): Promise<LifecycleProfile> => {
|
|
158
|
+
const registration = await readLifecycleContract<ProfileRegistrationV1>(client, registry, lifecycleRegistryAbi, "profile", [id], block);
|
|
159
|
+
const schema = registration.configSchema.toLowerCase();
|
|
160
|
+
const venueKind = schema === V4_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() || schema === V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? "uniswap-v4" : schema === ABYSS_LIFECYCLE_CONFIG_SCHEMA.toLowerCase() ? "abyss" : "unknown";
|
|
161
|
+
const [adapter, certification] = await Promise.all([
|
|
162
|
+
readLifecycleContract<AdapterRegistrationV1>(client, registry, lifecycleRegistryAbi, "adapter", [registration.adapterId], block),
|
|
163
|
+
venueKind === "unknown" ? undefined : Promise.all([
|
|
164
|
+
readLifecycleProfileTopology(client, registry, id, block),
|
|
165
|
+
venueKind === "uniswap-v4" ? Promise.all([
|
|
166
|
+
readLifecycleContract<LaunchEnvelopeV2>(client, registry, lifecycleRegistryAbi, "profileEnvelope", [id], block),
|
|
167
|
+
readLifecycleContract<readonly [Address, Address, number, Hex, boolean]>(client, registry, lifecycleRegistryAbi, "developerTerms", [id], block),
|
|
168
|
+
readProtocolMaximumDeveloperFeeBps(),
|
|
169
|
+
]) : undefined,
|
|
170
|
+
]).then((values) => ({ values }), (failure: unknown) => ({ failure })),
|
|
171
|
+
]);
|
|
172
|
+
const chainId = await chainIdPromise;
|
|
173
|
+
let topology: ProfileTopologyV1 = { hookTopology: 0, configVersion: adapter.configVersion, hookDeployer: zeroAddress, hookCreationCodeHash: zeroHash };
|
|
174
|
+
let envelope: LaunchEnvelopeV2 | undefined; let developerTerms: LifecycleDeveloperTerms | undefined;
|
|
175
|
+
let protocolMaximumDeveloperFeeBps: number | undefined;
|
|
176
|
+
let admitted = false; let reason: string | undefined;
|
|
177
|
+
try {
|
|
178
|
+
if (certification === undefined) throw new LifecyclePlanningError("UNSUPPORTED_SCHEMA", `Unsupported lifecycle config schema ${registration.configSchema}`);
|
|
179
|
+
if ("failure" in certification) throw certification.failure;
|
|
180
|
+
const [recordedTopology, reviewed] = certification.values;
|
|
181
|
+
topology = recordedTopology;
|
|
182
|
+
if (reviewed !== undefined) {
|
|
183
|
+
envelope = reviewed[0];
|
|
184
|
+
protocolMaximumDeveloperFeeBps = reviewed[2];
|
|
185
|
+
const [implementation, beneficiary, maximumDeveloperFeeBps, termsDigest, enabled] = reviewed[1];
|
|
186
|
+
developerTerms = { adapter: implementation, beneficiary, maximumDeveloperFeeBps, termsDigest, enabled };
|
|
187
|
+
if (implementation.toLowerCase() !== adapter.implementation.toLowerCase() || beneficiary.toLowerCase() !== envelope.beneficiary.toLowerCase() ||
|
|
188
|
+
termsDigest.toLowerCase() !== envelope.termsDigest.toLowerCase() || maximumDeveloperFeeBps !== envelope.maximumDeveloperFeeBps ||
|
|
189
|
+
maximumDeveloperFeeBps > reviewed[2]) throw new LifecyclePlanningError("PROFILE_TERMS", "Registry developer terms differ from the frozen reviewed envelope");
|
|
190
|
+
}
|
|
191
|
+
await certifyLifecycleProfile(client, options.orchestrator, registry, id, registration, adapter, topology, envelope, block, chainId);
|
|
192
|
+
if (developerTerms !== undefined && !developerTerms.enabled) throw new LifecyclePlanningError("INELIGIBLE_PROFILE", "Registry refuses this profile for pending source admission");
|
|
193
|
+
admitted = true;
|
|
194
|
+
} catch (failure) { reason = failure instanceof Error ? failure.message : String(failure); }
|
|
195
|
+
return { id, registration, adapter, topology, envelope, developerTerms, protocolMaximumDeveloperFeeBps: venueKind === "uniswap-v4" ? protocolMaximumDeveloperFeeBps : undefined, admitted, reason, venueKind };
|
|
196
|
+
};
|
|
197
|
+
const [authority, profiles] = await Promise.all([
|
|
198
|
+
readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "core", [], block),
|
|
199
|
+
Promise.resolve(options.profileIds ?? readLifecycleContract<readonly Hex[]>(client, registry, lifecycleRegistryAbi, "profileIds", [offset, limit], block)).then((ids) => Promise.all(ids.map(readProfile))),
|
|
182
200
|
]);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (implementation.toLowerCase() !== adapter.implementation.toLowerCase() || beneficiary.toLowerCase() !== envelope.beneficiary.toLowerCase() ||
|
|
190
|
-
termsDigest.toLowerCase() !== envelope.termsDigest.toLowerCase() || maximumDeveloperFeeBps !== envelope.maximumDeveloperFeeBps ||
|
|
191
|
-
maximumDeveloperFeeBps > reviewed[2]) throw new LifecyclePlanningError("PROFILE_TERMS", "Registry developer terms differ from the frozen reviewed envelope");
|
|
192
|
-
}
|
|
193
|
-
await certifyLifecycleProfile(client, options.orchestrator, registry, id, registration, adapter, topology, envelope, block, chainId);
|
|
194
|
-
const eligible = await readLifecycleContract<Address>(client, registry, lifecycleRegistryAbi, "requireEligible", [registration.adapterId, id, topology.configVersion, LIFECYCLE_REQUIRED_CAPABILITIES], block);
|
|
195
|
-
if (eligible.toLowerCase() !== adapter.implementation.toLowerCase() || (developerTerms !== undefined && !developerTerms.enabled)) throw new LifecyclePlanningError("INELIGIBLE_PROFILE", "Registry refuses this profile for pending source admission");
|
|
196
|
-
admitted = true;
|
|
197
|
-
} catch (failure) { reason = failure instanceof Error ? failure.message : String(failure); }
|
|
198
|
-
return { id, registration, adapter, topology, envelope, developerTerms, protocolMaximumDeveloperFeeBps: venueKind === "uniswap-v4" ? protocolMaximumDeveloperFeeBps : undefined, admitted, reason, venueKind };
|
|
199
|
-
}));
|
|
200
|
-
// A required V4 cap read still rejects the invocation on failure; Abyss has no developer-fee terms.
|
|
201
|
-
if (protocolMaximumDeveloperFeeBpsPromise !== undefined) await protocolMaximumDeveloperFeeBpsPromise;
|
|
201
|
+
if (authority.toLowerCase() !== options.orchestrator.toLowerCase()) throw new LifecyclePlanningError("REGISTRY_BINDING", "Registry is not bound to the selected lifecycle orchestrator");
|
|
202
|
+
// A required V4 cap read still rejects the invocation on failure; Abyss has no developer-fee terms.
|
|
203
|
+
if (protocolMaximumDeveloperFeeBpsPromise !== undefined) await protocolMaximumDeveloperFeeBpsPromise;
|
|
204
|
+
return { block, profiles };
|
|
205
|
+
})(),
|
|
206
|
+
]);
|
|
202
207
|
if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, chainId);
|
|
203
208
|
return profiles;
|
|
204
209
|
}));
|
|
@@ -392,8 +397,11 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
|
|
|
392
397
|
const confirmationDepth = options.confirmations ?? planned.confirmations;
|
|
393
398
|
if (!Number.isSafeInteger(confirmationDepth) || confirmationDepth < 1) throw new LifecyclePlanningError("INVALID_CONFIRMATIONS", "Confirmation depth must be a positive integer");
|
|
394
399
|
const confirmedNumber = block.number >= BigInt(confirmationDepth - 1) ? block.number - BigInt(confirmationDepth - 1) : 0n;
|
|
395
|
-
const confirmedBlock =
|
|
396
|
-
|
|
400
|
+
const [confirmedBlock, chainIdValue] = await Promise.all([
|
|
401
|
+
confirmedNumber === block.number ? block : readLifecycleBlock(client, toHex(confirmedNumber)),
|
|
402
|
+
lifecycleRpc(client, "eth_chainId"),
|
|
403
|
+
]);
|
|
404
|
+
const chainId = rpcQuantity(chainIdValue, "chain ID");
|
|
397
405
|
if (chainId !== planned.plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed launch");
|
|
398
406
|
if (planned.account.toLowerCase() !== planned.plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
|
|
399
407
|
if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan no longer matches its commitment");
|
|
@@ -2,7 +2,7 @@ import { decodeEventLog, decodeFunctionResult, encodeFunctionData, encodeFunctio
|
|
|
2
2
|
import { launchLifecycleAbi, lifecycleErc20Abi } from "./abi.js";
|
|
3
3
|
import { LifecyclePhase, type LaunchProgressV1, type LaunchReceiptV1 } from "./schema.js";
|
|
4
4
|
import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, lifecycleStage, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readNitroPosterGas, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity } from "./rpc.js";
|
|
5
|
-
import { LifecyclePlanningError, type ControlledLifecycleFork, type LifecycleBlock, type LifecyclePostcondition, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleSimulationStep, type LifecycleTransaction, type PlannedLaunch, type ResolvedLifecycleLimits, type SimulateLaunchPlanOptions } from "./types.js";
|
|
5
|
+
import { LifecyclePlanningError, type ControlledLifecycleFork, type LifecycleBlock, type LifecycleLimitContext, type LifecycleLimits, type LifecyclePostcondition, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleSimulationStep, type LifecycleTransaction, type PlannedLaunch, type ResolvedLifecycleLimits, type SimulateLaunchPlanOptions } from "./types.js";
|
|
6
6
|
|
|
7
7
|
function conditionRequest(condition: LifecyclePostcondition, planned: PlannedLaunch): { to: Hex; data: Hex } {
|
|
8
8
|
if (condition.kind === "allowance") return {
|
|
@@ -55,7 +55,8 @@ async function proveNitroBackend(client: LifecycleRpcClient, planned: PlannedLau
|
|
|
55
55
|
throw new LifecyclePlanningError("NITRO_METERING_UNAVAILABLE", "Simulated ArbOS version/compute getters differ from the exact pinned chain context");
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
// The enclosing simulation rechecks chain and canonical hash after its proofs
|
|
59
|
+
// (including every refusal path); a separate probe-only recheck is redundant.
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
async function simulateRpcPass(client: LifecycleRpcClient, planned: PlannedLaunch, block: LifecycleBlock, transactions: readonly LifecycleTransaction[], gasLimits: readonly bigint[], limits: ResolvedLifecycleLimits, gasPrice: bigint, validation: boolean): Promise<LifecycleSimulationStep[]> {
|
|
@@ -232,18 +233,54 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
|
|
|
232
233
|
}
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
|
|
236
|
+
/** Internal one-shot continuation. Only SDK reads can produce its context; callers
|
|
237
|
+
* cannot supply resolved policy or account/chain observations as authority. */
|
|
238
|
+
export function prepareLaunchSimulation(options: SimulateLaunchPlanOptions, block: LifecycleBlock, hasTransactions = true): (transactions: readonly LifecycleTransaction[]) => Promise<LifecycleSimulation> {
|
|
236
239
|
const { client, planned } = options;
|
|
237
240
|
const sourceClient = lifecycleSourceClient(client);
|
|
238
|
-
|
|
239
|
-
const block = pinnedBlock ?? await readLifecycleBlock(client);
|
|
241
|
+
const blockNumber = block.number, blockHash = block.hash;
|
|
240
242
|
const context = { client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId };
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
const pending = (async () => {
|
|
244
|
+
if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === sourceClient) throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
|
|
245
|
+
return lifecycleStage(client, "simulation.context", () => Promise.all([
|
|
246
|
+
resolveLifecycleLimits(context, options.limits === undefined ? planned.limits : options.limits),
|
|
247
|
+
lifecycleRpc(client, "eth_chainId"),
|
|
248
|
+
lifecyclePinnedRpc(client, "eth_getCode", [context.account, toHex(block.number)], block),
|
|
249
|
+
hasTransactions ? lifecycleRpc(client, "eth_gasPrice") : undefined,
|
|
250
|
+
]));
|
|
251
|
+
})().then((observations) => ({ observations }), (failure: unknown) => ({ failure }));
|
|
252
|
+
// Attach the failure handler before input validation can refuse or throw.
|
|
253
|
+
// Preparation is read-only; no probe, measurement, replay or fork work starts.
|
|
254
|
+
let consumed = false;
|
|
255
|
+
return async (transactions) => {
|
|
256
|
+
if (consumed) throw new LifecyclePlanningError("INVALID_SIMULATION", "Prepared simulation context is invocation-local and may only be consumed once");
|
|
257
|
+
consumed = true;
|
|
258
|
+
const result = await pending;
|
|
259
|
+
if ("failure" in result) throw result.failure;
|
|
260
|
+
if (options.client !== client || options.planned !== planned || context.client !== client || context.block !== block || lifecycleSourceClient(client) !== sourceClient ||
|
|
261
|
+
planned.account !== context.account || planned.plan.orchestrator !== context.orchestrator || planned.chainId !== context.chainId ||
|
|
262
|
+
block.number !== blockNumber || block.hash !== blockHash) throw new LifecyclePlanningError("LIMIT_CONTEXT_MISMATCH", "Prepared simulation context differs from the bound source/chain/account/core/block");
|
|
263
|
+
return simulateLaunchTransactionsWithContext(options, transactions, context, result.observations);
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function simulateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], pinnedBlock?: LifecycleBlock): Promise<LifecycleSimulation> {
|
|
268
|
+
// Standalone calls always produce fresh context, including live chain and fees.
|
|
269
|
+
if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === lifecycleSourceClient(options.client)) throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
|
|
270
|
+
const block = pinnedBlock ?? await readLifecycleBlock(options.client);
|
|
271
|
+
return prepareLaunchSimulation(options, block, transactions.length !== 0)(transactions);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function simulateLaunchTransactionsWithContext(
|
|
275
|
+
options: SimulateLaunchPlanOptions,
|
|
276
|
+
transactions: readonly LifecycleTransaction[],
|
|
277
|
+
context: LifecycleLimitContext,
|
|
278
|
+
observations: [{ limits: ResolvedLifecycleLimits; configuration: LifecycleLimits }, unknown, unknown, unknown],
|
|
279
|
+
): Promise<LifecycleSimulation> {
|
|
280
|
+
const { client, planned } = options;
|
|
281
|
+
const sourceClient = lifecycleSourceClient(client);
|
|
282
|
+
const { block } = context;
|
|
283
|
+
const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = observations;
|
|
247
284
|
const base = { confidence: "stateful" as const, executionProof: "failed" as const, protocolFit: "unknown" as const, transportPreflight: "not-requested" as const, blockNumber: block.number, blockHash: block.hash, account: planned.account, chainId: planned.chainId, limits };
|
|
248
285
|
if (rpcQuantity(chainId, "chain ID") !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
|
|
249
286
|
const accountCode = rpcHex(accountCodeValue, "account code");
|
|
@@ -256,21 +293,31 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
256
293
|
const gasPrice = rpcQuantity(gasPriceValue, "gas price");
|
|
257
294
|
if (gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
|
|
258
295
|
const buffer = (gas: bigint) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
|
|
296
|
+
const computeCeiling = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
|
|
297
|
+
const capGas = transactions.map(() => computeCeiling);
|
|
298
|
+
let parallelMeasurement: Promise<{ steps: LifecycleSimulationStep[] } | { failure: unknown }> | undefined;
|
|
259
299
|
const posterGas: bigint[] = [];
|
|
260
300
|
if (limits.protocol === "nitro") {
|
|
301
|
+
// The isolated capability probe, poster quote and permissive cap measurement
|
|
302
|
+
// have no data dependency. Only an actually batching source overlaps all three.
|
|
303
|
+
const nitroEvidence = Promise.all([
|
|
304
|
+
lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, block, limits, gasPrice)),
|
|
305
|
+
lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
|
|
306
|
+
]);
|
|
307
|
+
if (sourceClient.supportsReadBatching === true) {
|
|
308
|
+
// Handle settlement immediately so a failed measurement cannot escape while
|
|
309
|
+
// the independent metering evidence is still pending. It is never admission.
|
|
310
|
+
parallelMeasurement = lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false))
|
|
311
|
+
.then((steps) => ({ steps }), (failure: unknown) => ({ failure }));
|
|
312
|
+
}
|
|
261
313
|
try {
|
|
262
|
-
const [, estimates] = await
|
|
263
|
-
lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, block, limits, gasPrice)),
|
|
264
|
-
lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
|
|
265
|
-
]);
|
|
314
|
+
const [, estimates] = await nitroEvidence;
|
|
266
315
|
posterGas.push(...estimates);
|
|
267
316
|
} catch (failure) {
|
|
268
317
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
269
318
|
return { ...base, confidence: "provisional", executionProof: "unavailable", backend: "unavailable", admitted: false, steps: [], reason: `Native Nitro compute/poster proof unavailable: ${failure instanceof Error ? failure.message : String(failure)}. Generic Anvil/Hardhat forks cannot prove ArbOS metering.` };
|
|
270
319
|
}
|
|
271
320
|
}
|
|
272
|
-
const computeCeiling = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
|
|
273
|
-
const capGas = transactions.map(() => computeCeiling);
|
|
274
321
|
// The poster quote allocates gas only. The native exact replay enforces actual
|
|
275
322
|
// compute hold; subtracting this approximate budget from gasUsed would not.
|
|
276
323
|
const discoveryGas = transactions.map((_, index) => {
|
|
@@ -282,7 +329,14 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
282
329
|
let backend: "eth_simulateV1" | "controlled-fork" = "eth_simulateV1";
|
|
283
330
|
let measured: LifecycleSimulationStep[];
|
|
284
331
|
// Permissive cap measurement is not proof; only the exact-gas validated replay admits a plan.
|
|
285
|
-
try {
|
|
332
|
+
try {
|
|
333
|
+
if (parallelMeasurement === undefined) measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false));
|
|
334
|
+
else {
|
|
335
|
+
const observation = await parallelMeasurement;
|
|
336
|
+
if ("failure" in observation) throw observation.failure;
|
|
337
|
+
measured = observation.steps;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
286
340
|
catch (failure) {
|
|
287
341
|
if (options.fork === undefined || limits.protocol === "nitro") {
|
|
288
342
|
await assertLifecycleBlock(client, block, planned.chainId);
|