@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.
- package/README.md +10 -18
- package/dist/index.js +171 -68
- package/dist/index.js.map +1 -1
- package/dist/main.js +231 -136
- package/dist/main.js.map +1 -1
- package/dist/mcp-server.js +408 -7
- package/dist/mcp-server.js.map +1 -1
- package/package.json +1 -1
- package/skills/defi-cli/SKILL.md +121 -90
- package/skills/defi-cli/package.json +11 -3
- package/skills/defi-cli/references/commands.md +106 -53
- package/skills/defi-cli/references/protocols.md +100 -67
package/dist/mcp-server.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
5819
|
-
client.readContract({ address:
|
|
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).
|
|
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,
|
|
7804
|
-
to_chain: z.string().describe("Destination chain name,
|
|
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
|