@black-market/sdk 0.11.0 → 0.13.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 -79
- package/dist/lifecycle/abi.d.ts +1 -0
- package/dist/lifecycle/abi.d.ts.map +1 -1
- package/dist/lifecycle/abi.js +1 -0
- package/dist/lifecycle/abi.js.map +1 -1
- package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.d.ts +71 -0
- package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.d.ts.map +1 -0
- package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.js +73 -0
- package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.js.map +1 -0
- package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.d.ts +71 -0
- package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.d.ts.map +1 -0
- package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.js +73 -0
- package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.js.map +1 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +1 -0
- package/dist/lifecycle/index.js.map +1 -1
- package/dist/lifecycle/markets.d.ts +3 -1
- package/dist/lifecycle/markets.d.ts.map +1 -1
- package/dist/lifecycle/markets.js +20 -3
- package/dist/lifecycle/markets.js.map +1 -1
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +101 -46
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/presets.d.ts +4 -2
- package/dist/lifecycle/presets.d.ts.map +1 -1
- package/dist/lifecycle/presets.js +11 -4
- package/dist/lifecycle/presets.js.map +1 -1
- package/dist/lifecycle/progress.d.ts +3 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +145 -59
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +8 -2
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +165 -9
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/simulation.d.ts +1 -1
- package/dist/lifecycle/simulation.d.ts.map +1 -1
- package/dist/lifecycle/simulation.js +15 -11
- package/dist/lifecycle/simulation.js.map +1 -1
- package/dist/lifecycle/types.d.ts +23 -1
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/dist/lifecycle/ufg-abi.d.ts +2386 -0
- package/dist/lifecycle/ufg-abi.d.ts.map +1 -0
- package/dist/lifecycle/ufg-abi.js +171 -0
- package/dist/lifecycle/ufg-abi.js.map +1 -0
- package/dist/lifecycle/ufg-config.d.ts +38 -0
- package/dist/lifecycle/ufg-config.d.ts.map +1 -0
- package/dist/lifecycle/ufg-config.js +75 -0
- package/dist/lifecycle/ufg-config.js.map +1 -0
- package/dist/lifecycle/ufg.d.ts +21 -0
- package/dist/lifecycle/ufg.d.ts.map +1 -0
- package/dist/lifecycle/ufg.js +98 -0
- package/dist/lifecycle/ufg.js.map +1 -0
- package/docs/launch-lifecycle.md +611 -1363
- package/docs/reference.md +332 -0
- package/examples/.env.example +3 -2
- package/examples/README.md +172 -539
- package/examples/launch-example-support.ts +11 -3
- package/examples/launch-example.ts +1 -1
- package/examples/launch-lifecycle-smoke.ts +1 -1
- package/examples/ufg-launch-config.ts +31 -0
- package/package.json +1 -1
- package/src/lifecycle/abi.ts +1 -0
- package/src/lifecycle/deployments/ufg-dynamic-pool-20261021.ts +74 -0
- package/src/lifecycle/deployments/ufg-fixed-pool-20261021.ts +74 -0
- package/src/lifecycle/index.ts +1 -0
- package/src/lifecycle/markets.ts +16 -3
- package/src/lifecycle/planner.ts +95 -45
- package/src/lifecycle/presets.ts +11 -4
- package/src/lifecycle/progress.ts +131 -51
- package/src/lifecycle/rpc.ts +158 -10
- package/src/lifecycle/simulation.ts +14 -10
- package/src/lifecycle/types.ts +15 -2
- package/src/lifecycle/ufg-abi.ts +173 -0
- package/src/lifecycle/ufg-config.ts +77 -0
- package/src/lifecycle/ufg.ts +86 -0
|
@@ -2,10 +2,12 @@ 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 { assertLifecycleBlock, assertLifecycleReadClientOpen, lifecycleFailureReason, lifecyclePinnedRpc, lifecycleStaticRpc, lifecycleRpc, lifecycleStage, readLifecycleBlock, readLifecycleChainId, readLifecycleContract, readLifecycleStaticContract, readLifecycleProfileTopology, rpcHex, rpcObject, rpcQuantity, withLifecycleReadClient } from "./rpc.js";
|
|
5
|
+
import { assertLifecycleBlock, 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
|
+
import { ufgLaunchProxyV1Abi } from "./ufg-abi.js";
|
|
10
|
+
import { assertUfgLaunchAccount, hashUfgLaunchConfig, UFG_LAUNCH_PROXY_ADDRESS, type UfgLaunchConfig } from "./ufg-config.js";
|
|
9
11
|
|
|
10
12
|
export const ABYSS_LIFECYCLE_CONFIG_SCHEMA = keccak256(stringToHex("(uint8,uint24,bytes32,uint160,(int24,int24,uint128,uint256)[])"));
|
|
11
13
|
|
|
@@ -19,9 +21,18 @@ const lifecycleTokenPredictionAbi = [{
|
|
|
19
21
|
* avoiding full-plan prediction reads after salts or buy minima change. */
|
|
20
22
|
export async function readLifecycleTokenAtBlock(client: LifecycleRpcClient, plan: LaunchPlanV1, block: LifecycleBlock): Promise<Address> {
|
|
21
23
|
const deployment = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator });
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
if (deployment === undefined) return readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "predictToken", [plan], block);
|
|
25
|
+
const launchId = hashLaunchIdentity(plan);
|
|
26
|
+
const data = encodeFunctionData({ abi: lifecycleTokenPredictionAbi, functionName: "predictToken", args: [launchId, plan.token] });
|
|
27
|
+
const key = `${plan.chainId}:${plan.orchestrator.toLowerCase()}:${deployment.tokenFactory.toLowerCase()}:${deployment.tokenFactoryCodeHash}:${data}`;
|
|
28
|
+
return readLifecycleImmutable(client, key, () =>
|
|
29
|
+
readLifecycleContract<Address>(client, deployment.tokenFactory, lifecycleTokenPredictionAbi, "predictToken", [launchId, plan.token], block, undefined, false, true));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Internal pinned head observation. Known cores do not inspect msg.sender. */
|
|
33
|
+
export function readLifecycleHeadProgress(client: LifecycleRpcClient, plan: LaunchPlanV1, account: Address, block: LifecycleBlock): Promise<LaunchProgressV1> {
|
|
34
|
+
const known = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator }) !== undefined;
|
|
35
|
+
return readLifecycleContract<LaunchProgressV1>(client, plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [hashLaunchIdentity(plan)], block, account, false, known);
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
export async function predictLifecycleToken(options: { client: LifecycleRpcClient; plan: LaunchPlanV1 }): Promise<Address> {
|
|
@@ -222,7 +233,7 @@ export async function validateV4LifecycleOracle(options: {
|
|
|
222
233
|
|
|
223
234
|
export async function readLifecycleProfiles(options: { client: LifecycleRpcClient; orchestrator: Address; profileIds?: readonly Hex[]; offset?: bigint; limit?: bigint }, pinnedBlock?: LifecycleBlock): Promise<LifecycleProfile[]> {
|
|
224
235
|
return withLifecycleReadClient(options.client, (client) => lifecycleStage(client, "profile.certification", async () => {
|
|
225
|
-
const chainIdPromise =
|
|
236
|
+
const chainIdPromise = readLifecycleChainId(client);
|
|
226
237
|
// Block-bound discovery does not depend on chain identity. Join both branches
|
|
227
238
|
// immediately so a failed chain read cannot become detached or a profile refusal.
|
|
228
239
|
const [chainId, { block, profiles }] = await Promise.all([
|
|
@@ -301,7 +312,7 @@ async function readPoolBoundHookDeploymentDetails(options: { client: LifecycleRp
|
|
|
301
312
|
const market = plan.markets[marketIndex];
|
|
302
313
|
if (market === undefined || !isPoolBoundV4ConfigVersion(market.configVersion)) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Market index must select explicit pool-bound config version 5 or 6");
|
|
303
314
|
let chainId: bigint;
|
|
304
|
-
try { chainId =
|
|
315
|
+
try { chainId = await readLifecycleChainId(client); }
|
|
305
316
|
finally { assertDerivedReadClientOpen(client); }
|
|
306
317
|
if (hashLaunchPlan(plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while observing the hook deployment chain");
|
|
307
318
|
if (chainId !== plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed plan");
|
|
@@ -417,19 +428,21 @@ async function readPoolBoundHookDeploymentDetailsFresh(options: { client: Lifecy
|
|
|
417
428
|
export async function readPoolBoundHookDeployment(options: { client: LifecycleRpcClient; plan: LaunchPlanV1; marketIndex: number }, pinnedBlock?: LifecycleBlock): Promise<PoolBoundHookDeployment> {
|
|
418
429
|
const commitment = hashLaunchPlan(options.plan);
|
|
419
430
|
const block = pinnedBlock ?? await readLifecycleBlock(options.client);
|
|
420
|
-
|
|
431
|
+
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
421
432
|
const { deployment } = await readPoolBoundHookDeploymentDetails({ ...options, client }, block, commitment);
|
|
422
433
|
if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while reading hook deployment metadata");
|
|
423
434
|
if (pinnedBlock === undefined) await assertLifecycleBlock(client, block, options.plan.chainId);
|
|
424
435
|
return { ...deployment };
|
|
425
436
|
});
|
|
437
|
+
if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during the final hook deployment guard");
|
|
438
|
+
return result;
|
|
426
439
|
}
|
|
427
440
|
|
|
428
441
|
/** Optional permissionless predeployment calldata; no token deployment, pool registration or wallet broadcast. */
|
|
429
442
|
export async function buildPoolBoundHookDeploymentTransaction(options: { client: LifecycleRpcClient; plan: LaunchPlanV1; marketIndex: number }): Promise<{ to: Address; data: Hex; value: bigint; deployment: PoolBoundHookDeployment }> {
|
|
430
443
|
const commitment = hashLaunchPlan(options.plan);
|
|
431
444
|
const block = await readLifecycleBlock(options.client);
|
|
432
|
-
|
|
445
|
+
const result = await withLifecycleReadClient(options.client, async (client) => {
|
|
433
446
|
const { deployment, parameters, configVersion } = await readPoolBoundHookDeploymentDetails({ ...options, client }, block, commitment);
|
|
434
447
|
if (!hasLifecycleV4HookPermissions(deployment.predictedHook)) throw new LifecyclePlanningError("INVALID_HOOK_BITS", "Finalize a salt with the exact lifecycle hook permissions before predeployment");
|
|
435
448
|
if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed while constructing hook deployment calldata");
|
|
@@ -437,6 +450,8 @@ export async function buildPoolBoundHookDeploymentTransaction(options: { client:
|
|
|
437
450
|
const abi = configVersion === 5 ? poolHookDeployerV1Abi : poolHookDeployerConfigV6Abi;
|
|
438
451
|
return { to: deployment.deployer, data: encodeFunctionData({ abi, functionName: "deploy", args: [parameters, deployment.salt] }), value: 0n, deployment: { ...deployment } };
|
|
439
452
|
});
|
|
453
|
+
if (hashLaunchPlan(options.plan) !== commitment) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during the final hook transaction guard");
|
|
454
|
+
return result;
|
|
440
455
|
}
|
|
441
456
|
|
|
442
457
|
export function validateLifecycleMarketIdentity(identity: MarketIdentityV1, plan: LaunchPlanV1, token: Address, marketIndex: number): void {
|
|
@@ -453,19 +468,24 @@ export function validateLifecycleMarketIdentity(identity: MarketIdentityV1, plan
|
|
|
453
468
|
} else if (identity.venue !== LifecycleVenue.Abyss || identity.pool === zeroAddress || identity.factory === zeroAddress) throw new LifecyclePlanningError("MARKET_IDENTITY", "Abyss canonical factory/pool identity is missing");
|
|
454
469
|
}
|
|
455
470
|
|
|
456
|
-
async function transactionMatchesLaunch(transaction: Record<string, unknown>, options: ReadLaunchProgressOptions,
|
|
471
|
+
async function transactionMatchesLaunch(transaction: Record<string, unknown>, options: ReadLaunchProgressOptions, readBlock: () => Promise<LifecycleBlock>): Promise<boolean> {
|
|
457
472
|
const { planned } = options;
|
|
458
473
|
const from = rpcHex(transaction.from, "receipt transaction account");
|
|
459
474
|
if (from.toLowerCase() !== planned.account.toLowerCase()) throw new LifecyclePlanningError("RECEIPT_ACCOUNT", "Receipt transaction belongs to a different account");
|
|
460
475
|
if (transaction.chainId !== undefined && rpcQuantity(transaction.chainId, "receipt transaction chain") !== planned.plan.chainId) throw new LifecyclePlanningError("RECEIPT_CHAIN", "Receipt transaction belongs to a different chain");
|
|
461
476
|
const to = transaction.to === null ? zeroAddress : rpcHex(transaction.to, "receipt transaction target");
|
|
462
477
|
const input = rpcHex(transaction.input ?? transaction.data, "receipt transaction calldata");
|
|
463
|
-
|
|
478
|
+
const target = planned.ufg === undefined ? planned.plan.orchestrator : UFG_LAUNCH_PROXY_ADDRESS;
|
|
479
|
+
if (to.toLowerCase() === target.toLowerCase()) {
|
|
464
480
|
try {
|
|
465
|
-
const decoded = decodeFunctionData({ abi: launchLifecycleAbi, data: input });
|
|
481
|
+
const decoded = decodeFunctionData({ abi: planned.ufg === undefined ? launchLifecycleAbi : ufgLaunchProxyV1Abi, data: input });
|
|
466
482
|
if (!["launchAtomic", "beginLaunch", "prepareMarkets", "activateLaunch", "cancelLaunch"].includes(decoded.functionName) || !Array.isArray(decoded.args)) return false;
|
|
467
483
|
// Decoded plan layout is validated by the exact ABI, not trusted raw RPC JSON.
|
|
468
484
|
const plan = decoded.args[0] as LaunchPlanV1;
|
|
485
|
+
if (planned.ufg !== undefined && (decoded.functionName === "launchAtomic" || decoded.functionName === "beginLaunch")) {
|
|
486
|
+
const config = decoded.args[decoded.functionName === "launchAtomic" ? 1 : 2] as UfgLaunchConfig;
|
|
487
|
+
if (hashUfgLaunchConfig(config) !== planned.ufg.configHash) return false;
|
|
488
|
+
}
|
|
469
489
|
return hashLaunchPlan(plan).toLowerCase() === planned.planHash.toLowerCase();
|
|
470
490
|
} catch { return false; }
|
|
471
491
|
}
|
|
@@ -477,53 +497,103 @@ async function transactionMatchesLaunch(transaction: Record<string, unknown>, op
|
|
|
477
497
|
if (decoded.functionName !== "approve" || rpcQuantity(transaction.value ?? "0x0", "approval native value") !== 0n) return false;
|
|
478
498
|
spender = decoded.args[0];
|
|
479
499
|
} catch { return false; }
|
|
480
|
-
const
|
|
500
|
+
const known = getKnownLifecycleDeployment({ chainId: planned.plan.chainId, orchestrator: planned.plan.orchestrator });
|
|
501
|
+
const escrow = planned.ufg !== undefined ? UFG_LAUNCH_PROXY_ADDRESS : known?.fundingEscrow ??
|
|
502
|
+
await readLifecycleContract<Address>(options.client, planned.plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], await readBlock());
|
|
481
503
|
return spender.toLowerCase() === escrow.toLowerCase();
|
|
482
504
|
}
|
|
483
505
|
|
|
484
|
-
|
|
506
|
+
type ReceiptObservation = {
|
|
507
|
+
pins: readonly { number: bigint; hash: Hex }[];
|
|
508
|
+
finish(headers: ReadonlyMap<bigint, Hex | undefined>): LifecycleReceiptStatus;
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
async function readReceipt(reference: LifecycleReceiptReference, options: ReadLaunchProgressOptions, block: LifecycleBlock, readIdentityBlock: (number: bigint) => Promise<LifecycleBlock>): Promise<ReceiptObservation> {
|
|
485
512
|
const effectiveHash = reference.replacementHash ?? reference.transactionHash;
|
|
486
513
|
const base = { transactionHash: reference.transactionHash, effectiveHash, replaced: reference.replacementHash !== undefined && reference.replacementHash.toLowerCase() !== reference.transactionHash.toLowerCase() };
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
514
|
+
const pins: { number: bigint; hash: Hex }[] = [];
|
|
515
|
+
const observedNumber = reference.observedBlockNumber, observedHash = reference.observedBlockHash;
|
|
516
|
+
if (observedNumber !== undefined && observedHash !== undefined) pins.push({ number: observedNumber, hash: observedHash });
|
|
517
|
+
const finish = (result: LifecycleReceiptStatus, failure?: { error: unknown }): ReceiptObservation => ({
|
|
518
|
+
pins,
|
|
519
|
+
finish(headers) {
|
|
520
|
+
for (const pin of pins) {
|
|
521
|
+
if (failure !== undefined && (pin.number !== observedNumber || pin.hash !== observedHash)) continue;
|
|
522
|
+
if (headers.get(pin.number)?.toLowerCase() !== pin.hash.toLowerCase()) return { ...base, status: "reorged", blockNumber: pin.number, blockHash: pin.hash };
|
|
523
|
+
}
|
|
524
|
+
if (failure !== undefined) throw failure.error;
|
|
525
|
+
return result;
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
try {
|
|
529
|
+
const [receiptValue, transactionValue] = await Promise.all([
|
|
530
|
+
lifecycleRpc(options.client, "eth_getTransactionReceipt", [effectiveHash]),
|
|
531
|
+
lifecycleRpc(options.client, "eth_getTransactionByHash", [effectiveHash]),
|
|
532
|
+
]);
|
|
533
|
+
if (receiptValue === null) return finish({ ...base, status: "pending", blockNumber: observedNumber, blockHash: observedHash });
|
|
534
|
+
if (transactionValue === null) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Receipt exists without its canonical transaction");
|
|
535
|
+
const receipt = rpcObject(receiptValue, "receipt");
|
|
536
|
+
const blockNumber = rpcQuantity(receipt.blockNumber, "receipt block number");
|
|
537
|
+
const blockHash = rpcHex(receipt.blockHash, "receipt block hash");
|
|
538
|
+
pins.push({ number: blockNumber, hash: blockHash });
|
|
539
|
+
const matches = await transactionMatchesLaunch(rpcObject(transactionValue, "receipt transaction"), options,
|
|
540
|
+
() => readIdentityBlock(blockNumber));
|
|
541
|
+
const confirmations = block.number >= blockNumber ? block.number - blockNumber + 1n : 0n;
|
|
542
|
+
const required = reference.confirmations ?? 1;
|
|
543
|
+
if (!Number.isSafeInteger(required) || required < 1) throw new LifecyclePlanningError("INVALID_CONFIRMATIONS", "Receipt confirmations must be a positive integer");
|
|
544
|
+
const observation = finish({
|
|
545
|
+
...base, blockNumber, blockHash, confirmations,
|
|
546
|
+
status: confirmations < BigInt(required) ? "unconfirmed" : !matches ? "replacement-cancelled" :
|
|
547
|
+
rpcQuantity(receipt.status, "receipt status") !== 1n ? "reverted" : "confirmed",
|
|
548
|
+
});
|
|
549
|
+
return {
|
|
550
|
+
pins,
|
|
551
|
+
finish(headers) {
|
|
552
|
+
const result = observation.finish(headers);
|
|
553
|
+
if (result.status !== "reorged" && !matches && !base.replaced) throw new LifecyclePlanningError("RECEIPT_IDENTITY", "Receipt calldata does not belong to the exact committed launch");
|
|
554
|
+
return result;
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (error instanceof Error && error.name === "AbortError") throw error;
|
|
559
|
+
// A changed retained observation is a reorg even if the orphan's transaction
|
|
560
|
+
// or identity read is now unavailable. Otherwise preserve the original error.
|
|
561
|
+
return finish({ ...base, status: "pending" }, { error });
|
|
502
562
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function readReceipts(options: ReadLaunchProgressOptions, block: LifecycleBlock, confirmationDepth: number): Promise<LifecycleReceiptStatus[]> {
|
|
566
|
+
if (options.receipts === undefined || options.receipts.length === 0) return [];
|
|
567
|
+
const identityBlocks = new Map<bigint, Promise<LifecycleBlock>>();
|
|
568
|
+
const readIdentityBlock = (number: bigint) => {
|
|
569
|
+
let pending = identityBlocks.get(number);
|
|
570
|
+
if (pending === undefined) { pending = readLifecycleBlock(options.client, toHex(number)); identityBlocks.set(number, pending); }
|
|
571
|
+
return pending;
|
|
572
|
+
};
|
|
573
|
+
const observations = await Promise.all((options.receipts ?? []).map((reference) =>
|
|
574
|
+
readReceipt({ ...reference, confirmations: Math.max(reference.confirmations ?? 1, confirmationDepth) }, options, block, readIdentityBlock)));
|
|
575
|
+
const headers = new Map<bigint, Hex | undefined>();
|
|
576
|
+
for (const observation of observations) for (const pin of observation.pins) headers.set(pin.number, undefined);
|
|
577
|
+
// All asynchronous identity reads have settled. Equal observed/inclusion blocks
|
|
578
|
+
// and multiple receipts now share one fresh canonical header, never a stale cache.
|
|
579
|
+
await Promise.all([...headers.keys()].map(async (number) => {
|
|
580
|
+
const value = await lifecycleRpc(options.client, "eth_getBlockByNumber", [toHex(number), false]);
|
|
581
|
+
const hash = value === null ? undefined : rpcHex(rpcObject(value, "receipt canonical block").hash, "receipt canonical hash");
|
|
582
|
+
if (hash !== undefined && hash.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Receipt canonical block hash must have 32 bytes");
|
|
583
|
+
headers.set(number, hash);
|
|
584
|
+
}));
|
|
585
|
+
return observations.map((observation) => observation.finish(headers));
|
|
520
586
|
}
|
|
521
587
|
|
|
522
588
|
/** Reads confirmation-bound canonical state and receipt provenance; local step counters are never used. */
|
|
523
589
|
export async function readLaunchProgress(options: ReadLaunchProgressOptions, pinnedBlock?: LifecycleBlock): Promise<CanonicalLaunchProgress> {
|
|
524
590
|
if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
|
|
525
|
-
const
|
|
526
|
-
|
|
591
|
+
const planHash = options.planned.planHash, launchId = options.planned.launchId;
|
|
592
|
+
const result = await withLifecycleReadClient(options.client, async (client) =>
|
|
593
|
+
readLaunchProgressAtBlock(options, client, pinnedBlock ?? await readLifecycleBlock(client), pinnedBlock === undefined));
|
|
594
|
+
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
|
+
if (options.signal?.aborted) throw Object.assign(new Error("Lifecycle progress read cancelled"), { name: "AbortError" });
|
|
596
|
+
return result;
|
|
527
597
|
}
|
|
528
598
|
|
|
529
599
|
async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, client: LifecycleRpcClient, block: LifecycleBlock, checkBlock: boolean): Promise<CanonicalLaunchProgress> {
|
|
@@ -542,10 +612,13 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
|
|
|
542
612
|
assertActive();
|
|
543
613
|
const chainId = chainIdValue;
|
|
544
614
|
if (chainId !== planned.plan.chainId) throw new LifecyclePlanningError("CHAIN_MISMATCH", "RPC chain differs from the committed launch");
|
|
545
|
-
if (planned.
|
|
615
|
+
if (planned.ufg !== undefined) {
|
|
616
|
+
assertUfgLaunchAccount(planned.plan, planned.account);
|
|
617
|
+
if (hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG config differs from its finalized commitment");
|
|
618
|
+
} else if (planned.account.toLowerCase() !== planned.plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
|
|
546
619
|
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");
|
|
547
620
|
const [head, confirmedProgress, token, confirmedNonce, headNonce, pendingNonce] = await Promise.all([
|
|
548
|
-
|
|
621
|
+
readLifecycleHeadProgress(client, planned.plan, planned.account, block),
|
|
549
622
|
confirmedNumber === block.number ? undefined : readLifecycleContract<LaunchProgressV1>(client, planned.plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [planned.launchId], confirmedBlock, planned.account),
|
|
550
623
|
readLifecycleTokenAtBlock(client, planned.plan, block),
|
|
551
624
|
lifecycleRpc(client, "eth_getTransactionCount", [planned.account, toHex(confirmedBlock.number)]),
|
|
@@ -556,16 +629,23 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
|
|
|
556
629
|
const canonical = confirmedProgress ?? head;
|
|
557
630
|
if (token.toLowerCase() !== planned.predictedToken.toLowerCase()) throw new LifecyclePlanningError("TOKEN_IDENTITY", "Predicted token changed for the committed token configuration");
|
|
558
631
|
for (const progress of [canonical, head]) if (progress.phase !== LifecyclePhase.None) {
|
|
559
|
-
if (progress.planHash.toLowerCase() !== planned.planHash.toLowerCase() || progress.launchId.toLowerCase() !== planned.launchId.toLowerCase() || progress.creator.toLowerCase() !== planned.
|
|
632
|
+
if (progress.planHash.toLowerCase() !== planned.planHash.toLowerCase() || progress.launchId.toLowerCase() !== planned.launchId.toLowerCase() || progress.creator.toLowerCase() !== planned.plan.creator.toLowerCase() || progress.nonce !== planned.plan.nonce || progress.token.toLowerCase() !== token.toLowerCase() || progress.marketCount !== planned.plan.markets.length || progress.buyCount !== planned.plan.buys.length || progress.deadline !== planned.plan.deadline) throw new LifecyclePlanningError("PLAN_REPLAY", "This creator nonce is already bound to a different economic launch");
|
|
560
633
|
if (progress.mode !== (planned.mode === "atomic" ? LifecycleMode.Atomic : LifecycleMode.Staged)) throw new LifecyclePlanningError("MODE_MISMATCH", "Execution mode differs from the already recorded launch");
|
|
561
634
|
if (progress.preparedMarkets > progress.marketCount || progress.phase === LifecyclePhase.Activating || (progress.phase === LifecyclePhase.Ready && progress.preparedMarkets !== progress.marketCount)) throw new LifecyclePlanningError("INVALID_PROGRESS", "Canonical progress violates the committed ordered state machine");
|
|
562
635
|
}
|
|
636
|
+
if (planned.ufg !== undefined && head.phase !== LifecyclePhase.None) {
|
|
637
|
+
const [principal, configHash] = await Promise.all([
|
|
638
|
+
readLifecycleContract<Address>(client, UFG_LAUNCH_PROXY_ADDRESS, ufgLaunchProxyV1Abi, "principalOf", [planned.launchId], block),
|
|
639
|
+
readLifecycleContract<Hash>(client, UFG_LAUNCH_PROXY_ADDRESS, ufgLaunchProxyV1Abi, "launchConfigHash", [planned.launchId], block),
|
|
640
|
+
]);
|
|
641
|
+
if (principal.toLowerCase() !== planned.account.toLowerCase() || configHash.toLowerCase() !== planned.ufg.configHash.toLowerCase()) throw new LifecyclePlanningError("PLAN_REPLAY", "UFG proxy principal or launch config differs from the stored launch");
|
|
642
|
+
}
|
|
563
643
|
const confirmedAccountNonce = rpcQuantity(confirmedNonce, "confirmed account nonce");
|
|
564
644
|
const headAccountNonce = headNonce === undefined ? confirmedAccountNonce : rpcQuantity(headNonce, "head account nonce");
|
|
565
645
|
const pendingAccountNonce = rpcQuantity(pendingNonce, "pending account nonce");
|
|
566
646
|
const progressParameters = [{ type: "tuple", components: launchProgressV1Components }] as const;
|
|
567
647
|
const confirmationSafe = confirmedAccountNonce === headAccountNonce && pendingAccountNonce === headAccountNonce && keccak256(encodeAbiParameters(progressParameters, [canonical])) === keccak256(encodeAbiParameters(progressParameters, [head]));
|
|
568
|
-
const receipts = await
|
|
648
|
+
const receipts = await readReceipts({ ...options, client }, block, confirmationDepth);
|
|
569
649
|
assertActive();
|
|
570
650
|
let markets: LifecycleMarketProgress[] = [];
|
|
571
651
|
if (canonical.preparedMarkets > 0) {
|
package/src/lifecycle/rpc.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
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
|
+
type IndependentRead = { params: readonly unknown[]; resolve: (value: unknown) => void; reject: (failure: unknown) => void };
|
|
5
10
|
|
|
6
11
|
const lifecycleReadConcurrency = 8;
|
|
7
12
|
const diagnosticRpcMethods: Record<string, true> = {
|
|
@@ -67,6 +72,11 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
67
72
|
private readonly concurrency: number;
|
|
68
73
|
private diagnosticSequence = 0;
|
|
69
74
|
private chainId?: Promise<bigint>;
|
|
75
|
+
private gasPrice?: Promise<unknown>;
|
|
76
|
+
private readonly immutableReads = new Map<string, Promise<unknown>>();
|
|
77
|
+
private readonly canonicalPins = new Map<string, { block: Pick<LifecycleBlock, "number" | "hash">; chainId?: bigint }>();
|
|
78
|
+
private readonly independentReads = new Map<string, { block: LifecycleBlock; reads: IndependentRead[] }>();
|
|
79
|
+
readonly finalChecks = new Set<() => void>();
|
|
70
80
|
private readonly limitObservations = new Map<LifecycleLimitSource | undefined, Map<string, Promise<LifecycleLimitObservation>>>();
|
|
71
81
|
private readonly session?: SessionLifecycleCache;
|
|
72
82
|
private sessionGroup?: SessionReadGroup;
|
|
@@ -145,6 +155,103 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
145
155
|
return this.chainId ??= this.run("eth_chainId", []).then((value) => rpcQuantity(value, "chain ID"));
|
|
146
156
|
}
|
|
147
157
|
|
|
158
|
+
observeGasPrice(): Promise<unknown> {
|
|
159
|
+
this.assertOpen();
|
|
160
|
+
return this.gasPrice ??= this.run("eth_gasPrice", []);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
immutableRead(key: string, work: () => Promise<unknown>): Promise<unknown> {
|
|
164
|
+
this.assertOpen();
|
|
165
|
+
let pending = this.immutableReads.get(key);
|
|
166
|
+
if (pending === undefined) {
|
|
167
|
+
pending = work();
|
|
168
|
+
this.immutableReads.set(key, pending);
|
|
169
|
+
const owned = pending;
|
|
170
|
+
void pending.catch(() => { if (this.immutableReads.get(key) === owned) this.immutableReads.delete(key); });
|
|
171
|
+
}
|
|
172
|
+
return pending.then((value) => { this.assertOpen(); return value; });
|
|
173
|
+
}
|
|
174
|
+
|
|
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
|
+
|
|
187
|
+
async finalize(): Promise<void> {
|
|
188
|
+
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
|
+
for (const check of this.finalChecks) check();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
independentRead(params: readonly unknown[], block: LifecycleBlock, staticRead: boolean): Promise<unknown> {
|
|
210
|
+
return this.read("eth_call", params, block, staticRead, () => new Promise((resolve, reject) => {
|
|
211
|
+
const key = `${block.number}:${block.hash.toLowerCase()}`;
|
|
212
|
+
let group = this.independentReads.get(key);
|
|
213
|
+
if (group === undefined) {
|
|
214
|
+
group = { block, reads: [] };
|
|
215
|
+
this.independentReads.set(key, group);
|
|
216
|
+
queueMicrotask(() => {
|
|
217
|
+
this.independentReads.delete(key);
|
|
218
|
+
void this.flushIndependentReads(group!);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
group.reads.push({ params, resolve, reject });
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private async flushIndependentReads(group: { block: LifecycleBlock; reads: IndependentRead[] }): Promise<void> {
|
|
226
|
+
const { block, reads } = group;
|
|
227
|
+
try {
|
|
228
|
+
// Two cold reads cannot beat two direct calls after code attestation.
|
|
229
|
+
if (reads.length < 3 || await this.observeChainId() !== 4663n) {
|
|
230
|
+
await Promise.all(reads.map(async (read) => {
|
|
231
|
+
try { read.resolve(await this.run("eth_call", read.params)); }
|
|
232
|
+
catch (failure) { read.reject(failure); }
|
|
233
|
+
}));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const to = robinhoodMainnet.contracts.multicall3.address;
|
|
237
|
+
const code = rpcHex(await this.read("eth_getCode", [to, toHex(block.number)], block, true), "Multicall3 code");
|
|
238
|
+
if (keccak256(code) !== multicall3CodeHash) throw new LifecyclePlanningError("MULTICALL_CODE_MISMATCH", "Pinned Multicall3 runtime differs from the reviewed chain4663 deployment");
|
|
239
|
+
const calls = reads.map((read) => {
|
|
240
|
+
const request = read.params[0] as { to: Address; data: Hex };
|
|
241
|
+
return { target: request.to, allowFailure: true, callData: request.data };
|
|
242
|
+
});
|
|
243
|
+
const data = encodeFunctionData({ abi: multicall3Abi, functionName: "aggregate3", args: [calls] });
|
|
244
|
+
const raw = rpcHex(await this.run("eth_call", [{ to, data }, toHex(block.number)]), "Multicall3 result");
|
|
245
|
+
const results = decodeFunctionResult({ abi: multicall3Abi, functionName: "aggregate3", data: raw });
|
|
246
|
+
if (results.length !== reads.length) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Multicall3 omitted independent pinned results");
|
|
247
|
+
for (let index = 0; index < reads.length; index += 1) {
|
|
248
|
+
const result = results[index]!;
|
|
249
|
+
if (result.success) reads[index]!.resolve(result.returnData);
|
|
250
|
+
else reads[index]!.reject(new LifecyclePlanningError("MULTICALL_READ_FAILED", "An independent pinned getter reverted"));
|
|
251
|
+
}
|
|
252
|
+
} catch (failure) { for (const read of reads) read.reject(failure); }
|
|
253
|
+
}
|
|
254
|
+
|
|
148
255
|
observeLimits(context: LifecycleLimitContext, source?: LifecycleLimitSource): Promise<LifecycleLimitObservation> {
|
|
149
256
|
this.assertOpen();
|
|
150
257
|
// A caller callback is live policy, not an immutable pinned observation.
|
|
@@ -168,7 +275,7 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
168
275
|
return observation.then((result) => { this.assertOpen(); result.assertCurrent(); return result; });
|
|
169
276
|
}
|
|
170
277
|
|
|
171
|
-
read(method: string, params: readonly unknown[], block: LifecycleBlock, staticRead = false): Promise<unknown> {
|
|
278
|
+
read(method: string, params: readonly unknown[], block: LifecycleBlock, staticRead = false, load?: () => Promise<unknown>): Promise<unknown> {
|
|
172
279
|
this.assertOpen();
|
|
173
280
|
// Source identity is this scope's original client. The complete wire call includes
|
|
174
281
|
// caller, value, gas and any other call fields; no selector-only/ABI-name aliasing.
|
|
@@ -189,7 +296,7 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
189
296
|
pending = Promise.resolve(cached);
|
|
190
297
|
} else {
|
|
191
298
|
if (staticRead && this.session !== undefined) this.sessionMisses += 1;
|
|
192
|
-
pending = this.run(method, params);
|
|
299
|
+
pending = load === undefined ? this.run(method, params) : load();
|
|
193
300
|
if (group !== undefined) pending = pending.then((value) => {
|
|
194
301
|
// Never share in-flight work or install late results after invalidation/closure.
|
|
195
302
|
// Only wire hex is retained, so public decoded DTOs cannot mutate authority.
|
|
@@ -207,6 +314,11 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
207
314
|
this.reads.clear();
|
|
208
315
|
this.chainId = undefined;
|
|
209
316
|
this.limitObservations.clear();
|
|
317
|
+
this.gasPrice = undefined;
|
|
318
|
+
this.immutableReads.clear();
|
|
319
|
+
this.canonicalPins.clear();
|
|
320
|
+
this.independentReads.clear();
|
|
321
|
+
this.finalChecks.clear();
|
|
210
322
|
const listener = this.source.onDiagnostic;
|
|
211
323
|
if (this.session !== undefined && listener !== undefined) emitLifecycleDiagnostic(listener, {
|
|
212
324
|
stage: "session-cache", phase: success ? "success" : "failure",
|
|
@@ -221,7 +333,15 @@ export async function withLifecycleReadClient<T>(source: LifecycleRpcClient, wor
|
|
|
221
333
|
const client = new InvocationLifecycleClient(source);
|
|
222
334
|
let success = false;
|
|
223
335
|
try {
|
|
224
|
-
|
|
336
|
+
let result: T;
|
|
337
|
+
try { result = await work(client); }
|
|
338
|
+
catch (failure) {
|
|
339
|
+
// Refused simulations still report a changed canonical source, not a
|
|
340
|
+
// misleading economic refusal derived from an orphaned observation.
|
|
341
|
+
await client.finalize();
|
|
342
|
+
throw failure;
|
|
343
|
+
}
|
|
344
|
+
await client.finalize();
|
|
225
345
|
success = true;
|
|
226
346
|
return result;
|
|
227
347
|
} finally { client.close(success); }
|
|
@@ -285,6 +405,28 @@ export function readLifecycleChainId(client: LifecycleRpcClient): Promise<bigint
|
|
|
285
405
|
? client.observeChainId()
|
|
286
406
|
: lifecycleRpc(client, "eth_chainId").then((value) => rpcQuantity(value, "chain ID"));
|
|
287
407
|
}
|
|
408
|
+
|
|
409
|
+
/** A single operation-local recommendation, never a session or cross-head quote. */
|
|
410
|
+
export function readLifecycleGasPrice(client: LifecycleRpcClient): Promise<unknown> {
|
|
411
|
+
return client instanceof InvocationLifecycleClient ? client.observeGasPrice() : lifecycleRpc(client, "eth_gasPrice");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Only the reviewed immutable factory identity may use an invocation-wide key. */
|
|
415
|
+
export function readLifecycleImmutable<T>(client: LifecycleRpcClient, key: string, work: () => Promise<T>): Promise<T> {
|
|
416
|
+
return (client instanceof InvocationLifecycleClient ? client.immutableRead(key, work) : work()) as Promise<T>;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Recheck live policy after the owner's final asynchronous canonical guards. */
|
|
420
|
+
export function registerLifecycleFinalCheck(client: LifecycleRpcClient, check: () => void): void {
|
|
421
|
+
if (client instanceof InvocationLifecycleClient) { client.assertOpen(); client.finalChecks.add(check); }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** SDK-selected caller-independent getters only; never poster quotes or execution. */
|
|
425
|
+
function lifecycleIndependentRpc(client: LifecycleRpcClient, params: readonly unknown[], block: LifecycleBlock, staticRead: boolean): Promise<unknown> {
|
|
426
|
+
return client instanceof InvocationLifecycleClient
|
|
427
|
+
? client.independentRead(params, block, staticRead)
|
|
428
|
+
: lifecyclePinnedRpc(client, "eth_call", params, block, staticRead);
|
|
429
|
+
}
|
|
288
430
|
export function rpcObject(value: unknown, label: string): Record<string, unknown> {
|
|
289
431
|
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", `${label} must be an object`);
|
|
290
432
|
// The object guard establishes the RPC record boundary; fields remain untrusted unknowns.
|
|
@@ -325,7 +467,11 @@ export async function readLifecycleBlock(client: LifecycleRpcClient, tag: Hex |
|
|
|
325
467
|
if (client instanceof InvocationLifecycleClient) client.observeBlock(block, tag === "latest");
|
|
326
468
|
return block;
|
|
327
469
|
}
|
|
328
|
-
export async function assertLifecycleBlock(client: LifecycleRpcClient, block: LifecycleBlock, expectedChainId?: bigint): Promise<void> {
|
|
470
|
+
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
|
+
}
|
|
329
475
|
const [chainId, current] = await Promise.all([
|
|
330
476
|
expectedChainId === undefined ? undefined : lifecycleRpc(client, "eth_chainId"),
|
|
331
477
|
readLifecycleBlock(client, toHex(block.number)),
|
|
@@ -339,13 +485,15 @@ export async function assertLifecycleBlock(client: LifecycleRpcClient, block: Li
|
|
|
339
485
|
throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch state changed during the operation; read canonical progress again");
|
|
340
486
|
}
|
|
341
487
|
}
|
|
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> {
|
|
488
|
+
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
489
|
const data = encodeFunctionData({ abi, functionName, args });
|
|
344
490
|
const request = [{ to, from, data }, toHex(block.number)];
|
|
345
491
|
const getter = abi.some((item) => item.type === "function" && item.name === functionName && (item.stateMutability === "view" || item.stateMutability === "pure"));
|
|
346
|
-
const result = rpcHex(await (
|
|
347
|
-
?
|
|
348
|
-
:
|
|
492
|
+
const result = rpcHex(await (independent
|
|
493
|
+
? lifecycleIndependentRpc(client, request, block, staticRead)
|
|
494
|
+
: getter
|
|
495
|
+
? staticRead ? lifecycleStaticRpc(client, "eth_call", request, block) : lifecyclePinnedRpc(client, "eth_call", request, block)
|
|
496
|
+
: lifecycleRpc(client, "eth_call", request)), functionName);
|
|
349
497
|
// ABI decoding validates scalar widths and tuple layout; T names that exact frozen ABI result.
|
|
350
498
|
const decoded = decodeFunctionResult({ abi, functionName, data: result }) as T;
|
|
351
499
|
return decoded;
|
|
@@ -382,7 +530,7 @@ export async function readNitroPosterGas(transaction: LifecycleTransaction, cont
|
|
|
382
530
|
|
|
383
531
|
async function readNitroScalar(context: LifecycleLimitContext, to: Address, abi: Abi, functionName: string): Promise<bigint> {
|
|
384
532
|
const data = encodeFunctionData({ abi, functionName });
|
|
385
|
-
const result = rpcHex(await
|
|
533
|
+
const result = rpcHex(await lifecycleIndependentRpc(context.client, [{ to, data }, toHex(context.block.number)], context.block, true), functionName);
|
|
386
534
|
if (result.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", `${functionName} must return exactly one ABI word`);
|
|
387
535
|
return decodeFunctionResult({ abi, functionName, data: result }) as bigint;
|
|
388
536
|
}
|