@elizaos/plugin-moonwell
Version:
Moonwell Protocol integration plugin for ElizaOS
1,321 lines (1,316 loc) • 163 kB
JavaScript
// src/index.ts
import { logger as logger11 } from "@elizaos/core";
import { z } from "zod";
// src/services/moonwell-service.ts
import { Service, logger as logger2 } from "@elizaos/core";
import { BigNumber as BigNumber2 } from "bignumber.js";
import { ethers } from "ethers";
import {
createMoonwellClient
} from "@moonwell-fi/moonwell-sdk";
// src/types/index.ts
var MoonwellErrorCode = /* @__PURE__ */ ((MoonwellErrorCode2) => {
MoonwellErrorCode2["INVALID_AMOUNT"] = "INVALID_AMOUNT";
MoonwellErrorCode2["UNSUPPORTED_ASSET"] = "UNSUPPORTED_ASSET";
MoonwellErrorCode2["INVALID_PARAMETERS"] = "INVALID_PARAMETERS";
MoonwellErrorCode2["INSUFFICIENT_COLLATERAL"] = "INSUFFICIENT_COLLATERAL";
MoonwellErrorCode2["LIQUIDATION_RISK"] = "LIQUIDATION_RISK";
MoonwellErrorCode2["EXCEEDS_BORROW_CAPACITY"] = "EXCEEDS_BORROW_CAPACITY";
MoonwellErrorCode2["INSUFFICIENT_LIQUIDITY"] = "INSUFFICIENT_LIQUIDITY";
MoonwellErrorCode2["MARKET_PAUSED"] = "MARKET_PAUSED";
MoonwellErrorCode2["RATE_LIMIT_EXCEEDED"] = "RATE_LIMIT_EXCEEDED";
MoonwellErrorCode2["RPC_ERROR"] = "RPC_ERROR";
MoonwellErrorCode2["TRANSACTION_FAILED"] = "TRANSACTION_FAILED";
MoonwellErrorCode2["TIMEOUT"] = "TIMEOUT";
MoonwellErrorCode2["WALLET_NOT_CONNECTED"] = "WALLET_NOT_CONNECTED";
MoonwellErrorCode2["INSUFFICIENT_BALANCE"] = "INSUFFICIENT_BALANCE";
MoonwellErrorCode2["APPROVAL_REQUIRED"] = "APPROVAL_REQUIRED";
return MoonwellErrorCode2;
})(MoonwellErrorCode || {});
// src/utils/validation.ts
import { BigNumber } from "bignumber.js";
var SUPPORTED_ASSETS = {
USDC: {
symbol: "USDC",
decimals: 6,
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
// Base mainnet USDC
},
WETH: {
symbol: "WETH",
decimals: 18,
address: "0x4200000000000000000000000000000000000006"
// Base mainnet WETH
},
cbETH: {
symbol: "cbETH",
decimals: 18,
address: "0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22"
// Base mainnet cbETH
},
DAI: {
symbol: "DAI",
decimals: 18,
address: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb"
// Base mainnet DAI
},
USDbC: {
symbol: "USDbC",
decimals: 6,
address: "0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA"
// Base mainnet USDbC
}
};
function validateAmount(amount) {
const bigAmount = new BigNumber(amount);
if (bigAmount.isNaN() || bigAmount.isNegative()) {
throw createError(
"INVALID_AMOUNT" /* INVALID_AMOUNT */,
"Amount must be a positive number"
);
}
if (bigAmount.isZero()) {
throw createError(
"INVALID_AMOUNT" /* INVALID_AMOUNT */,
"Amount cannot be zero"
);
}
return bigAmount;
}
function validateAsset(asset) {
const normalizedAsset = asset.toUpperCase();
if (!(normalizedAsset in SUPPORTED_ASSETS)) {
throw createError(
"UNSUPPORTED_ASSET" /* UNSUPPORTED_ASSET */,
`Asset ${asset} is not supported. Supported assets: ${Object.keys(SUPPORTED_ASSETS).join(", ")}`,
{ supportedAssets: Object.keys(SUPPORTED_ASSETS) }
);
}
return normalizedAsset;
}
function validateHealthFactor(healthFactor, threshold = 1.5) {
if (healthFactor < threshold) {
const suggestions = [];
if (healthFactor < 1) {
suggestions.push("URGENT: Your position is at risk of liquidation!");
suggestions.push("Repay some debt immediately or add more collateral");
} else if (healthFactor < 1.2) {
suggestions.push(
"WARNING: Your position is close to liquidation threshold"
);
suggestions.push("Consider repaying debt or adding collateral soon");
} else {
suggestions.push("Your health factor is below recommended levels");
suggestions.push("Monitor your position closely");
}
throw createError(
"LIQUIDATION_RISK" /* LIQUIDATION_RISK */,
`Health factor ${healthFactor.toFixed(2)} is below safe threshold ${threshold}`,
{ healthFactor, threshold },
suggestions,
healthFactor
);
}
}
function validateBorrowCapacity(requestedAmount, availableCapacity) {
if (requestedAmount.gt(availableCapacity)) {
throw createError(
"EXCEEDS_BORROW_CAPACITY" /* EXCEEDS_BORROW_CAPACITY */,
`Requested borrow amount exceeds available capacity`,
{
requested: requestedAmount.toString(),
available: availableCapacity.toString()
},
[`Maximum borrowable amount: ${availableCapacity.toString()}`]
);
}
}
function validateLiquidity(requestedAmount, availableLiquidity) {
if (requestedAmount.gt(availableLiquidity)) {
throw createError(
"INSUFFICIENT_LIQUIDITY" /* INSUFFICIENT_LIQUIDITY */,
`Insufficient liquidity in the market`,
{
requested: requestedAmount.toString(),
available: availableLiquidity.toString()
},
[`Maximum available: ${availableLiquidity.toString()}`]
);
}
}
function createError(code, message, details, suggestions, healthFactor) {
return {
code,
message,
details,
suggestions,
healthFactor
};
}
function formatAmount(amount, decimals) {
return amount.dividedBy(new BigNumber(10).pow(decimals)).toFixed();
}
function parseAmount(amount, decimals) {
return new BigNumber(amount).multipliedBy(new BigNumber(10).pow(decimals));
}
function calculateHealthFactor(totalCollateralInUSD, totalDebtInUSD, liquidationThreshold) {
if (totalDebtInUSD.isZero()) {
return 999;
}
const weightedCollateral = totalCollateralInUSD.multipliedBy(liquidationThreshold);
return Number(weightedCollateral.dividedBy(totalDebtInUSD).toFixed(2));
}
function formatAPY(apy) {
return `${(apy * 100).toFixed(2)}%`;
}
function formatUSD(amount) {
return `$${amount.toFixed(2)}`;
}
// src/utils/error-handler.ts
import { logger } from "@elizaos/core";
function handleError(error) {
logger.error("Moonwell plugin error:", error);
if (error.code && Object.values(MoonwellErrorCode).includes(error.code)) {
return error;
}
const errorMessage = error.message?.toLowerCase() || "";
const errorString = error.toString().toLowerCase();
if (errorMessage.includes("insufficient") || errorMessage.includes("exceeds balance")) {
return createError(
"INSUFFICIENT_BALANCE" /* INSUFFICIENT_BALANCE */,
"Insufficient funds for transaction",
{ originalError: error.message }
);
}
if (errorMessage.includes("gas") || errorMessage.includes("reverted")) {
return createError(
"TRANSACTION_FAILED" /* TRANSACTION_FAILED */,
"Transaction would fail - check parameters",
{ originalError: error.message },
[
"Verify you have enough balance",
"Check if the market is paused",
"Ensure health factor remains safe"
]
);
}
if (errorMessage.includes("network") || errorMessage.includes("rpc") || errorMessage.includes("timeout")) {
return createError(
"RPC_ERROR" /* RPC_ERROR */,
"Network error - please try again",
{ originalError: error.message }
);
}
if (errorMessage.includes("paused") || errorMessage.includes("frozen")) {
return createError(
"MARKET_PAUSED" /* MARKET_PAUSED */,
"Market is currently paused",
{ originalError: error.message },
["Try again later", "Check Moonwell status page"]
);
}
return createError(
"TRANSACTION_FAILED" /* TRANSACTION_FAILED */,
error.message || "An unexpected error occurred",
{ originalError: error }
);
}
function formatErrorResponse(error) {
let response = `Error: ${error.message}`;
if (error.healthFactor !== void 0) {
response += `
Health Factor: ${error.healthFactor.toFixed(2)}`;
}
if (error.suggestions && error.suggestions.length > 0) {
response += "\n\nSuggestions:";
error.suggestions.forEach((suggestion, index) => {
response += `
${index + 1}. ${suggestion}`;
});
}
return response;
}
// src/services/moonwell-service.ts
var DEFAULT_RPC_URLS = {
base: "https://mainnet.base.org",
"base-sepolia": "https://sepolia.base.org"
};
var MOONWELL_ADDRESSES = {
base: {
comptroller: ethers.getAddress("0xfBb21d0380beE3312B33c4353c8936a0F13EF26C"),
oracle: "0xEc942bE8A8114bFD0396A5052c36027f2cA6C9d0",
multiRewardDistributor: ethers.getAddress("0xe9005b078701e2A0948D2EaC43010D35870Ad9d2"),
morphoViews: ethers.getAddress("0xc72fCC9793a10b9c363EeaAcaAbe422E0672B42B"),
morphoBundler: ethers.getAddress("0xb98c948CFA24072e58935BC004a8A7b376AE746A"),
markets: {
USDC: ethers.getAddress("0xEdc817A28E8B93B03976FBd4a3dDBc9f7D176c22"),
WETH: ethers.getAddress("0x628ff693426583D9a7FB391E54366292F509D457"),
cbETH: ethers.getAddress("0x3bf93770f2d4a794c3d9EBEfBAeBAE2a8f09A5E5"),
DAI: ethers.getAddress("0x73b06D8d18De422E269645eaCe15400DE7462417"),
USDbC: ethers.getAddress("0x703843C3379b52F9FF486c9f5892218d2a065cC8"),
wstETH: ethers.getAddress("0x627Fe393Bc6EdDA28e99AE648fD6fF362514304b"),
rETH: ethers.getAddress("0xcb1dacd30638ae38f2b94ea64f066045b7d45f44")
},
morphoVaults: {
mwUSDC: ethers.getAddress("0xc1256Ae5FF1cf2719D4937adb3bbCCab2E00A2Ca"),
mwETH: ethers.getAddress("0xa0E430870c4604CcfC7B38Ca7845B1FF653D0ff1"),
mwEURC: ethers.getAddress("0xf24608E0CCb972b0b0f4A6446a0BBf58c701a026"),
mwcbBTC: ethers.getAddress("0x543257ef2161176d7c8cd90ba65c2d4caef5a796")
}
},
"base-sepolia": {
// Testnet addresses - these would be the actual testnet deployment addresses
comptroller: ethers.getAddress("0x0000000000000000000000000000000000000000"),
oracle: ethers.getAddress("0x0000000000000000000000000000000000000000"),
multiRewardDistributor: ethers.getAddress("0x0000000000000000000000000000000000000000"),
morphoViews: ethers.getAddress("0x0000000000000000000000000000000000000000"),
morphoBundler: ethers.getAddress("0x0000000000000000000000000000000000000000"),
markets: {
USDC: ethers.getAddress("0x0000000000000000000000000000000000000000"),
WETH: ethers.getAddress("0x0000000000000000000000000000000000000000")
},
morphoVaults: {}
}
};
var MTOKEN_ABI = [
"function mint(uint256 mintAmount) returns (uint256)",
"function redeem(uint256 redeemTokens) returns (uint256)",
"function redeemUnderlying(uint256 redeemAmount) returns (uint256)",
"function borrow(uint256 borrowAmount) returns (uint256)",
"function repayBorrow(uint256 repayAmount) returns (uint256)",
"function repayBorrowBehalf(address borrower, uint256 repayAmount) returns (uint256)",
"function balanceOf(address owner) view returns (uint256)",
"function borrowBalanceStored(address account) view returns (uint256)",
"function exchangeRateStored() view returns (uint256)",
"function supplyRatePerBlock() view returns (uint256)",
"function borrowRatePerBlock() view returns (uint256)",
"function totalSupply() view returns (uint256)",
"function totalBorrows() view returns (uint256)",
"function getCash() view returns (uint256)",
"function underlying() view returns (address)"
];
var COMPTROLLER_ABI = [
"function enterMarkets(address[] calldata mTokens) returns (uint256[] memory)",
"function exitMarket(address mTokenAddress) returns (uint256)",
"function getAccountLiquidity(address account) view returns (uint256, uint256, uint256)",
"function markets(address mTokenAddress) view returns (bool, uint256, bool)",
"function checkMembership(address account, address mToken) view returns (bool)"
];
var ORACLE_ABI = [
"function getUnderlyingPrice(address mToken) view returns (uint256)"
];
var MULTI_REWARD_DISTRIBUTOR_ABI = [
"function claimAllRewards(address user) external returns (uint256[] memory)",
"function claimReward(address user, address mToken) external returns (uint256)",
"function getOutstandingRewardsForUser(address user) view returns (address[] memory, uint256[] memory)",
"function getOutstandingRewardsForUserAndMarket(address user, address mToken) view returns (address[] memory, uint256[] memory)"
];
var MoonwellService = class _MoonwellService extends Service {
constructor(runtime) {
super(runtime);
this.runtime = runtime;
const network = runtime.getSetting("MOONWELL_NETWORK") || "base";
const rpcUrl = runtime.getSetting("BASE_RPC_URL") || DEFAULT_RPC_URLS.base;
this.moonwellConfig = {
network,
rpcUrl,
moonwellApiKey: runtime.getSetting("MOONWELL_API_KEY"),
healthFactorThreshold: Number(runtime.getSetting("HEALTH_FACTOR_ALERT")) || 1.5,
retryAttempts: 3,
monitoringInterval: 6e4
// 1 minute
};
}
static serviceType = "moonwell";
capabilityDescription = "Provides integration with Moonwell Protocol for DeFi lending and borrowing operations on Base L2";
state = {
isInitialized: false,
network: "base"
};
moonwellConfig;
provider;
signer;
comptroller;
oracle;
multiRewardDistributor;
markets = /* @__PURE__ */ new Map();
moonwellClient;
static async start(runtime) {
logger2.info("Starting Moonwell service...");
const service = new _MoonwellService(runtime);
await service.initialize();
return service;
}
static async stop(runtime) {
logger2.info("Stopping Moonwell service...");
const service = runtime.getService(
_MoonwellService.serviceType
);
if (service) {
await service.stop();
}
}
async initialize() {
try {
logger2.info("Initializing Moonwell service...");
this.provider = new ethers.JsonRpcProvider(this.moonwellConfig.rpcUrl);
const privateKey = this.runtime.getSetting("WALLET_PRIVATE_KEY");
if (privateKey) {
this.signer = new ethers.Wallet(privateKey, this.provider);
this.state.userAddress = await this.signer.getAddress();
logger2.info(`Wallet connected: ${this.state.userAddress}`);
}
const addresses = MOONWELL_ADDRESSES[this.moonwellConfig.network];
this.comptroller = new ethers.Contract(
addresses.comptroller,
COMPTROLLER_ABI,
this.signer || this.provider
);
this.oracle = new ethers.Contract(
addresses.oracle,
ORACLE_ABI,
this.provider
);
this.multiRewardDistributor = new ethers.Contract(
addresses.multiRewardDistributor,
MULTI_REWARD_DISTRIBUTOR_ABI,
this.signer || this.provider
);
for (const [asset, address] of Object.entries(addresses.markets)) {
this.markets.set(
asset,
new ethers.Contract(
address,
MTOKEN_ABI,
this.signer || this.provider
)
);
}
try {
this.moonwellClient = await createMoonwellClient({
networks: {
[this.moonwellConfig.network]: {
rpcUrls: [this.moonwellConfig.rpcUrl]
}
}
});
logger2.info("Moonwell SDK client initialized");
} catch (error) {
logger2.warn("Failed to initialize Moonwell SDK client:", error);
}
this.state.isInitialized = true;
this.state.network = this.moonwellConfig.network;
if (this.state.userAddress) {
this.startPositionMonitoring();
}
logger2.info("Moonwell service initialized successfully");
} catch (error) {
logger2.error("Failed to initialize Moonwell service:", error);
throw handleError(error);
}
}
ensureInitialized() {
if (!this.state.isInitialized) {
throw createError(
"WALLET_NOT_CONNECTED" /* WALLET_NOT_CONNECTED */,
"Moonwell service not initialized"
);
}
}
ensureWallet() {
if (!this.signer || !this.state.userAddress) {
throw createError(
"WALLET_NOT_CONNECTED" /* WALLET_NOT_CONNECTED */,
"Wallet not connected. Please provide WALLET_PRIVATE_KEY"
);
}
}
async supply(params) {
try {
this.ensureInitialized();
this.ensureWallet();
const asset = validateAsset(params.asset);
const amount = validateAmount(params.amount);
const assetInfo = SUPPORTED_ASSETS[asset];
const market = this.markets.get(asset);
if (!market) {
throw createError(
"UNSUPPORTED_ASSET" /* UNSUPPORTED_ASSET */,
`Market not found for ${asset}`
);
}
const positionBefore = await this.getUserPosition();
const amountInWei = parseAmount(amount.toString(), assetInfo.decimals);
if (asset !== "ETH") {
const tokenContract = new ethers.Contract(
assetInfo.address,
["function approve(address spender, uint256 amount) returns (bool)"],
this.signer
);
const approveTx = await tokenContract.approve(
await market.getAddress(),
amountInWei.toString()
);
await approveTx.wait();
logger2.info(`Approved ${asset} for supply`);
}
const tx = await market.mint(amountInWei.toString(), {
value: asset === "ETH" ? amountInWei.toString() : 0
});
const receipt = await tx.wait();
logger2.info(`Supply transaction confirmed: ${receipt.hash}`);
if (params.enableAsCollateral) {
const enterMarketsTx = await this.comptroller.enterMarkets([
await market.getAddress()
]);
await enterMarketsTx.wait();
logger2.info(`Enabled ${asset} as collateral`);
}
const positionAfter = await this.getUserPosition();
const marketData = await this.getMarketData(asset);
const mTokenBalance = await market.balanceOf(this.state.userAddress);
return {
transactionHash: receipt.hash,
mTokenBalance: new BigNumber2(mTokenBalance.toString()),
currentAPY: marketData[0].supplyAPY,
collateralEnabled: params.enableAsCollateral
};
} catch (error) {
throw handleError(error);
}
}
async borrow(params) {
try {
this.ensureInitialized();
this.ensureWallet();
const asset = validateAsset(params.asset);
const amount = validateAmount(params.amount);
const assetInfo = SUPPORTED_ASSETS[asset];
const market = this.markets.get(asset);
if (!market) {
throw createError(
"UNSUPPORTED_ASSET" /* UNSUPPORTED_ASSET */,
`Market not found for ${asset}`
);
}
const position = await this.getUserPosition();
validateBorrowCapacity(amount, position.availableToBorrow);
const cash = await market.getCash();
const cashBN = new BigNumber2(cash.toString());
validateLiquidity(amount, cashBN);
const amountInWei = parseAmount(amount.toString(), assetInfo.decimals);
const tx = await market.borrow(amountInWei.toString());
const receipt = await tx.wait();
logger2.info(`Borrow transaction confirmed: ${receipt.hash}`);
const positionAfter = await this.getUserPosition();
const marketData = await this.getMarketData(asset);
validateHealthFactor(
positionAfter.healthFactor,
this.moonwellConfig.healthFactorThreshold
);
return {
transactionHash: receipt.hash,
borrowedAmount: amount,
interestRate: marketData[0].borrowAPY,
healthFactor: positionAfter.healthFactor
};
} catch (error) {
throw handleError(error);
}
}
async repay(params) {
try {
this.ensureInitialized();
this.ensureWallet();
const asset = validateAsset(params.asset);
const assetInfo = SUPPORTED_ASSETS[asset];
const market = this.markets.get(asset);
if (!market) {
throw createError(
"UNSUPPORTED_ASSET" /* UNSUPPORTED_ASSET */,
`Market not found for ${asset}`
);
}
const borrowBalance = await market.borrowBalanceStored(
this.state.userAddress
);
const borrowBalanceBN = new BigNumber2(borrowBalance.toString());
let repayAmount;
if (params.isMax) {
repayAmount = borrowBalanceBN;
} else {
const amount = validateAmount(params.amount);
repayAmount = parseAmount(amount.toString(), assetInfo.decimals);
if (repayAmount.gt(borrowBalanceBN)) {
repayAmount = borrowBalanceBN;
}
}
if (asset !== "ETH") {
const tokenContract = new ethers.Contract(
assetInfo.address,
["function approve(address spender, uint256 amount) returns (bool)"],
this.signer
);
const approveTx = await tokenContract.approve(
await market.getAddress(),
repayAmount.toString()
);
await approveTx.wait();
logger2.info(`Approved ${asset} for repayment`);
}
const tx = await market.repayBorrow(repayAmount.toString(), {
value: asset === "ETH" ? repayAmount.toString() : 0
});
const receipt = await tx.wait();
logger2.info(`Repay transaction confirmed: ${receipt.hash}`);
const positionAfter = await this.getUserPosition();
const remainingBorrow = await market.borrowBalanceStored(
this.state.userAddress
);
return {
transactionHash: receipt.hash,
repaidAmount: new BigNumber2(repayAmount.toString()),
remainingDebt: new BigNumber2(remainingBorrow.toString()),
healthFactor: positionAfter.healthFactor
};
} catch (error) {
throw handleError(error);
}
}
async withdraw(params) {
try {
this.ensureInitialized();
this.ensureWallet();
const asset = validateAsset(params.asset);
const assetInfo = SUPPORTED_ASSETS[asset];
const market = this.markets.get(asset);
if (!market) {
throw createError(
"UNSUPPORTED_ASSET" /* UNSUPPORTED_ASSET */,
`Market not found for ${asset}`
);
}
const positionBefore = await this.getUserPosition();
const mTokenBalance = await market.balanceOf(this.state.userAddress);
const exchangeRate = await market.exchangeRateStored();
const underlyingBalance = new BigNumber2(mTokenBalance.toString()).multipliedBy(new BigNumber2(exchangeRate.toString())).dividedBy(new BigNumber2(10).pow(18));
let withdrawAmount;
let redeemTokens;
if (params.isMax) {
withdrawAmount = underlyingBalance;
redeemTokens = new BigNumber2(mTokenBalance.toString());
} else {
const amount = validateAmount(params.amount);
withdrawAmount = parseAmount(amount.toString(), assetInfo.decimals);
redeemTokens = withdrawAmount.multipliedBy(new BigNumber2(10).pow(18)).dividedBy(new BigNumber2(exchangeRate.toString()));
}
const simulatedCollateral = positionBefore.totalSupplied.minus(withdrawAmount);
const simulatedHealthFactor = calculateHealthFactor(
simulatedCollateral,
positionBefore.totalBorrowed,
positionBefore.liquidationThreshold
);
validateHealthFactor(
simulatedHealthFactor,
this.moonwellConfig.healthFactorThreshold
);
const tx = params.isMax ? await market.redeem(redeemTokens.toString()) : await market.redeemUnderlying(withdrawAmount.toString());
const receipt = await tx.wait();
logger2.info(`Withdraw transaction confirmed: ${receipt.hash}`);
const positionAfter = await this.getUserPosition();
const remainingBalance = await market.balanceOf(this.state.userAddress);
return {
transactionHash: receipt.hash,
withdrawnAmount: withdrawAmount,
remainingSupply: new BigNumber2(remainingBalance.toString()),
healthFactor: positionAfter.healthFactor
};
} catch (error) {
throw handleError(error);
}
}
async getUserPosition() {
try {
this.ensureInitialized();
this.ensureWallet();
const supplies = [];
const borrows = [];
let totalSuppliedUSD = new BigNumber2(0);
let totalBorrowedUSD = new BigNumber2(0);
const [error, collateral, shortfall] = await this.comptroller.getAccountLiquidity(this.state.userAddress);
if (error.toString() !== "0") {
throw createError(
"RPC_ERROR" /* RPC_ERROR */,
"Failed to get account liquidity"
);
}
for (const [asset, market] of this.markets.entries()) {
const marketAddress = await market.getAddress();
const mTokenBalance = await market.balanceOf(this.state.userAddress);
const exchangeRate = await market.exchangeRateStored();
const underlyingPrice = await this.oracle.getUnderlyingPrice(marketAddress);
const underlyingBalance = new BigNumber2(mTokenBalance.toString()).multipliedBy(new BigNumber2(exchangeRate.toString())).dividedBy(new BigNumber2(10).pow(18));
if (underlyingBalance.gt(0)) {
const balanceInUSD = underlyingBalance.multipliedBy(new BigNumber2(underlyingPrice.toString())).dividedBy(new BigNumber2(10).pow(36));
const supplyRate = await market.supplyRatePerBlock();
const blocksPerYear = 2628e3;
const apy = new BigNumber2(supplyRate.toString()).multipliedBy(blocksPerYear).dividedBy(new BigNumber2(10).pow(18)).toNumber();
const isCollateral = await this.comptroller.checkMembership(
this.state.userAddress,
marketAddress
);
supplies.push({
asset,
symbol: asset,
balance: underlyingBalance,
balanceInUSD,
apy,
isCollateral
});
if (isCollateral) {
totalSuppliedUSD = totalSuppliedUSD.plus(balanceInUSD);
}
}
const borrowBalance = await market.borrowBalanceStored(
this.state.userAddress
);
if (new BigNumber2(borrowBalance.toString()).gt(0)) {
const borrowBalanceBN = new BigNumber2(borrowBalance.toString());
const balanceInUSD = borrowBalanceBN.multipliedBy(new BigNumber2(underlyingPrice.toString())).dividedBy(new BigNumber2(10).pow(36));
const borrowRate = await market.borrowRatePerBlock();
const blocksPerYear = 2628e3;
const apy = new BigNumber2(borrowRate.toString()).multipliedBy(blocksPerYear).dividedBy(new BigNumber2(10).pow(18)).toNumber();
borrows.push({
asset,
symbol: asset,
balance: borrowBalanceBN,
balanceInUSD,
apy
});
totalBorrowedUSD = totalBorrowedUSD.plus(balanceInUSD);
}
}
const healthFactor = totalBorrowedUSD.isZero() ? 999 : calculateHealthFactor(totalSuppliedUSD, totalBorrowedUSD, 0.8);
const availableToBorrow = new BigNumber2(collateral.toString()).dividedBy(
new BigNumber2(10).pow(18)
);
const position = {
totalSupplied: totalSuppliedUSD,
totalBorrowed: totalBorrowedUSD,
healthFactor,
liquidationThreshold: 0.8,
availableToBorrow,
supplies,
borrows
};
this.state.positionCache = position;
this.state.lastUpdate = Date.now();
return position;
} catch (error) {
throw handleError(error);
}
}
async getMarketData(asset) {
try {
this.ensureInitialized();
const marketDataList = [];
const assetsToCheck = asset ? [validateAsset(asset)] : Object.keys(SUPPORTED_ASSETS);
for (const assetSymbol of assetsToCheck) {
const market = this.markets.get(assetSymbol);
if (!market) continue;
const marketAddress = await market.getAddress();
const assetInfo = SUPPORTED_ASSETS[asset];
const [
supplyRate,
borrowRate,
totalSupply,
totalBorrows,
cash,
exchangeRate,
underlyingPrice
] = await Promise.all([
market.supplyRatePerBlock(),
market.borrowRatePerBlock(),
market.totalSupply(),
market.totalBorrows(),
market.getCash(),
market.exchangeRateStored(),
this.oracle.getUnderlyingPrice(marketAddress)
]);
const marketInfo = await this.comptroller.markets(marketAddress);
const collateralFactor = new BigNumber2(
marketInfo[1].toString()
).dividedBy(new BigNumber2(10).pow(18));
const blocksPerYear = 2628e3;
const supplyAPY = new BigNumber2(supplyRate.toString()).multipliedBy(blocksPerYear).dividedBy(new BigNumber2(10).pow(18)).toNumber();
const borrowAPY = new BigNumber2(borrowRate.toString()).multipliedBy(blocksPerYear).dividedBy(new BigNumber2(10).pow(18)).toNumber();
const totalSupplyUnderlying = new BigNumber2(totalSupply.toString()).multipliedBy(new BigNumber2(exchangeRate.toString())).dividedBy(new BigNumber2(10).pow(18));
const totalLiquidity = new BigNumber2(cash.toString()).plus(
new BigNumber2(totalBorrows.toString())
);
const utilizationRate = totalLiquidity.isZero() ? 0 : new BigNumber2(totalBorrows.toString()).dividedBy(totalLiquidity).toNumber();
const priceInUSD = new BigNumber2(underlyingPrice.toString()).dividedBy(new BigNumber2(10).pow(18)).toNumber();
marketDataList.push({
asset: assetSymbol,
symbol: assetSymbol,
supplyAPY,
borrowAPY,
totalSupply: totalSupplyUnderlying,
totalBorrow: new BigNumber2(totalBorrows.toString()),
utilizationRate,
liquidityAvailable: new BigNumber2(cash.toString()),
collateralFactor: collateralFactor.toNumber(),
priceInUSD
});
}
this.state.marketDataCache = marketDataList;
this.state.lastUpdate = Date.now();
return marketDataList;
} catch (error) {
throw handleError(error);
}
}
getCachedPosition() {
if (this.state.positionCache && this.state.lastUpdate && Date.now() - this.state.lastUpdate < 3e4) {
return this.state.positionCache;
}
return null;
}
async updatePositionCache() {
if (this.state.userAddress) {
await this.getUserPosition();
}
}
startPositionMonitoring() {
setInterval(async () => {
try {
const position = await this.getUserPosition();
if (position.healthFactor < this.moonwellConfig.healthFactorThreshold) {
logger2.warn(
`Health factor alert: ${position.healthFactor.toFixed(2)} is below threshold ${this.moonwellConfig.healthFactorThreshold}`
);
}
} catch (error) {
logger2.error("Error monitoring position:", error);
}
}, this.moonwellConfig.monitoringInterval);
}
async getUserRewards() {
try {
this.ensureInitialized();
this.ensureWallet();
if (!this.multiRewardDistributor) {
throw createError(
"RPC_ERROR" /* RPC_ERROR */,
"Multi-Reward Distributor not initialized"
);
}
const [rewardTokens, rewardAmounts] = await this.multiRewardDistributor.getOutstandingRewardsForUser(
this.state.userAddress
);
const rewards = [];
let totalValueInUSD = new BigNumber2(0);
for (let i = 0; i < rewardTokens.length; i++) {
const tokenAddress = rewardTokens[i];
const amount = new BigNumber2(rewardAmounts[i].toString());
if (amount.gt(0)) {
let symbol = "UNKNOWN";
if (tokenAddress.toLowerCase() === "0xa88594d404727625a9437c3f886c7643872296ae".toLowerCase()) {
symbol = "WELL";
}
const valueInUSD = new BigNumber2(0);
rewards.push({
token: tokenAddress,
symbol,
amount,
valueInUSD
});
totalValueInUSD = totalValueInUSD.plus(valueInUSD);
}
}
return {
rewards,
totalValueInUSD
};
} catch (error) {
throw handleError(error);
}
}
async claimAllRewards() {
try {
this.ensureInitialized();
this.ensureWallet();
if (!this.multiRewardDistributor) {
throw createError(
"RPC_ERROR" /* RPC_ERROR */,
"Multi-Reward Distributor not initialized"
);
}
const rewardsBefore = await this.getUserRewards();
const tx = await this.multiRewardDistributor.claimAllRewards(
this.state.userAddress
);
const receipt = await tx.wait();
logger2.info(`Claimed all rewards: ${receipt.hash}`);
return {
transactionHash: receipt.hash,
rewardsClaimed: rewardsBefore.rewards.map((r) => ({
token: r.token,
amount: r.amount
}))
};
} catch (error) {
throw handleError(error);
}
}
// Morpho Markets Methods
async getMorphoMarkets() {
try {
this.ensureInitialized();
if (!this.moonwellClient) {
logger2.warn("Moonwell client not initialized");
return [];
}
try {
logger2.info("Fetching Morpho markets using Moonwell SDK...");
const morphoMarkets = await this.moonwellClient.getMorphoMarkets({
chainId: this.moonwellConfig.network === "base" ? 8453 : 84532,
includeRewards: true
});
logger2.info(`Fetched ${morphoMarkets.length} Morpho markets from SDK`);
return morphoMarkets;
} catch (error) {
logger2.error("Failed to fetch Morpho markets from SDK:", error);
return [];
}
} catch (error) {
logger2.error("Failed to fetch Morpho markets:", error);
return [];
}
}
async getMorphoUserPosition(marketId) {
try {
this.ensureInitialized();
this.ensureWallet();
if (!this.moonwellClient) {
logger2.warn("Moonwell client not initialized");
return null;
}
try {
logger2.info(`Fetching Morpho user position for market ${marketId}...`);
const userPosition = await this.moonwellClient.getMorphoMarketUserPosition({
chainId: this.moonwellConfig.network === "base" ? 8453 : 84532,
marketId,
userAddress: this.state.userAddress
});
if (!userPosition) {
logger2.debug(`No position found for market ${marketId}`);
return null;
}
logger2.info(`Fetched Morpho position for market ${marketId}`);
return userPosition;
} catch (error) {
logger2.error(`Failed to fetch Morpho user position for market ${marketId}:`, error);
return null;
}
} catch (error) {
logger2.error(`Failed to fetch Morpho user position for market ${marketId}:`, error);
return null;
}
}
async getMorphoUserBalances() {
try {
this.ensureInitialized();
this.ensureWallet();
if (!this.moonwellClient) {
logger2.warn("Moonwell client not initialized");
return [];
}
try {
logger2.info("Fetching Morpho user balances using Moonwell SDK...");
const userBalances = await this.moonwellClient.getMorphoUserBalances({
chainId: this.moonwellConfig.network === "base" ? 8453 : 84532,
userAddress: this.state.userAddress
});
logger2.info(`Fetched ${userBalances.length} Morpho user balances`);
return userBalances;
} catch (error) {
logger2.error("Failed to fetch Morpho user balances from SDK:", error);
return [];
}
} catch (error) {
logger2.error("Failed to fetch Morpho user balances:", error);
return [];
}
}
async getMorphoUserRewards() {
try {
this.ensureInitialized();
this.ensureWallet();
if (!this.moonwellClient) {
logger2.warn("Moonwell client not initialized");
return { rewards: [], totalValueInUSD: new BigNumber2(0) };
}
try {
logger2.info("Fetching Morpho user rewards using Moonwell SDK...");
const userRewards = await this.moonwellClient.getMorphoUserRewards({
chainId: this.moonwellConfig.network === "base" ? 8453 : 84532,
userAddress: this.state.userAddress
});
const rewards = {
rewards: (userRewards.rewards || []).map((reward) => ({
token: reward.token,
symbol: reward.symbol || "UNKNOWN",
amount: new BigNumber2(reward.amount?.toString() || "0"),
valueInUSD: new BigNumber2(reward.valueInUSD?.toString() || "0"),
marketId: reward.marketId
})),
totalValueInUSD: new BigNumber2(userRewards.totalValueInUSD?.toString() || "0")
};
logger2.info(`Fetched ${rewards.rewards.length} Morpho reward tokens`);
return rewards;
} catch (error) {
logger2.error("Failed to fetch Morpho user rewards from SDK:", error);
return { rewards: [], totalValueInUSD: new BigNumber2(0) };
}
} catch (error) {
logger2.error("Failed to fetch Morpho user rewards:", error);
return { rewards: [], totalValueInUSD: new BigNumber2(0) };
}
}
// Morpho Vault Methods
async getMorphoVaults() {
try {
this.ensureInitialized();
if (!this.moonwellClient) {
logger2.warn("Moonwell client not initialized");
return [];
}
try {
logger2.info("Fetching Morpho vaults using Moonwell SDK...");
const morphoVaults = await this.moonwellClient.getMorphoVaults({
chainId: this.moonwellConfig.network === "base" ? 8453 : 84532,
includeRewards: true
});
logger2.info(`Fetched ${morphoVaults.length} Morpho vaults from SDK`);
return morphoVaults;
} catch (error) {
logger2.error("Failed to fetch Morpho vaults from SDK:", error);
return [];
}
} catch (error) {
logger2.error("Failed to fetch Morpho vaults:", error);
return [];
}
}
async getMorphoVaultUserPosition(vaultId) {
try {
this.ensureInitialized();
this.ensureWallet();
if (this.moonwellClient && "getMorphoVaultUserPosition" in this.moonwellClient) {
try {
logger2.info(`Fetching Morpho vault user position for vault ${vaultId}...`);
const userPosition = await this.moonwellClient.getMorphoVaultUserPosition(
vaultId,
this.state.userAddress
);
if (!userPosition || new BigNumber2(userPosition.shares?.toString() || "0").isZero()) {
return null;
}
return userPosition;
logger2.info(`Fetched Morpho vault position for vault ${vaultId}`);
return userPosition;
} catch (error) {
logger2.warn("SDK method failed, falling back to mock data:", error);
}
}
if (vaultId === "mw-usdc-vault-1") {
logger2.warn("Morpho vault user position integration not yet fully implemented - returning mock data");
return null;
}
return null;
} catch (error) {
logger2.error(`Failed to fetch Morpho vault user position for vault ${vaultId}:`, error);
throw handleError(error);
}
}
async getMorphoVaultSnapshots(vaultId, timeframe = "30d") {
try {
this.ensureInitialized();
if (this.moonwellClient && "getMorphoVaultSnapshots" in this.moonwellClient) {
try {
logger2.info(`Fetching Morpho vault snapshots for vault ${vaultId}...`);
const snapshots2 = await this.moonwellClient.getMorphoVaultSnapshots(vaultId, timeframe);
const vaultSnapshots = snapshots2.map((snapshot) => ({
vaultId,
timestamp: snapshot.timestamp || Date.now(),
totalAssets: new BigNumber2(snapshot.totalAssets?.toString() || "0"),
totalShares: new BigNumber2(snapshot.totalShares?.toString() || "0"),
sharePrice: new BigNumber2(snapshot.sharePrice?.toString() || "1"),
apy: snapshot.apy || 0,
tvl: new BigNumber2(snapshot.tvl?.toString() || "0"),
tvlInUSD: new BigNumber2(snapshot.tvlInUSD?.toString() || "0"),
utilizationRate: snapshot.utilizationRate || 0,
performance1d: snapshot.performance1d || 0,
performance7d: snapshot.performance7d || 0,
performance30d: snapshot.performance30d || 0,
volume24h: new BigNumber2(snapshot.volume24h?.toString() || "0"),
uniqueDepositors: snapshot.uniqueDepositors || 0,
strategyAllocations: snapshot.strategyAllocations || []
}));
logger2.info(`Fetched ${vaultSnapshots.length} snapshots for vault ${vaultId}`);
return vaultSnapshots;
} catch (error) {
logger2.warn("SDK method failed, falling back to mock data:", error);
}
}
logger2.warn("Morpho vault snapshots integration not yet fully implemented - returning mock data");
const days = timeframe === "7d" ? 7 : timeframe === "30d" ? 30 : 90;
const snapshots = [];
const now = Date.now();
for (let i = days - 1; i >= 0; i--) {
const timestamp = now - i * 864e5;
const progressFactor = (days - i) / days;
snapshots.push({
chainId: this.moonwellConfig.network === "base" ? 8453 : 84532,
vaultAddress: vaultId,
totalSupply: 5e6 + progressFactor * 1e5,
totalSupplyUsd: 5e6 + progressFactor * 1e5,
totalBorrows: 0,
totalBorrowsUsd: 0,
totalLiquidity: 45e5 + progressFactor * 9e4,
totalLiquidityUsd: 45e5 + progressFactor * 9e4,
timestamp
});
}
return snapshots;
} catch (error) {
logger2.error(`Failed to fetch Morpho vault snapshots for vault ${vaultId}:`, error);
throw handleError(error);
}
}
async getMorphoVaultSummary() {
try {
const vaults = await this.getMorphoVaults();
return {
totalVaults: vaults.length,
totalTVL: vaults.reduce((sum, vault) => sum.plus(new BigNumber2(vault.totalSupply.value.toString())), new BigNumber2(0)),
totalTVLInUSD: vaults.reduce((sum, vault) => sum.plus(vault.totalSupplyUsd), new BigNumber2(0)),
averageAPY: vaults.length > 0 ? vaults.reduce((sum, vault) => sum + vault.totalApy, 0) / vaults.length : 0,
vaults
};
} catch (error) {
logger2.error("Failed to get Morpho vault summary:", error);
throw handleError(error);
}
}
async getMorphoVaultPortfolio() {
try {
this.ensureWallet();
const vaults = await this.getMorphoVaults();
const positions = [];
for (const vault of vaults) {
const position = await this.getMorphoVaultUserPosition(vault.vaultKey);
if (position) {
positions.push(position);
}
}
if (positions.length === 0) {
return null;
}
return {
userAddress: this.state.userAddress,
positions,
lastUpdated: Date.now()
};
} catch (error) {
logger2.error("Failed to get Morpho vault portfolio:", error);
throw handleError(error);
}
}
applyVaultFilters(vaults, filters) {
if (!filters) {
return vaults;
}
if (!filters) {
return vaults;
}
return vaults.filter((vault) => {
if (filters.asset && vault.underlyingToken.symbol.toLowerCase() !== filters.asset.toLowerCase()) {
return false;
}
if (filters.minAPY && vault.totalApy < filters.minAPY) {
return false;
}
if (filters.minTVL && new BigNumber2(filters.minTVL).gt(vault.totalSupplyUsd)) {
return false;
}
return true;
});
}
// Enhanced Balance Methods
async getAllUserBalances(params = {}) {
try {
this.ensureInitialized();
this.ensureWallet();
const {
includeWallet = true,
includeCore = true,
includeMorpho = true,
includeVaults = true,
minBalanceThreshold = new BigNumber2(0.01)
// $0.01 minimum
} = params;
const breakdown = {
walletBalances: [],
corePositions: [],
morphoPositions: [],
vaultPositions: [],
totalBalanceInUSD: new BigNumber2(0),
totalWalletValueInUSD: new BigNumber2(0),
totalCoreValueInUSD: new BigNumber2(0),
totalMorphoValueInUSD: new BigNumber2(0),
totalVaultValueInUSD: new BigNumber2(0)
};
if (includeWallet) {
try {
const walletBalances = await this.getWalletBalances();
breakdown.walletBalances = walletBalances.filter(
(balance) => balance.balanceInUSD.gte(minBalanceThreshold)
);
breakdown.totalWalletValueInUSD = breakdown.walletBalances.reduce((sum, balance) => sum.plus(balance.balanceInUSD), new BigNumber2(0));
} catch (error) {
logger2.warn("Failed to fetch wallet balances:", error);
}
}
if (includeCore) {
try {
const corePosition = await this.getUserPosition();
breakdown.corePositions = this.convertCorePositionsToEnhanced(corePosition);
breakdown.totalCoreValueInUSD = breakdown.corePositions.reduce((sum, balance) => sum.plus(balance.balanceInUSD), new BigNumber2(0));
} catch (error) {
logger2.warn("Failed to fetch core positions:", error);
}
}
if (includeMorpho) {
try {
const morphoBalances = await this.getMorphoBalances();
breakdown.morphoPositions = morphoBalances.filter(
(balance) => balance.balanceInUSD.gte(minBalanceThreshold)
);
breakdown.totalMorphoValueInUSD = breakdown.morphoPositions.reduce((sum, balance) => sum.plus(balance.balanceInUSD), new BigNumber2(0));
} catch (error) {
logger2.warn("Failed to fetch Morpho positions:", error);
}
}
if (includeVaults) {
try {
const vaultBalances = await this.getVaultBalances();
breakdown.vaultPositions = vaultBalances.filter(
(balance) => balance.balanceInUSD.gte(minBalanceThreshold)
);
breakdown.totalVaultValueInUSD = breakdown.vaultPositions.reduce((sum, balance) => sum.plus(balance.balanceInUSD), new BigNumber2(0));
} catch (error) {
logger2.warn("Failed to fetch vault positions:", error);
}
}
breakdown.totalBalanceInUSD = breakdown.totalWalletValueInUSD.plus(breakdown.totalCoreValueInUSD).plus(breakdown.totalMorphoValueInUSD).plus(breakdown.totalVaultValueInUSD);
return breakdown;
} catch (error) {
logger2.error("Failed to get all user balances:", error);
throw handleError(error);
}
}
async getComprehensiveUserData() {
try {
this.ensureInitialized();
this.ensureWallet();
logger2.info("Fetching comprehensive user data...");
const [corePosition, coreRewards, morphoMarkets, morphoRewards, morphoVaultPortfolio, balanceBreakdown] = await Promise.all([
this.getUserPosition().catch((error) => {
logger2.warn("Failed to fetch core position:", error);
return this.getEmptyUserPosition();
}),
this.getUserRewards().catch((error) => {
logger2.warn("Failed to fetch core rewards:", error);
return { rewards: [], totalValueInUSD: new BigNumber2(0) };
}),
this.getMorphoMarkets().catch((error) => {
logger2.warn("Failed to fetch Morpho markets:", error);
return [];
}),
this.getMorphoUserRewards().catch((error) => {
logger2.warn("Failed to fetch Morpho rewards:", error);
return { rewards: [], totalValueInUSD: new BigNumber2(0) };
}),
this.getMorphoVaultPortfolio().catch((error) => {
logger2.warn("Failed to fetch Morpho vault portfolio:", error);
return null;
}),
this.getAllUserBalances().catch((error) => {
logger2.warn("Failed to fetch balance breakdown:", error);
return this.getEmptyBalanceBreakdown();
})
]);
const morphoPositions = [];
for (const market of morphoMarkets) {
try {
const position = await this.getMorphoUserPosition(market.marketId);
if (position) {
morphoPositions.push(position);
}
} catch (error) {
logger2.debug(`Failed to fetch position for market ${market.marketId}:`, error);
}
}
const portfolioSummary = this.calculatePortfolioSummary(
corePosition,
coreRewards,
morphoPositions,
morphoRewards,
morphoVaultPortfolio,
balanceBreakdown
);
const comprehensiveData = {
userAddress: this.state.userAddress,
corePosition,
coreRewards,
morphoMarkets,
morphoPositions,
morphoRewards,
morphoVaultPortfolio,
balanceBreakdown,
portfolioSummary,
lastUpdated: Date.now()
};
logger2.info("Successfully fetched comprehensive user data");
return comprehensiveData;
} catch (error) {
logger2.error("Failed to get comprehensive user data:", error);
throw handleError(error);
}
}
// Helper methods for enhanced balance functionality
async getWalletBalances() {
const balances = [];
if (!this.moonwellClient) {
logger2.debug("Moonwell client not initialized, using fallback");
} else {
try {
const sdkBalances = await this.moonwellClient.getUserBalances({
chainId: this.moonwellConfig.network === "base" ? 8453 : 84532,
userAddress: this.state.userAddress
});
return sdkBalances.map((balance) => ({
tokenAddress: balance.tokenAddress,
symbol: balance.symbol,
balance: new BigNumber2(balance.balance.toString()),
balanceInUSD: new BigNumber2(balance.balanceInUSD.toString()),
price: balance.price || 0,
source: "wallet"
}));
} catch (error) {
logger2.debug("SDK getUserBalances failed, using fallback", error);
}
}
for (const [asset, ass