@black-market/sdk 0.13.0 → 0.14.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/README.md +4 -4
- 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 +70 -62
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/presets.d.ts +5 -0
- package/dist/lifecycle/presets.d.ts.map +1 -1
- package/dist/lifecycle/presets.js +5 -0
- package/dist/lifecycle/presets.js.map +1 -1
- package/dist/lifecycle/progress.d.ts +48 -0
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +31 -20
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +6 -2
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +47 -44
- 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 +130 -109
- 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 +15 -5
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/docs/launch-lifecycle.md +42 -20
- package/docs/reference.md +43 -6
- package/examples/README.md +2 -2
- 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 +72 -58
- package/src/lifecycle/presets.ts +10 -0
- package/src/lifecycle/progress.ts +26 -16
- package/src/lifecycle/rpc.ts +45 -43
- package/src/lifecycle/simulation.ts +126 -100
- package/src/lifecycle/snapshot.ts +72 -0
- package/src/lifecycle/types.ts +15 -5
|
@@ -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" }],
|
|
@@ -39,7 +40,6 @@ export async function predictLifecycleToken(options: { client: LifecycleRpcClien
|
|
|
39
40
|
return withLifecycleReadClient(options.client, async (client) => {
|
|
40
41
|
const block = await readLifecycleBlock(client);
|
|
41
42
|
const token = await readLifecycleTokenAtBlock(client, options.plan, block);
|
|
42
|
-
await assertLifecycleBlock(client, block, options.plan.chainId);
|
|
43
43
|
return token;
|
|
44
44
|
});
|
|
45
45
|
}
|
|
@@ -236,7 +236,7 @@ export async function readLifecycleProfiles(options: { client: LifecycleRpcClien
|
|
|
236
236
|
const chainIdPromise = readLifecycleChainId(client);
|
|
237
237
|
// Block-bound discovery does not depend on chain identity. Join both branches
|
|
238
238
|
// immediately so a failed chain read cannot become detached or a profile refusal.
|
|
239
|
-
const [
|
|
239
|
+
const [, { profiles }] = await Promise.all([
|
|
240
240
|
chainIdPromise,
|
|
241
241
|
(async () => {
|
|
242
242
|
const block = pinnedBlock ?? await readLifecycleBlock(client);
|
|
@@ -302,7 +302,6 @@ export async function readLifecycleProfiles(options: { client: LifecycleRpcClien
|
|
|
302
302
|
return { block, profiles };
|
|
303
303
|
})(),
|
|
304
304
|
]);
|
|
305
|
-
if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, chainId);
|
|
306
305
|
return profiles;
|
|
307
306
|
}));
|
|
308
307
|
}
|
|
@@ -431,7 +430,6 @@ export async function readPoolBoundHookDeployment(options: { client: LifecycleRp
|
|
|
431
430
|
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
432
431
|
const { deployment } = await readPoolBoundHookDeploymentDetails({ ...options, client }, block, commitment);
|
|
433
432
|
if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while reading hook deployment metadata");
|
|
434
|
-
if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, options.plan.chainId);
|
|
435
433
|
return { ...deployment };
|
|
436
434
|
});
|
|
437
435
|
if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during the final hook deployment guard");
|
|
@@ -446,7 +444,6 @@ export async function buildPoolBoundHookDeploymentTransaction(options: { client:
|
|
|
446
444
|
const { deployment, parameters, configVersion } = await readPoolBoundHookDeploymentDetails({ ...options, client }, block, commitment);
|
|
447
445
|
if (!hasLifecycleV4HookPermissions(deployment.predictedHook)) throw new LifecyclePlanningError("INVALID_HOOK_BITS", "Finalize a salt with the exact lifecycle hook permissions before predeployment");
|
|
448
446
|
if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while constructing hook deployment calldata");
|
|
449
|
-
await assertLifecycleBlock(client, block, options.plan.chainId);
|
|
450
447
|
const abi = configVersion === 5 ? poolHookDeployerV1Abi : poolHookDeployerConfigV6Abi;
|
|
451
448
|
return { to: deployment.deployer, data: encodeFunctionData({ abi, functionName: "deploy", args: [parameters, deployment.salt] }), value: 0n, deployment: { ...deployment } };
|
|
452
449
|
});
|
|
@@ -566,6 +563,7 @@ async function readReceipts(options: ReadLaunchProgressOptions, block: Lifecycle
|
|
|
566
563
|
if (options.receipts === undefined || options.receipts.length === 0) return [];
|
|
567
564
|
const identityBlocks = new Map<bigint, Promise<LifecycleBlock>>();
|
|
568
565
|
const readIdentityBlock = (number: bigint) => {
|
|
566
|
+
if (number === block.number) return Promise.resolve(block);
|
|
569
567
|
let pending = identityBlocks.get(number);
|
|
570
568
|
if (pending === undefined) { pending = readLifecycleBlock(options.client, toHex(number)); identityBlocks.set(number, pending); }
|
|
571
569
|
return pending;
|
|
@@ -574,9 +572,10 @@ async function readReceipts(options: ReadLaunchProgressOptions, block: Lifecycle
|
|
|
574
572
|
readReceipt({ ...reference, confirmations: Math.max(reference.confirmations ?? 1, confirmationDepth) }, options, block, readIdentityBlock)));
|
|
575
573
|
const headers = new Map<bigint, Hex | undefined>();
|
|
576
574
|
for (const observation of observations) for (const pin of observation.pins) headers.set(pin.number, undefined);
|
|
577
|
-
//
|
|
578
|
-
//
|
|
575
|
+
// Reuse this invocation's authoritative L2 head header. Older receipt blocks
|
|
576
|
+
// share one canonical read after their asynchronous identity observations.
|
|
579
577
|
await Promise.all([...headers.keys()].map(async (number) => {
|
|
578
|
+
if (number === block.number) { headers.set(number, block.hash); return; }
|
|
580
579
|
const value = await lifecycleRpc(options.client, "eth_getBlockByNumber", [toHex(number), false]);
|
|
581
580
|
const hash = value === null ? undefined : rpcHex(rpcObject(value, "receipt canonical block").hash, "receipt canonical hash");
|
|
582
581
|
if (hash !== undefined && hash.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Receipt canonical block hash must have 32 bytes");
|
|
@@ -590,13 +589,19 @@ export async function readLaunchProgress(options: ReadLaunchProgressOptions, pin
|
|
|
590
589
|
if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
|
|
591
590
|
const planHash = options.planned.planHash, launchId = options.planned.launchId;
|
|
592
591
|
const result = await withLifecycleReadClient(options.client, async (client) =>
|
|
593
|
-
readLaunchProgressAtBlock(options, client, pinnedBlock ?? await readLifecycleBlock(client)
|
|
592
|
+
readLaunchProgressAtBlock(options, client, pinnedBlock ?? await readLifecycleBlock(client)));
|
|
594
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");
|
|
595
594
|
if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
|
|
596
595
|
return result;
|
|
597
596
|
}
|
|
598
597
|
|
|
599
|
-
|
|
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> {
|
|
600
605
|
const { planned } = options;
|
|
601
606
|
const assertActive = () => {
|
|
602
607
|
if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
|
|
@@ -648,13 +653,20 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
|
|
|
648
653
|
const receipts = await readReceipts({ ...options, client }, block, confirmationDepth);
|
|
649
654
|
assertActive();
|
|
650
655
|
let markets: LifecycleMarketProgress[] = [];
|
|
651
|
-
|
|
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) {
|
|
652
664
|
const directory = await readLifecycleContract<Address>(client, planned.plan.orchestrator, launchLifecycleAbi, "directory", [], confirmedBlock);
|
|
653
665
|
markets = await Promise.all(Array.from({ length: canonical.preparedMarkets }, async (_, index): Promise<LifecycleMarketProgress> => {
|
|
654
|
-
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);
|
|
655
667
|
validateLifecycleMarketIdentity(prepared.identity, planned.plan, token, index);
|
|
656
668
|
try {
|
|
657
|
-
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);
|
|
658
670
|
return { index, prepared, live };
|
|
659
671
|
} catch (failure) {
|
|
660
672
|
if (failure instanceof Error && failure.name === "AbortError") throw failure;
|
|
@@ -662,8 +674,6 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
|
|
|
662
674
|
}
|
|
663
675
|
}));
|
|
664
676
|
}
|
|
665
|
-
if (checkBlock) await assertLifecycleBlock(client, block, chainId);
|
|
666
|
-
if (confirmedNumber !== block.number) await assertLifecycleBlock(client, confirmedBlock, chainId);
|
|
667
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");
|
|
668
678
|
assertActive();
|
|
669
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
|
@@ -6,6 +6,8 @@ import { robinhoodMainnet } from "../addresses.js";
|
|
|
6
6
|
|
|
7
7
|
// Reviewed chain4663 deployment, independently observed at block 98,136,655.
|
|
8
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;
|
|
9
11
|
type IndependentRead = { params: readonly unknown[]; resolve: (value: unknown) => void; reject: (failure: unknown) => void };
|
|
10
12
|
|
|
11
13
|
const lifecycleReadConcurrency = 8;
|
|
@@ -74,7 +76,6 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
74
76
|
private chainId?: Promise<bigint>;
|
|
75
77
|
private gasPrice?: Promise<unknown>;
|
|
76
78
|
private readonly immutableReads = new Map<string, Promise<unknown>>();
|
|
77
|
-
private readonly canonicalPins = new Map<string, { block: Pick<LifecycleBlock, "number" | "hash">; chainId?: bigint }>();
|
|
78
79
|
private readonly independentReads = new Map<string, { block: LifecycleBlock; reads: IndependentRead[] }>();
|
|
79
80
|
readonly finalChecks = new Set<() => void>();
|
|
80
81
|
private readonly limitObservations = new Map<LifecycleLimitSource | undefined, Map<string, Promise<LifecycleLimitObservation>>>();
|
|
@@ -172,37 +173,9 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
172
173
|
return pending.then((value) => { this.assertOpen(); return value; });
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
deferCanonicalCheck(block: Pick<LifecycleBlock, "number" | "hash">, chainId?: bigint): void {
|
|
176
|
-
this.assertOpen();
|
|
177
|
-
const key = block.number.toString();
|
|
178
|
-
const existing = this.canonicalPins.get(key);
|
|
179
|
-
if (existing !== undefined && (existing.block.hash.toLowerCase() !== block.hash.toLowerCase() ||
|
|
180
|
-
existing.chainId !== undefined && chainId !== undefined && existing.chainId !== chainId)) {
|
|
181
|
-
this.invalidateSession();
|
|
182
|
-
throw new LifecyclePlanningError("STATE_REORGED", "One lifecycle invocation observed incompatible canonical pins");
|
|
183
|
-
}
|
|
184
|
-
this.canonicalPins.set(key, { block: { ...block }, chainId: chainId ?? existing?.chainId });
|
|
185
|
-
}
|
|
186
176
|
|
|
187
177
|
async finalize(): Promise<void> {
|
|
188
178
|
this.assertOpen();
|
|
189
|
-
const pins = [...this.canonicalPins.values()];
|
|
190
|
-
if (pins.length === 0) return;
|
|
191
|
-
const [chainId, current] = await Promise.all([
|
|
192
|
-
pins.some((pin) => pin.chainId !== undefined) ? this.run("eth_chainId", []).then((value) => rpcQuantity(value, "chain ID")) : undefined,
|
|
193
|
-
Promise.all(pins.map((pin) => readLifecycleBlock(this, toHex(pin.block.number)))),
|
|
194
|
-
]);
|
|
195
|
-
for (let index = 0; index < pins.length; index += 1) {
|
|
196
|
-
const pin = pins[index]!;
|
|
197
|
-
if (pin.chainId !== undefined && chainId !== pin.chainId) {
|
|
198
|
-
this.invalidateSession();
|
|
199
|
-
throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during the pinned operation");
|
|
200
|
-
}
|
|
201
|
-
if (current[index]!.hash.toLowerCase() !== pin.block.hash.toLowerCase()) {
|
|
202
|
-
this.invalidateSession();
|
|
203
|
-
throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch state changed during the operation; read canonical progress again");
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
179
|
for (const check of this.finalChecks) check();
|
|
207
180
|
}
|
|
208
181
|
|
|
@@ -225,8 +198,7 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
225
198
|
private async flushIndependentReads(group: { block: LifecycleBlock; reads: IndependentRead[] }): Promise<void> {
|
|
226
199
|
const { block, reads } = group;
|
|
227
200
|
try {
|
|
228
|
-
|
|
229
|
-
if (reads.length < 3 || await this.observeChainId() !== 4663n) {
|
|
201
|
+
if (await this.observeChainId() !== 4663n) {
|
|
230
202
|
await Promise.all(reads.map(async (read) => {
|
|
231
203
|
try { read.resolve(await this.run("eth_call", read.params)); }
|
|
232
204
|
catch (failure) { read.reject(failure); }
|
|
@@ -234,8 +206,7 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
234
206
|
return;
|
|
235
207
|
}
|
|
236
208
|
const to = robinhoodMainnet.contracts.multicall3.address;
|
|
237
|
-
|
|
238
|
-
if (keccak256(code) !== multicall3CodeHash) throw new LifecyclePlanningError("MULTICALL_CODE_MISMATCH", "Pinned Multicall3 runtime differs from the reviewed chain4663 deployment");
|
|
209
|
+
await attestLifecycleMulticall(this, block);
|
|
239
210
|
const calls = reads.map((read) => {
|
|
240
211
|
const request = read.params[0] as { to: Address; data: Hex };
|
|
241
212
|
return { target: request.to, allowFailure: true, callData: request.data };
|
|
@@ -279,7 +250,7 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
279
250
|
this.assertOpen();
|
|
280
251
|
// Source identity is this scope's original client. The complete wire call includes
|
|
281
252
|
// caller, value, gas and any other call fields; no selector-only/ABI-name aliasing.
|
|
282
|
-
const key = JSON.stringify([block.number.toString(), block.hash
|
|
253
|
+
const key = JSON.stringify([block.number.toString(), block.hash, method, params], canonicalWireValue);
|
|
283
254
|
const existing = this.reads.get(key);
|
|
284
255
|
if (existing !== undefined) {
|
|
285
256
|
if (staticRead && this.session !== undefined) this.sessionReuses += 1;
|
|
@@ -309,6 +280,12 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
309
280
|
return pending;
|
|
310
281
|
}
|
|
311
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
|
+
|
|
312
289
|
close(success: boolean): void {
|
|
313
290
|
this.closed = true;
|
|
314
291
|
this.reads.clear();
|
|
@@ -316,7 +293,6 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
316
293
|
this.limitObservations.clear();
|
|
317
294
|
this.gasPrice = undefined;
|
|
318
295
|
this.immutableReads.clear();
|
|
319
|
-
this.canonicalPins.clear();
|
|
320
296
|
this.independentReads.clear();
|
|
321
297
|
this.finalChecks.clear();
|
|
322
298
|
const listener = this.source.onDiagnostic;
|
|
@@ -336,8 +312,7 @@ export async function withLifecycleReadClient<T>(source: LifecycleRpcClient, wor
|
|
|
336
312
|
let result: T;
|
|
337
313
|
try { result = await work(client); }
|
|
338
314
|
catch (failure) {
|
|
339
|
-
//
|
|
340
|
-
// misleading economic refusal derived from an orphaned observation.
|
|
315
|
+
// Live policy is rechecked even when exact execution or a source read refuses.
|
|
341
316
|
await client.finalize();
|
|
342
317
|
throw failure;
|
|
343
318
|
}
|
|
@@ -398,8 +373,7 @@ export function lifecycleStaticRpc(client: LifecycleRpcClient, method: "eth_call
|
|
|
398
373
|
export async function lifecycleRpc(client: LifecycleRpcClient, method: string, params: readonly unknown[] = []): Promise<unknown> {
|
|
399
374
|
return client instanceof InvocationLifecycleClient ? client.run(method, params) : client.request({ method, params });
|
|
400
375
|
}
|
|
401
|
-
/** One invocation's observed identity
|
|
402
|
-
* 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. */
|
|
403
377
|
export function readLifecycleChainId(client: LifecycleRpcClient): Promise<bigint> {
|
|
404
378
|
return client instanceof InvocationLifecycleClient
|
|
405
379
|
? client.observeChainId()
|
|
@@ -416,6 +390,37 @@ export function readLifecycleImmutable<T>(client: LifecycleRpcClient, key: strin
|
|
|
416
390
|
return (client instanceof InvocationLifecycleClient ? client.immutableRead(key, work) : work()) as Promise<T>;
|
|
417
391
|
}
|
|
418
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
|
+
|
|
419
424
|
/** Recheck live policy after the owner's final asynchronous canonical guards. */
|
|
420
425
|
export function registerLifecycleFinalCheck(client: LifecycleRpcClient, check: () => void): void {
|
|
421
426
|
if (client instanceof InvocationLifecycleClient) { client.assertOpen(); client.finalChecks.add(check); }
|
|
@@ -468,10 +473,7 @@ export async function readLifecycleBlock(client: LifecycleRpcClient, tag: Hex |
|
|
|
468
473
|
return block;
|
|
469
474
|
}
|
|
470
475
|
export async function assertLifecycleBlock(client: LifecycleRpcClient, block: Pick<LifecycleBlock, "number" | "hash">, expectedChainId?: bigint): Promise<void> {
|
|
471
|
-
if (client instanceof InvocationLifecycleClient)
|
|
472
|
-
client.deferCanonicalCheck(block, expectedChainId);
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
476
|
+
if (client instanceof InvocationLifecycleClient) return;
|
|
475
477
|
const [chainId, current] = await Promise.all([
|
|
476
478
|
expectedChainId === undefined ? undefined : lifecycleRpc(client, "eth_chainId"),
|
|
477
479
|
readLifecycleBlock(client, toHex(block.number)),
|