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 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<LendingReserve>;
withdrawReserve?: ParsedAccount<LendingReserve>;
withdrawReserve: ParsedAccount<LendingReserve>;
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",
}}
>
<div className="liquidate-input-title">
{LABELS.SELECT_COLLATERAL}
<div className="repay-input-title">{LABELS.LIQUIDATE_QUESTION}</div>
<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>
<CollateralSelector
reserve={repayReserve.info}
collateralReserve={withdrawReserve?.pubkey.toBase58()}
disabled={true}
/>
<Button
<Slider marks={marks} value={pct} onChange={setPct} />
<div
style={{
display: "flex",
flexDirection: "row",
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"
size="large"
onClick={onLiquidate}
disabled={fromAccounts.length === 0}
loading={pendingTx}
disabled={fromAccounts.length === 0}
>
{LABELS.LIQUIDATE_ACTION}
</Button>
<BackButton />
</ConnectButton>
</div>
)}
</Card>

View File

@ -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;

View File

@ -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",

View File

@ -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 }>();