@black-market/sdk 0.6.3 → 0.7.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 (82) hide show
  1. package/README.md +28 -4
  2. package/dist/addresses.d.ts +1 -1
  3. package/dist/addresses.js +4 -4
  4. package/dist/launch.d.ts +61 -0
  5. package/dist/launch.d.ts.map +1 -1
  6. package/dist/launch.js +171 -6
  7. package/dist/launch.js.map +1 -1
  8. package/dist/lifecycle/abi.d.ts +1999 -253
  9. package/dist/lifecycle/abi.d.ts.map +1 -1
  10. package/dist/lifecycle/abi.js +43 -14
  11. package/dist/lifecycle/abi.js.map +1 -1
  12. package/dist/lifecycle/allocations.d.ts +11 -0
  13. package/dist/lifecycle/allocations.d.ts.map +1 -0
  14. package/dist/lifecycle/allocations.js +36 -0
  15. package/dist/lifecycle/allocations.js.map +1 -0
  16. package/dist/lifecycle/calibration.d.ts +19 -0
  17. package/dist/lifecycle/calibration.d.ts.map +1 -0
  18. package/dist/lifecycle/calibration.js +65 -0
  19. package/dist/lifecycle/calibration.js.map +1 -0
  20. package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.d.ts +11 -0
  21. package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.d.ts.map +1 -0
  22. package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.js +10 -0
  23. package/dist/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.js.map +1 -0
  24. package/dist/lifecycle/index.d.ts +4 -2
  25. package/dist/lifecycle/index.d.ts.map +1 -1
  26. package/dist/lifecycle/index.js +4 -2
  27. package/dist/lifecycle/index.js.map +1 -1
  28. package/dist/lifecycle/markets.d.ts +170 -6
  29. package/dist/lifecycle/markets.d.ts.map +1 -1
  30. package/dist/lifecycle/markets.js +164 -37
  31. package/dist/lifecycle/markets.js.map +1 -1
  32. package/dist/lifecycle/planner.d.ts +7 -14
  33. package/dist/lifecycle/planner.d.ts.map +1 -1
  34. package/dist/lifecycle/planner.js +355 -392
  35. package/dist/lifecycle/planner.js.map +1 -1
  36. package/dist/lifecycle/presets.d.ts +62 -0
  37. package/dist/lifecycle/presets.d.ts.map +1 -0
  38. package/dist/lifecycle/presets.js +195 -0
  39. package/dist/lifecycle/presets.js.map +1 -0
  40. package/dist/lifecycle/progress.d.ts +5 -1
  41. package/dist/lifecycle/progress.d.ts.map +1 -1
  42. package/dist/lifecycle/progress.js +286 -118
  43. package/dist/lifecycle/progress.js.map +1 -1
  44. package/dist/lifecycle/rpc.d.ts +13 -2
  45. package/dist/lifecycle/rpc.d.ts.map +1 -1
  46. package/dist/lifecycle/rpc.js +92 -8
  47. package/dist/lifecycle/rpc.js.map +1 -1
  48. package/dist/lifecycle/simulation.d.ts +14 -1
  49. package/dist/lifecycle/simulation.d.ts.map +1 -1
  50. package/dist/lifecycle/simulation.js +400 -100
  51. package/dist/lifecycle/simulation.js.map +1 -1
  52. package/dist/lifecycle/transactions.d.ts +6 -0
  53. package/dist/lifecycle/transactions.d.ts.map +1 -0
  54. package/dist/lifecycle/transactions.js +40 -0
  55. package/dist/lifecycle/transactions.js.map +1 -0
  56. package/dist/lifecycle/types.d.ts +39 -2
  57. package/dist/lifecycle/types.d.ts.map +1 -1
  58. package/dist/lifecycle/types.js.map +1 -1
  59. package/docs/launch-lifecycle.md +236 -39
  60. package/examples/README.md +108 -18
  61. package/examples/generate-lifecycle-commitment-fixture.mjs +3 -1
  62. package/examples/launch-example-support.mjs +1 -1
  63. package/examples/launch-example.mjs +73 -26
  64. package/examples/launch-lifecycle-smoke.mjs +13 -11
  65. package/examples/launch.mjs +12 -11
  66. package/examples/quickstart.mjs +1 -1
  67. package/package.json +1 -1
  68. package/src/addresses.ts +4 -4
  69. package/src/launch.ts +213 -6
  70. package/src/lifecycle/abi.ts +43 -14
  71. package/src/lifecycle/allocations.ts +35 -0
  72. package/src/lifecycle/calibration.ts +59 -0
  73. package/src/lifecycle/deployments/fixed-fee-pool-hook-v1-20261008.ts +11 -0
  74. package/src/lifecycle/index.ts +4 -2
  75. package/src/lifecycle/markets.ts +169 -35
  76. package/src/lifecycle/planner.ts +326 -321
  77. package/src/lifecycle/presets.ts +243 -0
  78. package/src/lifecycle/progress.ts +254 -102
  79. package/src/lifecycle/rpc.ts +97 -9
  80. package/src/lifecycle/simulation.ts +391 -94
  81. package/src/lifecycle/transactions.ts +34 -0
  82. package/src/lifecycle/types.ts +35 -3
