Allow users to liquidate partially

This commit is contained in:
juan 2021-01-27 15:47:07 -05:00
parent 07c19bd183
commit 805faf8b36
4 changed files with 155 additions and 25 deletions

View File

@ -1,35 +1,61 @@
import { Button } from "antd"; import {Slider} from "antd";
import Card from "antd/lib/card"; import Card from "antd/lib/card";
import React, { useCallback } from "react"; import React, {useCallback, useEffect} from "react";
import { useState } from "react"; import { useState } from "react";
import { LABELS } from "../../constants"; import {LABELS, marks} from "../../constants";
import { ParsedAccount } from "../../contexts/accounts"; import {ParsedAccount, useMint} from "../../contexts/accounts";
import { EnrichedLendingObligation, useUserBalance } from "../../hooks"; import {EnrichedLendingObligation, InputType, useSliderInput, useUserBalance} from "../../hooks";
import { LendingReserve } from "../../models"; import { LendingReserve } from "../../models";
import { ActionConfirmation } from "../ActionConfirmation"; import { ActionConfirmation } from "../ActionConfirmation";
import { BackButton } from "../BackButton";
import { CollateralSelector } from "../CollateralSelector";
import { liquidate } from "../../actions"; import { liquidate } from "../../actions";
import "./style.less"; import "./style.less";
import { useConnection } from "../../contexts/connection"; import { useConnection } from "../../contexts/connection";
import { useWallet } from "../../contexts/wallet"; 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: { export const LiquidateInput = (props: {
className?: string; className?: string;
repayReserve: ParsedAccount<LendingReserve>; repayReserve: ParsedAccount<LendingReserve>;
withdrawReserve?: ParsedAccount<LendingReserve>; withdrawReserve: ParsedAccount<LendingReserve>;
obligation: EnrichedLendingObligation; obligation: EnrichedLendingObligation;
}) => { }) => {
const connection = useConnection(); const connection = useConnection();
const { wallet } = useWallet(); const { wallet } = useWallet();
const { repayReserve, withdrawReserve, obligation } = props; const { repayReserve, withdrawReserve, obligation } = props;
const [lastTyped, setLastTyped] = useState("liquidate");
const [pendingTx, setPendingTx] = useState(false); const [pendingTx, setPendingTx] = useState(false);
const [showConfirmation, setShowConfirmation] = 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 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(() => { const onLiquidate = useCallback(() => {
if (!withdrawReserve) { if (!withdrawReserve) {
@ -40,20 +66,33 @@ export const LiquidateInput = (props: {
(async () => { (async () => {
try { try {
const toLiquidateLamports =
type === InputType.Percent && tokenBalance >= borrowAmount
? (pct * borrowAmountLamports) / 100
: Math.ceil(
borrowAmountLamports * (parseFloat(value) / borrowAmount)
);
await liquidate( await liquidate(
connection, connection,
wallet, wallet,
fromAccounts[0], fromAccounts[0],
// TODO: ensure user has available amount // TODO: ensure user has available amount
wadToLamports(obligation.info.borrowAmountWad).toNumber(), toLiquidateLamports,
obligation.account, obligation.account,
repayReserve, repayReserve,
withdrawReserve withdrawReserve
); );
setValue("");
setCollateralValue("");
setShowConfirmation(true); setShowConfirmation(true);
} catch { } catch (error){
// TODO: // TODO:
notify({
message: "Unable to liquidate loan.",
type: "error",
description: error.message,
});
} finally { } finally {
setPendingTx(false); setPendingTx(false);
} }
@ -65,8 +104,64 @@ export const LiquidateInput = (props: {
repayReserve, repayReserve,
wallet, wallet,
connection, 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 = { const bodyStyle: React.CSSProperties = {
display: "flex", display: "flex",
flex: 1, flex: 1,
@ -87,23 +182,58 @@ export const LiquidateInput = (props: {
justifyContent: "space-around", justifyContent: "space-around",
}} }}
> >
<div className="liquidate-input-title"> <div className="repay-input-title">{LABELS.LIQUIDATE_QUESTION}</div>
{LABELS.SELECT_COLLATERAL} <div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-evenly",
alignItems: "center",
}}
>
<CollateralInput
title="Liquidate Amount"
reserve={repayReserve.info}
amount={parseFloat(value) || 0}
onInputChange={(val: number | null) => {
setValue(val?.toString() || "");
setLastTyped("liquidate");
}}
disabled={true}
useWalletBalance={true}
/>
</div> </div>
<CollateralSelector <Slider marks={marks} value={pct} onChange={setPct} />
reserve={repayReserve.info} <div
collateralReserve={withdrawReserve?.pubkey.toBase58()} style={{
disabled={true} display: "flex",
/> flexDirection: "row",
<Button justifyContent: "space-evenly",
alignItems: "center",
marginBottom: 20,
}}
>
<CollateralInput
title="Collateral Amount (estimated)"
reserve={withdrawReserve?.info}
amount={parseFloat(collateralValue) || 0}
onInputChange={(val: number | null) => {
setCollateralValue(val?.toString() || "");
setLastTyped("collateral");
}}
disabled={true}
hideBalance={true}
/>
</div>
<ConnectButton
type="primary" type="primary"
size="large"
onClick={onLiquidate} onClick={onLiquidate}
disabled={fromAccounts.length === 0}
loading={pendingTx} loading={pendingTx}
disabled={fromAccounts.length === 0}
> >
{LABELS.LIQUIDATE_ACTION} {LABELS.LIQUIDATE_ACTION}
</Button> </ConnectButton>
<BackButton />
</div> </div>
)} )}
</Card> </Card>

View File

@ -56,7 +56,7 @@ export const RepayInput = (props: {
const convert = useCallback( const convert = useCallback(
(val: string | number) => { (val: string | number) => {
const minAmount = Math.min(tokenBalance, borrowAmount); const minAmount = Math.min(tokenBalance || Infinity, borrowAmount);
setLastTyped("repay"); setLastTyped("repay");
if (typeof val === "string") { if (typeof val === "string") {
return (parseFloat(val) / minAmount) * 100; return (parseFloat(val) / minAmount) * 100;

View File

@ -40,6 +40,7 @@ export const LABELS = {
NO_COLLATERAL: "No collateral", NO_COLLATERAL: "No collateral",
NO_DEPOSITS: "No deposits", NO_DEPOSITS: "No deposits",
NO_LOANS: "No loans", NO_LOANS: "No loans",
LIQUIDATE_QUESTION: "How much would you like to liquidate?",
LIQUIDATE_ACTION: "Liquidate", LIQUIDATE_ACTION: "Liquidate",
LIQUIDATE_NO_LOANS: "There are no loans to liquidate.", LIQUIDATE_NO_LOANS: "There are no loans to liquidate.",
TABLE_TITLE_ASSET: "Asset", TABLE_TITLE_ASSET: "Asset",

View File

@ -13,7 +13,6 @@ import { LiquidateInput } from "../../components/LiquidateInput";
import "./style.less"; import "./style.less";
import { Col, Row } from "antd"; import { Col, Row } from "antd";
import { GUTTER } from "../../constants"; import { GUTTER } from "../../constants";
import { BorrowInput } from "../../components/BorrowInput";
export const LiquidateReserveView = () => { export const LiquidateReserveView = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();