@black-market/sdk 0.6.1 → 0.6.2
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 +15 -11
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/progress.js +3 -3
- 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 +18 -10
- 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 +35 -0
- package/package.json +1 -1
- package/src/lifecycle/planner.ts +15 -11
- package/src/lifecycle/progress.ts +3 -3
- package/src/lifecycle/rpc.ts +58 -5
- package/src/lifecycle/simulation.ts +18 -10
- 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
|
|
|
@@ -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) {
|
|
@@ -2,7 +2,7 @@ import { concatHex, decodeFunctionData, encodeAbiParameters, encodeFunctionData,
|
|
|
2
2
|
import { abyssLifecycleAdapterAbi, 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)[])"));
|
|
@@ -144,7 +144,7 @@ export async function validateV4LifecycleOracle(options: {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
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 (
|
|
147
|
+
return withLifecycleReadClient(options.client, (client) => lifecycleStage(client, "profile.certification", async () => {
|
|
148
148
|
const [block, chainIdValue] = await Promise.all([
|
|
149
149
|
pinnedBlock ?? readLifecycleBlock(client),
|
|
150
150
|
lifecycleRpc(client, "eth_chainId"),
|
|
@@ -201,7 +201,7 @@ export async function readLifecycleProfiles(options: { client: LifecycleRpcClien
|
|
|
201
201
|
if (protocolMaximumDeveloperFeeBpsPromise !== undefined) await protocolMaximumDeveloperFeeBpsPromise;
|
|
202
202
|
if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, chainId);
|
|
203
203
|
return profiles;
|
|
204
|
-
});
|
|
204
|
+
}));
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
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 } {
|
|
@@ -238,24 +238,32 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
238
238
|
if (options.fork !== undefined && lifecycleSourceClient(options.fork.client) === sourceClient) throw new LifecyclePlanningError("UNSAFE_FORK", "Source RPC must never receive controlled simulation transactions/snapshots/resets");
|
|
239
239
|
const block = pinnedBlock ?? await readLifecycleBlock(client);
|
|
240
240
|
const context = { client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId };
|
|
241
|
-
const { limits, configuration } = await
|
|
241
|
+
const [{ limits, configuration }, chainId, accountCodeValue, gasPriceValue] = await lifecycleStage(client, "simulation.context", () => Promise.all([
|
|
242
|
+
resolveLifecycleLimits(context, options.limits === undefined ? planned.limits : options.limits),
|
|
243
|
+
lifecycleRpc(client, "eth_chainId"),
|
|
244
|
+
lifecyclePinnedRpc(client, "eth_getCode", [planned.account, toHex(block.number)], block),
|
|
245
|
+
transactions.length === 0 ? undefined : lifecycleRpc(client, "eth_gasPrice"),
|
|
246
|
+
]));
|
|
242
247
|
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(
|
|
248
|
+
if (rpcQuantity(chainId, "chain ID") !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
|
|
249
|
+
const accountCode = rpcHex(accountCodeValue, "account code");
|
|
245
250
|
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
251
|
for (const transaction of transactions) {
|
|
247
252
|
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
253
|
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
254
|
}
|
|
250
255
|
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(
|
|
256
|
+
const gasPrice = rpcQuantity(gasPriceValue, "gas price");
|
|
252
257
|
if (gasPrice >= 1n << 256n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Gas price must be a uint256 quantity");
|
|
253
258
|
const buffer = (gas: bigint) => (gas * BigInt(10000 + limits.headroomBps) + 9999n) / 10000n;
|
|
254
259
|
const posterGas: bigint[] = [];
|
|
255
260
|
if (limits.protocol === "nitro") {
|
|
256
261
|
try {
|
|
257
|
-
|
|
258
|
-
|
|
262
|
+
const [, estimates] = await Promise.all([
|
|
263
|
+
lifecycleStage(client, "simulation.nitro", () => proveNitroBackend(client, planned, block, limits, gasPrice)),
|
|
264
|
+
lifecycleStage(client, "simulation.poster", () => Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas)))),
|
|
265
|
+
]);
|
|
266
|
+
posterGas.push(...estimates);
|
|
259
267
|
} catch (failure) {
|
|
260
268
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
261
269
|
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.` };
|
|
@@ -274,7 +282,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
274
282
|
let backend: "eth_simulateV1" | "controlled-fork" = "eth_simulateV1";
|
|
275
283
|
let measured: LifecycleSimulationStep[];
|
|
276
284
|
// Permissive cap measurement is not proof; only the exact-gas validated replay admits a plan.
|
|
277
|
-
try { measured = await simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false); }
|
|
285
|
+
try { measured = await lifecycleStage(client, "simulation.measure", () => simulateRpcPass(client, planned, block, transactions, capGas, limits, gasPrice, false)); }
|
|
278
286
|
catch (failure) {
|
|
279
287
|
if (options.fork === undefined || limits.protocol === "nitro") {
|
|
280
288
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
@@ -301,7 +309,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
301
309
|
let verified: LifecycleSimulationStep[];
|
|
302
310
|
let validatedReplayError: string | undefined;
|
|
303
311
|
try {
|
|
304
|
-
if (backend === "eth_simulateV1") verified = await simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true);
|
|
312
|
+
if (backend === "eth_simulateV1") verified = await lifecycleStage(client, "simulation.replay", () => simulateRpcPass(client, planned, block, transactions, gasLimits, limits, gasPrice, true));
|
|
305
313
|
else {
|
|
306
314
|
if (options.fork === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Controlled fork is missing for the exact-gas replay");
|
|
307
315
|
verified = await simulateForkPass(options.fork, planned, block, transactions, gasLimits, gasPrice, false, limits.headroomBps);
|
|
@@ -326,7 +334,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
326
334
|
try {
|
|
327
335
|
const ceilingLimits = discoveryGas;
|
|
328
336
|
const retried = backend === "eth_simulateV1"
|
|
329
|
-
? await simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true)
|
|
337
|
+
? await lifecycleStage(client, "simulation.envelope", () => simulateRpcPass(client, planned, block, transactions, ceilingLimits, limits, gasPrice, true))
|
|
330
338
|
: options.fork === undefined ? undefined : await simulateForkPass(options.fork, planned, block, transactions, ceilingLimits, gasPrice, false, limits.headroomBps);
|
|
331
339
|
if (retried === undefined || retried.some((step) => !step.success)) {
|
|
332
340
|
await assertLifecycleBlock(client, block, planned.chainId);
|
package/src/lifecycle/types.ts
CHANGED
|
@@ -4,11 +4,28 @@ import type {
|
|
|
4
4
|
MarketLiveStateV1, PreparedMarketV1, ProfileRegistrationV1, ProfileTopologyV1, LaunchEnvelopeV2, LifecycleDeveloperTerms,
|
|
5
5
|
} from "./schema.js";
|
|
6
6
|
|
|
7
|
+
/** Local opt-in timing only: never contains request arguments, addresses, results or errors. */
|
|
8
|
+
export type LifecycleDiagnosticStage =
|
|
9
|
+
| "plan" | "plan.context" | "plan.domain" | "plan.progress" | "plan.inputs" | "rpc"
|
|
10
|
+
| "profile.certification" | "simulation.context" | "simulation.nitro"
|
|
11
|
+
| "simulation.poster" | "simulation.measure" | "simulation.replay" | "simulation.envelope";
|
|
12
|
+
export type LifecycleDiagnosticEvent = {
|
|
13
|
+
stage: LifecycleDiagnosticStage;
|
|
14
|
+
phase: "queued" | "start" | "success" | "failure" | "reuse";
|
|
15
|
+
durationMs?: number;
|
|
16
|
+
queueMs?: number;
|
|
17
|
+
requestId?: number;
|
|
18
|
+
method?: string;
|
|
19
|
+
};
|
|
20
|
+
export type LifecycleDiagnosticListener = (event: LifecycleDiagnosticEvent) => void | Promise<void>;
|
|
21
|
+
|
|
7
22
|
/** viem public clients or a raw JSON-RPC client; neither selects a transport. */
|
|
8
23
|
export type LifecycleRpcClient = {
|
|
9
24
|
request(args: { method: string; params?: readonly unknown[] }): Promise<unknown>;
|
|
10
25
|
/** Opt in to bounded parallel reads only when the supplied transport batches concurrent requests. */
|
|
11
26
|
supportsReadBatching?: boolean;
|
|
27
|
+
/** Optional local observer. Listener failures cannot change execution or admission. */
|
|
28
|
+
onDiagnostic?: LifecycleDiagnosticListener;
|
|
12
29
|
};
|
|
13
30
|
export type LifecycleBlock = { number: bigint; hash: Hash; timestamp: bigint; gasLimit: bigint; baseFeePerGas?: bigint };
|
|
14
31
|
export type LifecycleLimitContext = { client: LifecycleRpcClient; account: Address; orchestrator: Address; block: LifecycleBlock; chainId: bigint };
|