@@ -1,4 +1,4 @@
1
- import { concatHex, decodeAbiParameters, encodeAbiParameters, getCreate2Address, keccak256, stringToHex, toHex, zeroHash, type Address, type Hex } from "viem";
1
+ import { concatHex, decodeAbiParameters, encodeAbiParameters, getAddress, hexToBytes, keccak256, pad, stringToHex, toHex, zeroHash, type Address, type Hex } from "viem";
2
2
  import type { MarketConfigV1 } from "./schema.js";
3
3
  import { LifecyclePlanningError, type LifecycleProfile, type PoolBoundHookSaltProgress } from "./types.js";
4
4
 
@@ -36,32 +36,87 @@ export function decodeV4LifecycleMarketConfig(encoded: Hex): V4LifecycleMarketCo
36
36
  return config as V4LifecycleMarketConfig;
37
37
  }
38
38
 
39
- export type V4PoolBoundLifecycleMarketConfig = Omit<V4LifecycleMarketConfig, "version"> & { version: 5; hookSalt: Hex };
39
+ export type PoolBoundLifecycleConfigVersion = 5 | 6;
40
+ export type V4PoolBoundLifecycleMarketConfigV5 = Omit<V4LifecycleMarketConfig, "version"> & {
41
+ version: 5; hookSalt: Hex; minimumHookFeePips?: never; feeSensitivityPipsSecondsPerTick?: never;
42
+ };
43
+ export type V4PoolBoundLifecycleMarketConfigV6 = Omit<V4LifecycleMarketConfig, "version"> & {
44
+ version: 6; minimumHookFeePips: number; feeSensitivityPipsSecondsPerTick: number; hookSalt: Hex;
45
+ };
46
+ export type V4PoolBoundLifecycleMarketConfig = V4PoolBoundLifecycleMarketConfigV5 | V4PoolBoundLifecycleMarketConfigV6;
40
47
  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)[])"));
41
- export const V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA = keccak256(stringToHex("(uint16,uint24,int24,uint160,uint24,uint8,uint8,address,bool,bytes32,bytes32,bytes32,bytes32,address,uint16,(int24,int24,uint128,bytes32,uint256)[])"));
48
+ export const V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA_V5 = keccak256(stringToHex("(uint16,uint24,int24,uint160,uint24,uint8,uint8,address,bool,bytes32,bytes32,bytes32,bytes32,address,uint16,(int24,int24,uint128,bytes32,uint256)[])"));
49
+ export const V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA_V6 = keccak256(stringToHex("(uint16,uint24,int24,uint160,uint24,uint24,uint32,uint8,uint8,address,bool,bytes32,bytes32,bytes32,bytes32,address,uint16,(int24,int24,uint128,bytes32,uint256)[])"));
42
50
  export const V4_POOL_BOUND_MARKET_ECONOMICS_DOMAIN = keccak256(stringToHex("black-market.pool-bound-market-economics.v1"));
43
51
  export const V4_LIFECYCLE_HOOK_PERMISSION_MASK = 0x3fffn;
