mirror of https://github.com/certusone/oyster.git
Allow users to liquidate partially
This commit is contained in:
parent
07c19bd183
commit
805faf8b36
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 }>();
|
||||||
|
|
Loading…
Reference in New Issue