@black-market/sdk 0.12.0 → 0.14.0
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/README.md +96 -96
- package/dist/lifecycle/index.d.ts +3 -1
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +3 -1
- package/dist/lifecycle/index.js.map +1 -1
- package/dist/lifecycle/lens.d.ts +409 -0
- package/dist/lifecycle/lens.d.ts.map +1 -0
- package/dist/lifecycle/lens.js +30 -0
- package/dist/lifecycle/lens.js.map +1 -0
- package/dist/lifecycle/mining.d.ts +47 -0
- package/dist/lifecycle/mining.d.ts.map +1 -0
- package/dist/lifecycle/mining.js +89 -0
- package/dist/lifecycle/mining.js.map +1 -0
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +121 -78
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/presets.d.ts +3 -0
- package/dist/lifecycle/presets.d.ts.map +1 -1
- package/dist/lifecycle/presets.js +1 -0
- package/dist/lifecycle/presets.js.map +1 -1
- package/dist/lifecycle/progress.d.ts +51 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +149 -73
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +14 -4
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +171 -12
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts +5 -3
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +137 -115
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/snapshot.d.ts +8 -0
- package/dist/lifecycle/snapshot.d.ts.map +1 -0
- package/dist/lifecycle/snapshot.js +79 -0
- package/dist/lifecycle/snapshot.js.map +1 -0
- package/dist/lifecycle/types.d.ts +27 -5
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +627 -1495
- package/docs/reference.md +365 -0
- package/examples/.env.example +3 -2
- package/examples/README.md +171 -552
- package/examples/launch-example-support.ts +11 -3
- package/examples/launch-example.ts +1 -1
- package/examples/launch-lifecycle-smoke.ts +1 -1
- package/package.json +1 -1
- package/src/lifecycle/index.ts +5 -1
- package/src/lifecycle/lens.ts +42 -0
- package/src/lifecycle/mining.ts +80 -0
- package/src/lifecycle/planner.ts +122 -73
- package/src/lifecycle/presets.ts +4 -0
- package/src/lifecycle/progress.ts +132 -59
- package/src/lifecycle/rpc.ts +163 -13
- package/src/lifecycle/simulation.ts +134 -106
- package/src/lifecycle/snapshot.ts +72 -0
- package/src/lifecycle/types.ts +24 -6
package/src/lifecycle/presets.ts
CHANGED
|
@@ -61,6 +61,9 @@ export type LifecycleDeploymentPreset = {
|
|
|
61
61
|
readonly tokenFactory: Address;
|
|
62
62
|
readonly tokenFactoryCodeHash: Hex;
|
|
63
63
|
readonly fundingEscrow: Address;
|
|
64
|
+
/** Optional read-only acceleration contracts. An unset lens is not a deployed feature. */
|
|
65
|
+
readonly hookSaltMiner?: Address;
|
|
66
|
+
readonly launchLens?: Address;
|
|
64
67
|
readonly profiles: readonly LifecycleProfilePreset[];
|
|
65
68
|
readonly provenance: {
|
|
66
69
|
readonly sourceCommit: string;
|
|
@@ -325,6 +328,7 @@ const knownDeployment: LifecycleDeploymentPreset = Object.freeze({
|
|
|
325
328
|
tokenFactory: "0xd3cE64E49224a9a96075633f63761FE6BE22FE30",
|
|
326
329
|
tokenFactoryCodeHash: "0x6bbbdfe93ec615b7f1e70bac5e69aa890eba5e5f248f25e7022e866b4064d541",
|
|
327
330
|
fundingEscrow: "0x7bc77946CeF52A178583FCe1EEB7751983cEC703",
|
|
331
|
+
hookSaltMiner: "0x2f06176ba739b609b6a7334e2e6647558ba78ad5",
|
|
328
332
|
profiles: Object.freeze([
|
|
329
333
|
abyssPreset(KnownLifecycleProfile.Abyss0, "0x49b2ed65e2021247951193dd2c671b40867b6e064af65a2d98a70f57556412fc"),
|
|
330
334
|
abyssPreset(KnownLifecycleProfile.Abyss1, "0x2f6f494f8810473b3c0ac6b03b1a1353d067ffa5c86d3cba16b04c6b61f73c05"),
|
|
@@ -2,16 +2,17 @@ import { concatHex, decodeFunctionData, encodeAbiParameters, encodeFunctionData,
|
|
|
2
2
|
import { launchLifecycleAbi, lifecycleAdapterAbi, lifecycleDirectoryAbi, lifecycleErc20Abi, lifecycleOracleFactoryAbi, lifecycleRegistryAbi, poolFeeCollectorFactoryV1Abi, poolFeeCollectorFactoryConfigV6Abi, lifecycleV4HookAbi, lifecycleV4LockerAbi, poolHookDeployerV1Abi, poolHookDeployerConfigV6Abi, sharedHookDeployerV1Abi, fixedFeePoolHookV1Abi, fixedFeePoolHookConfigV6Abi, poolMarketAdapterV1Abi, sharedMarketAdapterV1Abi } from "./abi.js";
|
|
3
3
|
import { buildPoolBoundHookParameters, decodePoolBoundV4LifecycleMarketConfig, encodePoolBoundHookParameters, hasLifecycleV4HookPermissions, isPoolBoundV4ConfigVersion, poolBoundHookInitCodeHash, predictPoolBoundHookAddress, validateV4LifecycleMarket, V4_LIFECYCLE_CONFIG_SCHEMA, V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA_V5, V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA_V6, type PoolBoundHookParameters, type PoolBoundLifecycleConfigVersion } from "./markets.js";
|
|
4
4
|
import { hashLaunchIdentity, hashLaunchPlan, hashLaunchBounds, hashLaunchDependencies, hashLifecycleProfile, lifecycleTokenComponents, 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 {
|
|
5
|
+
import { assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleChainId, readLifecycleContract, readLifecycleImmutable, readLifecycleStaticContract, 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
|
import { deriveKnownPoolBoundHookDeployment } from "./calibration.js";
|
|
8
8
|
import { getKnownLifecycleDeployment, getKnownLifecycleProfile } from "./presets.js";
|
|
9
9
|
import { ufgLaunchProxyV1Abi } from "./ufg-abi.js";
|
|
10
10
|
import { assertUfgLaunchAccount, hashUfgLaunchConfig, UFG_LAUNCH_PROXY_ADDRESS, type UfgLaunchConfig } from "./ufg-config.js";
|
|
11
|
+
import { lifecycleLensSnapshot } from "./lens.js";
|
|
11
12
|
|
|
12
13
|
export const ABYSS_LIFECYCLE_CONFIG_SCHEMA = keccak256(stringToHex("(uint8,uint24,bytes32,uint160,(int24,int24,uint128,uint256)[])"));
|
|
13
14
|
|
|
14
|
-
const lifecycleTokenPredictionAbi = [{
|
|
15
|
+
export const lifecycleTokenPredictionAbi = [{
|
|
15
16
|
type: "function", name: "predictToken", stateMutability: "view",
|
|
16
17
|
inputs: [{ name: "launchId", type: "bytes32" }, { name: "config", type: "tuple", components: lifecycleTokenComponents }],
|
|
17
18
|
outputs: [{ type: "address" }],
|
|
@@ -21,16 +22,24 @@ const lifecycleTokenPredictionAbi = [{
|
|
|
21
22
|
* avoiding full-plan prediction reads after salts or buy minima change. */
|
|
22
23
|
export async function readLifecycleTokenAtBlock(client: LifecycleRpcClient, plan: LaunchPlanV1, block: LifecycleBlock): Promise<Address> {
|
|
23
24
|
const deployment = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator });
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
if (deployment === undefined) return readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block);
|
|
26
|
+
const launchId = hashLaunchIdentity(plan);
|
|
27
|
+
const data = encodeFunctionData({ abi: lifecycleTokenPredictionAbi, functionName: "predictToken", args: [launchId, plan.token] });
|
|
28
|
+
const key = `${plan.chainId}:${plan.orchestrator.toLowerCase()}:${deployment.tokenFactory.toLowerCase()}:${deployment.tokenFactoryCodeHash}:${data}`;
|
|
29
|
+
return readLifecycleImmutable(client, key, () =>
|
|
30
|
+
readLifecycleContract<Address>(client, deployment.tokenFactory, lifecycleTokenPredictionAbi, "predictToken", [launchId, plan.token], block, undefined, false, true));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Internal pinned head observation. Known cores do not inspect msg.sender. */
|
|
34
|
+
export function readLifecycleHeadProgress(client: LifecycleRpcClient, plan: LaunchPlanV1, account: Address, block: LifecycleBlock): Promise<LaunchProgressV1> {
|
|
35
|
+
const known = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator }) !== undefined;
|
|
36
|
+
return readLifecycleContract<LaunchProgressV1>(client, plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [hashLaunchIdentity(plan)], block, account, false, known);
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
export async function predictLifecycleToken(options: { client: LifecycleRpcClient; plan: LaunchPlanV1 }): Promise<Address> {
|
|
30
40
|
return withLifecycleReadClient(options.client, async (client) => {
|
|
31
41
|
const block = await readLifecycleBlock(client);
|
|
32
42
|
const token = await readLifecycleTokenAtBlock(client, options.plan, block);
|
|
33
|
-
await assertLifecycleBlock(client, block, options.plan.chainId);
|
|
34
43
|
return token;
|
|
35
44
|
});
|
|
36
45
|
}
|
|
@@ -224,10 +233,10 @@ export async function validateV4LifecycleOracle(options: {
|
|
|
224
233
|
|
|
225
234
|
export async function readLifecycleProfiles(options: { client: LifecycleRpcClient; orchestrator: Address; profileIds?: readonly Hex[]; offset?: bigint; limit?: bigint }, pinnedBlock?: LifecycleBlock): Promise<LifecycleProfile[]> {
|
|
226
235
|
return withLifecycleReadClient(options.client, (client) => lifecycleStage(client, "profile.certification", async () => {
|
|
227
|
-
const chainIdPromise =
|
|
236
|
+
const chainIdPromise = readLifecycleChainId(client);
|
|
228
237
|
// Block-bound discovery does not depend on chain identity. Join both branches
|
|
229
238
|
// immediately so a failed chain read cannot become detached or a profile refusal.
|
|
230
|
-
const [
|
|
239
|
+
const [, { profiles }] = await Promise.all([
|
|
231
240
|
chainIdPromise,
|
|
232
241
|
(async () => {
|
|
233
242
|
const block = pinnedBlock ?? await readLifecycleBlock(client);
|
|
@@ -293,7 +302,6 @@ export async function readLifecycleProfiles(options: { client: LifecycleRpcClien
|
|
|
293
302
|
return { block, profiles };
|
|
294
303
|
})(),
|
|
295
304
|
]);
|
|
296
|
-
if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, chainId);
|
|
297
305
|
return profiles;
|
|
298
306
|
}));
|
|
299
307
|
}
|
|
@@ -303,7 +311,7 @@ async function readPoolBoundHookDeploymentDetails(options: { client: LifecycleRp
|
|
|
303
311
|
const market = plan.markets[marketIndex];
|
|
304
312
|
if (market === undefined || !isPoolBoundV4ConfigVersion(market.configVersion)) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Market index must select explicit pool-bound config version 5 or 6");
|
|
305
313
|
let chainId: bigint;
|
|
306
|
-
try { chainId =
|
|
314
|
+
try { chainId = await readLifecycleChainId(client); }
|
|
307
315
|
finally { assertDerivedReadClientOpen(client); }
|
|
308
316
|
if (hashLaunchPlan(plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while observing the hook deployment chain");
|
|
309
317
|
if (chainId !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
|
|
@@ -419,26 +427,28 @@ async function readPoolBoundHookDeploymentDetailsFresh(options: { client: Lifecy
|
|
|
419
427
|
export async function readPoolBoundHookDeployment(options: { client: LifecycleRpcClient; plan: LaunchPlanV1; marketIndex: number }, pinnedBlock?: LifecycleBlock): Promise<PoolBoundHookDeployment> {
|
|
420
428
|
const commitment = hashLaunchPlan(options.plan);
|
|
421
429
|
const block = pinnedBlock ?? await readLifecycleBlock(options.client);
|
|
422
|
-
|
|
430
|
+
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
423
431
|
const { deployment } = await readPoolBoundHookDeploymentDetails({ ...options, client }, block, commitment);
|
|
424
432
|
if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while reading hook deployment metadata");
|
|
425
|
-
if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, options.plan.chainId);
|
|
426
433
|
return { ...deployment };
|
|
427
434
|
});
|
|
435
|
+
if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during the final hook deployment guard");
|
|
436
|
+
return result;
|
|
428
437
|
}
|
|
429
438
|
|
|
430
439
|
/** Optional permissionless predeployment calldata; no token deployment, pool registration or wallet broadcast. */
|
|
431
440
|
export async function buildPoolBoundHookDeploymentTransaction(options: { client: LifecycleRpcClient; plan: LaunchPlanV1; marketIndex: number }): Promise<{ to: Address; data: Hex; value: bigint; deployment: PoolBoundHookDeployment }> {
|
|
432
441
|
const commitment = hashLaunchPlan(options.plan);
|
|
433
442
|
const block = await readLifecycleBlock(options.client);
|
|
434
|
-
|
|
443
|
+
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
435
444
|
const { deployment, parameters, configVersion } = await readPoolBoundHookDeploymentDetails({ ...options, client }, block, commitment);
|
|
436
445
|
if (!hasLifecycleV4HookPermissions(deployment.predictedHook)) throw new LifecyclePlanningError("INVALID_HOOK_BITS", "Finalize a salt with the exact lifecycle hook permissions before predeployment");
|
|
437
446
|
if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while constructing hook deployment calldata");
|
|
438
|
-
await assertLifecycleBlock(client, block, options.plan.chainId);
|
|
439
447
|
const abi = configVersion === 5 ? poolHookDeployerV1Abi : poolHookDeployerConfigV6Abi;
|
|
440
448
|
return { to: deployment.deployer, data: encodeFunctionData({ abi, functionName: "deploy", args: [parameters, deployment.salt] }), value: 0n, deployment: { ...deployment } };
|
|
441
449
|
});
|
|
450
|
+
if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during the final hook transaction guard");
|
|
451
|
+
return result;
|
|
442
452
|
}
|
|
443
453
|
|
|
444
454
|
export function validateLifecycleMarketIdentity(identity: MarketIdentityV1, plan: LaunchPlanV1, token: Address, marketIndex: number): void {
|
|
@@ -455,7 +465,7 @@ export function validateLifecycleMarketIdentity(identity: MarketIdentityV1, plan
|
|
|
455
465
|
} else if (identity.venue !== LifecycleVenue.Abyss || identity.pool === zeroAddress || identity.factory === zeroAddress) throw new LifecyclePlanningError("MARKET_IDENTITY", "Abyss canonical factory/pool identity is missing");
|
|
456
466
|
}
|
|
457
467
|
|
|
458
|
-
async function transactionMatchesLaunch(transaction: Record<string, unknown>, options: ReadLaunchProgressOptions,
|
|
468
|
+
async function transactionMatchesLaunch(transaction: Record<string, unknown>, options: ReadLaunchProgressOptions, readBlock: () => Promise<LifecycleBlock>): Promise<boolean> {
|
|
459
469
|
const { planned } = options;
|
|
460
470
|
const from = rpcHex(transaction.from, "receipt transaction account");
|
|
461
471
|
if (from.toLowerCase() !== planned.account.toLowerCase()) throw new LifecyclePlanningError("RECEIPT_ACCOUNT", "Receipt transaction belongs to a different account");
|
|
@@ -484,56 +494,114 @@ async function transactionMatchesLaunch(transaction: Record<string, unknown>, op
|
|
|
484
494
|
if (decoded.functionName !== "approve" || rpcQuantity(transaction.value ?? "0x0", "approval native value") !== 0n) return false;
|
|
485
495
|
spender = decoded.args[0];
|
|
486
496
|
} catch { return false; }
|
|
487
|
-
const
|
|
497
|
+
const known = getKnownLifecycleDeployment({ chainId: planned.plan.chainId, orchestrator: planned.plan.orchestrator });
|
|
498
|
+
const escrow = planned.ufg !== undefined ? UFG_LAUNCH_PROXY_ADDRESS : known?.fundingEscrow ??
|
|
499
|
+
await readLifecycleContract<Address>(options.client, planned.plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], await readBlock());
|
|
488
500
|
return spender.toLowerCase() === escrow.toLowerCase();
|
|
489
501
|
}
|
|
490
502
|
|
|
491
|
-
|
|
503
|
+
type ReceiptObservation = {
|
|
504
|
+
pins: readonly { number: bigint; hash: Hex }[];
|
|
505
|
+
finish(headers: ReadonlyMap<bigint, Hex | undefined>): LifecycleReceiptStatus;
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
async function readReceipt(reference: LifecycleReceiptReference, options: ReadLaunchProgressOptions, block: LifecycleBlock, readIdentityBlock: (number: bigint) => Promise<LifecycleBlock>): Promise<ReceiptObservation> {
|
|
492
509
|
const effectiveHash = reference.replacementHash ?? reference.transactionHash;
|
|
493
510
|
const base = { transactionHash: reference.transactionHash, effectiveHash, replaced: reference.replacementHash !== undefined && reference.replacementHash.toLowerCase() !== reference.transactionHash.toLowerCase() };
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
511
|
+
const pins: { number: bigint; hash: Hex }[] = [];
|
|
512
|
+
const observedNumber = reference.observedBlockNumber, observedHash = reference.observedBlockHash;
|
|
513
|
+
if (observedNumber !== undefined && observedHash !== undefined) pins.push({ number: observedNumber, hash: observedHash });
|
|
514
|
+
const finish = (result: LifecycleReceiptStatus, failure?: { error: unknown }): ReceiptObservation => ({
|
|
515
|
+
pins,
|
|
516
|
+
finish(headers) {
|
|
517
|
+
for (const pin of pins) {
|
|
518
|
+
if (failure !== undefined && (pin.number !== observedNumber || pin.hash !== observedHash)) continue;
|
|
519
|
+
if (headers.get(pin.number)?.toLowerCase() !== pin.hash.toLowerCase()) return { ...base, status: "reorged", blockNumber: pin.number, blockHash: pin.hash };
|
|
520
|
+
}
|
|
521
|
+
if (failure !== undefined) throw failure.error;
|
|
522
|
+
return result;
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
try {
|
|
526
|
+
const [receiptValue, transactionValue] = await Promise.all([
|
|
527
|
+
lifecycleRpc(options.client, "eth_getTransactionReceipt", [effectiveHash]),
|
|
528
|
+
lifecycleRpc(options.client, "eth_getTransactionByHash", [effectiveHash]),
|
|
529
|
+
]);
|
|
530
|
+
if (receiptValue === null) return finish({ ...base, status: "pending", blockNumber: observedNumber, blockHash: observedHash });
|
|
531
|
+
if (transactionValue === null) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Receipt exists without its canonical transaction");
|
|
532
|
+
const receipt = rpcObject(receiptValue, "receipt");
|
|
533
|
+
const blockNumber = rpcQuantity(receipt.blockNumber, "receipt block number");
|
|
534
|
+
const blockHash = rpcHex(receipt.blockHash, "receipt block hash");
|
|
535
|
+
pins.push({ number: blockNumber, hash: blockHash });
|
|
536
|
+
const matches = await transactionMatchesLaunch(rpcObject(transactionValue, "receipt transaction"), options,
|
|
537
|
+
() => readIdentityBlock(blockNumber));
|
|
538
|
+
const confirmations = block.number >= blockNumber ? block.number - blockNumber + 1n : 0n;
|
|
539
|
+
const required = reference.confirmations ?? 1;
|
|
540
|
+
if (!Number.isSafeInteger(required) || required < 1) throw new LifecyclePlanningError("INVALID_CONFIRMATIONS", "Receipt confirmations must be a positive integer");
|
|
541
|
+
const observation = finish({
|
|
542
|
+
...base, blockNumber, blockHash, confirmations,
|
|
543
|
+
status: confirmations < BigInt(required) ? "unconfirmed" : !matches ? "replacement-cancelled" :
|
|
544
|
+
rpcQuantity(receipt.status, "receipt status") !== 1n ? "reverted" : "confirmed",
|
|
545
|
+
});
|
|
546
|
+
return {
|
|
547
|
+
pins,
|
|
548
|
+
finish(headers) {
|
|
549
|
+
const result = observation.finish(headers);
|
|
550
|
+
if (result.status !== "reorged" && !matches && !base.replaced) throw new LifecyclePlanningError("RECEIPT_IDENTITY", "Receipt calldata does not belong to the exact committed launch");
|
|
551
|
+
return result;
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
} catch (error) {
|
|
555
|
+
if (error instanceof Error && error.name === "AbortError") throw error;
|
|
556
|
+
// A changed retained observation is a reorg even if the orphan's transaction
|
|
557
|
+
// or identity read is now unavailable. Otherwise preserve the original error.
|
|
558
|
+
return finish({ ...base, status: "pending" }, { error });
|
|
509
559
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function readReceipts(options: ReadLaunchProgressOptions, block: LifecycleBlock, confirmationDepth: number): Promise<LifecycleReceiptStatus[]> {
|
|
563
|
+
if (options.receipts === undefined || options.receipts.length === 0) return [];
|
|
564
|
+
const identityBlocks = new Map<bigint, Promise<LifecycleBlock>>();
|
|
565
|
+
const readIdentityBlock = (number: bigint) => {
|
|
566
|
+
if (number === block.number) return Promise.resolve(block);
|
|
567
|
+
let pending = identityBlocks.get(number);
|
|
568
|
+
if (pending === undefined) { pending = readLifecycleBlock(options.client, toHex(number)); identityBlocks.set(number, pending); }
|
|
569
|
+
return pending;
|
|
570
|
+
};
|
|
571
|
+
const observations = await Promise.all((options.receipts ?? []).map((reference) =>
|
|
572
|
+
readReceipt({ ...reference, confirmations: Math.max(reference.confirmations ?? 1, confirmationDepth) }, options, block, readIdentityBlock)));
|
|
573
|
+
const headers = new Map<bigint, Hex | undefined>();
|
|
574
|
+
for (const observation of observations) for (const pin of observation.pins) headers.set(pin.number, undefined);
|
|
575
|
+
// Reuse this invocation's authoritative L2 head header. Older receipt blocks
|
|
576
|
+
// share one canonical read after their asynchronous identity observations.
|
|
577
|
+
await Promise.all([...headers.keys()].map(async (number) => {
|
|
578
|
+
if (number === block.number) { headers.set(number, block.hash); return; }
|
|
579
|
+
const value = await lifecycleRpc(options.client, "eth_getBlockByNumber", [toHex(number), false]);
|
|
580
|
+
const hash = value === null ? undefined : rpcHex(rpcObject(value, "receipt canonical block").hash, "receipt canonical hash");
|
|
581
|
+
if (hash !== undefined && hash.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Receipt canonical block hash must have 32 bytes");
|
|
582
|
+
headers.set(number, hash);
|
|
583
|
+
}));
|
|
584
|
+
return observations.map((observation) => observation.finish(headers));
|
|
527
585
|
}
|
|
528
586
|
|
|
529
587
|
/** Reads confirmation-bound canonical state and receipt provenance; local step counters are never used. */
|
|
530
588
|
export async function readLaunchProgress(options: ReadLaunchProgressOptions, pinnedBlock?: LifecycleBlock): Promise<CanonicalLaunchProgress> {
|
|
531
589
|
if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
|
|
532
|
-
const
|
|
533
|
-
|
|
590
|
+
const planHash = options.planned.planHash, launchId = options.planned.launchId;
|
|
591
|
+
const result = await withLifecycleReadClient(options.client, async (client) =>
|
|
592
|
+
readLaunchProgressAtBlock(options, client, pinnedBlock ?? await readLifecycleBlock(client)));
|
|
593
|
+
if (hashLaunchPlan(options.planned.plan) !== planHash || hashLaunchIdentity(options.planned.plan) !== launchId) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during the final canonical progress guard");
|
|
594
|
+
if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
|
|
595
|
+
return result;
|
|
534
596
|
}
|
|
535
597
|
|
|
536
|
-
|
|
598
|
+
/** Build-next needs the state machine, receipts and nonce, not display-only live markets. */
|
|
599
|
+
export async function readLifecycleExecutionProgress(options: ReadLaunchProgressOptions, block: LifecycleBlock): Promise<CanonicalLaunchProgress> {
|
|
600
|
+
if (!assertLifecycleReadClientOpen(options.client)) throw new LifecyclePlanningError("INVALID_READ_CONTEXT", "Execution progress requires an invocation owner");
|
|
601
|
+
return readLaunchProgressAtBlock(options, options.client, block, false);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, client: LifecycleRpcClient, block: LifecycleBlock, readMarkets = true): Promise<CanonicalLaunchProgress> {
|
|
537
605
|
const { planned } = options;
|
|
538
606
|
const assertActive = () => {
|
|
539
607
|
if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
|
|
@@ -555,7 +623,7 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
|
|
|
555
623
|
} else if (planned.account.toLowerCase() !== planned.plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
|
|
556
624
|
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");
|
|
557
625
|
const [head, confirmedProgress, token, confirmedNonce, headNonce, pendingNonce] = await Promise.all([
|
|
558
|
-
|
|
626
|
+
readLifecycleHeadProgress(client, planned.plan, planned.account, block),
|
|
559
627
|
confirmedNumber === block.number ? undefined : readLifecycleContract<LaunchProgressV1>(client, planned.plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [planned.launchId], confirmedBlock, planned.account),
|
|
560
628
|
readLifecycleTokenAtBlock(client, planned.plan, block),
|
|
561
629
|
lifecycleRpc(client, "eth_getTransactionCount", [planned.account, toHex(confirmedBlock.number)]),
|
|
@@ -582,16 +650,23 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
|
|
|
582
650
|
const pendingAccountNonce = rpcQuantity(pendingNonce, "pending account nonce");
|
|
583
651
|
const progressParameters = [{ type: "tuple", components: launchProgressV1Components }] as const;
|
|
584
652
|
const confirmationSafe = confirmedAccountNonce === headAccountNonce && pendingAccountNonce === headAccountNonce && keccak256(encodeAbiParameters(progressParameters, [canonical])) === keccak256(encodeAbiParameters(progressParameters, [head]));
|
|
585
|
-
const receipts = await
|
|
653
|
+
const receipts = await readReceipts({ ...options, client }, block, confirmationDepth);
|
|
586
654
|
assertActive();
|
|
587
655
|
let markets: LifecycleMarketProgress[] = [];
|
|
588
|
-
|
|
656
|
+
const lens = lifecycleLensSnapshot(client, confirmedBlock);
|
|
657
|
+
if (readMarkets && lens !== undefined) {
|
|
658
|
+
markets = lens.preparedMarkets.map((market) => {
|
|
659
|
+
validateLifecycleMarketIdentity(market.prepared.identity, planned.plan, token, market.index);
|
|
660
|
+
return { index: market.index, prepared: market.prepared,
|
|
661
|
+
...(market.liveStateSuccess ? { live: market.live } : { error: "Prepared market live-state read failed" }) };
|
|
662
|
+
});
|
|
663
|
+
} else if (readMarkets && canonical.preparedMarkets > 0) {
|
|
589
664
|
const directory = await readLifecycleContract<Address>(client, planned.plan.orchestrator, launchLifecycleAbi, "directory", [], confirmedBlock);
|
|
590
665
|
markets = await Promise.all(Array.from({ length: canonical.preparedMarkets }, async (_, index): Promise<LifecycleMarketProgress> => {
|
|
591
|
-
const [adapter, prepared] = await readLifecycleContract<readonly [Address, PreparedMarketV1]>(client, directory, lifecycleDirectoryAbi, "market", [planned.launchId, index], confirmedBlock);
|
|
666
|
+
const [adapter, prepared] = await readLifecycleContract<readonly [Address, PreparedMarketV1]>(client, directory, lifecycleDirectoryAbi, "market", [planned.launchId, index], confirmedBlock, undefined, false, true);
|
|
592
667
|
validateLifecycleMarketIdentity(prepared.identity, planned.plan, token, index);
|
|
593
668
|
try {
|
|
594
|
-
const live = await readLifecycleContract<MarketLiveStateV1>(client, adapter, lifecycleAdapterAbi, "readMarket", [planned.launchId, index], confirmedBlock);
|
|
669
|
+
const live = await readLifecycleContract<MarketLiveStateV1>(client, adapter, lifecycleAdapterAbi, "readMarket", [planned.launchId, index], confirmedBlock, undefined, false, true);
|
|
595
670
|
return { index, prepared, live };
|
|
596
671
|
} catch (failure) {
|
|
597
672
|
if (failure instanceof Error && failure.name === "AbortError") throw failure;
|
|
@@ -599,8 +674,6 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
|
|
|
599
674
|
}
|
|
600
675
|
}));
|
|
601
676
|
}
|
|
602
|
-
if (checkBlock) await assertLifecycleBlock(client, block, chainId);
|
|
603
|
-
if (confirmedNumber !== block.number) await assertLifecycleBlock(client, confirmedBlock, chainId);
|
|
604
677
|
if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan changed during asynchronous progress reads");
|
|
605
678
|
assertActive();
|
|
606
679
|
return { canonical, head, confirmationDepth, confirmedBlockNumber: confirmedBlock.number, confirmedBlockHash: confirmedBlock.hash, confirmedAccountNonce, headAccountNonce, pendingAccountNonce, confirmationSafe, token, planHash: planned.planHash, launchId: planned.launchId, chainId, blockNumber: block.number, blockHash: block.hash, markets, receipts };
|
package/src/lifecycle/rpc.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { decodeFunctionResult, encodeFunctionData, isAddress, isHex, parseAbi, toHex, type Abi, type Address, type Hex } from "viem";
|
|
1
|
+
import { decodeFunctionResult, encodeFunctionData, isAddress, isHex, keccak256, multicall3Abi, parseAbi, toHex, type Abi, type Address, type Hex } from "viem";
|
|
2
2
|
import { LifecyclePlanningError, type LifecycleBlock, type LifecycleDiagnosticEvent, type LifecycleDiagnosticListener, type LifecycleDiagnosticStage, type LifecycleLimitContext, type LifecycleLimitSource, type LifecycleLimits, type LifecycleRpcClient, type LifecycleSessionCache, type LifecycleTransaction, type ResolvedLifecycleLimits } from "./types.js";
|
|
3
3
|
import { lifecycleRegistryAbi } from "./abi.js";
|
|
4
4
|
import type { ProfileTopologyV1 } from "./schema.js";
|
|
5
|
+
import { robinhoodMainnet } from "../addresses.js";
|
|
6
|
+
|
|
7
|
+
// Reviewed chain4663 deployment, independently observed at block 98,136,655.
|
|
8
|
+
const multicall3CodeHash = "0xd5c15df687b16f2ff992fc8d767b4216323184a2bbc6ee2f9c398c318e770891";
|
|
9
|
+
const sessionObservations = new WeakMap<object, Map<string, Promise<unknown>>>();
|
|
10
|
+
const canonicalWireValue = (_key: string, value: unknown): unknown => typeof value === "string" && value.startsWith("0x") ? value.toLowerCase() : value;
|
|
11
|
+
type IndependentRead = { params: readonly unknown[]; resolve: (value: unknown) => void; reject: (failure: unknown) => void };
|
|
5
12
|
|
|
6
13
|
const lifecycleReadConcurrency = 8;
|
|
7
14
|
const diagnosticRpcMethods: Record<string, true> = {
|
|
@@ -67,6 +74,10 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
67
74
|
private readonly concurrency: number;
|
|
68
75
|
private diagnosticSequence = 0;
|
|
69
76
|
private chainId?: Promise<bigint>;
|
|
77
|
+
private gasPrice?: Promise<unknown>;
|
|
78
|
+
private readonly immutableReads = new Map<string, Promise<unknown>>();
|
|
79
|
+
private readonly independentReads = new Map<string, { block: LifecycleBlock; reads: IndependentRead[] }>();
|
|
80
|
+
readonly finalChecks = new Set<() => void>();
|
|
70
81
|
private readonly limitObservations = new Map<LifecycleLimitSource | undefined, Map<string, Promise<LifecycleLimitObservation>>>();
|
|
71
82
|
private readonly session?: SessionLifecycleCache;
|
|
72
83
|
private sessionGroup?: SessionReadGroup;
|
|
@@ -145,6 +156,73 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
145
156
|
return this.chainId ??= this.run("eth_chainId", []).then((value) => rpcQuantity(value, "chain ID"));
|
|
146
157
|
}
|
|
147
158
|
|
|
159
|
+
observeGasPrice(): Promise<unknown> {
|
|
160
|
+
this.assertOpen();
|
|
161
|
+
return this.gasPrice ??= this.run("eth_gasPrice", []);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
immutableRead(key: string, work: () => Promise<unknown>): Promise<unknown> {
|
|
165
|
+
this.assertOpen();
|
|
166
|
+
let pending = this.immutableReads.get(key);
|
|
167
|
+
if (pending === undefined) {
|
|
168
|
+
pending = work();
|
|
169
|
+
this.immutableReads.set(key, pending);
|
|
170
|
+
const owned = pending;
|
|
171
|
+
void pending.catch(() => { if (this.immutableReads.get(key) === owned) this.immutableReads.delete(key); });
|
|
172
|
+
}
|
|
173
|
+
return pending.then((value) => { this.assertOpen(); return value; });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async finalize(): Promise<void> {
|
|
178
|
+
this.assertOpen();
|
|
179
|
+
for (const check of this.finalChecks) check();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
independentRead(params: readonly unknown[], block: LifecycleBlock, staticRead: boolean): Promise<unknown> {
|
|
183
|
+
return this.read("eth_call", params, block, staticRead, () => new Promise((resolve, reject) => {
|
|
184
|
+
const key = `${block.number}:${block.hash.toLowerCase()}`;
|
|
185
|
+
let group = this.independentReads.get(key);
|
|
186
|
+
if (group === undefined) {
|
|
187
|
+
group = { block, reads: [] };
|
|
188
|
+
this.independentReads.set(key, group);
|
|
189
|
+
queueMicrotask(() => {
|
|
190
|
+
this.independentReads.delete(key);
|
|
191
|
+
void this.flushIndependentReads(group!);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
group.reads.push({ params, resolve, reject });
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private async flushIndependentReads(group: { block: LifecycleBlock; reads: IndependentRead[] }): Promise<void> {
|
|
199
|
+
const { block, reads } = group;
|
|
200
|
+
try {
|
|
201
|
+
if (await this.observeChainId() !== 4663n) {
|
|
202
|
+
await Promise.all(reads.map(async (read) => {
|
|
203
|
+
try { read.resolve(await this.run("eth_call", read.params)); }
|
|
204
|
+
catch (failure) { read.reject(failure); }
|
|
205
|
+
}));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const to = robinhoodMainnet.contracts.multicall3.address;
|
|
209
|
+
await attestLifecycleMulticall(this, block);
|
|
210
|
+
const calls = reads.map((read) => {
|
|
211
|
+
const request = read.params[0] as { to: Address; data: Hex };
|
|
212
|
+
return { target: request.to, allowFailure: true, callData: request.data };
|
|
213
|
+
});
|
|
214
|
+
const data = encodeFunctionData({ abi: multicall3Abi, functionName: "aggregate3", args: [calls] });
|
|
215
|
+
const raw = rpcHex(await this.run("eth_call", [{ to, data }, toHex(block.number)]), "Multicall3 result");
|
|
216
|
+
const results = decodeFunctionResult({ abi: multicall3Abi, functionName: "aggregate3", data: raw });
|
|
217
|
+
if (results.length !== reads.length) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Multicall3 omitted independent pinned results");
|
|
218
|
+
for (let index = 0; index < reads.length; index += 1) {
|
|
219
|
+
const result = results[index]!;
|
|
220
|
+
if (result.success) reads[index]!.resolve(result.returnData);
|
|
221
|
+
else reads[index]!.reject(new LifecyclePlanningError("MULTICALL_READ_FAILED", "An independent pinned getter reverted"));
|
|
222
|
+
}
|
|
223
|
+
} catch (failure) { for (const read of reads) read.reject(failure); }
|
|
224
|
+
}
|
|
225
|
+
|
|
148
226
|
observeLimits(context: LifecycleLimitContext, source?: LifecycleLimitSource): Promise<LifecycleLimitObservation> {
|
|
149
227
|
this.assertOpen();
|
|
150
228
|
// A caller callback is live policy, not an immutable pinned observation.
|
|
@@ -168,11 +246,11 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
168
246
|
return observation.then((result) => { this.assertOpen(); result.assertCurrent(); return result; });
|
|
169
247
|
}
|
|
170
248
|
|
|
171
|
-
read(method: string, params: readonly unknown[], block: LifecycleBlock, staticRead = false): Promise<unknown> {
|
|
249
|
+
read(method: string, params: readonly unknown[], block: LifecycleBlock, staticRead = false, load?: () => Promise<unknown>): Promise<unknown> {
|
|
172
250
|
this.assertOpen();
|
|
173
251
|
// Source identity is this scope's original client. The complete wire call includes
|
|
174
252
|
// caller, value, gas and any other call fields; no selector-only/ABI-name aliasing.
|
|
175
|
-
const key = JSON.stringify([block.number.toString(), block.hash
|
|
253
|
+
const key = JSON.stringify([block.number.toString(), block.hash, method, params], canonicalWireValue);
|
|
176
254
|
const existing = this.reads.get(key);
|
|
177
255
|
if (existing !== undefined) {
|
|
178
256
|
if (staticRead && this.session !== undefined) this.sessionReuses += 1;
|
|
@@ -189,7 +267,7 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
189
267
|
pending = Promise.resolve(cached);
|
|
190
268
|
} else {
|
|
191
269
|
if (staticRead && this.session !== undefined) this.sessionMisses += 1;
|
|
192
|
-
pending = this.run(method, params);
|
|
270
|
+
pending = load === undefined ? this.run(method, params) : load();
|
|
193
271
|
if (group !== undefined) pending = pending.then((value) => {
|
|
194
272
|
// Never share in-flight work or install late results after invalidation/closure.
|
|
195
273
|
// Only wire hex is retained, so public decoded DTOs cannot mutate authority.
|
|
@@ -202,11 +280,21 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
202
280
|
return pending;
|
|
203
281
|
}
|
|
204
282
|
|
|
283
|
+
seed(method: string, params: readonly unknown[], block: LifecycleBlock, value: unknown): void {
|
|
284
|
+
this.assertOpen();
|
|
285
|
+
const key = JSON.stringify([block.number.toString(), block.hash, method, params], canonicalWireValue);
|
|
286
|
+
this.reads.set(key, Promise.resolve(value));
|
|
287
|
+
}
|
|
288
|
+
|
|
205
289
|
close(success: boolean): void {
|
|
206
290
|
this.closed = true;
|
|
207
291
|
this.reads.clear();
|
|
208
292
|
this.chainId = undefined;
|
|
209
293
|
this.limitObservations.clear();
|
|
294
|
+
this.gasPrice = undefined;
|
|
295
|
+
this.immutableReads.clear();
|
|
296
|
+
this.independentReads.clear();
|
|
297
|
+
this.finalChecks.clear();
|
|
210
298
|
const listener = this.source.onDiagnostic;
|
|
211
299
|
if (this.session !== undefined && listener !== undefined) emitLifecycleDiagnostic(listener, {
|
|
212
300
|
stage: "session-cache", phase: success ? "success" : "failure",
|
|
@@ -221,7 +309,14 @@ export async function withLifecycleReadClient<T>(source: LifecycleRpcClient, wor
|
|
|
221
309
|
const client = new InvocationLifecycleClient(source);
|
|
222
310
|
let success = false;
|
|
223
311
|
try {
|
|
224
|
-
|
|
312
|
+
let result: T;
|
|
313
|
+
try { result = await work(client); }
|
|
314
|
+
catch (failure) {
|
|
315
|
+
// Live policy is rechecked even when exact execution or a source read refuses.
|
|
316
|
+
await client.finalize();
|
|
317
|
+
throw failure;
|
|
318
|
+
}
|
|
319
|
+
await client.finalize();
|
|
225
320
|
success = true;
|
|
226
321
|
return result;
|
|
227
322
|
} finally { client.close(success); }
|
|
@@ -278,13 +373,65 @@ export function lifecycleStaticRpc(client: LifecycleRpcClient, method: "eth_call
|
|
|
278
373
|
export async function lifecycleRpc(client: LifecycleRpcClient, method: string, params: readonly unknown[] = []): Promise<unknown> {
|
|
279
374
|
return client instanceof InvocationLifecycleClient ? client.run(method, params) : client.request({ method, params });
|
|
280
375
|
}
|
|
281
|
-
/** One invocation's observed identity
|
|
282
|
-
* deliberately use uncached eth_chainId so a switched provider cannot retain proof. */
|
|
376
|
+
/** One invocation's observed identity; wallet submission owns its final chain check. */
|
|
283
377
|
export function readLifecycleChainId(client: LifecycleRpcClient): Promise<bigint> {
|
|
284
378
|
return client instanceof InvocationLifecycleClient
|
|
285
379
|
? client.observeChainId()
|
|
286
380
|
: lifecycleRpc(client, "eth_chainId").then((value) => rpcQuantity(value, "chain ID"));
|
|
287
381
|
}
|
|
382
|
+
|
|
383
|
+
/** A single operation-local recommendation, never a session or cross-head quote. */
|
|
384
|
+
export function readLifecycleGasPrice(client: LifecycleRpcClient): Promise<unknown> {
|
|
385
|
+
return client instanceof InvocationLifecycleClient ? client.observeGasPrice() : lifecycleRpc(client, "eth_gasPrice");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Only the reviewed immutable factory identity may use an invocation-wide key. */
|
|
389
|
+
export function readLifecycleImmutable<T>(client: LifecycleRpcClient, key: string, work: () => Promise<T>): Promise<T> {
|
|
390
|
+
return (client instanceof InvocationLifecycleClient ? client.immutableRead(key, work) : work()) as Promise<T>;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Successful deployment/backend observations live for the caller's session, not one block. */
|
|
394
|
+
export function lifecycleSessionRead<T>(client: LifecycleRpcClient, key: string, work: () => Promise<T>): Promise<T> {
|
|
395
|
+
const source = lifecycleSourceClient(client);
|
|
396
|
+
const owner = source.sessionCache ?? source;
|
|
397
|
+
let values = sessionObservations.get(owner);
|
|
398
|
+
if (values === undefined) { values = new Map(); sessionObservations.set(owner, values); }
|
|
399
|
+
let pending = values.get(key);
|
|
400
|
+
if (pending === undefined) {
|
|
401
|
+
pending = work();
|
|
402
|
+
values.set(key, pending);
|
|
403
|
+
const owned = pending;
|
|
404
|
+
void pending.catch(() => { if (values.get(key) === owned) values.delete(key); });
|
|
405
|
+
}
|
|
406
|
+
return pending as Promise<T>;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export async function attestLifecycleMulticall(client: LifecycleRpcClient, block?: LifecycleBlock): Promise<void> {
|
|
410
|
+
const to = robinhoodMainnet.contracts.multicall3.address;
|
|
411
|
+
await lifecycleSessionRead(client, `4663:multicall:${to}`, async () => {
|
|
412
|
+
const code = rpcHex(await lifecycleRpc(client, "eth_getCode", [to, block === undefined ? "latest" : toHex(block.number)]), "Multicall3 code");
|
|
413
|
+
if (keccak256(code) !== multicall3CodeHash) throw new LifecyclePlanningError("MULTICALL_CODE_MISMATCH", "Multicall3 runtime differs from the reviewed chain4663 deployment");
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Installs only SDK-observed wire results from this invocation's aggregate/lens. */
|
|
418
|
+
export function seedLifecycleRead(client: LifecycleRpcClient, method: string, params: readonly unknown[], block: LifecycleBlock, value: unknown): void {
|
|
419
|
+
if (!(client instanceof InvocationLifecycleClient)) throw new LifecyclePlanningError("INVALID_READ_CONTEXT", "Snapshot reads require an invocation owner");
|
|
420
|
+
client.observeBlock(block, true);
|
|
421
|
+
client.seed(method, params, block, value);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Recheck live policy after the owner's final asynchronous canonical guards. */
|
|
425
|
+
export function registerLifecycleFinalCheck(client: LifecycleRpcClient, check: () => void): void {
|
|
426
|
+
if (client instanceof InvocationLifecycleClient) { client.assertOpen(); client.finalChecks.add(check); }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** SDK-selected caller-independent getters only; never poster quotes or execution. */
|
|
430
|
+
function lifecycleIndependentRpc(client: LifecycleRpcClient, params: readonly unknown[], block: LifecycleBlock, staticRead: boolean): Promise<unknown> {
|
|
431
|
+
return client instanceof InvocationLifecycleClient
|
|
432
|
+
? client.independentRead(params, block, staticRead)
|
|
433
|
+
: lifecyclePinnedRpc(client, "eth_call", params, block, staticRead);
|
|
434
|
+
}
|
|
288
435
|
export function rpcObject(value: unknown, label: string): Record<string, unknown> {
|
|
289
436
|
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", `${label} must be an object`);
|
|
290
437
|
// The object guard establishes the RPC record boundary; fields remain untrusted unknowns.
|
|
@@ -325,7 +472,8 @@ export async function readLifecycleBlock(client: LifecycleRpcClient, tag: Hex |
|
|
|
325
472
|
if (client instanceof InvocationLifecycleClient) client.observeBlock(block, tag === "latest");
|
|
326
473
|
return block;
|
|
327
474
|
}
|
|
328
|
-
export async function assertLifecycleBlock(client: LifecycleRpcClient, block: LifecycleBlock, expectedChainId?: bigint): Promise<void> {
|
|
475
|
+
export async function assertLifecycleBlock(client: LifecycleRpcClient, block: Pick<LifecycleBlock, "number" | "hash">, expectedChainId?: bigint): Promise<void> {
|
|
476
|
+
if (client instanceof InvocationLifecycleClient) return;
|
|
329
477
|
const [chainId, current] = await Promise.all([
|
|
330
478
|
expectedChainId === undefined ? undefined : lifecycleRpc(client, "eth_chainId"),
|
|
331
479
|
readLifecycleBlock(client, toHex(block.number)),
|
|
@@ -339,13 +487,15 @@ export async function assertLifecycleBlock(client: LifecycleRpcClient, block: Li
|
|
|
339
487
|
throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch state changed during the operation; read canonical progress again");
|
|
340
488
|
}
|
|
341
489
|
}
|
|
342
|
-
export async function readLifecycleContract<T>(client: LifecycleRpcClient, to: Address, abi: Abi, functionName: string, args: readonly unknown[], block: LifecycleBlock, from?: Address, staticRead = false): Promise<T> {
|
|
490
|
+
export async function readLifecycleContract<T>(client: LifecycleRpcClient, to: Address, abi: Abi, functionName: string, args: readonly unknown[], block: LifecycleBlock, from?: Address, staticRead = false, independent = false): Promise<T> {
|
|
343
491
|
const data = encodeFunctionData({ abi, functionName, args });
|
|
344
492
|
const request = [{ to, from, data }, toHex(block.number)];
|
|
345
493
|
const getter = abi.some((item) => item.type === "function" && item.name === functionName && (item.stateMutability === "view" || item.stateMutability === "pure"));
|
|
346
|
-
const result = rpcHex(await (
|
|
347
|
-
?
|
|
348
|
-
:
|
|
494
|
+
const result = rpcHex(await (independent
|
|
495
|
+
? lifecycleIndependentRpc(client, request, block, staticRead)
|
|
496
|
+
: getter
|
|
497
|
+
? staticRead ? lifecycleStaticRpc(client, "eth_call", request, block) : lifecyclePinnedRpc(client, "eth_call", request, block)
|
|
498
|
+
: lifecycleRpc(client, "eth_call", request)), functionName);
|
|
349
499
|
// ABI decoding validates scalar widths and tuple layout; T names that exact frozen ABI result.
|
|
350
500
|
const decoded = decodeFunctionResult({ abi, functionName, data: result }) as T;
|
|
351
501
|
return decoded;
|
|
@@ -382,7 +532,7 @@ export async function readNitroPosterGas(transaction: LifecycleTransaction, cont
|
|
|
382
532
|
|
|
383
533
|
async function readNitroScalar(context: LifecycleLimitContext, to: Address, abi: Abi, functionName: string): Promise<bigint> {
|
|
384
534
|
const data = encodeFunctionData({ abi, functionName });
|
|
385
|
-
const result = rpcHex(await
|
|
535
|
+
const result = rpcHex(await lifecycleIndependentRpc(context.client, [{ to, data }, toHex(context.block.number)], context.block, true), functionName);
|
|
386
536
|
if (result.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", `${functionName} must return exactly one ABI word`);
|
|
387
537
|
return decodeFunctionResult({ abi, functionName, data: result }) as bigint;
|
|
388
538
|
}
|