@black-market/sdk 0.6.0 → 0.6.1
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 +192 -131
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +204 -143
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +5 -0
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +89 -8
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +8 -7
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/types.d.ts +2 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +72 -0
- package/package.json +1 -1
- package/src/lifecycle/planner.ts +158 -107
- package/src/lifecycle/progress.ts +187 -126
- package/src/lifecycle/rpc.ts +90 -8
- package/src/lifecycle/simulation.ts +8 -6
- package/src/lifecycle/types.ts +2 -0
package/src/lifecycle/rpc.ts
CHANGED
|
@@ -3,8 +3,81 @@ import { LifecyclePlanningError, type LifecycleBlock, type LifecycleLimitContext
|
|
|
3
3
|
import { lifecycleRegistryAbi } from "./abi.js";
|
|
4
4
|
import type { ProfileTopologyV1 } from "./schema.js";
|
|
5
5
|
|
|
6
|
+
const lifecycleReadConcurrency = 8;
|
|
7
|
+
|
|
8
|
+
/** Owns only one invocation's immutable RPC observations, never live checks or writes. */
|
|
9
|
+
class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
10
|
+
private readonly reads = new Map<string, Promise<unknown>>();
|
|
11
|
+
private readonly active = new Set<Promise<void>>();
|
|
12
|
+
private closed = false;
|
|
13
|
+
private readonly concurrency: number;
|
|
14
|
+
constructor(readonly source: LifecycleRpcClient) {
|
|
15
|
+
this.concurrency = source.supportsReadBatching === true ? lifecycleReadConcurrency : 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async request(args: { method: string; params?: readonly unknown[] }): Promise<unknown> {
|
|
19
|
+
this.assertOpen();
|
|
20
|
+
const result = await this.source.request(args);
|
|
21
|
+
this.assertOpen();
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private assertOpen(): void {
|
|
26
|
+
if (this.closed) throw new LifecyclePlanningError("READ_SCOPE_CLOSED", "Lifecycle read invocation has already settled");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async run(method: string, params: readonly unknown[]): Promise<unknown> {
|
|
30
|
+
this.assertOpen();
|
|
31
|
+
while (this.active.size >= this.concurrency) await Promise.race(this.active);
|
|
32
|
+
this.assertOpen();
|
|
33
|
+
const pending = this.request({ method, params });
|
|
34
|
+
const settled = pending.then(
|
|
35
|
+
() => { this.active.delete(settled); },
|
|
36
|
+
() => { this.active.delete(settled); },
|
|
37
|
+
);
|
|
38
|
+
this.active.add(settled);
|
|
39
|
+
return pending;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
read(method: string, params: readonly unknown[], block: LifecycleBlock): Promise<unknown> {
|
|
43
|
+
this.assertOpen();
|
|
44
|
+
// Source identity is this scope's original client. The complete wire call includes
|
|
45
|
+
// caller, value, gas and any other call fields; no selector-only/ABI-name aliasing.
|
|
46
|
+
const key = JSON.stringify([block.number.toString(), block.hash.toLowerCase(), method, params]);
|
|
47
|
+
const existing = this.reads.get(key);
|
|
48
|
+
if (existing !== undefined) return existing;
|
|
49
|
+
const pending = this.run(method, params);
|
|
50
|
+
this.reads.set(key, pending);
|
|
51
|
+
void pending.catch(() => { if (this.reads.get(key) === pending) this.reads.delete(key); });
|
|
52
|
+
return pending;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
close(): void {
|
|
56
|
+
this.closed = true;
|
|
57
|
+
this.reads.clear();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Internal callers join the same invocation; no process-wide/source-wide read cache. */
|
|
62
|
+
export async function withLifecycleReadClient<T>(source: LifecycleRpcClient, work: (client: LifecycleRpcClient) => Promise<T>): Promise<T> {
|
|
63
|
+
if (source instanceof InvocationLifecycleClient) return work(source);
|
|
64
|
+
const client = new InvocationLifecycleClient(source);
|
|
65
|
+
try { return await work(client); }
|
|
66
|
+
finally { client.close(); }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function lifecycleSourceClient(client: LifecycleRpcClient): LifecycleRpcClient {
|
|
70
|
+
return client instanceof InvocationLifecycleClient ? client.source : client;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Only explicitly pinned fixed-state reads may participate in invocation reuse. */
|
|
74
|
+
export async function lifecyclePinnedRpc(client: LifecycleRpcClient, method: "eth_call" | "eth_getCode" | "eth_getBalance", params: readonly unknown[], block: LifecycleBlock): Promise<unknown> {
|
|
75
|
+
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");
|
|
76
|
+
return client instanceof InvocationLifecycleClient ? client.read(method, params, block) : lifecycleRpc(client, method, params);
|
|
77
|
+
}
|
|
78
|
+
|
|
6
79
|
export async function lifecycleRpc(client: LifecycleRpcClient, method: string, params: readonly unknown[] = []): Promise<unknown> {
|
|
7
|
-
return client.request({ method, params });
|
|
80
|
+
return client instanceof InvocationLifecycleClient ? client.run(method, params) : client.request({ method, params });
|
|
8
81
|
}
|
|
9
82
|
export function rpcObject(value: unknown, label: string): Record<string, unknown> {
|
|
10
83
|
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", `${label} must be an object`);
|
|
@@ -38,7 +111,11 @@ export async function assertLifecycleBlock(client: LifecycleRpcClient, block: Li
|
|
|
38
111
|
}
|
|
39
112
|
export async function readLifecycleContract<T>(client: LifecycleRpcClient, to: Address, abi: Abi, functionName: string, args: readonly unknown[], block: LifecycleBlock, from?: Address): Promise<T> {
|
|
40
113
|
const data = encodeFunctionData({ abi, functionName, args });
|
|
41
|
-
const
|
|
114
|
+
const request = [{ to, from, data }, toHex(block.number)];
|
|
115
|
+
const getter = abi.some((item) => item.type === "function" && item.name === functionName && (item.stateMutability === "view" || item.stateMutability === "pure"));
|
|
116
|
+
const result = rpcHex(await (getter && functionName !== "readLaunchProgress"
|
|
117
|
+
? lifecyclePinnedRpc(client, "eth_call", request, block)
|
|
118
|
+
: lifecycleRpc(client, "eth_call", request)), functionName);
|
|
42
119
|
// ABI decoding validates scalar widths and tuple layout; T names that exact frozen ABI result.
|
|
43
120
|
const decoded = decodeFunctionResult({ abi, functionName, data: result }) as T;
|
|
44
121
|
return decoded;
|
|
@@ -61,7 +138,7 @@ const maxUint64 = (1n << 64n) - 1n;
|
|
|
61
138
|
/** A poster quote is an envelope budget, never an execution/compute proof. */
|
|
62
139
|
export async function readNitroPosterGas(transaction: LifecycleTransaction, context: LifecycleLimitContext): Promise<{ posterGas: bigint; baseFee: bigint; l1BaseFeeEstimate: bigint }> {
|
|
63
140
|
const data = encodeFunctionData({ abi: nitroNodeInterfaceAbi, functionName: "gasEstimateL1Component", args: [transaction.to, false, transaction.data] });
|
|
64
|
-
const result = rpcHex(await
|
|
141
|
+
const result = rpcHex(await lifecyclePinnedRpc(context.client, "eth_call", [{ from: transaction.from, to: nitroNodeInterface, data, value: toHex(transaction.value) }, toHex(context.block.number)], context.block), "Nitro poster estimate");
|
|
65
142
|
if (result.length !== 194) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Nitro poster estimate must contain exactly three ABI words");
|
|
66
143
|
const [posterGas, baseFee, l1BaseFeeEstimate] = decodeFunctionResult({ abi: nitroNodeInterfaceAbi, functionName: "gasEstimateL1Component", data: result });
|
|
67
144
|
if (posterGas < 0n || posterGas > maxUint64 || baseFee <= 0n || l1BaseFeeEstimate < 0n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Nitro poster estimate has invalid gas or fee values");
|
|
@@ -70,14 +147,15 @@ export async function readNitroPosterGas(transaction: LifecycleTransaction, cont
|
|
|
70
147
|
|
|
71
148
|
async function readNitroScalar(context: LifecycleLimitContext, to: Address, abi: Abi, functionName: string): Promise<bigint> {
|
|
72
149
|
const data = encodeFunctionData({ abi, functionName });
|
|
73
|
-
const result = rpcHex(await
|
|
150
|
+
const result = rpcHex(await lifecyclePinnedRpc(context.client, "eth_call", [{ to, data }, toHex(context.block.number)], context.block), functionName);
|
|
74
151
|
if (result.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", `${functionName} must return exactly one ABI word`);
|
|
75
152
|
return decodeFunctionResult({ abi, functionName, data: result }) as bigint;
|
|
76
153
|
}
|
|
77
154
|
|
|
78
155
|
export async function resolveLifecycleLimits(context: LifecycleLimitContext, source?: LifecycleLimitSource): Promise<{ limits: ResolvedLifecycleLimits; configuration: LifecycleLimits }> {
|
|
79
156
|
let configuration: LifecycleLimits;
|
|
80
|
-
|
|
157
|
+
const policyContext = typeof source === "function" && context.client instanceof InvocationLifecycleClient ? { ...context, client: context.client.source } : context;
|
|
158
|
+
try { configuration = source === undefined ? {} : typeof source === "function" ? await source(policyContext) : source; }
|
|
81
159
|
catch (failure) { throw new LifecyclePlanningError("LIMIT_SOURCE_FAILED", `Supplied execution policy could not be resolved: ${failure instanceof Error ? failure.message : String(failure)}`); }
|
|
82
160
|
if (configuration === null || typeof configuration !== "object" || Array.isArray(configuration)) throw new LifecyclePlanningError("INVALID_LIMITS", "Supplied execution policy must be an object");
|
|
83
161
|
const headroomBps = configuration.headroomBps === undefined ? 1500 : configuration.headroomBps;
|
|
@@ -98,12 +176,16 @@ export async function resolveLifecycleLimits(context: LifecycleLimitContext, sou
|
|
|
98
176
|
let arbOSVersion: bigint | undefined; let maxTxComputeGas: bigint | undefined; let maxBlockComputeGas: bigint | undefined;
|
|
99
177
|
if (protocol === "nitro") {
|
|
100
178
|
try {
|
|
101
|
-
const rawVersion = await
|
|
179
|
+
const [rawVersion, txComputeGas, blockComputeGas] = await Promise.all([
|
|
180
|
+
readNitroScalar(context, nitroArbSys, nitroArbSysAbi, "arbOSVersion"),
|
|
181
|
+
readNitroScalar(context, nitroGasInfo, nitroGasInfoAbi, "getMaxTxGasLimit"),
|
|
182
|
+
readNitroScalar(context, nitroGasInfo, nitroGasInfoAbi, "getMaxBlockGasLimit"),
|
|
183
|
+
]);
|
|
102
184
|
// ArbSys alone adds 55 to the logical Nitro ArbOS version.
|
|
103
185
|
if (typeof rawVersion !== "bigint" || rawVersion < 105n) throw new Error("ArbOS version 50 or newer is required (ArbSys raw version >= 105)");
|
|
104
186
|
arbOSVersion = rawVersion - 55n;
|
|
105
|
-
maxTxComputeGas =
|
|
106
|
-
maxBlockComputeGas =
|
|
187
|
+
maxTxComputeGas = txComputeGas;
|
|
188
|
+
maxBlockComputeGas = blockComputeGas;
|
|
107
189
|
if ([maxTxComputeGas, maxBlockComputeGas].some((limit) => typeof limit !== "bigint" || limit <= 0n || limit > maxUint64)) throw new Error("Nitro compute getters must be positive uint64 values");
|
|
108
190
|
} catch (failure) { throw new LifecyclePlanningError("NITRO_LIMITS_UNAVAILABLE", `Pinned Nitro compute limits could not be established: ${failure instanceof Error ? failure.message : String(failure)}; header gasLimit is not a fallback`); }
|
|
109
191
|
if (configuration.estimateDataFee !== undefined) throw new LifecyclePlanningError("INVALID_LIMITS", "Nitro poster fees are already included in gas; an external data-fee estimator would double-charge them");
|
|
@@ -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, lifecycleRpc, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readNitroPosterGas, resolveLifecycleLimits, rpcHex, rpcObject, rpcQuantity } from "./rpc.js";
|
|
4
|
+
import { assertLifecycleBlock, lifecyclePinnedRpc, lifecycleRpc, lifecycleSourceClient, 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 } {
|
|
@@ -234,13 +234,14 @@ async function simulateForkPass(fork: ControlledLifecycleFork, planned: PlannedL
|
|
|
234
234
|
|
|
235
235
|
export async function simulateLaunchTransactions(options: SimulateLaunchPlanOptions, transactions: readonly LifecycleTransaction[], pinnedBlock?: LifecycleBlock): Promise<LifecycleSimulation> {
|
|
236
236
|
const { client, planned } = options;
|
|
237
|
-
|
|
237
|
+
const sourceClient = lifecycleSourceClient(client);
|
|
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");
|
|
238
239
|
const block = pinnedBlock ?? await readLifecycleBlock(client);
|
|
239
240
|
const context = { client, account: planned.account, orchestrator: planned.plan.orchestrator, block, chainId: planned.chainId };
|
|
240
241
|
const { limits, configuration } = await resolveLifecycleLimits(context, options.limits === undefined ? planned.limits : options.limits);
|
|
241
242
|
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 };
|
|
242
243
|
if (rpcQuantity(await lifecycleRpc(client, "eth_chainId"), "chain ID") !== planned.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain changed before sequential simulation");
|
|
243
|
-
const accountCode = rpcHex(await
|
|
244
|
+
const accountCode = rpcHex(await lifecyclePinnedRpc(client, "eth_getCode", [planned.account, toHex(block.number)], block), "account code");
|
|
244
245
|
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" };
|
|
245
246
|
for (const transaction of transactions) {
|
|
246
247
|
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");
|
|
@@ -254,7 +255,7 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
254
255
|
if (limits.protocol === "nitro") {
|
|
255
256
|
try {
|
|
256
257
|
await proveNitroBackend(client, planned, block, limits, gasPrice);
|
|
257
|
-
|
|
258
|
+
posterGas.push(...await Promise.all(transactions.map(async (transaction) => buffer((await readNitroPosterGas(transaction, context)).posterGas))));
|
|
258
259
|
} catch (failure) {
|
|
259
260
|
await assertLifecycleBlock(client, block, planned.chainId);
|
|
260
261
|
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.` };
|
|
@@ -368,13 +369,14 @@ export async function simulateLaunchTransactions(options: SimulateLaunchPlanOpti
|
|
|
368
369
|
}
|
|
369
370
|
}
|
|
370
371
|
const estimates: LifecycleSimulationStep[] = [];
|
|
371
|
-
const balance = rpcQuantity(await
|
|
372
|
+
const balance = rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [planned.account, toHex(block.number)], block), "creator native balance");
|
|
372
373
|
let remainingBalance = balance;
|
|
373
374
|
let affordabilityFailure: string | undefined;
|
|
375
|
+
const feeContext = configuration.estimateDataFee === undefined || sourceClient === client ? context : { ...context, client: sourceClient };
|
|
374
376
|
for (let index = 0; index < verifiedSteps.length; index += 1) {
|
|
375
377
|
const step = verifiedSteps[index]; const transaction = transactions[index]; const gasLimit = admittedGasLimits[index];
|
|
376
378
|
if (step === undefined || transaction === undefined || gasLimit === undefined) throw new LifecyclePlanningError("INVALID_SIMULATION", "Missing verified fee context");
|
|
377
|
-
const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit },
|
|
379
|
+
const dataFee = limits.protocol === "nitro" ? 0n : configuration.estimateDataFee === undefined ? undefined : await configuration.estimateDataFee({ ...transaction, gas: gasLimit }, feeContext);
|
|
378
380
|
if (dataFee !== undefined && (typeof dataFee !== "bigint" || dataFee < 0n || dataFee >= 1n << 256n)) throw new LifecyclePlanningError("INVALID_DATA_FEE", "Data-fee oracle must return a nonnegative uint256 bigint amount");
|
|
379
381
|
// Nitro gas includes poster charges. Never add the quoted poster fee again.
|
|
380
382
|
const executionFee = gasLimit * gasPrice;
|
package/src/lifecycle/types.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
/** viem public clients or a raw JSON-RPC client; neither selects a transport. */
|
|
8
8
|
export type LifecycleRpcClient = {
|
|
9
9
|
request(args: { method: string; params?: readonly unknown[] }): Promise<unknown>;
|
|
10
|
+
/** Opt in to bounded parallel reads only when the supplied transport batches concurrent requests. */
|
|
11
|
+
supportsReadBatching?: boolean;
|
|
10
12
|
};
|
|
11
13
|
export type LifecycleBlock = { number: bigint; hash: Hash; timestamp: bigint; gasLimit: bigint; baseFeePerGas?: bigint };
|
|
12
14
|
export type LifecycleLimitContext = { client: LifecycleRpcClient; account: Address; orchestrator: Address; block: LifecycleBlock; chainId: bigint };
|