UNPKG

@moonwell-fi/moonwell-sdk

Version:

TypeScript Interface for Moonwell

253 lines 10.7 kB
import { getContract, parseAbi, zeroAddress } from "viem"; import { Amount } from "../../../common/amount.js"; import { MOONWELL_FETCH_JSON_HEADERS } from "../../../common/fetch-headers.js"; import { publicEnvironments, } from "../../../environments/index.js"; import { findMarketByAddress, findTokenByAddress, } from "../../../environments/utils/index.js"; /** * Error thrown for any failure communicating with the Merkl API: non-ok HTTP * responses, network rejections (fetch threw), and response-body parse errors. * * - HTTP failures populate `status` and `statusText`. * - Network and parse failures leave `status`/`statusText` undefined and * carry the original error via `cause`. */ export class MerklApiError extends Error { constructor(params) { super(params.message, params.cause !== undefined ? { cause: params.cause } : undefined); Object.defineProperty(this, "status", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "statusText", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "url", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "chainId", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.name = "MerklApiError"; this.url = params.url; this.chainId = params.chainId; this.status = params.status; this.statusText = params.statusText; } } export async function getUserMorphoRewardsData(params) { // The Morpho URD distributions endpoint (rewards.morpho.org) was // deprecated and now 301-redirects to a SPA, so JSON parsing fails. // Surface only Merkl rewards. const merklRewards = await getMerklRewardsData(params.environment, params.account, { throwOnError: params.throwOnExternalApiError ?? false }); const isFullDeployment = params.environment.custom.morpho?.minimalDeployment === false; // For full deployments (Base), restrict to Moonwell vault campaigns so the // result excludes staking and other Moonwell campaigns; those are returned // by their own actions (e.g. getUserStakingInfo). On other chains, surface // every Merkl reward we get back. const vaultCampaignIds = isFullDeployment ? new Set(Object.values(publicEnvironments).flatMap((environment) => Object.values(environment.config.vaults ?? {}) .map((vault) => vault.campaignId) .filter((id) => id !== undefined))) : null; const sumBreakdowns = (breakdowns, field) => breakdowns.reduce((acc, curr) => vaultCampaignIds === null || vaultCampaignIds.has(curr.campaignId) ? acc + BigInt(curr[field]) : acc, 0n); const merklResult = []; for (const chainData of merklRewards) { for (const reward of chainData.rewards) { const rewardToken = { address: reward.token.address, decimals: reward.token.decimals, symbol: reward.token.symbol, name: reward.token.symbol, }; const amount = vaultCampaignIds ? sumBreakdowns(reward.breakdowns, "amount") : BigInt(reward.amount); const claimed = vaultCampaignIds ? sumBreakdowns(reward.breakdowns, "claimed") : BigInt(reward.claimed); const pending = vaultCampaignIds ? sumBreakdowns(reward.breakdowns, "pending") : BigInt(reward.pending); const claimableNow = new Amount(amount - claimed, rewardToken.decimals); const claimableNowUsd = claimableNow.value * (reward.token.price ?? 0); const claimableFuture = new Amount(pending, rewardToken.decimals); const claimableFutureUsd = claimableFuture.value * (reward.token.price ?? 0); merklResult.push({ type: "merkl-reward", chainId: chainData.chain.id, account: params.account, rewardToken, claimableNow, claimableNowUsd, claimableFuture, claimableFutureUsd, }); } } return merklResult; } export async function getUserMorphoStakingRewardsData(params) { const vaultsWithStaking = Object.values(params.environment.config.vaults).filter((vault) => Boolean(vault.multiReward)); if (!vaultsWithStaking.length) { return []; } // Hoist shared contract reads outside the per-vault loop const homeEnvironment = Object.values(publicEnvironments).find((e) => e.custom?.governance?.chainIds?.includes(params.environment.chainId)) || params.environment; const viewsContract = params.environment.contracts.views; const homeViewsContract = homeEnvironment.contracts.views; const [allMarkets, nativeTokenPriceRaw, governanceTokenPriceRaw] = await Promise.all([ viewsContract?.read.getAllMarketsInfo(), homeViewsContract?.read.getNativeTokenPrice(), homeViewsContract?.read.getGovernanceTokenPrice(), ]); const governanceTokenPrice = new Amount(governanceTokenPriceRaw || 0n, 18); const nativeTokenPrice = new Amount(nativeTokenPriceRaw || 0n, 18); let tokenPrices = allMarkets ?.map((marketInfo) => { const marketFound = findMarketByAddress(params.environment, marketInfo.market); if (marketFound) { return { token: marketFound.underlyingToken, tokenPrice: new Amount(marketInfo.underlyingPrice, 36 - marketFound.underlyingToken.decimals), }; } else { return; } }) .filter((token) => !!token) || []; // Add governance token to token prices if (params.environment.custom?.governance?.token) { tokenPrices = [ ...tokenPrices, { token: params.environment.config.tokens[params.environment.custom.governance.token], tokenPrice: governanceTokenPrice, }, ]; } // Add native token to token prices tokenPrices = [ ...tokenPrices, { token: findTokenByAddress(params.environment, zeroAddress), tokenPrice: nativeTokenPrice, }, ]; const rewards = await Promise.all(vaultsWithStaking.map(async (vault) => { if (!vault.multiReward) return []; const vaultRewards = await getRewardsEarnedData(params.environment, params.account, vault.multiReward); return vaultRewards .filter((reward) => { return reward !== undefined && reward.amount.value > 0; }) .map((reward) => { const market = tokenPrices.find((m) => m?.token.address === reward.token.address); const priceUsd = market?.tokenPrice.value ?? 0; return { ...reward, chainId: params.environment.chainId, amountUsd: reward.amount.value * priceUsd, }; }); })); return rewards.flat(); } const getRewardsEarnedData = async (environment, userAddress, multiRewardsAddress) => { if (!environment.custom.multiRewarder) { return []; } const multiRewardAbi = parseAbi([ "function earned(address account, address token) view returns (uint256)", ]); const multiRewardContract = getContract({ address: multiRewardsAddress, abi: multiRewardAbi, client: environment.publicClient, }); const rewards = await Promise.all(environment.custom.multiRewarder.map(async (multiRewarder) => { const token = environment.config.tokens[multiRewarder.rewardToken]; if (!token) { return; } try { const earned = await multiRewardContract.read.earned([ userAddress, token.address, ]); return { amount: new Amount(BigInt(earned), token.decimals), token }; } catch { return { amount: new Amount(0n, token.decimals), token }; } })); return rewards.filter(Boolean); }; async function getMerklRewardsData(environment, account, options = { throwOnError: false }) { const url = `https://api.merkl.xyz/v4/users/${account}/rewards?chainId=${environment.chainId}&test=false&breakdownPage=0&reloadChainId=${environment.chainId}`; let response; try { // Merkl campaigns always distribute rewards on the same chain as the // opportunity, so environment.chainId is the only chain we need to query. // The previous two-phase approach (fetch opportunities per vault → extract // chain IDs → fetch rewards per chain) made N+1 HTTP calls to discover // a chain ID we already know. response = await fetch(url, { headers: MOONWELL_FETCH_JSON_HEADERS }); } catch (error) { if (options.throwOnError) { throw new MerklApiError({ message: `Merkl API network error for chain ${environment.chainId}`, url, chainId: environment.chainId, cause: error, }); } console.error(`[getMerklRewardsData:network] chain=${environment.chainId} url=${url}`, error); return []; } if (!response.ok) { const message = `Merkl API request failed for chain ${environment.chainId}: ${response.status} ${response.statusText}`; if (options.throwOnError) { throw new MerklApiError({ message, url, chainId: environment.chainId, status: response.status, statusText: response.statusText, }); } console.warn(`${message} (url=${url})`); return []; } try { return (await response.json()); } catch (error) { if (options.throwOnError) { throw new MerklApiError({ message: `Merkl API response parse error for chain ${environment.chainId}`, url, chainId: environment.chainId, cause: error, }); } console.error(`[getMerklRewardsData:parse] chain=${environment.chainId} url=${url}`, error); return []; } } //# sourceMappingURL=common.js.map