Merge pull request #53 from yamijuan/repay-partially

Allow users to liquidate partially
This commit is contained in:
Bartosz Lipinski 2021-01-27 16:46:12 -06:00 committed by GitHub
commit c5db4c6e6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 256 additions and 83 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@ -148,7 +148,7 @@ export const cache = {
obj: AccountInfo<Buffer>,
parser?: AccountParser
) => {
if(obj.data.length === 0) {
if (obj.data.length === 0) {
return;
}

View File

@ -39,7 +39,7 @@ export function approve(
)
);
if(autoRevoke) {
if (autoRevoke) {
cleanupInstructions.push(
Token.createRevokeInstruction(tokenProgram, account, owner, [])
);

View File

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

View File

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

View File

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

View File

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

View File

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

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