mirror of https://github.com/certusone/oyster.git
feat: add collateral reserve selection
This commit is contained in:
parent
2b31d79654
commit
c7a08a5c66
|
@ -34,16 +34,16 @@ export const BorrowInput = (props: {
|
|||
|
||||
const borrowReserve = props.reserve;
|
||||
|
||||
const [collateralReserveMint, setCollateralReserveMint] = useState<string>();
|
||||
const [collateralReserveKey, setCollateralReserveKey] = useState<string>();
|
||||
|
||||
const collateralReserve = useMemo(() => {
|
||||
const id: string =
|
||||
cache
|
||||
.byParser(LendingReserveParser)
|
||||
.find((acc) => acc === collateralReserveMint) || "";
|
||||
.find((acc) => acc === collateralReserveKey) || "";
|
||||
|
||||
return cache.get(id) as ParsedAccount<LendingReserve>;
|
||||
}, [collateralReserveMint]);
|
||||
}, [collateralReserveKey]);
|
||||
|
||||
const name = useTokenName(borrowReserve?.info.liquidityMint);
|
||||
const { accounts: fromAccounts } = useUserBalance(
|
||||
|
@ -51,7 +51,8 @@ export const BorrowInput = (props: {
|
|||
);
|
||||
|
||||
const { userObligationsByReserve } = useUserObligationByReserve(
|
||||
borrowReserve.pubkey
|
||||
borrowReserve?.pubkey,
|
||||
collateralReserve?.pubkey,
|
||||
);
|
||||
|
||||
const onBorrow = useCallback(() => {
|
||||
|
@ -74,6 +75,7 @@ export const BorrowInput = (props: {
|
|||
borrowReserve,
|
||||
collateralReserve,
|
||||
|
||||
// TODO: select exsisting obligations by collateral reserve
|
||||
userObligationsByReserve.length > 0
|
||||
? userObligationsByReserve[0].obligation
|
||||
: undefined,
|
||||
|
@ -126,8 +128,8 @@ export const BorrowInput = (props: {
|
|||
<div className="borrow-input-title">{LABELS.SELECT_COLLATERAL}</div>
|
||||
<CollateralSelector
|
||||
reserve={borrowReserve.info}
|
||||
mint={collateralReserveMint}
|
||||
onMintChange={setCollateralReserveMint}
|
||||
collateralReserve={collateralReserveKey}
|
||||
onCollateralReserve={setCollateralReserveKey}
|
||||
/>
|
||||
|
||||
<div className="borrow-input-title">{LABELS.BORROW_QUESTION}</div>
|
||||
|
|
|
@ -11,8 +11,9 @@ const { Option } = Select;
|
|||
|
||||
export const CollateralSelector = (props: {
|
||||
reserve: LendingReserve;
|
||||
mint?: string;
|
||||
onMintChange: (id: string) => void;
|
||||
collateralReserve?: string;
|
||||
disabled?: boolean;
|
||||
onCollateralReserve?: (id: string) => void;
|
||||
}) => {
|
||||
const { reserveAccounts } = useLendingReserves();
|
||||
const { tokenMap } = useConnectionConfig();
|
||||
|
@ -20,20 +21,22 @@ export const CollateralSelector = (props: {
|
|||
const market = cache.get(props.reserve.lendingMarket) as ParsedAccount<
|
||||
LendingMarket
|
||||
>;
|
||||
const onlyQuoteAllowed = !props.reserve.liquidityMint.equals(
|
||||
market.info.quoteMint
|
||||
const onlyQuoteAllowed = !props.reserve?.liquidityMint?.equals(
|
||||
market?.info?.quoteMint
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
size="large"
|
||||
showSearch
|
||||
style={{ minWidth: 120 }}
|
||||
style={{ minWidth: 120, marginBottom: 10 }}
|
||||
placeholder="Collateral"
|
||||
value={props.mint}
|
||||
value={props.collateralReserve}
|
||||
disabled={props.disabled}
|
||||
defaultValue={props.collateralReserve}
|
||||
onChange={(item) => {
|
||||
if (props.onMintChange) {
|
||||
props.onMintChange(item);
|
||||
if (props.onCollateralReserve) {
|
||||
props.onCollateralReserve(item);
|
||||
}
|
||||
}}
|
||||
filterOption={(input, option) =>
|
||||
|
|
|
@ -21,7 +21,8 @@ import { notify } from "../../utils/notifications";
|
|||
|
||||
export const RepayInput = (props: {
|
||||
className?: string;
|
||||
reserve: ParsedAccount<LendingReserve>;
|
||||
borrowReserve: ParsedAccount<LendingReserve>;
|
||||
collateralReserve?: ParsedAccount<LendingReserve>;
|
||||
obligation: ParsedAccount<LendingObligation>;
|
||||
}) => {
|
||||
const connection = useConnection();
|
||||
|
@ -29,7 +30,7 @@ export const RepayInput = (props: {
|
|||
const [pendingTx, setPendingTx] = useState(false);
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
|
||||
const repayReserve = props.reserve;
|
||||
const repayReserve = props.borrowReserve;
|
||||
const obligation = props.obligation;
|
||||
|
||||
const liquidityMint = useMint(repayReserve.info.liquidityMint);
|
||||
|
@ -39,17 +40,7 @@ export const RepayInput = (props: {
|
|||
borrowAmountLamports,
|
||||
liquidityMint
|
||||
);
|
||||
|
||||
const [collateralReserveMint, setCollateralReserveMint] = useState<string>();
|
||||
|
||||
const collateralReserve = useMemo(() => {
|
||||
const id: string =
|
||||
cache
|
||||
.byParser(LendingReserveParser)
|
||||
.find((acc) => acc === collateralReserveMint) || "";
|
||||
|
||||
return cache.get(id) as ParsedAccount<LendingReserve>;
|
||||
}, [collateralReserveMint]);
|
||||
const collateralReserve = props.collateralReserve;
|
||||
|
||||
const name = useTokenName(repayReserve?.info.liquidityMint);
|
||||
const { accounts: fromAccounts } = useUserBalance(
|
||||
|
@ -173,11 +164,11 @@ export const RepayInput = (props: {
|
|||
value={pct}
|
||||
onChange={setPct}
|
||||
/>
|
||||
<div className="repay-input-title">{LABELS.SELECT_COLLATERAL}</div>
|
||||
<div className="repay-input-title">{LABELS.COLLATERAL}</div>
|
||||
<CollateralSelector
|
||||
reserve={repayReserve.info}
|
||||
mint={collateralReserveMint}
|
||||
onMintChange={setCollateralReserveMint}
|
||||
collateralReserve={collateralReserve?.pubkey.toBase58()}
|
||||
disabled={true}
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
|
|
@ -23,10 +23,12 @@ export const LABELS = {
|
|||
WALLET_TOOLTIP: "Wallet public key",
|
||||
SETTINGS_TOOLTIP: "Settings",
|
||||
SELECT_COLLATERAL: "Select collateral",
|
||||
COLLATERAL: "Collateral",
|
||||
BORROW_QUESTION: "How much would you like to borrow?",
|
||||
BORROW_ACTION: "Borrow",
|
||||
TABLE_TITLE_ASSET: "Asset",
|
||||
TABLE_TITLE_LOAN_BALANCE: "Your loan balance",
|
||||
TABLE_TITLE_COLLATERAL_BALANCE: "Collateral",
|
||||
TABLE_TITLE_DEPOSIT_BALANCE: "Your deposit balance",
|
||||
TABLE_TITLE_APY: "APY",
|
||||
TABLE_TITLE_BORROW_APY: "Borrow APY",
|
||||
|
|
|
@ -9,6 +9,7 @@ import { useMemo } from "react";
|
|||
import { EventEmitter } from "./../utils/eventEmitter";
|
||||
|
||||
import { DexMarketParser } from "./../models/dex";
|
||||
import { LendingReserve } from "../models";
|
||||
|
||||
export interface MarketsContextState {
|
||||
midPriceInUSD: (mint: string) => number;
|
||||
|
@ -248,6 +249,55 @@ export const usePrecacheMarket = () => {
|
|||
return context.precacheMarkets;
|
||||
};
|
||||
|
||||
export const simulateMarketOrderFill = (amount: number, reserve: LendingReserve) => {
|
||||
const marketInfo = cache.get(reserve?.dexMarket);
|
||||
if (!marketInfo) {
|
||||
return 0.0;
|
||||
}
|
||||
const decodedMarket = marketInfo.info;
|
||||
|
||||
const baseMintDecimals =
|
||||
cache.get(decodedMarket.baseMint)?.info.decimals || 0;
|
||||
const quoteMintDecimals =
|
||||
cache.get(decodedMarket.quoteMint)?.info.decimals || 0;
|
||||
|
||||
const market = new Market(
|
||||
decodedMarket,
|
||||
baseMintDecimals,
|
||||
quoteMintDecimals,
|
||||
undefined,
|
||||
decodedMarket.programId
|
||||
);
|
||||
|
||||
const bids = cache.get(decodedMarket.bids)?.info;
|
||||
const asks = cache.get(decodedMarket.asks)?.info;
|
||||
|
||||
if (bids && asks) {
|
||||
const bidsBook = new Orderbook(market, bids.accountFlags, bids.slab);
|
||||
const asksBook = new Orderbook(market, asks.accountFlags, asks.slab);
|
||||
|
||||
// TODO: pick side
|
||||
let book = bidsBook;
|
||||
|
||||
let cost = 0;
|
||||
let remaining = amount;
|
||||
|
||||
for (const level of book) {
|
||||
let size = remaining > level.size ? level.size : level.size - remaining;
|
||||
cost = cost + level.price * size;
|
||||
remaining = remaining - size;
|
||||
|
||||
if(remaining <= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return cost;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const getMidPrice = (marketAddress?: string, mintAddress?: string) => {
|
||||
const SERUM_TOKEN = TOKEN_MINTS.find(
|
||||
(a) => a.address.toBase58() === mintAddress
|
||||
|
|
|
@ -2,15 +2,19 @@ import { useMemo } from "react";
|
|||
import { useUserObligations } from "./useUserObligations";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
|
||||
export function useUserObligationByReserve(reserve?: string | PublicKey) {
|
||||
export function useUserObligationByReserve(
|
||||
borrowReserve?: string | PublicKey,
|
||||
collateralReserve?: string | PublicKey) {
|
||||
const { userObligations } = useUserObligations();
|
||||
|
||||
const userObligationsByReserve = useMemo(() => {
|
||||
const id = typeof reserve === "string" ? reserve : reserve?.toBase58();
|
||||
const borrowId = typeof borrowReserve === "string" ? borrowReserve : borrowReserve?.toBase58();
|
||||
const collateralId = typeof collateralReserve === "string" ? collateralReserve : collateralReserve?.toBase58();
|
||||
return userObligations.filter(
|
||||
(item) => item.obligation.info.borrowReserve.toBase58() === id
|
||||
(item) => item.obligation.info.borrowReserve.toBase58() === borrowId &&
|
||||
item.obligation.info.collateralReserve.toBase58() === collateralId
|
||||
);
|
||||
}, [reserve, userObligations]);
|
||||
}, [borrowReserve, collateralReserve, userObligations]);
|
||||
|
||||
return {
|
||||
userObligationsByReserve,
|
||||
|
|
|
@ -47,7 +47,7 @@ export const liquidateInstruction = (
|
|||
dataLayout.encode(
|
||||
{
|
||||
instruction: LendingInstruction.LiquidateObligation,
|
||||
collateralAmount: new BN(liquidityAmount),
|
||||
liquidityAmount: new BN(liquidityAmount),
|
||||
},
|
||||
data
|
||||
);
|
||||
|
|
|
@ -8,9 +8,9 @@ export const LendingObligationLayout: typeof BufferLayout.Structure = BufferLayo
|
|||
/// Slot when obligation was updated. Used for calculating interest.
|
||||
Layout.uint64("lastUpdateSlot"),
|
||||
/// Amount of collateral tokens deposited for this obligation
|
||||
Layout.uint64("collateralAmount"),
|
||||
Layout.uint64("depositedCollateral"),
|
||||
/// Reserve which collateral tokens were deposited into
|
||||
Layout.publicKey("collateralSupply"),
|
||||
Layout.publicKey("collateralReserve"),
|
||||
/// Borrow rate used for calculating interest.
|
||||
Layout.uint128("cumulativeBorrowRateWad"),
|
||||
/// Amount of tokens borrowed for this obligation plus interest
|
||||
|
@ -28,8 +28,8 @@ export const isLendingObligation = (info: AccountInfo<Buffer>) => {
|
|||
|
||||
export interface LendingObligation {
|
||||
lastUpdateSlot: BN;
|
||||
collateralAmount: BN;
|
||||
collateralSupply: PublicKey;
|
||||
depositedCollateral: BN;
|
||||
collateralReserve: PublicKey;
|
||||
cumulativeBorrowRateWad: BN; // decimals
|
||||
borrowAmountWad: BN; // decimals
|
||||
borrowReserve: PublicKey;
|
||||
|
|
|
@ -187,3 +187,12 @@ export const reserveMarketCap = (reserve?: LendingReserve) => {
|
|||
export const collateralExchangeRate = (reserve?: LendingReserve) => {
|
||||
return (reserve?.collateralMintSupply.toNumber() || 1) / reserveMarketCap(reserve);
|
||||
}
|
||||
|
||||
export const collateralToLiquidity = (collateralAmount: BN, reserve?: LendingReserve) => {
|
||||
return Math.floor(collateralAmount.toNumber() / collateralExchangeRate(reserve));
|
||||
}
|
||||
|
||||
|
||||
export const liquidityToCollateral = (liquidityAmount: BN, reserve?: LendingReserve) => {
|
||||
return Math.floor(liquidityAmount.toNumber() * collateralExchangeRate(reserve));
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ export const DashboardView = () => {
|
|||
<div className="dashboard-item dashboard-header">
|
||||
<div>{LABELS.TABLE_TITLE_ASSET}</div>
|
||||
<div>{LABELS.TABLE_TITLE_LOAN_BALANCE}</div>
|
||||
<div>{LABELS.TABLE_TITLE_COLLATERAL_BALANCE}</div>
|
||||
<div>{LABELS.TABLE_TITLE_APY}</div>
|
||||
<div>{LABELS.TABLE_TITLE_ACTION}</div>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useUserCollateralBalance, useTokenName } from "../../hooks";
|
|||
import {
|
||||
calculateBorrowAPY,
|
||||
collateralExchangeRate,
|
||||
collateralToLiquidity,
|
||||
LendingObligation,
|
||||
LendingReserve,
|
||||
reserveMarketCap,
|
||||
|
@ -17,6 +18,7 @@ import {
|
|||
import { Button, Card } from "antd";
|
||||
import { Link } from "react-router-dom";
|
||||
import { cache, ParsedAccount, useMint } from "../../contexts/accounts";
|
||||
import { simulateMarketOrderFill } from "../../contexts/market";
|
||||
|
||||
export const ObligationItem = (props: {
|
||||
obligation: ParsedAccount<LendingObligation>;
|
||||
|
@ -27,10 +29,12 @@ export const ObligationItem = (props: {
|
|||
obligation.info.borrowReserve
|
||||
) as ParsedAccount<LendingReserve>;
|
||||
|
||||
const name = useTokenName(borrowReserve?.info.liquidityMint);
|
||||
const collateralReserve = cache.get(
|
||||
obligation.info.collateralReserve
|
||||
) as ParsedAccount<LendingReserve>;
|
||||
|
||||
const liquidityMint = useMint(borrowReserve.info.liquidityMint);
|
||||
const collateralMint = useMint(borrowReserve.info.collateralMint);
|
||||
const collateralMint = useMint(collateralReserve.info.liquidityMint);
|
||||
|
||||
const borrowAmount = fromLamports(
|
||||
wadToLamports(obligation.info.borrowAmountWad),
|
||||
|
@ -41,20 +45,48 @@ export const ObligationItem = (props: {
|
|||
borrowReserve,
|
||||
]);
|
||||
|
||||
const collateralLamports = collateralToLiquidity(obligation.info.depositedCollateral, borrowReserve.info);
|
||||
const collateral = fromLamports(collateralLamports, collateralMint);
|
||||
|
||||
const rate = collateralExchangeRate(borrowReserve.info) ;
|
||||
const borrowName = useTokenName(borrowReserve?.info.liquidityMint);
|
||||
const collateralName = useTokenName(collateralReserve?.info.liquidityMint);
|
||||
|
||||
console.log(`collateral ${fromLamports(obligation.info.collateralAmount.toNumber() / rate, collateralMint)}`);
|
||||
|
||||
const cost = simulateMarketOrderFill(collateralLamports, borrowReserve.info);
|
||||
console.log(cost);
|
||||
|
||||
// TODO: health factor
|
||||
// let borrow_amount_as_collateral = withdraw_reserve_rates.liquidity_to_collateral(
|
||||
// simulate_market_order_fill(
|
||||
// obligation.borrowed_liquidity_wads,
|
||||
// memory,
|
||||
// dex_market_order_book_side_info,
|
||||
// dex_market_info,
|
||||
// &repay_reserve,
|
||||
// )?
|
||||
// .round_u64(),
|
||||
// );
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="dashboard-item">
|
||||
<span style={{ display: "flex" }}>
|
||||
<div style={{ display: "flex" }}>
|
||||
<TokenIcon
|
||||
mintAddress={collateralReserve?.info.liquidityMint}
|
||||
style={{ marginRight: "-0.5rem" }}
|
||||
/>
|
||||
<TokenIcon mintAddress={borrowReserve?.info.liquidityMint} />
|
||||
{name}
|
||||
</div>
|
||||
{collateralName}
|
||||
/
|
||||
{borrowName}
|
||||
</span>
|
||||
<div>
|
||||
{formatNumber.format(borrowAmount)} {name}
|
||||
{formatNumber.format(borrowAmount)} {borrowName}
|
||||
</div>
|
||||
<div>
|
||||
{formatNumber.format(collateral)} {collateralName}
|
||||
</div>
|
||||
<div>{formatPct.format(borrowAPY)}</div>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
}
|
||||
|
||||
& > :first-child {
|
||||
flex: 80px
|
||||
flex: 120px
|
||||
}
|
||||
|
||||
& > :last-child {
|
||||
|
@ -28,7 +28,7 @@
|
|||
|
||||
& > :first-child {
|
||||
text-align: left;
|
||||
flex: 80px
|
||||
flex: 120px
|
||||
}
|
||||
|
||||
& > :last-child {
|
||||
|
|
|
@ -20,6 +20,12 @@ export const RepayReserveView = () => {
|
|||
const lendingReserve = useLendingReserve(
|
||||
obligationId ? lendingObligation?.info.borrowReserve : reserveId
|
||||
);
|
||||
|
||||
|
||||
const repayReserve = useLendingReserve(
|
||||
obligationId ? lendingObligation?.info.collateralReserve : reserveId
|
||||
);
|
||||
|
||||
const reserve = lendingReserve?.info;
|
||||
|
||||
if (!reserve || !lendingReserve || !lendingObligation) {
|
||||
|
@ -31,7 +37,8 @@ export const RepayReserveView = () => {
|
|||
<div className="repay-reserve-container">
|
||||
<RepayInput
|
||||
className="repay-reserve-item repay-reserve-item-left"
|
||||
reserve={lendingReserve}
|
||||
borrowReserve={lendingReserve}
|
||||
collateralReserve={repayReserve}
|
||||
obligation={lendingObligation}
|
||||
/>
|
||||
<SideReserveOverview
|
||||
|
|
Loading…
Reference in New Issue