@hypurrquant/defi-cli 0.5.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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
  };
@@ -4321,6 +4822,235 @@ var init_dist2 = __esm({
4321
4822
  return results;
4322
4823
  }
4323
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" } };
5052
+ }
5053
+ };
4324
5054
  POOL_ABI = parseAbi14([
4325
5055
  "function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external",
4326
5056
  "function borrow(address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf) external",
@@ -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
  ]);
@@ -7168,6 +7992,9 @@ function loadWhitelist() {
7168
7992
  }
7169
7993
  }
7170
7994
 
7995
+ // src/signer/resolve.ts
7996
+ import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
7997
+
7171
7998
  // src/signer/ows-loader.ts
7172
7999
  import { createRequire } from "module";
7173
8000
  var _require = createRequire(import.meta.url);
@@ -7273,7 +8100,6 @@ function resolveWalletWithSigner(opts) {
7273
8100
  }
7274
8101
  const pk = process.env["DEFI_PRIVATE_KEY"];
7275
8102
  if (pk) {
7276
- const { privateKeyToAccount: privateKeyToAccount2 } = __require("viem/accounts");
7277
8103
  return { address: privateKeyToAccount2(pk).address, signer: null };
7278
8104
  }
7279
8105
  if (envWallet) {
@@ -7290,10 +8116,116 @@ function resolveAccount(optOwner, optWallet) {
7290
8116
  const { address } = resolveWalletWithSigner(optWallet ? { wallet: optWallet } : void 0);
7291
8117
  return address;
7292
8118
  }
8119
+ function buildCmd(parts) {
8120
+ const segs = [];
8121
+ for (const [flag, value, placeholder] of parts) {
8122
+ segs.push(`${flag} ${value ?? `<${placeholder}>`}`);
8123
+ }
8124
+ return segs.join(" ");
8125
+ }
8126
+ function buildPipelineSteps(p, input = {}) {
8127
+ const slug = p.slug;
8128
+ const chainFlag = input.chain ? `--chain ${input.chain} ` : "";
8129
+ const baseAdd = `defi ${chainFlag}lp add ` + buildCmd([
8130
+ ["--protocol", slug, "slug"],
8131
+ ["--token-a", input.tokenA, "token-a"],
8132
+ ["--token-b", input.tokenB, "token-b"],
8133
+ ["--amount-a", input.amountA, "amount-a"],
8134
+ ["--amount-b", input.amountB, "amount-b"],
8135
+ ...input.pool ? [["--pool", input.pool, "pool"]] : [],
8136
+ ...input.tickLower ? [["--tick-lower", input.tickLower, "tick-lower"]] : [],
8137
+ ...input.tickUpper ? [["--tick-upper", input.tickUpper, "tick-upper"]] : [],
8138
+ ...input.range ? [["--range", input.range, "range"]] : []
8139
+ ]);
8140
+ const baseFarm = `defi ${chainFlag}lp farm ` + buildCmd([
8141
+ ["--protocol", slug, "slug"],
8142
+ ["--token-a", input.tokenA, "token-a"],
8143
+ ["--token-b", input.tokenB, "token-b"],
8144
+ ["--amount-a", input.amountA, "amount-a"],
8145
+ ["--amount-b", input.amountB, "amount-b"],
8146
+ ...input.pool ? [["--pool", input.pool, "pool"]] : []
8147
+ ]);
8148
+ const claimWithTokenId = (extra = "") => `defi ${chainFlag}lp claim ` + buildCmd([
8149
+ ["--protocol", slug, "slug"],
8150
+ ["--token-id", input.tokenId, "token-id-from-mint-result"],
8151
+ ...input.pool ? [["--pool", input.pool, "pool"]] : [],
8152
+ ...input.gauge ? [["--gauge", input.gauge, "gauge"]] : []
8153
+ ]) + extra;
8154
+ const claimWithGauge = () => `defi ${chainFlag}lp claim ` + buildCmd([
8155
+ ["--protocol", slug, "slug"],
8156
+ ["--gauge", input.gauge, "gauge-from-voter.gaugeForPool"]
8157
+ ]);
8158
+ switch (p.reward_strategy) {
8159
+ case "lp_fee_only":
8160
+ return [
8161
+ { step: "mint", function: "NPM.mint(MintParams)", cli_command: baseAdd },
8162
+ { step: "collect", function: "NPM.collect(tokenId, recipient)", note: "No emissions; collects accrued LP trading fees only", cli_command: claimWithTokenId() }
8163
+ ];
8164
+ case "on_chain_farming_center":
8165
+ return [
8166
+ { step: "mint", function: "NPM.mint(MintParams)", cli_command: baseAdd },
8167
+ { step: "stake", function: "farmingCenter.enterFarming(incentiveKey, tokenId)", cli_command: baseFarm, note: "lp farm chains mint+stake into one tx sequence" },
8168
+ { step: "claim", function: "farmingCenter.collectRewards(incentiveKey, tokenId)", cli_command: claimWithTokenId() }
8169
+ ];
8170
+ case "on_chain_gauge_tokenid":
8171
+ return [
8172
+ { step: "mint", function: "NPM.mint(MintParams)", cli_command: baseAdd },
8173
+ { step: "stake", function: "gauge.deposit(tokenId)", cli_command: baseFarm, note: "lp farm chains mint+stake into one tx sequence" },
8174
+ { step: "claim", function: "gauge.earned(tokenId) \u2192 gauge.getReward(tokenId)", cli_command: claimWithTokenId() }
8175
+ ];
8176
+ case "on_chain_gauge":
8177
+ return [
8178
+ { step: "mint", function: "Router.addLiquidity / NPM.mint", cli_command: baseAdd },
8179
+ { step: "stake", function: "gauge.deposit(amount)", cli_command: baseFarm },
8180
+ { step: "claim", function: "gauge.earned(token, account) \u2192 gauge.getReward(account, tokens[])", cli_command: claimWithGauge() }
8181
+ ];
8182
+ case "auto_stake":
8183
+ return [
8184
+ { step: "mint", function: "Router.addLiquidity / NPM.mint", note: "LP automatically receives x(3,3) emissions \u2014 no separate stake step", cli_command: baseAdd },
8185
+ { step: "claim", function: "gauge.getReward(account, tokens[])", note: "Multi-token reward (xRAM + WHYPE on Ramses HL)", cli_command: claimWithGauge() }
8186
+ ];
8187
+ case "on_chain_masterchef":
8188
+ return [
8189
+ { step: "mint", function: "NPM.mint or pool.mint", cli_command: baseAdd },
8190
+ { step: "stake", function: "MasterChef.deposit(pid, amount)", cli_command: baseFarm },
8191
+ { step: "claim", function: "MasterChef.harvest(pid) or pendingCake(pid, user)", cli_command: claimWithTokenId() }
8192
+ ];
8193
+ case "off_chain_api":
8194
+ return [
8195
+ { step: "mint", function: "NPM.mint(MintParams)", cli_command: baseAdd },
8196
+ { 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>` }
8197
+ ];
8198
+ case "none":
8199
+ return [{ step: "mint", function: "Router/NPM mint", note: "No reward path declared", cli_command: baseAdd }];
8200
+ default:
8201
+ return [{ step: "mint", function: "Router/NPM mint", note: "reward_strategy unset \u2014 pipeline cannot be inferred", cli_command: baseAdd }];
8202
+ }
8203
+ }
7293
8204
  function resolvePoolAddress(registry, protocolSlug, pool) {
7294
8205
  if (pool.startsWith("0x")) return pool;
7295
8206
  return registry.resolvePool(protocolSlug, pool).address;
7296
8207
  }
8208
+ async function detectV3Liquidity(client, npm, tokenId) {
8209
+ const ramsesAbi = parseAbi30([
8210
+ "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)"
8211
+ ]);
8212
+ const standardAbi = parseAbi30([
8213
+ "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)"
8214
+ ]);
8215
+ try {
8216
+ const r = await client.readContract({ address: npm, abi: ramsesAbi, functionName: "positions", args: [tokenId] });
8217
+ if (r[5] !== void 0) {
8218
+ return { token0: r[0], token1: r[1], tickLower: r[3], tickUpper: r[4], liquidity: r[5] };
8219
+ }
8220
+ } catch {
8221
+ }
8222
+ try {
8223
+ const r = await client.readContract({ address: npm, abi: standardAbi, functionName: "positions", args: [tokenId] });
8224
+ return { token0: r[2], token1: r[3], tickLower: r[5], tickUpper: r[6], liquidity: r[7] };
8225
+ } catch {
8226
+ return null;
8227
+ }
8228
+ }
7297
8229
  var V2_PAIR_ABI = parseAbi30([
7298
8230
  "function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)",
7299
8231
  "function totalSupply() external view returns (uint256)"
@@ -7576,18 +8508,59 @@ function registerLP(parent, getOpts, makeExecutor2) {
7576
8508
  } catch {
7577
8509
  }
7578
8510
  }
8511
+ if (protocol.interface === "curve_stableswap" && protocol.contracts?.["stableswap_factory"]) {
8512
+ const factory = protocol.contracts["stableswap_factory"];
8513
+ const factoryAbi = parseAbi30([
8514
+ "function pool_count() view returns (uint256)",
8515
+ "function pool_list(uint256) view returns (address)"
8516
+ ]);
8517
+ const poolAbi2 = parseAbi30([
8518
+ "function coins(uint256) view returns (address)",
8519
+ "function name() view returns (string)"
8520
+ ]);
8521
+ const cClient = createPublicClient23({ transport: http23(rpcUrl) });
8522
+ try {
8523
+ const count = await cClient.readContract({ address: factory, abi: factoryAbi, functionName: "pool_count" });
8524
+ const MAX_SCAN = Math.min(Number(count), 50);
8525
+ for (let i = 0; i < MAX_SCAN; i++) {
8526
+ try {
8527
+ const pool = await cClient.readContract({ address: factory, abi: factoryAbi, functionName: "pool_list", args: [BigInt(i)] });
8528
+ const [c0, c1, name] = await Promise.all([
8529
+ cClient.readContract({ address: pool, abi: poolAbi2, functionName: "coins", args: [0n] }).catch(() => null),
8530
+ cClient.readContract({ address: pool, abi: poolAbi2, functionName: "coins", args: [1n] }).catch(() => null),
8531
+ cClient.readContract({ address: pool, abi: poolAbi2, functionName: "name" }).catch(() => "")
8532
+ ]);
8533
+ results.push({
8534
+ protocol: protocol.slug,
8535
+ pool,
8536
+ pair: name || `${c0 ?? "?"}/${c1 ?? "?"}`,
8537
+ type: "FEE",
8538
+ source: "curve_factory"
8539
+ });
8540
+ } catch {
8541
+ }
8542
+ }
8543
+ } catch {
8544
+ }
8545
+ }
7579
8546
  } catch {
7580
8547
  }
7581
8548
  })
7582
8549
  );
7583
8550
  await _enrichGaugeAprs(results, rpcUrl, registry, chainName);
8551
+ results.sort((a, b) => (b.aprPercent ?? 0) - (a.aprPercent ?? 0));
7584
8552
  if (opts.emissionOnly) {
7585
- printOutput(results.filter((r) => r.type === "EMISSION"), getOpts());
8553
+ printOutput(
8554
+ results.filter(
8555
+ (r) => r.type === "EMISSION" && ((r.moePerDay ?? 0) > 0 || r.rewardRate && BigInt(r.rewardRate) > 0n)
8556
+ ),
8557
+ getOpts()
8558
+ );
7586
8559
  } else {
7587
8560
  printOutput(results, getOpts());
7588
8561
  }
7589
8562
  });
7590
- 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) => {
8563
+ 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) => {
7591
8564
  const executor = makeExecutor2();
7592
8565
  const chainName = parent.opts().chain;
7593
8566
  if (!chainName) {
@@ -7597,11 +8570,35 @@ function registerLP(parent, getOpts, makeExecutor2) {
7597
8570
  const registry = Registry.loadEmbedded();
7598
8571
  const chain = registry.getChain(chainName);
7599
8572
  const protocol = registry.getProtocol(opts.protocol);
7600
- const adapter = createDex(protocol, chain.effectiveRpcUrl());
7601
8573
  const tokenA = opts.tokenA.startsWith("0x") ? opts.tokenA : registry.resolveToken(chainName, opts.tokenA).address;
7602
8574
  const tokenB = opts.tokenB.startsWith("0x") ? opts.tokenB : registry.resolveToken(chainName, opts.tokenB).address;
7603
8575
  const recipient = opts.recipient ?? process.env["DEFI_WALLET_ADDRESS"] ?? "0x0000000000000000000000000000000000000001";
7604
8576
  const poolAddr = opts.pool ? resolvePoolAddress(registry, opts.protocol, opts.pool) : void 0;
8577
+ if (protocol.interface === "uniswap_v2" && protocol.contracts?.["lb_factory"]) {
8578
+ if (!poolAddr) throw new Error("--pool is required for Merchant Moe LB add");
8579
+ const lbAdapter = createMerchantMoeLB(protocol, chain.effectiveRpcUrl());
8580
+ 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)];
8581
+ const client = createPublicClient23({ transport: http23(chain.effectiveRpcUrl()) });
8582
+ const binStep = await client.readContract({
8583
+ address: poolAddr,
8584
+ abi: parseAbi30(["function getBinStep() view returns (uint16)"]),
8585
+ functionName: "getBinStep"
8586
+ });
8587
+ const tx2 = await lbAdapter.buildAddLiquidity({
8588
+ pool: poolAddr,
8589
+ tokenX,
8590
+ tokenY,
8591
+ binStep,
8592
+ amountX,
8593
+ amountY,
8594
+ recipient,
8595
+ numBins: opts.numBins !== void 0 ? parseInt(opts.numBins, 10) : 5
8596
+ });
8597
+ const result2 = await executor.execute(tx2);
8598
+ printOutput(result2, getOpts());
8599
+ return;
8600
+ }
8601
+ const adapter = createDex(protocol, chain.effectiveRpcUrl());
7605
8602
  const tx = await adapter.buildAddLiquidity({
7606
8603
  protocol: protocol.name,
7607
8604
  token_a: tokenA,
@@ -7667,7 +8664,8 @@ function registerLP(parent, getOpts, makeExecutor2) {
7667
8664
  printOutput({ step: "stake_farming", ...stakeResult }, getOpts());
7668
8665
  return;
7669
8666
  }
7670
- if (["solidly_v2", "solidly_cl", "hybra"].includes(iface)) {
8667
+ const isGaugeStakeable = ["solidly_v2", "solidly_cl", "hybra"].includes(iface) || iface === "uniswap_v3" && protocol.contracts?.["voter"];
8668
+ if (isGaugeStakeable) {
7671
8669
  if (!mintedTokenId && iface !== "solidly_v2") {
7672
8670
  process.stderr.write("Step 2/2: Skipped staking (no tokenId \u2014 run in --broadcast mode to get minted NFT)\n");
7673
8671
  return;
@@ -7686,7 +8684,21 @@ function registerLP(parent, getOpts, makeExecutor2) {
7686
8684
  process.stderr.write("Step 2/2: Staking into gauge...\n");
7687
8685
  const gaugeAdapter = createGauge(protocol, rpcUrl);
7688
8686
  const tokenIdArg = mintedTokenId;
7689
- const amountArg = iface === "solidly_v2" ? BigInt("115792089237316195423570985008687907853269984665640564039457584007913129639935") : 0n;
8687
+ let amountArg = 0n;
8688
+ if (iface === "solidly_v2" && poolAddr) {
8689
+ const erc20Abi3 = parseAbi30(["function balanceOf(address) view returns (uint256)"]);
8690
+ const lpClient = createPublicClient23({ transport: http23(rpcUrl) });
8691
+ amountArg = await lpClient.readContract({
8692
+ address: poolAddr,
8693
+ abi: erc20Abi3,
8694
+ functionName: "balanceOf",
8695
+ args: [recipient]
8696
+ });
8697
+ if (amountArg === 0n) {
8698
+ process.stderr.write("Step 2/2: Skipped staking \u2014 LP balance 0 (Step 1 add returned no LP).\n");
8699
+ return;
8700
+ }
8701
+ }
7690
8702
  const lpTokenArg = iface === "solidly_v2" ? poolAddr : void 0;
7691
8703
  const stakeTx = await gaugeAdapter.buildDeposit(gaugeAddr, amountArg, tokenIdArg, lpTokenArg);
7692
8704
  const stakeResult = await executor.execute(stakeTx);
@@ -7699,7 +8711,7 @@ function registerLP(parent, getOpts, makeExecutor2) {
7699
8711
  }
7700
8712
  process.stderr.write("Step 2/2: No staking adapter found for this protocol interface \u2014 liquidity added only.\n");
7701
8713
  });
7702
- 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) => {
8714
+ 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) => {
7703
8715
  const executor = makeExecutor2();
7704
8716
  const chainName = parent.opts().chain;
7705
8717
  if (!chainName) {
@@ -7712,6 +8724,86 @@ function registerLP(parent, getOpts, makeExecutor2) {
7712
8724
  const protocol = registry.getProtocol(opts.protocol);
7713
8725
  const account = resolveAccount(opts.address, lp.opts().wallet);
7714
8726
  const iface = protocol.interface;
8727
+ const isV3Fee = protocol.reward_strategy === "lp_fee_only" || iface === "uniswap_v3" && opts.tokenId && !opts.gauge;
8728
+ if (isV3Fee) {
8729
+ if (!opts.tokenId) throw new Error("--token-id is required for V3 LP fee collection");
8730
+ const adapter = createDex(protocol, rpcUrl);
8731
+ if (!("buildCollectFees" in adapter) || typeof adapter.buildCollectFees !== "function") {
8732
+ throw new Error(`[${protocol.name}] adapter does not support buildCollectFees`);
8733
+ }
8734
+ const tx = await adapter.buildCollectFees(BigInt(opts.tokenId), account);
8735
+ const result = await executor.execute(tx);
8736
+ printOutput(result, getOpts());
8737
+ return;
8738
+ }
8739
+ if (protocol.reward_strategy === "off_chain_api") {
8740
+ const nest = createNestOffChain(protocol);
8741
+ const status = await nest.getClaimStatus(account);
8742
+ const ticket = await nest.getClaimTicket(account);
8743
+ if (!ticket) {
8744
+ printOutput({
8745
+ protocol: protocol.slug,
8746
+ wallet: account,
8747
+ voter: nest.getVoterAddress(),
8748
+ reward_symbol: "NEST",
8749
+ pending_amount: 0,
8750
+ note: "No claim ticket available \u2014 backend reports no points to claim."
8751
+ }, getOpts());
8752
+ return;
8753
+ }
8754
+ const tx = await nest.buildClaim(account);
8755
+ const preflightClient = createPublicClient23({ transport: http23(rpcUrl) });
8756
+ try {
8757
+ await preflightClient.call({
8758
+ account,
8759
+ to: tx.to,
8760
+ data: tx.data,
8761
+ value: tx.value
8762
+ });
8763
+ } catch (e) {
8764
+ const msg = e instanceof Error ? e.message : String(e);
8765
+ printOutput({
8766
+ protocol: protocol.slug,
8767
+ wallet: account,
8768
+ voter: nest.getVoterAddress(),
8769
+ reward_symbol: "NEST",
8770
+ pending_amount: status.pendingFormatted,
8771
+ pending_raw: status.pendingRaw.toString(),
8772
+ ticket: {
8773
+ amount: ticket.amount.toString(),
8774
+ timestamp: ticket.timestamp.toString(),
8775
+ day: ticket.day === null ? null : ticket.day.toString(),
8776
+ signature: ticket.signature
8777
+ },
8778
+ preflight: "revert",
8779
+ preflight_error: msg.length > 400 ? msg.slice(0, 400) + "..." : msg,
8780
+ broadcast: "skipped",
8781
+ note: "Simulation reverted \u2014 broadcast aborted to avoid wasted gas. Common causes: ticket expired, caller != ticket signer, ticket already claimed."
8782
+ }, getOpts());
8783
+ return;
8784
+ }
8785
+ const result = await executor.execute(tx);
8786
+ printOutput({
8787
+ protocol: protocol.slug,
8788
+ wallet: account,
8789
+ voter: nest.getVoterAddress(),
8790
+ reward_symbol: "NEST",
8791
+ pending_amount: status.pendingFormatted,
8792
+ pending_raw: status.pendingRaw.toString(),
8793
+ total_claimed_raw: status.totalClaimedRaw.toString(),
8794
+ total_available_raw: status.totalAvailableRaw.toString(),
8795
+ ticket: {
8796
+ amount: ticket.amount.toString(),
8797
+ timestamp: ticket.timestamp.toString(),
8798
+ day: ticket.day === null ? null : ticket.day.toString(),
8799
+ signature: ticket.signature
8800
+ },
8801
+ preflight: "passed",
8802
+ claim_result: result,
8803
+ note: "Calldata derived from byte-level template (verified against two known-successful onchain claim txs). Pre-flight eth_call simulation passed before broadcast."
8804
+ }, getOpts());
8805
+ return;
8806
+ }
7715
8807
  if (iface === "algebra_v3" && protocol.contracts?.["farming_center"]) {
7716
8808
  if (!opts.pool) throw new Error("--pool is required for KittenSwap farming claim");
7717
8809
  if (!opts.tokenId) throw new Error("--token-id is required for KittenSwap farming claim");
@@ -7738,9 +8830,19 @@ function registerLP(parent, getOpts, makeExecutor2) {
7738
8830
  if (!opts.gauge) throw new Error("--gauge is required for gauge claim");
7739
8831
  const adapter = createGauge(protocol, rpcUrl);
7740
8832
  let tx;
7741
- if (opts.tokenId) {
8833
+ if (opts.tokenId && iface === "uniswap_v3" && protocol.reward_strategy === "auto_stake" && "buildClaimRewardsViaNPMPeriodReward" in adapter && typeof adapter.buildClaimRewardsViaNPMPeriodReward === "function") {
8834
+ const npm = protocol.contracts?.["position_manager"];
8835
+ if (!npm) throw new Error(`${protocol.name} requires contracts.position_manager for NPM-based claim`);
8836
+ tx = await adapter.buildClaimRewardsViaNPMPeriodReward(
8837
+ npm,
8838
+ BigInt(opts.tokenId),
8839
+ account,
8840
+ { gauge: opts.gauge }
8841
+ );
8842
+ } else if (opts.tokenId) {
7742
8843
  if (!adapter.buildClaimRewardsByTokenId) throw new Error(`${protocol.name} does not support NFT-based claim`);
7743
- tx = await adapter.buildClaimRewardsByTokenId(opts.gauge, BigInt(opts.tokenId));
8844
+ const claimOpts = opts.redeemType !== void 0 ? { redeemType: parseInt(opts.redeemType, 10) } : void 0;
8845
+ tx = await adapter.buildClaimRewardsByTokenId(opts.gauge, BigInt(opts.tokenId), claimOpts);
7744
8846
  } else {
7745
8847
  tx = await adapter.buildClaimRewards(opts.gauge, account);
7746
8848
  }
@@ -7750,7 +8852,35 @@ function registerLP(parent, getOpts, makeExecutor2) {
7750
8852
  }
7751
8853
  throw new Error(`No claim method found for protocol interface '${iface}'`);
7752
8854
  });
7753
- 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) => {
8855
+ 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) => {
8856
+ const registry = Registry.loadEmbedded();
8857
+ const protocol = registry.getProtocol(opts.protocol);
8858
+ const chainName = parent.opts().chain;
8859
+ const steps = buildPipelineSteps(protocol, {
8860
+ chain: chainName,
8861
+ pool: opts.pool,
8862
+ tokenA: opts.tokenA,
8863
+ tokenB: opts.tokenB,
8864
+ amountA: opts.amountA,
8865
+ amountB: opts.amountB,
8866
+ tickLower: opts.tickLower,
8867
+ tickUpper: opts.tickUpper,
8868
+ range: opts.range,
8869
+ tokenId: opts.tokenId,
8870
+ gauge: opts.gauge
8871
+ });
8872
+ printOutput({
8873
+ protocol: protocol.slug,
8874
+ chain: protocol.chain,
8875
+ interface: protocol.interface,
8876
+ is_active: protocol.is_active !== false,
8877
+ verified: protocol.verified === true,
8878
+ reward_strategy: protocol.reward_strategy ?? "(unset \u2014 falls back to interface inference)",
8879
+ steps,
8880
+ 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."
8881
+ }, getOpts());
8882
+ });
8883
+ 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) => {
7754
8884
  const executor = makeExecutor2();
7755
8885
  const chainName = parent.opts().chain;
7756
8886
  if (!chainName) {
@@ -7763,6 +8893,51 @@ function registerLP(parent, getOpts, makeExecutor2) {
7763
8893
  const protocol = registry.getProtocol(opts.protocol);
7764
8894
  const iface = protocol.interface;
7765
8895
  const recipient = opts.recipient ?? process.env["DEFI_WALLET_ADDRESS"] ?? "0x0000000000000000000000000000000000000001";
8896
+ if (iface === "uniswap_v2" && protocol.contracts?.["lb_factory"]) {
8897
+ if (!opts.pool) throw new Error("--pool is required for Merchant Moe LB remove");
8898
+ if (!opts.bins) throw new Error("--bins <id1,id2,...> is required for Merchant Moe LB remove");
8899
+ const lbAdapter = createMerchantMoeLB(protocol, rpcUrl);
8900
+ const tokenA2 = opts.tokenA.startsWith("0x") ? opts.tokenA : registry.resolveToken(chainName, opts.tokenA).address;
8901
+ const tokenB2 = opts.tokenB.startsWith("0x") ? opts.tokenB : registry.resolveToken(chainName, opts.tokenB).address;
8902
+ const [tokenX, tokenY] = tokenA2.toLowerCase() < tokenB2.toLowerCase() ? [tokenA2, tokenB2] : [tokenB2, tokenA2];
8903
+ const binIds = opts.bins.split(",").map((s) => parseInt(s.trim()));
8904
+ const client = createPublicClient23({ transport: http23(rpcUrl) });
8905
+ const binStep = await client.readContract({
8906
+ address: opts.pool,
8907
+ abi: parseAbi30(["function getBinStep() view returns (uint16)"]),
8908
+ functionName: "getBinStep"
8909
+ });
8910
+ let amounts;
8911
+ if (opts.amounts) {
8912
+ amounts = opts.amounts.split(",").map((s) => BigInt(s.trim()));
8913
+ } else {
8914
+ const balanceAbi = parseAbi30(["function balanceOf(address account, uint256 id) view returns (uint256)"]);
8915
+ amounts = await Promise.all(binIds.map(
8916
+ (id) => client.readContract({ address: opts.pool, abi: balanceAbi, functionName: "balanceOf", args: [recipient, BigInt(id)] })
8917
+ ));
8918
+ }
8919
+ const approveForAllAbi = parseAbi30(["function approveForAll(address operator, bool approved) external"]);
8920
+ const lbRouter = protocol.contracts["lb_router"];
8921
+ const approveTx = {
8922
+ description: `[${protocol.name}] approveForAll LBPair \u2192 LBRouter`,
8923
+ to: opts.pool,
8924
+ data: encodeFunctionData27({ abi: approveForAllAbi, functionName: "approveForAll", args: [lbRouter, true] }),
8925
+ value: 0n,
8926
+ gas_estimate: 8e4
8927
+ };
8928
+ const tx = await lbAdapter.buildRemoveLiquidity({
8929
+ tokenX,
8930
+ tokenY,
8931
+ binStep,
8932
+ binIds,
8933
+ amounts,
8934
+ recipient
8935
+ });
8936
+ tx.pre_txs = [approveTx, ...tx.pre_txs ?? []];
8937
+ const result = await executor.execute(tx);
8938
+ printOutput({ step: "lb_remove", ...result }, getOpts());
8939
+ return;
8940
+ }
7766
8941
  const tokenA = opts.tokenA.startsWith("0x") ? opts.tokenA : registry.resolveToken(chainName, opts.tokenA).address;
7767
8942
  const tokenB = opts.tokenB.startsWith("0x") ? opts.tokenB : registry.resolveToken(chainName, opts.tokenB).address;
7768
8943
  const poolAddr = opts.pool ? opts.pool : void 0;
@@ -7778,7 +8953,7 @@ function registerLP(parent, getOpts, makeExecutor2) {
7778
8953
  return;
7779
8954
  }
7780
8955
  didUnstake = true;
7781
- } else if (["solidly_v2", "solidly_cl", "hybra"].includes(iface)) {
8956
+ } else if (["solidly_v2", "solidly_cl", "hybra"].includes(iface) || iface === "uniswap_v3" && protocol.contracts?.["voter"]) {
7782
8957
  let gaugeAddr = opts.gauge;
7783
8958
  if (!gaugeAddr && poolAddr) {
7784
8959
  try {
@@ -7789,11 +8964,32 @@ function registerLP(parent, getOpts, makeExecutor2) {
7789
8964
  } catch {
7790
8965
  }
7791
8966
  }
8967
+ if (gaugeAddr) {
8968
+ if (iface === "solidly_v2" && !opts.tokenId) {
8969
+ const erc20Abi3 = parseAbi30(["function balanceOf(address) view returns (uint256)"]);
8970
+ const gClient = createPublicClient23({ transport: http23(rpcUrl) });
8971
+ const gaugeBal = await gClient.readContract({
8972
+ address: gaugeAddr,
8973
+ abi: erc20Abi3,
8974
+ functionName: "balanceOf",
8975
+ args: [recipient]
8976
+ });
8977
+ if (gaugeBal === 0n) {
8978
+ process.stderr.write("Step 1/2: Skipped unstake \u2014 gauge balance 0 (already unstaked).\n");
8979
+ didUnstake = true;
8980
+ gaugeAddr = void 0;
8981
+ }
8982
+ }
8983
+ }
7792
8984
  if (gaugeAddr) {
7793
8985
  process.stderr.write("Step 1/2: Withdrawing from gauge...\n");
7794
8986
  const gaugeAdapter = createGauge(protocol, rpcUrl);
7795
8987
  const tokenId = opts.tokenId ? BigInt(opts.tokenId) : void 0;
7796
- const withdrawTx = await gaugeAdapter.buildWithdraw(gaugeAddr, BigInt(opts.liquidity), tokenId);
8988
+ const wOpts = opts.redeemType !== void 0 ? { redeemType: parseInt(opts.redeemType, 10) } : void 0;
8989
+ if (iface === "hybra" && (!wOpts || wOpts.redeemType === 1)) {
8990
+ 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");
8991
+ }
8992
+ const withdrawTx = await gaugeAdapter.buildWithdraw(gaugeAddr, BigInt(opts.liquidity), tokenId, wOpts);
7797
8993
  const withdrawResult = await executor.execute(withdrawTx);
7798
8994
  printOutput({ step: "unstake_gauge", ...withdrawResult }, getOpts());
7799
8995
  if (withdrawResult.status !== "confirmed" && withdrawResult.status !== "simulated") {
@@ -7813,11 +9009,33 @@ function registerLP(parent, getOpts, makeExecutor2) {
7813
9009
  token_a: tokenA,
7814
9010
  token_b: tokenB,
7815
9011
  liquidity: BigInt(opts.liquidity),
7816
- recipient
9012
+ recipient,
9013
+ token_id: opts.tokenId ? BigInt(opts.tokenId) : void 0
7817
9014
  });
7818
9015
  const removeResult = await executor.execute(removeTx);
7819
9016
  printOutput({ step: "lp_remove", ...removeResult }, getOpts());
7820
9017
  });
9018
+ 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) => {
9019
+ const executor = makeExecutor2();
9020
+ const chainName = parent.opts().chain;
9021
+ if (!chainName) {
9022
+ printOutput({ error: "--chain is required (e.g. --chain hyperevm)" }, getOpts());
9023
+ return;
9024
+ }
9025
+ const registry = Registry.loadEmbedded();
9026
+ const chain = registry.getChain(chainName);
9027
+ const rpcUrl = chain.effectiveRpcUrl();
9028
+ const protocol = registry.getProtocol(opts.protocol);
9029
+ const recipient = resolveAccount(opts.address, lp.opts().wallet);
9030
+ const adapter = createDex(protocol, rpcUrl);
9031
+ if (!("buildCompound" in adapter) || typeof adapter.buildCompound !== "function") {
9032
+ throw new Error(`[${protocol.name}] adapter does not support compound (v1 supports V3 fee-only protocols)`);
9033
+ }
9034
+ const compoundOpts = opts.slippage !== void 0 ? { slippageBps: parseInt(opts.slippage, 10) } : void 0;
9035
+ const tx = await adapter.buildCompound(BigInt(opts.tokenId), recipient, compoundOpts);
9036
+ const result = await executor.execute(tx);
9037
+ printOutput(result, getOpts());
9038
+ });
7821
9039
  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) => {
7822
9040
  const chainName = parent.opts().chain;
7823
9041
  if (!chainName) {
@@ -7835,17 +9053,68 @@ function registerLP(parent, getOpts, makeExecutor2) {
7835
9053
  protocols.map(async (protocol) => {
7836
9054
  try {
7837
9055
  if (protocol.interface === "uniswap_v2" && protocol.contracts?.["lb_factory"]) {
7838
- if (!opts.pool) return;
7839
9056
  const adapter = createMerchantMoeLB(protocol, rpcUrl);
7840
9057
  const binIds = opts.bins ? opts.bins.split(",").map((s) => parseInt(s.trim())) : void 0;
7841
- const positions = await adapter.getUserPositions(user, opts.pool, binIds);
7842
- for (const pos of positions) {
7843
- results.push({
7844
- protocol: protocol.slug,
7845
- type: "lb",
7846
- pool: opts.pool,
7847
- ...pos
7848
- });
9058
+ const poolsToScan = opts.pool ? [opts.pool] : (await adapter.discoverRewardedPools()).map((p) => p.pool);
9059
+ for (const poolAddr of poolsToScan) {
9060
+ try {
9061
+ const userBins = binIds ?? await adapter.findUserBinsWithBalance(poolAddr, user);
9062
+ if (userBins.length === 0) continue;
9063
+ const positions = await adapter.getUserPositions(user, poolAddr, userBins);
9064
+ if (positions.length === 0) continue;
9065
+ const pending = await adapter.getPendingRewards(user, poolAddr, userBins).catch(() => []);
9066
+ const totalPending = pending.reduce((s, r) => s + (r.amount ?? 0n), 0n);
9067
+ for (const pos of positions) {
9068
+ results.push({
9069
+ protocol: protocol.slug,
9070
+ type: "lb",
9071
+ pool: poolAddr,
9072
+ ...pos,
9073
+ pending_reward: totalPending.toString(),
9074
+ pending_reward_token: pending[0]?.token
9075
+ });
9076
+ }
9077
+ } catch {
9078
+ }
9079
+ }
9080
+ }
9081
+ const npm = protocol.contracts?.["position_manager"];
9082
+ if (npm && ["uniswap_v3", "algebra_v3", "hybra"].includes(protocol.interface)) {
9083
+ const npmAbi = parseAbi30([
9084
+ "function balanceOf(address owner) view returns (uint256)",
9085
+ "function tokenOfOwnerByIndex(address owner, uint256 index) view returns (uint256)"
9086
+ ]);
9087
+ const client = createPublicClient23({ transport: http23(rpcUrl) });
9088
+ let count;
9089
+ try {
9090
+ count = await client.readContract({ address: npm, abi: npmAbi, functionName: "balanceOf", args: [user] });
9091
+ } catch {
9092
+ return;
9093
+ }
9094
+ const max = Math.min(Number(count), 50);
9095
+ for (let i = 0; i < max; i++) {
9096
+ try {
9097
+ const tokenId = await client.readContract({
9098
+ address: npm,
9099
+ abi: npmAbi,
9100
+ functionName: "tokenOfOwnerByIndex",
9101
+ args: [user, BigInt(i)]
9102
+ });
9103
+ const liq = await detectV3Liquidity(client, npm, tokenId);
9104
+ if (liq && liq.liquidity > 0n) {
9105
+ results.push({
9106
+ protocol: protocol.slug,
9107
+ type: "v3_nft",
9108
+ token_id: tokenId.toString(),
9109
+ token0: liq.token0,
9110
+ token1: liq.token1,
9111
+ liquidity: liq.liquidity.toString(),
9112
+ tickLower: liq.tickLower,
9113
+ tickUpper: liq.tickUpper
9114
+ });
9115
+ }
9116
+ } catch {
9117
+ }
7849
9118
  }
7850
9119
  }
7851
9120
  } catch {
@@ -9749,10 +11018,9 @@ function registerBridge(parent, getOpts) {
9749
11018
 
9750
11019
  // src/commands/swap.ts
9751
11020
  init_dist();
9752
- var CHAIN_NAMES = {
9753
- hyperevm: { kyber: "hyperevm", openocean: "hyperevm" },
9754
- mantle: { openocean: "mantle" }
9755
- };
11021
+ function getAggregatorSlugs(chainCfg) {
11022
+ return chainCfg.aggregators ?? {};
11023
+ }
9756
11024
  var KYBER_API = "https://aggregator-api.kyberswap.com";
9757
11025
  async function kyberGetQuote(chain, tokenIn, tokenOut, amountIn) {
9758
11026
  const params = new URLSearchParams({ tokenIn, tokenOut, amountIn });
@@ -9804,6 +11072,64 @@ async function openoceanSwap(chain, inTokenAddress, outTokenAddress, amountIn, s
9804
11072
  outAmount: String(data.outAmount ?? "0")
9805
11073
  };
9806
11074
  }
11075
+ var LIFI_API2 = "https://li.quest/v1";
11076
+ async function lifiQuote(chainId, fromToken, toToken, fromAmount, fromAddress, slippagePct) {
11077
+ const params = new URLSearchParams({
11078
+ fromChain: String(chainId),
11079
+ toChain: String(chainId),
11080
+ fromToken,
11081
+ toToken,
11082
+ fromAmount,
11083
+ fromAddress,
11084
+ slippage: (Number(slippagePct) / 100).toFixed(4)
11085
+ });
11086
+ const url = `${LIFI_API2}/quote?${params}`;
11087
+ const res = await fetch(url);
11088
+ if (!res.ok) throw new Error(`LI.FI quote failed: ${res.status} ${await res.text()}`);
11089
+ const json = await res.json();
11090
+ const txReq = json.transactionRequest;
11091
+ if (!txReq) throw new Error("LI.FI: no transactionRequest in response");
11092
+ const estimate = json.estimate;
11093
+ return {
11094
+ to: String(txReq.to),
11095
+ data: String(txReq.data),
11096
+ value: String(txReq.value ?? "0x0"),
11097
+ outAmount: String(estimate?.toAmount ?? "0")
11098
+ };
11099
+ }
11100
+ var RELAY_API = "https://api.relay.link";
11101
+ async function relayQuote(chainId, fromToken, toToken, amount, user) {
11102
+ const body = {
11103
+ user,
11104
+ originChainId: chainId,
11105
+ destinationChainId: chainId,
11106
+ originCurrency: fromToken,
11107
+ destinationCurrency: toToken,
11108
+ recipient: user,
11109
+ tradeType: "EXACT_INPUT",
11110
+ amount
11111
+ };
11112
+ const res = await fetch(`${RELAY_API}/quote`, {
11113
+ method: "POST",
11114
+ headers: { "content-type": "application/json" },
11115
+ body: JSON.stringify(body)
11116
+ });
11117
+ if (!res.ok) throw new Error(`Relay quote failed: ${res.status} ${await res.text()}`);
11118
+ const json = await res.json();
11119
+ const steps = json.steps;
11120
+ const swapStep = steps?.find((s) => s.id !== "approve") ?? steps?.[steps.length - 1];
11121
+ const items = swapStep?.items;
11122
+ const txData = items?.[0]?.data;
11123
+ if (!txData) throw new Error("Relay: no swap step in quote");
11124
+ const details = json.details;
11125
+ const currencyOut = details?.currencyOut;
11126
+ return {
11127
+ to: String(txData.to),
11128
+ data: String(txData.data),
11129
+ value: String(txData.value ?? "0x0"),
11130
+ outAmount: String(currencyOut?.amount ?? "0")
11131
+ };
11132
+ }
9807
11133
  var LIQD_API = "https://api.liqd.ag/v2";
9808
11134
  var LIQD_ROUTER = "0x744489ee3d540777a66f2cf297479745e0852f7a";
9809
11135
  async function liquidSwapRoute(tokenIn, tokenOut, amountIn, slippagePct) {
@@ -9823,7 +11149,7 @@ async function liquidSwapRoute(tokenIn, tokenOut, amountIn, slippagePct) {
9823
11149
  };
9824
11150
  }
9825
11151
  function registerSwap(parent, getOpts, makeExecutor2) {
9826
- 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) => {
11152
+ 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) => {
9827
11153
  const executor = makeExecutor2();
9828
11154
  const chainName = requireChain(parent, getOpts);
9829
11155
  if (!chainName) return;
@@ -9834,12 +11160,13 @@ function registerSwap(parent, getOpts, makeExecutor2) {
9834
11160
  const toAddr = resolveTokenAddress(registry, chainName, opts.to);
9835
11161
  const wallet = resolveWallet();
9836
11162
  if (provider === "kyber") {
9837
- const chainNames = CHAIN_NAMES[chainName];
9838
- if (!chainNames?.kyber) {
9839
- printOutput({ error: `KyberSwap: unsupported chain '${chainName}'. Supported: hyperevm` }, getOpts());
11163
+ const aggCfg = getAggregatorSlugs(registry.getChain(chainName));
11164
+ if (!aggCfg.kyber) {
11165
+ const supported = Array.from(registry.chains.keys()).filter((c) => registry.getChain(c).aggregators?.kyber).join(", ");
11166
+ printOutput({ error: `KyberSwap: unsupported chain '${chainName}'. Supported: ${supported || "(none)"}` }, getOpts());
9840
11167
  return;
9841
11168
  }
9842
- const kyberChain = chainNames.kyber;
11169
+ const kyberChain = aggCfg.kyber;
9843
11170
  try {
9844
11171
  const quoteData = await kyberGetQuote(kyberChain, fromAddr, toAddr, opts.amount);
9845
11172
  const routeSummary = quoteData.routeSummary;
@@ -9875,12 +11202,13 @@ function registerSwap(parent, getOpts, makeExecutor2) {
9875
11202
  return;
9876
11203
  }
9877
11204
  if (provider === "openocean") {
9878
- const chainNames = CHAIN_NAMES[chainName];
9879
- if (!chainNames) {
9880
- printOutput({ error: `OpenOcean: unsupported chain '${chainName}'. Supported: ${Object.keys(CHAIN_NAMES).join(", ")}` }, getOpts());
11205
+ const aggCfg = getAggregatorSlugs(registry.getChain(chainName));
11206
+ if (!aggCfg.openocean) {
11207
+ const supported = Array.from(registry.chains.keys()).filter((c) => registry.getChain(c).aggregators?.openocean).join(", ");
11208
+ printOutput({ error: `OpenOcean: unsupported chain '${chainName}'. Supported: ${supported || "(none)"}` }, getOpts());
9881
11209
  return;
9882
11210
  }
9883
- const ooChain = chainNames.openocean;
11211
+ const ooChain = aggCfg.openocean;
9884
11212
  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());
9885
11213
  const fromDecimals = fromToken?.decimals ?? 18;
9886
11214
  const humanAmount = (Number(opts.amount) / 10 ** fromDecimals).toString();
@@ -9948,7 +11276,41 @@ function registerSwap(parent, getOpts, makeExecutor2) {
9948
11276
  }
9949
11277
  return;
9950
11278
  }
9951
- printOutput({ error: `Unknown provider '${opts.provider}'. Choose: kyber, openocean, liquid` }, getOpts());
11279
+ if (provider === "lifi" || provider === "relay") {
11280
+ const chainCfg = registry.getChain(chainName);
11281
+ const chainId = chainCfg.chain_id;
11282
+ if (!chainId) {
11283
+ printOutput({ error: `${provider}: chain '${chainName}' has no chain_id in registry` }, getOpts());
11284
+ return;
11285
+ }
11286
+ const slippagePct = (slippageBps / 100).toFixed(2);
11287
+ try {
11288
+ const route = provider === "lifi" ? await lifiQuote(chainId, fromAddr, toAddr, opts.amount, wallet, slippagePct) : await relayQuote(chainId, fromAddr, toAddr, opts.amount, wallet);
11289
+ const tx = {
11290
+ description: `${provider}: swap ${opts.amount} of ${fromAddr} -> ${toAddr}`,
11291
+ to: route.to,
11292
+ data: route.data,
11293
+ value: parseBigIntValue(route.value),
11294
+ approvals: [{ token: fromAddr, spender: route.to, amount: BigInt(opts.amount) }]
11295
+ };
11296
+ const result = await executor.execute(tx);
11297
+ printOutput({
11298
+ provider,
11299
+ chain: chainName,
11300
+ chain_id: chainId,
11301
+ from_token: fromAddr,
11302
+ to_token: toAddr,
11303
+ amount_in: opts.amount,
11304
+ amount_out: route.outAmount,
11305
+ router: route.to,
11306
+ ...result
11307
+ }, getOpts());
11308
+ } catch (e) {
11309
+ printOutput({ error: `${provider} error: ${errMsg(e)}` }, getOpts());
11310
+ }
11311
+ return;
11312
+ }
11313
+ printOutput({ error: `Unknown provider '${opts.provider}'. Choose: kyber, openocean, liquid, lifi, relay` }, getOpts());
9952
11314
  });
9953
11315
  }
9954
11316
 
@@ -10000,8 +11362,8 @@ function isValidPrivateKey(s) {
10000
11362
  }
10001
11363
  async function deriveAddress(privateKey) {
10002
11364
  try {
10003
- const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
10004
- const account = privateKeyToAccount2(privateKey);
11365
+ const { privateKeyToAccount: privateKeyToAccount3 } = await import("viem/accounts");
11366
+ const account = privateKeyToAccount3(privateKey);
10005
11367
  return account.address;
10006
11368
  } catch {
10007
11369
  return null;