@hypurrquant/defi-cli 0.5.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -1,12 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
- }) : x)(function(x) {
7
- if (typeof require !== "undefined") return require.apply(this, arguments);
8
- throw Error('Dynamic require of "' + x + '" is not supported');
9
- });
10
4
  var __esm = (fn, res) => function __init() {
11
5
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
6
  };
@@ -230,6 +224,7 @@ var init_dist = __esm({
230
224
  native_token;
231
225
  wrapped_native;
232
226
  multicall3;
227
+ aggregators;
233
228
  effectiveRpcUrl() {
234
229
  const chainEnv = this.name.toUpperCase().replace(/ /g, "_") + "_RPC_URL";
235
230
  return process.env[chainEnv] ?? process.env["HYPEREVM_RPC_URL"] ?? this.rpc_url;
@@ -332,7 +327,7 @@ var init_dist = __esm({
332
327
  }
333
328
  getProtocolsForChain(chain, includeUnverified = false) {
334
329
  return this.protocols.filter(
335
- (p) => p.chain.toLowerCase() === chain.toLowerCase() && (includeUnverified || p.verified !== false)
330
+ (p) => p.chain.toLowerCase() === chain.toLowerCase() && (includeUnverified || p.verified !== false) && p.is_active !== false
336
331
  );
337
332
  }
338
333
  resolveToken(chain, symbol) {
@@ -394,6 +389,7 @@ __export(dist_exports, {
394
389
  MasterChefAdapter: () => MasterChefAdapter,
395
390
  MerchantMoeLBAdapter: () => MerchantMoeLBAdapter,
396
391
  MorphoBlueAdapter: () => MorphoBlueAdapter,
392
+ NestOffChainAdapter: () => NestOffChainAdapter,
397
393
  PendleAdapter: () => PendleAdapter,
398
394
  RyskAdapter: () => RyskAdapter,
399
395
  SolidlyAdapter: () => SolidlyAdapter,
@@ -412,10 +408,12 @@ __export(dist_exports, {
412
408
  createLiquidStaking: () => createLiquidStaking,
413
409
  createMasterChef: () => createMasterChef,
414
410
  createMerchantMoeLB: () => createMerchantMoeLB,
411
+ createNestOffChain: () => createNestOffChain,
415
412
  createNft: () => createNft,
416
413
  createOptions: () => createOptions,
417
414
  createOracleFromCdp: () => createOracleFromCdp,
418
415
  createOracleFromLending: () => createOracleFromLending,
416
+ createRewardReader: () => createRewardReader,
419
417
  createVault: () => createVault,
420
418
  createYieldSource: () => createYieldSource
421
419
  });
@@ -656,8 +654,8 @@ function encodeClaimReward(rewardToken, to) {
656
654
  return encodeFunctionData13({
657
655
  abi: farmingCenterAbi,
658
656
  functionName: "claimReward",
659
- args: [rewardToken, to, 2n ** 128n - 1n]
660
- // max uint128
657
+ args: [rewardToken, to, 2n ** 256n - 1n]
658
+ // max uint256 (KittenSwap variant uses uint256 amount, not uint128)
661
659
  });
662
660
  }
663
661
  function encodeMulticall(calls) {
@@ -900,6 +898,46 @@ function createOracleFromCdp(entry, _asset, rpcUrl) {
900
898
  function createMerchantMoeLB(entry, rpcUrl) {
901
899
  return new MerchantMoeLBAdapter(entry, rpcUrl);
902
900
  }
901
+ function createNestOffChain(entry) {
902
+ return new NestOffChainAdapter(entry);
903
+ }
904
+ function createRewardReader(entry, rpcUrl, tokens) {
905
+ const strategy = entry.reward_strategy ?? inferRewardStrategy(entry);
906
+ switch (strategy) {
907
+ case "off_chain_api":
908
+ return { kind: "off_chain_api", adapter: new NestOffChainAdapter(entry) };
909
+ case "on_chain_farming_center":
910
+ if (!rpcUrl) throw DefiError.invalidParam("createRewardReader: rpcUrl required for on_chain_farming_center");
911
+ return { kind: "on_chain_farming_center", adapter: createKittenSwapFarming(entry, rpcUrl) };
912
+ case "on_chain_gauge_tokenid":
913
+ return { kind: "on_chain_gauge_tokenid", adapter: new HybraGaugeAdapter(entry, rpcUrl) };
914
+ case "on_chain_gauge":
915
+ return { kind: "on_chain_gauge", adapter: new SolidlyGaugeAdapter(entry, rpcUrl, tokens) };
916
+ case "auto_stake":
917
+ return { kind: "auto_stake", adapter: new SolidlyGaugeAdapter(entry, rpcUrl, tokens) };
918
+ case "on_chain_masterchef":
919
+ return { kind: "on_chain_masterchef", adapter: new MasterChefAdapter(entry, rpcUrl) };
920
+ case "none":
921
+ return { kind: "none" };
922
+ default:
923
+ throw DefiError.unsupported(`Unknown reward_strategy '${strategy}' on '${entry.slug}'`);
924
+ }
925
+ }
926
+ function inferRewardStrategy(entry) {
927
+ if (entry.interface === "hybra" || entry.contracts?.["gauge_manager"]) {
928
+ return "on_chain_gauge_tokenid";
929
+ }
930
+ if (entry.contracts?.["farming_center"] && entry.contracts?.["eternal_farming"]) {
931
+ return "on_chain_farming_center";
932
+ }
933
+ if (entry.contracts?.["voter"]) {
934
+ return "on_chain_gauge";
935
+ }
936
+ if (entry.contracts?.["master_chef"] || entry.contracts?.["masterChef"]) {
937
+ return "on_chain_masterchef";
938
+ }
939
+ return "none";
940
+ }
903
941
  function createKittenSwapFarming(entry, rpcUrl) {
904
942
  const farmingCenter = entry.contracts?.["farming_center"];
905
943
  if (!farmingCenter) {
@@ -914,9 +952,11 @@ function createKittenSwapFarming(entry, rpcUrl) {
914
952
  throw new DefiError("CONTRACT_ERROR", `[${entry.name}] Missing 'position_manager' contract address`);
915
953
  }
916
954
  const factory = entry.contracts?.["factory"];
917
- return new KittenSwapFarmingAdapter(entry.name, farmingCenter, eternalFarming, positionManager, rpcUrl, factory);
955
+ const rewardToken = entry.contracts?.["reward_token"];
956
+ const bonusRewardToken = entry.contracts?.["bonus_reward_token"];
957
+ return new KittenSwapFarmingAdapter(entry.name, farmingCenter, eternalFarming, positionManager, rpcUrl, factory, rewardToken, bonusRewardToken);
918
958
  }
919
- var DEFAULT_FEE, swapRouterAbi, quoterAbi, ramsesQuoterAbi, positionManagerAbi, UniswapV3Adapter, abi, lbQuoterAbi, UniswapV2Adapter, abi2, algebraQuoterAbi, algebraSingleQuoterAbi, algebraIntegralPmAbi, algebraV2PmAbi, AlgebraV3Adapter, abi3, BalancerV3Adapter, poolAbi, CurveStableSwapAdapter, abi4, abiV2, SolidlyAdapter, thenaPmAbi, thenaRouterAbi, thenaPoolAbi, thenaFactoryAbi, ThenaCLAdapter, _addressDecodeAbi, _symbolDecodeAbi, gaugeManagerAbi, gaugeCLAbi, nfpmAbi, veAbi, voterAbi, HybraGaugeAdapter, abi5, WooFiAdapter, gaugeAbi, veAbi2, voterAbi2, _addressDecodeAbi2, _symbolDecodeAbi2, _boolDecodeAbi, HYPEREVM_TOKENS, CL_TICK_SPACINGS, SolidlyGaugeAdapter, masterchefAbi, MasterChefAdapter, lbRouterAbi, lbFactoryAbi, lbPairAbi, lbRewarderAbi, masterChefAbi, veMoeAbi, lbPairBinAbi, lbQuoterAbi2, erc20Abi2, _addressAbi, _uint256Abi, _boolAbi, _rangeAbi, _binAbi, _uint256ArrayAbi, MerchantMoeLBAdapter, KITTEN_TOKEN, WHYPE_TOKEN, MAX_NONCE_SCAN, HYPEREVM_TOKENS2, farmingCenterAbi, positionManagerAbi2, eternalFarmingAbi, algebraFactoryAbi, _addressDecodeAbi3, nonceCache, KittenSwapFarmingAdapter, POOL_ABI, ERC20_ABI2, INCENTIVES_ABI, REWARDS_CONTROLLER_ABI, POOL_PROVIDER_ABI, ADDRESSES_PROVIDER_ABI, ORACLE_ABI, ERC20_DECIMALS_ABI, AaveV3Adapter, POOL_ABI2, ERC20_ABI22, AaveV2Adapter, ORACLE_ABI2, AaveOracleAdapter, CTOKEN_ABI, BSC_BLOCKS_PER_YEAR, CompoundV2Adapter, COMET_ABI, SECONDS_PER_YEAR, CompoundV3Adapter, EULER_VAULT_ABI, SECONDS_PER_YEAR2, EulerV2Adapter, MORPHO_ABI, META_MORPHO_ABI, IRM_ABI, SECONDS_PER_YEAR3, MorphoBlueAdapter, BORROWER_OPS_ABI, TROVE_MANAGER_ABI, HINT_HELPERS_ABI, SORTED_TROVES_ABI, FelixCdpAdapter, PRICE_FEED_ABI, FelixOracleAdapter, ERC4626_ABI, ERC4626VaultAdapter, GENERIC_LST_ABI, GenericLstAdapter, STHYPE_ABI, ERC20_ABI3, StHypeAdapter, KINETIQ_ABI, ORACLE_ABI3, WHYPE, HYPERLEND_ORACLE, KinetiqAdapter, PendleAdapter, GenericYieldAdapter, HLP_ABI, HlpVaultAdapter, GenericDerivativesAdapter, RYSK_ABI, RyskAdapter, GenericOptionsAdapter, ERC721_ABI, ERC721Adapter, DexSpotPrice;
959
+ var DEFAULT_FEE, swapRouterAbi, quoterAbi, ramsesQuoterAbi, positionManagerAbi, slipstreamMintAbi, UniswapV3Adapter, abi, lbQuoterAbi, UniswapV2Adapter, abi2, algebraQuoterAbi, algebraSingleQuoterAbi, algebraIntegralPmAbi, algebraV2PmAbi, algebraSharedPmAbi, AlgebraV3Adapter, abi3, BalancerV3Adapter, poolAbi, CurveStableSwapAdapter, abi4, abiV2, SolidlyAdapter, thenaPmAbi, thenaRouterAbi, thenaPoolAbi, thenaFactoryAbi, ThenaCLAdapter, _addressDecodeAbi, _symbolDecodeAbi, gaugeManagerAbi, gaugeCLAbi, nfpmAbi, veAbi, voterAbi, HybraGaugeAdapter, abi5, WooFiAdapter, gaugeAbi, veAbi2, voterAbi2, _addressDecodeAbi2, _symbolDecodeAbi2, _boolDecodeAbi, HYPEREVM_TOKENS, CL_TICK_SPACINGS, SolidlyGaugeAdapter, masterchefAbi, MasterChefAdapter, lbRouterAbi, lbFactoryAbi, lbPairAbi, lbRewarderAbi, masterChefAbi, veMoeAbi, lbPairBinAbi, lbQuoterAbi2, erc20Abi2, _addressAbi, _uint256Abi, _boolAbi, _rangeAbi, _binAbi, _uint256ArrayAbi, MerchantMoeLBAdapter, KITTEN_TOKEN, WHYPE_TOKEN, MAX_NONCE_SCAN, HYPEREVM_TOKENS2, farmingCenterAbi, positionManagerAbi2, eternalFarmingAbi, algebraFactoryAbi, _addressDecodeAbi3, nonceCache, KittenSwapFarmingAdapter, DEFAULT_BASE_URL, FALLBACK_BASE_URL, NEST_TOKEN, NEST_DECIMALS, NestOffChainAdapter, POOL_ABI, ERC20_ABI2, INCENTIVES_ABI, REWARDS_CONTROLLER_ABI, POOL_PROVIDER_ABI, ADDRESSES_PROVIDER_ABI, ORACLE_ABI, ERC20_DECIMALS_ABI, AaveV3Adapter, POOL_ABI2, ERC20_ABI22, AaveV2Adapter, ORACLE_ABI2, AaveOracleAdapter, CTOKEN_ABI, BSC_BLOCKS_PER_YEAR, CompoundV2Adapter, COMET_ABI, SECONDS_PER_YEAR, CompoundV3Adapter, EULER_VAULT_ABI, SECONDS_PER_YEAR2, EulerV2Adapter, MORPHO_ABI, META_MORPHO_ABI, ERC4626_ABI, MAX_UINT256, IRM_ABI, SECONDS_PER_YEAR3, MorphoBlueAdapter, BORROWER_OPS_ABI, TROVE_MANAGER_ABI, HINT_HELPERS_ABI, SORTED_TROVES_ABI, FelixCdpAdapter, PRICE_FEED_ABI, FelixOracleAdapter, ERC4626_ABI2, ERC4626VaultAdapter, GENERIC_LST_ABI, GenericLstAdapter, STHYPE_ABI, ERC20_ABI3, StHypeAdapter, KINETIQ_ABI, ORACLE_ABI3, WHYPE, HYPERLEND_ORACLE, KinetiqAdapter, PendleAdapter, GenericYieldAdapter, HLP_ABI, HlpVaultAdapter, GenericDerivativesAdapter, RYSK_ABI, RyskAdapter, GenericOptionsAdapter, ERC721_ABI, ERC721Adapter, DexSpotPrice;
920
960
  var init_dist2 = __esm({
921
961
  "../defi-protocols/dist/index.js"() {
922
962
  "use strict";
@@ -954,6 +994,7 @@ var init_dist2 = __esm({
954
994
  init_dist();
955
995
  init_dist();
956
996
  init_dist();
997
+ init_dist();
957
998
  DEFAULT_FEE = 3e3;
958
999
  swapRouterAbi = parseAbi4([
959
1000
  "struct ExactInputSingleParams { address tokenIn; address tokenOut; uint24 fee; address recipient; uint256 deadline; uint256 amountIn; uint256 amountOutMinimum; uint160 sqrtPriceLimitX96; }",
@@ -969,7 +1010,19 @@ var init_dist2 = __esm({
969
1010
  ]);
970
1011
  positionManagerAbi = parseAbi4([
971
1012
  "struct MintParams { address token0; address token1; uint24 fee; int24 tickLower; int24 tickUpper; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; uint256 amount1Min; address recipient; uint256 deadline; }",
972
- "function mint(MintParams calldata params) external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)"
1013
+ "function mint(MintParams calldata params) external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)",
1014
+ "struct CollectParams { uint256 tokenId; address recipient; uint128 amount0Max; uint128 amount1Max; }",
1015
+ "function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1)",
1016
+ "struct DecreaseLiquidityParams { uint256 tokenId; uint128 liquidity; uint256 amount0Min; uint256 amount1Min; uint256 deadline; }",
1017
+ "function decreaseLiquidity(DecreaseLiquidityParams calldata params) external payable returns (uint256 amount0, uint256 amount1)",
1018
+ "struct IncreaseLiquidityParams { uint256 tokenId; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; uint256 amount1Min; uint256 deadline; }",
1019
+ "function increaseLiquidity(IncreaseLiquidityParams calldata params) external payable returns (uint128 liquidity, uint256 amount0, uint256 amount1)",
1020
+ "function positions(uint256 tokenId) external view returns (uint96 nonce, address operator, address token0, address token1, uint24 fee, int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint128 tokensOwed0, uint128 tokensOwed1)",
1021
+ "function multicall(bytes[] data) external payable returns (bytes[] memory results)"
1022
+ ]);
1023
+ slipstreamMintAbi = parseAbi4([
1024
+ "struct SlipstreamMintParams { address token0; address token1; int24 tickSpacing; int24 tickLower; int24 tickUpper; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; uint256 amount1Min; address recipient; uint256 deadline; uint160 sqrtPriceX96; }",
1025
+ "function mint(SlipstreamMintParams calldata params) external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)"
973
1026
  ]);
974
1027
  UniswapV3Adapter = class {
975
1028
  protocolName;
@@ -992,7 +1045,7 @@ var init_dist2 = __esm({
992
1045
  this.factory = entry.contracts?.["factory"];
993
1046
  this.fee = DEFAULT_FEE;
994
1047
  this.rpcUrl = rpcUrl;
995
- this.useTickSpacingQuoter = entry.contracts?.["pool_deployer"] !== void 0 || entry.contracts?.["gauge_factory"] !== void 0;
1048
+ this.useTickSpacingQuoter = entry.cl_style === "slipstream" || entry.cl_style === "ramses" || entry.contracts?.["pool_deployer"] !== void 0 || entry.contracts?.["gauge_factory"] !== void 0;
996
1049
  }
997
1050
  name() {
998
1051
  return this.protocolName;
@@ -1168,16 +1221,64 @@ var init_dist2 = __esm({
1168
1221
  const [token0, token1, rawAmount0, rawAmount1] = params.token_a.toLowerCase() < params.token_b.toLowerCase() ? [params.token_a, params.token_b, params.amount_a, params.amount_b] : [params.token_b, params.token_a, params.amount_b, params.amount_a];
1169
1222
  const amount0 = rawAmount0 === 0n && rawAmount1 > 0n ? 1n : rawAmount0;
1170
1223
  const amount1 = rawAmount1 === 0n && rawAmount0 > 0n ? 1n : rawAmount1;
1171
- const data = encodeFunctionData4({
1224
+ let thirdField = this.fee;
1225
+ let tickLower = -887220;
1226
+ let tickUpper = 887220;
1227
+ if (params.pool && this.rpcUrl) {
1228
+ const poolAbi2 = parseAbi4([
1229
+ "function fee() view returns (uint24)",
1230
+ "function tickSpacing() view returns (int24)",
1231
+ "function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16, uint16, uint16, bool)"
1232
+ ]);
1233
+ const client = createPublicClient4({ transport: http4(this.rpcUrl) });
1234
+ const [poolFee, poolTs, slot0] = await Promise.all([
1235
+ client.readContract({ address: params.pool, abi: poolAbi2, functionName: "fee" }).catch(() => null),
1236
+ client.readContract({ address: params.pool, abi: poolAbi2, functionName: "tickSpacing" }).catch(() => null),
1237
+ client.readContract({ address: params.pool, abi: poolAbi2, functionName: "slot0" }).catch(() => null)
1238
+ ]);
1239
+ if (this.useTickSpacingQuoter && poolTs !== null) {
1240
+ thirdField = poolTs;
1241
+ } else if (poolFee !== null) {
1242
+ thirdField = poolFee;
1243
+ }
1244
+ if (params.range_pct !== void 0 && slot0 && poolTs !== null) {
1245
+ const currentTick = slot0[1];
1246
+ const rangeTicks = Math.floor(params.range_pct * 100);
1247
+ tickLower = Math.floor((currentTick - rangeTicks) / poolTs) * poolTs;
1248
+ tickUpper = Math.ceil((currentTick + rangeTicks) / poolTs) * poolTs;
1249
+ }
1250
+ }
1251
+ if (params.tick_lower !== void 0) tickLower = params.tick_lower;
1252
+ if (params.tick_upper !== void 0) tickUpper = params.tick_upper;
1253
+ const data = this.useTickSpacingQuoter ? encodeFunctionData4({
1254
+ abi: slipstreamMintAbi,
1255
+ functionName: "mint",
1256
+ args: [
1257
+ {
1258
+ token0,
1259
+ token1,
1260
+ tickSpacing: thirdField,
1261
+ tickLower,
1262
+ tickUpper,
1263
+ amount0Desired: amount0,
1264
+ amount1Desired: amount1,
1265
+ amount0Min: 0n,
1266
+ amount1Min: 0n,
1267
+ recipient: params.recipient,
1268
+ deadline: BigInt("18446744073709551615"),
1269
+ sqrtPriceX96: 0n
1270
+ }
1271
+ ]
1272
+ }) : encodeFunctionData4({
1172
1273
  abi: positionManagerAbi,
1173
1274
  functionName: "mint",
1174
1275
  args: [
1175
1276
  {
1176
1277
  token0,
1177
1278
  token1,
1178
- fee: this.fee,
1179
- tickLower: -887220,
1180
- tickUpper: 887220,
1279
+ fee: thirdField,
1280
+ tickLower,
1281
+ tickUpper,
1181
1282
  amount0Desired: amount0,
1182
1283
  amount1Desired: amount1,
1183
1284
  amount0Min: 0n,
@@ -1199,10 +1300,125 @@ var init_dist2 = __esm({
1199
1300
  ]
1200
1301
  };
1201
1302
  }
1202
- async buildRemoveLiquidity(_params) {
1203
- throw DefiError.unsupported(
1204
- `[${this.protocolName}] remove_liquidity requires tokenId \u2014 use NFT position manager directly`
1205
- );
1303
+ async buildRemoveLiquidity(params) {
1304
+ const pm = this.positionManager;
1305
+ if (!pm) {
1306
+ throw DefiError.contractError(
1307
+ `[${this.protocolName}] Missing 'position_manager' for liquidity removal`
1308
+ );
1309
+ }
1310
+ if (!params.token_id) {
1311
+ throw DefiError.invalidParam(
1312
+ `[${this.protocolName}] V3 remove_liquidity requires --token-id (NFT positionId)`
1313
+ );
1314
+ }
1315
+ const tokenId = params.token_id;
1316
+ const liquidity = params.liquidity;
1317
+ const MAX_UINT128 = (1n << 128n) - 1n;
1318
+ const deadline = BigInt("18446744073709551615");
1319
+ const decreaseData = encodeFunctionData4({
1320
+ abi: positionManagerAbi,
1321
+ functionName: "decreaseLiquidity",
1322
+ args: [{ tokenId, liquidity, amount0Min: 0n, amount1Min: 0n, deadline }]
1323
+ });
1324
+ const collectData = encodeFunctionData4({
1325
+ abi: positionManagerAbi,
1326
+ functionName: "collect",
1327
+ args: [{ tokenId, recipient: params.recipient, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128 }]
1328
+ });
1329
+ const data = encodeFunctionData4({
1330
+ abi: positionManagerAbi,
1331
+ functionName: "multicall",
1332
+ args: [[decreaseData, collectData]]
1333
+ });
1334
+ return {
1335
+ description: `[${this.protocolName}] Remove ${liquidity} liquidity from tokenId ${tokenId}`,
1336
+ to: pm,
1337
+ data,
1338
+ value: 0n,
1339
+ gas_estimate: 4e5
1340
+ };
1341
+ }
1342
+ /**
1343
+ * Collect accrued LP trading fees for a CL position via NPM.collect().
1344
+ * Used as the reward path for V3 forks with reward_strategy = "lp_fee_only"
1345
+ * (e.g., HyperSwap V3, Project X — no gauge/emissions, fees are the only reward).
1346
+ */
1347
+ async buildCollectFees(tokenId, recipient) {
1348
+ const pm = this.positionManager;
1349
+ if (!pm) {
1350
+ throw DefiError.contractError(
1351
+ `[${this.protocolName}] Missing 'position_manager' for fee collection`
1352
+ );
1353
+ }
1354
+ const MAX_UINT128 = (1n << 128n) - 1n;
1355
+ const data = encodeFunctionData4({
1356
+ abi: positionManagerAbi,
1357
+ functionName: "collect",
1358
+ args: [{ tokenId, recipient, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128 }]
1359
+ });
1360
+ return {
1361
+ description: `[${this.protocolName}] Collect LP fees for tokenId ${tokenId}`,
1362
+ to: pm,
1363
+ data,
1364
+ value: 0n,
1365
+ gas_estimate: 2e5
1366
+ };
1367
+ }
1368
+ /**
1369
+ * Compound: collect accrued fees and immediately re-add them as liquidity to the same position.
1370
+ * Flow: static-call collect to learn fee amounts → multicall([collect, increaseLiquidity]) on NPM.
1371
+ * Requires existing token approvals on the NPM (set during initial mint).
1372
+ * v1: V3 fee-only protocols (Project X, HyperSwap V3). Gauge protocols need swap routing first.
1373
+ */
1374
+ async buildCompound(tokenId, recipient, opts) {
1375
+ const pm = this.positionManager;
1376
+ if (!pm) {
1377
+ throw DefiError.contractError(`[${this.protocolName}] Missing 'position_manager' for compound`);
1378
+ }
1379
+ if (!this.rpcUrl) throw DefiError.rpcError("RPC required to preview fees");
1380
+ const MAX_UINT128 = (1n << 128n) - 1n;
1381
+ const deadline = BigInt("18446744073709551615");
1382
+ const slippageBps = BigInt(opts?.slippageBps ?? 50);
1383
+ if (slippageBps > 10000n) {
1384
+ throw DefiError.invalidParam(`[${this.protocolName}] slippageBps must be <= 10000 (got ${slippageBps})`);
1385
+ }
1386
+ const client = createPublicClient4({ transport: http4(this.rpcUrl) });
1387
+ const sim = await client.simulateContract({
1388
+ address: pm,
1389
+ abi: positionManagerAbi,
1390
+ functionName: "collect",
1391
+ args: [{ tokenId, recipient, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128 }],
1392
+ account: recipient
1393
+ });
1394
+ const [amount0, amount1] = sim.result;
1395
+ if (amount0 === 0n && amount1 === 0n) {
1396
+ throw DefiError.invalidParam(`[${this.protocolName}] No fees to compound for tokenId ${tokenId}`);
1397
+ }
1398
+ const amount0Min = amount0 * (10000n - slippageBps) / 10000n;
1399
+ const amount1Min = amount1 * (10000n - slippageBps) / 10000n;
1400
+ const collectData = encodeFunctionData4({
1401
+ abi: positionManagerAbi,
1402
+ functionName: "collect",
1403
+ args: [{ tokenId, recipient, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128 }]
1404
+ });
1405
+ const increaseData = encodeFunctionData4({
1406
+ abi: positionManagerAbi,
1407
+ functionName: "increaseLiquidity",
1408
+ args: [{ tokenId, amount0Desired: amount0, amount1Desired: amount1, amount0Min, amount1Min, deadline }]
1409
+ });
1410
+ const data = encodeFunctionData4({
1411
+ abi: positionManagerAbi,
1412
+ functionName: "multicall",
1413
+ args: [[collectData, increaseData]]
1414
+ });
1415
+ return {
1416
+ description: `[${this.protocolName}] Compound tokenId ${tokenId}: collect ${amount0}/${amount1} \u2192 increaseLiquidity (slippage ${slippageBps}bps)`,
1417
+ to: pm,
1418
+ data,
1419
+ value: 0n,
1420
+ gas_estimate: 5e5
1421
+ };
1206
1422
  }
1207
1423
  };
1208
1424
  abi = parseAbi22([
@@ -1423,6 +1639,13 @@ var init_dist2 = __esm({
1423
1639
  "struct MintParams { address token0; address token1; int24 tickLower; int24 tickUpper; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; uint256 amount1Min; address recipient; uint256 deadline; }",
1424
1640
  "function mint(MintParams calldata params) external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)"
1425
1641
  ]);
1642
+ algebraSharedPmAbi = parseAbi32([
1643
+ "struct DecreaseLiquidityParams { uint256 tokenId; uint128 liquidity; uint256 amount0Min; uint256 amount1Min; uint256 deadline; }",
1644
+ "function decreaseLiquidity(DecreaseLiquidityParams calldata params) external payable returns (uint256 amount0, uint256 amount1)",
1645
+ "struct CollectParams { uint256 tokenId; address recipient; uint128 amount0Max; uint128 amount1Max; }",
1646
+ "function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1)",
1647
+ "function multicall(bytes[] data) external payable returns (bytes[] memory results)"
1648
+ ]);
1426
1649
  AlgebraV3Adapter = class {
1427
1650
  protocolName;
1428
1651
  router;
@@ -1618,10 +1841,34 @@ var init_dist2 = __esm({
1618
1841
  approvals
1619
1842
  };
1620
1843
  }
1621
- async buildRemoveLiquidity(_params) {
1622
- throw DefiError.unsupported(
1623
- `[${this.protocolName}] remove_liquidity requires tokenId \u2014 use NFT position manager directly`
1624
- );
1844
+ async buildRemoveLiquidity(params) {
1845
+ const pm = this.positionManager;
1846
+ if (!pm) throw DefiError.contractError(`[${this.protocolName}] Missing 'position_manager'`);
1847
+ if (!params.token_id) throw DefiError.invalidParam(`[${this.protocolName}] V3 remove_liquidity requires --token-id`);
1848
+ const MAX_UINT128 = (1n << 128n) - 1n;
1849
+ const deadline = BigInt("18446744073709551615");
1850
+ const decreaseData = encodeFunctionData32({
1851
+ abi: algebraSharedPmAbi,
1852
+ functionName: "decreaseLiquidity",
1853
+ args: [{ tokenId: params.token_id, liquidity: params.liquidity, amount0Min: 0n, amount1Min: 0n, deadline }]
1854
+ });
1855
+ const collectData = encodeFunctionData32({
1856
+ abi: algebraSharedPmAbi,
1857
+ functionName: "collect",
1858
+ args: [{ tokenId: params.token_id, recipient: params.recipient, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128 }]
1859
+ });
1860
+ const data = encodeFunctionData32({
1861
+ abi: algebraSharedPmAbi,
1862
+ functionName: "multicall",
1863
+ args: [[decreaseData, collectData]]
1864
+ });
1865
+ return {
1866
+ description: `[${this.protocolName}] Remove ${params.liquidity} liquidity from tokenId ${params.token_id}`,
1867
+ to: pm,
1868
+ data,
1869
+ value: 0n,
1870
+ gas_estimate: 4e5
1871
+ };
1625
1872
  }
1626
1873
  };
1627
1874
  abi3 = parseAbi42([
@@ -1909,7 +2156,12 @@ var init_dist2 = __esm({
1909
2156
  };
1910
2157
  thenaPmAbi = parseAbi7([
1911
2158
  "struct MintParams { address token0; address token1; int24 tickSpacing; int24 tickLower; int24 tickUpper; uint256 amount0Desired; uint256 amount1Desired; uint256 amount0Min; uint256 amount1Min; address recipient; uint256 deadline; uint160 sqrtPriceX96; }",
1912
- "function mint(MintParams calldata params) external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)"
2159
+ "function mint(MintParams calldata params) external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)",
2160
+ "struct DecreaseLiquidityParams { uint256 tokenId; uint128 liquidity; uint256 amount0Min; uint256 amount1Min; uint256 deadline; }",
2161
+ "function decreaseLiquidity(DecreaseLiquidityParams calldata params) external payable returns (uint256 amount0, uint256 amount1)",
2162
+ "struct CollectParams { uint256 tokenId; address recipient; uint128 amount0Max; uint128 amount1Max; }",
2163
+ "function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1)",
2164
+ "function multicall(bytes[] data) external payable returns (bytes[] memory results)"
1913
2165
  ]);
1914
2166
  thenaRouterAbi = parseAbi7([
1915
2167
  "struct ExactInputSingleParams { address tokenIn; address tokenOut; int24 tickSpacing; address recipient; uint256 deadline; uint256 amountIn; uint256 amountOutMinimum; uint160 sqrtPriceLimitX96; }",
@@ -2052,8 +2304,34 @@ var init_dist2 = __esm({
2052
2304
  approvals
2053
2305
  };
2054
2306
  }
2055
- async buildRemoveLiquidity(_params) {
2056
- throw DefiError.unsupported(`[${this.protocolName}] remove_liquidity requires tokenId`);
2307
+ async buildRemoveLiquidity(params) {
2308
+ const pm = this.positionManager;
2309
+ if (!pm) throw DefiError.contractError(`[${this.protocolName}] Missing 'position_manager'`);
2310
+ if (!params.token_id) throw DefiError.invalidParam(`[${this.protocolName}] V3 remove_liquidity requires --token-id`);
2311
+ const MAX_UINT128 = (1n << 128n) - 1n;
2312
+ const deadline = BigInt("18446744073709551615");
2313
+ const decreaseData = encodeFunctionData7({
2314
+ abi: thenaPmAbi,
2315
+ functionName: "decreaseLiquidity",
2316
+ args: [{ tokenId: params.token_id, liquidity: params.liquidity, amount0Min: 0n, amount1Min: 0n, deadline }]
2317
+ });
2318
+ const collectData = encodeFunctionData7({
2319
+ abi: thenaPmAbi,
2320
+ functionName: "collect",
2321
+ args: [{ tokenId: params.token_id, recipient: params.recipient, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128 }]
2322
+ });
2323
+ const data = encodeFunctionData7({
2324
+ abi: thenaPmAbi,
2325
+ functionName: "multicall",
2326
+ args: [[decreaseData, collectData]]
2327
+ });
2328
+ return {
2329
+ description: `[${this.protocolName}] Remove ${params.liquidity} liquidity from tokenId ${params.token_id}`,
2330
+ to: pm,
2331
+ data,
2332
+ value: 0n,
2333
+ gas_estimate: 4e5
2334
+ };
2057
2335
  }
2058
2336
  };
2059
2337
  _addressDecodeAbi = parseAbi8(["function f() external view returns (address)"]);
@@ -2232,12 +2510,14 @@ var init_dist2 = __esm({
2232
2510
  pre_txs: [approveTx]
2233
2511
  };
2234
2512
  }
2235
- async buildWithdraw(gauge, _amount, tokenId) {
2513
+ async buildWithdraw(gauge, _amount, tokenId, opts) {
2236
2514
  if (tokenId === void 0) throw new DefiError("CONTRACT_ERROR", "tokenId required for CL gauge withdraw");
2515
+ const redeemType = opts?.redeemType ?? 1;
2516
+ const warning = redeemType === 1 ? " \u2014 WARNING: redeemType=1 locks accumulated rewards into 2-year veHYBR NFT. Use --redeem-type 0 for instant exit (with penalty)." : "";
2237
2517
  return {
2238
- description: `[${this.protocolName}] Withdraw NFT #${tokenId} from gauge`,
2518
+ description: `[${this.protocolName}] Withdraw NFT #${tokenId} from gauge (redeemType=${redeemType})${warning}`,
2239
2519
  to: gauge,
2240
- data: encodeFunctionData8({ abi: gaugeCLAbi, functionName: "withdraw", args: [tokenId, 1] }),
2520
+ data: encodeFunctionData8({ abi: gaugeCLAbi, functionName: "withdraw", args: [tokenId, redeemType] }),
2241
2521
  value: 0n,
2242
2522
  gas_estimate: 1e6
2243
2523
  };
@@ -2246,15 +2526,15 @@ var init_dist2 = __esm({
2246
2526
  async buildClaimRewards(gauge, _account) {
2247
2527
  throw DefiError.unsupported(`[${this.protocolName}] Use buildClaimRewardsByTokenId for CL gauges`);
2248
2528
  }
2249
- async buildClaimRewardsByTokenId(gauge, tokenId) {
2529
+ async buildClaimRewardsByTokenId(gauge, tokenId, opts) {
2530
+ const redeemType = opts?.redeemType ?? 1;
2250
2531
  return {
2251
- description: `[${this.protocolName}] Claim rewards for NFT #${tokenId}`,
2532
+ description: `[${this.protocolName}] Claim rewards for NFT #${tokenId} (redeemType=${redeemType})`,
2252
2533
  to: this.gaugeManager,
2253
2534
  data: encodeFunctionData8({
2254
2535
  abi: gaugeManagerAbi,
2255
2536
  functionName: "claimRewards",
2256
- args: [gauge, [tokenId], 1]
2257
- // redeemType=1
2537
+ args: [gauge, [tokenId], redeemType]
2258
2538
  }),
2259
2539
  value: 0n,
2260
2540
  gas_estimate: 1e6
@@ -2399,15 +2679,21 @@ var init_dist2 = __esm({
2399
2679
  "function getReward(address account) external",
2400
2680
  "function getReward(address account, address[] tokens) external",
2401
2681
  "function getReward(uint256 tokenId) external",
2682
+ // Ramses CL gauge factory (delegate target): tokenId-keyed multi-token claim
2683
+ "function getReward(uint256 tokenId, address[] tokens) external",
2402
2684
  "function earned(address account) external view returns (uint256)",
2403
2685
  "function earned(address account, uint256 tokenId) external view returns (uint256)",
2404
2686
  "function earned(address token, address account) external view returns (uint256)",
2405
2687
  "function earned(uint256 tokenId) external view returns (uint256)",
2688
+ // Ramses CL: token-first + tokenId (no account; account is ownerOf(tokenId) inferred by gauge)
2689
+ "function earned(address token, uint256 tokenId) external view returns (uint256)",
2406
2690
  "function rewardRate() external view returns (uint256)",
2407
2691
  "function rewardToken() external view returns (address)",
2408
2692
  "function totalSupply() external view returns (uint256)",
2409
2693
  "function rewardsListLength() external view returns (uint256)",
2410
2694
  "function rewardData(address token) external view returns (uint256 periodFinish, uint256 rewardRate, uint256 lastUpdateTime, uint256 rewardPerTokenStored)",
2695
+ // Ramses CL gauge factory exposes the canonical reward-token list
2696
+ "function getRewardTokens() external view returns (address[])",
2411
2697
  "function nonfungiblePositionManager() external view returns (address)"
2412
2698
  ]);
2413
2699
  veAbi2 = parseAbi10([
@@ -2452,6 +2738,10 @@ var init_dist2 = __esm({
2452
2738
  clFactory;
2453
2739
  v2Factory;
2454
2740
  tokens;
2741
+ // CL gauges (Aerodrome Slipstream / Velodrome CL) take an LP NFT via deposit(tokenId);
2742
+ // V2 gauges take an LP token amount via deposit(amount). Detect via uniswap_v3 + position_manager.
2743
+ clNftMode;
2744
+ positionManager;
2455
2745
  constructor(entry, rpcUrl, tokens) {
2456
2746
  this.protocolName = entry.name;
2457
2747
  const voter = entry.contracts?.["voter"];
@@ -2468,6 +2758,8 @@ var init_dist2 = __esm({
2468
2758
  this.tokens = tokens;
2469
2759
  this.clFactory = entry.contracts?.["cl_factory"] ?? entry.contracts?.["factory"];
2470
2760
  this.v2Factory = entry.contracts?.["pair_factory"] ?? entry.contracts?.["factory"];
2761
+ this.positionManager = entry.contracts?.["position_manager"];
2762
+ this.clNftMode = entry.interface === "uniswap_v3" && this.positionManager !== void 0;
2471
2763
  }
2472
2764
  name() {
2473
2765
  return this.protocolName;
@@ -2802,6 +3094,25 @@ var init_dist2 = __esm({
2802
3094
  }
2803
3095
  // IGauge
2804
3096
  async buildDeposit(gauge, amount, tokenId, lpToken) {
3097
+ if (this.clNftMode && tokenId !== void 0 && this.positionManager) {
3098
+ const nftAbi = parseAbi10(["function approve(address to, uint256 tokenId) external"]);
3099
+ const clGaugeAbi = parseAbi10(["function deposit(uint256 tokenId) external"]);
3100
+ const approveTx = {
3101
+ description: `[${this.protocolName}] Approve LP NFT #${tokenId} to gauge`,
3102
+ to: this.positionManager,
3103
+ data: encodeFunctionData10({ abi: nftAbi, functionName: "approve", args: [gauge, tokenId] }),
3104
+ value: 0n,
3105
+ gas_estimate: 8e4
3106
+ };
3107
+ return {
3108
+ description: `[${this.protocolName}] Deposit LP NFT #${tokenId} to CL gauge`,
3109
+ to: gauge,
3110
+ data: encodeFunctionData10({ abi: clGaugeAbi, functionName: "deposit", args: [tokenId] }),
3111
+ value: 0n,
3112
+ gas_estimate: 9e5,
3113
+ pre_txs: [approveTx]
3114
+ };
3115
+ }
2805
3116
  if (tokenId !== void 0) {
2806
3117
  const data2 = encodeFunctionData10({
2807
3118
  abi: gaugeAbi,
@@ -2831,7 +3142,17 @@ var init_dist2 = __esm({
2831
3142
  approvals: lpToken ? [{ token: lpToken, spender: gauge, amount }] : void 0
2832
3143
  };
2833
3144
  }
2834
- async buildWithdraw(gauge, amount) {
3145
+ async buildWithdraw(gauge, amount, tokenId) {
3146
+ if (this.clNftMode && tokenId !== void 0) {
3147
+ const clGaugeAbi = parseAbi10(["function withdraw(uint256 tokenId) external"]);
3148
+ return {
3149
+ description: `[${this.protocolName}] Withdraw LP NFT #${tokenId} from CL gauge`,
3150
+ to: gauge,
3151
+ data: encodeFunctionData10({ abi: clGaugeAbi, functionName: "withdraw", args: [tokenId] }),
3152
+ value: 0n,
3153
+ gas_estimate: 6e5
3154
+ };
3155
+ }
2835
3156
  const data = encodeFunctionData10({
2836
3157
  abi: gaugeAbi,
2837
3158
  functionName: "withdraw",
@@ -2921,16 +3242,16 @@ var init_dist2 = __esm({
2921
3242
  }
2922
3243
  async buildClaimRewards(gauge, account) {
2923
3244
  if (!this.rpcUrl || !account) {
2924
- const data2 = encodeFunctionData10({
3245
+ const data = encodeFunctionData10({
2925
3246
  abi: gaugeAbi,
2926
3247
  functionName: "getReward",
2927
3248
  args: [account ?? zeroAddress6]
2928
3249
  });
2929
- return { description: `[${this.protocolName}] Claim gauge rewards`, to: gauge, data: data2, value: 0n, gas_estimate: 2e5 };
3250
+ return { description: `[${this.protocolName}] Claim gauge rewards`, to: gauge, data, value: 0n, gas_estimate: 2e5 };
2930
3251
  }
2931
3252
  const { tokens, multiToken } = await this.discoverRewardTokens(gauge);
2932
3253
  if (multiToken && tokens.length > 0) {
2933
- const data2 = encodeFunctionData10({
3254
+ const data = encodeFunctionData10({
2934
3255
  abi: gaugeAbi,
2935
3256
  functionName: "getReward",
2936
3257
  args: [account, tokens]
@@ -2938,41 +3259,179 @@ var init_dist2 = __esm({
2938
3259
  return {
2939
3260
  description: `[${this.protocolName}] Claim gauge rewards (${tokens.length} tokens)`,
2940
3261
  to: gauge,
2941
- data: data2,
3262
+ data,
2942
3263
  value: 0n,
2943
3264
  gas_estimate: 3e5
2944
3265
  };
2945
3266
  }
3267
+ const accountVariant = encodeFunctionData10({
3268
+ abi: gaugeAbi,
3269
+ functionName: "getReward",
3270
+ args: [account]
3271
+ });
3272
+ try {
3273
+ const client = createPublicClient6({ transport: http6(this.rpcUrl) });
3274
+ await client.call({ account, to: gauge, data: accountVariant });
3275
+ return {
3276
+ description: `[${this.protocolName}] Claim gauge rewards (getReward(account))`,
3277
+ to: gauge,
3278
+ data: accountVariant,
3279
+ value: 0n,
3280
+ gas_estimate: 2e5
3281
+ };
3282
+ } catch {
3283
+ const noArg = encodeFunctionData10({ abi: gaugeAbi, functionName: "getReward", args: [] });
3284
+ return {
3285
+ description: `[${this.protocolName}] Claim gauge rewards (getReward())`,
3286
+ to: gauge,
3287
+ data: noArg,
3288
+ value: 0n,
3289
+ gas_estimate: 2e5
3290
+ };
3291
+ }
3292
+ }
3293
+ /**
3294
+ * Claim rewards for a CL gauge by NFT tokenId (Hybra V4 style — single-arg getReward(tokenId)).
3295
+ */
3296
+ async buildClaimRewardsByTokenId(gauge, tokenId) {
2946
3297
  const data = encodeFunctionData10({
2947
3298
  abi: gaugeAbi,
2948
3299
  functionName: "getReward",
2949
- args: []
3300
+ args: [tokenId]
2950
3301
  });
2951
3302
  return {
2952
- description: `[${this.protocolName}] Claim gauge rewards`,
3303
+ description: `[${this.protocolName}] Claim gauge rewards for NFT #${tokenId}`,
2953
3304
  to: gauge,
2954
3305
  data,
2955
3306
  value: 0n,
2956
- gas_estimate: 2e5
3307
+ gas_estimate: 3e5
2957
3308
  };
2958
3309
  }
2959
3310
  /**
2960
- * Claim rewards for a CL gauge by NFT tokenId (Hybra V4 style).
3311
+ * Ramses-CL claim via NPM.getPeriodReward the user-facing claim path.
3312
+ * The gauge contract restricts `getReward*` to authorized claimers (voter + NPM only);
3313
+ * EOAs must route through NPM, which calls into the gauge with msg.sender = NPM.
3314
+ *
3315
+ * ABI: getPeriodReward(uint256 period, uint256 tokenId, address[] tokens, address receiver)
3316
+ * `period` defaults to current Solidly weekly epoch index (block.timestamp / 604800).
3317
+ * `tokens` defaults to gauge.getRewardTokens() when `gauge` is provided.
3318
+ *
3319
+ * Verified 2026-04-29 on anvil fork: NPM.getPeriodReward(2938, 177068, [..., xRAM], wallet)
3320
+ * delivered 71.11 xRAM after 1h emission warp; direct gauge.getReward(...) reverts
3321
+ * with NOT_AUTHORIZED_CLAIMER for the same EOA.
2961
3322
  */
2962
- async buildClaimRewardsByTokenId(gauge, tokenId) {
3323
+ async buildClaimRewardsViaNPMPeriodReward(npm, tokenId, receiver, opts) {
3324
+ let rewardTokens = opts?.tokens;
3325
+ if (!rewardTokens || rewardTokens.length === 0) {
3326
+ if (!opts?.gauge) {
3327
+ throw DefiError.invalidParam(
3328
+ "Ramses CL claim requires either `tokens` or `gauge` (for getRewardTokens lookup)"
3329
+ );
3330
+ }
3331
+ if (!this.rpcUrl) throw DefiError.rpcError("RPC URL required to discover reward tokens");
3332
+ const client = createPublicClient6({ transport: http6(this.rpcUrl) });
3333
+ try {
3334
+ rewardTokens = await client.readContract({
3335
+ address: opts.gauge,
3336
+ abi: gaugeAbi,
3337
+ functionName: "getRewardTokens"
3338
+ });
3339
+ } catch {
3340
+ throw DefiError.contractError(
3341
+ `[${this.protocolName}] gauge.getRewardTokens() reverted \u2014 pass tokens[] explicitly`
3342
+ );
3343
+ }
3344
+ }
3345
+ if (rewardTokens.length === 0) {
3346
+ throw DefiError.contractError(`[${this.protocolName}] no reward tokens to claim`);
3347
+ }
3348
+ const epochSeconds = 604800n;
3349
+ const period = opts?.period ?? BigInt(Math.floor(Date.now() / 1e3)) / epochSeconds;
3350
+ const npmClaimAbi = parseAbi10([
3351
+ "function getPeriodReward(uint256 period, uint256 tokenId, address[] tokens, address receiver) external"
3352
+ ]);
3353
+ const data = encodeFunctionData10({
3354
+ abi: npmClaimAbi,
3355
+ functionName: "getPeriodReward",
3356
+ args: [period, tokenId, rewardTokens, receiver]
3357
+ });
3358
+ return {
3359
+ description: `[${this.protocolName}] Claim via NPM.getPeriodReward(period=${period}, tokenId=${tokenId}, ${rewardTokens.length} tokens)`,
3360
+ to: npm,
3361
+ data,
3362
+ value: 0n,
3363
+ gas_estimate: 6e5
3364
+ };
3365
+ }
3366
+ /**
3367
+ * @deprecated Direct gauge.getReward(tokenId, tokens[]) reverts with NOT_AUTHORIZED_CLAIMER
3368
+ * for EOAs on Ramses CL. Use buildClaimRewardsViaNPMPeriodReward instead.
3369
+ */
3370
+ async buildClaimRewardsByCLTokenIdMulti(gauge, tokenId, tokens) {
3371
+ let rewardTokens = tokens;
3372
+ if (!rewardTokens || rewardTokens.length === 0) {
3373
+ if (!this.rpcUrl) throw DefiError.rpcError("RPC URL required to discover reward tokens");
3374
+ const client = createPublicClient6({ transport: http6(this.rpcUrl) });
3375
+ try {
3376
+ rewardTokens = await client.readContract({
3377
+ address: gauge,
3378
+ abi: gaugeAbi,
3379
+ functionName: "getRewardTokens"
3380
+ });
3381
+ } catch {
3382
+ throw DefiError.contractError(
3383
+ `[${this.protocolName}] gauge.getRewardTokens() reverted \u2014 pass tokens[] explicitly`
3384
+ );
3385
+ }
3386
+ }
3387
+ if (rewardTokens.length === 0) {
3388
+ throw DefiError.contractError(`[${this.protocolName}] no reward tokens to claim`);
3389
+ }
2963
3390
  const data = encodeFunctionData10({
2964
3391
  abi: gaugeAbi,
2965
3392
  functionName: "getReward",
2966
- args: [tokenId]
3393
+ args: [tokenId, rewardTokens]
2967
3394
  });
2968
3395
  return {
2969
- description: `[${this.protocolName}] Claim gauge rewards for NFT #${tokenId}`,
3396
+ description: `[${this.protocolName}] Claim CL gauge rewards for NFT #${tokenId} (${rewardTokens.length} tokens)`,
2970
3397
  to: gauge,
2971
3398
  data,
2972
3399
  value: 0n,
2973
- gas_estimate: 3e5
3400
+ gas_estimate: 4e5
2974
3401
  };
2975
3402
  }
3403
+ /**
3404
+ * Ramses-CL-style pending rewards: earned(token, tokenId) per reward token from
3405
+ * gauge.getRewardTokens(). Returns raw amounts; caller resolves USD value.
3406
+ */
3407
+ async getPendingRewardsByCLTokenIdMulti(gauge, tokenId) {
3408
+ if (!this.rpcUrl) throw DefiError.rpcError("RPC URL required");
3409
+ const client = createPublicClient6({ transport: http6(this.rpcUrl) });
3410
+ let rewardTokens;
3411
+ try {
3412
+ rewardTokens = await client.readContract({
3413
+ address: gauge,
3414
+ abi: gaugeAbi,
3415
+ functionName: "getRewardTokens"
3416
+ });
3417
+ } catch {
3418
+ return [];
3419
+ }
3420
+ const out = [];
3421
+ for (const token of rewardTokens) {
3422
+ try {
3423
+ const amount = await client.readContract({
3424
+ address: gauge,
3425
+ abi: gaugeAbi,
3426
+ functionName: "earned",
3427
+ args: [token, tokenId]
3428
+ });
3429
+ out.push({ token, symbol: token.slice(0, 10), amount });
3430
+ } catch {
3431
+ }
3432
+ }
3433
+ return out;
3434
+ }
2976
3435
  async getPendingRewards(gauge, user) {
2977
3436
  if (!this.rpcUrl) throw DefiError.rpcError("RPC URL required");
2978
3437
  const client = createPublicClient6({ transport: http6(this.rpcUrl) });
@@ -3539,9 +3998,44 @@ var init_dist2 = __esm({
3539
3998
  }
3540
3999
  ];
3541
4000
  }
4001
+ /**
4002
+ * Scan ±scanRange bins around the active bin and return the user's non-zero balance bin IDs.
4003
+ * Critical: the rewarder may track pending rewards for bins OUTSIDE its current rewarded range
4004
+ * (e.g. when the rewarded range shifts after a position was already in place). Always claim
4005
+ * against the user's actual positions, not the rewarder's "current" range.
4006
+ */
4007
+ async findUserBinsWithBalance(pool, user, scanRange = 50) {
4008
+ const rpcUrl = this.requireRpc();
4009
+ const client = createPublicClient8({ transport: http8(rpcUrl) });
4010
+ const activeId = await client.readContract({
4011
+ address: pool,
4012
+ abi: lbPairAbi,
4013
+ functionName: "getActiveId"
4014
+ });
4015
+ const calls = [];
4016
+ const binIds = [];
4017
+ for (let b = activeId - scanRange; b <= activeId + scanRange; b++) {
4018
+ binIds.push(b);
4019
+ calls.push([pool, encodeFunctionData12({
4020
+ abi: parseAbi12(["function balanceOf(address account, uint256 id) view returns (uint256)"]),
4021
+ functionName: "balanceOf",
4022
+ args: [user, BigInt(b)]
4023
+ })]);
4024
+ }
4025
+ const results = await multicallRead(rpcUrl, calls);
4026
+ const owned = [];
4027
+ for (let i = 0; i < binIds.length; i++) {
4028
+ const data = results[i];
4029
+ if (!data) continue;
4030
+ const hex = data.slice(2).padStart(64, "0");
4031
+ if (hex !== "0".repeat(64)) owned.push(binIds[i]);
4032
+ }
4033
+ return owned;
4034
+ }
3542
4035
  /**
3543
4036
  * Build a claim rewards transaction for specific LB bins.
3544
- * If binIds is omitted, auto-detects from the rewarder's rewarded range.
4037
+ * If binIds is omitted, auto-detects from the user's actual non-zero balance bins (active ±50 scan).
4038
+ * This catches rewards accumulated in bins outside the rewarder's current rewarded range.
3545
4039
  */
3546
4040
  async buildClaimRewards(user, pool, binIds) {
3547
4041
  const rpcUrl = this.requireRpc();
@@ -3557,15 +4051,18 @@ var init_dist2 = __esm({
3557
4051
  }
3558
4052
  let resolvedBinIds = binIds;
3559
4053
  if (!resolvedBinIds || resolvedBinIds.length === 0) {
3560
- const range = await client.readContract({
3561
- address: rewarder,
3562
- abi: lbRewarderAbi,
3563
- functionName: "getRewardedRange"
3564
- });
3565
- const min = Number(range[0]);
3566
- const max = Number(range[1]);
3567
- resolvedBinIds = [];
3568
- for (let b = min; b <= max; b++) resolvedBinIds.push(b);
4054
+ resolvedBinIds = await this.findUserBinsWithBalance(pool, user);
4055
+ if (resolvedBinIds.length === 0) {
4056
+ const range = await client.readContract({
4057
+ address: rewarder,
4058
+ abi: lbRewarderAbi,
4059
+ functionName: "getRewardedRange"
4060
+ });
4061
+ const min = Number(range[0]);
4062
+ const max = Number(range[1]);
4063
+ resolvedBinIds = [];
4064
+ for (let b = min; b <= max; b++) resolvedBinIds.push(b);
4065
+ }
3569
4066
  }
3570
4067
  const data = encodeFunctionData12({
3571
4068
  abi: lbRewarderAbi,
@@ -3976,7 +4473,7 @@ var init_dist2 = __esm({
3976
4473
  "function enterFarming((address rewardToken, address bonusRewardToken, address pool, uint256 nonce) key, uint256 tokenId) external",
3977
4474
  "function exitFarming((address rewardToken, address bonusRewardToken, address pool, uint256 nonce) key, uint256 tokenId) external",
3978
4475
  "function collectRewards((address rewardToken, address bonusRewardToken, address pool, uint256 nonce) key, uint256 tokenId) external",
3979
- "function claimReward(address rewardToken, address to, uint128 amountRequested) external returns (uint256 reward)"
4476
+ "function claimReward(address rewardToken, address to, uint256 amountRequested) external returns (uint256 reward)"
3980
4477
  ]);
3981
4478
  positionManagerAbi2 = parseAbi13([
3982
4479
  "function approveForFarming(uint256 tokenId, bool approve, address farmingAddress) external",
@@ -3998,13 +4495,17 @@ var init_dist2 = __esm({
3998
4495
  positionManager;
3999
4496
  rpcUrl;
4000
4497
  factory;
4001
- constructor(protocolName, farmingCenter, eternalFarming, positionManager, rpcUrl, factory) {
4498
+ rewardToken;
4499
+ bonusRewardToken;
4500
+ constructor(protocolName, farmingCenter, eternalFarming, positionManager, rpcUrl, factory, rewardToken = KITTEN_TOKEN, bonusRewardToken = WHYPE_TOKEN) {
4002
4501
  this.protocolName = protocolName;
4003
4502
  this.farmingCenter = farmingCenter;
4004
4503
  this.eternalFarming = eternalFarming;
4005
4504
  this.positionManager = positionManager;
4006
4505
  this.rpcUrl = rpcUrl;
4007
4506
  this.factory = factory;
4507
+ this.rewardToken = rewardToken;
4508
+ this.bonusRewardToken = bonusRewardToken;
4008
4509
  }
4009
4510
  name() {
4010
4511
  return this.protocolName;
@@ -4019,8 +4520,8 @@ var init_dist2 = __esm({
4019
4520
  const poolLc = pool.toLowerCase();
4020
4521
  if (nonceCache.has(poolLc)) {
4021
4522
  return {
4022
- rewardToken: KITTEN_TOKEN,
4023
- bonusRewardToken: WHYPE_TOKEN,
4523
+ rewardToken: this.rewardToken,
4524
+ bonusRewardToken: this.bonusRewardToken,
4024
4525
  pool,
4025
4526
  nonce: nonceCache.get(poolLc)
4026
4527
  };
@@ -4031,8 +4532,8 @@ var init_dist2 = __esm({
4031
4532
  const nonce = BigInt(n);
4032
4533
  nonces.push(nonce);
4033
4534
  const key = {
4034
- rewardToken: KITTEN_TOKEN,
4035
- bonusRewardToken: WHYPE_TOKEN,
4535
+ rewardToken: this.rewardToken,
4536
+ bonusRewardToken: this.bonusRewardToken,
4036
4537
  pool,
4037
4538
  nonce
4038
4539
  };
@@ -4067,8 +4568,8 @@ var init_dist2 = __esm({
4067
4568
  const nonce = nonces[i];
4068
4569
  nonceCache.set(poolLc, nonce);
4069
4570
  return {
4070
- rewardToken: KITTEN_TOKEN,
4071
- bonusRewardToken: WHYPE_TOKEN,
4571
+ rewardToken: this.rewardToken,
4572
+ bonusRewardToken: this.bonusRewardToken,
4072
4573
  pool,
4073
4574
  nonce
4074
4575
  };
@@ -4160,8 +4661,8 @@ var init_dist2 = __esm({
4160
4661
  }
4161
4662
  const calls = [
4162
4663
  encodeCollectRewards(key, tokenId),
4163
- encodeClaimReward(KITTEN_TOKEN, owner),
4164
- encodeClaimReward(WHYPE_TOKEN, owner)
4664
+ encodeClaimReward(this.rewardToken, owner),
4665
+ encodeClaimReward(this.bonusRewardToken, owner)
4165
4666
  ];
4166
4667
  return {
4167
4668
  description: `[${this.protocolName}] Collect + claim rewards for NFT #${tokenId} in pool ${pool}`,
@@ -4176,8 +4677,8 @@ var init_dist2 = __esm({
4176
4677
  */
4177
4678
  async buildClaimReward(owner) {
4178
4679
  const calls = [
4179
- encodeClaimReward(KITTEN_TOKEN, owner),
4180
- encodeClaimReward(WHYPE_TOKEN, owner)
4680
+ encodeClaimReward(this.rewardToken, owner),
4681
+ encodeClaimReward(this.bonusRewardToken, owner)
4181
4682
  ];
4182
4683
  return {
4183
4684
  description: `[${this.protocolName}] Claim KITTEN + WHYPE farming rewards to ${owner}`,
@@ -4246,8 +4747,8 @@ var init_dist2 = __esm({
4246
4747
  for (const pool of pools) {
4247
4748
  for (let n = 0; n <= MAX_NONCE_SCAN; n++) {
4248
4749
  const key = {
4249
- rewardToken: KITTEN_TOKEN,
4250
- bonusRewardToken: WHYPE_TOKEN,
4750
+ rewardToken: this.rewardToken,
4751
+ bonusRewardToken: this.bonusRewardToken,
4251
4752
  pool,
4252
4753
  nonce: BigInt(n)
4253
4754
  };
@@ -4294,8 +4795,8 @@ var init_dist2 = __esm({
4294
4795
  const isActive = !deactivated;
4295
4796
  if (!bestKey || isActive && !bestActive || isActive === bestActive && nonce > bestKey.nonce) {
4296
4797
  bestKey = {
4297
- rewardToken: KITTEN_TOKEN,
4298
- bonusRewardToken: WHYPE_TOKEN,
4798
+ rewardToken: this.rewardToken,
4799
+ bonusRewardToken: this.bonusRewardToken,
4299
4800
  pool,
4300
4801
  nonce
4301
4802
  };
@@ -4318,7 +4819,236 @@ var init_dist2 = __esm({
4318
4819
  });
4319
4820
  }
4320
4821
  }
4321
- return results;
4822
+ return results;
4823
+ }
4824
+ };
4825
+ DEFAULT_BASE_URL = "https://app.usenest.xyz/api/blaze";
4826
+ FALLBACK_BASE_URL = "https://blaze.nest.aegas.it";
4827
+ NEST_TOKEN = "0x07c57E32a3C29D5659bda1d3EFC2E7BF004E3035";
4828
+ NEST_DECIMALS = 18;
4829
+ NestOffChainAdapter = class {
4830
+ baseUrl;
4831
+ fallbackUrl;
4832
+ voter;
4833
+ constructor(entry) {
4834
+ const voter = entry.contracts?.["voter"];
4835
+ if (!voter) {
4836
+ throw DefiError.contractError("Nest off-chain: missing 'voter' contract");
4837
+ }
4838
+ this.voter = voter;
4839
+ this.baseUrl = process.env["NEST_API_URL"] ?? DEFAULT_BASE_URL;
4840
+ this.fallbackUrl = FALLBACK_BASE_URL;
4841
+ }
4842
+ name() {
4843
+ return "Nest";
4844
+ }
4845
+ /** Cumulative claimed + available NEST emissions for a wallet */
4846
+ async getClaimStatus(wallet) {
4847
+ const data = await this.fetchJson(
4848
+ `/claim/claim-status?publicAddress=${wallet}`
4849
+ );
4850
+ const totalClaimedRaw = BigInt(data.totalClaimed);
4851
+ const totalAvailableRaw = BigInt(data.totalAvailable);
4852
+ const pendingRaw = totalAvailableRaw > totalClaimedRaw ? totalAvailableRaw - totalClaimedRaw : 0n;
4853
+ return {
4854
+ totalClaimedRaw,
4855
+ totalAvailableRaw,
4856
+ pendingRaw,
4857
+ pendingFormatted: Number(pendingRaw) / 10 ** NEST_DECIMALS
4858
+ };
4859
+ }
4860
+ /**
4861
+ * Backend-signed claim ticket (or null when nothing to claim).
4862
+ * Returns the raw ticket; `buildClaim()` is not yet implemented because the
4863
+ * voter contract source is unverified — function selector 0xd6d7a454 takes
4864
+ * 5 dynamic arrays we have not been able to disambiguate yet.
4865
+ */
4866
+ async getClaimTicket(wallet) {
4867
+ const url = `${this.baseUrl}/claim/claim-data?publicAddress=${wallet}`;
4868
+ const res = await fetch(url, this.requestInit());
4869
+ const text = await res.text();
4870
+ if (text.includes("no points to claim")) return null;
4871
+ if (!res.ok) {
4872
+ throw DefiError.providerError(`Nest claim-data ${res.status}: ${text.slice(0, 200)}`);
4873
+ }
4874
+ let json;
4875
+ try {
4876
+ json = JSON.parse(text);
4877
+ } catch {
4878
+ throw DefiError.providerError(`Nest claim-data: non-JSON response: ${text.slice(0, 200)}`);
4879
+ }
4880
+ return {
4881
+ user: json.user,
4882
+ amount: BigInt(json.amount),
4883
+ timestamp: BigInt(json.timestamp),
4884
+ day: json.day === null ? null : BigInt(json.day),
4885
+ signature: json.signature.startsWith("0x") ? json.signature : `0x${json.signature}`
4886
+ };
4887
+ }
4888
+ /** APR estimate (percent) for a CL position with given tick range and amounts */
4889
+ async estimateLpApr(params) {
4890
+ const qs = new URLSearchParams({
4891
+ poolAddress: params.poolAddress,
4892
+ minTick: String(params.minTick),
4893
+ maxTick: String(params.maxTick),
4894
+ token0Amount: params.token0Amount.toString(),
4895
+ token1Amount: params.token1Amount.toString()
4896
+ });
4897
+ const data = await this.fetchJson(`/liquidity/apr/estimate?${qs}`);
4898
+ const apr = Number(data.apr);
4899
+ if (!Number.isFinite(apr)) {
4900
+ throw DefiError.providerError(`Nest apr/estimate: invalid apr value '${data.apr}'`);
4901
+ }
4902
+ return apr;
4903
+ }
4904
+ /** Pending NEST emissions as IGauge-compatible RewardInfo[] */
4905
+ async getPendingRewards(user) {
4906
+ const status = await this.getClaimStatus(user);
4907
+ if (status.pendingRaw === 0n) return [];
4908
+ return [{
4909
+ token: NEST_TOKEN,
4910
+ symbol: "NEST",
4911
+ amount: status.pendingRaw
4912
+ }];
4913
+ }
4914
+ /** Voter address used by aggregateClaim() — exposed for callers that build the tx themselves */
4915
+ getVoterAddress() {
4916
+ return this.voter;
4917
+ }
4918
+ /**
4919
+ * Build a Nest voter claim transaction by reproducing the byte-level calldata
4920
+ * pattern observed in successful onchain claims, swapping in the ticket's
4921
+ * (amount, timestamp, signature) words.
4922
+ *
4923
+ * The voter implementation source is not verified, so we cannot derive a
4924
+ * Solidity ABI for selector 0xd6d7a454. Instead, two known-successful claim
4925
+ * transactions were diffed:
4926
+ *
4927
+ * tx1: 0x99f35cfdb6fc3885ebe046c4625acc083e42d5afe6ca6962c6c81cd9006b99ba
4928
+ * tx2: 0x3e120ab95e9e0a9148cb8964993dd066b8a36363353fe727462231857724e7bb
4929
+ *
4930
+ * 31 of 34 calldata words are identical between the two; only words 21, 22,
4931
+ * 25, 26, 27 differ — and those map exactly to the backend ticket's
4932
+ * (amount, timestamp, sigR, sigS, sigVPadded). msg.sender is not encoded in
4933
+ * calldata; voter binds the claim to the caller, so the ticket signature
4934
+ * authorizes the EOA holding the wallet.
4935
+ *
4936
+ * Throws if no claim ticket is available.
4937
+ */
4938
+ async buildClaim(wallet) {
4939
+ const ticket = await this.getClaimTicket(wallet);
4940
+ if (!ticket) {
4941
+ throw DefiError.invalidParam(`Nest: no claim ticket available for ${wallet}`);
4942
+ }
4943
+ const sigHex = ticket.signature.startsWith("0x") ? ticket.signature.slice(2) : ticket.signature;
4944
+ if (sigHex.length !== 130) {
4945
+ throw DefiError.providerError(`Nest: signature must be 65 bytes (130 hex chars), got ${sigHex.length}`);
4946
+ }
4947
+ const r = sigHex.slice(0, 64);
4948
+ const s = sigHex.slice(64, 128);
4949
+ const v = sigHex.slice(128, 130);
4950
+ const vPadded = v + "0".repeat(62);
4951
+ const amountHex = ticket.amount.toString(16).padStart(64, "0");
4952
+ const timestampHex = ticket.timestamp.toString(16).padStart(64, "0");
4953
+ const words = [
4954
+ "0000000000000000000000000000000000000000000000000000000000000160",
4955
+ // 0
4956
+ "0000000000000000000000000000000000000000000000000000000000000180",
4957
+ // 1
4958
+ "0000000000000000000000000000000000000000000000000000000000000200",
4959
+ // 2
4960
+ "00000000000000000000000000000000000000000000000000000000000002a0",
4961
+ // 3
4962
+ "0000000000000000000000000000000000000000000000000000000000000380",
4963
+ // 4
4964
+ "0000000000000000000000000000000000000000000000000000000000000000",
4965
+ // 5
4966
+ "0000000000000000000000000000000000000000000000000000000000000000",
4967
+ // 6
4968
+ "0000000000000000000000000000000000000000000000000000000000000000",
4969
+ // 7
4970
+ "0000000000000000000000000000000000000000000000000000000000000000",
4971
+ // 8
4972
+ "0000000000000000000000000000000000000000000000000000000000000001",
4973
+ // 9
4974
+ "0000000000000000000000000000000000000000000000000000000000000001",
4975
+ // 10
4976
+ "0000000000000000000000000000000000000000000000000000000000000000",
4977
+ // 11 — empty array length
4978
+ "0000000000000000000000000000000000000000000000000000000000000040",
4979
+ // 12
4980
+ "0000000000000000000000000000000000000000000000000000000000000060",
4981
+ // 13
4982
+ "0000000000000000000000000000000000000000000000000000000000000000",
4983
+ // 14
4984
+ "0000000000000000000000000000000000000000000000000000000000000000",
4985
+ // 15
4986
+ "0000000000000000000000000000000000000000000000000000000000000000",
4987
+ // 16
4988
+ "0000000000000000000000000000000000000000000000000000000000000060",
4989
+ // 17
4990
+ "0000000000000000000000000000000000000000000000000000000000000080",
4991
+ // 18
4992
+ "0000000000000000000000000000000000000000000000000000000000000000",
4993
+ // 19
4994
+ "0000000000000000000000000000000000000000000000000000000000000000",
4995
+ // 20
4996
+ amountHex,
4997
+ // 21 — ticket amount
4998
+ timestampHex,
4999
+ // 22 — ticket timestamp
5000
+ "0000000000000000000000000000000000000000000000000000000000000060",
5001
+ // 23 — sig offset
5002
+ "0000000000000000000000000000000000000000000000000000000000000041",
5003
+ // 24 — sig length (65)
5004
+ r,
5005
+ // 25 — sig r
5006
+ s,
5007
+ // 26 — sig s
5008
+ vPadded,
5009
+ // 27 — sig v + zero padding
5010
+ "0000000000000000000000000000000000000000000000000000000000000000",
5011
+ // 28
5012
+ "0000000000000000000000000000000000000000000000000000000000000000",
5013
+ // 29
5014
+ "0000000000000000000000000000000000000000000000000000000000000001",
5015
+ // 30
5016
+ "0000000000000000000000000000000000000000000000000000000000000000",
5017
+ // 31
5018
+ "00000000000000000000000000000000000000000000000000000000000000a0",
5019
+ // 32
5020
+ "0000000000000000000000000000000000000000000000000000000000000000"
5021
+ // 33
5022
+ ];
5023
+ const data = "0xd6d7a454" + words.join("");
5024
+ return {
5025
+ description: `[${this.name()}] Claim NEST emissions (${(Number(ticket.amount) / 1e18).toFixed(2)} NEST cumulative; backend-signed ts=${ticket.timestamp})`,
5026
+ to: this.voter,
5027
+ data,
5028
+ value: 0n,
5029
+ gas_estimate: 6e5
5030
+ };
5031
+ }
5032
+ // ── internal ──
5033
+ async fetchJson(path) {
5034
+ const primary = `${this.baseUrl}${path.startsWith("/claim") ? path : path}`;
5035
+ try {
5036
+ const res = await fetch(primary, this.requestInit());
5037
+ if (res.ok) return await res.json();
5038
+ if (res.status >= 500) throw new Error(`upstream ${res.status}`);
5039
+ throw DefiError.providerError(`Nest API ${res.status}: ${(await res.text()).slice(0, 200)}`);
5040
+ } catch (e) {
5041
+ if (this.baseUrl === this.fallbackUrl) throw e;
5042
+ const fallback = `${this.fallbackUrl}${path}`;
5043
+ const res = await fetch(fallback, this.requestInit());
5044
+ if (!res.ok) {
5045
+ throw DefiError.providerError(`Nest fallback ${res.status}: ${(await res.text()).slice(0, 200)}`);
5046
+ }
5047
+ return await res.json();
5048
+ }
5049
+ }
5050
+ requestInit() {
5051
+ return { headers: { "User-Agent": "defi-cli/0.5", "Accept": "application/json" } };
4322
5052
  }
4323
5053
  };
4324
5054
  POOL_ABI = parseAbi14([
@@ -4622,8 +5352,8 @@ var init_dist2 = __esm({
4622
5352
  throw DefiError.rpcError(`[${this.protocolName}] getUserAccountData failed: ${e}`);
4623
5353
  });
4624
5354
  const [totalCollateralBase, totalDebtBase, , , ltv, healthFactor] = result;
4625
- const MAX_UINT256 = 2n ** 256n - 1n;
4626
- const hf = healthFactor >= MAX_UINT256 ? Infinity : Number(healthFactor) / 1e18;
5355
+ const MAX_UINT2562 = 2n ** 256n - 1n;
5356
+ const hf = healthFactor >= MAX_UINT2562 ? Infinity : Number(healthFactor) / 1e18;
4627
5357
  const collateralUsd = u256ToF64(totalCollateralBase) / 1e8;
4628
5358
  const debtUsd = u256ToF64(totalDebtBase) / 1e8;
4629
5359
  const ltvBps = u256ToF64(ltv);
@@ -4787,8 +5517,8 @@ var init_dist2 = __esm({
4787
5517
  throw DefiError.rpcError(`[${this.protocolName}] getUserAccountData failed: ${e}`);
4788
5518
  });
4789
5519
  const [totalCollateralBase, totalDebtBase, , , ltv, healthFactor] = result;
4790
- const MAX_UINT256 = 2n ** 256n - 1n;
4791
- const hf = healthFactor >= MAX_UINT256 ? Infinity : Number(healthFactor) / 1e18;
5520
+ const MAX_UINT2562 = 2n ** 256n - 1n;
5521
+ const hf = healthFactor >= MAX_UINT2562 ? Infinity : Number(healthFactor) / 1e18;
4792
5522
  const collateralUsd = u256ToF642(totalCollateralBase) / 1e18;
4793
5523
  const debtUsd = u256ToF642(totalDebtBase) / 1e18;
4794
5524
  const ltvBps = u256ToF642(ltv);
@@ -5035,7 +5765,8 @@ var init_dist2 = __esm({
5035
5765
  to: this.comet,
5036
5766
  data,
5037
5767
  value: 0n,
5038
- gas_estimate: 3e5
5768
+ gas_estimate: 3e5,
5769
+ approvals: [{ token: params.asset, spender: this.comet, amount: params.amount }]
5039
5770
  };
5040
5771
  }
5041
5772
  async buildBorrow(params) {
@@ -5063,7 +5794,8 @@ var init_dist2 = __esm({
5063
5794
  to: this.comet,
5064
5795
  data,
5065
5796
  value: 0n,
5066
- gas_estimate: 3e5
5797
+ gas_estimate: 3e5,
5798
+ approvals: [{ token: params.asset, spender: this.comet, amount: params.amount }]
5067
5799
  };
5068
5800
  }
5069
5801
  async buildWithdraw(params) {
@@ -5252,6 +5984,14 @@ var init_dist2 = __esm({
5252
5984
  "function totalAssets() external view returns (uint256)",
5253
5985
  "function totalSupply() external view returns (uint256)"
5254
5986
  ]);
5987
+ ERC4626_ABI = parseAbi20([
5988
+ "function asset() external view returns (address)",
5989
+ "function deposit(uint256 assets, address receiver) external returns (uint256 shares)",
5990
+ "function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets)",
5991
+ "function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares)",
5992
+ "function balanceOf(address owner) external view returns (uint256)"
5993
+ ]);
5994
+ MAX_UINT256 = (1n << 256n) - 1n;
5255
5995
  IRM_ABI = parseAbi20([
5256
5996
  "function borrowRateView((address loanToken, address collateralToken, address oracle, address irm, uint256 lltv) marketParams, (uint128 totalSupplyAssets, uint128 totalSupplyShares, uint128 totalBorrowAssets, uint128 totalBorrowShares, uint128 lastUpdate, uint128 fee) market) external view returns (uint256)"
5257
5997
  ]);
@@ -5261,6 +6001,9 @@ var init_dist2 = __esm({
5261
6001
  morpho;
5262
6002
  defaultVault;
5263
6003
  rpcUrl;
6004
+ metaMorphoVaults;
6005
+ metaMorphoVaultEntries;
6006
+ vaultAssetMap = null;
5264
6007
  constructor(entry, rpcUrl) {
5265
6008
  this.protocolName = entry.name;
5266
6009
  this.rpcUrl = rpcUrl;
@@ -5269,11 +6012,58 @@ var init_dist2 = __esm({
5269
6012
  if (!morpho) throw DefiError.contractError("Missing 'morpho_blue' contract address");
5270
6013
  this.morpho = morpho;
5271
6014
  this.defaultVault = contracts["fehype"] ?? contracts["vault"] ?? contracts["feusdc"];
6015
+ this.metaMorphoVaultEntries = Object.entries(contracts).filter(([key]) => /^fe[a-z0-9_]+$/i.test(key) || key === "vault").map(([key, addr]) => ({ key, addr }));
6016
+ this.metaMorphoVaults = this.metaMorphoVaultEntries.map((e) => e.addr);
6017
+ }
6018
+ async resolveVault(asset, preferKey) {
6019
+ if (this.metaMorphoVaultEntries.length === 0 || !this.rpcUrl) return null;
6020
+ if (preferKey) {
6021
+ const direct = this.metaMorphoVaultEntries.find((e) => e.key === preferKey);
6022
+ if (direct) return direct.addr;
6023
+ }
6024
+ if (!this.vaultAssetMap) {
6025
+ const calls = this.metaMorphoVaultEntries.map((e) => [
6026
+ e.addr,
6027
+ encodeFunctionData19({ abi: ERC4626_ABI, functionName: "asset" })
6028
+ ]);
6029
+ const results = await multicallRead(this.rpcUrl, calls).catch(() => []);
6030
+ const map = /* @__PURE__ */ new Map();
6031
+ for (let i = 0; i < results.length; i++) {
6032
+ const data = results[i];
6033
+ if (!data || data.length < 66) continue;
6034
+ const a = `0x${data.slice(26, 66)}`.toLowerCase();
6035
+ const entry = this.metaMorphoVaultEntries[i];
6036
+ const existing = map.get(a);
6037
+ if (!existing || entry.key.length < existing.key.length) {
6038
+ map.set(a, entry);
6039
+ }
6040
+ }
6041
+ const flatMap = /* @__PURE__ */ new Map();
6042
+ for (const [k, v] of map) flatMap.set(k, v.addr);
6043
+ this.vaultAssetMap = flatMap;
6044
+ }
6045
+ return this.vaultAssetMap.get(asset.toLowerCase()) ?? null;
5272
6046
  }
5273
6047
  name() {
5274
6048
  return this.protocolName;
5275
6049
  }
5276
6050
  async buildSupply(params) {
6051
+ const vault = await this.resolveVault(params.asset);
6052
+ if (vault) {
6053
+ const data2 = encodeFunctionData19({
6054
+ abi: ERC4626_ABI,
6055
+ functionName: "deposit",
6056
+ args: [params.amount, params.on_behalf_of]
6057
+ });
6058
+ return {
6059
+ description: `[${this.protocolName}] Deposit ${params.amount} into MetaMorpho vault`,
6060
+ to: vault,
6061
+ data: data2,
6062
+ value: 0n,
6063
+ gas_estimate: 4e5,
6064
+ approvals: [{ token: params.asset, spender: vault, amount: params.amount }]
6065
+ };
6066
+ }
5277
6067
  const market = defaultMarketParams(params.asset);
5278
6068
  const data = encodeFunctionData19({
5279
6069
  abi: MORPHO_ABI,
@@ -5319,6 +6109,40 @@ var init_dist2 = __esm({
5319
6109
  };
5320
6110
  }
5321
6111
  async buildWithdraw(params) {
6112
+ const vault = await this.resolveVault(params.asset);
6113
+ if (vault) {
6114
+ if (params.amount === MAX_UINT256) {
6115
+ if (!this.rpcUrl) throw DefiError.rpcError("RPC required to fetch vault shares");
6116
+ const [balRaw] = await multicallRead(this.rpcUrl, [
6117
+ [vault, encodeFunctionData19({ abi: ERC4626_ABI, functionName: "balanceOf", args: [params.to] })]
6118
+ ]);
6119
+ const shares = decodeU256(balRaw ?? null);
6120
+ const data3 = encodeFunctionData19({
6121
+ abi: ERC4626_ABI,
6122
+ functionName: "redeem",
6123
+ args: [shares, params.to, params.to]
6124
+ });
6125
+ return {
6126
+ description: `[${this.protocolName}] Redeem all shares (${shares}) from MetaMorpho vault`,
6127
+ to: vault,
6128
+ data: data3,
6129
+ value: 0n,
6130
+ gas_estimate: 4e5
6131
+ };
6132
+ }
6133
+ const data2 = encodeFunctionData19({
6134
+ abi: ERC4626_ABI,
6135
+ functionName: "withdraw",
6136
+ args: [params.amount, params.to, params.to]
6137
+ });
6138
+ return {
6139
+ description: `[${this.protocolName}] Withdraw ${params.amount} assets from MetaMorpho vault`,
6140
+ to: vault,
6141
+ data: data2,
6142
+ value: 0n,
6143
+ gas_estimate: 4e5
6144
+ };
6145
+ }
5322
6146
  const market = defaultMarketParams(params.asset);
5323
6147
  const data = encodeFunctionData19({
5324
6148
  abi: MORPHO_ABI,
@@ -5637,7 +6461,7 @@ var init_dist2 = __esm({
5637
6461
  return results;
5638
6462
  }
5639
6463
  };
5640
- ERC4626_ABI = parseAbi23([
6464
+ ERC4626_ABI2 = parseAbi23([
5641
6465
  "function asset() external view returns (address)",
5642
6466
  "function totalAssets() external view returns (uint256)",
5643
6467
  "function totalSupply() external view returns (uint256)",
@@ -5662,7 +6486,7 @@ var init_dist2 = __esm({
5662
6486
  }
5663
6487
  async buildDeposit(assets, receiver) {
5664
6488
  const data = encodeFunctionData21({
5665
- abi: ERC4626_ABI,
6489
+ abi: ERC4626_ABI2,
5666
6490
  functionName: "deposit",
5667
6491
  args: [assets, receiver]
5668
6492
  });
@@ -5676,7 +6500,7 @@ var init_dist2 = __esm({
5676
6500
  }
5677
6501
  async buildWithdraw(assets, receiver, owner) {
5678
6502
  const data = encodeFunctionData21({
5679
- abi: ERC4626_ABI,
6503
+ abi: ERC4626_ABI2,
5680
6504
  functionName: "withdraw",
5681
6505
  args: [assets, receiver, owner]
5682
6506
  });
@@ -5693,7 +6517,7 @@ var init_dist2 = __esm({
5693
6517
  const client = createPublicClient18({ transport: http18(this.rpcUrl) });
5694
6518
  return client.readContract({
5695
6519
  address: this.vaultAddress,
5696
- abi: ERC4626_ABI,
6520
+ abi: ERC4626_ABI2,
5697
6521
  functionName: "totalAssets"
5698
6522
  }).catch((e) => {
5699
6523
  throw DefiError.rpcError(`[${this.protocolName}] totalAssets failed: ${e}`);
@@ -5704,7 +6528,7 @@ var init_dist2 = __esm({
5704
6528
  const client = createPublicClient18({ transport: http18(this.rpcUrl) });
5705
6529
  return client.readContract({
5706
6530
  address: this.vaultAddress,
5707
- abi: ERC4626_ABI,
6531
+ abi: ERC4626_ABI2,
5708
6532
  functionName: "convertToShares",
5709
6533
  args: [assets]
5710
6534
  }).catch((e) => {
@@ -5716,7 +6540,7 @@ var init_dist2 = __esm({
5716
6540
  const client = createPublicClient18({ transport: http18(this.rpcUrl) });
5717
6541
  return client.readContract({
5718
6542
  address: this.vaultAddress,
5719
- abi: ERC4626_ABI,
6543
+ abi: ERC4626_ABI2,
5720
6544
  functionName: "convertToAssets",
5721
6545
  args: [shares]
5722
6546
  }).catch((e) => {
@@ -5727,13 +6551,13 @@ var init_dist2 = __esm({
5727
6551
  if (!this.rpcUrl) throw DefiError.rpcError("No RPC URL configured");
5728
6552
  const client = createPublicClient18({ transport: http18(this.rpcUrl) });
5729
6553
  const [totalAssets, totalSupply, asset] = await Promise.all([
5730
- client.readContract({ address: this.vaultAddress, abi: ERC4626_ABI, functionName: "totalAssets" }).catch((e) => {
6554
+ client.readContract({ address: this.vaultAddress, abi: ERC4626_ABI2, functionName: "totalAssets" }).catch((e) => {
5731
6555
  throw DefiError.rpcError(`[${this.protocolName}] totalAssets failed: ${e}`);
5732
6556
  }),
5733
- client.readContract({ address: this.vaultAddress, abi: ERC4626_ABI, functionName: "totalSupply" }).catch((e) => {
6557
+ client.readContract({ address: this.vaultAddress, abi: ERC4626_ABI2, functionName: "totalSupply" }).catch((e) => {
5734
6558
  throw DefiError.rpcError(`[${this.protocolName}] totalSupply failed: ${e}`);
5735
6559
  }),
5736
- client.readContract({ address: this.vaultAddress, abi: ERC4626_ABI, functionName: "asset" }).catch((e) => {
6560
+ client.readContract({ address: this.vaultAddress, abi: ERC4626_ABI2, functionName: "asset" }).catch((e) => {
5737
6561
  throw DefiError.rpcError(`[${this.protocolName}] asset failed: ${e}`);
5738
6562
  })
5739
6563
  ]);
@@ -7172,6 +7996,9 @@ function loadWhitelist() {
7172
7996
  }
7173
7997
  }
7174
7998
 
7999
+ // src/signer/resolve.ts
8000
+ import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
8001
+
7175
8002
  // src/signer/ows-loader.ts
7176
8003
  import { createRequire } from "module";
7177
8004
  var _require = createRequire(import.meta.url);
@@ -7277,7 +8104,6 @@ function resolveWalletWithSigner(opts) {
7277
8104
  }
7278
8105
  const pk = process.env["DEFI_PRIVATE_KEY"];
7279
8106
  if (pk) {
7280
- const { privateKeyToAccount: privateKeyToAccount2 } = __require("viem/accounts");
7281
8107
  return { address: privateKeyToAccount2(pk).address, signer: null };
7282
8108
  }
7283
8109
  if (envWallet) {
@@ -7294,10 +8120,116 @@ function resolveAccount(optOwner, optWallet) {
7294
8120
  const { address } = resolveWalletWithSigner(optWallet ? { wallet: optWallet } : void 0);
7295
8121
  return address;
7296
8122
  }
8123
+ function buildCmd(parts) {
8124
+ const segs = [];
8125
+ for (const [flag, value, placeholder] of parts) {
8126
+ segs.push(`${flag} ${value ?? `<${placeholder}>`}`);
8127
+ }
8128
+ return segs.join(" ");
8129
+ }
8130
+ function buildPipelineSteps(p, input = {}) {
8131
+ const slug = p.slug;
8132
+ const chainFlag = input.chain ? `--chain ${input.chain} ` : "";
8133
+ const baseAdd = `defi ${chainFlag}lp add ` + buildCmd([
8134
+ ["--protocol", slug, "slug"],
8135
+ ["--token-a", input.tokenA, "token-a"],
8136
+ ["--token-b", input.tokenB, "token-b"],
8137
+ ["--amount-a", input.amountA, "amount-a"],
8138
+ ["--amount-b", input.amountB, "amount-b"],
8139
+ ...input.pool ? [["--pool", input.pool, "pool"]] : [],
8140
+ ...input.tickLower ? [["--tick-lower", input.tickLower, "tick-lower"]] : [],
8141
+ ...input.tickUpper ? [["--tick-upper", input.tickUpper, "tick-upper"]] : [],
8142
+ ...input.range ? [["--range", input.range, "range"]] : []
8143
+ ]);
8144
+ const baseFarm = `defi ${chainFlag}lp farm ` + buildCmd([
8145
+ ["--protocol", slug, "slug"],
8146
+ ["--token-a", input.tokenA, "token-a"],
8147
+ ["--token-b", input.tokenB, "token-b"],
8148
+ ["--amount-a", input.amountA, "amount-a"],
8149
+ ["--amount-b", input.amountB, "amount-b"],
8150
+ ...input.pool ? [["--pool", input.pool, "pool"]] : []
8151
+ ]);
8152
+ const claimWithTokenId = (extra = "") => `defi ${chainFlag}lp claim ` + buildCmd([
8153
+ ["--protocol", slug, "slug"],
8154
+ ["--token-id", input.tokenId, "token-id-from-mint-result"],
8155
+ ...input.pool ? [["--pool", input.pool, "pool"]] : [],
8156
+ ...input.gauge ? [["--gauge", input.gauge, "gauge"]] : []
8157
+ ]) + extra;
8158
+ const claimWithGauge = () => `defi ${chainFlag}lp claim ` + buildCmd([
8159
+ ["--protocol", slug, "slug"],
8160
+ ["--gauge", input.gauge, "gauge-from-voter.gaugeForPool"]
8161
+ ]);
8162
+ switch (p.reward_strategy) {
8163
+ case "lp_fee_only":
8164
+ return [
8165
+ { step: "mint", function: "NPM.mint(MintParams)", cli_command: baseAdd },
8166
+ { step: "collect", function: "NPM.collect(tokenId, recipient)", note: "No emissions; collects accrued LP trading fees only", cli_command: claimWithTokenId() }
8167
+ ];
8168
+ case "on_chain_farming_center":
8169
+ return [
8170
+ { step: "mint", function: "NPM.mint(MintParams)", cli_command: baseAdd },
8171
+ { step: "stake", function: "farmingCenter.enterFarming(incentiveKey, tokenId)", cli_command: baseFarm, note: "lp farm chains mint+stake into one tx sequence" },
8172
+ { step: "claim", function: "farmingCenter.collectRewards(incentiveKey, tokenId)", cli_command: claimWithTokenId() }
8173
+ ];
8174
+ case "on_chain_gauge_tokenid":
8175
+ return [
8176
+ { step: "mint", function: "NPM.mint(MintParams)", cli_command: baseAdd },
8177
+ { step: "stake", function: "gauge.deposit(tokenId)", cli_command: baseFarm, note: "lp farm chains mint+stake into one tx sequence" },
8178
+ { step: "claim", function: "gauge.earned(tokenId) \u2192 gauge.getReward(tokenId)", cli_command: claimWithTokenId() }
8179
+ ];
8180
+ case "on_chain_gauge":
8181
+ return [
8182
+ { step: "mint", function: "Router.addLiquidity / NPM.mint", cli_command: baseAdd },
8183
+ { step: "stake", function: "gauge.deposit(amount)", cli_command: baseFarm },
8184
+ { step: "claim", function: "gauge.earned(token, account) \u2192 gauge.getReward(account, tokens[])", cli_command: claimWithGauge() }
8185
+ ];
8186
+ case "auto_stake":
8187
+ return [
8188
+ { step: "mint", function: "Router.addLiquidity / NPM.mint", note: "LP automatically receives x(3,3) emissions \u2014 no separate stake step", cli_command: baseAdd },
8189
+ { step: "claim", function: "gauge.getReward(account, tokens[])", note: "Multi-token reward (xRAM + WHYPE on Ramses HL)", cli_command: claimWithGauge() }
8190
+ ];
8191
+ case "on_chain_masterchef":
8192
+ return [
8193
+ { step: "mint", function: "NPM.mint or pool.mint", cli_command: baseAdd },
8194
+ { step: "stake", function: "MasterChef.deposit(pid, amount)", cli_command: baseFarm },
8195
+ { step: "claim", function: "MasterChef.harvest(pid) or pendingCake(pid, user)", cli_command: claimWithTokenId() }
8196
+ ];
8197
+ case "off_chain_api":
8198
+ return [
8199
+ { step: "mint", function: "NPM.mint(MintParams)", cli_command: baseAdd },
8200
+ { step: "claim", function: "GET claim-data \u2192 voter.aggregateClaim(...)", note: "Read-only: backend-signed ticket only; broadcast ABI unresolved (selector 0xd6d7a454, 11 args, public registries miss). Use the printed `ticket` payload to submit through the Nest UI.", cli_command: `defi ${chainFlag}lp claim --protocol ${slug} --address <wallet>` }
8201
+ ];
8202
+ case "none":
8203
+ return [{ step: "mint", function: "Router/NPM mint", note: "No reward path declared", cli_command: baseAdd }];
8204
+ default:
8205
+ return [{ step: "mint", function: "Router/NPM mint", note: "reward_strategy unset \u2014 pipeline cannot be inferred", cli_command: baseAdd }];
8206
+ }
8207
+ }
7297
8208
  function resolvePoolAddress(registry, protocolSlug, pool) {
7298
8209
  if (pool.startsWith("0x")) return pool;
7299
8210
  return registry.resolvePool(protocolSlug, pool).address;
7300
8211
  }
8212
+ async function detectV3Liquidity(client, npm, tokenId) {
8213
+ const ramsesAbi = parseAbi30([
8214
+ "function positions(uint256) view returns (address t0, address t1, int24 ts, int24 tl, int24 tu, uint128 liq, uint256 a, uint256 b, uint128 o0, uint128 o1)"
8215
+ ]);
8216
+ const standardAbi = parseAbi30([
8217
+ "function positions(uint256) view returns (uint96 nonce, address op, address t0, address t1, uint24 fee, int24 tl, int24 tu, uint128 liq, uint256 a, uint256 b, uint128 o0, uint128 o1)"
8218
+ ]);
8219
+ try {
8220
+ const r = await client.readContract({ address: npm, abi: ramsesAbi, functionName: "positions", args: [tokenId] });
8221
+ if (r[5] !== void 0) {
8222
+ return { token0: r[0], token1: r[1], tickLower: r[3], tickUpper: r[4], liquidity: r[5] };
8223
+ }
8224
+ } catch {
8225
+ }
8226
+ try {
8227
+ const r = await client.readContract({ address: npm, abi: standardAbi, functionName: "positions", args: [tokenId] });
8228
+ return { token0: r[2], token1: r[3], tickLower: r[5], tickUpper: r[6], liquidity: r[7] };
8229
+ } catch {
8230
+ return null;
8231
+ }
8232
+ }
7301
8233
  var V2_PAIR_ABI = parseAbi30([
7302
8234
  "function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)",
7303
8235
  "function totalSupply() external view returns (uint256)"
@@ -7580,18 +8512,59 @@ function registerLP(parent, getOpts, makeExecutor2) {
7580
8512
  } catch {
7581
8513
  }
7582
8514
  }
8515
+ if (protocol.interface === "curve_stableswap" && protocol.contracts?.["stableswap_factory"]) {
8516
+ const factory = protocol.contracts["stableswap_factory"];
8517
+ const factoryAbi = parseAbi30([
8518
+ "function pool_count() view returns (uint256)",
8519
+ "function pool_list(uint256) view returns (address)"
8520
+ ]);
8521
+ const poolAbi2 = parseAbi30([
8522
+ "function coins(uint256) view returns (address)",
8523
+ "function name() view returns (string)"
8524
+ ]);
8525
+ const cClient = createPublicClient23({ transport: http23(rpcUrl) });
8526
+ try {
8527
+ const count = await cClient.readContract({ address: factory, abi: factoryAbi, functionName: "pool_count" });
8528
+ const MAX_SCAN = Math.min(Number(count), 50);
8529
+ for (let i = 0; i < MAX_SCAN; i++) {
8530
+ try {
8531
+ const pool = await cClient.readContract({ address: factory, abi: factoryAbi, functionName: "pool_list", args: [BigInt(i)] });
8532
+ const [c0, c1, name] = await Promise.all([
8533
+ cClient.readContract({ address: pool, abi: poolAbi2, functionName: "coins", args: [0n] }).catch(() => null),
8534
+ cClient.readContract({ address: pool, abi: poolAbi2, functionName: "coins", args: [1n] }).catch(() => null),
8535
+ cClient.readContract({ address: pool, abi: poolAbi2, functionName: "name" }).catch(() => "")
8536
+ ]);
8537
+ results.push({
8538
+ protocol: protocol.slug,
8539
+ pool,
8540
+ pair: name || `${c0 ?? "?"}/${c1 ?? "?"}`,
8541
+ type: "FEE",
8542
+ source: "curve_factory"
8543
+ });
8544
+ } catch {
8545
+ }
8546
+ }
8547
+ } catch {
8548
+ }
8549
+ }
7583
8550
  } catch {
7584
8551
  }
7585
8552
  })
7586
8553
  );
7587
8554
  await _enrichGaugeAprs(results, rpcUrl, registry, chainName);
8555
+ results.sort((a, b) => (b.aprPercent ?? 0) - (a.aprPercent ?? 0));
7588
8556
  if (opts.emissionOnly) {
7589
- printOutput(results.filter((r) => r.type === "EMISSION"), getOpts());
8557
+ printOutput(
8558
+ results.filter(
8559
+ (r) => r.type === "EMISSION" && ((r.moePerDay ?? 0) > 0 || r.rewardRate && BigInt(r.rewardRate) > 0n)
8560
+ ),
8561
+ getOpts()
8562
+ );
7590
8563
  } else {
7591
8564
  printOutput(results, getOpts());
7592
8565
  }
7593
8566
  });
7594
- lp.command("add").description("Add liquidity to a pool").requiredOption("--protocol <protocol>", "Protocol slug").requiredOption("--token-a <token>", "First token symbol or address").requiredOption("--token-b <token>", "Second token symbol or address").requiredOption("--amount-a <amount>", "Amount of token A in wei").requiredOption("--amount-b <amount>", "Amount of token B in wei").option("--pool <name_or_address>", "Pool name (e.g. WHYPE/USDC) or address").option("--recipient <address>", "Recipient address").option("--tick-lower <tick>", "Lower tick for concentrated LP (default: full range)").option("--tick-upper <tick>", "Upper tick for concentrated LP (default: full range)").option("--range <percent>", "\xB1N% concentrated range around current price (e.g. --range 2)").action(async (opts) => {
8567
+ lp.command("add").description("Add liquidity to a pool").requiredOption("--protocol <protocol>", "Protocol slug").requiredOption("--token-a <token>", "First token symbol or address").requiredOption("--token-b <token>", "Second token symbol or address").requiredOption("--amount-a <amount>", "Amount of token A in wei").requiredOption("--amount-b <amount>", "Amount of token B in wei").option("--pool <name_or_address>", "Pool name (e.g. WHYPE/USDC) or address").option("--recipient <address>", "Recipient address").option("--tick-lower <tick>", "Lower tick for concentrated LP (default: full range)").option("--tick-upper <tick>", "Upper tick for concentrated LP (default: full range)").option("--range <percent>", "\xB1N% concentrated range around current price (e.g. --range 2)").option("--num-bins <n>", "Merchant Moe LB: bins on each side of active (default 5)").action(async (opts) => {
7595
8568
  const executor = makeExecutor2();
7596
8569
  const chainName = parent.opts().chain;
7597
8570
  if (!chainName) {
@@ -7601,11 +8574,35 @@ function registerLP(parent, getOpts, makeExecutor2) {
7601
8574
  const registry = Registry.loadEmbedded();
7602
8575
  const chain = registry.getChain(chainName);
7603
8576
  const protocol = registry.getProtocol(opts.protocol);
7604
- const adapter = createDex(protocol, chain.effectiveRpcUrl());
7605
8577
  const tokenA = opts.tokenA.startsWith("0x") ? opts.tokenA : registry.resolveToken(chainName, opts.tokenA).address;
7606
8578
  const tokenB = opts.tokenB.startsWith("0x") ? opts.tokenB : registry.resolveToken(chainName, opts.tokenB).address;
7607
8579
  const recipient = opts.recipient ?? process.env["DEFI_WALLET_ADDRESS"] ?? "0x0000000000000000000000000000000000000001";
7608
8580
  const poolAddr = opts.pool ? resolvePoolAddress(registry, opts.protocol, opts.pool) : void 0;
8581
+ if (protocol.interface === "uniswap_v2" && protocol.contracts?.["lb_factory"]) {
8582
+ if (!poolAddr) throw new Error("--pool is required for Merchant Moe LB add");
8583
+ const lbAdapter = createMerchantMoeLB(protocol, chain.effectiveRpcUrl());
8584
+ const [tokenX, tokenY, amountX, amountY] = tokenA.toLowerCase() < tokenB.toLowerCase() ? [tokenA, tokenB, BigInt(opts.amountA), BigInt(opts.amountB)] : [tokenB, tokenA, BigInt(opts.amountB), BigInt(opts.amountA)];
8585
+ const client = createPublicClient23({ transport: http23(chain.effectiveRpcUrl()) });
8586
+ const binStep = await client.readContract({
8587
+ address: poolAddr,
8588
+ abi: parseAbi30(["function getBinStep() view returns (uint16)"]),
8589
+ functionName: "getBinStep"
8590
+ });
8591
+ const tx2 = await lbAdapter.buildAddLiquidity({
8592
+ pool: poolAddr,
8593
+ tokenX,
8594
+ tokenY,
8595
+ binStep,
8596
+ amountX,
8597
+ amountY,
8598
+ recipient,
8599
+ numBins: opts.numBins !== void 0 ? parseInt(opts.numBins, 10) : 5
8600
+ });
8601
+ const result2 = await executor.execute(tx2);
8602
+ printOutput(result2, getOpts());
8603
+ return;
8604
+ }
8605
+ const adapter = createDex(protocol, chain.effectiveRpcUrl());
7609
8606
  const tx = await adapter.buildAddLiquidity({
7610
8607
  protocol: protocol.name,
7611
8608
  token_a: tokenA,
@@ -7671,7 +8668,8 @@ function registerLP(parent, getOpts, makeExecutor2) {
7671
8668
  printOutput({ step: "stake_farming", ...stakeResult }, getOpts());
7672
8669
  return;
7673
8670
  }
7674
- if (["solidly_v2", "solidly_cl", "hybra"].includes(iface)) {
8671
+ const isGaugeStakeable = ["solidly_v2", "solidly_cl", "hybra"].includes(iface) || iface === "uniswap_v3" && protocol.contracts?.["voter"];
8672
+ if (isGaugeStakeable) {
7675
8673
  if (!mintedTokenId && iface !== "solidly_v2") {
7676
8674
  process.stderr.write("Step 2/2: Skipped staking (no tokenId \u2014 run in --broadcast mode to get minted NFT)\n");
7677
8675
  return;
@@ -7690,7 +8688,21 @@ function registerLP(parent, getOpts, makeExecutor2) {
7690
8688
  process.stderr.write("Step 2/2: Staking into gauge...\n");
7691
8689
  const gaugeAdapter = createGauge(protocol, rpcUrl);
7692
8690
  const tokenIdArg = mintedTokenId;
7693
- const amountArg = iface === "solidly_v2" ? BigInt("115792089237316195423570985008687907853269984665640564039457584007913129639935") : 0n;
8691
+ let amountArg = 0n;
8692
+ if (iface === "solidly_v2" && poolAddr) {
8693
+ const erc20Abi3 = parseAbi30(["function balanceOf(address) view returns (uint256)"]);
8694
+ const lpClient = createPublicClient23({ transport: http23(rpcUrl) });
8695
+ amountArg = await lpClient.readContract({
8696
+ address: poolAddr,
8697
+ abi: erc20Abi3,
8698
+ functionName: "balanceOf",
8699
+ args: [recipient]
8700
+ });
8701
+ if (amountArg === 0n) {
8702
+ process.stderr.write("Step 2/2: Skipped staking \u2014 LP balance 0 (Step 1 add returned no LP).\n");
8703
+ return;
8704
+ }
8705
+ }
7694
8706
  const lpTokenArg = iface === "solidly_v2" ? poolAddr : void 0;
7695
8707
  const stakeTx = await gaugeAdapter.buildDeposit(gaugeAddr, amountArg, tokenIdArg, lpTokenArg);
7696
8708
  const stakeResult = await executor.execute(stakeTx);
@@ -7703,7 +8715,7 @@ function registerLP(parent, getOpts, makeExecutor2) {
7703
8715
  }
7704
8716
  process.stderr.write("Step 2/2: No staking adapter found for this protocol interface \u2014 liquidity added only.\n");
7705
8717
  });
7706
- lp.command("claim").description("Claim rewards from a pool (fee or emission)").requiredOption("--protocol <protocol>", "Protocol slug").option("--pool <address>", "Pool address (required for farming/LB)").option("--gauge <address>", "Gauge contract address (required for solidly/hybra)").option("--token-id <id>", "NFT tokenId (for CL gauge or farming positions)").option("--bins <binIds>", "Comma-separated bin IDs (for Merchant Moe LB)").option("--address <address>", "Wallet address (defaults to DEFI_WALLET_ADDRESS)").action(async (opts) => {
8718
+ lp.command("claim").description("Claim rewards from a pool (fee or emission)").requiredOption("--protocol <protocol>", "Protocol slug").option("--pool <address>", "Pool address (required for farming/LB)").option("--gauge <address>", "Gauge contract address (required for solidly/hybra)").option("--token-id <id>", "NFT tokenId (for CL gauge or farming positions)").option("--bins <binIds>", "Comma-separated bin IDs (for Merchant Moe LB)").option("--address <address>", "Wallet address (defaults to DEFI_WALLET_ADDRESS)").option("--redeem-type <n>", "Hybra: 0=instant exit (with penalty), 1=lock into 2-year veHYBR (default)").action(async (opts) => {
7707
8719
  const executor = makeExecutor2();
7708
8720
  const chainName = parent.opts().chain;
7709
8721
  if (!chainName) {
@@ -7716,6 +8728,86 @@ function registerLP(parent, getOpts, makeExecutor2) {
7716
8728
  const protocol = registry.getProtocol(opts.protocol);
7717
8729
  const account = resolveAccount(opts.address, lp.opts().wallet);
7718
8730
  const iface = protocol.interface;
8731
+ const isV3Fee = protocol.reward_strategy === "lp_fee_only" || iface === "uniswap_v3" && opts.tokenId && !opts.gauge;
8732
+ if (isV3Fee) {
8733
+ if (!opts.tokenId) throw new Error("--token-id is required for V3 LP fee collection");
8734
+ const adapter = createDex(protocol, rpcUrl);
8735
+ if (!("buildCollectFees" in adapter) || typeof adapter.buildCollectFees !== "function") {
8736
+ throw new Error(`[${protocol.name}] adapter does not support buildCollectFees`);
8737
+ }
8738
+ const tx = await adapter.buildCollectFees(BigInt(opts.tokenId), account);
8739
+ const result = await executor.execute(tx);
8740
+ printOutput(result, getOpts());
8741
+ return;
8742
+ }
8743
+ if (protocol.reward_strategy === "off_chain_api") {
8744
+ const nest = createNestOffChain(protocol);
8745
+ const status = await nest.getClaimStatus(account);
8746
+ const ticket = await nest.getClaimTicket(account);
8747
+ if (!ticket) {
8748
+ printOutput({
8749
+ protocol: protocol.slug,
8750
+ wallet: account,
8751
+ voter: nest.getVoterAddress(),
8752
+ reward_symbol: "NEST",
8753
+ pending_amount: 0,
8754
+ note: "No claim ticket available \u2014 backend reports no points to claim."
8755
+ }, getOpts());
8756
+ return;
8757
+ }
8758
+ const tx = await nest.buildClaim(account);
8759
+ const preflightClient = createPublicClient23({ transport: http23(rpcUrl) });
8760
+ try {
8761
+ await preflightClient.call({
8762
+ account,
8763
+ to: tx.to,
8764
+ data: tx.data,
8765
+ value: tx.value
8766
+ });
8767
+ } catch (e) {
8768
+ const msg = e instanceof Error ? e.message : String(e);
8769
+ printOutput({
8770
+ protocol: protocol.slug,
8771
+ wallet: account,
8772
+ voter: nest.getVoterAddress(),
8773
+ reward_symbol: "NEST",
8774
+ pending_amount: status.pendingFormatted,
8775
+ pending_raw: status.pendingRaw.toString(),
8776
+ ticket: {
8777
+ amount: ticket.amount.toString(),
8778
+ timestamp: ticket.timestamp.toString(),
8779
+ day: ticket.day === null ? null : ticket.day.toString(),
8780
+ signature: ticket.signature
8781
+ },
8782
+ preflight: "revert",
8783
+ preflight_error: msg.length > 400 ? msg.slice(0, 400) + "..." : msg,
8784
+ broadcast: "skipped",
8785
+ note: "Simulation reverted \u2014 broadcast aborted to avoid wasted gas. Common causes: ticket expired, caller != ticket signer, ticket already claimed."
8786
+ }, getOpts());
8787
+ return;
8788
+ }
8789
+ const result = await executor.execute(tx);
8790
+ printOutput({
8791
+ protocol: protocol.slug,
8792
+ wallet: account,
8793
+ voter: nest.getVoterAddress(),
8794
+ reward_symbol: "NEST",
8795
+ pending_amount: status.pendingFormatted,
8796
+ pending_raw: status.pendingRaw.toString(),
8797
+ total_claimed_raw: status.totalClaimedRaw.toString(),
8798
+ total_available_raw: status.totalAvailableRaw.toString(),
8799
+ ticket: {
8800
+ amount: ticket.amount.toString(),
8801
+ timestamp: ticket.timestamp.toString(),
8802
+ day: ticket.day === null ? null : ticket.day.toString(),
8803
+ signature: ticket.signature
8804
+ },
8805
+ preflight: "passed",
8806
+ claim_result: result,
8807
+ note: "Calldata derived from byte-level template (verified against two known-successful onchain claim txs). Pre-flight eth_call simulation passed before broadcast."
8808
+ }, getOpts());
8809
+ return;
8810
+ }
7719
8811
  if (iface === "algebra_v3" && protocol.contracts?.["farming_center"]) {
7720
8812
  if (!opts.pool) throw new Error("--pool is required for KittenSwap farming claim");
7721
8813
  if (!opts.tokenId) throw new Error("--token-id is required for KittenSwap farming claim");
@@ -7742,9 +8834,19 @@ function registerLP(parent, getOpts, makeExecutor2) {
7742
8834
  if (!opts.gauge) throw new Error("--gauge is required for gauge claim");
7743
8835
  const adapter = createGauge(protocol, rpcUrl);
7744
8836
  let tx;
7745
- if (opts.tokenId) {
8837
+ if (opts.tokenId && iface === "uniswap_v3" && protocol.reward_strategy === "auto_stake" && "buildClaimRewardsViaNPMPeriodReward" in adapter && typeof adapter.buildClaimRewardsViaNPMPeriodReward === "function") {
8838
+ const npm = protocol.contracts?.["position_manager"];
8839
+ if (!npm) throw new Error(`${protocol.name} requires contracts.position_manager for NPM-based claim`);
8840
+ tx = await adapter.buildClaimRewardsViaNPMPeriodReward(
8841
+ npm,
8842
+ BigInt(opts.tokenId),
8843
+ account,
8844
+ { gauge: opts.gauge }
8845
+ );
8846
+ } else if (opts.tokenId) {
7746
8847
  if (!adapter.buildClaimRewardsByTokenId) throw new Error(`${protocol.name} does not support NFT-based claim`);
7747
- tx = await adapter.buildClaimRewardsByTokenId(opts.gauge, BigInt(opts.tokenId));
8848
+ const claimOpts = opts.redeemType !== void 0 ? { redeemType: parseInt(opts.redeemType, 10) } : void 0;
8849
+ tx = await adapter.buildClaimRewardsByTokenId(opts.gauge, BigInt(opts.tokenId), claimOpts);
7748
8850
  } else {
7749
8851
  tx = await adapter.buildClaimRewards(opts.gauge, account);
7750
8852
  }
@@ -7754,7 +8856,35 @@ function registerLP(parent, getOpts, makeExecutor2) {
7754
8856
  }
7755
8857
  throw new Error(`No claim method found for protocol interface '${iface}'`);
7756
8858
  });
7757
- lp.command("remove").description("Auto-unstake (if staked) and remove liquidity from a pool").requiredOption("--protocol <protocol>", "Protocol slug").requiredOption("--token-a <token>", "First token symbol or address").requiredOption("--token-b <token>", "Second token symbol or address").requiredOption("--liquidity <amount>", "Liquidity amount to remove in wei").option("--pool <address>", "Pool address (needed to resolve gauge)").option("--gauge <address>", "Gauge contract address (for solidly/hybra unstake)").option("--token-id <id>", "NFT tokenId (for CL gauge or farming positions)").option("--recipient <address>", "Recipient address").action(async (opts) => {
8859
+ lp.command("pipeline").description("Show the mint\u2192stake\u2192claim sequence implied by a protocol's reward_strategy. Pass --token-a/-b/--amount-a/-b etc. to get fully-resolved CLI commands you can copy-paste.").requiredOption("--protocol <protocol>", "Protocol slug").option("--pool <pool>", "Pool name or address (e.g. WHYPE/USDC)").option("--token-a <token>", "First token symbol or address").option("--token-b <token>", "Second token symbol or address").option("--amount-a <amount>", "Amount of token A in wei").option("--amount-b <amount>", "Amount of token B in wei").option("--tick-lower <tick>", "Lower tick (V3/CL)").option("--tick-upper <tick>", "Upper tick (V3/CL)").option("--range <percent>", "\xB1N% concentrated range").option("--token-id <id>", "NFT tokenId for already-minted positions (skips mint step)").option("--gauge <addr>", "Gauge address for stake/claim").action((opts) => {
8860
+ const registry = Registry.loadEmbedded();
8861
+ const protocol = registry.getProtocol(opts.protocol);
8862
+ const chainName = parent.opts().chain;
8863
+ const steps = buildPipelineSteps(protocol, {
8864
+ chain: chainName,
8865
+ pool: opts.pool,
8866
+ tokenA: opts.tokenA,
8867
+ tokenB: opts.tokenB,
8868
+ amountA: opts.amountA,
8869
+ amountB: opts.amountB,
8870
+ tickLower: opts.tickLower,
8871
+ tickUpper: opts.tickUpper,
8872
+ range: opts.range,
8873
+ tokenId: opts.tokenId,
8874
+ gauge: opts.gauge
8875
+ });
8876
+ printOutput({
8877
+ protocol: protocol.slug,
8878
+ chain: protocol.chain,
8879
+ interface: protocol.interface,
8880
+ is_active: protocol.is_active !== false,
8881
+ verified: protocol.verified === true,
8882
+ reward_strategy: protocol.reward_strategy ?? "(unset \u2014 falls back to interface inference)",
8883
+ steps,
8884
+ note: "Plan output. Run each cli_command sequentially. After the mint step, broadcast mode prints `details.minted_token_id` \u2014 feed that into the next step's --token-id."
8885
+ }, getOpts());
8886
+ });
8887
+ lp.command("remove").description("Auto-unstake (if staked) and remove liquidity from a pool").requiredOption("--protocol <protocol>", "Protocol slug").requiredOption("--token-a <token>", "First token symbol or address").requiredOption("--token-b <token>", "Second token symbol or address").requiredOption("--liquidity <amount>", "Liquidity amount to remove in wei").option("--pool <address>", "Pool address (needed to resolve gauge)").option("--gauge <address>", "Gauge contract address (for solidly/hybra unstake)").option("--token-id <id>", "NFT tokenId (for CL gauge or farming positions)").option("--recipient <address>", "Recipient address").option("--redeem-type <n>", "Hybra: 0=instant exit (with penalty), 1=lock into 2-year veHYBR (default \u2014 WARNING: long lock)").option("--bins <binIds>", "Merchant Moe LB: comma-separated bin IDs to withdraw").option("--amounts <wei>", "Merchant Moe LB: comma-separated bin amounts (parallel to --bins, default: full balance)").action(async (opts) => {
7758
8888
  const executor = makeExecutor2();
7759
8889
  const chainName = parent.opts().chain;
7760
8890
  if (!chainName) {
@@ -7767,6 +8897,51 @@ function registerLP(parent, getOpts, makeExecutor2) {
7767
8897
  const protocol = registry.getProtocol(opts.protocol);
7768
8898
  const iface = protocol.interface;
7769
8899
  const recipient = opts.recipient ?? process.env["DEFI_WALLET_ADDRESS"] ?? "0x0000000000000000000000000000000000000001";
8900
+ if (iface === "uniswap_v2" && protocol.contracts?.["lb_factory"]) {
8901
+ if (!opts.pool) throw new Error("--pool is required for Merchant Moe LB remove");
8902
+ if (!opts.bins) throw new Error("--bins <id1,id2,...> is required for Merchant Moe LB remove");
8903
+ const lbAdapter = createMerchantMoeLB(protocol, rpcUrl);
8904
+ const tokenA2 = opts.tokenA.startsWith("0x") ? opts.tokenA : registry.resolveToken(chainName, opts.tokenA).address;
8905
+ const tokenB2 = opts.tokenB.startsWith("0x") ? opts.tokenB : registry.resolveToken(chainName, opts.tokenB).address;
8906
+ const [tokenX, tokenY] = tokenA2.toLowerCase() < tokenB2.toLowerCase() ? [tokenA2, tokenB2] : [tokenB2, tokenA2];
8907
+ const binIds = opts.bins.split(",").map((s) => parseInt(s.trim()));
8908
+ const client = createPublicClient23({ transport: http23(rpcUrl) });
8909
+ const binStep = await client.readContract({
8910
+ address: opts.pool,
8911
+ abi: parseAbi30(["function getBinStep() view returns (uint16)"]),
8912
+ functionName: "getBinStep"
8913
+ });
8914
+ let amounts;
8915
+ if (opts.amounts) {
8916
+ amounts = opts.amounts.split(",").map((s) => BigInt(s.trim()));
8917
+ } else {
8918
+ const balanceAbi = parseAbi30(["function balanceOf(address account, uint256 id) view returns (uint256)"]);
8919
+ amounts = await Promise.all(binIds.map(
8920
+ (id) => client.readContract({ address: opts.pool, abi: balanceAbi, functionName: "balanceOf", args: [recipient, BigInt(id)] })
8921
+ ));
8922
+ }
8923
+ const approveForAllAbi = parseAbi30(["function approveForAll(address operator, bool approved) external"]);
8924
+ const lbRouter = protocol.contracts["lb_router"];
8925
+ const approveTx = {
8926
+ description: `[${protocol.name}] approveForAll LBPair \u2192 LBRouter`,
8927
+ to: opts.pool,
8928
+ data: encodeFunctionData27({ abi: approveForAllAbi, functionName: "approveForAll", args: [lbRouter, true] }),
8929
+ value: 0n,
8930
+ gas_estimate: 8e4
8931
+ };
8932
+ const tx = await lbAdapter.buildRemoveLiquidity({
8933
+ tokenX,
8934
+ tokenY,
8935
+ binStep,
8936
+ binIds,
8937
+ amounts,
8938
+ recipient
8939
+ });
8940
+ tx.pre_txs = [approveTx, ...tx.pre_txs ?? []];
8941
+ const result = await executor.execute(tx);
8942
+ printOutput({ step: "lb_remove", ...result }, getOpts());
8943
+ return;
8944
+ }
7770
8945
  const tokenA = opts.tokenA.startsWith("0x") ? opts.tokenA : registry.resolveToken(chainName, opts.tokenA).address;
7771
8946
  const tokenB = opts.tokenB.startsWith("0x") ? opts.tokenB : registry.resolveToken(chainName, opts.tokenB).address;
7772
8947
  const poolAddr = opts.pool ? opts.pool : void 0;
@@ -7782,7 +8957,7 @@ function registerLP(parent, getOpts, makeExecutor2) {
7782
8957
  return;
7783
8958
  }
7784
8959
  didUnstake = true;
7785
- } else if (["solidly_v2", "solidly_cl", "hybra"].includes(iface)) {
8960
+ } else if (["solidly_v2", "solidly_cl", "hybra"].includes(iface) || iface === "uniswap_v3" && protocol.contracts?.["voter"]) {
7786
8961
  let gaugeAddr = opts.gauge;
7787
8962
  if (!gaugeAddr && poolAddr) {
7788
8963
  try {
@@ -7793,11 +8968,32 @@ function registerLP(parent, getOpts, makeExecutor2) {
7793
8968
  } catch {
7794
8969
  }
7795
8970
  }
8971
+ if (gaugeAddr) {
8972
+ if (iface === "solidly_v2" && !opts.tokenId) {
8973
+ const erc20Abi3 = parseAbi30(["function balanceOf(address) view returns (uint256)"]);
8974
+ const gClient = createPublicClient23({ transport: http23(rpcUrl) });
8975
+ const gaugeBal = await gClient.readContract({
8976
+ address: gaugeAddr,
8977
+ abi: erc20Abi3,
8978
+ functionName: "balanceOf",
8979
+ args: [recipient]
8980
+ });
8981
+ if (gaugeBal === 0n) {
8982
+ process.stderr.write("Step 1/2: Skipped unstake \u2014 gauge balance 0 (already unstaked).\n");
8983
+ didUnstake = true;
8984
+ gaugeAddr = void 0;
8985
+ }
8986
+ }
8987
+ }
7796
8988
  if (gaugeAddr) {
7797
8989
  process.stderr.write("Step 1/2: Withdrawing from gauge...\n");
7798
8990
  const gaugeAdapter = createGauge(protocol, rpcUrl);
7799
8991
  const tokenId = opts.tokenId ? BigInt(opts.tokenId) : void 0;
7800
- const withdrawTx = await gaugeAdapter.buildWithdraw(gaugeAddr, BigInt(opts.liquidity), tokenId);
8992
+ const wOpts = opts.redeemType !== void 0 ? { redeemType: parseInt(opts.redeemType, 10) } : void 0;
8993
+ if (iface === "hybra" && (!wOpts || wOpts.redeemType === 1)) {
8994
+ process.stderr.write("WARNING: Hybra default redeemType=1 locks rewards into 2-year veHYBR NFT. Pass --redeem-type 0 for instant exit (with penalty).\n");
8995
+ }
8996
+ const withdrawTx = await gaugeAdapter.buildWithdraw(gaugeAddr, BigInt(opts.liquidity), tokenId, wOpts);
7801
8997
  const withdrawResult = await executor.execute(withdrawTx);
7802
8998
  printOutput({ step: "unstake_gauge", ...withdrawResult }, getOpts());
7803
8999
  if (withdrawResult.status !== "confirmed" && withdrawResult.status !== "simulated") {
@@ -7817,11 +9013,33 @@ function registerLP(parent, getOpts, makeExecutor2) {
7817
9013
  token_a: tokenA,
7818
9014
  token_b: tokenB,
7819
9015
  liquidity: BigInt(opts.liquidity),
7820
- recipient
9016
+ recipient,
9017
+ token_id: opts.tokenId ? BigInt(opts.tokenId) : void 0
7821
9018
  });
7822
9019
  const removeResult = await executor.execute(removeTx);
7823
9020
  printOutput({ step: "lp_remove", ...removeResult }, getOpts());
7824
9021
  });
9022
+ lp.command("compound").description("Auto-compound: collect accrued LP fees and immediately re-add them as liquidity (V3 fee-only protocols).").requiredOption("--protocol <protocol>", "Protocol slug").requiredOption("--token-id <id>", "NFT tokenId of the position to compound").option("--address <address>", "Wallet/recipient address (defaults to DEFI_WALLET_ADDRESS)").option("--slippage <bps>", "Slippage tolerance in basis points (default 50 = 0.5%). Sets amount0Min/amount1Min on increaseLiquidity to protect against MEV.").action(async (opts) => {
9023
+ const executor = makeExecutor2();
9024
+ const chainName = parent.opts().chain;
9025
+ if (!chainName) {
9026
+ printOutput({ error: "--chain is required (e.g. --chain hyperevm)" }, getOpts());
9027
+ return;
9028
+ }
9029
+ const registry = Registry.loadEmbedded();
9030
+ const chain = registry.getChain(chainName);
9031
+ const rpcUrl = chain.effectiveRpcUrl();
9032
+ const protocol = registry.getProtocol(opts.protocol);
9033
+ const recipient = resolveAccount(opts.address, lp.opts().wallet);
9034
+ const adapter = createDex(protocol, rpcUrl);
9035
+ if (!("buildCompound" in adapter) || typeof adapter.buildCompound !== "function") {
9036
+ throw new Error(`[${protocol.name}] adapter does not support compound (v1 supports V3 fee-only protocols)`);
9037
+ }
9038
+ const compoundOpts = opts.slippage !== void 0 ? { slippageBps: parseInt(opts.slippage, 10) } : void 0;
9039
+ const tx = await adapter.buildCompound(BigInt(opts.tokenId), recipient, compoundOpts);
9040
+ const result = await executor.execute(tx);
9041
+ printOutput(result, getOpts());
9042
+ });
7825
9043
  lp.command("positions").description("Show all LP positions across protocols").option("--protocol <protocol>", "Filter to a single protocol slug").option("--pool <address>", "Filter to a specific pool address").option("--bins <binIds>", "Comma-separated bin IDs (for Merchant Moe LB, auto-detected if omitted)").option("--address <address>", "Wallet address (defaults to DEFI_WALLET_ADDRESS)").action(async (opts) => {
7826
9044
  const chainName = parent.opts().chain;
7827
9045
  if (!chainName) {
@@ -7839,17 +9057,68 @@ function registerLP(parent, getOpts, makeExecutor2) {
7839
9057
  protocols.map(async (protocol) => {
7840
9058
  try {
7841
9059
  if (protocol.interface === "uniswap_v2" && protocol.contracts?.["lb_factory"]) {
7842
- if (!opts.pool) return;
7843
9060
  const adapter = createMerchantMoeLB(protocol, rpcUrl);
7844
9061
  const binIds = opts.bins ? opts.bins.split(",").map((s) => parseInt(s.trim())) : void 0;
7845
- const positions = await adapter.getUserPositions(user, opts.pool, binIds);
7846
- for (const pos of positions) {
7847
- results.push({
7848
- protocol: protocol.slug,
7849
- type: "lb",
7850
- pool: opts.pool,
7851
- ...pos
7852
- });
9062
+ const poolsToScan = opts.pool ? [opts.pool] : (await adapter.discoverRewardedPools()).map((p) => p.pool);
9063
+ for (const poolAddr of poolsToScan) {
9064
+ try {
9065
+ const userBins = binIds ?? await adapter.findUserBinsWithBalance(poolAddr, user);
9066
+ if (userBins.length === 0) continue;
9067
+ const positions = await adapter.getUserPositions(user, poolAddr, userBins);
9068
+ if (positions.length === 0) continue;
9069
+ const pending = await adapter.getPendingRewards(user, poolAddr, userBins).catch(() => []);
9070
+ const totalPending = pending.reduce((s, r) => s + (r.amount ?? 0n), 0n);
9071
+ for (const pos of positions) {
9072
+ results.push({
9073
+ protocol: protocol.slug,
9074
+ type: "lb",
9075
+ pool: poolAddr,
9076
+ ...pos,
9077
+ pending_reward: totalPending.toString(),
9078
+ pending_reward_token: pending[0]?.token
9079
+ });
9080
+ }
9081
+ } catch {
9082
+ }
9083
+ }
9084
+ }
9085
+ const npm = protocol.contracts?.["position_manager"];
9086
+ if (npm && ["uniswap_v3", "algebra_v3", "hybra"].includes(protocol.interface)) {
9087
+ const npmAbi = parseAbi30([
9088
+ "function balanceOf(address owner) view returns (uint256)",
9089
+ "function tokenOfOwnerByIndex(address owner, uint256 index) view returns (uint256)"
9090
+ ]);
9091
+ const client = createPublicClient23({ transport: http23(rpcUrl) });
9092
+ let count;
9093
+ try {
9094
+ count = await client.readContract({ address: npm, abi: npmAbi, functionName: "balanceOf", args: [user] });
9095
+ } catch {
9096
+ return;
9097
+ }
9098
+ const max = Math.min(Number(count), 50);
9099
+ for (let i = 0; i < max; i++) {
9100
+ try {
9101
+ const tokenId = await client.readContract({
9102
+ address: npm,
9103
+ abi: npmAbi,
9104
+ functionName: "tokenOfOwnerByIndex",
9105
+ args: [user, BigInt(i)]
9106
+ });
9107
+ const liq = await detectV3Liquidity(client, npm, tokenId);
9108
+ if (liq && liq.liquidity > 0n) {
9109
+ results.push({
9110
+ protocol: protocol.slug,
9111
+ type: "v3_nft",
9112
+ token_id: tokenId.toString(),
9113
+ token0: liq.token0,
9114
+ token1: liq.token1,
9115
+ liquidity: liq.liquidity.toString(),
9116
+ tickLower: liq.tickLower,
9117
+ tickUpper: liq.tickUpper
9118
+ });
9119
+ }
9120
+ } catch {
9121
+ }
7853
9122
  }
7854
9123
  }
7855
9124
  } catch {
@@ -9753,10 +11022,9 @@ function registerBridge(parent, getOpts) {
9753
11022
 
9754
11023
  // src/commands/swap.ts
9755
11024
  init_dist();
9756
- var CHAIN_NAMES = {
9757
- hyperevm: { kyber: "hyperevm", openocean: "hyperevm" },
9758
- mantle: { openocean: "mantle" }
9759
- };
11025
+ function getAggregatorSlugs(chainCfg) {
11026
+ return chainCfg.aggregators ?? {};
11027
+ }
9760
11028
  var KYBER_API = "https://aggregator-api.kyberswap.com";
9761
11029
  async function kyberGetQuote(chain, tokenIn, tokenOut, amountIn) {
9762
11030
  const params = new URLSearchParams({ tokenIn, tokenOut, amountIn });
@@ -9808,6 +11076,64 @@ async function openoceanSwap(chain, inTokenAddress, outTokenAddress, amountIn, s
9808
11076
  outAmount: String(data.outAmount ?? "0")
9809
11077
  };
9810
11078
  }
11079
+ var LIFI_API2 = "https://li.quest/v1";
11080
+ async function lifiQuote(chainId, fromToken, toToken, fromAmount, fromAddress, slippagePct) {
11081
+ const params = new URLSearchParams({
11082
+ fromChain: String(chainId),
11083
+ toChain: String(chainId),
11084
+ fromToken,
11085
+ toToken,
11086
+ fromAmount,
11087
+ fromAddress,
11088
+ slippage: (Number(slippagePct) / 100).toFixed(4)
11089
+ });
11090
+ const url = `${LIFI_API2}/quote?${params}`;
11091
+ const res = await fetch(url);
11092
+ if (!res.ok) throw new Error(`LI.FI quote failed: ${res.status} ${await res.text()}`);
11093
+ const json = await res.json();
11094
+ const txReq = json.transactionRequest;
11095
+ if (!txReq) throw new Error("LI.FI: no transactionRequest in response");
11096
+ const estimate = json.estimate;
11097
+ return {
11098
+ to: String(txReq.to),
11099
+ data: String(txReq.data),
11100
+ value: String(txReq.value ?? "0x0"),
11101
+ outAmount: String(estimate?.toAmount ?? "0")
11102
+ };
11103
+ }
11104
+ var RELAY_API = "https://api.relay.link";
11105
+ async function relayQuote(chainId, fromToken, toToken, amount, user) {
11106
+ const body = {
11107
+ user,
11108
+ originChainId: chainId,
11109
+ destinationChainId: chainId,
11110
+ originCurrency: fromToken,
11111
+ destinationCurrency: toToken,
11112
+ recipient: user,
11113
+ tradeType: "EXACT_INPUT",
11114
+ amount
11115
+ };
11116
+ const res = await fetch(`${RELAY_API}/quote`, {
11117
+ method: "POST",
11118
+ headers: { "content-type": "application/json" },
11119
+ body: JSON.stringify(body)
11120
+ });
11121
+ if (!res.ok) throw new Error(`Relay quote failed: ${res.status} ${await res.text()}`);
11122
+ const json = await res.json();
11123
+ const steps = json.steps;
11124
+ const swapStep = steps?.find((s) => s.id !== "approve") ?? steps?.[steps.length - 1];
11125
+ const items = swapStep?.items;
11126
+ const txData = items?.[0]?.data;
11127
+ if (!txData) throw new Error("Relay: no swap step in quote");
11128
+ const details = json.details;
11129
+ const currencyOut = details?.currencyOut;
11130
+ return {
11131
+ to: String(txData.to),
11132
+ data: String(txData.data),
11133
+ value: String(txData.value ?? "0x0"),
11134
+ outAmount: String(currencyOut?.amount ?? "0")
11135
+ };
11136
+ }
9811
11137
  var LIQD_API = "https://api.liqd.ag/v2";
9812
11138
  var LIQD_ROUTER = "0x744489ee3d540777a66f2cf297479745e0852f7a";
9813
11139
  async function liquidSwapRoute(tokenIn, tokenOut, amountIn, slippagePct) {
@@ -9827,7 +11153,7 @@ async function liquidSwapRoute(tokenIn, tokenOut, amountIn, slippagePct) {
9827
11153
  };
9828
11154
  }
9829
11155
  function registerSwap(parent, getOpts, makeExecutor2) {
9830
- parent.command("swap").description("Swap tokens via DEX aggregator (KyberSwap, OpenOcean, LiquidSwap)").requiredOption("--from <token>", "Input token symbol or address").requiredOption("--to <token>", "Output token symbol or address").requiredOption("--amount <amount>", "Amount of input token in wei").option("--provider <name>", "Aggregator: kyber, openocean, liquid", "kyber").option("--slippage <bps>", "Slippage tolerance in bps", "50").action(async (opts) => {
11156
+ parent.command("swap").description("Swap tokens via DEX aggregator (KyberSwap, OpenOcean, LiquidSwap, LI.FI, Relay)").requiredOption("--from <token>", "Input token symbol or address").requiredOption("--to <token>", "Output token symbol or address").requiredOption("--amount <amount>", "Amount of input token in wei").option("--provider <name>", "Aggregator: kyber, openocean, liquid, lifi, relay", "kyber").option("--slippage <bps>", "Slippage tolerance in bps", "50").action(async (opts) => {
9831
11157
  const executor = makeExecutor2();
9832
11158
  const chainName = requireChain(parent, getOpts);
9833
11159
  if (!chainName) return;
@@ -9838,12 +11164,13 @@ function registerSwap(parent, getOpts, makeExecutor2) {
9838
11164
  const toAddr = resolveTokenAddress(registry, chainName, opts.to);
9839
11165
  const wallet = resolveWallet();
9840
11166
  if (provider === "kyber") {
9841
- const chainNames = CHAIN_NAMES[chainName];
9842
- if (!chainNames?.kyber) {
9843
- printOutput({ error: `KyberSwap: unsupported chain '${chainName}'. Supported: hyperevm` }, getOpts());
11167
+ const aggCfg = getAggregatorSlugs(registry.getChain(chainName));
11168
+ if (!aggCfg.kyber) {
11169
+ const supported = Array.from(registry.chains.keys()).filter((c) => registry.getChain(c).aggregators?.kyber).join(", ");
11170
+ printOutput({ error: `KyberSwap: unsupported chain '${chainName}'. Supported: ${supported || "(none)"}` }, getOpts());
9844
11171
  return;
9845
11172
  }
9846
- const kyberChain = chainNames.kyber;
11173
+ const kyberChain = aggCfg.kyber;
9847
11174
  try {
9848
11175
  const quoteData = await kyberGetQuote(kyberChain, fromAddr, toAddr, opts.amount);
9849
11176
  const routeSummary = quoteData.routeSummary;
@@ -9879,12 +11206,13 @@ function registerSwap(parent, getOpts, makeExecutor2) {
9879
11206
  return;
9880
11207
  }
9881
11208
  if (provider === "openocean") {
9882
- const chainNames = CHAIN_NAMES[chainName];
9883
- if (!chainNames) {
9884
- printOutput({ error: `OpenOcean: unsupported chain '${chainName}'. Supported: ${Object.keys(CHAIN_NAMES).join(", ")}` }, getOpts());
11209
+ const aggCfg = getAggregatorSlugs(registry.getChain(chainName));
11210
+ if (!aggCfg.openocean) {
11211
+ const supported = Array.from(registry.chains.keys()).filter((c) => registry.getChain(c).aggregators?.openocean).join(", ");
11212
+ printOutput({ error: `OpenOcean: unsupported chain '${chainName}'. Supported: ${supported || "(none)"}` }, getOpts());
9885
11213
  return;
9886
11214
  }
9887
- const ooChain = chainNames.openocean;
11215
+ const ooChain = aggCfg.openocean;
9888
11216
  const fromToken = opts.from.startsWith("0x") ? registry.tokens.get(chainName)?.find((t) => t.address.toLowerCase() === opts.from.toLowerCase()) : registry.tokens.get(chainName)?.find((t) => t.symbol.toLowerCase() === opts.from.toLowerCase());
9889
11217
  const fromDecimals = fromToken?.decimals ?? 18;
9890
11218
  const humanAmount = (Number(opts.amount) / 10 ** fromDecimals).toString();
@@ -9952,7 +11280,41 @@ function registerSwap(parent, getOpts, makeExecutor2) {
9952
11280
  }
9953
11281
  return;
9954
11282
  }
9955
- printOutput({ error: `Unknown provider '${opts.provider}'. Choose: kyber, openocean, liquid` }, getOpts());
11283
+ if (provider === "lifi" || provider === "relay") {
11284
+ const chainCfg = registry.getChain(chainName);
11285
+ const chainId = chainCfg.chain_id;
11286
+ if (!chainId) {
11287
+ printOutput({ error: `${provider}: chain '${chainName}' has no chain_id in registry` }, getOpts());
11288
+ return;
11289
+ }
11290
+ const slippagePct = (slippageBps / 100).toFixed(2);
11291
+ try {
11292
+ const route = provider === "lifi" ? await lifiQuote(chainId, fromAddr, toAddr, opts.amount, wallet, slippagePct) : await relayQuote(chainId, fromAddr, toAddr, opts.amount, wallet);
11293
+ const tx = {
11294
+ description: `${provider}: swap ${opts.amount} of ${fromAddr} -> ${toAddr}`,
11295
+ to: route.to,
11296
+ data: route.data,
11297
+ value: parseBigIntValue(route.value),
11298
+ approvals: [{ token: fromAddr, spender: route.to, amount: BigInt(opts.amount) }]
11299
+ };
11300
+ const result = await executor.execute(tx);
11301
+ printOutput({
11302
+ provider,
11303
+ chain: chainName,
11304
+ chain_id: chainId,
11305
+ from_token: fromAddr,
11306
+ to_token: toAddr,
11307
+ amount_in: opts.amount,
11308
+ amount_out: route.outAmount,
11309
+ router: route.to,
11310
+ ...result
11311
+ }, getOpts());
11312
+ } catch (e) {
11313
+ printOutput({ error: `${provider} error: ${errMsg(e)}` }, getOpts());
11314
+ }
11315
+ return;
11316
+ }
11317
+ printOutput({ error: `Unknown provider '${opts.provider}'. Choose: kyber, openocean, liquid, lifi, relay` }, getOpts());
9956
11318
  });
9957
11319
  }
9958
11320
 
@@ -10004,8 +11366,8 @@ function isValidPrivateKey(s) {
10004
11366
  }
10005
11367
  async function deriveAddress(privateKey) {
10006
11368
  try {
10007
- const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
10008
- const account = privateKeyToAccount2(privateKey);
11369
+ const { privateKeyToAccount: privateKeyToAccount3 } = await import("viem/accounts");
11370
+ const account = privateKeyToAccount3(privateKey);
10009
11371
  return account.address;
10010
11372
  } catch {
10011
11373
  return null;