@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.
Files changed (103) hide show
  1. package/README.md +9 -7
  2. package/dist/lifecycle/authors.js +2 -2
  3. package/dist/lifecycle/authors.js.map +1 -1
  4. package/dist/lifecycle/auto-compound.d.ts +21 -0
  5. package/dist/lifecycle/auto-compound.d.ts.map +1 -0
  6. package/dist/lifecycle/auto-compound.js +34 -0
  7. package/dist/lifecycle/auto-compound.js.map +1 -0
  8. package/dist/lifecycle/calibration.d.ts +2 -4
  9. package/dist/lifecycle/calibration.d.ts.map +1 -1
  10. package/dist/lifecycle/calibration.js +1 -26
  11. package/dist/lifecycle/calibration.js.map +1 -1
  12. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.d.ts +73 -0
  13. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.d.ts.map +1 -0
  14. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.js +75 -0
  15. package/dist/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.js.map +1 -0
  16. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.d.ts +73 -0
  17. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.d.ts.map +1 -0
  18. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.js +75 -0
  19. package/dist/lifecycle/deployments/auto-compound-static-pool-v1-20261028.js.map +1 -0
  20. package/dist/lifecycle/gas-profile.d.ts +39 -0
  21. package/dist/lifecycle/gas-profile.d.ts.map +1 -0
  22. package/dist/lifecycle/gas-profile.js +193 -0
  23. package/dist/lifecycle/gas-profile.js.map +1 -0
  24. package/dist/lifecycle/index.d.ts +6 -4
  25. package/dist/lifecycle/index.d.ts.map +1 -1
  26. package/dist/lifecycle/index.js +6 -4
  27. package/dist/lifecycle/index.js.map +1 -1
  28. package/dist/lifecycle/markets.d.ts +2 -0
  29. package/dist/lifecycle/markets.d.ts.map +1 -1
  30. package/dist/lifecycle/markets.js +30 -3
  31. package/dist/lifecycle/markets.js.map +1 -1
  32. package/dist/lifecycle/opening-buys.d.ts +13 -0
  33. package/dist/lifecycle/opening-buys.d.ts.map +1 -0
  34. package/dist/lifecycle/opening-buys.js +171 -0
  35. package/dist/lifecycle/opening-buys.js.map +1 -0
  36. package/dist/lifecycle/planner.d.ts +6 -9
  37. package/dist/lifecycle/planner.d.ts.map +1 -1
  38. package/dist/lifecycle/planner.js +175 -265
  39. package/dist/lifecycle/planner.js.map +1 -1
  40. package/dist/lifecycle/presets.d.ts +10 -3
  41. package/dist/lifecycle/presets.d.ts.map +1 -1
  42. package/dist/lifecycle/presets.js +13 -1
  43. package/dist/lifecycle/presets.js.map +1 -1
  44. package/dist/lifecycle/progress.d.ts +1 -1
  45. package/dist/lifecycle/progress.d.ts.map +1 -1
  46. package/dist/lifecycle/progress.js +21 -20
  47. package/dist/lifecycle/progress.js.map +1 -1
  48. package/dist/lifecycle/rpc.d.ts +25 -30
  49. package/dist/lifecycle/rpc.d.ts.map +1 -1
  50. package/dist/lifecycle/rpc.js +63 -81
  51. package/dist/lifecycle/rpc.js.map +1 -1
  52. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  53. package/dist/lifecycle/snapshot.js +2 -4
  54. package/dist/lifecycle/snapshot.js.map +1 -1
  55. package/dist/lifecycle/transactions.d.ts +29 -6
  56. package/dist/lifecycle/transactions.d.ts.map +1 -1
  57. package/dist/lifecycle/transactions.js +62 -27
  58. package/dist/lifecycle/transactions.js.map +1 -1
  59. package/dist/lifecycle/types.d.ts +55 -122
  60. package/dist/lifecycle/types.d.ts.map +1 -1
  61. package/dist/lifecycle/types.js +3 -3
  62. package/dist/lifecycle/types.js.map +1 -1
  63. package/dist/lifecycle/ufg.d.ts +1 -1
  64. package/dist/lifecycle/ufg.d.ts.map +1 -1
  65. package/dist/lifecycle/ufg.js +4 -8
  66. package/dist/lifecycle/ufg.js.map +1 -1
  67. package/docs/launch-lifecycle.md +110 -78
  68. package/docs/lifecycle-gas-profiles.md +90 -0
  69. package/docs/reference.md +99 -47
  70. package/examples/.env.example +0 -3
  71. package/examples/README.md +14 -11
  72. package/examples/launch-example-support.ts +4 -9
  73. package/examples/launch-example.ts +30 -36
  74. package/examples/launch-lifecycle-smoke.ts +11 -21
  75. package/examples/launch.ts +8 -11
  76. package/package.json +1 -1
  77. package/src/lifecycle/authors.ts +2 -2
  78. package/src/lifecycle/auto-compound.ts +38 -0
  79. package/src/lifecycle/calibration.ts +3 -23
  80. package/src/lifecycle/deployments/auto-compound-dynamic-pool-v1-20261028.ts +77 -0
  81. package/src/lifecycle/deployments/auto-compound-static-pool-v1-20261028.ts +77 -0
  82. package/src/lifecycle/gas-profile.ts +186 -0
  83. package/src/lifecycle/index.ts +6 -4
  84. package/src/lifecycle/markets.ts +25 -3
  85. package/src/lifecycle/opening-buys.ts +157 -0
  86. package/src/lifecycle/planner.ts +168 -247
  87. package/src/lifecycle/presets.ts +15 -1
  88. package/src/lifecycle/progress.ts +19 -20
  89. package/src/lifecycle/rpc.ts +58 -68
  90. package/src/lifecycle/snapshot.ts +2 -4
  91. package/src/lifecycle/transactions.ts +69 -27
  92. package/src/lifecycle/types.ts +60 -99
  93. package/src/lifecycle/ufg.ts +4 -8
  94. package/dist/lifecycle/fork.d.ts +0 -15
  95. package/dist/lifecycle/fork.d.ts.map +0 -1
  96. package/dist/lifecycle/fork.js +0 -36
  97. package/dist/lifecycle/fork.js.map +0 -1
  98. package/dist/lifecycle/simulation.d.ts +0 -18
  99. package/dist/lifecycle/simulation.d.ts.map +0 -1
  100. package/dist/lifecycle/simulation.js +0 -754
  101. package/dist/lifecycle/simulation.js.map +0 -1
  102. package/src/lifecycle/fork.ts +0 -36
  103. package/src/lifecycle/simulation.ts +0 -689
