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 signers
) )
: undefined; : undefined;
let amountLamports: number = 0; let amountLamports: number = 0;
let fromLamports: number = 0; let fromLamports: number = 0;
if (amountType === BorrowAmountType.LiquidityBorrowAmount) { if (amountType === BorrowAmountType.LiquidityBorrowAmount) {
@ -197,7 +195,6 @@ export const borrow = async (
instructions = []; instructions = [];
cleanupInstructions = [...finalCleanupInstructions]; cleanupInstructions = [...finalCleanupInstructions];
// create approval for transfer transactions // create approval for transfer transactions
const transferAuthority = approve( const transferAuthority = approve(
instructions, instructions,

View File

@ -195,6 +195,7 @@ export const BorrowInput = (props: {
onCollateralReserve={(key) => { onCollateralReserve={(key) => {
setCollateralReserveKey(key); setCollateralReserveKey(key);
}} }}
useFirstReserve={true}
/> />
</div> </div>
<ArrowDownOutlined /> <ArrowDownOutlined />
@ -204,7 +205,7 @@ export const BorrowInput = (props: {
flexDirection: "row", flexDirection: "row",
justifyContent: "space-evenly", justifyContent: "space-evenly",
alignItems: "center", alignItems: "center",
marginBottom: 20 marginBottom: 20,
}} }}
> >
<CollateralInput <CollateralInput

View File

@ -1,7 +1,11 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { cache, ParsedAccount } from "../../contexts/accounts"; import { cache, ParsedAccount } from "../../contexts/accounts";
import { useConnectionConfig } from "../../contexts/connection"; import { useConnectionConfig } from "../../contexts/connection";
import { useLendingReserves, useUserDeposits } from "../../hooks"; import {
useLendingReserves,
useUserBalance,
useUserDeposits,
} from "../../hooks";
import { import {
LendingReserve, LendingReserve,
LendingMarket, LendingMarket,
@ -27,9 +31,12 @@ export default function CollateralInput(props: {
onLeverage?: (leverage: number) => void; onLeverage?: (leverage: number) => void;
onInputChange: (value: number | null) => void; onInputChange: (value: number | null) => void;
hideBalance?: boolean; hideBalance?: boolean;
useWalletBalance?: boolean;
useFirstReserve?: boolean;
showLeverageSelector?: boolean; showLeverageSelector?: boolean;
leverage?: number; leverage?: number;
}) { }) {
const { balance: tokenBalance } = useUserBalance(props.reserve.liquidityMint);
const { reserveAccounts } = useLendingReserves(); const { reserveAccounts } = useLendingReserves();
const { tokenMap } = useConnectionConfig(); const { tokenMap } = useConnectionConfig();
const [collateralReserve, setCollateralReserve] = useState<string>(); const [collateralReserve, setCollateralReserve] = useState<string>();
@ -38,21 +45,26 @@ export default function CollateralInput(props: {
const userDeposits = useUserDeposits(); const userDeposits = useUserDeposits();
useEffect(() => { useEffect(() => {
const id: string = if (props.useWalletBalance) {
cache setBalance(tokenBalance);
.byParser(LendingReserveParser) } else {
.find((acc) => acc === collateralReserve) || ""; const id: string =
const parser = cache.get(id) as ParsedAccount<LendingReserve>; cache
if (parser) { .byParser(LendingReserveParser)
const collateralDeposit = userDeposits.userDeposits.find( .find((acc) => acc === collateralReserve) || "";
(u) => const parser = cache.get(id) as ParsedAccount<LendingReserve>;
u.reserve.info.liquidityMint.toBase58() ===
parser.info.liquidityMint.toBase58() if (parser) {
); const collateralDeposit = userDeposits.userDeposits.find(
if (collateralDeposit) setBalance(collateralDeposit.info.amount); (u) =>
else setBalance(0); 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< const market = cache.get(props.reserve.lendingMarket) as ParsedAccount<
LendingMarket LendingMarket
@ -63,26 +75,35 @@ export default function CollateralInput(props: {
market?.info?.quoteMint market?.info?.quoteMint
); );
const renderReserveAccounts = reserveAccounts const filteredReserveAccounts = reserveAccounts
.filter((reserve) => reserve.info !== props.reserve) .filter((reserve) => reserve.info !== props.reserve)
.filter( .filter(
(reserve) => (reserve) =>
!onlyQuoteAllowed || !onlyQuoteAllowed ||
reserve.info.liquidityMint.equals(market.info.quoteMint) reserve.info.liquidityMint.equals(market.info.quoteMint)
) );
.map((reserve) => {
const mint = reserve.info.liquidityMint.toBase58(); if (
const address = reserve.pubkey.toBase58(); !collateralReserve &&
const name = getTokenName(tokenMap, mint); props.useFirstReserve &&
return ( filteredReserveAccounts.length
<Option key={address} value={address} name={name} title={address}> ) {
<div key={address} style={{ display: "flex", alignItems: "center" }}> const address = filteredReserveAccounts[0].pubkey.toBase58();
<TokenIcon mintAddress={mint} /> setCollateralReserve(address);
{name} }
</div> const renderReserveAccounts = filteredReserveAccounts.map((reserve) => {
</Option> 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 ( return (
<Card <Card

View File

@ -1,35 +1,65 @@
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 +70,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 +108,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 +186,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

@ -70,7 +70,7 @@ export const LoanInfoLine = (props: {
<Card className={props.className}> <Card className={props.className}>
<Statistic <Statistic
title="Collateral" title="Collateral"
value={obligation.info.borrowedInQuote} value={obligation.info.collateralInQuote}
formatter={(val) => ( formatter={(val) => (
<div> <div>
<div> <div>

View File

@ -38,6 +38,9 @@ export const RepayInput = (props: {
const obligation = props.obligation; const obligation = props.obligation;
const liquidityMint = useMint(repayReserve.info.liquidityMint); const liquidityMint = useMint(repayReserve.info.liquidityMint);
const { balance: tokenBalance } = useUserBalance(
repayReserve.info.liquidityMint
);
const borrowAmountLamports = wadToLamports( const borrowAmountLamports = wadToLamports(
obligation.info.borrowAmountWad obligation.info.borrowAmountWad
@ -53,14 +56,15 @@ export const RepayInput = (props: {
const convert = useCallback( const convert = useCallback(
(val: string | number) => { (val: string | number) => {
const minAmount = Math.min(tokenBalance || Infinity, borrowAmount);
setLastTyped("repay"); setLastTyped("repay");
if (typeof val === "string") { if (typeof val === "string") {
return (parseFloat(val) / borrowAmount) * 100; return (parseFloat(val) / minAmount) * 100;
} else { } else {
return (val * borrowAmount) / 100; return (val * minAmount) / 100;
} }
}, },
[borrowAmount] [borrowAmount, tokenBalance]
); );
const { value, setValue, pct, setPct, type } = useSliderInput(convert); const { value, setValue, pct, setPct, type } = useSliderInput(convert);
@ -211,7 +215,7 @@ export const RepayInput = (props: {
setLastTyped("repay"); setLastTyped("repay");
}} }}
disabled={true} disabled={true}
hideBalance={true} useWalletBalance={true}
/> />
</div> </div>
<Slider marks={marks} value={pct} onChange={setPct} /> <Slider marks={marks} value={pct} onChange={setPct} />
@ -221,7 +225,7 @@ export const RepayInput = (props: {
flexDirection: "row", flexDirection: "row",
justifyContent: "space-evenly", justifyContent: "space-evenly",
alignItems: "center", alignItems: "center",
marginBottom: 20 marginBottom: 20,
}} }}
> >
<CollateralInput <CollateralInput

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

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

View File

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

View File

@ -18,7 +18,11 @@ export const BorrowView = () => {
<div></div> <div></div>
</div> </div>
{reserveAccounts.map((account) => ( {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> </Card>
</div> </div>

View File

@ -33,7 +33,10 @@ export const DashboardDeposits = () => {
<div></div> <div></div>
</div> </div>
{userDeposits.map((deposit) => ( {userDeposits.map((deposit) => (
<DepositItem key={deposit.account.pubkey.toBase58()} userDeposit={deposit} /> <DepositItem
key={deposit.account.pubkey.toBase58()}
userDeposit={deposit}
/>
))} ))}
</Card> </Card>
); );

View File

@ -23,9 +23,7 @@ export const DashboardView = () => {
/> />
{LABELS.DASHBOARD_INFO} {LABELS.DASHBOARD_INFO}
</div> </div>
): ) : userDeposits.length === 0 && userObligations.length === 0 ? (
userDeposits.length === 0 && userObligations.length === 0 ?
(
<div className="dashboard-info"> <div className="dashboard-info">
<img <img
src="splash.svg" src="splash.svg"
@ -34,17 +32,21 @@ export const DashboardView = () => {
/> />
{LABELS.NO_LOANS_NO_DEPOSITS} {LABELS.NO_LOANS_NO_DEPOSITS}
</div> </div>
): ( ) : (
<Row gutter={GUTTER}> <Row gutter={GUTTER}>
<Col md={24} xl={12} span={24}> <Col md={24} xl={12} span={24}>
{userDeposits.length > 0 ? {userDeposits.length > 0 ? (
<DashboardDeposits /> : <DashboardDeposits />
<Card>{LABELS.NO_DEPOSITS}</Card> } ) : (
<Card>{LABELS.NO_DEPOSITS}</Card>
)}
</Col> </Col>
<Col md={24} xl={12} span={24}> <Col md={24} xl={12} span={24}>
{userObligations.length > 0 ? {userObligations.length > 0 ? (
<DashboardObligations /> : <DashboardObligations />
<Card>{LABELS.NO_LOANS}</Card> } ) : (
<Card>{LABELS.NO_LOANS}</Card>
)}
</Col> </Col>
</Row> </Row>
)} )}

View File

@ -35,9 +35,12 @@ export const DashboardObligations = () => {
<div></div> <div></div>
</div> </div>
{userObligations.map((item) => { {userObligations.map((item) => {
return <ObligationItem return (
key={item.obligation.account.pubkey.toBase58()} <ObligationItem
obligation={item.obligation} />; key={item.obligation.account.pubkey.toBase58()}
obligation={item.obligation}
/>
);
})} })}
</Card> </Card>
); );

View File

@ -17,7 +17,11 @@ export const DepositView = () => {
<div></div> <div></div>
</div> </div>
{reserveAccounts.map((account) => ( {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> </Card>
</div> </div>

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