antoncoding/monarch

View on GitHub
src/hooks/useMarkets.ts

Summary

Maintainability
A
2 hrs
Test Coverage
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
'use client';

import { useState, useEffect, useCallback } from 'react';
import { getRewardPer1000USD } from '@/utils/morpho';
import { isSupportedChain } from '@/utils/networks';
import { MORPHOTokenAddress } from '@/utils/tokens';
import { Market } from '@/utils/types';
import { getMarketWarningsWithDetail } from '@/utils/warnings';
import useLiquidations from './useLiquidations';

export type Reward = {
  id: string;
  net_reward_apr: null | string;
  reward_token_rates: {
    token: {
      address: string;
      symbol: string;
    };
    supply_rate: {
      token_amount_per1000_market_token: string; // with decimals
      token_amount_per1000_usd: string; // with decimals
    };
  }[];
};

const marketsQuery = `
  fragment FeedFields on OracleFeed {
    address
    chain {
      id
    }
    description
    id
    pair
    vendor
  }

  query getMarkets($first: Int, $where: MarketFilters) {
    markets(first: $first, where: $where) {
      items {
        id
        lltv
        uniqueKey
        irmAddress
        oracleAddress
        collateralPrice
        morphoBlue {
            id
            address
            chain {
              id
              __typename
            }
            __typename
        }
        oracleInfo {
          type
          __typename
        }
        oracle {
          data {
            ... on MorphoChainlinkOracleData {
              baseFeedOne {
                ...FeedFields
              }
              baseFeedTwo {
                ...FeedFields
              }
              quoteFeedOne {
                ...FeedFields
              }
              quoteFeedTwo {
                ...FeedFields
              }
            }
            ... on MorphoChainlinkOracleV2Data {
              baseFeedOne {
                ...FeedFields
              }
              baseFeedTwo {
                ...FeedFields
              }
              quoteFeedOne {
                ...FeedFields
              }
              quoteFeedTwo {
                ...FeedFields
              }
            }
          }
        }
        loanAsset {
          id
          address
          symbol
          name
          decimals
          priceUsd
          __typename
        }
        collateralAsset {
          id
          address
          symbol
          name
          decimals
          priceUsd
          __typename
        }
        state {
          borrowAssets
          supplyAssets
          borrowAssetsUsd
          supplyAssetsUsd
          borrowShares
          supplyShares
          liquidityAssets
          liquidityAssetsUsd
          collateralAssets
          collateralAssetsUsd
          utilization
          supplyApy
          borrowApy
          fee
          timestamp
          rateAtUTarget
          rewards {
            yearlySupplyTokens
            asset {
              address
              priceUsd
              spotPriceEth
            }
            amountPerSuppliedToken
            amountPerBorrowedToken
          }
          __typename
        }
        warnings {
          type
          level
          __typename
        }
        badDebt {
          underlying
          usd
        }
        realizedBadDebt {
          underlying
          usd
        }
      }
      pageInfo {
        countTotal
        count
        limit
        skip
        __typename
      }  
      __typename
    }
  }
`;

const useMarkets = () => {
  const [loading, setLoading] = useState(true);
  const [isRefetching, setIsRefetching] = useState(false);
  const [data, setData] = useState<Market[]>([]);
  const [error, setError] = useState<unknown | null>(null);
  const {
    loading: liquidationsLoading,
    liquidatedMarketIds,
    error: liquidationsError,
    refetch: refetchLiquidations,
  } = useLiquidations();

  const fetchData = useCallback(
    async (isRefetch = false) => {
      try {
        if (isRefetch) {
          setIsRefetching(true);
        } else {
          setLoading(true);
        }

        // Fetch markets
        const marketsResponse = await fetch('https://blue-api.morpho.org/graphql', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            query: marketsQuery,
            variables: { first: 1000, where: { whitelisted: true } },
          }),
        });
        const marketsResult = await marketsResponse.json();
        const markets = marketsResult.data.markets.items as Market[];

        const filtered = markets
          .filter((market) => market.collateralAsset != undefined)
          .filter(
            (market) => market.warnings.find((w) => w.type === 'not_whitelisted') === undefined,
          )
          .filter((market) => isSupportedChain(market.morphoBlue.chain.id));

        const final = filtered.map((market) => {
          const entry = market.state.rewards.find(
            (reward) => reward.asset.address.toLowerCase() === MORPHOTokenAddress.toLowerCase(),
          );

          const warningsWithDetail = getMarketWarningsWithDetail(market);
          const isProtectedByLiquidationBots = liquidatedMarketIds.has(market.id);

          if (!entry) {
            return {
              ...market,
              rewardPer1000USD: undefined,
              warningsWithDetail,
              isProtectedByLiquidationBots,
            };
          }

          const supplyAssetUSD = Number(market.state.supplyAssetsUsd);
          const rewardPer1000USD = getRewardPer1000USD(entry.yearlySupplyTokens, supplyAssetUSD);

          return {
            ...market,
            rewardPer1000USD,
            warningsWithDetail,
            isProtectedByLiquidationBots,
          };
        });

        setData(final);
      } catch (_error) {
        setError(_error);
      } finally {
        setLoading(false);
        setIsRefetching(false);
      }
    },
    [liquidatedMarketIds],
  );

  useEffect(() => {
    if (!liquidationsLoading) {
      fetchData().catch(console.error);
    }
  }, [liquidationsLoading, fetchData]);

  const refetch = useCallback(
    (onSuccess?: () => void) => {
      refetchLiquidations();
      fetchData(true).then(onSuccess).catch(console.error);
    },
    [refetchLiquidations, fetchData],
  );

  const isLoading = loading || liquidationsLoading;
  const combinedError = error || liquidationsError;

  return { loading: isLoading, isRefetching, data, error: combinedError, refetch };
};

export default useMarkets;