@@ -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 variants, fixed/dynamic V4, HMM, then UFG fixed/dynamic. */
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 Hash, type Hex } from "viem";
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 = { transactionHash: reference.transactionHash, effectiveHash, replaced: reference.replacementHash !== undefined && reference.replacementHash.toLowerCase() !== reference.transactionHash.toLowerCase() };
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(rpcObject(transactionValue, "receipt transaction"), options,
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, receipts and nonce, not display-only live markets. */
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, confirmedNonce, headNonce, pendingNonce] = await Promise.all([
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 = confirmedAccountNonce === headAccountNonce && pendingAccountNonce === headAccountNonce && keccak256(encodeAbiParameters(progressParameters, [canonical])) === keccak256(encodeAbiParameters(progressParameters, [head]));
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, confirmedAccountNonce, headAccountNonce, pendingAccountNonce, confirmationSafe, token, planHash: planned.planHash, launchId: planned.launchId, chainId, blockNumber: block.number, blockHash: block.hash, markets, receipts };
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
  }
@@ -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, eth_estimateGas: true, eth_gasPrice: true,
17
- eth_simulateV1: true, eth_getTransactionCount: true, eth_getTransactionReceipt: true, eth_getTransactionByHash: true,
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 in per caller/session; there is no global or transport-owned lifecycle cache. */
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
- return this.chainId ??= this.run("eth_chainId", []).then((value) => rpcQuantity(value, "chain ID"));
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
- /** One invocation's observed identity; wallet submission owns its final chain check. */
395
+ /** Client-bound identity, observed once per underlying client, never a freshness guard. */
377
396
  export function readLifecycleChainId(client: LifecycleRpcClient): Promise<bigint> {
378
- return client instanceof InvocationLifecycleClient
379
- ? client.observeChainId()
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<unknown> {
385
- return client instanceof InvocationLifecycleClient ? client.observeGasPrice() : lifecycleRpc(client, "eth_gasPrice");
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/backend observations live for the caller's session, not one block. */
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">, expectedChainId?: bigint): Promise<void> {
495
+ export async function assertLifecycleBlock(client: LifecycleRpcClient, block: Pick<LifecycleBlock, "number" | "hash">): Promise<void> {
476
496
  if (client instanceof InvocationLifecycleClient) return;
477
- const [chainId, current] = await Promise.all([
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 nitroNodeInterface = "0x00000000000000000000000000000000000000c8";
518
- export const nitroArbSysAbi = parseAbi(["function arbOSVersion() view returns (uint256)"]);
519
- export const nitroGasInfoAbi = parseAbi(["function getMaxTxGasLimit() view returns (uint256)", "function getMaxBlockGasLimit() view returns (uint64)"]);
520
- const nitroNodeInterfaceAbi = parseAbi(["function gasEstimateL1Component(address to, bool contractCreation, bytes data) payable returns (uint64 gasEstimateForL1, uint256 baseFee, uint256 l1BaseFeeEstimate)"]);
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
- /** A poster quote is an envelope budget, never an execution/compute proof. */
524
- export async function readNitroPosterGas(transaction: LifecycleTransaction, context: LifecycleLimitContext): Promise<{ posterGas: bigint; baseFee: bigint; l1BaseFeeEstimate: bigint }> {
525
- const data = encodeFunctionData({ abi: nitroNodeInterfaceAbi, functionName: "gasEstimateL1Component", args: [transaction.to, false, transaction.data] });
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
- async function readNitroScalar(context: LifecycleLimitContext, to: Address, abi: Abi, functionName: string): Promise<bigint> {
534
- const data = encodeFunctionData({ abi, functionName });
535
- const result = rpcHex(await lifecycleIndependentRpc(context.client, [{ to, data }, toHex(context.block.number)], context.block, true), functionName);
536
- if (result.length !== 66) throw new LifecyclePlanningError("INVALID_RPC_RESPONSE", `${functionName} must return exactly one ABI word`);
537
- return decodeFunctionResult({ abi, functionName, data: result }) as bigint;
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, configuration.maxSimulationGas]) {
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
- const maxSimulationGas = configuration.maxSimulationGas ?? (context.chainId === 4663n ? 50_000_000n : undefined);
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 = maxTxComputeGas !== undefined && maxBlockComputeGas !== undefined
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 execution cap is unknown; proof is bounded by the pinned header and validated backend"] : []),
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, arbOSVersion, maxTxComputeGas, maxBlockComputeGas, headroomBps,
625
- maxCalldataBytes: configuration.maxCalldataBytes, maxSimulationGas,
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, nitroArbSys, nitroArbSysAbi, nitroGasInfo, nitroGasInfoAbi, readLifecycleBlock, readLifecycleChainId, rpcHex, seedLifecycleRead } from "./rpc.js";
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(nitroArbSys, nitroArbSysAbi, "arbOSVersion", []);
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 { HMM_V2_PREPARE_GAS, hmmV2ForwardingReserve } from "./presets.js";
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
- const OTHER_MARKET_PREPARE_GAS = 16_000_000n;
10
- const BEGIN_GAS = 9_500_000n;
11
- const ACTIVATE_MARKET_GAS = 6_000_000n;
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) gas += isHmmProfile(plan.markets[index]!.profileId) ? HMM_V2_PREPARE_GAS : OTHER_MARKET_PREPARE_GAS;
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 cost packing preserves committed market order and recovery progress. */
20
- export function packPreparationBatches(plan: LaunchPlanV1, prepared: number, computeCap: bigint): number[] {
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 (count > 0 && gas + estimate > computeCap) { batches.push(count); count = 0; gas = 0n; }
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
- /** Estimates select RPC proof coverage only; measurement and exact replay size gas. */
34
- export function transactionGasEstimate(plan: LaunchPlanV1, transaction: LifecycleTransaction): bigint {
35
- switch (transaction.kind) {
36
- case "begin": return BEGIN_GAS;
37
- case "prepare": return preparationGasEstimate(plan, transaction.marketStart!, transaction.marketCount!);
38
- case "activate": return ACTIVATE_MARKET_GAS * BigInt(plan.markets.length) + hmmV2ForwardingReserve(plan);
39
- case "atomic": return BEGIN_GAS + preparationGasEstimate(plan, 0, plan.markets.length) + ACTIVATE_MARKET_GAS * BigInt(plan.markets.length) + hmmV2ForwardingReserve(plan);
40
- default: return 100_000n;
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 execution proof belong to the planner. */
47
- export function buildLaunchTransactions({ plan, mode, preparationBatches }: BuildLaunchTransactionsOptions): LifecycleTransaction[] {
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({ ...base, id: "atomic", kind: "atomic", data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "launchAtomic", args: [plan] }), value: nativeValue, dependencies: [], postconditions: [{ kind: "launch", phase: "Active", preparedMarkets: plan.markets.length }] });
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({ ...base, id: "begin", kind: "begin", data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "beginLaunch", args: [plan, LifecycleMode.Staged] }), value: nativeValue, dependencies: [], postconditions: [{ kind: "launch", phase: "Preparing", preparedMarkets: 0 }] });
99
+ transactions.push(buildLifecycleCommand(plan, { kind: "begin" }, ufg));
62
100
  let first = 0;
63
101
  for (const count of batches) {
64
- const completed = first + count;
65
- transactions.push({ ...base, id: `prepare:${first}:${count}`, kind: "prepare", data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "prepareMarkets", args: [plan, first, count] }), value: 0n, dependencies: [], marketStart: first, marketCount: count, postconditions: [{ kind: "launch", phase: completed === plan.markets.length ? "Ready" : "Preparing", preparedMarkets: completed }] });
66
- first = completed;
102
+ transactions.push(buildLifecycleCommand(plan, { kind: "prepare", first, count }, ufg));
103
+ first += count;
67
104
  }
68
- transactions.push({ ...base, id: "activate", kind: "activate", data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "activateLaunch", args: [plan] }), value: 0n, dependencies: [], postconditions: [{ kind: "launch", phase: "Active", preparedMarkets: plan.markets.length }] });
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
+ }