Merge pull request #53 from yamijuan/repay-partially
Allow users to liquidate partially
This commit is contained in:
commit
c5db4c6e6e
|
@ -125,9 +125,7 @@ export const borrow = async (
|
|||
signers
|
||||
)
|
||||
: undefined;
|
||||
|
||||
|
||||
|
||||
|
||||
let amountLamports: number = 0;
|
||||
let fromLamports: number = 0;
|
||||
if (amountType === BorrowAmountType.LiquidityBorrowAmount) {
|
||||
|
@ -197,7 +195,6 @@ export const borrow = async (
|
|||
instructions = [];
|
||||
cleanupInstructions = [...finalCleanupInstructions];
|
||||
|
||||
|
||||
// create approval for transfer transactions
|
||||
const transferAuthority = approve(
|
||||
instructions,
|
||||
|
|
|
@ -195,6 +195,7 @@ export const BorrowInput = (props: {
|
|||
onCollateralReserve={(key) => {
|
||||
setCollateralReserveKey(key);
|
||||
}}
|
||||
useFirstReserve={true}
|
||||
/>
|
||||
</div>
|
||||
<ArrowDownOutlined />
|
||||
|
@ -204,7 +205,7 @@ export const BorrowInput = (props: {
|
|||
flexDirection: "row",
|
||||
justifyContent: "space-evenly",
|
||||
alignItems: "center",
|
||||
marginBottom: 20
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<CollateralInput
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { cache, ParsedAccount } from "../../contexts/accounts";
|
||||
import { useConnectionConfig } from "../../contexts/connection";
|
||||
import { useLendingReserves, useUserDeposits } from "../../hooks";
|
||||
import {
|
||||
useLendingReserves,
|
||||
useUserBalance,
|
||||
useUserDeposits,
|
||||
} from "../../hooks";
|
||||
import {
|
||||
LendingReserve,
|
||||
LendingMarket,
|
||||
|
@ -27,9 +31,12 @@ export default function CollateralInput(props: {
|
|||
onLeverage?: (leverage: number) => void;
|
||||
onInputChange: (value: number | null) => void;
|
||||
hideBalance?: boolean;
|
||||
useWalletBalance?: boolean;
|
||||
useFirstReserve?: boolean;
|
||||
showLeverageSelector?: boolean;
|
||||
leverage?: number;
|
||||
}) {
|
||||
const { balance: tokenBalance } = useUserBalance(props.reserve.liquidityMint);
|
||||
const { reserveAccounts } = useLendingReserves();
|
||||
const { tokenMap } = useConnectionConfig();
|
||||
const [collateralReserve, setCollateralReserve] = useState<string>();
|
||||
|
@ -38,21 +45,26 @@ export default function CollateralInput(props: {
|
|||
const userDeposits = useUserDeposits();
|
||||
|
||||
useEffect(() => {
|
||||
const id: string =
|
||||
cache
|
||||
.byParser(LendingReserveParser)
|
||||
.find((acc) => acc === collateralReserve) || "";
|
||||
const parser = cache.get(id) as ParsedAccount<LendingReserve>;
|
||||
if (parser) {
|
||||
const collateralDeposit = userDeposits.userDeposits.find(
|
||||
(u) =>
|
||||
u.reserve.info.liquidityMint.toBase58() ===
|
||||
parser.info.liquidityMint.toBase58()
|
||||
);
|
||||
if (collateralDeposit) setBalance(collateralDeposit.info.amount);
|
||||
else setBalance(0);
|
||||
if (props.useWalletBalance) {
|
||||
setBalance(tokenBalance);
|
||||
} else {
|
||||
const id: string =
|
||||
cache
|
||||
.byParser(LendingReserveParser)
|
||||
.find((acc) => acc === collateralReserve) || "";
|
||||
const parser = cache.get(id) as ParsedAccount<LendingReserve>;
|
||||
|
||||
if (parser) {
|
||||
const collateralDeposit = userDeposits.userDeposits.find(
|
||||
(u) =>
|
||||
u.reserve.info.liquidityMint.toBase58() ===
|
||||
parser.info.liquidityMint.toBase58()
|
||||
);
|
||||
if (collateralDeposit) setBalance(collateralDeposit.info.amount);
|
||||
else setBalance(0);
|
||||
}
|
||||
}
|
||||
}, [collateralReserve, userDeposits]);
|
||||
}, [collateralReserve, userDeposits, tokenBalance, props.useWalletBalance]);
|
||||
|
||||
const market = cache.get(props.reserve.lendingMarket) as ParsedAccount<
|
||||
LendingMarket
|
||||
|
@ -63,26 +75,35 @@ export default function CollateralInput(props: {
|
|||
market?.info?.quoteMint
|
||||
);
|
||||
|
||||
const renderReserveAccounts = reserveAccounts
|
||||
const filteredReserveAccounts = reserveAccounts
|
||||
.filter((reserve) => reserve.info !== props.reserve)
|
||||
.filter(
|
||||
(reserve) =>
|
||||
!onlyQuoteAllowed ||
|
||||
reserve.info.liquidityMint.equals(market.info.quoteMint)
|
||||
)
|
||||
.map((reserve) => {
|
||||
const mint = reserve.info.liquidityMint.toBase58();
|
||||
const address = reserve.pubkey.toBase58();
|
||||
const name = getTokenName(tokenMap, mint);
|
||||
return (
|
||||
<Option key={address} value={address} name={name} title={address}>
|
||||
<div key={address} style={{ display: "flex", alignItems: "center" }}>
|
||||
<TokenIcon mintAddress={mint} />
|
||||
{name}
|
||||
</div>
|
||||
</Option>
|
||||
);
|
||||
});
|
||||
);
|
||||
|
||||
if (
|
||||
!collateralReserve &&
|
||||
props.useFirstReserve &&
|
||||
filteredReserveAccounts.length
|
||||
) {
|
||||
const address = filteredReserveAccounts[0].pubkey.toBase58();
|
||||
setCollateralReserve(address);
|
||||
}
|
||||
const renderReserveAccounts = filteredReserveAccounts.map((reserve) => {
|
||||
const mint = reserve.info.liquidityMint.toBase58();
|
||||
const address = reserve.pubkey.toBase58();
|
||||
const name = getTokenName(tokenMap, mint);
|
||||
return (
|
||||
<Option key={address} value={address} name={name} title={address}>
|
||||
<div key={address} style={{ display: "flex", alignItems: "center" }}>
|
||||
<TokenIcon mintAddress={mint} />
|
||||
{name}
|
||||
</div>
|
||||
</Option>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
|
|
@ -1,35 +1,65 @@
|
|||
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 +70,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 +108,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 +186,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>
|
||||
|
|
|
@ -70,7 +70,7 @@ export const LoanInfoLine = (props: {
|
|||
<Card className={props.className}>
|
||||
<Statistic
|
||||
title="Collateral"
|
||||
value={obligation.info.borrowedInQuote}
|
||||
value={obligation.info.collateralInQuote}
|
||||
formatter={(val) => (
|
||||
<div>
|
||||
<div>
|
||||
|
|
|
@ -38,6 +38,9 @@ export const RepayInput = (props: {
|
|||
const obligation = props.obligation;
|
||||
|
||||
const liquidityMint = useMint(repayReserve.info.liquidityMint);
|
||||
const { balance: tokenBalance } = useUserBalance(
|
||||
repayReserve.info.liquidityMint
|
||||
);
|
||||
|
||||
const borrowAmountLamports = wadToLamports(
|
||||
obligation.info.borrowAmountWad
|
||||
|
@ -53,14 +56,15 @@ export const RepayInput = (props: {
|
|||
|
||||
const convert = useCallback(
|
||||
(val: string | number) => {
|
||||
const minAmount = Math.min(tokenBalance || Infinity, borrowAmount);
|
||||
setLastTyped("repay");
|
||||
if (typeof val === "string") {
|
||||
return (parseFloat(val) / borrowAmount) * 100;
|
||||
return (parseFloat(val) / minAmount) * 100;
|
||||
} else {
|
||||
return (val * borrowAmount) / 100;
|
||||
return (val * minAmount) / 100;
|
||||
}
|
||||
},
|
||||
[borrowAmount]
|
||||
[borrowAmount, tokenBalance]
|
||||
);
|
||||
|
||||
const { value, setValue, pct, setPct, type } = useSliderInput(convert);
|
||||
|
@ -211,7 +215,7 @@ export const RepayInput = (props: {
|
|||
setLastTyped("repay");
|
||||
}}
|
||||
disabled={true}
|
||||
hideBalance={true}
|
||||
useWalletBalance={true}
|
||||
/>
|
||||
</div>
|
||||
<Slider marks={marks} value={pct} onChange={setPct} />
|
||||
|
@ -221,7 +225,7 @@ export const RepayInput = (props: {
|
|||
flexDirection: "row",
|
||||
justifyContent: "space-evenly",
|
||||
alignItems: "center",
|
||||
marginBottom: 20
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<CollateralInput
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -148,7 +148,7 @@ export const cache = {
|
|||
obj: AccountInfo<Buffer>,
|
||||
parser?: AccountParser
|
||||
) => {
|
||||
if(obj.data.length === 0) {
|
||||
if (obj.data.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ export function approve(
|
|||
)
|
||||
);
|
||||
|
||||
if(autoRevoke) {
|
||||
if (autoRevoke) {
|
||||
cleanupInstructions.push(
|
||||
Token.createRevokeInstruction(tokenProgram, account, owner, [])
|
||||
);
|
||||
|
|
|
@ -18,7 +18,11 @@ export const BorrowView = () => {
|
|||
<div></div>
|
||||
</div>
|
||||
{reserveAccounts.map((account) => (
|
||||
<BorrowItem key={account.pubkey.toBase58()} reserve={account.info} address={account.pubkey} />
|
||||
<BorrowItem
|
||||
key={account.pubkey.toBase58()}
|
||||
reserve={account.info}
|
||||
address={account.pubkey}
|
||||
/>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
@ -33,7 +33,10 @@ export const DashboardDeposits = () => {
|
|||
<div></div>
|
||||
</div>
|
||||
{userDeposits.map((deposit) => (
|
||||
<DepositItem key={deposit.account.pubkey.toBase58()} userDeposit={deposit} />
|
||||
<DepositItem
|
||||
key={deposit.account.pubkey.toBase58()}
|
||||
userDeposit={deposit}
|
||||
/>
|
||||
))}
|
||||
</Card>
|
||||
);
|
||||
|
|
|
@ -23,9 +23,7 @@ export const DashboardView = () => {
|
|||
/>
|
||||
{LABELS.DASHBOARD_INFO}
|
||||
</div>
|
||||
):
|
||||
userDeposits.length === 0 && userObligations.length === 0 ?
|
||||
(
|
||||
) : userDeposits.length === 0 && userObligations.length === 0 ? (
|
||||
<div className="dashboard-info">
|
||||
<img
|
||||
src="splash.svg"
|
||||
|
@ -34,17 +32,21 @@ export const DashboardView = () => {
|
|||
/>
|
||||
{LABELS.NO_LOANS_NO_DEPOSITS}
|
||||
</div>
|
||||
): (
|
||||
) : (
|
||||
<Row gutter={GUTTER}>
|
||||
<Col md={24} xl={12} span={24}>
|
||||
{userDeposits.length > 0 ?
|
||||
<DashboardDeposits /> :
|
||||
<Card>{LABELS.NO_DEPOSITS}</Card> }
|
||||
{userDeposits.length > 0 ? (
|
||||
<DashboardDeposits />
|
||||
) : (
|
||||
<Card>{LABELS.NO_DEPOSITS}</Card>
|
||||
)}
|
||||
</Col>
|
||||
<Col md={24} xl={12} span={24}>
|
||||
{userObligations.length > 0 ?
|
||||
<DashboardObligations /> :
|
||||
<Card>{LABELS.NO_LOANS}</Card> }
|
||||
{userObligations.length > 0 ? (
|
||||
<DashboardObligations />
|
||||
) : (
|
||||
<Card>{LABELS.NO_LOANS}</Card>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
|
|
@ -35,9 +35,12 @@ export const DashboardObligations = () => {
|
|||
<div></div>
|
||||
</div>
|
||||
{userObligations.map((item) => {
|
||||
return <ObligationItem
|
||||
key={item.obligation.account.pubkey.toBase58()}
|
||||
obligation={item.obligation} />;
|
||||
return (
|
||||
<ObligationItem
|
||||
key={item.obligation.account.pubkey.toBase58()}
|
||||
obligation={item.obligation}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Card>
|
||||
);
|
||||
|
|
|
@ -17,7 +17,11 @@ export const DepositView = () => {
|
|||
<div></div>
|
||||
</div>
|
||||
{reserveAccounts.map((account) => (
|
||||
<ReserveItem key={account.pubkey.toBase58()} reserve={account.info} address={account.pubkey} />
|
||||
<ReserveItem
|
||||
key={account.pubkey.toBase58()}
|
||||
reserve={account.info}
|
||||
address={account.pubkey}
|
||||
/>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
@ -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 }>();
|
||||
|
|
Loading…
Reference in New Issue