diff --git a/components/BannerManager/BannerManager.tsx b/components/BannerManager/BannerManager.tsx index 1ff199f38..648ced2d2 100644 --- a/components/BannerManager/BannerManager.tsx +++ b/components/BannerManager/BannerManager.tsx @@ -5,62 +5,32 @@ import styled from 'styled-components'; import Banner, { BannerType } from 'sections/shared/Layout/Banner'; import { LOCAL_STORAGE_KEYS } from 'constants/storage'; import { ExternalLink } from 'styles/common'; -import { formatShortDateWithTime } from 'utils/formatters/date'; -import { wei } from '@synthetixio/wei'; import useSynthetixQueries from '@synthetixio/queries'; import { EXTERNAL_LINKS } from 'constants/links'; import Connector from 'containers/Connector'; import { isAnyElectionInNomination, isAnyElectionInVoting } from 'utils/governance'; +import { useLiquidation, LiquidationBanner } from './Liquidation'; const BannerManager: FC = () => { - const { subgraph, useGetLiquidationDataQuery, useGetDebtDataQuery, useGetElectionsPeriodStatus } = - useSynthetixQueries(); + const { subgraph, useGetElectionsPeriodStatus } = useSynthetixQueries(); const { L2DefaultProvider, isL2, walletAddress } = Connector.useContainer(); const periodStatusQuery = useGetElectionsPeriodStatus(L2DefaultProvider); const electionIsInNomination = isAnyElectionInNomination(periodStatusQuery.data); const electionIsInVoting = isAnyElectionInVoting(periodStatusQuery.data); - const liquidationData = useGetLiquidationDataQuery(walletAddress); - const debtData = useGetDebtDataQuery(walletAddress); - const feeClaims = subgraph.useGetFeesClaimeds( { first: 1, where: { account: walletAddress?.toLowerCase() } }, { timestamp: true, value: true, rewards: true } ); - - const issuanceRatio = debtData?.data?.targetCRatio ?? wei(0); - const cRatio = debtData?.data?.currentCRatio ?? wei(0); - const liquidationDeadlineForAccount = - liquidationData?.data?.liquidationDeadlineForAccount ?? wei(0); - - const issuanceRatioPercentage = issuanceRatio.eq(0) ? 0 : 100 / Number(issuanceRatio); - const hasClaimHistory = !!feeClaims.data?.length; - if (!liquidationDeadlineForAccount.eq(0) && cRatio.gt(issuanceRatio)) { - return ( - , - , - , - ]} - /> - } - /> - ); + const { deadline, hasWarning, ratio } = useLiquidation(); + + if (hasWarning) { + return ; } + if (electionIsInVoting) { return ( = ({ ratio, deadline }) => { + return ( + , + , + , + ]} + /> + } + /> + ); +}; + +const Strong = styled.strong` + font-family: ${(props) => props.theme.fonts.condensedBold}; +`; + +const StyledExternalLink = styled(ExternalLink)` + color: ${(props) => props.theme.colors.white}; + text-decoration: underline; + &:hover { + text-decoration: underline; + } +`; diff --git a/components/BannerManager/Liquidation/index.ts b/components/BannerManager/Liquidation/index.ts new file mode 100644 index 000000000..ec5bc8ada --- /dev/null +++ b/components/BannerManager/Liquidation/index.ts @@ -0,0 +1,3 @@ +export * from './useLiquidation'; +export * from './useLiquidationOptimismSubgraph'; +export * from './LiquidationBanner'; diff --git a/components/BannerManager/Liquidation/useLiquidation.ts b/components/BannerManager/Liquidation/useLiquidation.ts new file mode 100644 index 000000000..b86f24bfe --- /dev/null +++ b/components/BannerManager/Liquidation/useLiquidation.ts @@ -0,0 +1,34 @@ +import { wei } from '@synthetixio/wei'; +import useSynthetixQueries from '@synthetixio/queries'; +import Connector from 'containers/Connector'; +import { useLiquidationOptimismSubgraph } from './useLiquidationOptimismSubgraph'; + +export function useLiquidation() { + const { useGetLiquidationDataQuery, useGetDebtDataQuery } = useSynthetixQueries(); + const { walletAddress } = Connector.useContainer(); + + const liquidationData = useGetLiquidationDataQuery(walletAddress); + const debtData = useGetDebtDataQuery(walletAddress); + + const issuanceRatio = debtData?.data?.targetCRatio ?? wei(0); + const cRatio = debtData?.data?.currentCRatio ?? wei(0); + const liquidationDeadlineForAccount = + liquidationData?.data?.liquidationDeadlineForAccount ?? wei(0); + + const ratio = issuanceRatio.eq(0) ? 0 : 100 / Number(issuanceRatio); + + const optimismDeadline = useLiquidationOptimismSubgraph(); + const deadline = + optimismDeadline > 0 + ? optimismDeadline + : Number(liquidationDeadlineForAccount.toString()) * 1000; + + const hasWarning = + optimismDeadline > 0 || (!liquidationDeadlineForAccount.eq(0) && cRatio.gt(issuanceRatio)); + + return { + hasWarning, + ratio, + deadline, + }; +} diff --git a/components/BannerManager/Liquidation/useLiquidationOptimismSubgraph.ts b/components/BannerManager/Liquidation/useLiquidationOptimismSubgraph.ts new file mode 100644 index 000000000..15f69dd26 --- /dev/null +++ b/components/BannerManager/Liquidation/useLiquidationOptimismSubgraph.ts @@ -0,0 +1,86 @@ +import { useQuery, UseQueryOptions } from 'react-query'; +import Connector from 'containers/Connector'; + +function getEndpoint(networkName: String) { + switch (networkName) { + case 'kovan-ovm': + return 'https://api.thegraph.com/subgraphs/name/noisekit/liquidator-optimism-kovan'; + case 'mainnet-ovm': + return 'https://api.thegraph.com/subgraphs/name/noisekit/liquidator-optimism'; + default: + throw Error(`Called with unsupported network: ${networkName}`); + } +} + +const gql = (data: any) => data[0]; +const query = gql` + query stakerEntity($id: String!) { + stakerEntity(id: $id) { + id + timestamp + status + } + } +`; + +type StakerEntity = { + id: string; + timestamp: string; + status: string; +} | null; + +export async function fetchLiquidationInfo(walletAddress: string, networkName: string) { + const endpoint = getEndpoint(networkName); + + const body = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + query, + variables: { + id: walletAddress, + }, + }), + }); + + const { + errors, + data, + }: { + errors?: Error[]; + data: { stakerEntity: StakerEntity }; + } = await body.json(); + + if (errors?.[0]) { + throw new Error(errors?.[0]?.message || 'Unknown server error'); + } + return data?.stakerEntity; +} + +export function useLiquidationOptimismInfo(queryOptions?: UseQueryOptions) { + const { walletAddress, network } = Connector.useContainer(); + return useQuery( + [walletAddress, network?.name], + () => { + if (!walletAddress || !network?.name) { + throw Error('Missing address or network, query should not run without it'); + } + return fetchLiquidationInfo(walletAddress, network?.name); + }, + { + enabled: Boolean(walletAddress && network?.name), + ...queryOptions, + } + ); +} + +export function useLiquidationOptimismSubgraph(queryOptions?: UseQueryOptions) { + const { data, isSuccess } = useLiquidationOptimismInfo(queryOptions); + if (!isSuccess) { + return 0; + } + return data?.status === 'FLAGGED' ? parseInt(data.timestamp, 10) * 1000 : 0; +}