feat: repay
This commit is contained in:
parent
8bdcdab94f
commit
31020e7e86
|
@ -19,4 +19,5 @@ Any content produced by Solana, or developer resources that Solana provides, are
|
|||
- [] Add slider to borrow page (100% max collateral)
|
||||
- [] Add market size on front page
|
||||
- [] Add github link
|
||||
- [] Repay from reserve (add selection for obligation/loan)
|
||||
- []
|
|
@ -13,7 +13,7 @@ import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../constants/ids";
|
|||
import { findOrCreateAccountByMint } from "./account";
|
||||
import { LendingObligation, TokenAccount } from "../models";
|
||||
import { ParsedAccount } from "../contexts/accounts";
|
||||
import { decimalToLamports } from "../utils/utils";
|
||||
import { wadToLamports } from "../utils/utils";
|
||||
|
||||
export const repay = async (
|
||||
from: TokenAccount, // CollateralAccount
|
||||
|
@ -76,7 +76,7 @@ export const repay = async (
|
|||
signers
|
||||
);
|
||||
|
||||
const loanRatio = amountLamports / decimalToLamports(obligation.info.borrowAmount)
|
||||
const loanRatio = amountLamports / wadToLamports(obligation.info.borrowAmountWad)
|
||||
.toNumber();
|
||||
console.log(loanRatio);
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useWallet } from "../../contexts/wallet";
|
|||
import { CurrentUserBadge } from "../CurrentUserBadge";
|
||||
import { SettingOutlined } from "@ant-design/icons";
|
||||
import { Settings } from "../Settings";
|
||||
import { LABELS } from "../../constants";
|
||||
|
||||
export const AppBar = (props: { left?: JSX.Element; right?: JSX.Element }) => {
|
||||
const { connected, wallet } = useWallet();
|
||||
|
@ -19,20 +20,20 @@ export const AppBar = (props: { left?: JSX.Element; right?: JSX.Element }) => {
|
|||
onClick={connected ? wallet.disconnect : wallet.connect}
|
||||
style={{ color: "#2abdd2" }}
|
||||
>
|
||||
Connect
|
||||
{LABELS.CONNECT_BUTTON}
|
||||
</Button>
|
||||
)}
|
||||
{connected && (
|
||||
<Popover
|
||||
placement="bottomRight"
|
||||
title="Wallet public key"
|
||||
title={LABELS.WALLET_TOOLTIP}
|
||||
trigger="click"
|
||||
></Popover>
|
||||
)}
|
||||
</div>
|
||||
<Popover
|
||||
placement="topRight"
|
||||
title="Settings"
|
||||
title={LABELS.SETTINGS_TOOLTIP}
|
||||
content={<Settings />}
|
||||
trigger="click"
|
||||
>
|
||||
|
|
|
@ -11,6 +11,7 @@ import { borrow } from "../../actions";
|
|||
import { PublicKey } from "@solana/web3.js";
|
||||
import { CollateralSelector } from "./../CollateralSelector";
|
||||
import "./style.less";
|
||||
import { LABELS } from "../../constants";
|
||||
|
||||
export const BorrowInput = (props: {
|
||||
className?: string;
|
||||
|
@ -84,7 +85,7 @@ export const BorrowInput = (props: {
|
|||
}}
|
||||
>
|
||||
<div className="borrow-input-title">
|
||||
How much would you like to borrow?
|
||||
{LABELS.BORROW_QUESTION}
|
||||
</div>
|
||||
<div className="token-input">
|
||||
<TokenIcon mintAddress={borrowReserve?.liquidityMint} />
|
||||
|
@ -104,7 +105,7 @@ export const BorrowInput = (props: {
|
|||
/>
|
||||
<div>{name}</div>
|
||||
</div>
|
||||
<div className="borrow-input-title">Select collateral account?</div>
|
||||
<div className="borrow-input-title">{LABELS.SELECT_COLLATERAL}</div>
|
||||
<CollateralSelector
|
||||
reserve={borrowReserve}
|
||||
mint={collateralReserveMint}
|
||||
|
@ -116,7 +117,7 @@ export const BorrowInput = (props: {
|
|||
onClick={onBorrow}
|
||||
disabled={fromAccounts.length === 0}
|
||||
>
|
||||
Borrow
|
||||
{LABELS.BORROW_ACTION}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { Button } from "antd"
|
||||
import { ButtonProps } from "antd/lib/button"
|
||||
import React from "react"
|
||||
import { useWallet } from "../../contexts/wallet";
|
||||
import { LABELS } from './../../constants';
|
||||
|
||||
export const ConnectButton = (props: ButtonProps & React.RefAttributes<HTMLElement>) => {
|
||||
const { wallet, connected } = useWallet();
|
||||
const { onClick, children, ...rest } = props;
|
||||
return <Button {...rest} onClick={connected ? onClick : wallet.connect} >
|
||||
{connected ? props.children : LABELS.CONNECT_LABEL}
|
||||
</Button>
|
||||
}
|
|
@ -14,6 +14,7 @@ import BasicLayout from "@ant-design/pro-layout";
|
|||
import { AppBar } from "./../AppBar";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { useConnectionConfig } from "../../contexts/connection";
|
||||
import { LABELS } from "../../constants";
|
||||
|
||||
export const AppLayout = (props: any) => {
|
||||
const { env } = useConnectionConfig();
|
||||
|
@ -36,11 +37,11 @@ export const AppLayout = (props: any) => {
|
|||
<div className="App">
|
||||
<div className="Banner">
|
||||
<div className="Banner-description">
|
||||
Oyster Lending is unaudited software. Use at your own risk.
|
||||
{LABELS.AUDIT_WARNING}
|
||||
</div>
|
||||
</div>
|
||||
<BasicLayout
|
||||
title="Oyster Lending"
|
||||
title={LABELS.APP_TITLE}
|
||||
navTheme="realDark"
|
||||
headerTheme="dark"
|
||||
theme="dark"
|
||||
|
@ -62,7 +63,7 @@ export const AppLayout = (props: any) => {
|
|||
pathname: "/",
|
||||
}}
|
||||
>
|
||||
Home
|
||||
{LABELS.MENU_HOME}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="2" icon={<PieChartOutlined />}>
|
||||
|
@ -71,7 +72,7 @@ export const AppLayout = (props: any) => {
|
|||
pathname: "/dashboard",
|
||||
}}
|
||||
>
|
||||
Dashboard
|
||||
{LABELS.MENU_DASHBOARD}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="3" icon={<BankOutlined />}>
|
||||
|
@ -80,7 +81,7 @@ export const AppLayout = (props: any) => {
|
|||
pathname: "/deposit",
|
||||
}}
|
||||
>
|
||||
Deposit
|
||||
{LABELS.MENU_DEPOSIT}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="4" icon={<LogoutOutlined />}>
|
||||
|
@ -89,7 +90,7 @@ export const AppLayout = (props: any) => {
|
|||
pathname: "/borrow",
|
||||
}}
|
||||
>
|
||||
Borrow
|
||||
{LABELS.MENU_BORROW}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
{env !== "mainnet-beta" && (
|
||||
|
@ -99,7 +100,7 @@ export const AppLayout = (props: any) => {
|
|||
pathname: "/faucet",
|
||||
}}
|
||||
>
|
||||
Faucet
|
||||
{LABELS.MENU_FAUCET}
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
|
|
@ -14,7 +14,8 @@ import { useWallet } from "../../contexts/wallet";
|
|||
import { repay } from "../../actions";
|
||||
import { CollateralSelector } from "./../CollateralSelector";
|
||||
import "./style.less";
|
||||
import { decimalToLamports, formatNumber, fromLamports, toLamports } from "../../utils/utils";
|
||||
import { wadToLamports, formatNumber, fromLamports, toLamports } from "../../utils/utils";
|
||||
import { LABELS } from "../../constants";
|
||||
|
||||
export const RepayInput = (props: {
|
||||
className?: string;
|
||||
|
@ -51,9 +52,9 @@ export const RepayInput = (props: {
|
|||
|
||||
const lamports = useMemo(() => toLamports(parseFloat(value), repayLiquidityMint), [value, repayLiquidityMint]);
|
||||
|
||||
const mark = decimalToLamports(obligation?.info.borrowAmount).toNumber() / lamports * 100;
|
||||
const mark = wadToLamports(obligation?.info.borrowAmountWad).toNumber() / lamports * 100;
|
||||
|
||||
const onReoay = useCallback(() => {
|
||||
const onRepay = useCallback(() => {
|
||||
if (
|
||||
!collateralReserve ||
|
||||
!obligation ||
|
||||
|
@ -102,7 +103,7 @@ export const RepayInput = (props: {
|
|||
}}
|
||||
>
|
||||
<div className="repay-input-title">
|
||||
How much would you like to repay? (Currently:{" "}
|
||||
{LABELS.REPAY_QUESTION} (Currently:{" "})
|
||||
{formatNumber.format(balance)} {name})
|
||||
</div>
|
||||
<div className="token-input">
|
||||
|
@ -126,8 +127,8 @@ export const RepayInput = (props: {
|
|||
<Slider marks={marks}
|
||||
value={mark}
|
||||
onChange={(val: number) =>
|
||||
setValue((fromLamports(decimalToLamports(obligation?.info.borrowAmount).toNumber(), repayLiquidityMint) * val / 100).toFixed(2))} />
|
||||
<div className="repay-input-title">Select collateral account?</div>
|
||||
setValue((fromLamports(wadToLamports(obligation?.info.borrowAmountWad).toNumber(), repayLiquidityMint) * val / 100).toFixed(2))} />
|
||||
<div className="repay-input-title">{LABELS.SELECT_COLLATERAL}</div>
|
||||
<CollateralSelector
|
||||
reserve={repayReserve.info}
|
||||
mint={collateralReserveMint}
|
||||
|
@ -136,10 +137,10 @@ export const RepayInput = (props: {
|
|||
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={onReoay}
|
||||
onClick={onRepay}
|
||||
disabled={fromAccounts.length === 0}
|
||||
>
|
||||
Repay
|
||||
{LABELS.REPAY_ACTION}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { LendingReserve } from "../../models/lending";
|
|||
import { Card } from "antd";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import "./style.less";
|
||||
import { LABELS } from "../../constants";
|
||||
|
||||
export const ReserveStatus = (props: {
|
||||
className?: string;
|
||||
|
@ -18,7 +19,7 @@ export const ReserveStatus = (props: {
|
|||
return (
|
||||
<Card
|
||||
className={props.className}
|
||||
title={<>Reserve Status & Configuration</>}
|
||||
title={<>{LABELS.RESERVE_STATUS_TITLE}</>}
|
||||
bodyStyle={bodyStyle}
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -35,7 +35,7 @@ export const SideReserveOverview = (props: {
|
|||
const borrowApy = 0.048;
|
||||
|
||||
const utilizationRate = reserve.config.loanToValueRatio / 100;
|
||||
const liquidiationThreshold = reserve.config.maxUtilizationRate / 100;
|
||||
const liquidiationThreshold = reserve.config.optimalUtilizationRate / 100;
|
||||
const liquidiationPenalty = reserve.config.liquidationBonus / 100;
|
||||
const maxLTV = liquidiationThreshold - liquidiationPenalty;
|
||||
|
||||
|
|
|
@ -3,12 +3,15 @@ import {
|
|||
useCollateralBalance,
|
||||
useTokenName,
|
||||
useUserBalance,
|
||||
useBorrowedAmount,
|
||||
} from "./../../hooks";
|
||||
import { LendingReserve } from "../../models/lending";
|
||||
import { formatNumber } from "../../utils/utils";
|
||||
import { formatNumber, wadToLamports } from "../../utils/utils";
|
||||
import { Button, Card, Typography } from "antd";
|
||||
import { Link } from "react-router-dom";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { cache, ParsedAccount } from "../../contexts/accounts";
|
||||
import { MintInfo } from "@solana/spl-token";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
|
@ -25,8 +28,9 @@ export const UserLendingCard = (props: {
|
|||
const { balance: tokenBalance } = useUserBalance(props.reserve.liquidityMint);
|
||||
const { balance: collateralBalance } = useCollateralBalance(props.reserve);
|
||||
|
||||
const { borrowed: totalBorrowed } = useBorrowedAmount(address);
|
||||
|
||||
// TODO: calculate
|
||||
const borrowed = 0;
|
||||
const healthFactor = "--";
|
||||
const ltv = 0;
|
||||
const available = 0;
|
||||
|
@ -54,7 +58,7 @@ export const UserLendingCard = (props: {
|
|||
Borrowed
|
||||
</Text>
|
||||
<div className="card-cell ">
|
||||
{formatNumber.format(borrowed)} {name}
|
||||
{formatNumber.format(totalBorrowed)} {name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import { useWallet } from "../../contexts/wallet";
|
|||
import { withdraw } from "../../actions";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import "./style.less";
|
||||
import { LABELS } from "../../constants";
|
||||
|
||||
export const WithdrawInput = (props: {
|
||||
className?: string;
|
||||
|
@ -76,7 +77,7 @@ export const WithdrawInput = (props: {
|
|||
}}
|
||||
>
|
||||
<div className="withdraw-input-title">
|
||||
How much would you like to withdraw?
|
||||
{LABELS.WITHDRAW_QUESTION}
|
||||
</div>
|
||||
<div className="token-input">
|
||||
<TokenIcon mintAddress={reserve?.liquidityMint} />
|
||||
|
@ -102,7 +103,7 @@ export const WithdrawInput = (props: {
|
|||
onClick={onWithdraw}
|
||||
disabled={fromAccounts.length === 0}
|
||||
>
|
||||
Withdraw
|
||||
{LABELS.WITHDRAW_ACTION}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export * from './ids';
|
||||
export * from './labels';
|
||||
export * from './math';
|
|
@ -0,0 +1,30 @@
|
|||
export const LABELS = {
|
||||
CONNECT_LABEL: "Connect Wallet",
|
||||
GIVE_SOL: "Give me SOL",
|
||||
FAUCET_INFO: "This faucet will help you fund your accounts outside of Solana main network.",
|
||||
ACCOUNT_FUNDED: "Account funded.",
|
||||
REPAY_QUESTION: "How much would you like to repay?",
|
||||
REPAY_ACTION: "Repay",
|
||||
RESERVE_STATUS_TITLE: "Reserve Status & Configuration",
|
||||
AUDIT_WARNING: "Oyster Lending is unaudited software. Use at your own risk.",
|
||||
MENU_HOME: "Home",
|
||||
MENU_DASHBOARD: "Dashboard",
|
||||
MENU_DEPOSIT: "Deposit",
|
||||
MENU_BORROW: "Borrow",
|
||||
MENU_FAUCET: "Faucet",
|
||||
APP_TITLE: "Oyster Lending",
|
||||
CONNECT_BUTTON: "Connect",
|
||||
WALLET_TOOLTIP: "Wallet public key",
|
||||
SETTINGS_TOOLTIP: "Settings",
|
||||
SELECT_COLLATERAL: "Select collateral account?",
|
||||
BORROW_QUESTION: "How much would you like to borrow?",
|
||||
BORROW_ACTION: "Borrow",
|
||||
TABLE_TITLE_ASSET: "Asset",
|
||||
TABLE_TITLE_LOAN_BALANCE: "Your loan balan",
|
||||
TABLE_TITLE_APY: "APY",
|
||||
TABLE_TITLE_ACTION: "Action",
|
||||
DASHBOARD_TITLE_LOANS: "Loans",
|
||||
DASHBOARD_TITLE_DEPOSITS: "Deposts",
|
||||
WITHDRAW_ACTION: "Withdraw",
|
||||
WITHDRAW_QUESTION: "How much would you like to withdraw?",
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import BN from "bn.js";
|
||||
|
||||
export const TEN = new BN(10);
|
||||
export const WAD = TEN.pow(new BN(18));
|
||||
export const RAY = TEN.pow(new BN(27));
|
||||
export const ZERO = new BN(0);
|
|
@ -7,3 +7,4 @@ export * from "./useCollateralBalance";
|
|||
export * from "./useLendingObligations";
|
||||
export * from "./useUserObligations";
|
||||
export * from "./useUserObligationByReserve";
|
||||
export * from "./useBorrowedAmount";
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useUserAccounts } from "./useUserAccounts";
|
||||
import { useLendingObligations } from "./useLendingObligations";
|
||||
import { TokenAccount } from "../models";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { useUserObligationByReserve } from "./useUserObligationByReserve";
|
||||
import { fromLamports, wadToLamports } from "../utils/utils";
|
||||
import { cache, getMultipleAccounts, MintParser, ParsedAccount, useMint } from "../contexts/accounts";
|
||||
import { useConnection } from "../contexts/connection";
|
||||
import { MintInfo } from "@solana/spl-token";
|
||||
import { useLendingReserve } from "./useLendingReserves";
|
||||
|
||||
export function useBorrowedAmount(address?: string | PublicKey) {
|
||||
const connection = useConnection();
|
||||
const { userObligationsByReserve } = useUserObligationByReserve(address);
|
||||
const [borrowedLamports, setBorrowedLamports] = useState(0);
|
||||
const reserve = useLendingReserve(address);
|
||||
const liquidityMint = useMint(reserve?.info.liquidityMint);
|
||||
|
||||
useEffect(() => {
|
||||
setBorrowedLamports(0);
|
||||
|
||||
(async () => {
|
||||
// precache obligation mints
|
||||
const { keys, array } = await getMultipleAccounts(
|
||||
connection,
|
||||
userObligationsByReserve
|
||||
.map(item => item.obligation.info.tokenMint.toBase58()),
|
||||
"single");
|
||||
|
||||
array.forEach((item, index) => {
|
||||
const address = keys[index];
|
||||
cache.add(new PublicKey(address), item, MintParser);
|
||||
});
|
||||
|
||||
setBorrowedLamports(userObligationsByReserve.reduce((result, item) => {
|
||||
|
||||
const borrowed = wadToLamports(item.obligation.info.borrowAmountWad).toNumber();
|
||||
|
||||
const owned = item.userAccounts.reduce((amount, acc) => amount += acc.info.amount.toNumber(), 0);
|
||||
const obligationMint = cache.get(item.obligation.info.tokenMint) as ParsedAccount<MintInfo>;
|
||||
|
||||
result += borrowed * owned / obligationMint?.info.supply.toNumber();
|
||||
return result
|
||||
|
||||
}, 0));
|
||||
})();
|
||||
|
||||
|
||||
}, [userObligationsByReserve]);
|
||||
|
||||
return { borrowed: fromLamports(borrowedLamports, liquidityMint), borrowedLamports };
|
||||
}
|
|
@ -34,14 +34,14 @@ export function useLendingReserves() {
|
|||
};
|
||||
}
|
||||
|
||||
export function useLendingReserve(address: string | PublicKey) {
|
||||
export function useLendingReserve(address?: string | PublicKey) {
|
||||
const id = typeof address === "string" ? address : address?.toBase58();
|
||||
const [reserveAccount, setReserveAccount] = useState<
|
||||
ParsedAccount<LendingReserve>
|
||||
>();
|
||||
|
||||
useEffect(() => {
|
||||
setReserveAccount(cache.get(id));
|
||||
setReserveAccount(cache.get(id || ''));
|
||||
|
||||
const dispose = cache.emitter.onCache((args) => {
|
||||
if (args.id === id) {
|
||||
|
|
|
@ -2,15 +2,16 @@ import { useMemo } from "react";
|
|||
import { useUserObligations } from "./useUserObligations";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
|
||||
export function useUserObligationByReserve(reserve: PublicKey) {
|
||||
export function useUserObligationByReserve(reserve?: string | PublicKey) {
|
||||
const { userObligations } = useUserObligations();
|
||||
|
||||
const userObligationsByReserve = useMemo(
|
||||
() =>
|
||||
userObligations.filter((item) =>
|
||||
item.oblication.info.borrowReserve.equals(reserve)
|
||||
),
|
||||
[reserve, userObligations]
|
||||
() => {
|
||||
const id = typeof reserve === 'string' ? reserve : reserve?.toBase58();
|
||||
return userObligations.filter((item) =>
|
||||
item.obligation.info.borrowReserve.toBase58() === id
|
||||
)
|
||||
}, [reserve, userObligations]
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
@ -26,7 +26,7 @@ export function useUserObligations() {
|
|||
)
|
||||
.map((ob) => {
|
||||
return {
|
||||
oblication: ob,
|
||||
obligation: ob,
|
||||
userAccounts: [...accountsByMint.get(ob.info.tokenMint.toBase58())],
|
||||
|
||||
// TODO: add total borrowed amount?
|
||||
|
|
|
@ -12,9 +12,9 @@ export const LendingObligationLayout: typeof BufferLayout.Structure = BufferLayo
|
|||
/// Reserve which collateral tokens were deposited into
|
||||
Layout.publicKey("collateralSupply"),
|
||||
/// Borrow rate used for calculating interest.
|
||||
Layout.uint128("cumulativeBorrowRate"),
|
||||
Layout.uint128("cumulativeBorrowRateWad"),
|
||||
/// Amount of tokens borrowed for this obligation plus interest
|
||||
Layout.uint128("borrowAmount"),
|
||||
Layout.uint128("borrowAmountWad"),
|
||||
/// Reserve which tokens were borrowed from
|
||||
Layout.publicKey("borrowReserve"),
|
||||
/// Mint address of the tokens for this obligation
|
||||
|
@ -30,8 +30,8 @@ export interface LendingObligation {
|
|||
lastUpdateSlot: BN;
|
||||
collateralAmount: BN;
|
||||
collateralSupply: PublicKey;
|
||||
cumulativeBorrowRate: BN; // decimals
|
||||
borrowAmount: BN; // decimals
|
||||
cumulativeBorrowRateWad: BN; // decimals
|
||||
borrowAmountWad: BN; // decimals
|
||||
borrowReserve: PublicKey;
|
||||
tokenMint: PublicKey;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
} from "@solana/web3.js";
|
||||
import BN from "bn.js";
|
||||
import * as BufferLayout from "buffer-layout";
|
||||
import { WAD } from "../../constants";
|
||||
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids";
|
||||
import * as Layout from "./../../utils/layout";
|
||||
import { LendingInstruction } from "./lending";
|
||||
|
@ -26,20 +27,26 @@ export const LendingReserveLayout: typeof BufferLayout.Structure = BufferLayout.
|
|||
|
||||
BufferLayout.struct(
|
||||
[
|
||||
/// Max utilization rate as a percent
|
||||
BufferLayout.u8("maxUtilizationRate"),
|
||||
/// Optimal utilization rate as a percent
|
||||
BufferLayout.u8("optimalUtilizationRate"),
|
||||
/// The ratio of the loan to the value of the collateral as a percent
|
||||
BufferLayout.u8("loanToValueRatio"),
|
||||
/// The percent discount the liquidator gets when buying collateral for an unhealthy obligation
|
||||
BufferLayout.u8("liquidationBonus"),
|
||||
/// The percent at which an obligation is considered unhealthy
|
||||
BufferLayout.u8("liquidationThreshold"),
|
||||
/// Min borrow APY
|
||||
BufferLayout.u8("minBorrowRate"),
|
||||
/// Optimal (utilization) borrow APY
|
||||
BufferLayout.u8("optimalBorrowRate"),
|
||||
/// Max borrow APY
|
||||
BufferLayout.u8("maxBorrowRate"),
|
||||
],
|
||||
"config"
|
||||
),
|
||||
|
||||
Layout.uint128("cumulativeBorrowRate"),
|
||||
Layout.uint128("totalBorrows"),
|
||||
Layout.uint128("cumulativeBorrowRateWad"),
|
||||
Layout.uint128("totalBorrowsWad"),
|
||||
|
||||
Layout.uint64("totalLiquidity"),
|
||||
Layout.uint64("collateralMintSupply"),
|
||||
|
@ -66,15 +73,18 @@ export interface LendingReserve {
|
|||
dexMarketPrice: BN; // what is precision on the price?
|
||||
|
||||
config: {
|
||||
maxUtilizationRate: number;
|
||||
optimalUtilizationRate: number;
|
||||
loanToValueRatio: number;
|
||||
liquidationBonus: number;
|
||||
liquidationThreshold: number;
|
||||
minBorrowRate: number;
|
||||
optimalBorrowRate: number;
|
||||
maxBorrowRate: number;
|
||||
};
|
||||
// collateralFactor: number;
|
||||
|
||||
cumulativeBorrowRate: BN;
|
||||
totalBorrows: BN;
|
||||
cumulativeBorrowRateWad: BN;
|
||||
totalBorrowsWad: BN;
|
||||
|
||||
totalLiquidity: BN;
|
||||
collateralMintSupply: BN;
|
||||
|
@ -250,3 +260,23 @@ export const withdrawInstruction = (
|
|||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const calculateBorrowAPY = (reserve: LendingReserve) => {
|
||||
const totalBorrows = reserve.totalBorrowsWad.div(WAD).toNumber()
|
||||
const currentUtilization = totalBorrows / (reserve.totalLiquidity.toNumber() + totalBorrows)
|
||||
const optimalUtilization = reserve.config.optimalUtilizationRate
|
||||
let borrowAPY;
|
||||
if (currentUtilization < optimalUtilization) {
|
||||
const normalized_factor = currentUtilization / optimalUtilization;
|
||||
const optimalBorrowRate = reserve.config.optimalBorrowRate / 100;
|
||||
const minBorrowRate = reserve.config.minBorrowRate / 100;
|
||||
borrowAPY = normalized_factor * (optimalBorrowRate - minBorrowRate) + minBorrowRate;
|
||||
} else {
|
||||
const normalized_factor = (currentUtilization - optimalUtilization) / (100 - optimalUtilization);
|
||||
const optimalBorrowRate = reserve.config.optimalBorrowRate / 100;
|
||||
const maxBorrowRate = reserve.config.maxBorrowRate / 100;
|
||||
borrowAPY = normalized_factor * (maxBorrowRate - optimalBorrowRate) + optimalBorrowRate;
|
||||
}
|
||||
|
||||
return borrowAPY;
|
||||
}
|
|
@ -54,7 +54,7 @@ export function Routes() {
|
|||
children={<BorrowReserveView />}
|
||||
/>
|
||||
<Route
|
||||
path="/repay/:reserve/:obligation"
|
||||
path="/repay/loan/:obligation"
|
||||
children={<RepayReserveView />}
|
||||
/>
|
||||
<Route
|
||||
|
|
|
@ -4,6 +4,7 @@ import { MintInfo } from "@solana/spl-token";
|
|||
import { TokenAccount } from "./../models";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import BN from "bn.js";
|
||||
import { WAD, ZERO } from "../constants";
|
||||
|
||||
export interface KnownToken {
|
||||
tokenSymbol: string;
|
||||
|
@ -106,9 +107,8 @@ export function toLamports(
|
|||
return amount * precision;
|
||||
}
|
||||
|
||||
export function decimalToLamports(amount?: BN): BN {
|
||||
// TODO: check math
|
||||
return amount?.div(new BN(10).pow(new BN(18))) || new BN(0);
|
||||
export function wadToLamports(amount?: BN): BN {
|
||||
return amount?.div(WAD) || ZERO;
|
||||
}
|
||||
|
||||
export function fromLamports(
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import React from "react";
|
||||
import {
|
||||
useCollateralBalance,
|
||||
useTokenName,
|
||||
useUserBalance,
|
||||
} from "../../hooks";
|
||||
import { LendingReserve } from "../../models/lending";
|
||||
import { TokenIcon } from "../../components/TokenIcon";
|
||||
import { formatNumber } from "../../utils/utils";
|
||||
import { Button, Card } from "antd";
|
||||
import { Link } from "react-router-dom";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
|
||||
export const ReserveItem = (props: {
|
||||
reserve: LendingReserve;
|
||||
address: PublicKey;
|
||||
}) => {
|
||||
const name = useTokenName(props.reserve.liquidityMint);
|
||||
const { balance: tokenBalance } = useUserBalance(props.reserve.liquidityMint);
|
||||
const { balance: collateralBalance } = useCollateralBalance(props.reserve);
|
||||
|
||||
return (
|
||||
<Link to={`/deposit/${props.address.toBase58()}`}>
|
||||
<Card>
|
||||
<div className="deposit-item">
|
||||
<span style={{ display: "flex" }}>
|
||||
<TokenIcon mintAddress={props.reserve.liquidityMint} />
|
||||
{name}
|
||||
</span>
|
||||
<div>
|
||||
{formatNumber.format(tokenBalance)} {name}
|
||||
</div>
|
||||
<div>
|
||||
{formatNumber.format(collateralBalance)} {name}
|
||||
</div>
|
||||
<div>--</div>
|
||||
<div>
|
||||
<Button>
|
||||
<span>Deposit</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
};
|
|
@ -1,22 +1,27 @@
|
|||
import React from "react";
|
||||
import { LABELS } from "../../constants";
|
||||
import { useUserObligations } from "./../../hooks";
|
||||
import { ObligationItem } from "./obligationItem";
|
||||
import "./style.less"
|
||||
|
||||
export const DashboardView = () => {
|
||||
const { userObligations } = useUserObligations();
|
||||
|
||||
return (
|
||||
<div className="flexColumn">
|
||||
<div className="dashboard-container">
|
||||
<div>
|
||||
<span>Loans</span>
|
||||
{userObligations.length > 0 && <div className="deposit-item deposit-header">
|
||||
<div>Asset</div>
|
||||
<div>Your loan balance</div>
|
||||
<div>APY</div>
|
||||
<div>Action</div>
|
||||
<span>{LABELS.DASHBOARD_TITLE_DEPOSITS}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{LABELS.DASHBOARD_TITLE_LOANS}</span>
|
||||
{userObligations.length > 0 && <div className="dashboard-item dashboard-header">
|
||||
<div>{LABELS.TABLE_TITLE_ASSET}</div>
|
||||
<div>{LABELS.TABLE_TITLE_LOAN_BALANCE}</div>
|
||||
<div>{LABELS.TABLE_TITLE_APY}</div>
|
||||
<div>{LABELS.TABLE_TITLE_ACTION}</div>
|
||||
</div>}
|
||||
{userObligations.map((item) => {
|
||||
return <ObligationItem obligation={item.oblication} />;
|
||||
return <ObligationItem obligation={item.obligation} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useTokenName } from "../../hooks";
|
|||
import { LendingObligation, LendingReserve } from "../../models/lending";
|
||||
import { TokenIcon } from "../../components/TokenIcon";
|
||||
import {
|
||||
decimalToLamports,
|
||||
wadToLamports,
|
||||
formatNumber,
|
||||
fromLamports,
|
||||
} from "../../utils/utils";
|
||||
|
@ -25,16 +25,16 @@ export const ObligationItem = (props: {
|
|||
const liquidityMint = useMint(borrowReserve.info.liquidityMint);
|
||||
|
||||
const borrowAmount = fromLamports(
|
||||
decimalToLamports(obligation.info.borrowAmount),
|
||||
wadToLamports(obligation.info.borrowAmountWad),
|
||||
liquidityMint
|
||||
);
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/repay/${borrowReserve?.pubkey.toBase58()}/${obligation.pubkey.toBase58()}`}
|
||||
to={`/repay/loan/${obligation.pubkey.toBase58()}`}
|
||||
>
|
||||
<Card>
|
||||
<div className="borrow-item">
|
||||
<div className="dashboard-item">
|
||||
<span style={{ display: "flex" }}>
|
||||
<TokenIcon mintAddress={borrowReserve?.info.liquidityMint} />
|
||||
{name}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.borrow-item {
|
||||
.dashboard-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
@ -14,7 +14,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.borrow-header {
|
||||
.dashboard-header {
|
||||
margin: 0px 30px;
|
||||
|
||||
& > div {
|
|
@ -1,9 +1,11 @@
|
|||
import React, { useCallback } from "react";
|
||||
import { Button, Card } from "antd";
|
||||
import { Card } from "antd";
|
||||
import { useConnection } from "../../contexts/connection";
|
||||
import { useWallet } from "../../contexts/wallet";
|
||||
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
|
||||
import { notify } from "../../utils/notifications";
|
||||
import { ConnectButton } from "./../../components/ConnectButton";
|
||||
import { LABELS } from "../../constants";
|
||||
|
||||
export const FaucetView = () => {
|
||||
const connection = useConnection();
|
||||
|
@ -14,7 +16,7 @@ export const FaucetView = () => {
|
|||
.requestAirdrop(wallet.publicKey, 2 * LAMPORTS_PER_SOL)
|
||||
.then(() => {
|
||||
notify({
|
||||
message: "Account funded.",
|
||||
message: LABELS.ACCOUNT_FUNDED,
|
||||
type: "success",
|
||||
});
|
||||
});
|
||||
|
@ -40,12 +42,11 @@ export const FaucetView = () => {
|
|||
}}
|
||||
>
|
||||
<div className="deposit-input-title" style={{ margin: 10 }}>
|
||||
This Faucet will help you fund your accounts outside of Solana main
|
||||
network.
|
||||
{LABELS.FAUCET_INFO}
|
||||
</div>
|
||||
<Button type="primary" onClick={airdrop}>
|
||||
Give me SOL
|
||||
</Button>
|
||||
<ConnectButton type="primary" onClick={airdrop}>
|
||||
{LABELS.GIVE_SOL}
|
||||
</ConnectButton>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import React from "react";
|
||||
import { useTokenName } from "../../hooks";
|
||||
import { LendingReserve } from "../../models/lending";
|
||||
import { calculateBorrowAPY, LendingReserve } from "../../models/lending";
|
||||
import { TokenIcon } from "../../components/TokenIcon";
|
||||
import { decimalToLamports, formatNumber, fromLamports } from "../../utils/utils";
|
||||
import { wadToLamports, formatNumber, fromLamports } from "../../utils/utils";
|
||||
import { Card } from "antd";
|
||||
import { Link } from "react-router-dom";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { useMint } from "../../contexts/accounts";
|
||||
import { WAD } from "../../constants";
|
||||
|
||||
export const LendingReserveItem = (props: {
|
||||
reserve: LendingReserve;
|
||||
|
@ -21,7 +22,8 @@ export const LendingReserveItem = (props: {
|
|||
liquidityMint
|
||||
);
|
||||
|
||||
const totalBorrows = decimalToLamports(props.reserve.totalBorrows).toNumber();
|
||||
console.log(props.reserve.totalBorrowsWad.toString());
|
||||
const totalBorrows = fromLamports(wadToLamports(props.reserve.totalBorrowsWad), liquidityMint);
|
||||
|
||||
console.log(liquidityMint);
|
||||
|
||||
|
@ -40,9 +42,12 @@ export const LendingReserveItem = (props: {
|
|||
{formatNumber.format(totalBorrows)} {name}
|
||||
</div>
|
||||
<div>--</div>
|
||||
<div>--</div>
|
||||
<div>{calculateBorrowAPY(props.reserve)}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -12,11 +12,12 @@ import "./style.less";
|
|||
|
||||
export const RepayReserveView = () => {
|
||||
const { reserve: reserveId, obligation: obligationId } = useParams<{
|
||||
reserve: string;
|
||||
reserve?: string;
|
||||
obligation?: string;
|
||||
}>();
|
||||
const lendingReserve = useLendingReserve(reserveId);
|
||||
|
||||
const lendingObligation = useLendingObligation(obligationId);
|
||||
const lendingReserve = useLendingReserve(obligationId ? lendingObligation?.info.borrowReserve : reserveId);
|
||||
const reserve = lendingReserve?.info;
|
||||
|
||||
console.log([reserveId, obligationId]);
|
||||
|
|
Loading…
Reference in New Issue