UNPKG

@moonwell-fi/moonwell-sdk

Version:

TypeScript Interface for Moonwell

700 lines (633 loc) 20 kB
import lodash from "lodash"; const { uniq } = lodash; import { type Address, getContract, parseAbi, zeroAddress } from "viem"; import { Amount } from "../../../common/amount.js"; import { MOONWELL_FETCH_JSON_HEADERS } from "../../../common/fetch-headers.js"; import { type Environment, type TokenConfig, publicEnvironments, } from "../../../environments/index.js"; import { findMarketByAddress, findTokenByAddress, } from "../../../environments/utils/index.js"; import type { MorphoUserReward } from "../../../types/morphoUserReward.js"; import type { MorphoUserStakingReward } from "../../../types/morphoUserStakingReward.js"; import { getGraphQL } from "../utils/graphql.js"; export async function getUserMorphoRewardsData(params: { environment: Environment; account: `0x${string}`; }): Promise<MorphoUserReward[]> { const merklRewards = await getMerklRewardsData( params.environment, params.account, ); if (params.environment.custom.morpho?.minimalDeployment === false) { const morphoRewards = await getMorphoRewardsData( params.environment.chainId, params.account, ); // Process Morpho rewards const morphoAssets = await getMorphoAssetsData( morphoRewards.map((r) => r.asset.address), ); const morphoResult: (MorphoUserReward | undefined)[] = morphoRewards.map( (r) => { const asset = morphoAssets.find( (a) => a.address.toLowerCase() === r.asset.address.toLowerCase(), ); if (!asset) { return undefined; } const rewardToken: TokenConfig = { address: asset.address, decimals: asset.decimals, symbol: asset.symbol, name: asset.name, }; switch (r.type) { case "uniform-reward": { const claimableNow = new Amount( BigInt(r.amount?.claimable_now || 0), rewardToken.decimals, ); const claimableNowUsd = claimableNow.value * (asset.priceUsd || 0); const claimableFuture = new Amount( BigInt(r.amount?.claimable_next || 0), rewardToken.decimals, ); const claimableFutureUsd = claimableFuture.value * (asset.priceUsd || 0); const uniformReward: MorphoUserReward = { type: "uniform-reward", chainId: r.asset.chain_id, account: r.user, rewardToken, claimableNow, claimableNowUsd, claimableFuture, claimableFutureUsd, }; return uniformReward; } case "market-reward": { const claimableNow = new Amount( BigInt(r.for_supply?.claimable_now || 0), rewardToken.decimals, ); const claimableNowUsd = claimableNow.value * (asset.priceUsd || 0); const claimableFuture = new Amount( BigInt(r.for_supply?.claimable_next || 0), rewardToken.decimals, ); const claimableFutureUsd = claimableFuture.value * (asset.priceUsd || 0); const collateralClaimableNow = new Amount( BigInt(r.for_collateral?.claimable_now || 0), rewardToken.decimals, ); const collateralClaimableNowUsd = collateralClaimableNow.value * (asset.priceUsd || 0); const collateralClaimableFuture = new Amount( BigInt(r.for_collateral?.claimable_next || 0), rewardToken.decimals, ); const collateralClaimableFutureUsd = collateralClaimableFuture.value * (asset.priceUsd || 0); const borrowClaimableNow = new Amount( BigInt(r.for_borrow?.claimable_now || 0), rewardToken.decimals, ); const borrowClaimableNowUsd = borrowClaimableNow.value * (asset.priceUsd || 0); const borrowClaimableFuture = new Amount( BigInt(r.for_borrow?.claimable_next || 0), rewardToken.decimals, ); const borrowClaimableFutureUsd = borrowClaimableFuture.value * (asset.priceUsd || 0); //Rewards reallocated to vaults are reported as vault rewards if (r.reallocated_from) { const vaultReward: MorphoUserReward = { type: "vault-reward", chainId: r.program.chain_id, account: r.user, vaultId: r.reallocated_from, rewardToken, claimableNow, claimableNowUsd, claimableFuture, claimableFutureUsd, }; return vaultReward; } else { const marketReward: MorphoUserReward = { type: "market-reward", chainId: r.program.chain_id, account: r.user, marketId: r.program.market_id || "", rewardToken, collateralRewards: { claimableNow: collateralClaimableNow, claimableNowUsd: collateralClaimableNowUsd, claimableFuture: collateralClaimableFuture, claimableFutureUsd: collateralClaimableFutureUsd, }, borrowRewards: { claimableNow: borrowClaimableNow, claimableNowUsd: borrowClaimableNowUsd, claimableFuture: borrowClaimableFuture, claimableFutureUsd: borrowClaimableFutureUsd, }, }; return marketReward; } } case "vault-reward": { const claimableNow = new Amount( BigInt(r.for_supply?.claimable_now || 0), rewardToken.decimals, ); const claimableNowUsd = claimableNow.value * (asset.priceUsd || 0); const claimableFuture = new Amount( BigInt(r.for_supply?.claimable_next || 0), rewardToken.decimals, ); const claimableFutureUsd = claimableFuture.value * (asset.priceUsd || 0); const vaultReward: MorphoUserReward = { type: "vault-reward", chainId: r.program.chain_id, account: r.user, vaultId: r.program.vault, rewardToken, claimableNow, claimableNowUsd, claimableFuture, claimableFutureUsd, }; return vaultReward; } } }, ); // Process Merkl rewards const merklResult: MorphoUserReward[] = []; for (const chainData of merklRewards) { for (const reward of chainData.rewards) { // Try to find token info in morphoAssets first const morphoAsset = morphoAssets.find( (a) => a.address.toLowerCase() === reward.token.address.toLowerCase(), ); const rewardToken: TokenConfig = { address: reward.token.address as Address, decimals: morphoAsset?.decimals ?? reward.token.decimals, symbol: morphoAsset?.symbol ?? reward.token.symbol, name: morphoAsset?.name ?? reward.token.symbol, }; const claimableNow = new Amount( BigInt(reward.amount) - BigInt(reward.claimed), rewardToken.decimals, ); const claimableNowUsd = claimableNow.value * (morphoAsset?.priceUsd ?? reward.token.price ?? 0); const claimableFuture = new Amount( BigInt(reward.pending), rewardToken.decimals, ); const claimableFutureUsd = claimableFuture.value * (morphoAsset?.priceUsd ?? reward.token.price ?? 0); const merklReward: MorphoUserReward = { type: "merkl-reward", chainId: chainData.chain.id, account: params.account, rewardToken, claimableNow, claimableNowUsd, claimableFuture, claimableFutureUsd, }; merklResult.push(merklReward); } } // Combine both results const allResults = [ ...(morphoResult.filter((r) => r !== undefined) as MorphoUserReward[]), ...merklResult, ]; return allResults; } const merklResult: MorphoUserReward[] = []; for (const chainData of merklRewards) { for (const reward of chainData.rewards) { const rewardToken: TokenConfig = { address: reward.token.address as Address, decimals: reward.token.decimals, symbol: reward.token.symbol, name: reward.token.symbol, }; const claimableNow = new Amount( BigInt(reward.amount) - BigInt(reward.claimed), rewardToken.decimals, ); const claimableNowUsd = claimableNow.value * (reward.token.price ?? 0); const claimableFuture = new Amount( BigInt(reward.pending), rewardToken.decimals, ); const claimableFutureUsd = claimableFuture.value * (reward.token.price ?? 0); const merklReward: MorphoUserReward = { type: "merkl-reward", chainId: chainData.chain.id, account: params.account, rewardToken, claimableNow, claimableNowUsd, claimableFuture, claimableFutureUsd, }; merklResult.push(merklReward); } } return merklResult; } export async function getUserMorphoStakingRewardsData(params: { environment: Environment; account: `0x${string}`; }): Promise<MorphoUserStakingReward[]> { const vaultsWithStaking = Object.values( params.environment.config.vaults, ).filter((vault) => Boolean(vault.multiReward)); if (!vaultsWithStaking.length) { return []; } const rewards = await Promise.all( vaultsWithStaking.map(async (vault) => { if (!vault.multiReward) return []; const vaultRewards = await getRewardsEarnedData( params.environment, params.account, vault.multiReward, ); const homeEnvironment = (Object.values(publicEnvironments) as Environment[]).find((e) => e.custom?.governance?.chainIds?.includes(params.environment.chainId), ) || params.environment; const viewsContract = params.environment.contracts.views; const homeViewsContract = homeEnvironment.contracts.views; const userData = await Promise.all([ viewsContract?.read.getAllMarketsInfo(), homeViewsContract?.read.getNativeTokenPrice(), homeViewsContract?.read.getGovernanceTokenPrice(), ]); const [allMarkets, nativeTokenPriceRaw, governanceTokenPriceRaw] = userData; 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, }, ]; return vaultRewards .filter((reward): reward is { amount: Amount; token: TokenConfig } => { 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: Environment, userAddress: Address, multiRewardsAddress: Address, ) => { 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); }; type MorphoRewardsResponse = { user: Address; for_borrow: { claimable_next: string; claimable_now: string; claimed: string; total: string; }; for_collateral: { claimable_next: string; claimable_now: string; claimed: string; total: string; }; for_supply: { claimable_next: string; claimable_now: string; claimed: string; total: string; }; program: { asset: { address: Address }; market_id?: string; chain_id: number; vault: Address; }; asset: { address: Address; chain_id: number }; amount?: { claimable_next: string; claimable_now: string }; type: "vault-reward" | "market-reward" | "uniform-reward"; reallocated_from: Address; }; type MorphoAssetResponse = { address: Address; symbol: string; priceUsd: number | undefined; name: string; decimals: number; }; async function getMorphoRewardsData( chainId: number, account: Address, ): Promise<MorphoRewardsResponse[]> { const rewardsRequest = await fetch( `https://rewards.morpho.org/v1/users/${account}/rewards?chain_id=${chainId}&trusted=true&exclude_merkl_programs=true`, { headers: MOONWELL_FETCH_JSON_HEADERS, }, ); const rewards = await rewardsRequest.json(); return (rewards.data || []) as MorphoRewardsResponse[]; } async function getMorphoAssetsData( addresses: Address[], ): Promise<MorphoAssetResponse[]> { const rewardsRequest = await getGraphQL<{ assets: { items: MorphoAssetResponse[]; }; }>(` query { assets(where: { address_in:[${uniq(addresses) .map((a: string) => `"${a.toLowerCase()}"`) .join(",")}]}) { items { address symbol priceUsd name decimals } } } `); if (rewardsRequest) { return rewardsRequest.assets.items; } return []; } type MerklRewardsResponse = { chain: { id: number; name: string; icon: string; liveCampaigns: number; endOfDisputePeriod: number; Explorer: Array<{ id: string; type: string; url: string; chainId: number; }>; }; rewards: Array<{ root: string; recipient: string; amount: string; claimed: string; pending: string; proofs: string[]; token: { address: string; chainId: number; symbol: string; decimals: number; price: number; }; breakdowns: Array<{ reason: string; amount: string; claimed: string; pending: string; campaignId: string; subCampaignId?: string; }>; }>; }; // Types for Merkl Opportunities API type MerklToken = { id: string; name: string; chainId: number; address: string; decimals: number; symbol: string; displaySymbol: string; icon: string; verified: boolean; isTest: boolean; type: string; isNative: boolean; price: number; }; type MerklRewardBreakdown = { token: MerklToken; amount: string; value: number; distributionType: string; id: string; timestamp: string; campaignId: string; dailyRewardsRecordId: string; }; type MerklOpportunity = { chainId: number; type: string; identifier: string; name: string; description: string; howToSteps: string[]; status: string; action: string; tvl: number; apr: number; dailyRewards: number; tags: any[]; id: string; depositUrl: string; explorerAddress: string; lastCampaignCreatedAt: number; aprRecord: { cumulated: number; timestamp: string; breakdowns: { distributionType: string; identifier: string; type: string; value: number; timestamp: string; }[]; }; rewardsRecord: { id: string; total: number; timestamp: string; breakdowns: MerklRewardBreakdown[]; }; }; async function getMerklRewardsData( environment: Environment, account: Address, ): Promise<MerklRewardsResponse[]> { try { // Get unique chain IDs from vault opportunities const chainIdsPromises = Object.values(environment.config.vaults).map( async (vault) => { try { const response = await fetch( `https://api.merkl.xyz/v4/opportunities?identifier=${environment.config.tokens[vault.vaultToken]?.address}&chainId=${environment.chainId}&status=LIVE`, { headers: MOONWELL_FETCH_JSON_HEADERS, }, ); if (!response.ok) { console.warn( `Failed to fetch opportunities: ${response.status} ${response.statusText}`, ); return []; } const data: MerklOpportunity[] = await response.json(); return data.flatMap((opportunity) => opportunity.rewardsRecord.breakdowns.map( (breakdown) => breakdown.token.chainId, ), ); } catch (error) { console.warn( `Error fetching opportunities for vault ${vault.vaultToken}:`, error, ); return []; } }, ); const chainIds = [...new Set((await Promise.all(chainIdsPromises)).flat())]; // Fetch rewards for each unique chain ID const rewardsPromises = chainIds.map(async (chainId) => { try { const response = await fetch( `https://api.merkl.xyz/v4/users/${account}/rewards?chainId=${chainId}&test=false&breakdownPage=0&reloadChainId=${chainId}`, { headers: MOONWELL_FETCH_JSON_HEADERS, }, ); if (!response.ok) { console.warn( `Merkl API request failed: ${response.status} ${response.statusText}`, ); return []; } return (await response.json()) as MerklRewardsResponse[]; } catch (error) { console.warn( `Error fetching Merkl rewards for chain ${chainId}:`, error, ); return []; } }); const allRewards = await Promise.all(rewardsPromises); return allRewards.flat(); } catch (error) { console.error("Error in getMerklRewardsData:", error); return []; } }