44
52
  export const V4_LIFECYCLE_HOOK_PERMISSIONS = 0x1afcn;
45
- export const poolBoundV4LifecycleMarketComponents = [
53
+ export const poolBoundV4LifecycleMarketComponentsV5 = [
46
54
  { name: "version", type: "uint16" }, { name: "lpFeePips", type: "uint24" }, { name: "tickSpacing", type: "int24" },
47
55
  { name: "sqrtPriceX96", type: "uint160" }, { name: "hookFeePips", type: "uint24" }, { name: "feeMode", type: "uint8" },
48
56
  { name: "protocolFeeDenominator", type: "uint8" }, { name: "treasury", type: "address" },
49
57
  { name: "externalLiquidityDisabled", type: "bool" }, { name: "oracleConfigId", type: "bytes32" },
58
+ { name: "hookSalt", type: "bytes32" }, { name: "profileId", type: "bytes32" }, { name: "termsDigest", type: "bytes32" },
59
+ { name: "developerBeneficiary", type: "address" }, { name: "developerFeeBps", type: "uint16" },
60
+ { name: "positions", type: "tuple[]", components: v4LifecyclePositionComponents },
61
+ ] as const;
62
+ export const poolBoundV4LifecycleMarketComponentsV6 = [
63
+ { name: "version", type: "uint16" }, { name: "lpFeePips", type: "uint24" }, { name: "tickSpacing", type: "int24" },
64
+ { name: "sqrtPriceX96", type: "uint160" }, { name: "hookFeePips", type: "uint24" },
65
+ { name: "minimumHookFeePips", type: "uint24" }, { name: "feeSensitivityPipsSecondsPerTick", type: "uint32" },
66
+ { name: "feeMode", type: "uint8" },
67
+ { name: "protocolFeeDenominator", type: "uint8" }, { name: "treasury", type: "address" },
68
+ { name: "externalLiquidityDisabled", type: "bool" }, { name: "oracleConfigId", type: "bytes32" },
50
69
  { name: "hookSalt", type: "bytes32" },
51
70
  { name: "profileId", type: "bytes32" }, { name: "termsDigest", type: "bytes32" },
52
71
  { name: "developerBeneficiary", type: "address" }, { name: "developerFeeBps", type: "uint16" },
53
72
  { name: "positions", type: "tuple[]", components: v4LifecyclePositionComponents },
54
73
  ] as const;
55
74
 
75
+ export function isPoolBoundV4ConfigVersion(version: number): version is PoolBoundLifecycleConfigVersion {
76
+ return version === 5 || version === 6;
77
+ }
78
+
79
+ export function poolBoundV4LifecycleConfigSchema(version: PoolBoundLifecycleConfigVersion): Hex {
80
+ if (version === 5) return V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA_V5;
81
+ if (version === 6) return V4_POOL_BOUND_LIFECYCLE_CONFIG_SCHEMA_V6;
82
+ throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", "Pool-bound V4 config version must be explicitly 5 or 6");
83
+ }
84
+
85
+ function assertV1HookFeeShape(config: object): void {
86
+ if ("minimumHookFeePips" in config || "feeSensitivityPipsSecondsPerTick" in config) {
87
+ throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", "Config5/V1 cannot encode config6/V2 fee-policy fields");
88
+ }
89
+ }
90
+
56
91
  export function encodePoolBoundV4LifecycleMarketConfig(config: V4PoolBoundLifecycleMarketConfig): Hex {
57
- if (config.version !== 5) throw new Error("Pool-bound V4 lifecycle market config version must be 5");
92
+ if (!isPoolBoundV4ConfigVersion(config.version)) throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", "Pool-bound V4 config version must be explicitly 5 or 6");
93
+ if (config.version === 6) validatePoolBoundHookFees(config);
94
+ else assertV1HookFeeShape(config);
58
95
  if (!Number.isInteger(config.developerFeeBps) || config.developerFeeBps < 0 || config.developerFeeBps > 65535) throw new Error("developerFeeBps must be an explicit uint16");
59
- return encodeAbiParameters([{ type: "tuple", components: poolBoundV4LifecycleMarketComponents }], [config]);
96
+ return config.version === 5
97
+ ? encodeAbiParameters([{ type: "tuple", components: poolBoundV4LifecycleMarketComponentsV5 }], [config])
98
+ : encodeAbiParameters([{ type: "tuple", components: poolBoundV4LifecycleMarketComponentsV6 }], [config]);
60
99
  }
61
- export function decodePoolBoundV4LifecycleMarketConfig(encoded: Hex): V4PoolBoundLifecycleMarketConfig {
62
- const config = decodeAbiParameters([{ type: "tuple", components: poolBoundV4LifecycleMarketComponents }], encoded)[0];
63
- if (config.version !== 5) throw new Error("Pool-bound V4 lifecycle market config version must be 5");
64
- return config as V4PoolBoundLifecycleMarketConfig;
100
+ export function decodePoolBoundV4LifecycleMarketConfig(encoded: Hex, expectedVersion?: PoolBoundLifecycleConfigVersion): V4PoolBoundLifecycleMarketConfig {
101
+ // Both dynamic config tuples begin with their ABI offset and explicit uint16 version.
102
+ // The selected schema must re-encode byte-for-byte; relabelled or padded tuples are not another version.
103
+ const [offset, version] = decodeAbiParameters([{ type: "uint256" }, { type: "uint16" }], encoded);
104
+ if (!isPoolBoundV4ConfigVersion(version)) throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", "Pool-bound V4 config version must be explicitly 5 or 6");
105
+ if (offset !== 32n || expectedVersion !== undefined && version !== expectedVersion) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Pool-bound wire config does not match its committed version");
106
+ const config = version === 5
107
+ ? decodeAbiParameters([{ type: "tuple", components: poolBoundV4LifecycleMarketComponentsV5 }], encoded)[0] as V4PoolBoundLifecycleMarketConfigV5
108
+ : decodeAbiParameters([{ type: "tuple", components: poolBoundV4LifecycleMarketComponentsV6 }], encoded)[0] as V4PoolBoundLifecycleMarketConfigV6;
109
+ if (encodePoolBoundV4LifecycleMarketConfig(config).toLowerCase() !== encoded.toLowerCase()) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Pool-bound config is not canonical for its explicit schema");
110
+ return config;
111
+ }
112
+
113
+ /** Immutable per-pool fee policy: pips, and fee-pips * seconds per observed tick. */
114
+ export function validatePoolBoundHookFees(config: Pick<PoolBoundHookParametersV2, "hookFeePips" | "minimumHookFeePips" | "feeSensitivityPipsSecondsPerTick">): void {
115
+ if (!Number.isInteger(config.hookFeePips) || config.hookFeePips < 0 || config.hookFeePips > 1000000 ||
116
+ !Number.isInteger(config.minimumHookFeePips) || config.minimumHookFeePips < 0 || config.minimumHookFeePips > config.hookFeePips ||
117
+ !Number.isInteger(config.feeSensitivityPipsSecondsPerTick) || config.feeSensitivityPipsSecondsPerTick < 0 || config.feeSensitivityPipsSecondsPerTick > 0xffffffff) {
118
+ throw new LifecyclePlanningError("INVALID_HOOK_FEES", "Pool-bound fees require integer 0 <= minimum <= maximum <= 1000000 pips and sensitivity between 0 and 4294967295 fee-pips * seconds/tick");
119
+ }
65
120
  }
66
121
 
67
122
  /** Exact pending-source admission terms; author identity is never the live payout address. */
@@ -69,12 +124,15 @@ export function validateV4LifecycleMarket(options: {
69
124
  market: MarketConfigV1; config: V4LifecycleMarketConfig | V4PoolBoundLifecycleMarketConfig; profile: LifecycleProfile; token: Address;
70
125
  }): void {
71
126
  const { market, config, profile, token } = options;
72
- if (config.version !== 4 && config.version !== 5) throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", "Reviewed V4 config version must be 4 or 5");
127
+ if (config.version !== 4 && !isPoolBoundV4ConfigVersion(config.version)) throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", "Reviewed V4 config version must be 4, 5 or 6");
128
+ if (config.version === 6) validatePoolBoundHookFees(config);
129
+ else if (config.version === 5) assertV1HookFeeShape(config);
73
130
  const { envelope, developerTerms: terms, protocolMaximumDeveloperFeeBps: protocolCeiling } = profile;
74
131
  if (envelope === undefined || terms === undefined || protocolCeiling === undefined || !terms.enabled ||
75
132
  profile.topology.hookTopology !== (config.version === 4 ? 1 : 2) || market.configVersion !== config.version ||
76
133
  profile.topology.configVersion !== config.version || profile.adapter.configVersion !== config.version ||
77
134
  envelope.topology !== profile.topology.hookTopology || envelope.configVersion !== config.version || envelope.economicVersion !== 3 ||
135
+ profile.registration.configSchema.toLowerCase() !== (config.version === 4 ? V4_LIFECYCLE_CONFIG_SCHEMA : poolBoundV4LifecycleConfigSchema(config.version)).toLowerCase() ||
78
136
  market.adapterId.toLowerCase() !== profile.registration.adapterId.toLowerCase() ||
79
137
  market.profileId.toLowerCase() !== profile.id.toLowerCase() || config.profileId.toLowerCase() !== profile.id.toLowerCase() ||
80
138
  terms.adapter.toLowerCase() !== profile.adapter.implementation.toLowerCase() ||
@@ -91,7 +149,7 @@ export function validateV4LifecycleMarket(options: {
91
149
  const b = envelope.bounds;
92
150
  if (config.oracleConfigId.toLowerCase() === zeroHash) throw new LifecyclePlanningError("INVALID_ORACLE_CONFIG", "Market oracle configuration must be nonzero");
93
151
  if (config.treasury.toLowerCase() !== envelope.protocolTreasury.toLowerCase() ||
94
- config.protocolFeeDenominator !== envelope.protocolFeeDenominator || !Number.isInteger(config.hookFeePips) || config.hookFeePips < 0 || config.hookFeePips >= 1000000 ||
152
+ config.protocolFeeDenominator !== envelope.protocolFeeDenominator || !Number.isInteger(config.hookFeePips) || config.hookFeePips < 0 || (config.version === 6 ? config.hookFeePips > 1000000 : config.hookFeePips >= 1000000) ||
95
153
  !Number.isInteger(config.lpFeePips) || config.lpFeePips < 0 || config.lpFeePips >= 1000000 ||
96
154
  !Number.isInteger(config.feeMode) || config.feeMode < 0 || config.feeMode > 1 || (b.feeModeFlags & (1 << config.feeMode)) === 0 ||
97
155
  config.tickSpacing < b.minimumTickSpacing || config.tickSpacing > b.maximumTickSpacing ||
@@ -102,10 +160,14 @@ export function validateV4LifecycleMarket(options: {
102
160
 
103
161
  export type PoolBoundHookParametersV1 = {
104
162
  poolManager: Address; registrar: Address; oracleFactory: Address; core: Address; liquidityLocker: Address;
105
- token: Address; quoteCurrency: Address; lpFeePips: number; tickSpacing: number; sqrtPriceX96: bigint;
106
- hookFeePips: number; feeMode: number; protocolFeeDenominator: number; treasury: Address;
163
+ token: Address; quoteCurrency: Address; lpFeePips: number; tickSpacing: number; sqrtPriceX96: bigint; hookFeePips: number;
164
+ feeMode: number; protocolFeeDenominator: number; treasury: Address;
107
165
  externalLiquidityDisabled: boolean; oracleConfigId: Hex; marketCommitment: Hex; expectedPositionCount: number;
108
166
  };
167
+ export type PoolBoundHookParametersV2 = PoolBoundHookParametersV1 & {
168
+ minimumHookFeePips: number; feeSensitivityPipsSecondsPerTick: number;
169
+ };
170
+ export type PoolBoundHookParameters = PoolBoundHookParametersV1 | PoolBoundHookParametersV2;
109
171
  export const poolBoundHookParametersV1Components = [
110
172
  { name: "poolManager", type: "address" }, { name: "registrar", type: "address" },
111
173
  { name: "oracleFactory", type: "address" }, { name: "core", type: "address" },
@@ -117,13 +179,51 @@ export const poolBoundHookParametersV1Components = [
117
179
  { name: "externalLiquidityDisabled", type: "bool" }, { name: "oracleConfigId", type: "bytes32" },
118
180
  { name: "marketCommitment", type: "bytes32" }, { name: "expectedPositionCount", type: "uint32" },
119
181
  ] as const;
120
- export function encodePoolBoundHookParameters(parameters: PoolBoundHookParametersV1): Hex {
121
- return encodeAbiParameters([{ type: "tuple", components: poolBoundHookParametersV1Components }], [parameters]);
182
+ export const poolBoundHookParametersV2Components = [
183
+ { name: "poolManager", type: "address" }, { name: "registrar", type: "address" },
184
+ { name: "oracleFactory", type: "address" }, { name: "core", type: "address" },
185
+ { name: "liquidityLocker", type: "address" }, { name: "token", type: "address" },
186
+ { name: "quoteCurrency", type: "address" }, { name: "lpFeePips", type: "uint24" },
187
+ { name: "tickSpacing", type: "int24" }, { name: "sqrtPriceX96", type: "uint160" },
188
+ { name: "hookFeePips", type: "uint24" },
189
+ { name: "minimumHookFeePips", type: "uint24" }, { name: "feeSensitivityPipsSecondsPerTick", type: "uint32" },
190
+ { name: "feeMode", type: "uint8" },
191
+ { name: "protocolFeeDenominator", type: "uint8" }, { name: "treasury", type: "address" },
192
+ { name: "externalLiquidityDisabled", type: "bool" }, { name: "oracleConfigId", type: "bytes32" },
193
+ { name: "marketCommitment", type: "bytes32" }, { name: "expectedPositionCount", type: "uint32" },
194
+ ] as const;
195
+ export function encodePoolBoundHookParameters(parameters: PoolBoundHookParameters, configVersion: PoolBoundLifecycleConfigVersion): Hex {
196
+ if (configVersion === 5) {
197
+ assertV1HookFeeShape(parameters);
198
+ return encodeAbiParameters([{ type: "tuple", components: poolBoundHookParametersV1Components }], [parameters]);
199
+ }
200
+ if (configVersion !== 6) throw new LifecyclePlanningError("UNSUPPORTED_CONFIG_VERSION", "Constructor encoding requires explicit config5/V1 or config6/V2 context");
201
+ const v2 = parameters as PoolBoundHookParametersV2;
202
+ validatePoolBoundHookFees(v2);
203
+ return encodeAbiParameters([{ type: "tuple", components: poolBoundHookParametersV2Components }], [v2]);
204
+ }
205
+
206
+ /** The explicit market version selects constructor economics, never missing-field defaults. */
207
+ export function buildPoolBoundHookParameters(options: {
208
+ chainId: bigint; core: Address; poolManager: Address; registrar: Address; oracleFactory: Address;
209
+ liquidityLocker: Address; token: Address; market: MarketConfigV1; config: V4PoolBoundLifecycleMarketConfig;
210
+ }): PoolBoundHookParameters {
211
+ const { config, market } = options;
212
+ if (config.version !== market.configVersion) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Constructor config differs from the committed market version");
213
+ return {
214
+ poolManager: options.poolManager, registrar: options.registrar, oracleFactory: options.oracleFactory, core: options.core,
215
+ liquidityLocker: options.liquidityLocker, token: options.token, quoteCurrency: market.quoteAsset,
216
+ lpFeePips: config.lpFeePips, tickSpacing: config.tickSpacing, sqrtPriceX96: config.sqrtPriceX96, hookFeePips: config.hookFeePips,
217
+ ...(config.version === 6 ? { minimumHookFeePips: config.minimumHookFeePips, feeSensitivityPipsSecondsPerTick: config.feeSensitivityPipsSecondsPerTick } : {}),
218
+ feeMode: config.feeMode, protocolFeeDenominator: config.protocolFeeDenominator, treasury: config.treasury,
219
+ externalLiquidityDisabled: config.externalLiquidityDisabled, oracleConfigId: config.oracleConfigId,
220
+ marketCommitment: hashPoolBoundV4MarketCommitment(options), expectedPositionCount: config.positions.length,
221
+ };
122
222
  }
123
223
  export function hashPoolBoundV4MarketCommitment(options: { chainId: bigint; core: Address; registrar: Address; token: Address; market: MarketConfigV1 }): Hex {
124
224
  const { chainId, core, registrar, token, market } = options;
125
- if (market.configVersion !== 5) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Pool-bound V4 market requires reviewed config version 5");
126
- const config = decodePoolBoundV4LifecycleMarketConfig(market.config);
225
+ if (!isPoolBoundV4ConfigVersion(market.configVersion)) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Pool-bound V4 market requires explicit config version 5 or 6");
226
+ const config = decodePoolBoundV4LifecycleMarketConfig(market.config, market.configVersion);
127
227
  if (config.profileId.toLowerCase() !== market.profileId.toLowerCase()) throw new LifecyclePlanningError("INVALID_BOUND_MARKET", "Config profile differs from the committed market profile");
128
228
  const configHash = keccak256(encodePoolBoundV4LifecycleMarketConfig({ ...config, hookSalt: zeroHash }));
129
229
  return keccak256(encodeAbiParameters([
@@ -134,11 +234,26 @@ export function hashPoolBoundV4MarketCommitment(options: { chainId: bigint; core
134
234
  export function hasLifecycleV4HookPermissions(hook: Address): boolean {
135
235
  return (BigInt(hook) & V4_LIFECYCLE_HOOK_PERMISSION_MASK) === V4_LIFECYCLE_HOOK_PERMISSIONS;
136
236
  }
237
+ function poolBoundHookPreimage(deployer: Address, initCodeHash: Hex): Uint8Array {
238
+ const codeHash = hexToBytes(initCodeHash);
239
+ const preimage = new Uint8Array(53 + codeHash.length);
240
+ preimage[0] = 0xff;
241
+ preimage.set(hexToBytes(getAddress(deployer)), 1);
242
+ preimage.set(codeHash, 53);
243
+ return preimage;
244
+ }
245
+
246
+ function poolBoundHookAddress(digest: Uint8Array): Address {
247
+ return getAddress(toHex(digest.subarray(12)));
248
+ }
249
+
137
250
  export function predictPoolBoundHookAddress(options: { deployer: Address; initCodeHash: Hex; salt: Hex }): Address {
138
- return getCreate2Address({ from: options.deployer, bytecodeHash: options.initCodeHash, salt: options.salt });
251
+ const preimage = poolBoundHookPreimage(options.deployer, options.initCodeHash);
252
+ preimage.set(pad(hexToBytes(options.salt), { size: 32 }), 21);
253
+ return poolBoundHookAddress(keccak256(preimage, "bytes"));
139
254
  }
140
- export function poolBoundHookInitCodeHash(creationCode: Hex, parameters: PoolBoundHookParametersV1): Hex {
141
- return keccak256(concatHex([creationCode, encodePoolBoundHookParameters(parameters)]));
255
+ export function poolBoundHookInitCodeHash(creationCode: Hex, parameters: PoolBoundHookParameters, configVersion: PoolBoundLifecycleConfigVersion): Hex {
256
+ return keccak256(concatHex([creationCode, encodePoolBoundHookParameters(parameters, configVersion)]));
142
257
  }
143
258
 
144
259
  /** Deterministic CREATE2 search with a macrotask boundary before work and between bounded batches. */
@@ -147,36 +262,55 @@ export async function minePoolBoundHookSalt(options: {
147
262
  onProgress?: (progress: PoolBoundHookSaltProgress) => void;
148
263
  }): Promise<{ salt: Hex; predictedHook: Address }> {
149
264
  const { signal, onProgress } = options;
150
- let candidate = options.startSalt ?? 0n;
265
+ const candidate = options.startSalt ?? 0n;
151
266
  if (candidate < 0n || candidate >= 1n << 256n) throw new LifecyclePlanningError("INVALID_HOOK_SALT", "Starting hook salt must fit uint256");
152
267
  const checkCancelled = () => {
153
268
  if (signal?.aborted) throw Object.assign(new Error("Pool-bound hook salt mining cancelled"), { name: "AbortError" });
154
269
  };
155
270
  let attempts = 0n;
156
271
  checkCancelled();
157
- const initialSalt = toHex(candidate, { size: 32 });
158
- onProgress?.({ attempts, salt: initialSalt, predictedHook: predictPoolBoundHookAddress({ deployer: options.deployer, initCodeHash: options.initCodeHash, salt: initialSalt }) });
272
+ // The same EIP-1014 owner as the public predictor freezes immutable prefix/
273
+ // suffix once. Only uint256 salt bytes change; viem still owns exact Keccak.
274
+ const preimage = poolBoundHookPreimage(options.deployer, options.initCodeHash);
275
+ const saltBytes = preimage.subarray(21, 53);
276
+ saltBytes.set(hexToBytes(toHex(candidate, { size: 32 })));
277
+ const mask = Number(V4_LIFECYCLE_HOOK_PERMISSION_MASK);
278
+ const permissions = Number(V4_LIFECYCLE_HOOK_PERMISSIONS);
279
+ let firstDigest = onProgress === undefined ? undefined : keccak256(preimage, "bytes");
280
+ if (onProgress !== undefined) onProgress({ attempts, salt: toHex(saltBytes), predictedHook: poolBoundHookAddress(firstDigest!) });
281
+ let advanceSalt = false;
159
282
  for (;;) {
160
283
  checkCancelled();
161
284
  await new Promise<void>((resolve) => globalThis.setTimeout(resolve, 0));
162
285
  checkCancelled();
163
286
  const deadline = Date.now() + 8;
164
- let last: PoolBoundHookSaltProgress | undefined;
287
+ let lastDigest: Uint8Array | undefined;
288
+ let batchAttempts = 0;
165
289
  for (let batch = 0; batch < 256 && (batch === 0 || Date.now() < deadline); batch += 1) {
166
290
  checkCancelled();
167
- if (candidate >= 1n << 256n) throw new LifecyclePlanningError("HOOK_SALT_EXHAUSTED", "No valid hook address remains in the uint256 salt range");
168
- const salt = toHex(candidate, { size: 32 });
169
- const predictedHook = predictPoolBoundHookAddress({ deployer: options.deployer, initCodeHash: options.initCodeHash, salt });
170
- attempts += 1n;
171
- last = { attempts, salt, predictedHook };
172
- if (hasLifecycleV4HookPermissions(predictedHook)) {
173
- onProgress?.(last);
291
+ if (advanceSalt) {
292
+ let byte = saltBytes.length - 1;
293
+ while (byte >= 0 && saltBytes[byte] === 0xff) { saltBytes[byte] = 0; byte -= 1; }
294
+ if (byte < 0) throw new LifecyclePlanningError("HOOK_SALT_EXHAUSTED", "No valid hook address remains in the uint256 salt range");
295
+ saltBytes[byte] = saltBytes[byte]! + 1;
296
+ }
297
+ const digest = firstDigest ?? keccak256(preimage, "bytes");
298
+ firstDigest = undefined;
299
+ batchAttempts += 1;
300
+ // Check all fourteen permission bits without checksumming/materializing
301
+ // every rejected address or allocating a progress object per attempt.
302
+ if ((((digest[30]! << 8) | digest[31]!) & mask) === permissions) {
303
+ attempts += BigInt(batchAttempts);
304
+ const result = { salt: toHex(saltBytes), predictedHook: poolBoundHookAddress(digest) };
305
+ onProgress?.({ attempts, ...result });
174
306
  checkCancelled();
175
- return { salt, predictedHook };
307
+ return result;
176
308
  }
177
- candidate += 1n;
309
+ lastDigest = digest;
310
+ advanceSalt = true;
178
311
  }
179
- if (last !== undefined) onProgress?.(last);
312
+ attempts += BigInt(batchAttempts);
313
+ if (onProgress !== undefined && lastDigest !== undefined) onProgress({ attempts, salt: toHex(saltBytes), predictedHook: poolBoundHookAddress(lastDigest) });
180
314
  }
181
315
  }
182
316