@black-market/sdk 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +21 -2
  2. package/dist/lifecycle/abi.d.ts +1 -0
  3. package/dist/lifecycle/abi.d.ts.map +1 -1
  4. package/dist/lifecycle/abi.js +1 -0
  5. package/dist/lifecycle/abi.js.map +1 -1
  6. package/dist/lifecycle/calibration.d.ts.map +1 -1
  7. package/dist/lifecycle/calibration.js +2 -0
  8. package/dist/lifecycle/calibration.js.map +1 -1
  9. package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.d.ts +71 -0
  10. package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.d.ts.map +1 -0
  11. package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.js +73 -0
  12. package/dist/lifecycle/deployments/ufg-dynamic-pool-20261021.js.map +1 -0
  13. package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.d.ts +71 -0
  14. package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.d.ts.map +1 -0
  15. package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.js +73 -0
  16. package/dist/lifecycle/deployments/ufg-fixed-pool-20261021.js.map +1 -0
  17. package/dist/lifecycle/index.d.ts +1 -0
  18. package/dist/lifecycle/index.d.ts.map +1 -1
  19. package/dist/lifecycle/index.js +1 -0
  20. package/dist/lifecycle/index.js.map +1 -1
  21. package/dist/lifecycle/markets.d.ts +3 -1
  22. package/dist/lifecycle/markets.d.ts.map +1 -1
  23. package/dist/lifecycle/markets.js +20 -3
  24. package/dist/lifecycle/markets.js.map +1 -1
  25. package/dist/lifecycle/planner.d.ts.map +1 -1
  26. package/dist/lifecycle/planner.js +83 -54
  27. package/dist/lifecycle/planner.js.map +1 -1
  28. package/dist/lifecycle/presets.d.ts +9 -2
  29. package/dist/lifecycle/presets.d.ts.map +1 -1
  30. package/dist/lifecycle/presets.js +26 -5
  31. package/dist/lifecycle/presets.js.map +1 -1
  32. package/dist/lifecycle/progress.d.ts.map +1 -1
  33. package/dist/lifecycle/progress.js +26 -5
  34. package/dist/lifecycle/progress.js.map +1 -1
  35. package/dist/lifecycle/rpc.js +3 -2
  36. package/dist/lifecycle/rpc.js.map +1 -1
  37. package/dist/lifecycle/simulation.d.ts +1 -1
  38. package/dist/lifecycle/simulation.d.ts.map +1 -1
  39. package/dist/lifecycle/simulation.js +119 -175
  40. package/dist/lifecycle/simulation.js.map +1 -1
  41. package/dist/lifecycle/transactions.d.ts +6 -0
  42. package/dist/lifecycle/transactions.d.ts.map +1 -1
  43. package/dist/lifecycle/transactions.js +39 -0
  44. package/dist/lifecycle/transactions.js.map +1 -1
  45. package/dist/lifecycle/types.d.ts +17 -4
  46. package/dist/lifecycle/types.d.ts.map +1 -1
  47. package/dist/lifecycle/types.js.map +1 -1
  48. package/dist/lifecycle/ufg-abi.d.ts +2386 -0
  49. package/dist/lifecycle/ufg-abi.d.ts.map +1 -0
  50. package/dist/lifecycle/ufg-abi.js +171 -0
  51. package/dist/lifecycle/ufg-abi.js.map +1 -0
  52. package/dist/lifecycle/ufg-config.d.ts +38 -0
  53. package/dist/lifecycle/ufg-config.d.ts.map +1 -0
  54. package/dist/lifecycle/ufg-config.js +75 -0
  55. package/dist/lifecycle/ufg-config.js.map +1 -0
  56. package/dist/lifecycle/ufg.d.ts +21 -0
  57. package/dist/lifecycle/ufg.d.ts.map +1 -0
  58. package/dist/lifecycle/ufg.js +98 -0
  59. package/dist/lifecycle/ufg.js.map +1 -0
  60. package/docs/launch-lifecycle.md +215 -32
  61. package/examples/.env.example +4 -0
  62. package/examples/README.md +35 -2
  63. package/examples/launch-example-support.ts +1 -0
  64. package/examples/launch-example.ts +1 -0
  65. package/examples/launch.ts +1 -0
  66. package/examples/ufg-launch-config.ts +31 -0
  67. package/package.json +1 -1
  68. package/src/lifecycle/abi.ts +1 -0
  69. package/src/lifecycle/calibration.ts +1 -0
  70. package/src/lifecycle/deployments/ufg-dynamic-pool-20261021.ts +74 -0
  71. package/src/lifecycle/deployments/ufg-fixed-pool-20261021.ts +74 -0
  72. package/src/lifecycle/index.ts +1 -0
  73. package/src/lifecycle/markets.ts +16 -3
  74. package/src/lifecycle/planner.ts +75 -51
  75. package/src/lifecycle/presets.ts +27 -6
  76. package/src/lifecycle/progress.ts +22 -5
  77. package/src/lifecycle/rpc.ts +3 -2
  78. package/src/lifecycle/simulation.ts +114 -166
  79. package/src/lifecycle/transactions.ts +38 -1
  80. package/src/lifecycle/types.ts +15 -5
  81. package/src/lifecycle/ufg-abi.ts +173 -0
  82. package/src/lifecycle/ufg-config.ts +77 -0
  83. package/src/lifecycle/ufg.ts +86 -0
