@hypurrquant/defi-cli 1.0.0 → 1.0.2

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.
@@ -5724,6 +5724,7 @@ var init_dist2 = __esm({
5724
5724
  }
5725
5725
  };
5726
5726
  CTOKEN_ABI = parseAbi17([
5727
+ "function underlying() external view returns (address)",
5727
5728
  "function supplyRatePerBlock() external view returns (uint256)",
5728
5729
  "function borrowRatePerBlock() external view returns (uint256)",
5729
5730
  "function totalSupply() external view returns (uint256)",
@@ -5737,7 +5738,10 @@ var init_dist2 = __esm({
5737
5738
  CompoundV2Adapter = class {
5738
5739
  protocolName;
5739
5740
  defaultVtoken;
5741
+ vTokenCandidates;
5740
5742
  rpcUrl;
5743
+ // Lazy cache: underlying asset address (lowercased) → vToken address
5744
+ vTokenByAsset = null;
5741
5745
  constructor(entry, rpcUrl) {
5742
5746
  this.protocolName = entry.name;
5743
5747
  this.rpcUrl = rpcUrl;
@@ -5745,6 +5749,26 @@ var init_dist2 = __esm({
5745
5749
  const vtoken = contracts["vusdt"] ?? contracts["vusdc"] ?? contracts["vbnb"] ?? contracts["comptroller"];
5746
5750
  if (!vtoken) throw DefiError.contractError("Missing vToken or comptroller address");
5747
5751
  this.defaultVtoken = vtoken;
5752
+ this.vTokenCandidates = Object.entries(contracts).filter(([k]) => /^v[a-z][a-z0-9]*$/i.test(k)).map(([, v]) => v);
5753
+ if (this.vTokenCandidates.length === 0) this.vTokenCandidates = [vtoken];
5754
+ }
5755
+ async resolveVtoken(asset) {
5756
+ if (!this.rpcUrl) return null;
5757
+ if (!this.vTokenByAsset) {
5758
+ const client = createPublicClient13({ transport: http13(this.rpcUrl) });
5759
+ const map = /* @__PURE__ */ new Map();
5760
+ const lookups = await Promise.allSettled(
5761
+ this.vTokenCandidates.map(async (v) => {
5762
+ const u = await client.readContract({ address: v, abi: CTOKEN_ABI, functionName: "underlying" });
5763
+ return [u.toLowerCase(), v];
5764
+ })
5765
+ );
5766
+ for (const r of lookups) {
5767
+ if (r.status === "fulfilled") map.set(r.value[0], r.value[1]);
5768
+ }
5769
+ this.vTokenByAsset = map;
5770
+ }
5771
+ return this.vTokenByAsset.get(asset.toLowerCase()) ?? null;
5748
5772
  }
5749
5773
  name() {
5750
5774
  return this.protocolName;
@@ -5808,15 +5832,27 @@ var init_dist2 = __esm({
5808
5832
  async getRates(asset) {
5809
5833
  if (!this.rpcUrl) throw DefiError.rpcError("No RPC URL configured");
5810
5834
  const client = createPublicClient13({ transport: http13(this.rpcUrl) });
5835
+ const vtoken = await this.resolveVtoken(asset);
5836
+ if (!vtoken) {
5837
+ return {
5838
+ protocol: this.protocolName,
5839
+ asset,
5840
+ supply_apy: 0,
5841
+ borrow_variable_apy: 0,
5842
+ utilization: 0,
5843
+ total_supply: 0n,
5844
+ total_borrow: 0n
5845
+ };
5846
+ }
5811
5847
  const [supplyRate, borrowRate, totalSupply, totalBorrows] = await Promise.all([
5812
- client.readContract({ address: this.defaultVtoken, abi: CTOKEN_ABI, functionName: "supplyRatePerBlock" }).catch((e) => {
5848
+ client.readContract({ address: vtoken, abi: CTOKEN_ABI, functionName: "supplyRatePerBlock" }).catch((e) => {
5813
5849
  throw DefiError.rpcError(`[${this.protocolName}] supplyRatePerBlock failed: ${e}`);
5814
5850
  }),
5815
- client.readContract({ address: this.defaultVtoken, abi: CTOKEN_ABI, functionName: "borrowRatePerBlock" }).catch((e) => {
5851
+ client.readContract({ address: vtoken, abi: CTOKEN_ABI, functionName: "borrowRatePerBlock" }).catch((e) => {
5816
5852
  throw DefiError.rpcError(`[${this.protocolName}] borrowRatePerBlock failed: ${e}`);
5817
5853
  }),
5818
- client.readContract({ address: this.defaultVtoken, abi: CTOKEN_ABI, functionName: "totalSupply" }).catch(() => 0n),
5819
- client.readContract({ address: this.defaultVtoken, abi: CTOKEN_ABI, functionName: "totalBorrows" }).catch(() => 0n)
5854
+ client.readContract({ address: vtoken, abi: CTOKEN_ABI, functionName: "totalSupply" }).catch(() => 0n),
5855
+ client.readContract({ address: vtoken, abi: CTOKEN_ABI, functionName: "totalBorrows" }).catch(() => 0n)
5820
5856
  ]);
5821
5857
  const supplyPerBlock = Number(supplyRate) / 1e18;
5822
5858
  const borrowPerBlock = Number(borrowRate) / 1e18;
@@ -5842,6 +5878,7 @@ var init_dist2 = __esm({
5842
5878
  }
5843
5879
  };
5844
5880
  COMET_ABI = parseAbi18([
5881
+ "function baseToken() external view returns (address)",
5845
5882
  "function getUtilization() external view returns (uint256)",
5846
5883
  "function getSupplyRate(uint256 utilization) external view returns (uint64)",
5847
5884
  "function getBorrowRate(uint256 utilization) external view returns (uint64)",
@@ -5927,6 +5964,24 @@ var init_dist2 = __esm({
5927
5964
  async getRates(asset) {
5928
5965
  if (!this.rpcUrl) throw DefiError.rpcError("No RPC URL configured");
5929
5966
  const client = createPublicClient14({ transport: http14(this.rpcUrl) });
5967
+ const baseToken = await client.readContract({
5968
+ address: this.comet,
5969
+ abi: COMET_ABI,
5970
+ functionName: "baseToken"
5971
+ }).catch((e) => {
5972
+ throw DefiError.rpcError(`[${this.protocolName}] baseToken failed: ${e}`);
5973
+ });
5974
+ if (baseToken.toLowerCase() !== asset.toLowerCase()) {
5975
+ return {
5976
+ protocol: this.protocolName,
5977
+ asset,
5978
+ supply_apy: 0,
5979
+ borrow_variable_apy: 0,
5980
+ utilization: 0,
5981
+ total_supply: 0n,
5982
+ total_borrow: 0n
5983
+ };
5984
+ }
5930
5985
  const utilization = await client.readContract({
5931
5986
  address: this.comet,
5932
5987
  abi: COMET_ABI,
@@ -7543,7 +7598,7 @@ server.tool(
7543
7598
  "defi_status",
7544
7599
  "Show chain and protocol status: lists all protocols deployed on a chain with contract addresses and categories",
7545
7600
  {
7546
- chain: z.string().optional().describe("Chain name (default: hyperevm). E.g. hyperevm, ethereum, arbitrum, base")
7601
+ chain: z.string().optional().describe("Chain name (default: hyperevm). One of: hyperevm, mantle, base, bnb, monad")
7547
7602
  },
7548
7603
  async ({ chain }) => {
7549
7604
  try {
@@ -7800,8 +7855,8 @@ server.tool(
7800
7855
  "defi_bridge",
7801
7856
  "Get a cross-chain bridge quote via LI.FI, deBridge DLN, or Circle CCTP. Returns estimated output amount and fees",
7802
7857
  {
7803
- from_chain: z.string().describe("Source chain name, e.g. ethereum, arbitrum, base"),
7804
- to_chain: z.string().describe("Destination chain name, e.g. hyperevm, arbitrum"),
7858
+ from_chain: z.string().describe("Source chain name (supported source chains: hyperevm, mantle, base, bnb, monad)"),
7859
+ to_chain: z.string().describe("Destination chain name. CCTP V2 destinations include ethereum, arbitrum, optimism, polygon, avalanche; LI.FI/deBridge accept any chain"),
7805
7860
  token: z.string().optional().describe("Token symbol to bridge (default: USDC). Use native for native token"),
7806
7861
  amount: z.string().describe("Amount in human-readable units, e.g. '100' for 100 USDC"),
7807
7862
  recipient: z.string().optional().describe("Recipient address on destination chain")
@@ -8465,6 +8520,352 @@ server.tool(
8465
8520
  };
8466
8521
  }
8467
8522
  );
8523
+ server.tool(
8524
+ "defi_lp_compound",
8525
+ "V3 fee auto-compound: collect accrued LP fees and re-add them as liquidity in one tx. Requires NFT tokenId. Returns simulated/confirmed tx with collected fee amounts.",
8526
+ {
8527
+ chain: z.string().describe("Chain name (e.g. base, hyperevm, mantle)"),
8528
+ protocol: z.string().describe("Protocol slug (e.g. uniswap-v3-base, hyperswap, project-x)"),
8529
+ token_id: z.string().describe("NFT tokenId of the LP position to compound"),
8530
+ slippage_bps: z.number().optional().describe("Slippage tolerance in bps (default: 50 = 0.5%)"),
8531
+ address: z.string().optional().describe("Recipient address (defaults to DEFI_WALLET_ADDRESS)"),
8532
+ broadcast: z.boolean().optional().describe("Broadcast tx (default: false = dry-run)")
8533
+ },
8534
+ async ({ chain, protocol, token_id, slippage_bps, address, broadcast }) => {
8535
+ try {
8536
+ const chainName = chain.toLowerCase();
8537
+ const registry = getRegistry();
8538
+ const chainConfig = registry.getChain(chainName);
8539
+ const rpcUrl = chainConfig.effectiveRpcUrl();
8540
+ const proto = registry.getProtocol(protocol);
8541
+ const recipient = address ?? process.env["DEFI_WALLET_ADDRESS"];
8542
+ if (!recipient) throw new Error("address required (or set DEFI_WALLET_ADDRESS)");
8543
+ const adapter = createDex(proto, rpcUrl);
8544
+ if (!("buildCompound" in adapter) || typeof adapter.buildCompound !== "function") {
8545
+ throw new Error(`[${proto.name}] adapter does not support compound (V3 fee-only protocols only)`);
8546
+ }
8547
+ const compoundOpts = slippage_bps !== void 0 ? { slippageBps: slippage_bps } : void 0;
8548
+ const tx = await adapter.buildCompound(BigInt(token_id), recipient, compoundOpts);
8549
+ const executor = makeExecutor(broadcast ?? false, rpcUrl, chainConfig.explorer_url);
8550
+ const result = await executor.execute(tx);
8551
+ return {
8552
+ content: [{ type: "text", text: ok(result, { chain: chainName, protocol, token_id, broadcast: broadcast ?? false }) }]
8553
+ };
8554
+ } catch (e) {
8555
+ return { content: [{ type: "text", text: err(e instanceof Error ? e.message : String(e), { chain, protocol, token_id }) }], isError: true };
8556
+ }
8557
+ }
8558
+ );
8559
+ server.tool(
8560
+ "defi_lp_claim",
8561
+ "Claim LP rewards from a pool. Auto-dispatches by protocol interface: V3 fees (NPM.collect), gauge emission (Solidly/Hybra/Ramses CL), Merchant Moe LB hooks, KittenSwap eternal farming, off-chain Nest tickets. Requires either token-id (CL/NFT positions) or gauge (V2-style staking) plus pool for LB.",
8562
+ {
8563
+ chain: z.string().describe("Chain name"),
8564
+ protocol: z.string().describe("Protocol slug"),
8565
+ pool: z.string().optional().describe("Pool address (required for LB / KittenSwap farming)"),
8566
+ gauge: z.string().optional().describe("Gauge address (required for solidly/hybra)"),
8567
+ token_id: z.string().optional().describe("NFT tokenId (CL gauge or farming positions)"),
8568
+ bins: z.string().optional().describe("Comma-separated bin IDs (Merchant Moe LB; auto-detected if omitted)"),
8569
+ address: z.string().optional().describe("Wallet address (defaults to DEFI_WALLET_ADDRESS)"),
8570
+ redeem_type: z.number().optional().describe("Hybra: 0=instant exit (penalty), 1=2yr veHYBR lock (default)"),
8571
+ broadcast: z.boolean().optional().describe("Broadcast tx (default: false = dry-run)")
8572
+ },
8573
+ async ({ chain, protocol, pool, gauge, token_id, bins, address, redeem_type, broadcast }) => {
8574
+ try {
8575
+ const chainName = chain.toLowerCase();
8576
+ const registry = getRegistry();
8577
+ const chainConfig = registry.getChain(chainName);
8578
+ const rpcUrl = chainConfig.effectiveRpcUrl();
8579
+ const proto = registry.getProtocol(protocol);
8580
+ const account = address ?? process.env["DEFI_WALLET_ADDRESS"];
8581
+ if (!account) throw new Error("address required (or set DEFI_WALLET_ADDRESS)");
8582
+ const iface = proto.interface;
8583
+ const executor = makeExecutor(broadcast ?? false, rpcUrl, chainConfig.explorer_url);
8584
+ const { createKittenSwapFarming: createKittenSwapFarming2, createMerchantMoeLB: createMerchantMoeLB2, createGauge: createGauge2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
8585
+ const isV3Fee = proto.reward_strategy === "lp_fee_only" || iface === "uniswap_v3" && token_id && !gauge;
8586
+ if (isV3Fee) {
8587
+ if (!token_id) throw new Error("token_id required for V3 fee collection");
8588
+ const adapter = createDex(proto, rpcUrl);
8589
+ if (!("buildCollectFees" in adapter) || typeof adapter.buildCollectFees !== "function") {
8590
+ throw new Error(`[${proto.name}] adapter does not support buildCollectFees`);
8591
+ }
8592
+ const tx = await adapter.buildCollectFees(BigInt(token_id), account);
8593
+ const result = await executor.execute(tx);
8594
+ return { content: [{ type: "text", text: ok(result, { chain: chainName, protocol, kind: "v3_fee", broadcast: broadcast ?? false }) }] };
8595
+ }
8596
+ if (iface === "algebra_v3" && proto.contracts?.["farming_center"]) {
8597
+ if (!pool) throw new Error("pool required for KittenSwap farming claim");
8598
+ if (!token_id) throw new Error("token_id required for KittenSwap farming claim");
8599
+ const adapter = createKittenSwapFarming2(proto, rpcUrl);
8600
+ const tx = await adapter.buildCollectRewards(BigInt(token_id), pool, account);
8601
+ const result = await executor.execute(tx);
8602
+ return { content: [{ type: "text", text: ok(result, { chain: chainName, protocol, kind: "kittenswap_farming", broadcast: broadcast ?? false }) }] };
8603
+ }
8604
+ if (iface === "uniswap_v2" && proto.contracts?.["lb_factory"]) {
8605
+ if (!pool) throw new Error("pool required for Merchant Moe LB claim");
8606
+ const adapter = createMerchantMoeLB2(proto, rpcUrl);
8607
+ const binIds = bins ? bins.split(",").map((s) => parseInt(s.trim(), 10)) : void 0;
8608
+ const tx = await adapter.buildClaimRewards(account, pool, binIds);
8609
+ const result = await executor.execute(tx);
8610
+ return { content: [{ type: "text", text: ok(result, { chain: chainName, protocol, kind: "lb", broadcast: broadcast ?? false }) }] };
8611
+ }
8612
+ if (["solidly_v2", "solidly_cl", "algebra_v3", "hybra"].includes(iface) || iface === "uniswap_v3" && proto.contracts?.["voter"]) {
8613
+ if (!gauge) throw new Error("gauge required for gauge-based claim");
8614
+ const adapter = createGauge2(proto, rpcUrl);
8615
+ let tx;
8616
+ if (token_id && iface === "uniswap_v3" && proto.reward_strategy === "auto_stake" && "buildClaimRewardsViaNPMPeriodReward" in adapter && typeof adapter.buildClaimRewardsViaNPMPeriodReward === "function") {
8617
+ const npm = proto.contracts?.["position_manager"];
8618
+ if (!npm) throw new Error(`${proto.name} requires contracts.position_manager for NPM-based claim`);
8619
+ tx = await adapter.buildClaimRewardsViaNPMPeriodReward(npm, BigInt(token_id), account, { gauge });
8620
+ } else if (token_id) {
8621
+ if (!adapter.buildClaimRewardsByTokenId) throw new Error(`${proto.name} does not support NFT-based claim`);
8622
+ const claimOpts = redeem_type !== void 0 ? { redeemType: redeem_type } : void 0;
8623
+ tx = await adapter.buildClaimRewardsByTokenId(gauge, BigInt(token_id), claimOpts);
8624
+ } else {
8625
+ tx = await adapter.buildClaimRewards(gauge, account);
8626
+ }
8627
+ const result = await executor.execute(tx);
8628
+ return { content: [{ type: "text", text: ok(result, { chain: chainName, protocol, kind: "gauge", broadcast: broadcast ?? false }) }] };
8629
+ }
8630
+ throw new Error(`No claim method found for protocol interface '${iface}'`);
8631
+ } catch (e) {
8632
+ return { content: [{ type: "text", text: err(e instanceof Error ? e.message : String(e), { chain, protocol }) }], isError: true };
8633
+ }
8634
+ }
8635
+ );
8636
+ server.tool(
8637
+ "defi_lp_farm",
8638
+ "Add liquidity and auto-stake into gauge/farming for emissions. Two-step flow: mint LP NFT, then deposit into gauge (Solidly/Hybra) or enterFarming (KittenSwap eternal). Merchant Moe LB hooks need no staking.",
8639
+ {
8640
+ chain: z.string().describe("Chain name"),
8641
+ protocol: z.string().describe("Protocol slug"),
8642
+ token_a: z.string().describe("First token symbol or address"),
8643
+ token_b: z.string().describe("Second token symbol or address"),
8644
+ amount_a: z.string().describe("Amount of token A in wei"),
8645
+ amount_b: z.string().describe("Amount of token B in wei"),
8646
+ pool: z.string().optional().describe("Pool name (e.g. WHYPE/USDC) or address"),
8647
+ gauge: z.string().optional().describe("Gauge address (auto-resolved if omitted)"),
8648
+ recipient: z.string().optional().describe("Recipient/owner (defaults to DEFI_WALLET_ADDRESS)"),
8649
+ tick_lower: z.number().optional().describe("Lower tick (CL)"),
8650
+ tick_upper: z.number().optional().describe("Upper tick (CL)"),
8651
+ range_pct: z.number().optional().describe("\xB1N% concentrated range around current price"),
8652
+ broadcast: z.boolean().optional().describe("Broadcast tx (default: false = dry-run)")
8653
+ },
8654
+ async ({ chain, protocol, token_a, token_b, amount_a, amount_b, pool, gauge, recipient, tick_lower, tick_upper, range_pct, broadcast }) => {
8655
+ try {
8656
+ const chainName = chain.toLowerCase();
8657
+ const registry = getRegistry();
8658
+ const chainConfig = registry.getChain(chainName);
8659
+ const rpcUrl = chainConfig.effectiveRpcUrl();
8660
+ const proto = registry.getProtocol(protocol);
8661
+ const owner = recipient ?? process.env["DEFI_WALLET_ADDRESS"] ?? "0x0000000000000000000000000000000000000001";
8662
+ const tokenA = resolveToken(registry, chainName, token_a);
8663
+ const tokenB = resolveToken(registry, chainName, token_b);
8664
+ let poolAddr;
8665
+ if (pool) {
8666
+ poolAddr = pool.startsWith("0x") ? pool : registry.resolvePool(protocol, pool).address;
8667
+ }
8668
+ const dexAdapter = createDex(proto, rpcUrl);
8669
+ const addTx = await dexAdapter.buildAddLiquidity({
8670
+ protocol: proto.name,
8671
+ token_a: tokenA,
8672
+ token_b: tokenB,
8673
+ amount_a: BigInt(amount_a),
8674
+ amount_b: BigInt(amount_b),
8675
+ recipient: owner,
8676
+ tick_lower,
8677
+ tick_upper,
8678
+ range_pct,
8679
+ pool: poolAddr
8680
+ });
8681
+ const executor = makeExecutor(broadcast ?? false, rpcUrl, chainConfig.explorer_url);
8682
+ const addResult = await executor.execute(addTx);
8683
+ const steps = [{ step: "lp_add", ...addResult }];
8684
+ if (addResult.status !== "confirmed" && addResult.status !== "simulated") {
8685
+ return { content: [{ type: "text", text: ok({ steps, note: "LP add did not succeed; staking skipped." }, { chain: chainName, protocol, broadcast: broadcast ?? false }) }] };
8686
+ }
8687
+ const mintedTokenId = addResult.details?.minted_token_id ? BigInt(addResult.details.minted_token_id) : void 0;
8688
+ const iface = proto.interface;
8689
+ const { createKittenSwapFarming: createKittenSwapFarming2, createGauge: createGauge2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
8690
+ if (iface === "algebra_v3" && proto.contracts?.["farming_center"]) {
8691
+ if (!mintedTokenId) {
8692
+ steps.push({ step: "stake_farming", skipped: "no tokenId in dry-run mode" });
8693
+ } else if (!poolAddr) {
8694
+ throw new Error("pool required for KittenSwap farming");
8695
+ } else {
8696
+ const farmAdapter = createKittenSwapFarming2(proto, rpcUrl);
8697
+ const stakeTx = await farmAdapter.buildEnterFarming(mintedTokenId, poolAddr, owner);
8698
+ const stakeResult = await executor.execute(stakeTx);
8699
+ steps.push({ step: "stake_farming", ...stakeResult });
8700
+ }
8701
+ return { content: [{ type: "text", text: ok({ steps }, { chain: chainName, protocol, broadcast: broadcast ?? false }) }] };
8702
+ }
8703
+ const isGaugeStakeable = ["solidly_v2", "solidly_cl", "hybra"].includes(iface) || iface === "uniswap_v3" && proto.contracts?.["voter"];
8704
+ if (isGaugeStakeable) {
8705
+ if (!mintedTokenId && iface !== "solidly_v2") {
8706
+ steps.push({ step: "stake_gauge", skipped: "no tokenId in dry-run mode" });
8707
+ return { content: [{ type: "text", text: ok({ steps }, { chain: chainName, protocol, broadcast: broadcast ?? false }) }] };
8708
+ }
8709
+ let gaugeAddr = gauge;
8710
+ const gaugeAdapter = createGauge2(proto, rpcUrl);
8711
+ if (!gaugeAddr && poolAddr && gaugeAdapter.resolveGauge) {
8712
+ try {
8713
+ gaugeAddr = await gaugeAdapter.resolveGauge(poolAddr);
8714
+ } catch {
8715
+ }
8716
+ }
8717
+ if (!gaugeAddr) throw new Error("gauge required (could not auto-resolve)");
8718
+ let amountArg = 0n;
8719
+ if (iface === "solidly_v2" && poolAddr) {
8720
+ const { createPublicClient: createPublicClient24, http: httpTransport, parseAbi: parseAbi31 } = await import("viem");
8721
+ const erc20Abi3 = parseAbi31(["function balanceOf(address) view returns (uint256)"]);
8722
+ const lpClient = createPublicClient24({ transport: httpTransport(rpcUrl) });
8723
+ amountArg = await lpClient.readContract({
8724
+ address: poolAddr,
8725
+ abi: erc20Abi3,
8726
+ functionName: "balanceOf",
8727
+ args: [owner]
8728
+ });
8729
+ if (amountArg === 0n) {
8730
+ steps.push({ step: "stake_gauge", skipped: "LP balance 0 after mint" });
8731
+ return { content: [{ type: "text", text: ok({ steps }, { chain: chainName, protocol, broadcast: broadcast ?? false }) }] };
8732
+ }
8733
+ }
8734
+ const lpTokenArg = iface === "solidly_v2" ? poolAddr : void 0;
8735
+ const stakeTx = await gaugeAdapter.buildDeposit(gaugeAddr, amountArg, mintedTokenId, lpTokenArg);
8736
+ const stakeResult = await executor.execute(stakeTx);
8737
+ steps.push({ step: "stake_gauge", ...stakeResult });
8738
+ return { content: [{ type: "text", text: ok({ steps }, { chain: chainName, protocol, broadcast: broadcast ?? false }) }] };
8739
+ }
8740
+ if (iface === "uniswap_v2" && proto.contracts?.["lb_factory"]) {
8741
+ steps.push({ step: "stake_gauge", skipped: "Merchant Moe LB hooks auto-handle rewards" });
8742
+ return { content: [{ type: "text", text: ok({ steps }, { chain: chainName, protocol, broadcast: broadcast ?? false }) }] };
8743
+ }
8744
+ steps.push({ step: "stake_gauge", skipped: "No staking adapter for this interface" });
8745
+ return { content: [{ type: "text", text: ok({ steps }, { chain: chainName, protocol, broadcast: broadcast ?? false }) }] };
8746
+ } catch (e) {
8747
+ return { content: [{ type: "text", text: err(e instanceof Error ? e.message : String(e), { chain, protocol }) }], isError: true };
8748
+ }
8749
+ }
8750
+ );
8751
+ server.tool(
8752
+ "defi_lp_positions",
8753
+ "Show all LP positions for a wallet across protocols on a chain. Includes Merchant Moe LB bins (with pending MOE), V3/Algebra/Hybra NFT positions. Auto-detects user's actual bin IDs for LB pools.",
8754
+ {
8755
+ chain: z.string().describe("Chain name"),
8756
+ protocol: z.string().optional().describe("Filter to a single protocol slug"),
8757
+ pool: z.string().optional().describe("Filter to a specific pool address"),
8758
+ bins: z.string().optional().describe("Comma-separated bin IDs (LB; auto-detected if omitted)"),
8759
+ address: z.string().optional().describe("Wallet address (defaults to DEFI_WALLET_ADDRESS)")
8760
+ },
8761
+ async ({ chain, protocol, pool, bins, address }) => {
8762
+ try {
8763
+ const chainName = chain.toLowerCase();
8764
+ const registry = getRegistry();
8765
+ const chainConfig = registry.getChain(chainName);
8766
+ const rpcUrl = chainConfig.effectiveRpcUrl();
8767
+ const user = address ?? process.env["DEFI_WALLET_ADDRESS"];
8768
+ if (!user) throw new Error("address required (or set DEFI_WALLET_ADDRESS)");
8769
+ const { createMerchantMoeLB: createMerchantMoeLB2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
8770
+ const { createPublicClient: createPublicClient24, http: httpTransport, parseAbi: parseAbi31 } = await import("viem");
8771
+ const allProtocols = registry.getProtocolsForChain(chainName);
8772
+ const protocols = protocol ? [registry.getProtocol(protocol)] : allProtocols;
8773
+ const results = [];
8774
+ await Promise.allSettled(
8775
+ protocols.map(async (proto) => {
8776
+ try {
8777
+ if (proto.interface === "uniswap_v2" && proto.contracts?.["lb_factory"]) {
8778
+ const adapter = createMerchantMoeLB2(proto, rpcUrl);
8779
+ const binIds = bins ? bins.split(",").map((s) => parseInt(s.trim(), 10)) : void 0;
8780
+ const poolsToScan = pool ? [pool] : (await adapter.discoverRewardedPools()).map((p) => p.pool);
8781
+ for (const poolAddr of poolsToScan) {
8782
+ try {
8783
+ const userBins = binIds ?? await adapter.findUserBinsWithBalance(poolAddr, user);
8784
+ if (userBins.length === 0) continue;
8785
+ const positions = await adapter.getUserPositions(user, poolAddr, userBins);
8786
+ if (positions.length === 0) continue;
8787
+ const pending = await adapter.getPendingRewards(user, poolAddr, userBins).catch(() => []);
8788
+ const totalPending = pending.reduce((s, r) => s + (r.amount ?? 0n), 0n);
8789
+ for (const pos of positions) {
8790
+ results.push({
8791
+ protocol: proto.slug,
8792
+ type: "lb",
8793
+ pool: poolAddr,
8794
+ ...pos,
8795
+ pending_reward: totalPending.toString(),
8796
+ pending_reward_token: pending[0]?.token
8797
+ });
8798
+ }
8799
+ } catch {
8800
+ }
8801
+ }
8802
+ }
8803
+ const npm = proto.contracts?.["position_manager"];
8804
+ if (npm && ["uniswap_v3", "algebra_v3", "hybra"].includes(proto.interface)) {
8805
+ const npmAbi = parseAbi31([
8806
+ "function balanceOf(address) view returns (uint256)",
8807
+ "function tokenOfOwnerByIndex(address, uint256) view returns (uint256)",
8808
+ "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)"
8809
+ ]);
8810
+ const ramsesAbi = parseAbi31([
8811
+ "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)"
8812
+ ]);
8813
+ const client = createPublicClient24({ transport: httpTransport(rpcUrl) });
8814
+ let count;
8815
+ try {
8816
+ count = await client.readContract({ address: npm, abi: npmAbi, functionName: "balanceOf", args: [user] });
8817
+ } catch {
8818
+ return;
8819
+ }
8820
+ const max = Math.min(Number(count), 50);
8821
+ for (let i = 0; i < max; i++) {
8822
+ try {
8823
+ const tokenId = await client.readContract({
8824
+ address: npm,
8825
+ abi: npmAbi,
8826
+ functionName: "tokenOfOwnerByIndex",
8827
+ args: [user, BigInt(i)]
8828
+ });
8829
+ let liq = 0n;
8830
+ let token0;
8831
+ let token1;
8832
+ let tickLower;
8833
+ let tickUpper;
8834
+ try {
8835
+ const r = await client.readContract({ address: npm, abi: ramsesAbi, functionName: "positions", args: [tokenId] });
8836
+ [token0, token1, , tickLower, tickUpper, liq] = r;
8837
+ } catch {
8838
+ const r = await client.readContract({ address: npm, abi: npmAbi, functionName: "positions", args: [tokenId] });
8839
+ [, , token0, token1, , tickLower, tickUpper, liq] = r;
8840
+ }
8841
+ if (liq > 0n) {
8842
+ results.push({
8843
+ protocol: proto.slug,
8844
+ type: "v3_nft",
8845
+ token_id: tokenId.toString(),
8846
+ token0,
8847
+ token1,
8848
+ liquidity: liq.toString(),
8849
+ tick_lower: tickLower,
8850
+ tick_upper: tickUpper
8851
+ });
8852
+ }
8853
+ } catch {
8854
+ }
8855
+ }
8856
+ }
8857
+ } catch {
8858
+ }
8859
+ })
8860
+ );
8861
+ return {
8862
+ content: [{ type: "text", text: ok({ chain: chainName, positions: results, total: results.length }, { wallet: user, scanned_protocols: protocols.length }) }]
8863
+ };
8864
+ } catch (e) {
8865
+ return { content: [{ type: "text", text: err(e instanceof Error ? e.message : String(e), { chain }) }], isError: true };
8866
+ }
8867
+ }
8868
+ );
8468
8869
  var transport = new StdioServerTransport();
8469
8870
  await server.connect(transport);
8470
8871
  //# sourceMappingURL=mcp-server.js.map