@black-market/sdk 0.14.1 → 0.15.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 +9 -7
- package/dist/lifecycle/authors.js +2 -2
- package/dist/lifecycle/authors.js.map +1 -1
- package/dist/lifecycle/auto-compound.d.ts +21 -0
- package/dist/lifecycle/auto-compound.d.ts.map +1 -0
- package/dist/lifecycle/auto-compound.js +34 -0
- package/dist/lifecycle/auto-compound.js.map +1 -0
- package/dist/lifecycle/calibration.d.ts +2 -4
- package/dist/lifecycle/calibration.d.ts.map +1 -1
- package/dist/lifecycle/calibration.js +1 -26
- package/dist/lifecycle/calibration.js.map +1 -1
- package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.d.ts +73 -0
- package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.d.ts.map +1 -0
- package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.js +75 -0
- package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.js.map +1 -0
- package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.d.ts +73 -0
- package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.d.ts.map +1 -0
- package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.js +75 -0
- package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.js.map +1 -0
- package/dist/lifecycle/gas-profile.d.ts +39 -0
- package/dist/lifecycle/gas-profile.d.ts.map +1 -0
- package/dist/lifecycle/gas-profile.js +193 -0
- package/dist/lifecycle/gas-profile.js.map +1 -0
- package/dist/lifecycle/index.d.ts +6 -4
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +6 -4
- package/dist/lifecycle/index.js.map +1 -1
- package/dist/lifecycle/markets.d.ts +2 -0
- package/dist/lifecycle/markets.d.ts.map +1 -1
- package/dist/lifecycle/markets.js +30 -3
- package/dist/lifecycle/markets.js.map +1 -1
- package/dist/lifecycle/opening-buys.d.ts +13 -0
- package/dist/lifecycle/opening-buys.d.ts.map +1 -0
- package/dist/lifecycle/opening-buys.js +171 -0
- package/dist/lifecycle/opening-buys.js.map +1 -0
- package/dist/lifecycle/planner.d.ts +6 -9
- package/dist/lifecycle/planner.d.ts.map +1 -1
- package/dist/lifecycle/planner.js +175 -265
- package/dist/lifecycle/planner.js.map +1 -1
- package/dist/lifecycle/presets.d.ts +10 -3
- package/dist/lifecycle/presets.d.ts.map +1 -1
- package/dist/lifecycle/presets.js +13 -1
- package/dist/lifecycle/presets.js.map +1 -1
- package/dist/lifecycle/progress.d.ts +1 -1
- package/dist/lifecycle/progress.d.ts.map +1 -1
- package/dist/lifecycle/progress.js +21 -20
- package/dist/lifecycle/progress.js.map +1 -1
- package/dist/lifecycle/rpc.d.ts +25 -30
- package/dist/lifecycle/rpc.d.ts.map +1 -1
- package/dist/lifecycle/rpc.js +63 -81
- package/dist/lifecycle/rpc.js.map +1 -1
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.js +2 -4
- package/dist/lifecycle/snapshot.js.map +1 -1
- package/dist/lifecycle/transactions.d.ts +29 -6
- package/dist/lifecycle/transactions.d.ts.map +1 -1
- package/dist/lifecycle/transactions.js +62 -27
- package/dist/lifecycle/transactions.js.map +1 -1
- package/dist/lifecycle/types.d.ts +55 -122
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js +3 -3
- package/dist/lifecycle/types.js.map +1 -1
- package/dist/lifecycle/ufg.d.ts +1 -1
- package/dist/lifecycle/ufg.d.ts.map +1 -1
- package/dist/lifecycle/ufg.js +4 -8
- package/dist/lifecycle/ufg.js.map +1 -1
- package/docs/launch-lifecycle.md +110 -78
- package/docs/lifecycle-gas-profiles.md +90 -0
- package/docs/reference.md +99 -47
- package/examples/.env.example +0 -3
- package/examples/README.md +14 -11
- package/examples/launch-example-support.ts +4 -9
- package/examples/launch-example.ts +30 -36
- package/examples/launch-lifecycle-smoke.ts +11 -21
- package/examples/launch.ts +8 -11
- package/package.json +1 -1
- package/src/lifecycle/authors.ts +2 -2
- package/src/lifecycle/auto-compound.ts +38 -0
- package/src/lifecycle/calibration.ts +3 -23
- package/src/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.ts +77 -0
- package/src/lifecycle/deployments/auto-compound-static-pool-v1-20261028.ts +77 -0
- package/src/lifecycle/gas-profile.ts +186 -0
- package/src/lifecycle/index.ts +6 -4
- package/src/lifecycle/markets.ts +25 -3
- package/src/lifecycle/opening-buys.ts +157 -0
- package/src/lifecycle/planner.ts +168 -247
- package/src/lifecycle/presets.ts +15 -1
- package/src/lifecycle/progress.ts +19 -20
- package/src/lifecycle/rpc.ts +58 -68
- package/src/lifecycle/snapshot.ts +2 -4
- package/src/lifecycle/transactions.ts +69 -27
- package/src/lifecycle/types.ts +60 -99
- package/src/lifecycle/ufg.ts +4 -8
- package/dist/lifecycle/fork.d.ts +0 -15
- package/dist/lifecycle/fork.d.ts.map +0 -1
- package/dist/lifecycle/fork.js +0 -36
- package/dist/lifecycle/fork.js.map +0 -1
- package/dist/lifecycle/simulation.d.ts +0 -18
- package/dist/lifecycle/simulation.d.ts.map +0 -1
- package/dist/lifecycle/simulation.js +0 -754
- package/dist/lifecycle/simulation.js.map +0 -1
- package/src/lifecycle/fork.ts +0 -36
- package/src/lifecycle/simulation.ts +0 -689
package/src/lifecycle/presets.ts
CHANGED
|
@@ -11,6 +11,9 @@ import { HMM_FIXED_PROFILE_ID, HMM_DYNAMIC_PROFILE_ID, isHmmProfile } from "./hm
|
|
|
11
11
|
import { ufgFixedEnvelope, ufgFixedAdapter, ufgFixedRegistration } from "./deployments/ufg-fixed-pool-20261021.js";
|
|
12
12
|
import { ufgDynamicEnvelope, ufgDynamicAdapter, ufgDynamicRegistration } from "./deployments/ufg-dynamic-pool-20261021.js";
|
|
13
13
|
import { UFG_FIXED_PROFILE_ID, UFG_DYNAMIC_PROFILE_ID } from "./ufg-config.js";
|
|
14
|
+
import { autoCompoundStaticEnvelope, autoCompoundStaticAdapter, autoCompoundStaticRegistration, autoCompoundStaticCreationCode } from "./deployments/auto-compound-static-pool-v1-20261028.js";
|
|
15
|
+
import { autoCompoundDynamicEnvelope, autoCompoundDynamicAdapter, autoCompoundDynamicRegistration, autoCompoundDynamicCreationCode } from "./deployments/auto-compound-dynamic-pool-v1-20261028.js";
|
|
16
|
+
import { AUTO_COMPOUND_STATIC_V1_PROFILE_ID, AUTO_COMPOUND_DYNAMIC_V1_PROFILE_ID } from "./auto-compound.js";
|
|
14
17
|
|
|
15
18
|
export enum KnownLifecycleProfile {
|
|
16
19
|
Abyss0 = "abyss-0",
|
|
@@ -23,6 +26,8 @@ export enum KnownLifecycleProfile {
|
|
|
23
26
|
V4HmmDynamicPool = "v4-hmm-dynamic-pool",
|
|
24
27
|
V4UfgFixedPool = "v4-ufg-fixed-pool",
|
|
25
28
|
V4UfgDynamicPool = "v4-ufg-dynamic-pool",
|
|
29
|
+
V4AutoCompoundStaticPool = "v4-auto-compound-static-pool",
|
|
30
|
+
V4AutoCompoundDynamicPool = "v4-auto-compound-dynamic-pool",
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
export enum KnownLifecycleHook {
|
|
@@ -30,11 +35,16 @@ export enum KnownLifecycleHook {
|
|
|
30
35
|
DynamicFeePoolV1 = "dynamic-fee-pool-v1",
|
|
31
36
|
HmmFixedPoolV2 = "hmm-fixed-pool-v2",
|
|
32
37
|
HmmDynamicPoolV2 = "hmm-dynamic-pool-v2",
|
|
38
|
+
AutoCompoundStaticV1 = "auto-compound-static-v1",
|
|
39
|
+
AutoCompoundDynamicV1 = "auto-compound-dynamic-v1",
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
Object.freeze(KnownLifecycleProfile);
|
|
36
43
|
Object.freeze(KnownLifecycleHook);
|
|
37
44
|
|
|
45
|
+
/** Total launch-transaction gas limit, including Nitro poster gas; unused gas is refunded. */
|
|
46
|
+
export const ROBINHOOD_NATIVE_TRANSACTION_GAS_LIMIT = 32_000_000n;
|
|
47
|
+
|
|
38
48
|
/** Frozen construction metadata, not a current eligibility or execution observation. */
|
|
39
49
|
export type LifecycleProfilePreset = {
|
|
40
50
|
readonly key: KnownLifecycleProfile;
|
|
@@ -61,6 +71,7 @@ export type LifecycleDeploymentPreset = {
|
|
|
61
71
|
readonly tokenFactory: Address;
|
|
62
72
|
readonly tokenFactoryCodeHash: Hex;
|
|
63
73
|
readonly fundingEscrow: Address;
|
|
74
|
+
readonly nativeTransactionGasLimit?: bigint;
|
|
64
75
|
/** Optional SDK-owned read-only acceleration contracts. */
|
|
65
76
|
readonly hookSaltMiner?: Address;
|
|
66
77
|
readonly launchLens?: Address;
|
|
@@ -330,6 +341,7 @@ const knownDeployment: LifecycleDeploymentPreset = Object.freeze({
|
|
|
330
341
|
tokenFactory: "0xd3cE64E49224a9a96075633f63761FE6BE22FE30",
|
|
331
342
|
tokenFactoryCodeHash: "0x6bbbdfe93ec615b7f1e70bac5e69aa890eba5e5f248f25e7022e866b4064d541",
|
|
332
343
|
fundingEscrow: "0x7bc77946CeF52A178583FCe1EEB7751983cEC703",
|
|
344
|
+
nativeTransactionGasLimit: ROBINHOOD_NATIVE_TRANSACTION_GAS_LIMIT,
|
|
333
345
|
hookSaltMiner: "0x2f06176ba739b609b6a7334e2e6647558ba78ad5",
|
|
334
346
|
// LaunchLens/871f9385, L2 deployment block 100095353.
|
|
335
347
|
// tx 0xafdf35fa50d2c3fe08f1ab231a4e5d5e4dd5629c57f14dfe0a3aff5b0b8404ff
|
|
@@ -346,6 +358,8 @@ const knownDeployment: LifecycleDeploymentPreset = Object.freeze({
|
|
|
346
358
|
poolBoundPreset(KnownLifecycleProfile.V4HmmDynamicPool, HMM_DYNAMIC_PROFILE_ID, hmmDynamicEnvelope, hmmDynamicAdapter, hmmDynamicRegistration, hmmDynamicCreationCode),
|
|
347
359
|
poolBoundPreset(KnownLifecycleProfile.V4UfgFixedPool, UFG_FIXED_PROFILE_ID, ufgFixedEnvelope, ufgFixedAdapter, ufgFixedRegistration, fixedFeePoolHookV1CreationCode),
|
|
348
360
|
poolBoundPreset(KnownLifecycleProfile.V4UfgDynamicPool, UFG_DYNAMIC_PROFILE_ID, ufgDynamicEnvelope, ufgDynamicAdapter, ufgDynamicRegistration, dynamicFeePoolHookV1CreationCode),
|
|
361
|
+
poolBoundPreset(KnownLifecycleProfile.V4AutoCompoundStaticPool, AUTO_COMPOUND_STATIC_V1_PROFILE_ID, autoCompoundStaticEnvelope, autoCompoundStaticAdapter, autoCompoundStaticRegistration, autoCompoundStaticCreationCode),
|
|
362
|
+
poolBoundPreset(KnownLifecycleProfile.V4AutoCompoundDynamicPool, AUTO_COMPOUND_DYNAMIC_V1_PROFILE_ID, autoCompoundDynamicEnvelope, autoCompoundDynamicAdapter, autoCompoundDynamicRegistration, autoCompoundDynamicCreationCode),
|
|
349
363
|
]),
|
|
350
364
|
provenance: Object.freeze({ sourceCommit: "e174ce695c7010f50ca9211b07b9f178c990beac", deploymentBlock: 82880546n }),
|
|
351
365
|
});
|
|
@@ -375,7 +389,7 @@ export function getKnownLifecycleProfile(options: KnownLifecycleDeploymentIdenti
|
|
|
375
389
|
return undefined;
|
|
376
390
|
}
|
|
377
391
|
|
|
378
|
-
/** Stable, frozen order: Abyss
|
|
392
|
+
/** Stable, frozen order: Abyss, fixed/dynamic V4, HMM, UFG, then auto-compounding. */
|
|
379
393
|
export function listKnownLifecycleProfiles(options: KnownLifecycleDeploymentIdentity): readonly LifecycleProfilePreset[] {
|
|
380
394
|
return getKnownLifecycleDeployment(options)?.profiles ?? noProfiles;
|
|
381
395
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { concatHex, decodeFunctionData, encodeAbiParameters, encodeFunctionData, keccak256, stringToHex, toHex, zeroAddress, zeroHash, type Address, type
|
|
1
|
+
import { concatHex, decodeFunctionData, encodeAbiParameters, encodeFunctionData, keccak256, stringToHex, toHex, zeroAddress, zeroHash, type Address, type Hex } from "viem";
|
|
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";
|
|
@@ -507,7 +507,12 @@ type ReceiptObservation = {
|
|
|
507
507
|
|
|
508
508
|
async function readReceipt(reference: LifecycleReceiptReference, options: ReadLaunchProgressOptions, block: LifecycleBlock, readIdentityBlock: (number: bigint) => Promise<LifecycleBlock>): Promise<ReceiptObservation> {
|
|
509
509
|
const effectiveHash = reference.replacementHash ?? reference.transactionHash;
|
|
510
|
-
const base
|
|
510
|
+
const base: Pick<LifecycleReceiptStatus, "transactionHash" | "effectiveHash" | "replaced" | "nonce"> = {
|
|
511
|
+
transactionHash: reference.transactionHash, effectiveHash,
|
|
512
|
+
replaced: reference.replacementHash !== undefined && reference.replacementHash.toLowerCase() !== reference.transactionHash.toLowerCase(),
|
|
513
|
+
...(reference.nonce === undefined ? {} : { nonce: reference.nonce }),
|
|
514
|
+
};
|
|
515
|
+
if (reference.nonce !== undefined && (typeof reference.nonce !== "bigint" || reference.nonce < 0n || reference.nonce >= 1n << 64n)) throw new LifecyclePlanningError("INVALID_RECEIPT_NONCE", "Observed submitted nonce must be a uint64 bigint");
|
|
511
516
|
const pins: { number: bigint; hash: Hex }[] = [];
|
|
512
517
|
const observedNumber = reference.observedBlockNumber, observedHash = reference.observedBlockHash;
|
|
513
518
|
if (observedNumber !== undefined && observedHash !== undefined) pins.push({ number: observedNumber, hash: observedHash });
|
|
@@ -527,13 +532,20 @@ async function readReceipt(reference: LifecycleReceiptReference, options: ReadLa
|
|
|
527
532
|
lifecycleRpc(options.client, "eth_getTransactionReceipt", [effectiveHash]),
|
|
528
533
|
lifecycleRpc(options.client, "eth_getTransactionByHash", [effectiveHash]),
|
|
529
534
|
]);
|
|
535
|
+
const transaction = transactionValue === null ? undefined : rpcObject(transactionValue, "receipt transaction");
|
|
536
|
+
if (transaction !== undefined) {
|
|
537
|
+
if (rpcHex(transaction.from, "receipt transaction account").toLowerCase() !== options.planned.account.toLowerCase()) throw new LifecyclePlanningError("RECEIPT_ACCOUNT", "Submitted transaction belongs to a different account");
|
|
538
|
+
const nonce = rpcQuantity(transaction.nonce, "submitted transaction nonce");
|
|
539
|
+
if (reference.nonce !== undefined && nonce !== reference.nonce) throw new LifecyclePlanningError("RECEIPT_NONCE", "Submitted or replacement transaction differs from the retained nonce");
|
|
540
|
+
base.nonce = nonce;
|
|
541
|
+
}
|
|
530
542
|
if (receiptValue === null) return finish({ ...base, status: "pending", blockNumber: observedNumber, blockHash: observedHash });
|
|
531
543
|
if (transactionValue === null) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Receipt exists without its canonical transaction");
|
|
532
544
|
const receipt = rpcObject(receiptValue, "receipt");
|
|
533
545
|
const blockNumber = rpcQuantity(receipt.blockNumber, "receipt block number");
|
|
534
546
|
const blockHash = rpcHex(receipt.blockHash, "receipt block hash");
|
|
535
547
|
pins.push({ number: blockNumber, hash: blockHash });
|
|
536
|
-
const matches = await transactionMatchesLaunch(
|
|
548
|
+
const matches = await transactionMatchesLaunch(transaction!, options,
|
|
537
549
|
() => readIdentityBlock(blockNumber));
|
|
538
550
|
const confirmations = block.number >= blockNumber ? block.number - blockNumber + 1n : 0n;
|
|
539
551
|
const required = reference.confirmations ?? 1;
|
|
@@ -595,7 +607,7 @@ export async function readLaunchProgress(options: ReadLaunchProgressOptions, pin
|
|
|
595
607
|
return result;
|
|
596
608
|
}
|
|
597
609
|
|
|
598
|
-
/** Build-next needs the state machine
|
|
610
|
+
/** Build-next needs the state machine and receipts, not account nonce or display-only markets. */
|
|
599
611
|
export async function readLifecycleExecutionProgress(options: ReadLaunchProgressOptions, block: LifecycleBlock): Promise<CanonicalLaunchProgress> {
|
|
600
612
|
if (!assertLifecycleReadClientOpen(options.client)) throw new LifecyclePlanningError("INVALID_READ_CONTEXT", "Execution progress requires an invocation owner");
|
|
601
613
|
return readLaunchProgressAtBlock(options, options.client, block, false);
|
|
@@ -622,13 +634,10 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
|
|
|
622
634
|
if (hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG config differs from its finalized commitment");
|
|
623
635
|
} else if (planned.account.toLowerCase() !== planned.plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
|
|
624
636
|
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");
|
|
625
|
-
const [head, confirmedProgress, token
|
|
637
|
+
const [head, confirmedProgress, token] = await Promise.all([
|
|
626
638
|
readLifecycleHeadProgress(client, planned.plan, planned.account, block),
|
|
627
639
|
confirmedNumber === block.number ? undefined : readLifecycleContract<LaunchProgressV1>(client, planned.plan.orchestrator, launchLifecycleAbi, "readLaunchProgress", [planned.launchId], confirmedBlock, planned.account),
|
|
628
640
|
readLifecycleTokenAtBlock(client, planned.plan, block),
|
|
629
|
-
lifecycleRpc(client, "eth_getTransactionCount", [planned.account, toHex(confirmedBlock.number)]),
|
|
630
|
-
confirmedNumber === block.number ? undefined : lifecycleRpc(client, "eth_getTransactionCount", [planned.account, toHex(block.number)]),
|
|
631
|
-
lifecycleRpc(client, "eth_getTransactionCount", [planned.account, "pending"]),
|
|
632
641
|
]);
|
|
633
642
|
assertActive();
|
|
634
643
|
const canonical = confirmedProgress ?? head;
|
|
@@ -638,18 +647,8 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
|
|
|
638
647
|
if (progress.mode !== (planned.mode === "atomic" ? LifecycleMode.Atomic : LifecycleMode.Staged)) throw new LifecyclePlanningError("MODE_MISMATCH", "Execution mode differs from the already recorded launch");
|
|
639
648
|
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");
|
|
640
649
|
}
|
|
641
|
-
if (planned.ufg !== undefined && head.phase !== LifecyclePhase.None) {
|
|
642
|
-
const [principal, configHash] = await Promise.all([
|
|
643
|
-
readLifecycleContract<Address>(client, UFG_LAUNCH_PROXY_ADDRESS, ufgLaunchProxyV1Abi, "principalOf", [planned.launchId], block),
|
|
644
|
-
readLifecycleContract<Hash>(client, UFG_LAUNCH_PROXY_ADDRESS, ufgLaunchProxyV1Abi, "launchConfigHash", [planned.launchId], block),
|
|
645
|
-
]);
|
|
646
|
-
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");
|
|
647
|
-
}
|
|
648
|
-
const confirmedAccountNonce = rpcQuantity(confirmedNonce, "confirmed account nonce");
|
|
649
|
-
const headAccountNonce = headNonce === undefined ? confirmedAccountNonce : rpcQuantity(headNonce, "head account nonce");
|
|
650
|
-
const pendingAccountNonce = rpcQuantity(pendingNonce, "pending account nonce");
|
|
651
650
|
const progressParameters = [{ type: "tuple", components: launchProgressV1Components }] as const;
|
|
652
|
-
const confirmationSafe =
|
|
651
|
+
const confirmationSafe = keccak256(encodeAbiParameters(progressParameters, [canonical])) === keccak256(encodeAbiParameters(progressParameters, [head]));
|
|
653
652
|
const receipts = await readReceipts({ ...options, client }, block, confirmationDepth);
|
|
654
653
|
assertActive();
|
|
655
654
|
let markets: LifecycleMarketProgress[] = [];
|
|
@@ -676,5 +675,5 @@ async function readLaunchProgressAtBlock(options: ReadLaunchProgressOptions, cli
|
|
|
676
675
|
}
|
|
677
676
|
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");
|
|
678
677
|
assertActive();
|
|
679
|
-
return { canonical, head, confirmationDepth, confirmedBlockNumber: confirmedBlock.number, confirmedBlockHash: confirmedBlock.hash,
|
|
678
|
+
return { canonical, head, confirmationDepth, confirmedBlockNumber: confirmedBlock.number, confirmedBlockHash: confirmedBlock.hash, confirmationSafe, token, planHash: planned.planHash, launchId: planned.launchId, chainId, blockNumber: block.number, blockHash: block.hash, markets, receipts };
|
|
680
679
|
}
|
package/src/lifecycle/rpc.ts
CHANGED
|
@@ -3,18 +3,27 @@ import { LifecyclePlanningError, type LifecycleBlock, type LifecycleDiagnosticEv
|
|
|
3
3
|
import { lifecycleRegistryAbi } from "./abi.js";
|
|
4
4
|
import type { ProfileTopologyV1 } from "./schema.js";
|
|
5
5
|
import { robinhoodMainnet } from "../addresses.js";
|
|
6
|
+
import { ROBINHOOD_NATIVE_TRANSACTION_GAS_LIMIT } from "./presets.js";
|
|
6
7
|
|
|
7
8
|
// Reviewed chain4663 deployment, independently observed at block 98,136,655.
|
|
8
9
|
const multicall3CodeHash = "0xd5c15df687b16f2ff992fc8d767b4216323184a2bbc6ee2f9c398c318e770891";
|
|
9
10
|
const sessionObservations = new WeakMap<object, Map<string, Promise<unknown>>>();
|
|
11
|
+
const clientChainIds = new WeakMap<LifecycleRpcClient, Promise<bigint>>();
|
|
12
|
+
|
|
13
|
+
function rememberClientChainId(source: LifecycleRpcClient, request: Promise<unknown>): Promise<bigint> {
|
|
14
|
+
const pending = request.then((value) => rpcQuantity(value, "chain ID"));
|
|
15
|
+
clientChainIds.set(source, pending);
|
|
16
|
+
void pending.catch(() => { if (clientChainIds.get(source) === pending) clientChainIds.delete(source); });
|
|
17
|
+
return pending;
|
|
18
|
+
}
|
|
10
19
|
const canonicalWireValue = (_key: string, value: unknown): unknown => typeof value === "string" && value.startsWith("0x") ? value.toLowerCase() : value;
|
|
11
20
|
type IndependentRead = { params: readonly unknown[]; resolve: (value: unknown) => void; reject: (failure: unknown) => void };
|
|
12
21
|
|
|
13
22
|
const lifecycleReadConcurrency = 8;
|
|
14
23
|
const diagnosticRpcMethods: Record<string, true> = {
|
|
15
24
|
eth_chainId: true, eth_getBlockByNumber: true, eth_getBlockByHash: true, eth_getCode: true,
|
|
16
|
-
eth_getBalance: true, eth_call: true,
|
|
17
|
-
|
|
25
|
+
eth_getBalance: true, eth_call: true, eth_gasPrice: true,
|
|
26
|
+
eth_getTransactionReceipt: true, eth_getTransactionByHash: true,
|
|
18
27
|
};
|
|
19
28
|
|
|
20
29
|
type SessionReadGroup = {
|
|
@@ -61,7 +70,7 @@ class SessionLifecycleCache implements LifecycleSessionCache {
|
|
|
61
70
|
}
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
/** Opt
|
|
73
|
+
/** Opt-in pinned read reuse; client-bound identity/deployment facts are separate. */
|
|
65
74
|
export function createLifecycleSessionCache(): LifecycleSessionCache {
|
|
66
75
|
return new SessionLifecycleCache();
|
|
67
76
|
}
|
|
@@ -153,7 +162,17 @@ class InvocationLifecycleClient implements LifecycleRpcClient {
|
|
|
153
162
|
|
|
154
163
|
observeChainId(): Promise<bigint> {
|
|
155
164
|
this.assertOpen();
|
|
156
|
-
|
|
165
|
+
if (this.chainId === undefined) {
|
|
166
|
+
const observed = clientChainIds.get(this.source) ?? rememberClientChainId(this.source, this.run("eth_chainId", []));
|
|
167
|
+
this.chainId = this.session === undefined ? observed : observed.then((chainId) => {
|
|
168
|
+
if (!this.closed) {
|
|
169
|
+
this.observedChainId = chainId;
|
|
170
|
+
if (this.sessionGroup !== undefined) this.session!.observeChain(this.sessionGroup, chainId);
|
|
171
|
+
}
|
|
172
|
+
return chainId;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return this.chainId;
|
|
157
176
|
}
|
|
158
177
|
|
|
159
178
|
observeGasPrice(): Promise<unknown> {
|
|
@@ -373,16 +392,17 @@ export function lifecycleStaticRpc(client: LifecycleRpcClient, method: "eth_call
|
|
|
373
392
|
export async function lifecycleRpc(client: LifecycleRpcClient, method: string, params: readonly unknown[] = []): Promise<unknown> {
|
|
374
393
|
return client instanceof InvocationLifecycleClient ? client.run(method, params) : client.request({ method, params });
|
|
375
394
|
}
|
|
376
|
-
/**
|
|
395
|
+
/** Client-bound identity, observed once per underlying client, never a freshness guard. */
|
|
377
396
|
export function readLifecycleChainId(client: LifecycleRpcClient): Promise<bigint> {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
: lifecycleRpc(client, "eth_chainId").then((value) => rpcQuantity(value, "chain ID"));
|
|
397
|
+
if (client instanceof InvocationLifecycleClient) return client.observeChainId();
|
|
398
|
+
return clientChainIds.get(client) ?? rememberClientChainId(client, lifecycleRpc(client, "eth_chainId"));
|
|
381
399
|
}
|
|
382
400
|
|
|
383
401
|
/** A single operation-local recommendation, never a session or cross-head quote. */
|
|
384
|
-
export function readLifecycleGasPrice(client: LifecycleRpcClient): Promise<
|
|
385
|
-
|
|
402
|
+
export function readLifecycleGasPrice(client: LifecycleRpcClient, block: LifecycleBlock, chainId: bigint): Promise<bigint> {
|
|
403
|
+
if (chainId === 4663n) return readNitroPrices(client, block).then((prices) => prices[5]);
|
|
404
|
+
return (client instanceof InvocationLifecycleClient ? client.observeGasPrice() : lifecycleRpc(client, "eth_gasPrice"))
|
|
405
|
+
.then((price) => rpcQuantity(price, "gas price"));
|
|
386
406
|
}
|
|
387
407
|
|
|
388
408
|
/** Only the reviewed immutable factory identity may use an invocation-wide key. */
|
|
@@ -390,7 +410,7 @@ export function readLifecycleImmutable<T>(client: LifecycleRpcClient, key: strin
|
|
|
390
410
|
return (client instanceof InvocationLifecycleClient ? client.immutableRead(key, work) : work()) as Promise<T>;
|
|
391
411
|
}
|
|
392
412
|
|
|
393
|
-
/** Successful deployment
|
|
413
|
+
/** Successful deployment observations live for the caller's session, not one block. */
|
|
394
414
|
export function lifecycleSessionRead<T>(client: LifecycleRpcClient, key: string, work: () => Promise<T>): Promise<T> {
|
|
395
415
|
const source = lifecycleSourceClient(client);
|
|
396
416
|
const owner = source.sessionCache ?? source;
|
|
@@ -472,18 +492,10 @@ export async function readLifecycleBlock(client: LifecycleRpcClient, tag: Hex |
|
|
|
472
492
|
if (client instanceof InvocationLifecycleClient) client.observeBlock(block, tag === "latest");
|
|
473
493
|
return block;
|
|
474
494
|
}
|
|
475
|
-
export async function assertLifecycleBlock(client: LifecycleRpcClient, block: Pick<LifecycleBlock, "number" | "hash"
|
|
495
|
+
export async function assertLifecycleBlock(client: LifecycleRpcClient, block: Pick<LifecycleBlock, "number" | "hash">): Promise<void> {
|
|
476
496
|
if (client instanceof InvocationLifecycleClient) return;
|
|
477
|
-
const
|
|
478
|
-
expectedChainId === undefined ? undefined : lifecycleRpc(client, "eth_chainId"),
|
|
479
|
-
readLifecycleBlock(client, toHex(block.number)),
|
|
480
|
-
]);
|
|
481
|
-
if (expectedChainId !== undefined && rpcQuantity(chainId, "chain ID") !== expectedChainId) {
|
|
482
|
-
if (client instanceof InvocationLifecycleClient) client.invalidateSession();
|
|
483
|
-
throw new LifecyclePlanningError("CHAIN_MISMATCH", "Source RPC chain changed during the pinned operation");
|
|
484
|
-
}
|
|
497
|
+
const current = await readLifecycleBlock(client, toHex(block.number));
|
|
485
498
|
if (current.hash.toLowerCase() !== block.hash.toLowerCase()) {
|
|
486
|
-
if (client instanceof InvocationLifecycleClient) client.invalidateSession();
|
|
487
499
|
throw new LifecyclePlanningError("STATE_REORGED", "Pinned launch state changed during the operation; read canonical progress again");
|
|
488
500
|
}
|
|
489
501
|
}
|
|
@@ -512,31 +524,32 @@ export async function readLifecycleProfileTopology(client: LifecycleRpcClient, r
|
|
|
512
524
|
if (topology.hookTopology !== 0 && topology.hookTopology !== 1 && topology.hookTopology !== 2) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Registry returned an unsupported hook topology");
|
|
513
525
|
return { ...topology, hookTopology: topology.hookTopology };
|
|
514
526
|
}
|
|
515
|
-
export const nitroArbSys = "0x0000000000000000000000000000000000000064";
|
|
516
527
|
export const nitroGasInfo = "0x000000000000000000000000000000000000006c";
|
|
517
|
-
export const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
528
|
+
export const nitroGasInfoAbi = parseAbi([
|
|
529
|
+
// Transaction ceilings are release preset metadata, not guard-only getter reads.
|
|
530
|
+
"function getPricesInWei() view returns (uint256,uint256,uint256,uint256,uint256,uint256)",
|
|
531
|
+
]);
|
|
532
|
+
export type LifecycleNitroPrices = readonly [bigint, bigint, bigint, bigint, bigint, bigint];
|
|
521
533
|
const maxUint64 = (1n << 64n) - 1n;
|
|
522
534
|
|
|
523
|
-
/**
|
|
524
|
-
export
|
|
525
|
-
|
|
526
|
-
const result = rpcHex(await lifecycleStaticRpc(context.client, "eth_call", [{ from: transaction.from, to: nitroNodeInterface, data, value: toHex(transaction.value) }, toHex(context.block.number)], context.block), "Nitro poster estimate");
|
|
527
|
-
if (result.length !== 194) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Nitro poster estimate must contain exactly three ABI words");
|
|
528
|
-
const [posterGas, baseFee, l1BaseFeeEstimate] = decodeFunctionResult({ abi: nitroNodeInterfaceAbi, functionName: "gasEstimateL1Component", data: result });
|
|
529
|
-
if (posterGas < 0n || posterGas > maxUint64 || baseFee <= 0n || l1BaseFeeEstimate < 0n) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", "Nitro poster estimate has invalid gas or fee values");
|
|
530
|
-
return { posterGas, baseFee, l1BaseFeeEstimate };
|
|
535
|
+
/** Seeded by the explicitly pinned planning aggregate. */
|
|
536
|
+
export function readNitroPrices(client: LifecycleRpcClient, block: LifecycleBlock): Promise<LifecycleNitroPrices> {
|
|
537
|
+
return readLifecycleContract(client, nitroGasInfo, nitroGasInfoAbi, "getPricesInWei", [], block, undefined, true, true);
|
|
531
538
|
}
|
|
532
539
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
540
|
+
/** EOA legacy/1559 serialization and Nitro's Brotli allocation bound, not a
|
|
541
|
+
* per-transaction oracle quote. Wallet-added access/auth lists and AA wrappers
|
|
542
|
+
* need their own wallet envelope; they are outside the direct-EOA profile. */
|
|
543
|
+
export function nitroPosterGasBound(transaction: Pick<LifecycleTransaction, "data">, prices: LifecycleNitroPrices): { posterGas: bigint; feeBound: bigint } {
|
|
544
|
+
const serializedBytes = BigInt((transaction.data.length - 2) / 2) + 384n;
|
|
545
|
+
const compressedBytes = serializedBytes + 8n * (serializedBytes / 1024n) + 64n;
|
|
546
|
+
const feeBound = compressedBytes * prices[1];
|
|
547
|
+
if (feeBound === 0n) return { posterGas: 0n, feeBound };
|
|
548
|
+
if (prices[5] === 0n) throw new LifecyclePlanningError("NITRO_PRICES_UNAVAILABLE", "Positive poster fees cannot be converted at a zero L2 gas price");
|
|
549
|
+
return { posterGas: 2n * ((feeBound + prices[5] - 1n) / prices[5]), feeBound };
|
|
538
550
|
}
|
|
539
551
|
|
|
552
|
+
|
|
540
553
|
export type LifecycleLimitObservation = {
|
|
541
554
|
limits: ResolvedLifecycleLimits;
|
|
542
555
|
configuration: LifecycleLimits;
|
|
@@ -570,7 +583,7 @@ async function resolveLifecycleLimitsFresh(context: LifecycleLimitContext, sourc
|
|
|
570
583
|
};
|
|
571
584
|
const headroomBps = configuration.headroomBps === undefined ? 1500 : configuration.headroomBps;
|
|
572
585
|
if (!Number.isSafeInteger(headroomBps) || headroomBps < 0 || headroomBps > 10000) throw new LifecyclePlanningError("INVALID_LIMITS", "Headroom must be between zero and 10000 basis points");
|
|
573
|
-
for (const limit of [context.block.gasLimit, configuration.chainGasLimit, configuration.rpcGasLimit, configuration.accountGasLimit
|
|
586
|
+
for (const limit of [context.block.gasLimit, configuration.chainGasLimit, configuration.rpcGasLimit, configuration.accountGasLimit]) {
|
|
574
587
|
if (limit !== undefined && (typeof limit !== "bigint" || limit <= 0n || limit > maxUint64)) throw new LifecyclePlanningError("INVALID_LIMITS", "Gas caps must be positive exact uint64 bigint values");
|
|
575
588
|
}
|
|
576
589
|
if (configuration.maxCalldataBytes !== undefined && (!Number.isSafeInteger(configuration.maxCalldataBytes) || configuration.maxCalldataBytes <= 0)) throw new LifecyclePlanningError("INVALID_LIMITS", "Calldata cap must be a positive byte count");
|
|
@@ -583,46 +596,23 @@ async function resolveLifecycleLimitsFresh(context: LifecycleLimitContext, sourc
|
|
|
583
596
|
throw new LifecyclePlanningError("LIMIT_CONTEXT_MISMATCH", "Supplied execution policy provenance differs from the canonical chain/account/core/block context");
|
|
584
597
|
}
|
|
585
598
|
const protocol = context.chainId === 4663n ? "nitro" : "evm";
|
|
586
|
-
|
|
587
|
-
let arbOSVersion: bigint | undefined; let maxTxComputeGas: bigint | undefined; let maxBlockComputeGas: bigint | undefined;
|
|
588
|
-
if (protocol === "nitro") {
|
|
589
|
-
try {
|
|
590
|
-
const [rawVersion, txComputeGas, blockComputeGas] = await Promise.all([
|
|
591
|
-
readNitroScalar(context, nitroArbSys, nitroArbSysAbi, "arbOSVersion"),
|
|
592
|
-
readNitroScalar(context, nitroGasInfo, nitroGasInfoAbi, "getMaxTxGasLimit"),
|
|
593
|
-
readNitroScalar(context, nitroGasInfo, nitroGasInfoAbi, "getMaxBlockGasLimit"),
|
|
594
|
-
]);
|
|
595
|
-
// ArbSys alone adds 55 to the logical Nitro ArbOS version.
|
|
596
|
-
if (typeof rawVersion !== "bigint" || rawVersion < 105n) throw new Error("ArbOS version 50 or newer is required (ArbSys raw version >= 105)");
|
|
597
|
-
arbOSVersion = rawVersion - 55n;
|
|
598
|
-
maxTxComputeGas = txComputeGas;
|
|
599
|
-
maxBlockComputeGas = blockComputeGas;
|
|
600
|
-
if ([maxTxComputeGas, maxBlockComputeGas].some((limit) => typeof limit !== "bigint" || limit <= 0n || limit > maxUint64)) throw new Error("Nitro compute getters must be positive uint64 values");
|
|
601
|
-
} catch (failure) {
|
|
602
|
-
if (failure instanceof Error && failure.name === "AbortError") throw failure;
|
|
603
|
-
throw new LifecyclePlanningError("NITRO_LIMITS_UNAVAILABLE", `Pinned Nitro compute limits could not be established: ${lifecycleFailureReason(failure)}; header gasLimit is not a fallback`);
|
|
604
|
-
}
|
|
605
|
-
if (configuration.estimateDataFee !== undefined) throw new LifecyclePlanningError("INVALID_LIMITS", "Nitro poster fees are already included in gas; an external data-fee estimator would double-charge them");
|
|
606
|
-
}
|
|
599
|
+
if (protocol === "nitro" && configuration.estimateDataFee !== undefined) throw new LifecyclePlanningError("INVALID_LIMITS", "Nitro poster fees are already included in gas; an external data-fee estimator would double-charge them");
|
|
607
600
|
const envelopeCaps = [configuration.chainGasLimit, configuration.rpcGasLimit, configuration.accountGasLimit].filter((limit): limit is bigint => limit !== undefined);
|
|
608
601
|
if (protocol === "evm") envelopeCaps.push(context.block.gasLimit);
|
|
609
602
|
const transactionGasCeiling = envelopeCaps.length === 0 ? undefined : envelopeCaps.reduce((minimum, limit) => limit < minimum ? limit : minimum);
|
|
610
|
-
const executionGasCeiling =
|
|
611
|
-
? (maxTxComputeGas < maxBlockComputeGas ? maxTxComputeGas : maxBlockComputeGas)
|
|
612
|
-
: transactionGasCeiling ?? context.block.gasLimit;
|
|
603
|
+
const executionGasCeiling = protocol === "nitro" ? ROBINHOOD_NATIVE_TRANSACTION_GAS_LIMIT : transactionGasCeiling ?? context.block.gasLimit;
|
|
613
604
|
const unknownExecutionConstraints = [
|
|
614
|
-
...(protocol === "evm" && configuration.chainGasLimit === undefined ? ["Independent chain per-transaction
|
|
605
|
+
...(protocol === "evm" && configuration.chainGasLimit === undefined ? ["Independent chain per-transaction cap is unknown; the budget is bounded by the pinned header"] : []),
|
|
615
606
|
...(configuration.rpcGasLimit === undefined ? ["RPC transaction gas-envelope cap is unknown"] : []),
|
|
616
607
|
...(configuration.accountGasLimit === undefined ? ["Current account/wallet transaction gas-envelope cap is unknown"] : []),
|
|
617
608
|
...(configuration.maxCalldataBytes === undefined ? ["Applicable chain/account/RPC calldata byte cap is unknown"] : []),
|
|
618
|
-
...(maxSimulationGas === undefined ? ["RPC aggregate simulation gas cap is unknown"] : []),
|
|
619
609
|
];
|
|
620
610
|
assertCurrent();
|
|
621
611
|
return {
|
|
622
612
|
configuration, assertCurrent,
|
|
623
613
|
limits: Object.freeze({
|
|
624
|
-
protocol, executionGasCeiling, transactionGasCeiling,
|
|
625
|
-
maxCalldataBytes: configuration.maxCalldataBytes,
|
|
614
|
+
protocol, executionGasCeiling, transactionGasCeiling, headroomBps,
|
|
615
|
+
maxCalldataBytes: configuration.maxCalldataBytes,
|
|
626
616
|
blockGasLimit: context.block.gasLimit, chainGasLimit: configuration.chainGasLimit,
|
|
627
617
|
rpcGasLimit: configuration.rpcGasLimit, accountGasLimit: configuration.accountGasLimit,
|
|
628
618
|
unknownExecutionConstraints: Object.freeze(unknownExecutionConstraints),
|
|
@@ -5,7 +5,7 @@ import { getKnownLifecycleDeployment, type LifecycleDeploymentPreset } from "./p
|
|
|
5
5
|
import { lifecycleTokenPredictionAbi } from "./progress.js";
|
|
6
6
|
import { launchLensAbi, observeLifecycleLens, type LifecycleLensSnapshot } from "./lens.js";
|
|
7
7
|
import { hashLaunchIdentity, LifecycleFundingKind, type LaunchPlanV1 } from "./schema.js";
|
|
8
|
-
import { attestLifecycleMulticall, lifecycleRpc,
|
|
8
|
+
import { attestLifecycleMulticall, lifecycleRpc, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readLifecycleChainId, rpcHex, seedLifecycleRead } from "./rpc.js";
|
|
9
9
|
import { LifecyclePlanningError, type LifecycleBlock, type LifecycleRpcClient } from "./types.js";
|
|
10
10
|
import { UFG_LAUNCH_PROXY_ADDRESS } from "./ufg-config.js";
|
|
11
11
|
|
|
@@ -36,9 +36,7 @@ export async function readLifecyclePlanningBlock(client: LifecycleRpcClient, pla
|
|
|
36
36
|
if (escrow !== undefined) add(asset, lifecycleErc20Abi, "allowance", [account, escrow]);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
add(
|
|
40
|
-
add(nitroGasInfo, nitroGasInfoAbi, "getMaxTxGasLimit", []);
|
|
41
|
-
add(nitroGasInfo, nitroGasInfoAbi, "getMaxBlockGasLimit", []);
|
|
39
|
+
add(nitroGasInfo, nitroGasInfoAbi, "getPricesInWei", []);
|
|
42
40
|
await attestLifecycleMulticall(client, block);
|
|
43
41
|
const calls = reads.map((read) => ({ target: read.to, allowFailure: true, callData: encodeFunctionData({ abi: read.abi, functionName: read.functionName, args: read.args }) }));
|
|
44
42
|
const data = encodeFunctionData({ abi: multicall3Abi, functionName: "aggregate3", args: [calls] });
|
|
@@ -1,28 +1,59 @@
|
|
|
1
|
-
import { encodeFunctionData } from "viem";
|
|
1
|
+
import { encodeFunctionData, type Address } from "viem";
|
|
2
2
|
import { launchLifecycleAbi } from "./abi.js";
|
|
3
3
|
import { LifecycleFundingKind, LifecycleMode, type LaunchPlanV1 } from "./schema.js";
|
|
4
4
|
import { LifecyclePlanningError, type BuildLaunchTransactionsOptions, type LifecycleTransaction } from "./types.js";
|
|
5
5
|
import { assertLifecycleTokenAllocation } from "./allocations.js";
|
|
6
|
-
import {
|
|
6
|
+
import { ufgLaunchProxyV1Abi } from "./ufg-abi.js";
|
|
7
|
+
import { UFG_LAUNCH_PROXY_ADDRESS, isUfgProfile } from "./ufg-config.js";
|
|
8
|
+
import { decodeAbyssLifecycleMarketConfig, decodePoolBoundV4LifecycleMarketConfig, decodeV4LifecycleMarketConfig, isPoolBoundV4ConfigVersion } from "./markets.js";
|
|
7
9
|
import { isHmmProfile } from "./hmm.js";
|
|
10
|
+
import type { UfgLaunchConfig } from "./ufg-config.js";
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
+
// Native9,723,187 Abyss3 (0x679d…e77), fork32-position10,900,786;
|
|
13
|
+
// fork V4 fixed/dynamic8-position8,804,466; UFG9,896,176; native HMM8-position14,676,638 (0x459c…5b7).
|
|
14
|
+
const preparationCosts = {
|
|
15
|
+
abyss: { market: 9_800_000n, position: 70_000n },
|
|
16
|
+
v4: { market: 9_000_000n, position: 40_000n },
|
|
17
|
+
ufg: { market: 10_000_000n, position: 70_000n },
|
|
18
|
+
hmm: { market: 15_000_000n, position: 50_000n },
|
|
19
|
+
} as const;
|
|
20
|
+
export const LIFECYCLE_CAPACITY_PADDING_BPS = 2500;
|
|
21
|
+
/** Full single-market observations include call overhead. Add conservative full
|
|
22
|
+
* calldata/hash processing (16 intrinsic gas per nonzero byte plus8 for copying). */
|
|
23
|
+
export function preparationCalldataGas(data: `0x${string}`): bigint { return 24n * BigInt((data.length - 2) / 2); }
|
|
12
24
|
|
|
13
25
|
export function preparationGasEstimate(plan: LaunchPlanV1, first: number, count: number): bigint {
|
|
14
26
|
let gas = 0n;
|
|
15
|
-
for (let index = first; index < first + count; index += 1)
|
|
27
|
+
for (let index = first; index < first + count; index += 1) {
|
|
28
|
+
const market = plan.markets[index]!;
|
|
29
|
+
const positions = market.configVersion === 1 ? decodeAbyssLifecycleMarketConfig(market.config).positions.length
|
|
30
|
+
: isPoolBoundV4ConfigVersion(market.configVersion) ? decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion).positions.length
|
|
31
|
+
: decodeV4LifecycleMarketConfig(market.config).positions.length;
|
|
32
|
+
const cost = market.configVersion === 1 ? preparationCosts.abyss : isHmmProfile(market.profileId) ? preparationCosts.hmm
|
|
33
|
+
: isUfgProfile(market.profileId) ? preparationCosts.ufg : preparationCosts.v4;
|
|
34
|
+
gas += cost.market + cost.position * BigInt(positions - 1);
|
|
35
|
+
}
|
|
16
36
|
return gas;
|
|
17
37
|
}
|
|
18
38
|
|
|
19
|
-
/** Contiguous
|
|
20
|
-
|
|
39
|
+
/** Contiguous deterministic packing with compute headroom. The caller reserves
|
|
40
|
+
* poster allocation separately from computeCap; neither resizes transaction gas. */
|
|
41
|
+
export function packPreparationBatches(plan: LaunchPlanV1, prepared: number, computeCap: bigint, headroomBps = 1500): number[] {
|
|
42
|
+
const data = buildLifecycleCommand(plan, { kind: "prepare", first: prepared, count: 1 }).data;
|
|
43
|
+
return packLifecyclePreparationBatches(plan, prepared, computeCap, headroomBps, preparationCalldataGas(data));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Internal path reuses the command already encoded for poster allocation. */
|
|
47
|
+
export function packLifecyclePreparationBatches(plan: LaunchPlanV1, prepared: number, computeCap: bigint, headroomBps: number, calldataGas: bigint): number[] {
|
|
48
|
+
if (!Number.isSafeInteger(headroomBps) || headroomBps < 0 || headroomBps > 10000) throw new LifecyclePlanningError("INVALID_LIMITS", "Headroom must be between zero and10000 basis points");
|
|
49
|
+
const padding = BigInt(10000 + Math.max(LIFECYCLE_CAPACITY_PADDING_BPS, headroomBps));
|
|
50
|
+
const fits = (gas: bigint) => ((gas + calldataGas) * padding + 9999n) / 10000n <= computeCap;
|
|
21
51
|
const batches = prepared > 0 ? [prepared] : [];
|
|
22
52
|
let count = 0, gas = 0n;
|
|
23
53
|
for (let index = prepared; index < plan.markets.length; index += 1) {
|
|
24
54
|
const estimate = preparationGasEstimate(plan, index, 1);
|
|
25
|
-
if (
|
|
55
|
+
if (!fits(estimate)) throw new LifecyclePlanningError("PREPARATION_GAS_LIMIT", "A market exceeds the preparation gas budget. Reduce its positions before launching.");
|
|
56
|
+
if (count > 0 && !fits(gas + estimate)) { batches.push(count); count = 0; gas = 0n; }
|
|
26
57
|
count += 1;
|
|
27
58
|
gas += estimate;
|
|
28
59
|
}
|
|
@@ -30,42 +61,53 @@ export function packPreparationBatches(plan: LaunchPlanV1, prepared: number, com
|
|
|
30
61
|
return batches;
|
|
31
62
|
}
|
|
32
63
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
64
|
+
export type LifecycleCommand =
|
|
65
|
+
| { kind: "atomic" | "begin" | "activate" }
|
|
66
|
+
| { kind: "prepare"; first: number; count: number };
|
|
67
|
+
type LifecycleUfgRoute = { account: Address; config: UfgLaunchConfig };
|
|
68
|
+
|
|
69
|
+
/** Internal encoder shared by complete plans and the one-command continuation. */
|
|
70
|
+
export function buildLifecycleCommand(plan: LaunchPlanV1, command: LifecycleCommand, ufg?: LifecycleUfgRoute): LifecycleTransaction {
|
|
71
|
+
const abi = ufg === undefined ? launchLifecycleAbi : ufgLaunchProxyV1Abi;
|
|
72
|
+
const base = { chainId: Number(plan.chainId), from: ufg?.account ?? plan.creator, to: ufg === undefined ? plan.orchestrator : UFG_LAUNCH_PROXY_ADDRESS };
|
|
73
|
+
const value = command.kind === "atomic" || command.kind === "begin"
|
|
74
|
+
? plan.funding.reduce((total, funding) => total + (funding.kind === LifecycleFundingKind.NativeWrap || funding.kind === LifecycleFundingKind.Swap && BigInt(funding.inputAsset) === 0n ? funding.inputAmount : 0n), 0n) : 0n;
|
|
75
|
+
switch (command.kind) {
|
|
76
|
+
case "atomic": return { ...base, id: "atomic", kind: "atomic", data: encodeFunctionData({ abi, functionName: "launchAtomic", args: ufg === undefined ? [plan] : [plan, ufg.config] }), value, dependencies: [], postconditions: [{ kind: "launch", phase: "Active", preparedMarkets: plan.markets.length }] };
|
|
77
|
+
case "begin": return { ...base, id: "begin", kind: "begin", data: encodeFunctionData({ abi, functionName: "beginLaunch", args: ufg === undefined ? [plan, LifecycleMode.Staged] : [plan, LifecycleMode.Staged, ufg.config] }), value, dependencies: [], postconditions: [{ kind: "launch", phase: "Preparing", preparedMarkets: 0 }] };
|
|
78
|
+
case "activate": return { ...base, id: "activate", kind: "activate", data: encodeFunctionData({ abi, functionName: "activateLaunch", args: [plan] }), value, dependencies: [], postconditions: [{ kind: "launch", phase: "Active", preparedMarkets: plan.markets.length }] };
|
|
79
|
+
case "prepare": return { ...base, id: `prepare:${command.first}:${command.count}`, kind: "prepare", data: encodeFunctionData({ abi, functionName: "prepareMarkets", args: [plan, command.first, command.count] }), value, dependencies: [], marketStart: command.first, marketCount: command.count,
|
|
80
|
+
postconditions: [{ kind: "launch", phase: command.first + command.count === plan.markets.length ? "Ready" : "Preparing", preparedMarkets: command.first + command.count }] };
|
|
41
81
|
}
|
|
42
82
|
}
|
|
43
83
|
|
|
44
84
|
/** Pure unsigned launch commands. Every command carries the complete commitment;
|
|
45
85
|
* preparation splits only empty markets, never positions, buys or public opening.
|
|
46
|
-
* Allowance discovery, recovery and
|
|
47
|
-
export function
|
|
86
|
+
* Allowance discovery, recovery and gas-budget checks belong to the planner. */
|
|
87
|
+
export function buildLifecycleCommands({ plan, mode, preparationBatches }: BuildLaunchTransactionsOptions, ufg?: LifecycleUfgRoute): LifecycleTransaction[] {
|
|
48
88
|
if (mode !== "atomic" && mode !== "staged") throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose atomic or staged explicitly; staged is never silently substituted");
|
|
49
89
|
if (plan.chainId <= 0n || plan.chainId > BigInt(Number.MAX_SAFE_INTEGER)) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Chain ID must be a positive exact JavaScript integer");
|
|
50
90
|
if (plan.markets.length === 0 || plan.markets.length > 16) throw new LifecyclePlanningError("INVALID_MARKET_COUNT", "A launch requires between one and sixteen markets");
|
|
51
91
|
if (mode === "atomic" && preparationBatches !== undefined) throw new LifecyclePlanningError("INVALID_PREPARATION_BATCHES", "Atomic execution has no separate preparation batches");
|
|
52
92
|
assertLifecycleTokenAllocation(plan);
|
|
53
|
-
const nativeValue = plan.funding.reduce((total, funding) => total + (funding.kind === LifecycleFundingKind.NativeWrap || funding.kind === LifecycleFundingKind.Swap && BigInt(funding.inputAsset) === 0n ? funding.inputAmount : 0n), 0n);
|
|
54
|
-
const base = { chainId: Number(plan.chainId), from: plan.creator, to: plan.orchestrator };
|
|
55
93
|
const transactions: LifecycleTransaction[] = [];
|
|
56
94
|
if (mode === "atomic") {
|
|
57
|
-
transactions.push(
|
|
95
|
+
transactions.push(buildLifecycleCommand(plan, { kind: "atomic" }, ufg));
|
|
58
96
|
} else {
|
|
59
97
|
const batches = preparationBatches ?? [plan.markets.length];
|
|
60
98
|
if (batches.length === 0 || batches.some((count) => !Number.isSafeInteger(count) || count <= 0) || batches.reduce((total, count) => total + count, 0) !== plan.markets.length) throw new LifecyclePlanningError("INVALID_PREPARATION_BATCHES", "Positive ordered preparation counts must cover every market exactly");
|
|
61
|
-
transactions.push(
|
|
99
|
+
transactions.push(buildLifecycleCommand(plan, { kind: "begin" }, ufg));
|
|
62
100
|
let first = 0;
|
|
63
101
|
for (const count of batches) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
first = completed;
|
|
102
|
+
transactions.push(buildLifecycleCommand(plan, { kind: "prepare", first, count }, ufg));
|
|
103
|
+
first += count;
|
|
67
104
|
}
|
|
68
|
-
transactions.push(
|
|
105
|
+
transactions.push(buildLifecycleCommand(plan, { kind: "activate" }, ufg));
|
|
69
106
|
}
|
|
70
107
|
return transactions.map((transaction, index) => ({ ...transaction, calldataBytes: (transaction.data.length - 2) / 2, dependencies: index === 0 ? [] : [transactions[index - 1]!.id] }));
|
|
71
108
|
}
|
|
109
|
+
|
|
110
|
+
/** Pure unsigned core commands, with no account nonce assigned by the SDK. */
|
|
111
|
+
export function buildLaunchTransactions(options: BuildLaunchTransactionsOptions): LifecycleTransaction[] {
|
|
112
|
+
return buildLifecycleCommands(options);
|
|
113
|
+
}
|