@@ -43,6 +43,7 @@ export function validateBuySlippageBps(bps: number): void {
43
43
  export function protectOrderedBuyMinimums(planned: PlannedLaunch, bps: number): LaunchPlanV1 {
44
44
  validateBuySlippageBps(bps);
45
45
  const { simulation, plan } = planned;
46
+ if (simulation.deferredTransactionIds.length !== 0) throw new LifecyclePlanningError("LAUNCH_DIAGNOSTIC_FAILED", "Buy slippage calibration requires actual activation outputs beyond the proven RPC prefix; use a higher aggregate simulation cap/provider or preserve already committed minima without buySlippageBps", simulation);
46
47
  if (simulation.confidence !== "stateful" || simulation.backend === "unavailable" || simulation.steps.length !== planned.transactions.length || simulation.steps.some((step, index) => !step.success || step.transactionId !== planned.transactions[index]?.id)) throw new LifecyclePlanningError("LAUNCH_DIAGNOSTIC_FAILED", simulation.reason ?? "The complete diagnostic launch sequence did not execute successfully", simulation);
47
48
  const activation = planned.transactions.find((transaction) => transaction.kind === "atomic" || transaction.kind === "activate");
48
49
  const step = simulation.steps.find((candidate) => candidate.transactionId === activation?.id);
@@ -0,0 +1,74 @@
1
+ import type { LaunchEnvelopeV2, AdapterRegistrationV1, ProfileRegistrationV1, ProfileTopologyV1 } from "../schema.js";
2
+
3
+ // Generated by scripts/generate-ufg-presets.mjs from ufg-registry-readback.json.
4
+ // Robinhood block 94551140, 0x0778956b5a7bad65f8182b969ac4efc6a7182a0a922fbcb94dc87bfea26e9cc4. Creation code is byte-identical to the existing dynamic zero-royalty hook.
5
+ export const ufgDynamicEnvelope = Object.freeze({
6
+ artifactDigest: "0xd448a4117b78714cc4f96824356c6a90ec666449fde5c57e2b7bbe072f5e4a7a",
7
+ reviewManifestDigest: "0x0a3a0dfcae4fa51b2cccb2213e44800b7e3b74e681a66bc090f2e403a15b64b3",
8
+ configBoundsDigest: "0x72e0b2d542556d472a47f9c38b2c91079c72793af847dee77933f0eca203129d",
9
+ termsDigest: "0xbe3b09dc6d49fd548e76fed654b852039e8899e591ebacafc5e67f0692d0c699",
10
+ topology: 2,
11
+ configVersion: 6,
12
+ economicVersion: 3,
13
+ capabilities: 123n,
14
+ flags: 0n,
15
+ callbackFlags: 6908,
16
+ callbackMask: 16383,
17
+ protocolTreasury: "0x961981916AB6575C3af3eeCecbf6A3b7Fad7C9e1",
18
+ protocolFeeDenominator: 6,
19
+ beneficiary: "0x7cb44b8693b3C350A2BcD4b681aF85365aa27783",
20
+ maximumDeveloperFeeBps: 0,
21
+ bounds: Object.freeze({
22
+ minimumTickSpacing: 1,
23
+ maximumTickSpacing: 32767,
24
+ maximumPositions: 32,
25
+ maximumOracleCardinality: 4096,
26
+ feeModeFlags: 3,
27
+ }),
28
+ graph: Object.freeze({
29
+ manager: "0x8366a39CC670B4001A1121B8F6A443A643e40951",
30
+ hookRoot: "0x0000000000000000000000000000000000000000",
31
+ oracleFactory: "0xe7feF2BC860B25bbdEB6F6AB96d88bAAa77ddad7",
32
+ locker: "0xDa3bdeD722Bc45126752Ad6f28FAf27917D1a2fa",
33
+ collectorFactory: "0x46a8493b8f84E3Da412856Dd256263107e3873F2",
34
+ collectorDeployer: "0x50d6C10435dD928e426B8094FB944582c4FCa6f7",
35
+ hookDeployer: "0x435E0cb9b4C244edD5Bed40F9e39acdfcCd8d0f3",
36
+ coreCodeHash: "0x9fb8f46c3b980b9db25dfdb487a69a308a86be145860269b4036a4e74480787b",
37
+ managerCodeHash: "0xbd3881180b547f5fe817545743cfb4343e96b1bc6640dcd70c106b0066e95626",
38
+ hookRuntimeCodeHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
39
+ oracleFactoryCodeHash: "0x974fb5c72d91ccb99f1eb4679a5d4432c82bbaa0906b1c2917ec4da3499b7b98",
40
+ lockerCodeHash: "0x686024936a31a60193d2c6ea15ddb97ee36fc59a805eb1856cde61116075b370",
41
+ collectorFactoryCodeHash: "0xb0bd33507bebe1bb969153961237ee9a5661dd9ed60b9384a8869fa03ec5ed51",
42
+ collectorDeployerCodeHash: "0x79118bf755c242e2a190f9602c021e59ccc49d51eeff3b0c1524b4a53c25618d",
43
+ hookDeployerCodeHash: "0xe2a9753e6a8858b32443cd32b89c0ac9de95673a85611383d2a7bebc3b9d1187",
44
+ hookCreationCodeHash: "0xd448a4117b78714cc4f96824356c6a90ec666449fde5c57e2b7bbe072f5e4a7a",
45
+ codeChunk0: "0x550187621373bcece198E44EA31bE6b3936c699b",
46
+ codeChunk0Hash: "0x818c56bf10ae21129fe54f1b8991d6f807ce4ccef35b34c56251c1fd2d7d623f",
47
+ codeChunk1: "0x6C6c05e1C61c69C85548887e5E06384934AE7d71",
48
+ codeChunk1Hash: "0xbc73944bfa7e74df5968907a05fe2579a17ab155d3a0db407588d1baff51b4de",
49
+ sharedHookSalt: "0x0000000000000000000000000000000000000000000000000000000000000000",
50
+ }),
51
+ }) satisfies LaunchEnvelopeV2;
52
+ export const ufgDynamicAdapter = Object.freeze({
53
+ implementation: "0x38FAA8061c91BA6B2D3ef735f110017d12705067",
54
+ codeHash: "0xa13c2d0e05e0cc6a366f85ecbfab37ef26136e0993999bed4dd46538eabce23c",
55
+ capabilities: 123n,
56
+ configVersion: 6,
57
+ enabled: true,
58
+ }) satisfies AdapterRegistrationV1;
59
+ export const ufgDynamicRegistration = Object.freeze({
60
+ adapterId: "0xc0906e17625b48240a87a3afc6ee921b653bdacd72906a16b359d0776d938518",
61
+ configSchema: "0x51c5ca14c40012f78425cd1afb1ea77980da3fed0490a00911a8a4ed34792045",
62
+ dependencyDigest: "0x43078cffe54d685efc2c8e05243c50956569c98fd608861b6ed81f6ab1ad49b3",
63
+ venue: "0x8366a39CC670B4001A1121B8F6A443A643e40951",
64
+ factory: "0x0000000000000000000000000000000000000000",
65
+ hook: "0x0000000000000000000000000000000000000000",
66
+ capabilities: 123n,
67
+ enabled: true,
68
+ }) satisfies ProfileRegistrationV1;
69
+ export const ufgDynamicTopology = Object.freeze({
70
+ hookTopology: 2,
71
+ configVersion: 6,
72
+ hookDeployer: "0x435E0cb9b4C244edD5Bed40F9e39acdfcCd8d0f3",
73
+ hookCreationCodeHash: "0xd448a4117b78714cc4f96824356c6a90ec666449fde5c57e2b7bbe072f5e4a7a",
74
+ }) satisfies ProfileTopologyV1;
@@ -0,0 +1,74 @@
1
+ import type { LaunchEnvelopeV2, AdapterRegistrationV1, ProfileRegistrationV1, ProfileTopologyV1 } from "../schema.js";
2
+
3
+ // Generated by scripts/generate-ufg-presets.mjs from ufg-registry-readback.json.
4
+ // Robinhood block 94551140, 0x0778956b5a7bad65f8182b969ac4efc6a7182a0a922fbcb94dc87bfea26e9cc4. Creation code is byte-identical to the existing fixed zero-royalty hook.
5
+ export const ufgFixedEnvelope = Object.freeze({
6
+ artifactDigest: "0x11c4c7b1f265ed708b7d53ab828c89d237ca821333daa808f1913986c6f8f1ab",
7
+ reviewManifestDigest: "0x0a3a0dfcae4fa51b2cccb2213e44800b7e3b74e681a66bc090f2e403a15b64b3",
8
+ configBoundsDigest: "0x72e0b2d542556d472a47f9c38b2c91079c72793af847dee77933f0eca203129d",
9
+ termsDigest: "0xf2503b91eeec87f95c4f5e47df517a143422c766528c403ea44bdbd9b40df10e",
10
+ topology: 2,
11
+ configVersion: 6,
12
+ economicVersion: 3,
13
+ capabilities: 123n,
14
+ flags: 0n,
15
+ callbackFlags: 6908,
16
+ callbackMask: 16383,
17
+ protocolTreasury: "0x961981916AB6575C3af3eeCecbf6A3b7Fad7C9e1",
18
+ protocolFeeDenominator: 6,
19
+ beneficiary: "0x7cb44b8693b3C350A2BcD4b681aF85365aa27783",
20
+ maximumDeveloperFeeBps: 0,
21
+ bounds: Object.freeze({
22
+ minimumTickSpacing: 1,
23
+ maximumTickSpacing: 32767,
24
+ maximumPositions: 32,
25
+ maximumOracleCardinality: 4096,
26
+ feeModeFlags: 3,
27
+ }),
28
+ graph: Object.freeze({
29
+ manager: "0x8366a39CC670B4001A1121B8F6A443A643e40951",
30
+ hookRoot: "0x0000000000000000000000000000000000000000",
31
+ oracleFactory: "0xe7feF2BC860B25bbdEB6F6AB96d88bAAa77ddad7",
32
+ locker: "0xfbc0AbCD1A3EA65aF2196fa8Af1327a49B4C92FC",
33
+ collectorFactory: "0x46a8493b8f84E3Da412856Dd256263107e3873F2",
34
+ collectorDeployer: "0x50d6C10435dD928e426B8094FB944582c4FCa6f7",
35
+ hookDeployer: "0x4b04Ed1B02885DE905d8a14207EAC93F6AB231b2",
36
+ coreCodeHash: "0x9fb8f46c3b980b9db25dfdb487a69a308a86be145860269b4036a4e74480787b",
37
+ managerCodeHash: "0xbd3881180b547f5fe817545743cfb4343e96b1bc6640dcd70c106b0066e95626",
38
+ hookRuntimeCodeHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
39
+ oracleFactoryCodeHash: "0x974fb5c72d91ccb99f1eb4679a5d4432c82bbaa0906b1c2917ec4da3499b7b98",
40
+ lockerCodeHash: "0xa6fd4decc88671feb632d1871688a96582b46fd500b53f587d0fe18dad23fc2d",
41
+ collectorFactoryCodeHash: "0xb0bd33507bebe1bb969153961237ee9a5661dd9ed60b9384a8869fa03ec5ed51",
42
+ collectorDeployerCodeHash: "0x79118bf755c242e2a190f9602c021e59ccc49d51eeff3b0c1524b4a53c25618d",
43
+ hookDeployerCodeHash: "0xc97b52314cfc0225fcc0a901382898870e57394b318124956367d8fd3818506c",
44
+ hookCreationCodeHash: "0x11c4c7b1f265ed708b7d53ab828c89d237ca821333daa808f1913986c6f8f1ab",
45
+ codeChunk0: "0x94Fd6fB6E3db16Ef97c9bEEA229617b2Ec00c196",
46
+ codeChunk0Hash: "0xe5f56287276954b21f4211cabf2d9c251aae68836c5fa86ce68091ffccdee26b",
47
+ codeChunk1: "0x191F43334Ed2D7d154BF635E961BCb0e9848CB7c",
48
+ codeChunk1Hash: "0x76efd61e1a109a85140ce3473c43694a83f490eaac54d18673c17872a5262a47",
49
+ sharedHookSalt: "0x0000000000000000000000000000000000000000000000000000000000000000",
50
+ }),
51
+ }) satisfies LaunchEnvelopeV2;
52
+ export const ufgFixedAdapter = Object.freeze({
53
+ implementation: "0x3c7bb8541DBe2C8576E1000C4B6B803fDD36CA5d",
54
+ codeHash: "0x27079feaeb1a0ce8034588c46d15fead2a6407451d6f9e37059cade7c2df7610",
55
+ capabilities: 123n,
56
+ configVersion: 6,
57
+ enabled: true,
58
+ }) satisfies AdapterRegistrationV1;
59
+ export const ufgFixedRegistration = Object.freeze({
60
+ adapterId: "0x073a4d46af70a3d4f69641e4f9dcdc9d8c279cd443daff960bbdf65a3b5fc758",
61
+ configSchema: "0x51c5ca14c40012f78425cd1afb1ea77980da3fed0490a00911a8a4ed34792045",
62
+ dependencyDigest: "0xead7c65bd240844e961647f7cd059948afd384dfea490807f576325cad78aee0",
63
+ venue: "0x8366a39CC670B4001A1121B8F6A443A643e40951",
64
+ factory: "0x0000000000000000000000000000000000000000",
65
+ hook: "0x0000000000000000000000000000000000000000",
66
+ capabilities: 123n,
67
+ enabled: true,
68
+ }) satisfies ProfileRegistrationV1;
69
+ export const ufgFixedTopology = Object.freeze({
70
+ hookTopology: 2,
71
+ configVersion: 6,
72
+ hookDeployer: "0x4b04Ed1B02885DE905d8a14207EAC93F6AB231b2",
73
+ hookCreationCodeHash: "0x11c4c7b1f265ed708b7d53ab828c89d237ca821333daa808f1913986c6f8f1ab",
74
+ }) satisfies ProfileTopologyV1;
@@ -4,6 +4,7 @@ export * from "./types.js";
4
4
  export * from "./abi.js";
5
5
  export * from "./markets.js";
6
6
  export * from "./hmm.js";
7
+ export * from "./ufg.js";
7
8
  export * from "./progress.js";
8
9
  export * from "./fork.js";
9
10
  export * from "./authors.js";
@@ -2,6 +2,7 @@ import { concatHex, decodeAbiParameters, encodeAbiParameters, getAddress, hexToB
2
2
  import type { MarketConfigV1 } from "./schema.js";
3
3
  import { LifecyclePlanningError, type LifecycleProfile, type PoolBoundHookSaltProgress } from "./types.js";
4
4
  import { decodeHmmLaunchTail, encodeHmmLaunchTail, isHmmProfile, type HmmLaunchConfig } from "./hmm.js";
5
+ import { decodeUfgLaunchConfig, encodeUfgLaunchConfig, isUfgProfile, type UfgLaunchConfig } from "./ufg-config.js";
5
6
 
6
7
  export type V4LifecyclePositionConfig = {
7
8
  tickLower: number; tickUpper: number; liquidity: bigint; salt: Hex; maxTokenAmount: bigint;
@@ -44,6 +45,7 @@ export type V4PoolBoundLifecycleMarketConfigV5 = Omit<V4LifecycleMarketConfig, "
44
45
  export type V4PoolBoundLifecycleMarketConfigV6 = Omit<V4LifecycleMarketConfig, "version"> & {
45
46
  version: 6; minimumHookFeePips: number; feeSensitivityPipsSecondsPerTick: number; hookSalt: Hex;
46
47
  hmmLaunchConfig?: HmmLaunchConfig;
48
+ ufgLaunchConfig?: UfgLaunchConfig;
47
49
  };
48
50
  export type V4PoolBoundLifecycleMarketConfig = V4PoolBoundLifecycleMarketConfigV5 | V4PoolBoundLifecycleMarketConfigV6;
49
51
  export const V4_LIFECYCLE_CONFIG_SCHEMA = keccak256(stringToHex("(uint16,uint24,int24,uint160,uint24,uint8,uint8,address,bool,bytes32,bytes32,bytes32,address,uint16,(int24,int24,uint128,bytes32,uint256)[])"));
@@ -90,9 +92,16 @@ function assertV1HookFeeShape(config: object): void {
90
92
  }
91
93
  }
92
94
 
93
- /** HMM profile selection requires a typed tail and appends it automatically. */
95
+ /** Auxiliary launch profiles require their typed tail and append it automatically. */
94
96
  export function encodePoolBoundV4LifecycleMarketConfig(config: V4PoolBoundLifecycleMarketConfig): Hex {
95
97
  const prefix = encodePoolBoundV4Prefix(config);
98
+ if (isUfgProfile(config.profileId)) {
99
+ if (config.version !== 6 || config.ufgLaunchConfig === undefined) throw new LifecyclePlanningError("INVALID_UFG_CONFIG", "UFG profiles require config6 and an explicit ufgLaunchConfig");
100
+ if (config.hmmLaunchConfig !== undefined) throw new LifecyclePlanningError("INVALID_UFG_CONFIG", "UFG profiles cannot carry an HMM tail");
101
+ if (config.lpFeePips !== 0 || config.feeMode !== 1) throw new LifecyclePlanningError("INVALID_UFG_CONFIG", "UFG requires QuoteOnly feeMode 1 and zero LP fee");
102
+ return concatHex([prefix, encodeUfgLaunchConfig(config.ufgLaunchConfig)]);
103
+ }
104
+ if (config.version === 6 && config.ufgLaunchConfig !== undefined) throw new LifecyclePlanningError("INVALID_UFG_CONFIG", "A UFG launch tail requires a UFG profile");
96
105
  if (isHmmProfile(config.profileId)) {
97
106
  if (config.version !== 6 || config.hmmLaunchConfig === undefined) throw new LifecyclePlanningError("INVALID_HMM_CONFIG", "HMM profiles require config6 and an explicit hmmLaunchConfig");
98
107
  if (config.lpFeePips !== 0) throw new LifecyclePlanningError("INVALID_HOOK_FEES", "HMM profiles require zero LP fee");
@@ -126,6 +135,10 @@ export function decodePoolBoundV4LifecycleMarketConfig(encoded: Hex, expectedVer
126
135
  const prefixLength = encodePoolBoundV4Prefix(config).length;
127
136
  config.hmmLaunchConfig = decodeHmmLaunchTail(`0x${encoded.slice(prefixLength)}`);
128
137
  }
138
+ if (isUfgProfile(config.profileId)) {
139
+ if (config.version !== 6) throw new LifecyclePlanningError("INVALID_UFG_CONFIG", "UFG profiles require config6");
140
+ config.ufgLaunchConfig = decodeUfgLaunchConfig(`0x${encoded.slice(encodePoolBoundV4Prefix(config).length)}`);
141
+ }
129
142
  if (encodePoolBoundV4LifecycleMarketConfig(config).toLowerCase() !== encoded.toLowerCase()) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Pool-bound config is not canonical for its explicit schema");
130
143
  return config;
131
144
  }
@@ -245,8 +258,8 @@ export function hashPoolBoundV4MarketCommitment(options: { chainId: bigint; core
245
258
  if (!isPoolBoundV4ConfigVersion(market.configVersion)) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Pool-bound V4 market requires explicit config version 5 or 6");
246
259
  const config = decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion);
247
260
  if (config.profileId.toLowerCase() !== market.profileId.toLowerCase()) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Config profile differs from the committed market profile");
248
- // The collector factory commits the V6 prefix, not the auxiliary HMM tail.
249
- // The full plan commitment separately binds every byte of that tail.
261
+ // Collector factories commit only the V6 prefix, not auxiliary launch tails.
262
+ // The full plan commitment separately binds every byte of those tails.
250
263
  const configHash = keccak256(encodePoolBoundV4Prefix({ ...config, hookSalt: zeroHash }));
251
264
  return keccak256(encodeAbiParameters([
252
265
  { type: "bytes32" }, { type: "uint256" }, { type: "address" }, { type: "address" }, { type: "address" },
@@ -10,11 +10,14 @@ import { prepareLaunchSimulation, simulateLaunchTransactions, type NextTransacti
10
10
  import { LifecyclePlanningError, type BuildNextTransactionOptions, type CanonicalLaunchProgress, type LifecycleBlock, type LifecycleConstructionProfile, type LifecycleFundingPrerequisite, type LifecyclePoolBoundHookDeployment, type LifecycleProfile, type LifecycleRpcClient, type LifecycleSimulation, type LifecycleTransaction, type PlannedLaunch, type PlanLaunchOptions, type PrepareAndPlanLifecycleLaunchOptions, type SimulateLaunchPlanOptions } from "./types.js";
11
11
  import { deriveKnownPoolBoundHookDeployment, protectOrderedBuyMinimums, validateBuySlippageBps } from "./calibration.js";
12
12
  import { getKnownLifecycleDeployment, getKnownLifecycleProfile } from "./presets.js";
13
- import { buildLaunchTransactions } from "./transactions.js";
13
+ import { buildLaunchTransactions, packPreparationBatches } from "./transactions.js";
14
14
  import { assertLifecycleMarketAllocation, assertLifecycleSupplyAllocation } from "./allocations.js";
15
+ import { assertUfgLaunchAccount, hashUfgLaunchConfig, UFG_LAUNCH_PROXY_ADDRESS } from "./ufg-config.js";
16
+ import { buildUfgLaunchTransactions, buildUfgCancelTransaction, validateUfgLaunchPlan } from "./ufg.js";
15
17
 
16
18
  type ValidatedPlanShape = {
17
19
  commitment: Hex;
20
+ ufgConfigHash?: Hex;
18
21
  marketConfigs: (AbyssLifecycleMarketConfig | V4LifecycleMarketConfig | V4PoolBoundLifecycleMarketConfig)[];
19
22
  };
20
23
 
@@ -75,19 +78,19 @@ function validatePlanShape(plan: LaunchPlanV1): ValidatedPlanShape {
75
78
  return { commitment, marketConfigs };
76
79
  }
77
80
 
78
- async function readFunding(client: LifecycleRpcClient, plan: LaunchPlanV1, block: LifecycleBlock): Promise<{ prerequisites: LifecycleFundingPrerequisite[]; approvals: LifecycleTransaction[] }> {
81
+ async function readFunding(client: LifecycleRpcClient, plan: LaunchPlanV1, block: LifecycleBlock, account = plan.creator, ufg = false): Promise<{ prerequisites: LifecycleFundingPrerequisite[]; approvals: LifecycleTransaction[] }> {
79
82
  if (plan.funding.length === 0) return { prerequisites: [], approvals: [] };
80
83
  const preset = getKnownLifecycleDeployment({ chainId: plan.chainId, orchestrator: plan.orchestrator });
81
- const escrow = preset?.fundingEscrow ?? await readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], block);
84
+ const escrow = ufg ? UFG_LAUNCH_PROXY_ADDRESS : preset?.fundingEscrow ?? await readLifecycleContract<Address>(client, plan.orchestrator, launchLifecycleAbi, "fundingEscrow", [], block);
82
85
  // Only allowance observations construct approval commands. Core execution
83
86
  // enforces input/output asset, funding target, balance and conversion predicates.
84
87
  const prerequisites = await Promise.all(plan.funding.map(async (funding): Promise<LifecycleFundingPrerequisite> => {
85
88
  const native = funding.kind === LifecycleFundingKind.NativeWrap || funding.kind === LifecycleFundingKind.Swap && funding.inputAsset === zeroAddress;
86
89
  const [inputBalance, allowance] = native
87
- ? [rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [plan.creator, toHex(block.number)], block), "creator native balance"), undefined] as const
90
+ ? [rpcQuantity(await lifecyclePinnedRpc(client, "eth_getBalance", [account, toHex(block.number)], block), "creator native balance"), undefined] as const
88
91
  : await Promise.all([
89
- readLifecycleContract<bigint>(client, funding.inputAsset, lifecycleErc20Abi, "balanceOf", [plan.creator], block),
90
- readLifecycleContract<bigint>(client, funding.inputAsset, lifecycleErc20Abi, "allowance", [plan.creator, escrow], block),
92
+ readLifecycleContract<bigint>(client, funding.inputAsset, lifecycleErc20Abi, "balanceOf", [account], block),
93
+ readLifecycleContract<bigint>(client, funding.inputAsset, lifecycleErc20Abi, "allowance", [account, escrow], block),
91
94
  ]);
92
95
  return { funding, inputBalance, requiredInput: funding.inputAmount, allowance, spender: escrow, nativeValue: native ? funding.inputAmount : 0n, conversion: funding.kind === LifecycleFundingKind.NativeWrap ? "native-wrap" : funding.kind === LifecycleFundingKind.Swap ? "allowlisted-swap" : "none" };
93
96
  }));
@@ -103,8 +106,8 @@ async function readFunding(client: LifecycleRpcClient, plan: LaunchPlanV1, block
103
106
  const approvals: LifecycleTransaction[] = [];
104
107
  for (const total of totals.values()) {
105
108
  if (total.allowance >= total.amount) continue;
106
- if (total.allowance > 0n) approvals.push({ id: `approve-reset:${total.asset}`, kind: "approve-reset", chainId: Number(plan.chainId), from: plan.creator, to: total.asset, data: encodeFunctionData({ abi: lifecycleErc20Abi, functionName: "approve", args: [escrow, 0n] }), value: 0n, dependencies: [], postconditions: [{ kind: "allowance", asset: total.asset, spender: escrow, minimum: 0n, exact: 0n }] });
107
- approvals.push({ id: `approve:${total.asset}`, kind: "approve", chainId: Number(plan.chainId), from: plan.creator, to: total.asset, data: encodeFunctionData({ abi: lifecycleErc20Abi, functionName: "approve", args: [escrow, total.amount] }), value: 0n, dependencies: [], postconditions: [{ kind: "allowance", asset: total.asset, spender: escrow, minimum: total.amount }] });
109
+ if (total.allowance > 0n) approvals.push({ id: `approve-reset:${total.asset}`, kind: "approve-reset", chainId: Number(plan.chainId), from: account, to: total.asset, data: encodeFunctionData({ abi: lifecycleErc20Abi, functionName: "approve", args: [escrow, 0n] }), value: 0n, dependencies: [], postconditions: [{ kind: "allowance", asset: total.asset, spender: escrow, minimum: 0n, exact: 0n }] });
110
+ approvals.push({ id: `approve:${total.asset}`, kind: "approve", chainId: Number(plan.chainId), from: account, to: total.asset, data: encodeFunctionData({ abi: lifecycleErc20Abi, functionName: "approve", args: [escrow, total.amount] }), value: 0n, dependencies: [], postconditions: [{ kind: "allowance", asset: total.asset, spender: escrow, minimum: total.amount }] });
108
111
  }
109
112
  return { prerequisites, approvals };
110
113
  }
@@ -132,12 +135,12 @@ async function readPlanMetadata(client: LifecycleRpcClient, plan: LaunchPlanV1,
132
135
  return { profiles, hookDeployments, reasons: live.filter((profile) => !profile.admitted).map((profile) => profile.reason ?? "Unlisted lifecycle profile is unavailable") };
133
136
  }
134
137
 
135
- function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", progress: CanonicalLaunchProgress, approvals: readonly LifecycleTransaction[], batchSize: number): LifecycleTransaction[] {
138
+ function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", progress: CanonicalLaunchProgress, approvals: readonly LifecycleTransaction[], batches: readonly number[], ufg?: PlanLaunchOptions["ufg"]): LifecycleTransaction[] {
136
139
  if (progress.canonical.phase === LifecyclePhase.Active || progress.canonical.phase === LifecyclePhase.Cancelled) return [];
137
140
  const prepared = progress.canonical.preparedMarkets;
138
- const batches: number[] = prepared > 0 ? [prepared] : [];
139
- for (let first = prepared; first < plan.markets.length; first += batchSize) batches.push(Math.min(batchSize, plan.markets.length - first));
140
- const commands = buildLaunchTransactions({ plan, mode, preparationBatches: mode === "staged" ? batches : undefined }).filter((transaction) =>
141
+ const compiled = ufg === undefined ? buildLaunchTransactions({ plan, mode, preparationBatches: mode === "staged" ? batches : undefined })
142
+ : buildUfgLaunchTransactions({ plan, config: ufg.config, mode, preparationBatches: mode === "staged" ? batches : undefined });
143
+ const commands = compiled.filter((transaction) =>
141
144
  (transaction.kind !== "begin" && transaction.kind !== "atomic" || progress.canonical.phase === LifecyclePhase.None) &&
142
145
  (transaction.kind !== "prepare" || (transaction.marketStart ?? 0) >= prepared));
143
146
  const transactions = progress.canonical.phase === LifecyclePhase.None ? [...approvals, ...commands] : commands;
@@ -147,7 +150,8 @@ function orderedTransactions(plan: LaunchPlanV1, mode: "atomic" | "staged", prog
147
150
  function attachEstimates(transactions: readonly LifecycleTransaction[], simulation: LifecycleSimulation): LifecycleTransaction[] {
148
151
  return transactions.map((transaction) => {
149
152
  const estimate = simulation.steps.find((step) => step.transactionId === transaction.id)?.estimate;
150
- return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate, admission: { admitted: simulation.admitted, confidence: simulation.confidence, executionProof: simulation.executionProof, protocolFit: simulation.protocolFit, transportPreflight: simulation.transportPreflight, blockNumber: simulation.blockNumber, blockHash: simulation.blockHash, account: simulation.account, chainId: simulation.chainId, reason: simulation.reason, limits: simulation.limits } };
153
+ return { ...transaction, calldataBytes: (transaction.data.length - 2) / 2, gas: estimate?.gasLimit, gasPrice: estimate?.gasPrice, estimate,
154
+ admission: estimate === undefined ? undefined : { admitted: simulation.admitted, confidence: simulation.confidence, executionProof: simulation.executionProof, protocolFit: simulation.protocolFit, transportPreflight: simulation.transportPreflight, blockNumber: simulation.blockNumber, blockHash: simulation.blockHash, account: simulation.account, chainId: simulation.chainId, reason: simulation.reason, limits: simulation.limits } };
151
155
  });
152
156
  }
153
157
  type ReviewedNextTransaction = Pick<LifecycleTransaction, "id" | "kind" | "chainId" | "from" | "to" | "data" | "value"> & NextTransactionEnvelope;
@@ -176,7 +180,8 @@ function matchesReviewedNextTransaction(next: LifecycleTransaction | ReviewedNex
176
180
  }
177
181
 
178
182
  function assertReviewedTransactionUnchanged(options: BuildNextTransactionOptions, reviewed: ReviewedNextTransaction | undefined, next: LifecycleTransaction): void {
179
- if (options.planned.account.toLowerCase() !== next.from.toLowerCase() || options.planned.plan.creator.toLowerCase() !== next.from.toLowerCase()) {
183
+ if (options.planned.ufg !== undefined) assertUfgLaunchAccount(options.planned.plan, options.planned.account);
184
+ if (options.planned.account.toLowerCase() !== next.from.toLowerCase() || options.planned.ufg === undefined && options.planned.plan.creator.toLowerCase() !== next.from.toLowerCase()) {
180
185
  throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account changed during fresh execution or submission proof");
181
186
  }
182
187
  if (options.planned.chainId !== BigInt(next.chainId) || options.planned.plan.chainId !== BigInt(next.chainId)) {
@@ -203,6 +208,7 @@ async function readTokenFactoryBinding(client: LifecycleRpcClient, orchestrator:
203
208
 
204
209
  function assertPlannedDeploymentBindings(stored: PlannedLaunch, current: PlannedLaunch): void {
205
210
  if (hashLaunchPlan(stored.plan).toLowerCase() !== stored.planHash.toLowerCase() || current.planHash.toLowerCase() !== stored.planHash.toLowerCase() || current.launchId.toLowerCase() !== stored.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored economic plan no longer matches its finalized commitment");
211
+ if (stored.ufg === undefined ? current.ufg !== undefined : current.ufg === undefined || hashUfgLaunchConfig(stored.ufg.config) !== stored.ufg.configHash || current.ufg.configHash !== stored.ufg.configHash) throw new LifecyclePlanningError("PLAN_MUTATED", "Stored UFG route or treasury config no longer matches its finalized commitment");
206
212
  if (current.predictedToken.toLowerCase() !== stored.predictedToken.toLowerCase() || current.tokenFactory.toLowerCase() !== stored.tokenFactory.toLowerCase() || current.tokenFactoryCodeHash.toLowerCase() !== stored.tokenFactoryCodeHash.toLowerCase()) throw new LifecyclePlanningError("TOKEN_FACTORY_BINDING", "Final token prediction or its immutable factory code binding changed before execution");
207
213
  if (!current.simulation.admitted || current.progress.canonical.phase === LifecyclePhase.Active || current.progress.canonical.phase === LifecyclePhase.Cancelled) return;
208
214
  if (stored.hookDeployments.length !== current.hookDeployments.length || stored.hookDeployments.some((deployment, index) => {
@@ -219,9 +225,11 @@ function validatePlanningIntent(options: PlanLaunchOptions): ValidatedPlanShape
219
225
  const { plan, account, mode } = options;
220
226
  assertPlanningActive(options.signal);
221
227
  if (mode !== "atomic" && mode !== "staged") throw new LifecyclePlanningError("EXPLICIT_MODE_REQUIRED", "Choose atomic or staged explicitly; staged is never silently substituted");
222
- if (account.toLowerCase() !== plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
228
+ if (options.ufg !== undefined) assertUfgLaunchAccount(plan, account);
229
+ else if (account.toLowerCase() !== plan.creator.toLowerCase()) throw new LifecyclePlanningError("ACCOUNT_MISMATCH", "Execution account must be the committed creator/payer/refund account");
223
230
  if (plan.chainId > BigInt(Number.MAX_SAFE_INTEGER) || plan.chainId <= 0n) throw new LifecyclePlanningError("CHAIN_MISMATCH", "Chain ID must be a positive exact JavaScript integer");
224
- return validatePlanShape(plan);
231
+ const shape = validatePlanShape(plan);
232
+ return options.ufg === undefined ? shape : { ...shape, ufgConfigHash: hashUfgLaunchConfig(options.ufg.config) };
225
233
  }
226
234
 
227
235
  function validatePlanTopology(options: PlanLaunchOptions, marketConfigs: ValidatedPlanShape["marketConfigs"], token: Address): void {
@@ -257,12 +265,13 @@ function validatePlanTopology(options: PlanLaunchOptions, marketConfigs: Validat
257
265
  function assertFinalPlan(planned: PlannedLaunch, signal?: AbortSignal): void {
258
266
  assertPlanningActive(signal);
259
267
  if (hashLaunchPlan(planned.plan).toLowerCase() !== planned.planHash.toLowerCase() || hashLaunchIdentity(planned.plan).toLowerCase() !== planned.launchId.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous planning; previous reads and simulation are invalid");
268
+ if (planned.ufg !== undefined && hashUfgLaunchConfig(planned.ufg.config) !== planned.ufg.configHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG treasury config changed during asynchronous planning");
260
269
  }
261
270
 
262
271
  /** One owner constructs and mines, optionally measures actual ordered buy outputs,
263
272
  * then proves only the final protected commitment. No cached execution certificates. */
264
273
  export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifecycleLaunchOptions): Promise<PlannedLaunch> {
265
- const { commitment } = validatePlanningIntent(options);
274
+ const { commitment, ufgConfigHash } = validatePlanningIntent(options);
266
275
  if (options.buySlippageBps !== undefined) validateBuySlippageBps(options.buySlippageBps);
267
276
  const needsMining = options.plan.markets.some((market) => isPoolBoundV4ConfigVersion(market.configVersion));
268
277
  const needsProtection = options.buySlippageBps !== undefined && options.plan.buys.length !== 0;
@@ -271,6 +280,7 @@ export async function prepareAndPlanLifecycleLaunch(options: PrepareAndPlanLifec
271
280
  const assertDraft = () => {
272
281
  assertPlanningActive(options.signal);
273
282
  if (hashLaunchPlan(options.plan).toLowerCase() !== commitment.toLowerCase()) throw new LifecyclePlanningError("PLAN_MUTATED", "Economic plan changed during asynchronous preparation; previous construction is invalid");
283
+ if ((options.ufg === undefined ? undefined : hashUfgLaunchConfig(options.ufg.config)) !== ufgConfigHash) throw new LifecyclePlanningError("PLAN_MUTATED", "UFG config changed during asynchronous preparation");
274
284
  };
275
285
  return withLifecycleReadClient(options.client, async (client) => {
276
286
  const [block, chainId] = await Promise.all([readLifecycleBlock(client), readLifecycleChainId(client)]);
@@ -335,8 +345,8 @@ export async function planLaunch(options: PlanLaunchOptions): Promise<PlannedLau
335
345
  }
336
346
 
337
347
  async function planLaunchWithReads(options: PlanLaunchOptions, client: LifecycleRpcClient, pinnedBlock?: LifecycleBlock, ownedProgress?: CanonicalLaunchProgress, diagnostic = false, reviewed?: ReviewedNextTransaction): Promise<PlannedLaunch> {
338
- const { plan, account, mode } = options;
339
- const { commitment: planHash, marketConfigs } = validatePlanningIntent(options);
348
+ const { plan, account, mode, ufg } = options;
349
+ const { commitment: planHash, marketConfigs, ufgConfigHash } = validatePlanningIntent(options);
340
350
  const launchId = hashLaunchIdentity(plan);
341
351
  const [block, chainId] = await lifecycleStage(client, "plan.context", () => Promise.all([
342
352
  pinnedBlock ?? readLifecycleBlock(client), readLifecycleChainId(client),
@@ -357,15 +367,17 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
357
367
  resolveLifecycleLimits({ client, account, orchestrator: plan.orchestrator, block, chainId: plan.chainId }, options.limits),
358
368
  lifecycleStage(client, "plan.progress", async () => {
359
369
  const predictedToken = await readLifecycleTokenAtBlock(client, plan, block);
360
- const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1 };
370
+ const identity = { plan, planHash, launchId, predictedToken, account, mode, confirmations: options.confirmations ?? 1,
371
+ ...(ufg === undefined ? {} : { ufg: { config: ufg.config, configHash: ufgConfigHash! } }) };
372
+ if (ufg !== undefined) validateUfgLaunchPlan(plan, ufg.config, predictedToken);
361
373
  const progress = ownedProgress ?? await readLaunchProgress({ client, planned: identity, receipts: options.receipts, signal: options.signal }, block);
362
374
  if (ownedProgress !== undefined && progress.token.toLowerCase() !== predictedToken.toLowerCase()) throw new LifecyclePlanningError("TOKEN_IDENTITY", "Predicted token changed after its canonical progress was read");
363
375
  return { identity, progress };
364
376
  }),
365
377
  ]));
366
378
  assertPlanningActive(options.signal);
367
- const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [] };
368
- let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, preparationBatchSize: plan.markets.length, limits: options.limits,
379
+ const placeholder: LifecycleSimulation = { backend: "unavailable", confidence: "provisional", admitted: false, executionProof: "unavailable", protocolFit: "unknown", transportPreflight: "not-requested", blockNumber: block.number, blockHash: block.hash, account, chainId: plan.chainId, limits, steps: [], deferredTransactionIds: [] };
380
+ let planned: PlannedLaunch = { ...identity, tokenFactory, tokenFactoryCodeHash, chainId: plan.chainId, transactions: [], totalCalldataBytes: 0, prerequisites: [], profiles: [], hookDeployments: [], progress, simulation: placeholder, limits: options.limits,
369
381
  allowTopologyGaps: options.allowTopologyGaps === true, allowMissingPermanentTail: options.allowMissingPermanentTail === true };
370
382
  const receiptsPending = progress.receipts.some((receipt) => receipt.status === "pending" || receipt.status === "unconfirmed");
371
383
  if (!progress.confirmationSafe || receiptsPending) {
@@ -388,53 +400,63 @@ async function planLaunchWithReads(options: PlanLaunchOptions, client: Lifecycle
388
400
  // immutable onchain commitments. Waiting and terminal launches returned above.
389
401
  if (progress.canonical.phase === LifecyclePhase.None && progress.head.phase === LifecyclePhase.None) validatePlanTopology(options, marketConfigs, identity.predictedToken);
390
402
  const [funding, metadata] = await lifecycleStage(client, "plan.inputs", () => Promise.all([
391
- progress.canonical.phase === LifecyclePhase.None ? readFunding(client, plan, block) : { prerequisites: [], approvals: [] },
403
+ progress.canonical.phase === LifecyclePhase.None ? readFunding(client, plan, block, ufg === undefined ? plan.creator : account, ufg !== undefined) : { prerequisites: [], approvals: [] },
392
404
  readPlanMetadata(client, plan, identity.predictedToken, block),
393
405
  ]));
394
406
  assertFinalPlan(planned, options.signal);
395
407
  planned = { ...planned, prerequisites: funding.prerequisites, profiles: metadata.profiles, hookDeployments: metadata.hookDeployments };
396
- let preparationBatchSize = plan.markets.length;
397
- let transactions = orderedTransactions(plan, mode, progress, funding.approvals, preparationBatchSize);
408
+ const prepared = progress.canonical.preparedMarkets;
409
+ const computeCap = limits.transactionGasCeiling !== undefined && limits.transactionGasCeiling < limits.executionGasCeiling ? limits.transactionGasCeiling : limits.executionGasCeiling;
410
+ let batches: number[] = [];
411
+ if (mode === "staged") batches = reviewed === undefined ? packPreparationBatches(plan, prepared, computeCap)
412
+ : [...(prepared > 0 ? [prepared] : []), ...(prepared < plan.markets.length ? [plan.markets.length - prepared] : [])];
413
+ if (reviewed !== undefined && reviewed.kind === "prepare") {
414
+ // Canonical progress fixes the start; the exact held calldata fixes its span.
415
+ // Never rediscover a batch or enlarge a reviewed transaction.
416
+ const match = /^prepare:(\d+):(\d+)$/.exec(reviewed.id);
417
+ const start = Number(match?.[1]), count = Number(match?.[2]);
418
+ if (start !== prepared || !Number.isSafeInteger(count) || count <= 0 || start + count > plan.markets.length) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed preparation differs from canonical unfinished progress");
419
+ batches = [...(prepared > 0 ? [prepared] : []), count, ...(start + count < plan.markets.length ? [plan.markets.length - start - count] : [])];
420
+ }
421
+ let transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg);
422
+ if (reviewed !== undefined) {
423
+ if (!matchesReviewedNextTransaction(transactions[0], reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
424
+ transactions = transactions.slice(0, 1);
425
+ }
398
426
  if (metadata.reasons.length > 0) {
399
427
  await assertLifecycleBlock(client, block, plan.chainId);
400
428
  return { ...planned, simulation: { ...placeholder, confidence: "stateful", reason: metadata.reasons.join("; "), failureCategory: "semantic" }, transactions, totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
401
429
  }
402
- const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block);
430
+ const preparedSimulation = prepareLaunchSimulation({ client, planned, limits: options.limits, fork: options.fork, signal: options.signal }, block, true, reviewed);
403
431
  try {
404
- const execute = diagnostic ? preparedSimulation.measure : (candidate: readonly LifecycleTransaction[]) => {
405
- if (reviewed === undefined) return preparedSimulation(candidate);
406
- if (!matchesReviewedNextTransaction(candidate[0], reviewed)) {
407
- // A fresh prepare batch may still require SDK-owned capacity discovery.
408
- // Never use held fees for a different step or caller-selected calldata.
409
- if (candidate[0]?.kind === "prepare" && reviewed.kind === "prepare") return preparedSimulation(candidate);
410
- throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
411
- }
412
- return preparedSimulation(candidate, reviewed);
413
- };
432
+ const execute = diagnostic ? preparedSimulation.measure : (candidate: readonly LifecycleTransaction[]) => preparedSimulation(candidate, reviewed);
414
433
  let simulation = await execute(transactions);
415
434
  assertFinalPlan(planned, options.signal);
416
435
  if (mode === "staged") {
417
- // Only explicit gas/compute evidence can partition empty preparations.
418
- // Byte caps cannot shrink: each prepare still carries the entire plan.
419
- while (!simulation.admitted && simulation.failureCategory === "capacity" &&
420
- (simulation.capacityConstraint === "compute" || simulation.capacityConstraint === "gas-envelope") &&
421
- !(reviewed !== undefined && simulation.capacityConstraint === "gas-envelope" && simulation.failedTransactionId === transactions[0]?.id && matchesReviewedNextTransaction(transactions[0], reviewed)) &&
422
- simulation.failedTransactionId?.startsWith("prepare:") && preparationBatchSize > 1) {
423
- const failedMarketCount = transactions.find((transaction) => transaction.id === simulation.failedTransactionId)?.marketCount ?? 0;
424
- if (failedMarketCount <= 1) break;
425
- preparationBatchSize = Math.ceil(failedMarketCount / 2);
426
- transactions = orderedTransactions(plan, mode, progress, funding.approvals, preparationBatchSize);
436
+ // Only a measurement that consumed almost its entire legal envelope
437
+ // invalidates packing estimates, including gas hidden by CREATE wrappers.
438
+ while (reviewed === undefined && !simulation.admitted && simulation.failedTransactionId?.startsWith("prepare:")) {
439
+ const failedIndex = transactions.findIndex((transaction) => transaction.id === simulation.failedTransactionId);
440
+ const failedTransaction = transactions[failedIndex];
441
+ const step = simulation.steps.find((candidate) => candidate.transactionId === simulation.failedTransactionId);
442
+ const count = failedTransaction?.marketCount ?? 0;
443
+ if (count <= 1 || step?.estimate !== undefined || step?.gasUsed === undefined || step.gasLimit === undefined || step.gasUsed * 100n < step.gasLimit * 95n) break;
444
+ let first = 0;
445
+ const batchIndex = batches.findIndex((batch) => { const matches = first === failedTransaction?.marketStart; first += batch; return matches; });
446
+ const left = Math.ceil(count / 2);
447
+ batches.splice(batchIndex, 1, left, count - left);
448
+ transactions = orderedTransactions(plan, mode, progress, funding.approvals, batches, ufg);
427
449
  simulation = await execute(transactions);
428
450
  assertFinalPlan(planned, options.signal);
429
451
  }
430
452
  if (!simulation.admitted && simulation.failedTransactionId === "activate") simulation = { ...simulation, reason: `${simulation.reason ?? "Final activation failed"}; final mint/lock/all-buys/public-opening is indivisible and will not be partitioned` };
431
- } else if (!diagnostic && !simulation.admitted) {
453
+ } else if (!diagnostic && !simulation.admitted && !(simulation.capacityConstraint === "compute" && simulation.confidence === "provisional")) {
432
454
  simulation = { ...simulation, reason: `${simulation.reason ?? "Exact atomic plan is not admitted"}; choose staged explicitly only if empty preparation can be partitioned and complete activation fits` };
433
455
  }
434
456
  if (reviewed !== undefined && !matchesReviewedNextTransaction(transactions[0], reviewed)) {
435
457
  throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical unfinished next step");
436
458
  }
437
- return { ...planned, simulation, preparationBatchSize, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
459
+ return { ...planned, simulation, transactions: attachEstimates(transactions, simulation), totalCalldataBytes: transactions.reduce((total, transaction) => total + (transaction.data.length - 2) / 2, 0) };
438
460
  } finally { preparedSimulation.close(); }
439
461
  }
440
462
 
@@ -498,7 +520,9 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
498
520
  if (options.action === "cancel") {
499
521
  if (progress.canonical.phase === LifecyclePhase.None) throw new LifecyclePlanningError("NOT_STARTED", "An unstarted plan has no launch escrow to cancel");
500
522
  const { planned } = options;
501
- const transaction: LifecycleTransaction = { id: "cancel", kind: "cancel", chainId: Number(planned.chainId), from: planned.account, to: planned.plan.orchestrator, data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "cancelLaunch", args: [planned.plan] }), value: 0n, dependencies: [], postconditions: [{ kind: "launch", phase: "Cancelled", preparedMarkets: progress.canonical.preparedMarkets }] };
523
+ assertFinalPlan(planned, options.signal);
524
+ const transaction: LifecycleTransaction = planned.ufg !== undefined ? buildUfgCancelTransaction({ plan: planned.plan, preparedMarkets: progress.canonical.preparedMarkets })
525
+ : { id: "cancel", kind: "cancel", chainId: Number(planned.chainId), from: planned.account, to: planned.plan.orchestrator, data: encodeFunctionData({ abi: launchLifecycleAbi, functionName: "cancelLaunch", args: [planned.plan] }), value: 0n, dependencies: [], postconditions: [{ kind: "launch", phase: "Cancelled", preparedMarkets: progress.canonical.preparedMarkets }] };
502
526
  if (reviewed !== undefined && !matchesReviewedNextTransaction(transaction, reviewed)) throw new LifecyclePlanningError("REVIEWED_TRANSACTION_MISMATCH", "Reviewed transaction differs from the current canonical cancellation");
503
527
  const simulation = await simulateLaunchTransactions({ client, planned, limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork, signal: options.signal }, [transaction], block, reviewed);
504
528
  if (!simulation.admitted) throw new LifecyclePlanningError("CANCEL_NOT_ADMITTED", simulation.reason ?? "Cancellation could not be proved against current canonical state", simulation);
@@ -507,7 +531,7 @@ export async function buildNextTransaction(options: BuildNextTransactionOptions)
507
531
  return preflightNextTransaction(options, next, simulation, reviewed);
508
532
  }
509
533
  const refreshed = await lifecycleStage(client, "plan", () => planLaunchWithReads({ client, plan: options.planned.plan, account: options.planned.account, mode: options.planned.mode, limits: options.limits === undefined ? options.planned.limits : options.limits, fork: options.fork, receipts: options.receipts, confirmations: options.confirmations ?? options.planned.confirmations, signal: options.signal,
510
- allowTopologyGaps: options.planned.allowTopologyGaps, allowMissingPermanentTail: options.planned.allowMissingPermanentTail }, client, block, progress, false, reviewed));
534
+ ufg: options.planned.ufg, allowTopologyGaps: options.planned.allowTopologyGaps, allowMissingPermanentTail: options.planned.allowMissingPermanentTail }, client, block, progress, false, reviewed));
511
535
  assertFinalPlan(refreshed, options.signal);
512
536
  if (!refreshed.simulation.admitted) throw new LifecyclePlanningError("PLAN_NOT_ADMITTED", refreshed.simulation.reason ?? "Remaining exact launch sequence is not admitted", refreshed.simulation);
513
537
  assertPlannedDeploymentBindings(options.planned, refreshed);
@@ -526,7 +550,7 @@ export async function simulateLaunchPlan(options: SimulateLaunchPlanOptions): Pr
526
550
  client, plan: planned.plan, account: planned.account, mode: planned.mode,
527
551
  limits: options.limits === undefined ? planned.limits : options.limits, fork: options.fork,
528
552
  receipts: options.receipts, confirmations: options.confirmations ?? planned.confirmations, signal: options.signal,
529
- allowTopologyGaps: planned.allowTopologyGaps, allowMissingPermanentTail: planned.allowMissingPermanentTail,
553
+ ufg: planned.ufg, allowTopologyGaps: planned.allowTopologyGaps, allowMissingPermanentTail: planned.allowMissingPermanentTail,
530
554
  });
531
555
  assertPlannedDeploymentBindings(planned, current);
532
556
  return current.simulation;
@@ -1,13 +1,16 @@
1
1
  import type { Address, Hex } from "viem";
2
2
  import type {
3
- AdapterRegistrationV1, LaunchEnvelopeV2, ProfileRegistrationV1, ProfileTopologyV1,
3
+ AdapterRegistrationV1, LaunchEnvelopeV2, LaunchPlanV1, ProfileRegistrationV1, ProfileTopologyV1,
4
4
  } from "./schema.js";
5
5
  import type { LifecycleProfile } from "./types.js";
6
6
  import { fixedFeePoolHookV1CreationCode } from "./deployments/fixed-fee-pool-hook-v1-20261008.js";
7
7
  import { dynamicFeePoolHookV1CreationCode } from "./deployments/dynamic-fee-pool-hook-v1-20261011.js";
8
8
  import { hmmFixedEnvelope, hmmFixedAdapter, hmmFixedRegistration, hmmFixedCreationCode } from "./deployments/hmm-fixed-pool-hook-v2-20261017.js";
9
9
  import { hmmDynamicEnvelope, hmmDynamicAdapter, hmmDynamicRegistration, hmmDynamicCreationCode } from "./deployments/hmm-dynamic-pool-hook-v2-20261017.js";
10
- import { HMM_FIXED_PROFILE_ID, HMM_DYNAMIC_PROFILE_ID } from "./hmm.js";
10
+ import { HMM_FIXED_PROFILE_ID, HMM_DYNAMIC_PROFILE_ID, isHmmProfile } from "./hmm.js";
11
+ import { ufgFixedEnvelope, ufgFixedAdapter, ufgFixedRegistration } from "./deployments/ufg-fixed-pool-20261021.js";
12
+ import { ufgDynamicEnvelope, ufgDynamicAdapter, ufgDynamicRegistration } from "./deployments/ufg-dynamic-pool-20261021.js";
13
+ import { UFG_FIXED_PROFILE_ID, UFG_DYNAMIC_PROFILE_ID } from "./ufg-config.js";
11
14
 
12
15
  export enum KnownLifecycleProfile {
13
16
  Abyss0 = "abyss-0",
@@ -18,6 +21,8 @@ export enum KnownLifecycleProfile {
18
21
  V4DynamicFeePool = "v4-dynamic-fee-pool",
19
22
  V4HmmFixedPool = "v4-hmm-fixed-pool",
20
23
  V4HmmDynamicPool = "v4-hmm-dynamic-pool",
24
+ V4UfgFixedPool = "v4-ufg-fixed-pool",
25
+ V4UfgDynamicPool = "v4-ufg-dynamic-pool",
21
26
  }
22
27
 
23
28
  export enum KnownLifecycleHook {
@@ -280,7 +285,21 @@ const dynamicFeePoolPreset: LifecycleProfilePreset = Object.freeze({
280
285
  }),
281
286
  });
282
287
 
283
- function hmmPreset(key: KnownLifecycleProfile, id: Hex, envelope: LaunchEnvelopeV2,
288
+ // HMMLiquidityHookV2.sol:98-119 requires ~3.06M available before manager calls,
289
+ // including opening and swaps. Robinhood fork measurements: activation minima
290
+ // 9.60M/15.40M/21.11M for 1/2/3 markets despite used gas 5.85M/11.13M/16.54M.
291
+ // Apply this forwarding target once per activate/atomic envelope, not per callback.
292
+ export const HMM_V2_FORWARDING_BASE_GAS = 4_250_000n;
293
+ export const HMM_V2_FORWARDING_EXTRA_MARKET_GAS = 500_000n;
294
+ export const HMM_V2_PREPARE_GAS = 14_500_000n;
295
+
296
+ export function hmmV2ForwardingReserve(plan: LaunchPlanV1): bigint {
297
+ let count = 0;
298
+ for (const market of plan.markets) if (isHmmProfile(market.profileId)) count += 1;
299
+ return count === 0 ? 0n : HMM_V2_FORWARDING_BASE_GAS + HMM_V2_FORWARDING_EXTRA_MARKET_GAS * BigInt(count - 1);
300
+ }
301
+
302
+ function poolBoundPreset(key: KnownLifecycleProfile, id: Hex, envelope: LaunchEnvelopeV2,
284
303
  adapter: AdapterRegistrationV1, registration: ProfileRegistrationV1, creationCode: Hex): LifecycleProfilePreset {
285
304
  return Object.freeze({
286
305
  key, supported: true,
@@ -313,8 +332,10 @@ const knownDeployment: LifecycleDeploymentPreset = Object.freeze({
313
332
  abyssPreset(KnownLifecycleProfile.Abyss3, "0xe1bd95e086469e6afd9791ccc957e4d22b735012cb3db4a747f703673c62c306"),
314
333
  fixedFeePoolPreset,
315
334
  dynamicFeePoolPreset,
316
- hmmPreset(KnownLifecycleProfile.V4HmmFixedPool, HMM_FIXED_PROFILE_ID, hmmFixedEnvelope, hmmFixedAdapter, hmmFixedRegistration, hmmFixedCreationCode),
317
- hmmPreset(KnownLifecycleProfile.V4HmmDynamicPool, HMM_DYNAMIC_PROFILE_ID, hmmDynamicEnvelope, hmmDynamicAdapter, hmmDynamicRegistration, hmmDynamicCreationCode),
335
+ poolBoundPreset(KnownLifecycleProfile.V4HmmFixedPool, HMM_FIXED_PROFILE_ID, hmmFixedEnvelope, hmmFixedAdapter, hmmFixedRegistration, hmmFixedCreationCode),
336
+ poolBoundPreset(KnownLifecycleProfile.V4HmmDynamicPool, HMM_DYNAMIC_PROFILE_ID, hmmDynamicEnvelope, hmmDynamicAdapter, hmmDynamicRegistration, hmmDynamicCreationCode),
337
+ poolBoundPreset(KnownLifecycleProfile.V4UfgFixedPool, UFG_FIXED_PROFILE_ID, ufgFixedEnvelope, ufgFixedAdapter, ufgFixedRegistration, fixedFeePoolHookV1CreationCode),
338
+ poolBoundPreset(KnownLifecycleProfile.V4UfgDynamicPool, UFG_DYNAMIC_PROFILE_ID, ufgDynamicEnvelope, ufgDynamicAdapter, ufgDynamicRegistration, dynamicFeePoolHookV1CreationCode),
318
339
  ]),
319
340
  provenance: Object.freeze({ sourceCommit: "e174ce695c7010f50ca9211b07b9f178c990beac", deploymentBlock: 82880546n }),
320
341
  });
@@ -344,7 +365,7 @@ export function getKnownLifecycleProfile(options: KnownLifecycleDeploymentIdenti
344
365
  return undefined;
345
366
  }
346
367
 
347
- /** Stable, frozen order: Abyss variants 0 through 3, fixed/dynamic V4, then HMM fixed/dynamic. */
368
+ /** Stable, frozen order: Abyss variants, fixed/dynamic V4, HMM, then UFG fixed/dynamic. */
348
369
  export function listKnownLifecycleProfiles(options: KnownLifecycleDeploymentIdentity): readonly LifecycleProfilePreset[] {
349
370
  return getKnownLifecycleDeployment(options)?.profiles ?? noProfiles;
350
371
  }