feat: hookup liquidate

This commit is contained in:
bartosz-lipinski 2020-12-19 21:33:49 -06:00
parent 552b5c4527
commit f24645810a
17 changed files with 2753 additions and 146 deletions

2222
public/splash.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 216 KiB

View File

@ -17,7 +17,10 @@ body {
.footer { .footer {
background-color: black; background-color: black;
color: lightgray; color: lightgray;
padding: 20px; padding: 10px 10px;
max-height: 60px;
overflow: hidden;
text-overflow: ellipsis;
} }
.action-spinner { .action-spinner {
@ -156,7 +159,6 @@ body {
} }
.wallet-wrapper { .wallet-wrapper {
background: @background-color-base;
padding-left: 0.7rem; padding-left: 0.7rem;
border-radius: 0.5rem; border-radius: 0.5rem;
display: flex; display: flex;
@ -165,7 +167,6 @@ body {
} }
.wallet-key { .wallet-key {
background: @background-color-base;
padding: 0.1rem 0.5rem 0.1rem 0.7rem; padding: 0.1rem 0.5rem 0.1rem 0.7rem;
margin-left: 0.3rem; margin-left: 0.3rem;
border-radius: 0.5rem; border-radius: 0.5rem;
@ -268,6 +269,27 @@ body {
padding: 8px 8px; padding: 8px 8px;
} }
.ant-pro-global-header {
.ant-pro-global-header-logo a h1 {
color: white !important;
}
background-color: black !important;
color: white !important;
.ant-btn {
color: white !important;
}
}
.ant-statistic {
user-select: none;
}
.ant-statistic-content {
font-weight: bold;
}
.token-input { .token-input {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -61,31 +61,31 @@ export const borrow = async (
const obligation = existingObligation const obligation = existingObligation
? existingObligation.pubkey ? existingObligation.pubkey
: createUninitializedObligation( : createUninitializedObligation(
instructions, instructions,
wallet.publicKey, wallet.publicKey,
await connection.getMinimumBalanceForRentExemption( await connection.getMinimumBalanceForRentExemption(
LendingObligationLayout.span LendingObligationLayout.span
), ),
signers signers
); );
const obligationMint = existingObligation const obligationMint = existingObligation
? existingObligation.info.tokenMint ? existingObligation.info.tokenMint
: createUninitializedMint( : createUninitializedMint(
instructions, instructions,
wallet.publicKey, wallet.publicKey,
await connection.getMinimumBalanceForRentExemption(MintLayout.span), await connection.getMinimumBalanceForRentExemption(MintLayout.span),
signers signers
); );
const obligationTokenOutput = obligationAccount const obligationTokenOutput = obligationAccount
? obligationAccount ? obligationAccount
: createUninitializedAccount( : createUninitializedAccount(
instructions, instructions,
wallet.publicKey, wallet.publicKey,
accountRentExempt, accountRentExempt,
signers signers
); );
let toAccount = await findOrCreateAccountByMint( let toAccount = await findOrCreateAccountByMint(
wallet.publicKey, wallet.publicKey,
@ -172,10 +172,6 @@ export const borrow = async (
) )
); );
const market = cache.get(depositReserve.info.lendingMarket) as ParsedAccount<
LendingMarket
>;
const dexMarketAddress = borrowReserve.info.dexMarketOption const dexMarketAddress = borrowReserve.info.dexMarketOption
? borrowReserve.info.dexMarket ? borrowReserve.info.dexMarket
: depositReserve.info.dexMarket; : depositReserve.info.dexMarket;
@ -185,6 +181,9 @@ export const borrow = async (
throw new Error(`Dex market doesn't exist.`); throw new Error(`Dex market doesn't exist.`);
} }
const market = cache.get(depositReserve.info.lendingMarket) as ParsedAccount<
LendingMarket
>;
const dexOrderBookSide = market.info.quoteMint.equals( const dexOrderBookSide = market.info.quoteMint.equals(
depositReserve.info.liquidityMint depositReserve.info.liquidityMint
) )

View File

@ -2,4 +2,5 @@ export { borrow } from "./borrow";
export { deposit } from "./deposit"; export { deposit } from "./deposit";
export { repay } from "./repay"; export { repay } from "./repay";
export { withdraw } from "./withdraw"; export { withdraw } from "./withdraw";
export { liquidate } from "./liquidate";
export * from "./account"; export * from "./account";

141
src/actions/liquidate.tsx Normal file
View File

@ -0,0 +1,141 @@
import {
Account,
Connection,
PublicKey,
TransactionInstruction,
} from "@solana/web3.js";
import { sendTransaction } from "../contexts/connection";
import { notify } from "../utils/notifications";
import { LendingReserve } from "./../models/lending/reserve";
import { liquidateInstruction } from "./../models/lending/liquidate";
import { AccountLayout, Token } from "@solana/spl-token";
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../constants/ids";
import { createTempMemoryAccount, ensureSplAccount, findOrCreateAccountByMint } from "./account";
import { LendingMarket, LendingObligation, TokenAccount } from "../models";
import { cache, ParsedAccount } from "../contexts/accounts";
export const liquidate = async (
connection: Connection,
wallet: any,
from: TokenAccount, // liquidity account
amountLamports: number, // in liquidty token (lamports)
// which loan to repay
obligation: ParsedAccount<LendingObligation>,
repayReserve: ParsedAccount<LendingReserve>,
withdrawReserve: ParsedAccount<LendingReserve>,
) => {
notify({
message: "Repaing funds...",
description: "Please review transactions to approve.",
type: "warn",
});
// user from account
const signers: Account[] = [];
const instructions: TransactionInstruction[] = [];
const cleanupInstructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(
AccountLayout.span
);
const [authority] = await PublicKey.findProgramAddress(
[repayReserve.info.lendingMarket.toBuffer()],
LENDING_PROGRAM_ID
);
const fromAccount = ensureSplAccount(
instructions,
cleanupInstructions,
from,
wallet.publicKey,
amountLamports + accountRentExempt,
signers
);
// create approval for transfer transactions
instructions.push(
Token.createApproveInstruction(
TOKEN_PROGRAM_ID,
fromAccount,
authority,
wallet.publicKey,
[],
amountLamports
)
);
// get destination account
const toAccount = await findOrCreateAccountByMint(
wallet.publicKey,
wallet.publicKey,
instructions,
cleanupInstructions,
accountRentExempt,
withdrawReserve.info.collateralMint,
signers
);
const dexMarketAddress = repayReserve.info.dexMarketOption
? repayReserve.info.dexMarket
: withdrawReserve.info.dexMarket;
const dexMarket = cache.get(dexMarketAddress);
if (!dexMarket) {
throw new Error(`Dex market doesn't exist.`);
}
const market = cache.get(withdrawReserve.info.lendingMarket) as ParsedAccount<
LendingMarket
>;
const dexOrderBookSide = market.info.quoteMint.equals(
repayReserve.info.liquidityMint
)
? dexMarket?.info.bids
: dexMarket?.info.asks;
console.log(dexMarketAddress.toBase58())
const memory = createTempMemoryAccount(
instructions,
wallet.publicKey,
signers
);
instructions.push(
liquidateInstruction(
amountLamports,
fromAccount,
toAccount,
repayReserve.pubkey,
repayReserve.info.liquiditySupply,
withdrawReserve.pubkey,
withdrawReserve.info.collateralSupply,
obligation.pubkey,
authority,
dexMarketAddress,
dexOrderBookSide,
memory
)
);
let tx = await sendTransaction(
connection,
wallet,
instructions.concat(cleanupInstructions),
signers,
true
);
notify({
message: "Funds liquidated.",
type: "success",
description: `Transaction - ${tx}`,
});
};

View File

@ -0,0 +1,40 @@
import { Statistic } from "antd";
import React, { } from "react";
export const BarChartStatistic = <T, >(props: {
items: T[];
title: string;
name: (item: T) => string;
getPct: (item: T) => number;
}) => {
const colors = [
'#003f5c',
'#2f4b7c',
'#665191',
'#a05195',
'#d45087',
'#f95d6a',
'#ff7c43',
'#ffa600',
].reverse();
return (
<Statistic
title={props.title}
valueRender={() =>
<div style={{ width: '100%', height: 40, display: 'flex', backgroundColor: 'lightgrey',
fontSize: 12,
lineHeight: '40px', }}>
{props.items.map((item, i) =>
<div key={props.name(item)}
title={props.name(item)}
style={{
width: `${100 * props.getPct(item)}%` ,
backgroundColor: colors[i % props.items.length] }} >
{props.name(item)}
</div>)}
</div>}
>
</Statistic>
);
};

View File

@ -42,7 +42,7 @@ export const AppLayout = (props: any) => {
</div> </div>
<BasicLayout <BasicLayout
title={LABELS.APP_TITLE} title={LABELS.APP_TITLE}
footerRender={() => <div className="footer">{LABELS.FOOTER}</div>} footerRender={() => <div className="footer" title={LABELS.FOOTER}>{LABELS.FOOTER}</div>}
navTheme={theme} navTheme={theme}
headerTheme={theme} headerTheme={theme}
theme={theme} theme={theme}

View File

@ -4,25 +4,61 @@ import React, { useCallback } from "react";
import { useState } from "react"; import { useState } from "react";
import { LABELS } from "../../constants"; import { LABELS } from "../../constants";
import { ParsedAccount } from "../../contexts/accounts"; import { ParsedAccount } from "../../contexts/accounts";
import { EnrichedLendingObligation } from "../../hooks"; import { EnrichedLendingObligation, useUserBalance } from "../../hooks";
import { LendingReserve } from "../../models"; import { LendingReserve } from "../../models";
import { ActionConfirmation } from "../ActionConfirmation";
import { BackButton } from "../BackButton"; import { BackButton } from "../BackButton";
import { CollateralSelector } from "../CollateralSelector"; import { CollateralSelector } from "../CollateralSelector";
import { liquidate } from "../../actions";
import "./style.less"; import "./style.less";
import { useConnection } from "../../contexts/connection";
import { useWallet } from "../../contexts/wallet";
import { wadToLamports } from "../../utils/utils";
export const LiquidateInput = (props: { export const LiquidateInput = (props: {
className?: string; className?: string;
reserve: ParsedAccount<LendingReserve>; repayReserve: ParsedAccount<LendingReserve>;
collateralReserve?: ParsedAccount<LendingReserve>; withdrawReserve?: ParsedAccount<LendingReserve>;
obligation: EnrichedLendingObligation; obligation: EnrichedLendingObligation;
}) => { }) => {
const { reserve, collateralReserve } = props; const connection = useConnection();
const { wallet } = useWallet();
const { repayReserve, withdrawReserve, obligation } = props;
const [pendingTx, setPendingTx] = useState(false); const [pendingTx, setPendingTx] = useState(false);
const [showConfirmation, setShowConfirmation] = useState(false);
const { accounts: fromAccounts } = useUserBalance(
repayReserve?.info.liquidityMint
);
const onLiquidate = useCallback(() => { const onLiquidate = useCallback(() => {
if (!withdrawReserve) {
return;
}
setPendingTx(true); setPendingTx(true);
setPendingTx(false);
}, []); (async () => {
try {
await liquidate(
connection,
wallet,
fromAccounts[0],
// TODO: ensure user has available amount
wadToLamports(obligation.info.borrowAmountWad).toNumber(),
obligation.account,
repayReserve,
withdrawReserve,
);
setShowConfirmation(true);
} catch {
// TODO:
} finally {
setPendingTx(false);
}
})();
}, [withdrawReserve, fromAccounts, obligation, repayReserve, wallet, connection]);
const bodyStyle: React.CSSProperties = { const bodyStyle: React.CSSProperties = {
display: "flex", display: "flex",
@ -34,24 +70,30 @@ export const LiquidateInput = (props: {
return ( return (
<Card className={props.className} bodyStyle={bodyStyle}> <Card className={props.className} bodyStyle={bodyStyle}>
<div {showConfirmation ? (
style={{ <ActionConfirmation onClose={() => setShowConfirmation(false)} />
display: "flex", ) : (
flexDirection: "column", <div
justifyContent: "space-around", style={{
}} display: "flex",
> flexDirection: "column",
<div className="liquidate-input-title">{LABELS.SELECT_COLLATERAL}</div> justifyContent: "space-around",
<CollateralSelector }}
reserve={reserve.info} >
collateralReserve={collateralReserve?.pubkey.toBase58()} <div className="liquidate-input-title">{LABELS.SELECT_COLLATERAL}</div>
disabled={true} <CollateralSelector
/> reserve={repayReserve.info}
<Button type="primary" onClick={onLiquidate} loading={pendingTx}> collateralReserve={withdrawReserve?.pubkey.toBase58()}
{LABELS.LIQUIDATE_ACTION} disabled={true}
</Button> />
<BackButton /> <Button type="primary"
</div> onClick={onLiquidate}
disabled={fromAccounts.length === 0}
loading={pendingTx}>
{LABELS.LIQUIDATE_ACTION}
</Button>
<BackButton />
</div>)}
</Card> </Card>
); );
}; };

View File

@ -0,0 +1,22 @@
import React from "react";
import { LABELS } from "../../../constants";
import { useUserDeposits } from "./../../../hooks";
import { DepositItem } from "./item";
export const DashboardDeposits = () => {
const { userDeposits } = useUserDeposits();
return (<>
<span>{LABELS.DASHBOARD_TITLE_DEPOSITS}</span>
<div className="dashboard-item dashboard-header">
<div>{LABELS.TABLE_TITLE_ASSET}</div>
<div>{LABELS.TABLE_TITLE_DEPOSIT_BALANCE}</div>
<div>{LABELS.TABLE_TITLE_APY}</div>
<div>{LABELS.TABLE_TITLE_ACTION}</div>
</div>
{userDeposits.map((deposit) => (
<DepositItem reserve={deposit.reserve} account={deposit.account} />
))}
</>
);
};

View File

@ -0,0 +1,53 @@
import React, { useMemo } from "react";
import { useUserCollateralBalance, useTokenName } from "../../../hooks";
import { calculateDepositAPY, LendingReserve } from "../../../models/lending";
import { TokenIcon } from "../../../components/TokenIcon";
import { formatNumber, formatPct } from "../../../utils/utils";
import { Button, Card } from "antd";
import { Link } from "react-router-dom";
import { TokenAccount } from "../../../models";
import { ParsedAccount } from "../../../contexts/accounts";
import { LABELS } from "../../../constants";
export const DepositItem = (props: {
reserve: ParsedAccount<LendingReserve>;
account: TokenAccount;
}) => {
const mintAddress = props.reserve.info.liquidityMint;
const name = useTokenName(mintAddress);
const { balance: collateralBalance } = useUserCollateralBalance(
props.reserve.info,
props.account.pubkey
);
const depositAPY = useMemo(() => calculateDepositAPY(props.reserve.info), [
props.reserve,
]);
return (
<Card>
<div className="dashboard-item">
<span style={{ display: "flex" }}>
<TokenIcon mintAddress={mintAddress} />
{name}
</span>
<div>
{formatNumber.format(collateralBalance)} {name}
</div>
<div>{formatPct.format(depositAPY)}</div>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<Link to={`/deposit/${props.reserve.pubkey.toBase58()}`}>
<Button>
<span>{LABELS.DEPOSIT_ACTION}</span>
</Button>
</Link>
<Link to={`/withdraw/${props.reserve.pubkey.toBase58()}`}>
<Button>
<span>{LABELS.WITHDRAW_ACTION}</span>
</Button>
</Link>
</div>
</div>
</Card>
);
};

View File

@ -1,9 +1,10 @@
import { Col, Row } from "antd";
import React from "react"; import React from "react";
import { LABELS } from "../../constants"; import { LABELS } from "../../constants";
import { useWallet } from "../../contexts/wallet"; import { useWallet } from "../../contexts/wallet";
import { useUserDeposits, useUserObligations } from "./../../hooks"; import { useUserDeposits, useUserObligations } from "./../../hooks";
import { DepositItem } from "./depositItem"; import { DashboardObligations } from "./obligation";
import { ObligationItem } from "./obligationItem"; import { DashboardDeposits } from "./deposit";
import "./style.less"; import "./style.less";
export const DashboardView = () => { export const DashboardView = () => {
@ -13,44 +14,32 @@ export const DashboardView = () => {
return ( return (
<div className="dashboard-container"> <div className="dashboard-container">
{!connected && ( {!connected && (
<div className="dashboard-info">{LABELS.DASHBOARD_INFO}</div> <div className="dashboard-info">
<img src="splash.svg" alt="connect your wallet" className="dashboard-splash"/>
{LABELS.DASHBOARD_INFO}
</div>
)} )}
{connected && {connected &&
userDeposits.length === 0 && userDeposits.length === 0 &&
userObligations.length === 0 && ( userObligations.length === 0 && (
<div className="dashboard-info">{LABELS.NO_LOANS_NO_DEPOSITS}</div> <div className="dashboard-info">
<img src="splash.svg" alt="connect your wallet" className="dashboard-splash"/>
{LABELS.NO_LOANS_NO_DEPOSITS}
</div>
)} )}
{userDeposits.length > 0 && ( <Row gutter={[16, { xs: 8, sm: 16, md: 16, lg: 16 }]} >
<div className="dashboard-left"> {userDeposits.length >0 && (
<span>{LABELS.DASHBOARD_TITLE_DEPOSITS}</span> <Col md={24} xl={12} span={24}>
<div className="dashboard-item dashboard-header"> <DashboardDeposits />
<div>{LABELS.TABLE_TITLE_ASSET}</div> </Col>
<div>{LABELS.TABLE_TITLE_DEPOSIT_BALANCE}</div> )}
<div>{LABELS.TABLE_TITLE_APY}</div> {userObligations.length >0 && (
<div>{LABELS.TABLE_TITLE_ACTION}</div> <Col md={24} xl={12} span={24}>
</div> <DashboardObligations />
{userDeposits.map((deposit) => ( </Col>
<DepositItem reserve={deposit.reserve} account={deposit.account} /> )}
))} </Row>
</div>
)}
{userObligations.length > 0 && (
<div className="dashboard-right">
<span>{LABELS.DASHBOARD_TITLE_LOANS}</span>
<div className="dashboard-item dashboard-header">
<div>{LABELS.TABLE_TITLE_ASSET}</div>
<div>{LABELS.TABLE_TITLE_YOUR_LOAN_BALANCE}</div>
<div>{LABELS.TABLE_TITLE_COLLATERAL_BALANCE}</div>
<div>{LABELS.TABLE_TITLE_APY}</div>
<div>{LABELS.TABLE_TITLE_LTV}</div>
<div>{LABELS.TABLE_TITLE_ACTION}</div>
</div>
{userObligations.map((item) => {
return <ObligationItem obligation={item.obligation} />;
})}
</div>
)}
</div> </div>
); );
}; };

View File

@ -0,0 +1,25 @@
import React from "react";
import { LABELS } from "../../../constants";
import { useUserObligations } from "./../../../hooks";
import { ObligationItem } from "./item";
export const DashboardObligations = () => {
const { userObligations } = useUserObligations();
return (
<>
<span>{LABELS.DASHBOARD_TITLE_LOANS}</span>
<div className="dashboard-item dashboard-header">
<div>{LABELS.TABLE_TITLE_ASSET}</div>
<div>{LABELS.TABLE_TITLE_YOUR_LOAN_BALANCE}</div>
<div>{LABELS.TABLE_TITLE_COLLATERAL_BALANCE}</div>
<div>{LABELS.TABLE_TITLE_APY}</div>
<div>{LABELS.TABLE_TITLE_LTV}</div>
<div>{LABELS.TABLE_TITLE_ACTION}</div>
</div>
{userObligations.map((item) => {
return <ObligationItem obligation={item.obligation} />;
})}
</>
);
};

View File

@ -1,20 +1,20 @@
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { EnrichedLendingObligation, useTokenName } from "../../hooks"; import { EnrichedLendingObligation, useTokenName } from "../../../hooks";
import { import {
calculateBorrowAPY, calculateBorrowAPY,
collateralToLiquidity, collateralToLiquidity,
LendingReserve, LendingReserve,
} from "../../models/lending"; } from "../../../models/lending";
import { TokenIcon } from "../../components/TokenIcon"; import { TokenIcon } from "../../../components/TokenIcon";
import { import {
wadToLamports, wadToLamports,
formatNumber, formatNumber,
fromLamports, fromLamports,
formatPct, formatPct,
} from "../../utils/utils"; } from "../../../utils/utils";
import { Button, Card } from "antd"; import { Button, Card } from "antd";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { cache, ParsedAccount, useMint } from "../../contexts/accounts"; import { cache, ParsedAccount, useMint } from "../../../contexts/accounts";
export const ObligationItem = (props: { export const ObligationItem = (props: {
obligation: EnrichedLendingObligation; obligation: EnrichedLendingObligation;

View File

@ -41,25 +41,23 @@
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
flex: 1; flex: 1;
}
.dashboard-right { & > :first-child {
flex: 50%; flex: 1;
} }
.dashboard-left {
flex: 50%;
} }
.dashboard-info { .dashboard-info {
display: flex; display: flex;
flex-direction: column;
align-self: center; align-self: center;
justify-content: center; justify-content: center;
flex: 1; flex: 1;
}
@media (max-width: 700px) { .dashboard-splash {
.dashboard-right, .dashboard-left { max-width: 400px;
flex: 100%; width: 100%;
align-self: center;
} }
} }

View File

@ -3,23 +3,45 @@ import { Card, Col, Row, Statistic } from "antd";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { LABELS } from "../../constants"; import { LABELS } from "../../constants";
import { cache, ParsedAccount } from "../../contexts/accounts"; import { cache, ParsedAccount } from "../../contexts/accounts";
import { useConnectionConfig } from "../../contexts/connection";
import { useMarkets } from "../../contexts/market"; import { useMarkets } from "../../contexts/market";
import { useLendingReserves } from "../../hooks"; import { useLendingReserves } from "../../hooks";
import { reserveMarketCap } from "../../models"; import { reserveMarketCap } from "../../models";
import { fromLamports, wadToLamports } from "../../utils/utils"; import { fromLamports, getTokenName, wadToLamports } from "../../utils/utils";
import { LendingReserveItem } from "./item"; import { LendingReserveItem } from "./item";
import { BarChartStatistic } from "./../../components/BarChartStatistic";
import "./itemStyle.less"; import "./itemStyle.less";
interface Totals {
marketSize: number;
borrowed: number;
lentOutPct: number;
items: {
marketSize: number;
borrowed: number;
name: string;
}[];
}
export const HomeView = () => { export const HomeView = () => {
const { reserveAccounts } = useLendingReserves(); const { reserveAccounts } = useLendingReserves();
const [totalMarketSize, setTotalMarketSize] = useState(0);
const [totalBorrowed, setTotalBorrowed] = useState(0);
const { marketEmitter, midPriceInUSD } = useMarkets(); const { marketEmitter, midPriceInUSD } = useMarkets();
const { tokenMap } = useConnectionConfig();
const [totals, setTotals] = useState<Totals>({
marketSize: 0,
borrowed: 0,
lentOutPct: 0,
items: [],
})
useEffect(() => { useEffect(() => {
const refreshTotal = () => { const refreshTotal = () => {
let totalSize = 0; let newTotals: Totals = {
let borrowed = 0; marketSize: 0,
borrowed: 0,
lentOutPct: 0,
items: [],
};
reserveAccounts.forEach((item) => { reserveAccounts.forEach((item) => {
const marketCapLamports = reserveMarketCap(item.info); const marketCapLamports = reserveMarketCap(item.info);
@ -33,22 +55,28 @@ export const HomeView = () => {
return; return;
} }
const marketCap = let leaf = {
fromLamports(marketCapLamports, liquidityMint?.info) * marketSize: fromLamports(marketCapLamports, liquidityMint?.info) *
midPriceInUSD(liquidityMint?.pubkey.toBase58()); midPriceInUSD(liquidityMint?.pubkey.toBase58()),
borrowed: fromLamports(
totalSize = totalSize + marketCap;
borrowed =
borrowed +
fromLamports(
wadToLamports(item.info?.borrowedLiquidityWad).toNumber(), wadToLamports(item.info?.borrowedLiquidityWad).toNumber(),
liquidityMint.info liquidityMint.info
); ),
name: getTokenName(tokenMap, item.info.liquidityMint.toBase58())
}
newTotals.items.push(leaf);
newTotals.marketSize = newTotals.marketSize + leaf.marketSize;
newTotals.borrowed = newTotals.borrowed + leaf.borrowed;
}); });
setTotalMarketSize(totalSize); newTotals.lentOutPct = newTotals.borrowed / newTotals.marketSize;
setTotalBorrowed(borrowed); newTotals.lentOutPct = Number.isFinite(newTotals.lentOutPct) ? newTotals.lentOutPct : 0;
newTotals.items = newTotals.items.sort((a, b) => b.marketSize - a.marketSize)
setTotals(newTotals);
}; };
const dispose = marketEmitter.onMarket(() => { const dispose = marketEmitter.onMarket(() => {
@ -60,32 +88,53 @@ export const HomeView = () => {
return () => { return () => {
dispose(); dispose();
}; };
}, [marketEmitter, midPriceInUSD, setTotalMarketSize, reserveAccounts]); }, [marketEmitter, midPriceInUSD, setTotals, reserveAccounts, tokenMap]);
return ( return (
<div className="flexColumn"> <div className="flexColumn">
<Row gutter={16} className="home-info-row"> <Row
<Col span={12}> gutter={[16, { xs: 8, sm: 16, md: 16, lg: 16 }]}
className="home-info-row" >
<Col xs={24} xl={5}>
<Card> <Card>
<Statistic <Statistic
title="Current market size" title="Current market size"
value={totalMarketSize} value={totals.marketSize}
precision={2} precision={2}
valueStyle={{ color: "#3f8600" }} valueStyle={{ color: "#3f8600" }}
prefix="$" prefix="$"
/> />
</Card> </Card>
</Col> </Col>
<Col span={12}> <Col xs={24} xl={5}>
<Card> <Card>
<Statistic <Statistic
title="Total borrowed" title="Total borrowed"
value={totalBorrowed} value={totals.borrowed}
precision={2} precision={2}
prefix="$" prefix="$"
/> />
</Card> </Card>
</Col> </Col>
<Col xs={24} xl={5}>
<Card>
<Statistic
title="% Lent out"
value={totals.lentOutPct * 100}
precision={2}
suffix="%"
/>
</Card>
</Col>
<Col xs={24} xl={9}>
<Card>
<BarChartStatistic
title="Market composition"
name={(item) => item.name}
getPct={(item) => item.marketSize / totals.marketSize}
items={totals.items} />
</Card>
</Col>
</Row> </Row>
<div className="home-item home-header"> <div className="home-item home-header">

View File

@ -14,12 +14,12 @@ export const LiquidateReserveView = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const obligation = useEnrichedLendingObligation(id); const obligation = useEnrichedLendingObligation(id);
const reserve = useLendingReserve(obligation?.info.borrowReserve); const repayReserve = useLendingReserve(obligation?.info.borrowReserve);
const collateralReserve = useLendingReserve( const withdrawReserve = useLendingReserve(
obligation?.info.collateralReserve obligation?.info.collateralReserve
); );
if (!obligation || !reserve) { if (!obligation || !repayReserve) {
return null; return null;
} }
@ -29,12 +29,12 @@ export const LiquidateReserveView = () => {
<LiquidateInput <LiquidateInput
className="liquidate-reserve-item liquidate-reserve-item-left" className="liquidate-reserve-item liquidate-reserve-item-left"
obligation={obligation} obligation={obligation}
collateralReserve={collateralReserve} withdrawReserve={withdrawReserve}
reserve={reserve} repayReserve={repayReserve}
/> />
<SideReserveOverview <SideReserveOverview
className="liquidate-reserve-item liquidate-reserve-item-right" className="liquidate-reserve-item liquidate-reserve-item-right"
reserve={reserve} reserve={repayReserve}
mode={SideReserveOverviewMode.Deposit} mode={SideReserveOverviewMode.Deposit}
/> />
</div> </div>

View File

@ -42,7 +42,7 @@
resolved "https://registry.yarnpkg.com/@ant-design/icons-svg/-/icons-svg-4.1.0.tgz#480b025f4b20ef7fe8f47d4a4846e4fee84ea06c" resolved "https://registry.yarnpkg.com/@ant-design/icons-svg/-/icons-svg-4.1.0.tgz#480b025f4b20ef7fe8f47d4a4846e4fee84ea06c"
integrity sha512-Fi03PfuUqRs76aI3UWYpP864lkrfPo0hluwGqh7NJdLhvH4iRDc3jbJqZIvRDLHKbXrvAfPPV3+zjUccfFvWOQ== integrity sha512-Fi03PfuUqRs76aI3UWYpP864lkrfPo0hluwGqh7NJdLhvH4iRDc3jbJqZIvRDLHKbXrvAfPPV3+zjUccfFvWOQ==
"@ant-design/icons@^4.0.0": "@ant-design/icons@^4.0.0", "@ant-design/icons@^4.3.0":
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-4.3.0.tgz#420e0cd527486c0fe57f81310d681950fc4cfacf" resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-4.3.0.tgz#420e0cd527486c0fe57f81310d681950fc4cfacf"
integrity sha512-UoIbw4oz/L/msbkgqs2nls2KP7XNKScOxVR54wRrWwnXOzJaGNwwSdYjHQz+5ETf8C53YPpzMOnRX99LFCdeIQ== integrity sha512-UoIbw4oz/L/msbkgqs2nls2KP7XNKScOxVR54wRrWwnXOzJaGNwwSdYjHQz+5ETf8C53YPpzMOnRX99LFCdeIQ==
@ -66,14 +66,14 @@
insert-css "^2.0.0" insert-css "^2.0.0"
rc-util "^5.0.1" rc-util "^5.0.1"
"@ant-design/pro-layout@^6.5.14": "@ant-design/pro-layout@^6.7.0":
version "6.5.15" version "6.7.0"
resolved "https://registry.yarnpkg.com/@ant-design/pro-layout/-/pro-layout-6.5.15.tgz#a5b8fb414ca51deebc8c4214b09699cfc30b60a7" resolved "https://registry.yarnpkg.com/@ant-design/pro-layout/-/pro-layout-6.7.0.tgz#eba8d51bdf588c9a7ee58837a134f0f2bd8c14b3"
integrity sha512-AN9UQGbdK/4RwF0Uf5HkwdXsCRRzC1FeQv8jBKlzcsHEm/FGa9KslIDcjDzOR7q2P9NiysaBsWIGPIM/aRox5A== integrity sha512-IGTtbsiTuKLwyHj4DEq6CfwFdh0YOnz/jGnikDQFMPmUtK/yeFa/rM2M4xmtVycPTzOZAx7A+UARw5VWLNy9cg==
dependencies: dependencies:
"@ant-design/icons" "^4.0.0" "@ant-design/icons" "^4.0.0"
"@ant-design/pro-provider" "1.2.1" "@ant-design/pro-provider" "1.2.4"
"@ant-design/pro-utils" "1.2.0" "@ant-design/pro-utils" "1.5.1"
"@umijs/route-utils" "^1.0.33" "@umijs/route-utils" "^1.0.33"
classnames "^2.2.6" classnames "^2.2.6"
history "^4.10.1" history "^4.10.1"
@ -82,6 +82,7 @@
path-to-regexp "2.4.0" path-to-regexp "2.4.0"
qs "^6.9.0" qs "^6.9.0"
rc-resize-observer "^0.2.1" rc-resize-observer "^0.2.1"
rc-util "^5.0.6"
react-copy-to-clipboard "^5.0.1" react-copy-to-clipboard "^5.0.1"
react-responsive "^8.0.1" react-responsive "^8.0.1"
react-router-dom "5.1.2" react-router-dom "5.1.2"
@ -90,20 +91,23 @@
use-media-antd-query "^1.0.3" use-media-antd-query "^1.0.3"
warning "^4.0.3" warning "^4.0.3"
"@ant-design/pro-provider@1.2.1": "@ant-design/pro-provider@1.2.4":
version "1.2.1" version "1.2.4"
resolved "https://registry.yarnpkg.com/@ant-design/pro-provider/-/pro-provider-1.2.1.tgz#1dadd1e196dac363e002390aeb84a1ce0ed67118" resolved "https://registry.yarnpkg.com/@ant-design/pro-provider/-/pro-provider-1.2.4.tgz#de1dc8fdf9c3e3156e0540a4d5db61c4e6c4e2c1"
integrity sha512-k2kT7zb1QhzJQvCuhfKXUwJJV6KsvyNtxbguDA3zoiK57EEAMRyxuiLMeNk6ejeY/Kak3l9yx2YiANuRRzU6Vg== integrity sha512-tYk97VmhctXY/BAv6K+qVRBddHxmWkSf3QbU6cRhmnb0ofPV26wEX8oWa26MBqlS4QPry0+L4VO8n4iLQ8HaUw==
dependencies: dependencies:
rc-util "^5.0.1" rc-util "^5.0.1"
"@ant-design/pro-utils@1.2.0": "@ant-design/pro-utils@1.5.1":
version "1.2.0" version "1.5.1"
resolved "https://registry.yarnpkg.com/@ant-design/pro-utils/-/pro-utils-1.2.0.tgz#2bf2157558095b18224bba4ba0a18081f03a1689" resolved "https://registry.yarnpkg.com/@ant-design/pro-utils/-/pro-utils-1.5.1.tgz#e3cc1ef352cd804d562fcf1049e8d490602b48f4"
integrity sha512-k5Mo1Ma8cTB2aMSXI6VA5YwNO4BxGmUIugUrQJVRcu3OupTle1bYJf7tePUpPyN++Oe5AbSHwXl+c0CtfF01hA== integrity sha512-dM2Q9ex8d28+RuaoFczkCY50KHG5666RqryzxA1IeMNVNTsU8iqflPl3v94+yiYonDTyHECIm4ZKqUlz2H4c/g==
dependencies: dependencies:
"@ant-design/pro-provider" "1.2.1" "@ant-design/icons" "^4.3.0"
"@ant-design/pro-provider" "1.2.4"
classnames "^2.2.6"
fast-deep-equal "^3.1.3" fast-deep-equal "^3.1.3"
moment "^2.27.0"
rc-util "^5.0.6" rc-util "^5.0.6"
"@ant-design/react-slick@~0.27.0": "@ant-design/react-slick@~0.27.0":
@ -8042,7 +8046,7 @@ mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
dependencies: dependencies:
minimist "^1.2.5" minimist "^1.2.5"
moment@^2.24.0, moment@^2.25.3: moment@^2.24.0, moment@^2.25.3, moment@^2.27.0:
version "2.29.1" version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==