From 805faf8b36947e2f2dead81c4d6c88e5fdb90fcd Mon Sep 17 00:00:00 2001 From: juan Date: Wed, 27 Jan 2021 15:47:07 -0500 Subject: [PATCH] Allow users to liquidate partially --- src/components/LiquidateInput/index.tsx | 176 ++++++++++++++++++++---- src/components/RepayInput/index.tsx | 2 +- src/constants/labels.ts | 1 + src/views/liquidateReserve/index.tsx | 1 - 4 files changed, 155 insertions(+), 25 deletions(-) diff --git a/src/components/LiquidateInput/index.tsx b/src/components/LiquidateInput/index.tsx index 615017e..a7ad9ee 100644 --- a/src/components/LiquidateInput/index.tsx +++ b/src/components/LiquidateInput/index.tsx @@ -1,35 +1,61 @@ -import { Button } from "antd"; +import {Slider} from "antd"; import Card from "antd/lib/card"; -import React, { useCallback } from "react"; +import React, {useCallback, useEffect} from "react"; import { useState } from "react"; -import { LABELS } from "../../constants"; -import { ParsedAccount } from "../../contexts/accounts"; -import { EnrichedLendingObligation, useUserBalance } from "../../hooks"; +import {LABELS, marks} from "../../constants"; +import {ParsedAccount, useMint} from "../../contexts/accounts"; +import {EnrichedLendingObligation, InputType, useSliderInput, useUserBalance} from "../../hooks"; import { LendingReserve } from "../../models"; import { ActionConfirmation } from "../ActionConfirmation"; -import { BackButton } from "../BackButton"; -import { CollateralSelector } from "../CollateralSelector"; import { liquidate } from "../../actions"; import "./style.less"; import { useConnection } from "../../contexts/connection"; import { useWallet } from "../../contexts/wallet"; -import { wadToLamports } from "../../utils/utils"; +import {fromLamports, wadToLamports} from "../../utils/utils"; +import CollateralInput from "../CollateralInput"; +import {notify} from "../../utils/notifications"; +import {ConnectButton} from "../ConnectButton"; +import {useMidPriceInUSD} from "../../contexts/market"; export const LiquidateInput = (props: { className?: string; repayReserve: ParsedAccount; - withdrawReserve?: ParsedAccount; + withdrawReserve: ParsedAccount; obligation: EnrichedLendingObligation; }) => { const connection = useConnection(); const { wallet } = useWallet(); const { repayReserve, withdrawReserve, obligation } = props; + const [lastTyped, setLastTyped] = useState("liquidate"); const [pendingTx, setPendingTx] = useState(false); const [showConfirmation, setShowConfirmation] = useState(false); + const [collateralValue, setCollateralValue] = useState(""); - const { accounts: fromAccounts } = useUserBalance( + const liquidityMint = useMint(repayReserve.info.liquidityMint); + const { accounts: fromAccounts, balance: tokenBalance } = useUserBalance( repayReserve?.info.liquidityMint ); + const borrowAmountLamports = wadToLamports( + obligation.info.borrowAmountWad + ).toNumber(); + + const borrowAmount = fromLamports(borrowAmountLamports, liquidityMint); + + const convert = useCallback( + (val: string | number) => { + const minAmount = Math.min(tokenBalance || Infinity, borrowAmount); + setLastTyped("liquidate"); + if (typeof val === "string") { + return (parseFloat(val) / minAmount) * 100; + } else { + return (val * minAmount) / 100; + } + }, + [borrowAmount, tokenBalance] + ); + + const { value, setValue, pct, setPct, type } = useSliderInput(convert); + const onLiquidate = useCallback(() => { if (!withdrawReserve) { @@ -40,20 +66,33 @@ export const LiquidateInput = (props: { (async () => { try { + const toLiquidateLamports = + type === InputType.Percent && tokenBalance >= borrowAmount + ? (pct * borrowAmountLamports) / 100 + : Math.ceil( + borrowAmountLamports * (parseFloat(value) / borrowAmount) + ); await liquidate( connection, wallet, fromAccounts[0], // TODO: ensure user has available amount - wadToLamports(obligation.info.borrowAmountWad).toNumber(), + toLiquidateLamports, obligation.account, repayReserve, withdrawReserve ); + setValue(""); + setCollateralValue(""); setShowConfirmation(true); - } catch { + } catch (error){ // TODO: + notify({ + message: "Unable to liquidate loan.", + type: "error", + description: error.message, + }); } finally { setPendingTx(false); } @@ -65,8 +104,64 @@ export const LiquidateInput = (props: { repayReserve, wallet, connection, + value, + setValue, + borrowAmount, + borrowAmountLamports, + pct, + tokenBalance, + type ]); + const collateralPrice = useMidPriceInUSD( + withdrawReserve?.info.liquidityMint.toBase58() + )?.price; + + useEffect(() => { + if (withdrawReserve && lastTyped === "liquidate") { + const collateralInQuote = obligation.info.collateralInQuote; + const collateral = collateralInQuote / collateralPrice; + if (value) { + const borrowRatio = (parseFloat(value) / borrowAmount) * 100; + const collateralAmount = (borrowRatio * collateral) / 100; + setCollateralValue(collateralAmount.toString()); + } else { + setCollateralValue(""); + } + } + }, [ + borrowAmount, + collateralPrice, + withdrawReserve, + lastTyped, + obligation.info.collateralInQuote, + value, + ]); + + useEffect(() => { + if (withdrawReserve && lastTyped === "collateral") { + const collateralInQuote = obligation.info.collateralInQuote; + const collateral = collateralInQuote / collateralPrice; + if (collateralValue) { + const collateralRatio = + (parseFloat(collateralValue) / collateral) * 100; + const borrowValue = (collateralRatio * borrowAmount) / 100; + setValue(borrowValue.toString()); + } else { + setValue(""); + } + } + }, [ + borrowAmount, + collateralPrice, + withdrawReserve, + collateralValue, + lastTyped, + obligation.info.collateralInQuote, + setValue, + ]); + + if (!withdrawReserve) return null; const bodyStyle: React.CSSProperties = { display: "flex", flex: 1, @@ -87,23 +182,58 @@ export const LiquidateInput = (props: { justifyContent: "space-around", }} > -
- {LABELS.SELECT_COLLATERAL} +
{LABELS.LIQUIDATE_QUESTION}
+
+ { + setValue(val?.toString() || ""); + setLastTyped("liquidate"); + }} + disabled={true} + useWalletBalance={true} + />
- - - +
)} diff --git a/src/components/RepayInput/index.tsx b/src/components/RepayInput/index.tsx index 99954f1..190b449 100644 --- a/src/components/RepayInput/index.tsx +++ b/src/components/RepayInput/index.tsx @@ -56,7 +56,7 @@ export const RepayInput = (props: { const convert = useCallback( (val: string | number) => { - const minAmount = Math.min(tokenBalance, borrowAmount); + const minAmount = Math.min(tokenBalance || Infinity, borrowAmount); setLastTyped("repay"); if (typeof val === "string") { return (parseFloat(val) / minAmount) * 100; diff --git a/src/constants/labels.ts b/src/constants/labels.ts index 009443f..498f5ea 100644 --- a/src/constants/labels.ts +++ b/src/constants/labels.ts @@ -40,6 +40,7 @@ export const LABELS = { NO_COLLATERAL: "No collateral", NO_DEPOSITS: "No deposits", NO_LOANS: "No loans", + LIQUIDATE_QUESTION: "How much would you like to liquidate?", LIQUIDATE_ACTION: "Liquidate", LIQUIDATE_NO_LOANS: "There are no loans to liquidate.", TABLE_TITLE_ASSET: "Asset", diff --git a/src/views/liquidateReserve/index.tsx b/src/views/liquidateReserve/index.tsx index b722035..ade0c70 100644 --- a/src/views/liquidateReserve/index.tsx +++ b/src/views/liquidateReserve/index.tsx @@ -13,7 +13,6 @@ import { LiquidateInput } from "../../components/LiquidateInput"; import "./style.less"; import { Col, Row } from "antd"; import { GUTTER } from "../../constants"; -import { BorrowInput } from "../../components/BorrowInput"; export const LiquidateReserveView = () => { const { id } = useParams<{ id: string }>();