UNPKG

0xtrails

Version:

SDK for Trails

418 lines (376 loc) 10.3 kB
import { useMemo, useState, useEffect } from "react" import type { Address, PublicClient } from "viem" import { logger } from "./logger.js" type AccrualPosition = any const MORPHO_API_ENDPOINT = "https://api.morpho.org/graphql" // Supported vault configurations export interface VaultConfig { address: Address name: string chainId: number asset: { symbol: string name: string address: Address decimals: number logoUrl?: string } } // Chain ID to chain name mapping for URLs const CHAIN_ID_TO_NAME: Record<number, string> = { 1: "ethereum", 8453: "base", 42161: "arbitrum", 137: "polygon", 10: "optimism", 100: "gnosis", } // Pool data interface (same as pools.ts) export interface Pool { id: string name: string protocol: string chainId: number apy: number tvl: number token: { symbol: string name: string address: string decimals: number logoUrl?: string } depositAddress: string isActive: boolean poolUrl?: string protocolUrl?: string wrappedTokenGatewayAddress?: string } /** * Fetch all vaults from Morpho GraphQL API */ async function fetchAllVaults(): Promise<any[]> { const vaultsQuery = { query: ` query Vaults { vaults( first: 1000, where: { chainId_in: [1, 8453, 42161, 137, 10, 100] }, orderBy: TotalAssetsUsd, orderDirection: Desc ) { items { address symbol name whitelisted asset { id address decimals logoURI } chain { id network } state { totalAssets totalAssetsUsd totalSupply apy netApy netApyWithoutRewards rewards { asset { address chain { id } } supplyApr yearlySupplyTokens } allocation { supplyAssets supplyAssetsUsd market { uniqueKey state { rewards { asset { address chain { id } } supplyApr borrowApr } } } } } } } } `, } try { const response = await fetch(MORPHO_API_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(vaultsQuery), }) const vaultsData = (await response.json()) as any return vaultsData.data?.vaults?.items || [] } catch (apiError) { logger.console.error("Failed to fetch vaults from GraphQL API:", apiError) return [] } } /** * Fetch vault data from Morpho GraphQL API */ export async function fetchVaultData( vaultAddress: Address, chainId: number, ): Promise<any | null> { const vaultQuery = { query: ` query VaultByAddress($address: String!, $chainId: Int) { vaultByAddress(address: $address, chainId: $chainId) { address id state { totalAssets totalSupply rewards { asset { address name symbol chain { id } } amountPerSuppliedToken supplyApr } allocation { market { id uniqueKey state { rewards { supplyApr amountPerSuppliedToken asset { address symbol chain { id } } } } } supplyAssetsUsd } } chain { id } } } `, variables: { address: vaultAddress.toLowerCase(), chainId: chainId, }, } try { const response = await fetch(MORPHO_API_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(vaultQuery), }) const vaultData = (await response.json()) as any return vaultData.data?.vaultByAddress || null } catch (apiError) { logger.console.error( "Failed to fetch vault data from GraphQL API:", apiError, ) return null } } /** * Calculate vault APY from SDK data */ export function calculateVaultApy(vault: any): number { try { if (vault.totalAssets === 0n) { return 0 } // Convert allocations Map to array and calculate weighted APY const allocationsArray = Array.from(vault.allocations.values()) const totalWeightedApy = allocationsArray.reduce( (total: bigint, allocation: any) => { const position: AccrualPosition = allocation.position const market = position.market if (market && position.supplyShares > 0n) { // Get current supply assets and market APY const supplyAssets = position.supplyAssets const marketSupplyApy = BigInt(market.supplyApy || 0) // Calculate weighted APY for this allocation return total + marketSupplyApy * supplyAssets } return total }, 0n, ) // Calculate base APY (before fees) const baseApyBigInt = totalWeightedApy / vault.totalAssets const baseApy = Number(baseApyBigInt) / 1e18 // Apply vault fee (fee is in WAD format, 1e18 = 100%) const vaultFeeRate = Number(vault.fee) / 1e18 return baseApy * (1 - vaultFeeRate) } catch (calculationError) { logger.console.error("Failed to calculate vault APY:", calculationError) return 0 } } /** * Calculate rewards APR from API data */ export function calculateRewardsApr(vaultData: any): number { if (!vaultData?.state?.rewards) { return 0 } return vaultData.state.rewards.reduce((total: number, reward: any) => { return total + (reward.supplyApr || 0) }, 0) } /** * Transform API vault data to Pool interface */ function transformVaultToPool(vaultData: any): Pool | null { try { if (!vaultData || !vaultData.address || !vaultData.chain?.id) { return null } const chainId = vaultData.chain.id const asset = vaultData.asset const state = vaultData.state // Use the actual APY from the API const apy = state?.apy || state?.netApy || 0 // Calculate TVL from totalAssetsUsd if available, otherwise from totalAssets const tvl = state?.totalAssetsUsd ? Number(state.totalAssetsUsd) : state?.totalAssets ? Number(state.totalAssets) / 10 ** (asset?.decimals || 18) : 0 // Filter out vaults with TVL < 1M OR APY > 20% const tvlInMillions = tvl / 1_000_000 const apyPercentage = apy * 100 if (tvlInMillions < 1 || apyPercentage > 20) { // logger.console.log( // `Filtering out vault ${vaultData.address}: TVL=${tvlInMillions.toFixed(2)}M, APY=${apyPercentage.toFixed(2)}%`, // ) return null } return { id: `${vaultData.address}-${chainId}`, name: vaultData.name || `${vaultData.symbol || "Unknown"} Vault`, protocol: "Morpho", chainId: chainId, apy: apy * 100, // Convert to percentage tvl: tvl, token: { symbol: vaultData.symbol || asset?.symbol || "UNKNOWN", name: vaultData.name || asset?.name || "Unknown Token", address: asset?.address || vaultData.address, decimals: asset?.decimals || 18, logoUrl: asset?.logoURI || undefined, }, depositAddress: vaultData.address, isActive: vaultData.whitelisted !== false, // Consider whitelisted vaults as active protocolUrl: "https://app.morpho.org/", poolUrl: `https://app.morpho.org/${CHAIN_ID_TO_NAME[chainId]}/vault/${vaultData.address}`, } } catch (error) { logger.console.error(`Failed to transform vault data:`, error) return null } } /** * Hook to fetch Morpho vaults */ export function useMorphoVaults() { const [vaults, setVaults] = useState<Pool[]>([]) const [loading, setLoading] = useState(true) const [error, setError] = useState<Error | null>(null) useEffect(() => { async function fetchVaults() { try { setLoading(true) setError(null) const vaultsData = await fetchAllVaults() const transformedVaults = vaultsData .map(transformVaultToPool) .filter((vault): vault is Pool => vault !== null) setVaults(transformedVaults) } catch (err) { setError( err instanceof Error ? err : new Error("Failed to fetch vaults"), ) } finally { setLoading(false) } } fetchVaults() }, []) // Sort by APY descending const sortedVaults = useMemo(() => { return vaults.sort((a: Pool, b: Pool) => b.apy - a.apy) }, [vaults]) return { data: sortedVaults, loading, error, } } /** * Get all vaults for a specific chain */ export async function getVaultsForChain( chainId: number, _publicClient: PublicClient, ): Promise<Pool[]> { const vaultsData = await fetchAllVaults() const chainVaults = vaultsData.filter((vault) => vault.chain?.id === chainId) const transformedVaults = chainVaults .map(transformVaultToPool) .filter((vault): vault is Pool => vault !== null) return transformedVaults } /** * Get all vaults across all chains */ export async function getAllVaults( _publicClient: PublicClient, ): Promise<Pool[]> { const vaultsData = await fetchAllVaults() const transformedVaults = vaultsData .map(transformVaultToPool) .filter((vault): vault is Pool => vault !== null) return transformedVaults } export default useMorphoVaults