mirror of https://github.com/certusone/oyster.git
feat: hookup liquidate
This commit is contained in:
parent
552b5c4527
commit
f24645810a
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 216 KiB |
28
src/App.less
28
src/App.less
|
@ -17,7 +17,10 @@ body {
|
|||
.footer {
|
||||
background-color: black;
|
||||
color: lightgray;
|
||||
padding: 20px;
|
||||
padding: 10px 10px;
|
||||
max-height: 60px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.action-spinner {
|
||||
|
@ -156,7 +159,6 @@ body {
|
|||
}
|
||||
|
||||
.wallet-wrapper {
|
||||
background: @background-color-base;
|
||||
padding-left: 0.7rem;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
|
@ -165,7 +167,6 @@ body {
|
|||
}
|
||||
|
||||
.wallet-key {
|
||||
background: @background-color-base;
|
||||
padding: 0.1rem 0.5rem 0.1rem 0.7rem;
|
||||
margin-left: 0.3rem;
|
||||
border-radius: 0.5rem;
|
||||
|
@ -268,6 +269,27 @@ body {
|
|||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -172,10 +172,6 @@ export const borrow = async (
|
|||
)
|
||||
);
|
||||
|
||||
const market = cache.get(depositReserve.info.lendingMarket) as ParsedAccount<
|
||||
LendingMarket
|
||||
>;
|
||||
|
||||
const dexMarketAddress = borrowReserve.info.dexMarketOption
|
||||
? borrowReserve.info.dexMarket
|
||||
: depositReserve.info.dexMarket;
|
||||
|
@ -185,6 +181,9 @@ export const borrow = async (
|
|||
throw new Error(`Dex market doesn't exist.`);
|
||||
}
|
||||
|
||||
const market = cache.get(depositReserve.info.lendingMarket) as ParsedAccount<
|
||||
LendingMarket
|
||||
>;
|
||||
const dexOrderBookSide = market.info.quoteMint.equals(
|
||||
depositReserve.info.liquidityMint
|
||||
)
|
||||
|
|
|
@ -2,4 +2,5 @@ export { borrow } from "./borrow";
|
|||
export { deposit } from "./deposit";
|
||||
export { repay } from "./repay";
|
||||
export { withdraw } from "./withdraw";
|
||||
export { liquidate } from "./liquidate";
|
||||
export * from "./account";
|
||||
|
|
|
@ -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}`,
|
||||
});
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -42,7 +42,7 @@ export const AppLayout = (props: any) => {
|
|||
</div>
|
||||
<BasicLayout
|
||||
title={LABELS.APP_TITLE}
|
||||
footerRender={() => <div className="footer">{LABELS.FOOTER}</div>}
|
||||
footerRender={() => <div className="footer" title={LABELS.FOOTER}>{LABELS.FOOTER}</div>}
|
||||
navTheme={theme}
|
||||
headerTheme={theme}
|
||||
theme={theme}
|
||||
|
|
|
@ -4,25 +4,61 @@ import React, { useCallback } from "react";
|
|||
import { useState } from "react";
|
||||
import { LABELS } from "../../constants";
|
||||
import { ParsedAccount } from "../../contexts/accounts";
|
||||
import { EnrichedLendingObligation } from "../../hooks";
|
||||
import { EnrichedLendingObligation, 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";
|
||||
|
||||
export const LiquidateInput = (props: {
|
||||
className?: string;
|
||||
reserve: ParsedAccount<LendingReserve>;
|
||||
collateralReserve?: ParsedAccount<LendingReserve>;
|
||||
repayReserve: ParsedAccount<LendingReserve>;
|
||||
withdrawReserve?: ParsedAccount<LendingReserve>;
|
||||
obligation: EnrichedLendingObligation;
|
||||
}) => {
|
||||
const { reserve, collateralReserve } = props;
|
||||
const connection = useConnection();
|
||||
const { wallet } = useWallet();
|
||||
const { repayReserve, withdrawReserve, obligation } = props;
|
||||
const [pendingTx, setPendingTx] = useState(false);
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
|
||||
const { accounts: fromAccounts } = useUserBalance(
|
||||
repayReserve?.info.liquidityMint
|
||||
);
|
||||
|
||||
const onLiquidate = useCallback(() => {
|
||||
if (!withdrawReserve) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPendingTx(true);
|
||||
|
||||
(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 = {
|
||||
display: "flex",
|
||||
|
@ -34,6 +70,9 @@ export const LiquidateInput = (props: {
|
|||
|
||||
return (
|
||||
<Card className={props.className} bodyStyle={bodyStyle}>
|
||||
{showConfirmation ? (
|
||||
<ActionConfirmation onClose={() => setShowConfirmation(false)} />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
|
@ -43,15 +82,18 @@ export const LiquidateInput = (props: {
|
|||
>
|
||||
<div className="liquidate-input-title">{LABELS.SELECT_COLLATERAL}</div>
|
||||
<CollateralSelector
|
||||
reserve={reserve.info}
|
||||
collateralReserve={collateralReserve?.pubkey.toBase58()}
|
||||
reserve={repayReserve.info}
|
||||
collateralReserve={withdrawReserve?.pubkey.toBase58()}
|
||||
disabled={true}
|
||||
/>
|
||||
<Button type="primary" onClick={onLiquidate} loading={pendingTx}>
|
||||
<Button type="primary"
|
||||
onClick={onLiquidate}
|
||||
disabled={fromAccounts.length === 0}
|
||||
loading={pendingTx}>
|
||||
{LABELS.LIQUIDATE_ACTION}
|
||||
</Button>
|
||||
<BackButton />
|
||||
</div>
|
||||
</div>)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -1,9 +1,10 @@
|
|||
import { Col, Row } from "antd";
|
||||
import React from "react";
|
||||
import { LABELS } from "../../constants";
|
||||
import { useWallet } from "../../contexts/wallet";
|
||||
import { useUserDeposits, useUserObligations } from "./../../hooks";
|
||||
import { DepositItem } from "./depositItem";
|
||||
import { ObligationItem } from "./obligationItem";
|
||||
import { DashboardObligations } from "./obligation";
|
||||
import { DashboardDeposits } from "./deposit";
|
||||
import "./style.less";
|
||||
|
||||
export const DashboardView = () => {
|
||||
|
@ -14,43 +15,31 @@ export const DashboardView = () => {
|
|||
return (
|
||||
<div className="dashboard-container">
|
||||
{!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 &&
|
||||
userDeposits.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>
|
||||
)}
|
||||
<Row gutter={[16, { xs: 8, sm: 16, md: 16, lg: 16 }]} >
|
||||
{userDeposits.length >0 && (
|
||||
<div className="dashboard-left">
|
||||
<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} />
|
||||
))}
|
||||
</div>
|
||||
<Col md={24} xl={12} span={24}>
|
||||
<DashboardDeposits />
|
||||
</Col>
|
||||
)}
|
||||
{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>
|
||||
<Col md={24} xl={12} span={24}>
|
||||
<DashboardObligations />
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,20 +1,20 @@
|
|||
import React, { useMemo } from "react";
|
||||
import { EnrichedLendingObligation, useTokenName } from "../../hooks";
|
||||
import { EnrichedLendingObligation, useTokenName } from "../../../hooks";
|
||||
import {
|
||||
calculateBorrowAPY,
|
||||
collateralToLiquidity,
|
||||
LendingReserve,
|
||||
} from "../../models/lending";
|
||||
import { TokenIcon } from "../../components/TokenIcon";
|
||||
} from "../../../models/lending";
|
||||
import { TokenIcon } from "../../../components/TokenIcon";
|
||||
import {
|
||||
wadToLamports,
|
||||
formatNumber,
|
||||
fromLamports,
|
||||
formatPct,
|
||||
} from "../../utils/utils";
|
||||
} from "../../../utils/utils";
|
||||
import { Button, Card } from "antd";
|
||||
import { Link } from "react-router-dom";
|
||||
import { cache, ParsedAccount, useMint } from "../../contexts/accounts";
|
||||
import { cache, ParsedAccount, useMint } from "../../../contexts/accounts";
|
||||
|
||||
export const ObligationItem = (props: {
|
||||
obligation: EnrichedLendingObligation;
|
|
@ -41,25 +41,23 @@
|
|||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dashboard-right {
|
||||
flex: 50%;
|
||||
& > :first-child {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dashboard-left {
|
||||
flex: 50%;
|
||||
}
|
||||
|
||||
.dashboard-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
|
||||
.dashboard-splash {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.dashboard-right, .dashboard-left {
|
||||
flex: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,23 +3,45 @@ import { Card, Col, Row, Statistic } from "antd";
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { LABELS } from "../../constants";
|
||||
import { cache, ParsedAccount } from "../../contexts/accounts";
|
||||
import { useConnectionConfig } from "../../contexts/connection";
|
||||
import { useMarkets } from "../../contexts/market";
|
||||
import { useLendingReserves } from "../../hooks";
|
||||
import { reserveMarketCap } from "../../models";
|
||||
import { fromLamports, wadToLamports } from "../../utils/utils";
|
||||
import { fromLamports, getTokenName, wadToLamports } from "../../utils/utils";
|
||||
import { LendingReserveItem } from "./item";
|
||||
import { BarChartStatistic } from "./../../components/BarChartStatistic";
|
||||
import "./itemStyle.less";
|
||||
|
||||
interface Totals {
|
||||
marketSize: number;
|
||||
borrowed: number;
|
||||
lentOutPct: number;
|
||||
items: {
|
||||
marketSize: number;
|
||||
borrowed: number;
|
||||
name: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const HomeView = () => {
|
||||
const { reserveAccounts } = useLendingReserves();
|
||||
const [totalMarketSize, setTotalMarketSize] = useState(0);
|
||||
const [totalBorrowed, setTotalBorrowed] = useState(0);
|
||||
const { marketEmitter, midPriceInUSD } = useMarkets();
|
||||
const { tokenMap } = useConnectionConfig();
|
||||
const [totals, setTotals] = useState<Totals>({
|
||||
marketSize: 0,
|
||||
borrowed: 0,
|
||||
lentOutPct: 0,
|
||||
items: [],
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const refreshTotal = () => {
|
||||
let totalSize = 0;
|
||||
let borrowed = 0;
|
||||
let newTotals: Totals = {
|
||||
marketSize: 0,
|
||||
borrowed: 0,
|
||||
lentOutPct: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
reserveAccounts.forEach((item) => {
|
||||
const marketCapLamports = reserveMarketCap(item.info);
|
||||
|
@ -33,22 +55,28 @@ export const HomeView = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const marketCap =
|
||||
fromLamports(marketCapLamports, liquidityMint?.info) *
|
||||
midPriceInUSD(liquidityMint?.pubkey.toBase58());
|
||||
|
||||
totalSize = totalSize + marketCap;
|
||||
|
||||
borrowed =
|
||||
borrowed +
|
||||
fromLamports(
|
||||
let leaf = {
|
||||
marketSize: fromLamports(marketCapLamports, liquidityMint?.info) *
|
||||
midPriceInUSD(liquidityMint?.pubkey.toBase58()),
|
||||
borrowed: fromLamports(
|
||||
wadToLamports(item.info?.borrowedLiquidityWad).toNumber(),
|
||||
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);
|
||||
setTotalBorrowed(borrowed);
|
||||
newTotals.lentOutPct = newTotals.borrowed / newTotals.marketSize;
|
||||
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(() => {
|
||||
|
@ -60,32 +88,53 @@ export const HomeView = () => {
|
|||
return () => {
|
||||
dispose();
|
||||
};
|
||||
}, [marketEmitter, midPriceInUSD, setTotalMarketSize, reserveAccounts]);
|
||||
}, [marketEmitter, midPriceInUSD, setTotals, reserveAccounts, tokenMap]);
|
||||
|
||||
return (
|
||||
<div className="flexColumn">
|
||||
<Row gutter={16} className="home-info-row">
|
||||
<Col span={12}>
|
||||
<Row
|
||||
gutter={[16, { xs: 8, sm: 16, md: 16, lg: 16 }]}
|
||||
className="home-info-row" >
|
||||
<Col xs={24} xl={5}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Current market size"
|
||||
value={totalMarketSize}
|
||||
value={totals.marketSize}
|
||||
precision={2}
|
||||
valueStyle={{ color: "#3f8600" }}
|
||||
prefix="$"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Col xs={24} xl={5}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Total borrowed"
|
||||
value={totalBorrowed}
|
||||
value={totals.borrowed}
|
||||
precision={2}
|
||||
prefix="$"
|
||||
/>
|
||||
</Card>
|
||||
</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>
|
||||
|
||||
<div className="home-item home-header">
|
||||
|
|
|
@ -14,12 +14,12 @@ export const LiquidateReserveView = () => {
|
|||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const obligation = useEnrichedLendingObligation(id);
|
||||
const reserve = useLendingReserve(obligation?.info.borrowReserve);
|
||||
const collateralReserve = useLendingReserve(
|
||||
const repayReserve = useLendingReserve(obligation?.info.borrowReserve);
|
||||
const withdrawReserve = useLendingReserve(
|
||||
obligation?.info.collateralReserve
|
||||
);
|
||||
|
||||
if (!obligation || !reserve) {
|
||||
if (!obligation || !repayReserve) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -29,12 +29,12 @@ export const LiquidateReserveView = () => {
|
|||
<LiquidateInput
|
||||
className="liquidate-reserve-item liquidate-reserve-item-left"
|
||||
obligation={obligation}
|
||||
collateralReserve={collateralReserve}
|
||||
reserve={reserve}
|
||||
withdrawReserve={withdrawReserve}
|
||||
repayReserve={repayReserve}
|
||||
/>
|
||||
<SideReserveOverview
|
||||
className="liquidate-reserve-item liquidate-reserve-item-right"
|
||||
reserve={reserve}
|
||||
reserve={repayReserve}
|
||||
mode={SideReserveOverviewMode.Deposit}
|
||||
/>
|
||||
</div>
|
||||
|
|
38
yarn.lock
38
yarn.lock
|
@ -42,7 +42,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@ant-design/icons-svg/-/icons-svg-4.1.0.tgz#480b025f4b20ef7fe8f47d4a4846e4fee84ea06c"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-4.3.0.tgz#420e0cd527486c0fe57f81310d681950fc4cfacf"
|
||||
integrity sha512-UoIbw4oz/L/msbkgqs2nls2KP7XNKScOxVR54wRrWwnXOzJaGNwwSdYjHQz+5ETf8C53YPpzMOnRX99LFCdeIQ==
|
||||
|
@ -66,14 +66,14 @@
|
|||
insert-css "^2.0.0"
|
||||
rc-util "^5.0.1"
|
||||
|
||||
"@ant-design/pro-layout@^6.5.14":
|
||||
version "6.5.15"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/pro-layout/-/pro-layout-6.5.15.tgz#a5b8fb414ca51deebc8c4214b09699cfc30b60a7"
|
||||
integrity sha512-AN9UQGbdK/4RwF0Uf5HkwdXsCRRzC1FeQv8jBKlzcsHEm/FGa9KslIDcjDzOR7q2P9NiysaBsWIGPIM/aRox5A==
|
||||
"@ant-design/pro-layout@^6.7.0":
|
||||
version "6.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/pro-layout/-/pro-layout-6.7.0.tgz#eba8d51bdf588c9a7ee58837a134f0f2bd8c14b3"
|
||||
integrity sha512-IGTtbsiTuKLwyHj4DEq6CfwFdh0YOnz/jGnikDQFMPmUtK/yeFa/rM2M4xmtVycPTzOZAx7A+UARw5VWLNy9cg==
|
||||
dependencies:
|
||||
"@ant-design/icons" "^4.0.0"
|
||||
"@ant-design/pro-provider" "1.2.1"
|
||||
"@ant-design/pro-utils" "1.2.0"
|
||||
"@ant-design/pro-provider" "1.2.4"
|
||||
"@ant-design/pro-utils" "1.5.1"
|
||||
"@umijs/route-utils" "^1.0.33"
|
||||
classnames "^2.2.6"
|
||||
history "^4.10.1"
|
||||
|
@ -82,6 +82,7 @@
|
|||
path-to-regexp "2.4.0"
|
||||
qs "^6.9.0"
|
||||
rc-resize-observer "^0.2.1"
|
||||
rc-util "^5.0.6"
|
||||
react-copy-to-clipboard "^5.0.1"
|
||||
react-responsive "^8.0.1"
|
||||
react-router-dom "5.1.2"
|
||||
|
@ -90,20 +91,23 @@
|
|||
use-media-antd-query "^1.0.3"
|
||||
warning "^4.0.3"
|
||||
|
||||
"@ant-design/pro-provider@1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/pro-provider/-/pro-provider-1.2.1.tgz#1dadd1e196dac363e002390aeb84a1ce0ed67118"
|
||||
integrity sha512-k2kT7zb1QhzJQvCuhfKXUwJJV6KsvyNtxbguDA3zoiK57EEAMRyxuiLMeNk6ejeY/Kak3l9yx2YiANuRRzU6Vg==
|
||||
"@ant-design/pro-provider@1.2.4":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/pro-provider/-/pro-provider-1.2.4.tgz#de1dc8fdf9c3e3156e0540a4d5db61c4e6c4e2c1"
|
||||
integrity sha512-tYk97VmhctXY/BAv6K+qVRBddHxmWkSf3QbU6cRhmnb0ofPV26wEX8oWa26MBqlS4QPry0+L4VO8n4iLQ8HaUw==
|
||||
dependencies:
|
||||
rc-util "^5.0.1"
|
||||
|
||||
"@ant-design/pro-utils@1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/pro-utils/-/pro-utils-1.2.0.tgz#2bf2157558095b18224bba4ba0a18081f03a1689"
|
||||
integrity sha512-k5Mo1Ma8cTB2aMSXI6VA5YwNO4BxGmUIugUrQJVRcu3OupTle1bYJf7tePUpPyN++Oe5AbSHwXl+c0CtfF01hA==
|
||||
"@ant-design/pro-utils@1.5.1":
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/pro-utils/-/pro-utils-1.5.1.tgz#e3cc1ef352cd804d562fcf1049e8d490602b48f4"
|
||||
integrity sha512-dM2Q9ex8d28+RuaoFczkCY50KHG5666RqryzxA1IeMNVNTsU8iqflPl3v94+yiYonDTyHECIm4ZKqUlz2H4c/g==
|
||||
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"
|
||||
moment "^2.27.0"
|
||||
rc-util "^5.0.6"
|
||||
|
||||
"@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:
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
||||
|
|
Loading…
Reference in New Issue