Merge pull request #44 from solana-labs/feature/interest_instruction

Feature/interest instruction
This commit is contained in:
Bartosz Lipinski 2021-01-22 23:30:24 -06:00 committed by GitHub
commit be4545e77b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 2629 additions and 1449 deletions

View File

@ -1,3 +1,3 @@
## ⚠️ Warning
Any content produced by Solana, or developer resources that Solana provides, are for educational and inspiration purposes only. Solana does not encourage, induce or sanction the deployment of any such applications in violation of applicable laws or regulations.

View File

@ -1,8 +1,17 @@
import { AccountLayout, MintLayout, Token } from '@solana/spl-token';
import { Account, PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js';
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT } from '../utils/ids';
import { LendingObligationLayout, TokenAccount } from '../models';
import { cache, TokenAccountParser } from './../contexts/accounts';
import { AccountLayout, MintLayout, Token } from "@solana/spl-token";
import {
Account,
PublicKey,
SystemProgram,
TransactionInstruction,
} from "@solana/web3.js";
import {
LENDING_PROGRAM_ID,
TOKEN_PROGRAM_ID,
WRAPPED_SOL_MINT,
} from "../utils/ids";
import { LendingObligationLayout, TokenAccount } from "../models";
import { cache, TokenAccountParser } from "./../contexts/accounts";
export function ensureSplAccount(
instructions: TransactionInstruction[],
@ -16,11 +25,31 @@ export function ensureSplAccount(
return toCheck.pubkey;
}
const account = createUninitializedAccount(instructions, payer, amount, signers);
const account = createUninitializedAccount(
instructions,
payer,
amount,
signers
);
instructions.push(Token.createInitAccountInstruction(TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT, account, payer));
instructions.push(
Token.createInitAccountInstruction(
TOKEN_PROGRAM_ID,
WRAPPED_SOL_MINT,
account,
payer
)
);
cleanupInstructions.push(Token.createCloseAccountInstruction(TOKEN_PROGRAM_ID, account, payer, payer, []));
cleanupInstructions.push(
Token.createCloseAccountInstruction(
TOKEN_PROGRAM_ID,
account,
payer,
payer,
[]
)
);
return account;
}
@ -125,9 +154,16 @@ export function createTokenAccount(
owner: PublicKey,
signers: Account[]
) {
const account = createUninitializedAccount(instructions, payer, accountRentExempt, signers);
const account = createUninitializedAccount(
instructions,
payer,
accountRentExempt,
signers
);
instructions.push(Token.createInitAccountInstruction(TOKEN_PROGRAM_ID, mint, account, owner));
instructions.push(
Token.createInitAccountInstruction(TOKEN_PROGRAM_ID, mint, account, owner)
);
return account;
}
@ -161,10 +197,25 @@ export function findOrCreateAccountByMint(
toAccount = account.pubkey;
} else {
// creating depositor pool account
toAccount = createTokenAccount(instructions, payer, accountRentExempt, mint, owner, signers);
toAccount = createTokenAccount(
instructions,
payer,
accountRentExempt,
mint,
owner,
signers
);
if (isWrappedSol) {
cleanupInstructions.push(Token.createCloseAccountInstruction(TOKEN_PROGRAM_ID, toAccount, payer, payer, []));
cleanupInstructions.push(
Token.createCloseAccountInstruction(
TOKEN_PROGRAM_ID,
toAccount,
payer,
payer,
[]
)
);
}
}

View File

@ -6,7 +6,10 @@ import {
} from "@solana/web3.js";
import { sendTransaction } from "../contexts/connection";
import { notify } from "../utils/notifications";
import { LendingReserve } from "./../models/lending/reserve";
import {
accrueInterestInstruction,
LendingReserve,
} from "./../models/lending/reserve";
import { AccountLayout, MintInfo, MintLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID, LEND_HOST_FEE_ADDRESS } from "../utils/ids";
import {
@ -16,8 +19,8 @@ import {
createUninitializedObligation,
ensureSplAccount,
findOrCreateAccountByMint,
} from './account';
import { cache, MintParser, ParsedAccount } from '../contexts/accounts';
} from "./account";
import { cache, MintParser, ParsedAccount } from "../contexts/accounts";
import {
TokenAccount,
LendingObligationLayout,
@ -26,6 +29,7 @@ import {
BorrowAmountType,
LendingObligation,
approve,
initObligationInstruction,
} from "../models";
import { toLamports } from "../utils/utils";
@ -46,38 +50,52 @@ export const borrow = async (
obligationAccount?: PublicKey
) => {
notify({
message: 'Borrowing funds...',
description: 'Please review transactions to approve.',
type: 'warn',
message: "Borrowing funds...",
description: "Please review transactions to approve.",
type: "warn",
});
let signers: Account[] = [];
let instructions: TransactionInstruction[] = [];
let cleanupInstructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(AccountLayout.span);
const [authority] = await PublicKey.findProgramAddress(
[depositReserve.info.lendingMarket.toBuffer()],
LENDING_PROGRAM_ID
);
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(
AccountLayout.span
);
const obligation = existingObligation
? existingObligation.pubkey
: createUninitializedObligation(
instructions,
wallet.publicKey,
await connection.getMinimumBalanceForRentExemption(LendingObligationLayout.span),
signers
);
instructions,
wallet.publicKey,
await connection.getMinimumBalanceForRentExemption(
LendingObligationLayout.span
),
signers
);
const obligationMint = existingObligation
? existingObligation.info.tokenMint
: createUninitializedMint(
instructions,
wallet.publicKey,
await connection.getMinimumBalanceForRentExemption(MintLayout.span),
signers
);
instructions,
wallet.publicKey,
await connection.getMinimumBalanceForRentExemption(MintLayout.span),
signers
);
const obligationTokenOutput = obligationAccount
? obligationAccount
: createUninitializedAccount(instructions, wallet.publicKey, accountRentExempt, signers);
: createUninitializedAccount(
instructions,
wallet.publicKey,
accountRentExempt,
signers
);
let toAccount = await findOrCreateAccountByMint(
wallet.publicKey,
@ -89,32 +107,57 @@ export const borrow = async (
signers
);
if (!obligationAccount) {
instructions.push(
initObligationInstruction(
depositReserve.pubkey,
borrowReserve.pubkey,
obligation,
obligationMint,
obligationTokenOutput,
wallet.publicKey,
depositReserve.info.lendingMarket,
authority
)
);
}
// Creates host fee account if it doesn't exsist
let hostFeeReceiver = LEND_HOST_FEE_ADDRESS
? findOrCreateAccountByMint(
wallet.publicKey,
LEND_HOST_FEE_ADDRESS,
instructions,
cleanupInstructions,
accountRentExempt,
depositReserve.info.collateralMint,
signers
)
: undefined;
if (instructions.length > 0) {
// create all accounts in one transaction
let tx = await sendTransaction(connection, wallet, instructions, [...signers]);
let tx = await sendTransaction(connection, wallet, instructions, [
...signers,
]);
notify({
message: 'Obligation accounts created',
message: "Obligation accounts created",
description: `Transaction ${tx}`,
type: 'success',
type: "success",
});
}
notify({
message: 'Borrowing funds...',
description: 'Please review transactions to approve.',
type: 'warn',
message: "Borrowing funds...",
description: "Please review transactions to approve.",
type: "warn",
});
signers = [];
instructions = [];
cleanupInstructions = [];
const [authority] = await PublicKey.findProgramAddress(
[depositReserve.info.lendingMarket.toBuffer()],
LENDING_PROGRAM_ID
);
let amountLamports: number = 0;
let fromLamports: number = 0;
if (amountType === BorrowAmountType.LiquidityBorrowAmount) {
@ -124,15 +167,19 @@ export const borrow = async (
fromLamports = approvedAmount - accountRentExempt;
const mint = (await cache.query(connection, borrowReserve.info.liquidityMint, MintParser)) as ParsedAccount<
MintInfo
>;
const mint = (await cache.query(
connection,
borrowReserve.info.liquidityMint,
MintParser
)) as ParsedAccount<MintInfo>;
amountLamports = toLamports(amount, mint?.info);
} else if (amountType === BorrowAmountType.CollateralDepositAmount) {
const mint = (await cache.query(connection, depositReserve.info.collateralMint, MintParser)) as ParsedAccount<
MintInfo
>;
const mint = (await cache.query(
connection,
depositReserve.info.collateralMint,
MintParser
)) as ParsedAccount<MintInfo>;
amountLamports = toLamports(amount, mint?.info);
fromLamports = amountLamports;
}
@ -165,27 +212,27 @@ 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)
const market = cache.get(depositReserve.info.lendingMarket) as ParsedAccount<
LendingMarket
>;
const dexOrderBookSide = market.info.quoteMint.equals(
depositReserve.info.liquidityMint
)
? dexMarket?.info.asks
: dexMarket?.info.bids;
const memory = createTempMemoryAccount(instructions, wallet.publicKey, signers, LENDING_PROGRAM_ID);
const memory = createTempMemoryAccount(
instructions,
wallet.publicKey,
signers,
LENDING_PROGRAM_ID
);
// Creates host fee account if it doesn't exsist
let hostFeeReceiver = LEND_HOST_FEE_ADDRESS
? findOrCreateAccountByMint(
wallet.publicKey,
LEND_HOST_FEE_ADDRESS,
instructions,
cleanupInstructions,
accountRentExempt,
depositReserve.info.collateralMint,
signers
)
: undefined;
instructions.push(
accrueInterestInstruction(depositReserve.pubkey, borrowReserve.pubkey)
);
// deposit
// borrow
instructions.push(
borrowInstruction(
amountLamports,
@ -202,7 +249,6 @@ export const borrow = async (
obligation,
obligationMint,
obligationTokenOutput,
wallet.publicKey,
depositReserve.info.lendingMarket,
authority,
@ -213,19 +259,25 @@ export const borrow = async (
memory,
hostFeeReceiver,
hostFeeReceiver
)
);
try {
let tx = await sendTransaction(connection, wallet, instructions.concat(cleanupInstructions), signers, true);
let tx = await sendTransaction(
connection,
wallet,
instructions.concat(cleanupInstructions),
signers,
true
);
notify({
message: 'Funds borrowed.',
type: 'success',
message: "Funds borrowed.",
type: "success",
description: `Transaction - ${tx}`,
});
} catch {
// TODO:
} catch (ex) {
console.error(ex);
throw new Error();
}
};

View File

@ -7,6 +7,7 @@ import {
import { sendTransaction } from "../contexts/connection";
import { notify } from "../utils/notifications";
import {
accrueInterestInstruction,
depositInstruction,
initReserveInstruction,
LendingReserve,
@ -29,9 +30,9 @@ export const deposit = async (
wallet: any
) => {
notify({
message: 'Depositing funds...',
description: 'Please review transactions to approve.',
type: 'warn',
message: "Depositing funds...",
description: "Please review transactions to approve.",
type: "warn",
});
const isInitalized = true; // TODO: finish reserve init
@ -41,7 +42,9 @@ export const deposit = async (
const instructions: TransactionInstruction[] = [];
const cleanupInstructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(AccountLayout.span);
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(
AccountLayout.span
);
const [authority] = await PublicKey.findProgramAddress(
[reserve.lendingMarket.toBuffer()], // which account should be authority
@ -81,10 +84,17 @@ export const deposit = async (
signers
);
} else {
toAccount = createUninitializedAccount(instructions, wallet.publicKey, accountRentExempt, signers);
toAccount = createUninitializedAccount(
instructions,
wallet.publicKey,
accountRentExempt,
signers
);
}
if (isInitalized) {
instructions.push(accrueInterestInstruction(reserveAddress));
// deposit
instructions.push(
depositInstruction(
@ -122,11 +132,17 @@ export const deposit = async (
}
try {
let tx = await sendTransaction(connection, wallet, instructions.concat(cleanupInstructions), signers, true);
let tx = await sendTransaction(
connection,
wallet,
instructions.concat(cleanupInstructions),
signers,
true
);
notify({
message: 'Funds deposited.',
type: 'success',
message: "Funds deposited.",
type: "success",
description: `Transaction - ${tx}`,
});
} catch {

View File

@ -6,12 +6,24 @@ import {
} from "@solana/web3.js";
import { sendTransaction } from "../contexts/connection";
import { notify } from "../utils/notifications";
import { LendingReserve } from "./../models/lending/reserve";
import {
accrueInterestInstruction,
LendingReserve,
} from "./../models/lending/reserve";
import { liquidateInstruction } from "./../models/lending/liquidate";
import { AccountLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID } from "../utils/ids";
import { createTempMemoryAccount, ensureSplAccount, findOrCreateAccountByMint } from "./account";
import { approve, LendingMarket, LendingObligation, TokenAccount } from "../models";
import {
createTempMemoryAccount,
ensureSplAccount,
findOrCreateAccountByMint,
} from "./account";
import {
approve,
LendingMarket,
LendingObligation,
TokenAccount,
} from "../models";
import { cache, ParsedAccount } from "../contexts/accounts";
export const liquidate = async (
@ -28,9 +40,9 @@ export const liquidate = async (
withdrawReserve: ParsedAccount<LendingReserve>
) => {
notify({
message: 'Repaying funds...',
description: 'Please review transactions to approve.',
type: 'warn',
message: "Repaying funds...",
description: "Please review transactions to approve.",
type: "warn",
});
// user from account
@ -38,7 +50,9 @@ export const liquidate = async (
const instructions: TransactionInstruction[] = [];
const cleanupInstructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(AccountLayout.span);
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(
AccountLayout.span
);
const [authority] = await PublicKey.findProgramAddress(
[repayReserve.info.lendingMarket.toBuffer()],
@ -84,13 +98,26 @@ export const liquidate = async (
throw new Error(`Dex market doesn't exist.`);
}
const market = cache.get(withdrawReserve.info.lendingMarket) as ParsedAccount<LendingMarket>;
const market = cache.get(withdrawReserve.info.lendingMarket) as ParsedAccount<
LendingMarket
>;
const dexOrderBookSide = market.info.quoteMint.equals(repayReserve.info.liquidityMint)
const dexOrderBookSide = market.info.quoteMint.equals(
repayReserve.info.liquidityMint
)
? dexMarket?.info.asks
: dexMarket?.info.bids;
const memory = createTempMemoryAccount(instructions, wallet.publicKey, signers, LENDING_PROGRAM_ID);
const memory = createTempMemoryAccount(
instructions,
wallet.publicKey,
signers,
LENDING_PROGRAM_ID
);
instructions.push(
accrueInterestInstruction(repayReserve.pubkey, withdrawReserve.pubkey)
);
instructions.push(
liquidateInstruction(
@ -111,11 +138,17 @@ export const liquidate = async (
)
);
let tx = await sendTransaction(connection, wallet, instructions.concat(cleanupInstructions), signers, true);
let tx = await sendTransaction(
connection,
wallet,
instructions.concat(cleanupInstructions),
signers,
true
);
notify({
message: 'Funds liquidated.',
type: 'success',
message: "Funds liquidated.",
type: "success",
description: `Transaction - ${tx}`,
});
};

View File

@ -6,7 +6,10 @@ import {
} from "@solana/web3.js";
import { sendTransaction } from "../contexts/connection";
import { notify } from "../utils/notifications";
import { LendingReserve } from "./../models/lending/reserve";
import {
accrueInterestInstruction,
LendingReserve,
} from "./../models/lending/reserve";
import { repayInstruction } from "./../models/lending/repay";
import { AccountLayout, Token, NATIVE_MINT } from "@solana/spl-token";
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../utils/ids";
@ -31,9 +34,9 @@ export const repay = async (
wallet: any
) => {
notify({
message: 'Repaying funds...',
description: 'Please review transactions to approve.',
type: 'warn',
message: "Repaying funds...",
description: "Please review transactions to approve.",
type: "warn",
});
// user from account
@ -41,7 +44,9 @@ export const repay = async (
const instructions: TransactionInstruction[] = [];
const cleanupInstructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(AccountLayout.span);
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(
AccountLayout.span
);
const [authority] = await PublicKey.findProgramAddress(
[repayReserve.info.lendingMarket.toBuffer()],
@ -49,9 +54,27 @@ export const repay = async (
);
let fromAccount = from.pubkey;
if (wallet.publicKey.equals(fromAccount) && repayReserve.info.liquidityMint.equals(NATIVE_MINT)) {
fromAccount = createTokenAccount(instructions, wallet.publicKey, accountRentExempt + repayAmount, NATIVE_MINT, wallet.publicKey, signers);
cleanupInstructions.push(Token.createCloseAccountInstruction(TOKEN_PROGRAM_ID, fromAccount, wallet.publicKey, wallet.publicKey, []));
if (
wallet.publicKey.equals(fromAccount) &&
repayReserve.info.liquidityMint.equals(NATIVE_MINT)
) {
fromAccount = createTokenAccount(
instructions,
wallet.publicKey,
accountRentExempt + repayAmount,
NATIVE_MINT,
wallet.publicKey,
signers
);
cleanupInstructions.push(
Token.createCloseAccountInstruction(
TOKEN_PROGRAM_ID,
fromAccount,
wallet.publicKey,
wallet.publicKey,
[]
)
);
}
// create approval for transfer transactions
@ -84,10 +107,12 @@ export const repay = async (
obligationToken.info.amount.toNumber(),
// reuse transfer authority
transferAuthority.publicKey,
transferAuthority.publicKey
);
// TODO: add obligation
instructions.push(
accrueInterestInstruction(repayReserve.pubkey, withdrawReserve.pubkey)
);
instructions.push(
repayInstruction(
@ -103,15 +128,21 @@ export const repay = async (
obligationToken.pubkey,
repayReserve.info.lendingMarket,
authority,
transferAuthority.publicKey,
transferAuthority.publicKey
)
);
let tx = await sendTransaction(connection, wallet, instructions.concat(cleanupInstructions), signers, true);
let tx = await sendTransaction(
connection,
wallet,
instructions.concat(cleanupInstructions),
signers,
true
);
notify({
message: 'Funds repaid.',
type: 'success',
message: "Funds repaid.",
type: "success",
description: `Transaction - ${tx}`,
});
};

View File

@ -6,7 +6,11 @@ import {
} from "@solana/web3.js";
import { sendTransaction } from "../contexts/connection";
import { notify } from "../utils/notifications";
import { LendingReserve, withdrawInstruction } from "./../models/lending";
import {
accrueInterestInstruction,
LendingReserve,
withdrawInstruction,
} from "./../models/lending";
import { AccountLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID } from "../utils/ids";
import { findOrCreateAccountByMint } from "./account";
@ -21,9 +25,9 @@ export const withdraw = async (
wallet: any
) => {
notify({
message: 'Withdrawing funds...',
description: 'Please review transactions to approve.',
type: 'warn',
message: "Withdrawing funds...",
description: "Please review transactions to approve.",
type: "warn",
});
// user from account
@ -31,19 +35,24 @@ export const withdraw = async (
const instructions: TransactionInstruction[] = [];
const cleanupInstructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(AccountLayout.span);
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(
AccountLayout.span
);
const [authority] = await PublicKey.findProgramAddress([reserve.lendingMarket.toBuffer()], LENDING_PROGRAM_ID);
const [authority] = await PublicKey.findProgramAddress(
[reserve.lendingMarket.toBuffer()],
LENDING_PROGRAM_ID
);
const fromAccount = from.pubkey;
// create approval for transfer transactions
const transferAuthority = approve(
instructions,
cleanupInstructions,
fromAccount,
wallet.publicKey,
amountLamports
instructions,
cleanupInstructions,
fromAccount,
wallet.publicKey,
amountLamports
);
signers.push(transferAuthority);
@ -59,6 +68,8 @@ export const withdraw = async (
signers
);
instructions.push(accrueInterestInstruction(reserveAddress));
instructions.push(
withdrawInstruction(
amountLamports,
@ -69,16 +80,22 @@ export const withdraw = async (
reserve.liquiditySupply,
reserve.lendingMarket,
authority,
transferAuthority.publicKey,
transferAuthority.publicKey
)
);
try {
let tx = await sendTransaction(connection, wallet, instructions.concat(cleanupInstructions), signers, true);
let tx = await sendTransaction(
connection,
wallet,
instructions.concat(cleanupInstructions),
signers,
true
);
notify({
message: 'Funds deposited.',
type: 'success',
message: "Funds deposited.",
type: "success",
description: `Transaction - ${tx}`,
});
} catch {

View File

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

View File

@ -157,7 +157,9 @@ export const BorrowInput = (props: {
loading={pendingTx}
disabled={fromAccounts.length === 0}
>
{fromAccounts.length === 0 ? LABELS.NO_DEPOSITS : LABELS.BORROW_ACTION}
{fromAccounts.length === 0
? LABELS.NO_DEPOSITS
: LABELS.BORROW_ACTION}
</ConnectButton>
<BackButton />
</div>

View File

@ -1,14 +1,18 @@
import React, { useEffect, useState } from 'react';
import { cache, ParsedAccount } from '../../contexts/accounts';
import { useConnectionConfig } from '../../contexts/connection';
import { useLendingReserves, useUserDeposits } from '../../hooks';
import { LendingReserve, LendingMarket, LendingReserveParser } from '../../models';
import { getTokenName } from '../../utils/utils';
import { Card, Select } from 'antd';
import { TokenIcon } from '../TokenIcon';
import { NumericInput } from '../Input/numeric';
import './style.less';
import { TokenDisplay } from '../TokenDisplay';
import React, { useEffect, useState } from "react";
import { cache, ParsedAccount } from "../../contexts/accounts";
import { useConnectionConfig } from "../../contexts/connection";
import { useLendingReserves, useUserDeposits } from "../../hooks";
import {
LendingReserve,
LendingMarket,
LendingReserveParser,
} from "../../models";
import { getTokenName } from "../../utils/utils";
import { Card, Select } from "antd";
import { TokenIcon } from "../TokenIcon";
import { NumericInput } from "../Input/numeric";
import "./style.less";
import { TokenDisplay } from "../TokenDisplay";
const { Option } = Select;
@ -30,36 +34,49 @@ export default function CollateralInput(props: {
const { tokenMap } = useConnectionConfig();
const [collateralReserve, setCollateralReserve] = useState<string>();
const [balance, setBalance] = useState<number>(0);
const [lastAmount, setLastAmount] = useState<string>('');
const [lastAmount, setLastAmount] = useState<string>("");
const userDeposits = useUserDeposits();
useEffect(() => {
const id: string = cache.byParser(LendingReserveParser).find((acc) => acc === collateralReserve) || '';
const id: string =
cache
.byParser(LendingReserveParser)
.find((acc) => acc === collateralReserve) || "";
const parser = cache.get(id) as ParsedAccount<LendingReserve>;
if (parser) {
const collateralDeposit = userDeposits.userDeposits.find(
(u) => u.reserve.info.liquidityMint.toBase58() === parser.info.liquidityMint.toBase58()
(u) =>
u.reserve.info.liquidityMint.toBase58() ===
parser.info.liquidityMint.toBase58()
);
if (collateralDeposit) setBalance(collateralDeposit.info.amount);
else setBalance(0);
}
}, [collateralReserve, userDeposits]);
const market = cache.get(props.reserve.lendingMarket) as ParsedAccount<LendingMarket>;
const market = cache.get(props.reserve.lendingMarket) as ParsedAccount<
LendingMarket
>;
if (!market) return null;
const onlyQuoteAllowed = !props.reserve?.liquidityMint?.equals(market?.info?.quoteMint);
const onlyQuoteAllowed = !props.reserve?.liquidityMint?.equals(
market?.info?.quoteMint
);
const renderReserveAccounts = reserveAccounts
.filter((reserve) => reserve.info !== props.reserve)
.filter((reserve) => !onlyQuoteAllowed || reserve.info.liquidityMint.equals(market.info.quoteMint))
.filter(
(reserve) =>
!onlyQuoteAllowed ||
reserve.info.liquidityMint.equals(market.info.quoteMint)
)
.map((reserve) => {
const mint = reserve.info.liquidityMint.toBase58();
const address = reserve.pubkey.toBase58();
const name = getTokenName(tokenMap, mint);
return (
<Option key={address} value={address} name={name} title={address}>
<div key={address} style={{ display: 'flex', alignItems: 'center' }}>
<div key={address} style={{ display: "flex", alignItems: "center" }}>
<TokenIcon mintAddress={mint} />
{name}
</div>
@ -68,19 +85,30 @@ export default function CollateralInput(props: {
});
return (
<Card className='ccy-input' style={{ borderRadius: 20 }} bodyStyle={{ padding: 0 }}>
<div className='ccy-input-header'>
<div className='ccy-input-header-left'>{props.title}</div>
<Card
className="ccy-input"
style={{ borderRadius: 20 }}
bodyStyle={{ padding: 0 }}
>
<div className="ccy-input-header">
<div className="ccy-input-header-left">{props.title}</div>
{!props.hideBalance && (
<div className='ccy-input-header-right' onClick={(e) => props.onInputChange && props.onInputChange(balance)}>
<div
className="ccy-input-header-right"
onClick={(e) => props.onInputChange && props.onInputChange(balance)}
>
Balance: {balance.toFixed(6)}
</div>
)}
</div>
<div className='ccy-input-header' style={{ padding: '0px 10px 5px 7px' }}>
<div className="ccy-input-header" style={{ padding: "0px 10px 5px 7px" }}>
<NumericInput
value={parseFloat(lastAmount || '0.00') === props.amount ? lastAmount : props.amount?.toFixed(6)?.toString()}
value={
parseFloat(lastAmount || "0.00") === props.amount
? lastAmount
: props.amount?.toFixed(6)?.toString()
}
onChange={(val: string) => {
if (props.onInputChange && parseFloat(val) !== props.amount) {
if (!val || !parseFloat(val)) props.onInputChange(null);
@ -90,19 +118,19 @@ export default function CollateralInput(props: {
}}
style={{
fontSize: 20,
boxShadow: 'none',
borderColor: 'transparent',
outline: 'transparent',
boxShadow: "none",
borderColor: "transparent",
outline: "transparent",
}}
placeholder='0.00'
placeholder="0.00"
/>
<div className='ccy-input-header-right' style={{ display: 'flex' }}>
<div className="ccy-input-header-right" style={{ display: "flex" }}>
{props.showLeverageSelector && (
<Select
size='large'
size="large"
showSearch
style={{ width: 80 }}
placeholder='CCY'
placeholder="CCY"
value={props.leverage}
onChange={(item: number) => {
if (props.onLeverage) props.onLeverage(item);
@ -113,12 +141,22 @@ export default function CollateralInput(props: {
props.onLeverage(parseFloat(item));
}
}}
filterOption={(input, option) => option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0}
filterOption={(input, option) =>
option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{[1, 2, 3, 4, 5].map((val) => (
<Option key={val} value={val} name={val + 'x'} title={val + 'x'}>
<div key={val} style={{ display: 'flex', alignItems: 'center' }}>
{val + 'x'}
<Option
key={val}
value={val}
name={val + "x"}
title={val + "x"}
>
<div
key={val}
style={{ display: "flex", alignItems: "center" }}
>
{val + "x"}
</div>
</Option>
))}
@ -126,23 +164,28 @@ export default function CollateralInput(props: {
)}
{!props.disabled ? (
<Select
size='large'
size="large"
showSearch
style={{ minWidth: 150 }}
placeholder='CCY'
placeholder="CCY"
value={collateralReserve}
onChange={(item) => {
if (props.onCollateralReserve) props.onCollateralReserve(item);
setCollateralReserve(item);
}}
filterOption={(input, option) => option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0}
filterOption={(input, option) =>
option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{renderReserveAccounts}
</Select>
) : (
<TokenDisplay
key={props.reserve.liquidityMint.toBase58()}
name={getTokenName(tokenMap, props.reserve.liquidityMint.toBase58())}
name={getTokenName(
tokenMap,
props.reserve.liquidityMint.toBase58()
)}
mintAddress={props.reserve.liquidityMint.toBase58()}
showBalance={false}
/>

View File

@ -15,29 +15,20 @@ export const CollateralItem = (props: {
userDeposit?: UserDeposit;
name: string;
}) => {
const {
mint,
name,
userDeposit,
} = props;
const { mint, name, userDeposit } = props;
return (
<>
<div
style={{ display: "flex", alignItems: "center" }}
>
<div style={{ display: "flex", alignItems: "center" }}>
<TokenIcon mintAddress={mint} />
{name}
<span
className="token-balance"
>
&nbsp;{" "}
{userDeposit ? formatAmount(userDeposit.info.amount) : '--'}
<span className="token-balance">
&nbsp; {userDeposit ? formatAmount(userDeposit.info.amount) : "--"}
</span>
</div>
</>
);
}
};
export const CollateralSelector = (props: {
reserve: LendingReserve;
@ -56,15 +47,15 @@ export const CollateralSelector = (props: {
const quoteMintAddress = market?.info?.quoteMint?.toBase58();
const onlyQuoteAllowed = props.reserve?.liquidityMint?.toBase58() !==
quoteMintAddress;
const onlyQuoteAllowed =
props.reserve?.liquidityMint?.toBase58() !== quoteMintAddress;
return (
<Select
size='large'
size="large"
showSearch
style={{ minWidth: 300, margin: '5px 0px' }}
placeholder='Collateral'
style={{ minWidth: 300, margin: "5px 0px" }}
placeholder="Collateral"
value={props.collateralReserve}
disabled={props.disabled}
defaultValue={props.collateralReserve}
@ -73,23 +64,34 @@ export const CollateralSelector = (props: {
props.onCollateralReserve(item);
}
}}
filterOption={(input, option) => option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0}
filterOption={(input, option) =>
option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{reserveAccounts
.filter((reserve) => reserve.info !== props.reserve)
.filter((reserve) => !onlyQuoteAllowed || reserve.info.liquidityMint.equals(market.info.quoteMint))
.filter(
(reserve) =>
!onlyQuoteAllowed ||
reserve.info.liquidityMint.equals(market.info.quoteMint)
)
.map((reserve) => {
const mint = reserve.info.liquidityMint.toBase58();
const address = reserve.pubkey.toBase58();
const name = getTokenName(tokenMap, mint);
return <Option key={address} value={address} name={name} title={address}>
<CollateralItem
reserve={reserve}
userDeposit={userDeposits.find(dep => dep.reserve.pubkey.toBase58() === address)}
mint={mint}
name={name} />
</Option>
return (
<Option key={address} value={address} name={name} title={address}>
<CollateralItem
reserve={reserve}
userDeposit={userDeposits.find(
(dep) => dep.reserve.pubkey.toBase58() === address
)}
mint={mint}
name={name}
/>
</Option>
);
})}
</Select>
);

View File

@ -10,7 +10,11 @@ export const ConnectButton = (
const { wallet, connected } = useWallet();
const { onClick, children, disabled, ...rest } = props;
return (
<Button {...rest} onClick={connected ? onClick : wallet.connect} disabled={connected && disabled}>
<Button
{...rest}
onClick={connected ? onClick : wallet.connect}
disabled={connected && disabled}
>
{connected ? props.children : LABELS.CONNECT_LABEL}
</Button>
);

View File

@ -1,11 +1,11 @@
import React from 'react';
import { Input } from 'antd';
import React from "react";
import { Input } from "antd";
export class NumericInput extends React.Component<any, any> {
onChange = (e: any) => {
const { value } = e.target;
const reg = /^-?\d*(\.\d*)?$/;
if (reg.test(value) || value === '' || value === '-') {
if (reg.test(value) || value === "" || value === "-") {
this.props.onChange(value);
}
};
@ -15,19 +15,29 @@ export class NumericInput extends React.Component<any, any> {
const { value, onBlur, onChange } = this.props;
let valueTemp = value;
if (value === undefined || value === null) return;
if (value.charAt && (value.charAt(value.length - 1) === '.' || value === '-')) {
if (
value.charAt &&
(value.charAt(value.length - 1) === "." || value === "-")
) {
valueTemp = value.slice(0, -1);
}
if (value.startsWith && (value.startsWith('.') || value.startsWith('-.'))) {
valueTemp = valueTemp.replace('.', '0.');
if (value.startsWith && (value.startsWith(".") || value.startsWith("-."))) {
valueTemp = valueTemp.replace(".", "0.");
}
if (valueTemp.replace) onChange?.(valueTemp.replace(/0*(\d+)/, '$1'));
if (valueTemp.replace) onChange?.(valueTemp.replace(/0*(\d+)/, "$1"));
if (onBlur) {
onBlur();
}
};
render() {
return <Input {...this.props} onChange={this.onChange} onBlur={this.onBlur} maxLength={25} />;
return (
<Input
{...this.props}
onChange={this.onChange}
onBlur={this.onBlur}
maxLength={25}
/>
);
}
}

View File

@ -9,7 +9,7 @@ import {
ShoppingOutlined,
HomeOutlined,
RocketOutlined,
ForkOutlined
ForkOutlined,
// LineChartOutlined
} from "@ant-design/icons";
@ -18,7 +18,7 @@ import { AppBar } from "./../AppBar";
import { Link, useLocation } from "react-router-dom";
import { useConnectionConfig } from "../../contexts/connection";
import { LABELS } from "../../constants";
import config from './../../../package.json';
import config from "./../../../package.json";
export const AppLayout = React.memo((props: any) => {
const { env } = useConnectionConfig();
@ -37,8 +37,7 @@ export const AppLayout = React.memo((props: any) => {
[...Object.keys(paths)].find((key) => location.pathname.startsWith(key)) ||
"";
const defaultKey = paths[current] || "1";
const theme = 'light';
const theme = "light";
return (
<div className="App">
@ -47,7 +46,11 @@ export const AppLayout = React.memo((props: any) => {
</div>
<BasicLayout
title={LABELS.APP_TITLE}
footerRender={() => <div className="footer" title={LABELS.FOOTER}>{LABELS.FOOTER}</div>}
footerRender={() => (
<div className="footer" title={LABELS.FOOTER}>
{LABELS.FOOTER}
</div>
)}
navTheme={theme}
headerTheme={theme}
theme={theme}
@ -56,59 +59,62 @@ export const AppLayout = React.memo((props: any) => {
primaryColor="#d83aeb"
logo={<div className="App-logo" />}
rightContentRender={() => <AppBar />}
links={[
]}
links={[]}
menuContentRender={() => {
return (<div className="links">
<Menu theme={theme} defaultSelectedKeys={[defaultKey]} mode="inline">
<Menu.Item key="1" icon={<HomeOutlined />}>
<Link
to={{
pathname: "/",
}}
>
{LABELS.MENU_HOME}
</Link>
</Menu.Item>
<Menu.Item key="2" icon={<PieChartOutlined />}>
<Link
to={{
pathname: "/dashboard",
}}
>
{LABELS.MENU_DASHBOARD}
</Link>
</Menu.Item>
<Menu.Item key="3" icon={<BankOutlined />}>
<Link
to={{
pathname: "/deposit",
}}
>
{LABELS.MENU_DEPOSIT}
</Link>
</Menu.Item>
<Menu.Item key="4" icon={<LogoutOutlined />}>
<Link
to={{
pathname: "/borrow",
}}
>
{LABELS.MENU_BORROW}
</Link>
</Menu.Item>
<Menu.Item key="5" icon={<ShoppingOutlined />}>
<Link
to={{
pathname: "/liquidate",
}}
>
{LABELS.MENU_LIQUIDATE}
</Link>
</Menu.Item>
{/* Hide margin option for now */}
{/* <Menu.Item key="6" onItemHover={() => {}} icon={< LineChartOutlined/>}>
return (
<div className="links">
<Menu
theme={theme}
defaultSelectedKeys={[defaultKey]}
mode="inline"
>
<Menu.Item key="1" icon={<HomeOutlined />}>
<Link
to={{
pathname: "/",
}}
>
{LABELS.MENU_HOME}
</Link>
</Menu.Item>
<Menu.Item key="2" icon={<PieChartOutlined />}>
<Link
to={{
pathname: "/dashboard",
}}
>
{LABELS.MENU_DASHBOARD}
</Link>
</Menu.Item>
<Menu.Item key="3" icon={<BankOutlined />}>
<Link
to={{
pathname: "/deposit",
}}
>
{LABELS.MENU_DEPOSIT}
</Link>
</Menu.Item>
<Menu.Item key="4" icon={<LogoutOutlined />}>
<Link
to={{
pathname: "/borrow",
}}
>
{LABELS.MENU_BORROW}
</Link>
</Menu.Item>
<Menu.Item key="5" icon={<ShoppingOutlined />}>
<Link
to={{
pathname: "/liquidate",
}}
>
{LABELS.MENU_LIQUIDATE}
</Link>
</Menu.Item>
{/* Hide margin option for now */}
{/* <Menu.Item key="6" onItemHover={() => {}} icon={< LineChartOutlined/>}>
<Link
to={{
pathname: "/margin",
@ -117,31 +123,48 @@ export const AppLayout = React.memo((props: any) => {
{LABELS.MARGIN_TRADING}
</Link>
</Menu.Item> */}
{env !== "mainnet-beta" && (
<Menu.Item key="7" icon={<RocketOutlined />}>
<Link
to={{
pathname: "/faucet",
}}
{env !== "mainnet-beta" && (
<Menu.Item key="7" icon={<RocketOutlined />}>
<Link
to={{
pathname: "/faucet",
}}
>
{LABELS.MENU_FAUCET}
</Link>
</Menu.Item>
)}
</Menu>
<Menu
theme={theme}
defaultSelectedKeys={[defaultKey]}
selectable={false}
mode="inline"
className="bottom-links"
>
<Menu.Item key="16" icon={<ForkOutlined />}>
<a
title="Fork"
href={`${config.repository.url}/fork`}
target="_blank"
rel="noopener noreferrer"
>
{LABELS.MENU_FAUCET}
</Link>
Fork
</a>
</Menu.Item>
)}
</Menu>
<Menu theme={theme} defaultSelectedKeys={[defaultKey]} selectable={false} mode="inline" className="bottom-links">
<Menu.Item key="16" icon={<ForkOutlined />}>
<a title="Fork" href={`${config.repository.url}/fork`} target="_blank" rel="noopener noreferrer" >
Fork
</a>
</Menu.Item>,
,
<Menu.Item key="15" icon={<GithubOutlined />}>
<a title="Gtihub" href={config.repository.url} target="_blank" rel="noopener noreferrer" >
<a
title="Gtihub"
href={config.repository.url}
target="_blank"
rel="noopener noreferrer"
>
Github
</a>
</Menu.Item>
</Menu>
</div>
</div>
);
}}
>

View File

@ -48,7 +48,7 @@ export const LiquidateInput = (props: {
wadToLamports(obligation.info.borrowAmountWad).toNumber(),
obligation.account,
repayReserve,
withdrawReserve,
withdrawReserve
);
setShowConfirmation(true);
@ -58,7 +58,14 @@ export const LiquidateInput = (props: {
setPendingTx(false);
}
})();
}, [withdrawReserve, fromAccounts, obligation, repayReserve, wallet, connection]);
}, [
withdrawReserve,
fromAccounts,
obligation,
repayReserve,
wallet,
connection,
]);
const bodyStyle: React.CSSProperties = {
display: "flex",
@ -73,27 +80,32 @@ export const LiquidateInput = (props: {
{showConfirmation ? (
<ActionConfirmation onClose={() => setShowConfirmation(false)} />
) : (
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-around",
}}
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-around",
}}
>
<div className="liquidate-input-title">
{LABELS.SELECT_COLLATERAL}
</div>
<CollateralSelector
reserve={repayReserve.info}
collateralReserve={withdrawReserve?.pubkey.toBase58()}
disabled={true}
/>
<Button
type="primary"
onClick={onLiquidate}
disabled={fromAccounts.length === 0}
loading={pendingTx}
>
<div className="liquidate-input-title">{LABELS.SELECT_COLLATERAL}</div>
<CollateralSelector
reserve={repayReserve.info}
collateralReserve={withdrawReserve?.pubkey.toBase58()}
disabled={true}
/>
<Button type="primary"
onClick={onLiquidate}
disabled={fromAccounts.length === 0}
loading={pendingTx}>
{LABELS.LIQUIDATE_ACTION}
</Button>
<BackButton />
</div>)}
{LABELS.LIQUIDATE_ACTION}
</Button>
<BackButton />
</div>
)}
</Card>
);
};

View File

@ -1,14 +1,16 @@
import { Card, Row, Col } from 'antd';
import React, { useMemo } from 'react';
import { useMint } from '../../contexts/accounts';
import { useEnrichedPools } from '../../contexts/market';
import { useUserAccounts } from '../../hooks';
import { PoolInfo } from '../../models';
import { formatPriceNumber } from '../../utils/utils';
import { Card, Row, Col } from "antd";
import React, { useMemo } from "react";
import { useMint } from "../../contexts/accounts";
import { useEnrichedPools } from "../../contexts/market";
import { useUserAccounts } from "../../hooks";
import { PoolInfo } from "../../models";
import { formatPriceNumber } from "../../utils/utils";
export const PoolPrice = (props: { pool: PoolInfo }) => {
const pool = props.pool;
const pools = useMemo(() => [props.pool].filter((p) => p) as PoolInfo[], [props.pool]);
const pools = useMemo(() => [props.pool].filter((p) => p) as PoolInfo[], [
props.pool,
]);
const enriched = useEnrichedPools(pools)[0];
const { userAccounts } = useUserAccounts();
@ -17,32 +19,37 @@ export const PoolPrice = (props: { pool: PoolInfo }) => {
const ratio =
userAccounts
.filter((f) => pool.pubkeys.mint.equals(f.info.mint))
.reduce((acc, item) => item.info.amount.toNumber() + acc, 0) / (lpMint?.supply.toNumber() || 0);
.reduce((acc, item) => item.info.amount.toNumber() + acc, 0) /
(lpMint?.supply.toNumber() || 0);
if (!enriched) {
return null;
}
return (
<Card
className='ccy-input'
style={{ borderRadius: 20, width: '100%' }}
bodyStyle={{ padding: '7px' }}
size='small'
title='Prices and pool share'
className="ccy-input"
style={{ borderRadius: 20, width: "100%" }}
bodyStyle={{ padding: "7px" }}
size="small"
title="Prices and pool share"
>
<Row style={{ width: '100%' }}>
<Row style={{ width: "100%" }}>
<Col span={8}>
{formatPriceNumber.format(parseFloat(enriched.liquidityA) / parseFloat(enriched.liquidityB))}
{formatPriceNumber.format(
parseFloat(enriched.liquidityA) / parseFloat(enriched.liquidityB)
)}
</Col>
<Col span={8}>
{formatPriceNumber.format(parseFloat(enriched.liquidityB) / parseFloat(enriched.liquidityA))}
{formatPriceNumber.format(
parseFloat(enriched.liquidityB) / parseFloat(enriched.liquidityA)
)}
</Col>
<Col span={8}>
{ratio * 100 < 0.001 && ratio > 0 ? '<' : ''}
{ratio * 100 < 0.001 && ratio > 0 ? "<" : ""}
&nbsp;{formatPriceNumber.format(ratio * 100)}%
</Col>
</Row>
<Row style={{ width: '100%' }}>
<Row style={{ width: "100%" }}>
<Col span={8}>
{enriched.names[0]} per {enriched.names[1]}
</Col>

View File

@ -44,7 +44,7 @@ export const ReserveStatus = (props: {
const totalBorrowsInUSD = price * totalBorrows;
const depositAPY = useMemo(() => calculateDepositAPY(props.reserve), [
props.reserve
props.reserve,
]);
const liquidationThreshold = props.reserve.config.liquidationThreshold;
@ -54,10 +54,21 @@ export const ReserveStatus = (props: {
return (
<Card
className={props.className}
title={<>
<TokenIcon style={{ marginRight: 0, marginTop: 0, position: 'absolute', left: 15 }}
mintAddress={mintAddress} size={30} />
{LABELS.RESERVE_STATUS_TITLE}</>}
title={
<>
<TokenIcon
style={{
marginRight: 0,
marginTop: 0,
position: "absolute",
left: 15,
}}
mintAddress={mintAddress}
size={30}
/>
{LABELS.RESERVE_STATUS_TITLE}
</>
}
bodyStyle={bodyStyle}
>
<div className="flexColumn">
@ -66,21 +77,31 @@ export const ReserveStatus = (props: {
<Statistic
title="Available Liquidity"
value={availableLiquidity}
valueRender={(node) => <div>
{node}
<div className="dashboard-amount-quote-stat">${formatNumber.format(availableLiquidityInUSD)}</div>
</div>}
precision={2} />
valueRender={(node) => (
<div>
{node}
<div className="dashboard-amount-quote-stat">
${formatNumber.format(availableLiquidityInUSD)}
</div>
</div>
)}
precision={2}
/>
</Col>
<Col span={12}>
<Statistic
title="Total Borrowed"
value={totalBorrows}
valueRender={(node) => <div>
{node}
<div className="dashboard-amount-quote-stat">${formatNumber.format(totalBorrowsInUSD)}</div>
</div>}
precision={2} />
valueRender={(node) => (
<div>
{node}
<div className="dashboard-amount-quote-stat">
${formatNumber.format(totalBorrowsInUSD)}
</div>
</div>
)}
precision={2}
/>
</Col>
</Row>
<Row gutter={GUTTER}>
@ -102,7 +123,8 @@ export const ReserveStatus = (props: {
className="small-statisitc"
value={maxLTV}
precision={2}
suffix="%" />
suffix="%"
/>
</Col>
<Col span={6}>
<Statistic
@ -110,7 +132,8 @@ export const ReserveStatus = (props: {
className="small-statisitc"
value={liquidationThreshold}
precision={2}
suffix="%" />
suffix="%"
/>
</Col>
<Col span={6}>
<Statistic
@ -118,7 +141,8 @@ export const ReserveStatus = (props: {
className="small-statisitc"
value={liquidationPenalty}
precision={2}
suffix="%" />
suffix="%"
/>
</Col>
<Col span={6}>
<Statistic
@ -126,7 +150,8 @@ export const ReserveStatus = (props: {
className="small-statisitc"
value={depositAPY * 100}
precision={2}
suffix="%" />
suffix="%"
/>
</Col>
</Row>
</div>

View File

@ -23,7 +23,7 @@ export const ReserveUtilizationChart = (props: { reserve: LendingReserve }) => {
);
const totalSupply = availableLiquidity + totalBorrows;
const percent = 100 * totalBorrows / totalSupply;
const percent = (100 * totalBorrows) / totalSupply;
return (
<WaterWave
@ -31,10 +31,11 @@ export const ReserveUtilizationChart = (props: { reserve: LendingReserve }) => {
showPercent={false}
title={
<Statistic
title="Utilization"
value={percent}
suffix="%"
precision={2} />
title="Utilization"
value={percent}
suffix="%"
precision={2}
/>
}
percent={percent}
/>

View File

@ -1,8 +1,8 @@
import React, { useEffect, useMemo, useRef } from 'react';
import { PoolInfo } from '../../models';
import echarts from 'echarts';
import { formatNumber, formatUSD } from '../../utils/utils';
import { useEnrichedPools } from '../../contexts/market';
import React, { useEffect, useMemo, useRef } from "react";
import { PoolInfo } from "../../models";
import echarts from "echarts";
import { formatNumber, formatUSD } from "../../utils/utils";
import { useEnrichedPools } from "../../contexts/market";
export const SupplyOverview = (props: { pool?: PoolInfo }) => {
const { pool } = props;
@ -44,7 +44,7 @@ export const SupplyOverview = (props: { pool?: PoolInfo }) => {
instance.setOption({
tooltip: {
trigger: 'item',
trigger: "item",
formatter: function (params: any) {
var val = formatUSD.format(params.value);
var tokenAmount = formatNumber.format(params.data.tokens);
@ -53,8 +53,8 @@ export const SupplyOverview = (props: { pool?: PoolInfo }) => {
},
series: [
{
name: 'Liquidity',
type: 'pie',
name: "Liquidity",
type: "pie",
top: 0,
bottom: 0,
left: 0,
@ -70,20 +70,20 @@ export const SupplyOverview = (props: { pool?: PoolInfo }) => {
},
rich: {
c: {
color: 'black',
color: "black",
lineHeight: 22,
align: 'center',
align: "center",
},
r: {
color: 'black',
align: 'right',
color: "black",
align: "right",
},
},
color: 'rgba(255, 255, 255, 0.5)',
color: "rgba(255, 255, 255, 0.5)",
},
itemStyle: {
normal: {
borderColor: '#000',
borderColor: "#000",
},
},
data,
@ -96,5 +96,5 @@ export const SupplyOverview = (props: { pool?: PoolInfo }) => {
return null;
}
return <div ref={chartDiv} style={{ height: 150, width: '100%' }} />;
return <div ref={chartDiv} style={{ height: 150, width: "100%" }} />;
};

View File

@ -1,6 +1,6 @@
import React from 'react';
import { useMint, useAccountByMint } from '../../contexts/accounts';
import { TokenIcon } from '../TokenIcon';
import React from "react";
import { useMint, useAccountByMint } from "../../contexts/accounts";
import { TokenIcon } from "../TokenIcon";
export const TokenDisplay = (props: {
name: string;
@ -16,7 +16,8 @@ export const TokenDisplay = (props: {
let hasBalance: boolean = false;
if (showBalance) {
if (tokenAccount && tokenMint) {
balance = tokenAccount.info.amount.toNumber() / Math.pow(10, tokenMint.decimals);
balance =
tokenAccount.info.amount.toNumber() / Math.pow(10, tokenMint.decimals);
hasBalance = balance > 0;
}
}
@ -27,18 +28,27 @@ export const TokenDisplay = (props: {
title={mintAddress}
key={mintAddress}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div style={{ display: "flex", alignItems: "center" }}>
{icon || <TokenIcon mintAddress={mintAddress} />}
{name}
</div>
{showBalance ? (
<span title={balance.toString()} key={mintAddress} className='token-balance'>
&nbsp; {hasBalance ? (balance < 0.001 ? '<0.001' : balance.toFixed(3)) : '-'}
<span
title={balance.toString()}
key={mintAddress}
className="token-balance"
>
&nbsp;{" "}
{hasBalance
? balance < 0.001
? "<0.001"
: balance.toFixed(3)
: "-"}
</span>
) : null}
</div>

View File

@ -25,13 +25,25 @@ export const UserLendingCard = (props: {
const name = useTokenName(reserve?.liquidityMint);
const { balance: tokenBalance, balanceInUSD: tokenBalanceInUSD } = useUserBalance(props.reserve.liquidityMint);
const { balance: collateralBalance, balanceInUSD: collateralBalanceInUSD } = useUserCollateralBalance(
props.reserve
);
const {
balance: tokenBalance,
balanceInUSD: tokenBalanceInUSD,
} = useUserBalance(props.reserve.liquidityMint);
const {
balance: collateralBalance,
balanceInUSD: collateralBalanceInUSD,
} = useUserCollateralBalance(props.reserve);
const { borrowed: totalBorrowed, borrowedInUSD, ltv, health } = useBorrowedAmount(address);
const { totalInQuote: borrowingPowerInUSD, borrowingPower } = useBorrowingPower(address);
const {
borrowed: totalBorrowed,
borrowedInUSD,
ltv,
health,
} = useBorrowedAmount(address);
const {
totalInQuote: borrowingPowerInUSD,
borrowingPower,
} = useBorrowingPower(address);
return (
<Card
@ -57,8 +69,12 @@ export const UserLendingCard = (props: {
</Text>
<div className="card-cell ">
<div>
<div><em>{formatNumber.format(totalBorrowed)}</em> {name}</div>
<div className="dashboard-amount-quote">${formatNumber.format(borrowedInUSD)}</div>
<div>
<em>{formatNumber.format(totalBorrowed)}</em> {name}
</div>
<div className="dashboard-amount-quote">
${formatNumber.format(borrowedInUSD)}
</div>
</div>
</div>
</div>
@ -83,8 +99,12 @@ export const UserLendingCard = (props: {
</Text>
<div className="card-cell ">
<div>
<div><em>{formatNumber.format(borrowingPower)}</em> {name}</div>
<div className="dashboard-amount-quote">${formatNumber.format(borrowingPowerInUSD)}</div>
<div>
<em>{formatNumber.format(borrowingPower)}</em> {name}
</div>
<div className="dashboard-amount-quote">
${formatNumber.format(borrowingPowerInUSD)}
</div>
</div>
</div>
</div>
@ -97,8 +117,12 @@ export const UserLendingCard = (props: {
</Text>
<div className="card-cell ">
<div>
<div><em>{formatNumber.format(tokenBalance)}</em> {name}</div>
<div className="dashboard-amount-quote">${formatNumber.format(tokenBalanceInUSD)}</div>
<div>
<em>{formatNumber.format(tokenBalance)}</em> {name}
</div>
<div className="dashboard-amount-quote">
${formatNumber.format(tokenBalanceInUSD)}
</div>
</div>
</div>
</div>
@ -109,8 +133,12 @@ export const UserLendingCard = (props: {
</Text>
<div className="card-cell ">
<div>
<div><em>{formatNumber.format(collateralBalance)}</em> {name}</div>
<div className="dashboard-amount-quote">${formatNumber.format(collateralBalanceInUSD)}</div>
<div>
<em>{formatNumber.format(collateralBalance)}</em> {name}
</div>
<div className="dashboard-amount-quote">
${formatNumber.format(collateralBalanceInUSD)}
</div>
</div>
</div>
</div>

View File

@ -59,12 +59,15 @@ export const WithdrawInput = (props: {
(async () => {
try {
const withdrawAmount = Math.min(type === InputType.Percent
const withdrawAmount = Math.min(
type === InputType.Percent
? (pct * collateralBalanceLamports) / 100
: Math.ceil(
collateralBalanceLamports *
(parseFloat(value) / collateralBalanceInLiquidity)
), collateralBalanceLamports);
),
collateralBalanceLamports
);
await withdraw(
fromAccounts[0],
withdrawAmount,
@ -142,7 +145,9 @@ export const WithdrawInput = (props: {
loading={pendingTx}
disabled={fromAccounts.length === 0}
>
{fromAccounts.length === 0 ? LABELS.NO_DEPOSITS : LABELS.WITHDRAW_ACTION}
{fromAccounts.length === 0
? LABELS.NO_DEPOSITS
: LABELS.WITHDRAW_ACTION}
</ConnectButton>
</div>
)}

View File

@ -4,7 +4,8 @@ export const LABELS = {
BORROWING_POWER_VALUE: "Borrowing Power",
BORROWED_VALUE: "You borrowed",
GIVE_SOL: "Give me SOL",
LIQUIDATION_INFO: "This view displays all loans that can be liquidated. A liquidation is a process where borrower collateral does not cover value of the loan. It is represented by health factor falling below 1.0. When a loan is liquidated, an liquidator can purchase collateral at a discount by repaing the portio of the loan. ",
LIQUIDATION_INFO:
"This view displays all loans that can be liquidated. A liquidation is a process where borrower collateral does not cover value of the loan. It is represented by health factor falling below 1.0. When a loan is liquidated, an liquidator can purchase collateral at a discount by repaing the portio of the loan. ",
FAUCET_INFO:
"This faucet will help you fund your accounts outside of Solana main network.",
ACCOUNT_FUNDED: "Account funded.",
@ -12,7 +13,7 @@ export const LABELS = {
REPAY_ACTION: "Repay",
RESERVE_STATUS_TITLE: "Reserve Status & Configuration",
AUDIT_WARNING:
'Oyster is an unaudited software project used for internal purposes at the Solana Foundation. This app is not for public use.',
"Oyster is an unaudited software project used for internal purposes at the Solana Foundation. This app is not for public use.",
FOOTER:
'This page was produced by the Solana Foundation ("SF") for internal educational and inspiration purposes only. SF does not encourage, induce or sanction the deployment, integration or use of Oyster or any similar application (including its code) in violation of applicable laws or regulations and hereby prohibits any such deployment, integration or use. Anyone using this code or a derivation thereof must comply with applicable laws and regulations when releasing related software.',
MENU_HOME: "Home",
@ -26,7 +27,7 @@ export const LABELS = {
MENU_BORROW: "Borrow",
MENU_LIQUIDATE: "Liquidate",
MENU_FAUCET: "Faucet",
MARGIN_TRADING: 'Margin Trading',
MARGIN_TRADING: "Margin Trading",
APP_TITLE: "Oyster Lending",
CONNECT_BUTTON: "Connect",
WALLET_TOOLTIP: "Wallet public key",
@ -65,21 +66,26 @@ export const LABELS = {
GO_BACK_ACTION: "Go back",
DEPOSIT_ACTION: "Deposit",
TOTAL_TITLE: "Total",
TRADING_TABLE_TITLE_MY_COLLATERAL: 'Chosen Collateral',
TRADING_TABLE_TITLE_DESIRED_ASSET: 'Desired Asset',
TRADING_TABLE_TITLE_MULTIPLIER: 'Leverage',
TRADING_TABLE_TITLE_ASSET_PRICE: 'Asset Price',
TRADING_TABLE_TITLE_LIQUIDATION_PRICE: 'Liquidation Price',
TRADING_TABLE_TITLE_APY: 'APY',
TRADING_TABLE_TITLE_ACTIONS: 'Action',
TRADING_ADD_POSITION: 'Add Position',
MARGIN_TRADE_ACTION: 'Margin Trade',
MARGIN_TRADE_CHOOSE_COLLATERAL_AND_LEVERAGE: 'Please choose your collateral and leverage.',
MARGIN_TRADE_QUESTION: 'Please choose how much of this asset you wish to purchase.',
TABLE_TITLE_BUYING_POWER: 'Total Buying Power',
NOT_ENOUGH_MARGIN_MESSAGE: 'Not enough buying power in oyster to make this trade at this leverage.',
SET_MORE_MARGIN_MESSAGE: 'You need more margin to match this leverage amount to make this trade.',
LEVERAGE_LIMIT_MESSAGE: 'You will need more margin to make this trade.',
NO_DEPOSIT_MESSAGE: 'You need to deposit coin of this type into oyster before trading with it on margin.',
NO_COLL_TYPE_MESSAGE: 'Choose Collateral CCY',
TRADING_TABLE_TITLE_MY_COLLATERAL: "Chosen Collateral",
TRADING_TABLE_TITLE_DESIRED_ASSET: "Desired Asset",
TRADING_TABLE_TITLE_MULTIPLIER: "Leverage",
TRADING_TABLE_TITLE_ASSET_PRICE: "Asset Price",
TRADING_TABLE_TITLE_LIQUIDATION_PRICE: "Liquidation Price",
TRADING_TABLE_TITLE_APY: "APY",
TRADING_TABLE_TITLE_ACTIONS: "Action",
TRADING_ADD_POSITION: "Add Position",
MARGIN_TRADE_ACTION: "Margin Trade",
MARGIN_TRADE_CHOOSE_COLLATERAL_AND_LEVERAGE:
"Please choose your collateral and leverage.",
MARGIN_TRADE_QUESTION:
"Please choose how much of this asset you wish to purchase.",
TABLE_TITLE_BUYING_POWER: "Total Buying Power",
NOT_ENOUGH_MARGIN_MESSAGE:
"Not enough buying power in oyster to make this trade at this leverage.",
SET_MORE_MARGIN_MESSAGE:
"You need more margin to match this leverage amount to make this trade.",
LEVERAGE_LIMIT_MESSAGE: "You will need more margin to make this trade.",
NO_DEPOSIT_MESSAGE:
"You need to deposit coin of this type into oyster before trading with it on margin.",
NO_COLL_TYPE_MESSAGE: "Choose Collateral CCY",
};

View File

@ -2,4 +2,4 @@ export const GUTTER = [16, { xs: 8, sm: 16, md: 16, lg: 16 }] as any;
export const SMALL_STATISTIC: React.CSSProperties = {
fontSize: 10,
};
};

View File

@ -1,13 +1,23 @@
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useConnection } from './connection';
import { useWallet } from './wallet';
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
import { AccountLayout, u64, MintInfo, MintLayout } from '@solana/spl-token';
import { PoolInfo, TokenAccount } from './../models';
import { chunks } from './../utils/utils';
import { EventEmitter } from './../utils/eventEmitter';
import { useUserAccounts } from '../hooks/useUserAccounts';
import { WRAPPED_SOL_MINT, programIds, LEND_HOST_FEE_ADDRESS } from '../utils/ids';
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import { useConnection } from "./connection";
import { useWallet } from "./wallet";
import { AccountInfo, Connection, PublicKey } from "@solana/web3.js";
import { AccountLayout, u64, MintInfo, MintLayout } from "@solana/spl-token";
import { PoolInfo, TokenAccount } from "./../models";
import { chunks } from "./../utils/utils";
import { EventEmitter } from "./../utils/eventEmitter";
import { useUserAccounts } from "../hooks/useUserAccounts";
import {
WRAPPED_SOL_MINT,
programIds,
LEND_HOST_FEE_ADDRESS,
} from "../utils/ids";
const AccountsContext = React.createContext<any>(null);
@ -22,7 +32,10 @@ export interface ParsedAccountBase {
info: any; // TODO: change to unkown
}
export type AccountParser = (pubkey: PublicKey, data: AccountInfo<Buffer>) => ParsedAccountBase | undefined;
export type AccountParser = (
pubkey: PublicKey,
data: AccountInfo<Buffer>
) => ParsedAccountBase | undefined;
export interface ParsedAccount<T> extends ParsedAccountBase {
info: T;
@ -31,7 +44,7 @@ export interface ParsedAccount<T> extends ParsedAccountBase {
const getMintInfo = async (connection: Connection, pubKey: PublicKey) => {
const info = await connection.getAccountInfo(pubKey);
if (info === null) {
throw new Error('Failed to find mint account');
throw new Error("Failed to find mint account");
}
const data = Buffer.from(info.data);
@ -55,7 +68,10 @@ export const MintParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
return details;
};
export const TokenAccountParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
export const TokenAccountParser = (
pubKey: PublicKey,
info: AccountInfo<Buffer>
) => {
const buffer = Buffer.from(info.data);
const data = deserializeAccount(buffer);
@ -70,7 +86,10 @@ export const TokenAccountParser = (pubKey: PublicKey, info: AccountInfo<Buffer>)
return details;
};
export const GenericAccountParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
export const GenericAccountParser = (
pubKey: PublicKey,
info: AccountInfo<Buffer>
) => {
const buffer = Buffer.from(info.data);
const details = {
@ -88,9 +107,13 @@ export const keyToAccountParser = new Map<string, AccountParser>();
export const cache = {
emitter: new EventEmitter(),
query: async (connection: Connection, pubKey: string | PublicKey, parser?: AccountParser) => {
query: async (
connection: Connection,
pubKey: string | PublicKey,
parser?: AccountParser
) => {
let id: PublicKey;
if (typeof pubKey === 'string') {
if (typeof pubKey === "string") {
id = new PublicKey(pubKey);
} else {
id = pubKey;
@ -111,7 +134,7 @@ export const cache = {
// TODO: refactor to use multiple accounts query with flush like behavior
query = connection.getAccountInfo(id).then((data) => {
if (!data) {
throw new Error('Account not found');
throw new Error("Account not found");
}
return cache.add(id, data, parser);
@ -120,11 +143,17 @@ export const cache = {
return query;
},
add: (id: PublicKey | string, obj: AccountInfo<Buffer>, parser?: AccountParser) => {
const address = typeof id === 'string' ? id : id?.toBase58();
add: (
id: PublicKey | string,
obj: AccountInfo<Buffer>,
parser?: AccountParser
) => {
const address = typeof id === "string" ? id : id?.toBase58();
const deserialize = parser ? parser : keyToAccountParser.get(address);
if (!deserialize) {
throw new Error('Deserializer needs to be registered or passed as a parameter');
throw new Error(
"Deserializer needs to be registered or passed as a parameter"
);
}
cache.registerParser(id, deserialize);
@ -142,7 +171,7 @@ export const cache = {
},
get: (pubKey: string | PublicKey) => {
let key: string;
if (typeof pubKey !== 'string') {
if (typeof pubKey !== "string") {
key = pubKey.toBase58();
} else {
key = pubKey;
@ -152,7 +181,7 @@ export const cache = {
},
delete: (pubKey: string | PublicKey) => {
let key: string;
if (typeof pubKey !== 'string') {
if (typeof pubKey !== "string") {
key = pubKey.toBase58();
} else {
key = pubKey;
@ -178,7 +207,7 @@ export const cache = {
},
registerParser: (pubkey: PublicKey | string, parser: AccountParser) => {
if (pubkey) {
const address = typeof pubkey === 'string' ? pubkey : pubkey?.toBase58();
const address = typeof pubkey === "string" ? pubkey : pubkey?.toBase58();
keyToAccountParser.set(address, parser);
}
@ -186,7 +215,7 @@ export const cache = {
},
queryMint: async (connection: Connection, pubKey: string | PublicKey) => {
let id: PublicKey;
if (typeof pubKey === 'string') {
if (typeof pubKey === "string") {
id = new PublicKey(pubKey);
} else {
id = pubKey;
@ -215,7 +244,7 @@ export const cache = {
},
getMint: (pubKey: string | PublicKey) => {
let key: string;
if (typeof pubKey !== 'string') {
if (typeof pubKey !== "string") {
key = pubKey.toBase58();
} else {
key = pubKey;
@ -237,7 +266,10 @@ export const useAccountsContext = () => {
return context;
};
function wrapNativeAccount(pubkey: PublicKey, account?: AccountInfo<Buffer>): TokenAccount | undefined {
function wrapNativeAccount(
pubkey: PublicKey,
account?: AccountInfo<Buffer>
): TokenAccount | undefined {
if (!account) {
return undefined;
}
@ -273,7 +305,9 @@ export function useCachedPool(legacy = false) {
};
}
export const getCachedAccount = (predicate: (account: TokenAccount) => boolean) => {
export const getCachedAccount = (
predicate: (account: TokenAccount) => boolean
) => {
for (const account of genericCache.values()) {
if (predicate(account)) {
return account as TokenAccount;
@ -323,7 +357,10 @@ const UseNativeAccount = () => {
};
const PRECACHED_OWNERS = new Set<string>();
const precacheUserTokenAccounts = async (connection: Connection, owner?: PublicKey) => {
const precacheUserTokenAccounts = async (
connection: Connection,
owner?: PublicKey
) => {
if (!owner) {
return;
}
@ -351,12 +388,16 @@ export function AccountsProvider({ children = null as any }) {
return cache
.byParser(TokenAccountParser)
.map((id) => cache.get(id))
.filter((a) => a && a.info.owner.toBase58() === wallet.publicKey?.toBase58())
.filter(
(a) => a && a.info.owner.toBase58() === wallet.publicKey?.toBase58()
)
.map((a) => a as TokenAccount);
}, [wallet]);
useEffect(() => {
const accounts = selectUserAccounts().filter((a) => a !== undefined) as TokenAccount[];
const accounts = selectUserAccounts().filter(
(a) => a !== undefined
) as TokenAccount[];
setUserAccounts(accounts);
}, [nativeAccount, wallet, tokenAccounts, selectUserAccounts]);
@ -406,7 +447,7 @@ export function AccountsProvider({ children = null as any }) {
}
}
},
'singleGossip'
"singleGossip"
);
return () => {
@ -434,9 +475,15 @@ export function useNativeAccount() {
};
}
export const getMultipleAccounts = async (connection: any, keys: string[], commitment: string) => {
export const getMultipleAccounts = async (
connection: any,
keys: string[],
commitment: string
) => {
const result = await Promise.all(
chunks(keys, 99).map((chunk) => getMultipleAccountsCore(connection, chunk, commitment))
chunks(keys, 99).map((chunk) =>
getMultipleAccountsCore(connection, chunk, commitment)
)
);
const array = result
@ -451,7 +498,7 @@ export const getMultipleAccounts = async (connection: any, keys: string[], commi
const { data, ...rest } = acc;
const obj = {
...rest,
data: Buffer.from(data[0], 'base64'),
data: Buffer.from(data[0], "base64"),
} as AccountInfo<Buffer>;
return obj;
})
@ -461,12 +508,18 @@ export const getMultipleAccounts = async (connection: any, keys: string[], commi
return { keys, array };
};
const getMultipleAccountsCore = async (connection: any, keys: string[], commitment: string) => {
const args = connection._buildArgs([keys], commitment, 'base64');
const getMultipleAccountsCore = async (
connection: any,
keys: string[],
commitment: string
) => {
const args = connection._buildArgs([keys], commitment, "base64");
const unsafeRes = await connection._rpcRequest('getMultipleAccounts', args);
const unsafeRes = await connection._rpcRequest("getMultipleAccounts", args);
if (unsafeRes.error) {
throw new Error('failed to get info about account ' + unsafeRes.error.message);
throw new Error(
"failed to get info about account " + unsafeRes.error.message
);
}
if (unsafeRes.result.value) {
@ -482,7 +535,7 @@ export function useMint(key?: string | PublicKey) {
const connection = useConnection();
const [mint, setMint] = useState<MintInfo>();
const id = typeof key === 'string' ? key : key?.toBase58();
const id = typeof key === "string" ? key : key?.toBase58();
useEffect(() => {
if (!id) {
@ -497,7 +550,9 @@ export function useMint(key?: string | PublicKey) {
const dispose = cache.emitter.onCache((e) => {
const event = e;
if (event.id === id) {
cache.query(connection, id, MintParser).then((mint) => setMint(mint.info as any));
cache
.query(connection, id, MintParser)
.then((mint) => setMint(mint.info as any));
}
});
return () => {
@ -510,7 +565,9 @@ export function useMint(key?: string | PublicKey) {
export const useAccountByMint = (mint: string) => {
const { userAccounts } = useUserAccounts();
const index = userAccounts.findIndex((acc) => acc.info.mint.toBase58() === mint);
const index = userAccounts.findIndex(
(acc) => acc.info.mint.toBase58() === mint
);
if (index !== -1) {
return userAccounts[index];
@ -531,7 +588,9 @@ export function useAccount(pubKey?: PublicKey) {
return;
}
const acc = await cache.query(connection, key, TokenAccountParser).catch((err) => console.log(err));
const acc = await cache
.query(connection, key, TokenAccountParser)
.catch((err) => console.log(err));
if (acc) {
setAccount(acc);
}
@ -594,7 +653,7 @@ const deserializeAccount = (data: Buffer) => {
// TODO: expose in spl package
const deserializeMint = (data: Buffer) => {
if (data.length !== MintLayout.span) {
throw new Error('Not a valid Mint');
throw new Error("Not a valid Mint");
}
const mintInfo = MintLayout.decode(data);

View File

@ -1,29 +1,40 @@
import { KnownToken, useLocalStorageState } from './../utils/utils';
import { Account, clusterApiUrl, Connection, Transaction, TransactionInstruction } from '@solana/web3.js';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { notify } from './../utils/notifications';
import { ExplorerLink } from '../components/ExplorerLink';
import LocalTokens from '../config/tokens.json';
import { setProgramIds } from '../utils/ids';
import { KnownToken, useLocalStorageState } from "./../utils/utils";
import {
Account,
clusterApiUrl,
Connection,
Transaction,
TransactionInstruction,
} from "@solana/web3.js";
import React, { useContext, useEffect, useMemo, useState } from "react";
import { notify } from "./../utils/notifications";
import { ExplorerLink } from "../components/ExplorerLink";
import LocalTokens from "../config/tokens.json";
import { setProgramIds } from "../utils/ids";
export type ENV = 'mainnet-beta' | 'testnet' | 'devnet' | 'localnet' | 'lending';
export type ENV =
| "mainnet-beta"
| "testnet"
| "devnet"
| "localnet"
| "lending";
export const ENDPOINTS = [
{
name: 'mainnet-beta' as ENV,
endpoint: 'https://solana-api.projectserum.com/',
name: "mainnet-beta" as ENV,
endpoint: "https://solana-api.projectserum.com/",
},
{
name: 'Oyster Dev' as ENV,
endpoint: 'http://oyster-dev.solana.com/',
name: "Oyster Dev" as ENV,
endpoint: "http://oyster-dev.solana.com/",
},
{
name: 'Lending' as ENV,
endpoint: 'http://tln.solana.com/',
name: "Lending" as ENV,
endpoint: "http://tln.solana.com/",
},
{ name: 'testnet' as ENV, endpoint: clusterApiUrl('testnet') },
{ name: 'devnet' as ENV, endpoint: clusterApiUrl('devnet') },
{ name: 'localnet' as ENV, endpoint: 'http://127.0.0.1:8899' },
{ name: "testnet" as ENV, endpoint: clusterApiUrl("testnet") },
{ name: "devnet" as ENV, endpoint: clusterApiUrl("devnet") },
{ name: "localnet" as ENV, endpoint: "http://127.0.0.1:8899" },
];
const DEFAULT = ENDPOINTS[0].endpoint;
@ -46,29 +57,43 @@ const ConnectionContext = React.createContext<ConnectionConfig>({
setEndpoint: () => {},
slippage: DEFAULT_SLIPPAGE,
setSlippage: (val: number) => {},
connection: new Connection(DEFAULT, 'recent'),
sendConnection: new Connection(DEFAULT, 'recent'),
connection: new Connection(DEFAULT, "recent"),
sendConnection: new Connection(DEFAULT, "recent"),
env: ENDPOINTS[0].name,
tokens: [],
tokenMap: new Map<string, KnownToken>(),
});
export function ConnectionProvider({ children = undefined as any }) {
const [endpoint, setEndpoint] = useLocalStorageState('connectionEndpts', ENDPOINTS[0].endpoint);
const [endpoint, setEndpoint] = useLocalStorageState(
"connectionEndpts",
ENDPOINTS[0].endpoint
);
const [slippage, setSlippage] = useLocalStorageState('slippage', DEFAULT_SLIPPAGE.toString());
const [slippage, setSlippage] = useLocalStorageState(
"slippage",
DEFAULT_SLIPPAGE.toString()
);
const connection = useMemo(() => new Connection(endpoint, 'recent'), [endpoint]);
const sendConnection = useMemo(() => new Connection(endpoint, 'recent'), [endpoint]);
const connection = useMemo(() => new Connection(endpoint, "recent"), [
endpoint,
]);
const sendConnection = useMemo(() => new Connection(endpoint, "recent"), [
endpoint,
]);
const env = ENDPOINTS.find((end) => end.endpoint === endpoint)?.name || ENDPOINTS[0].name;
const env =
ENDPOINTS.find((end) => end.endpoint === endpoint)?.name ||
ENDPOINTS[0].name;
const [tokens, setTokens] = useState<KnownToken[]>([]);
const [tokenMap, setTokenMap] = useState<Map<string, KnownToken>>(new Map());
useEffect(() => {
// fetch token files
window
.fetch(`https://raw.githubusercontent.com/solana-labs/token-list/main/src/tokens/${env}.json`)
.fetch(
`https://raw.githubusercontent.com/solana-labs/token-list/main/src/tokens/${env}.json`
)
.then((res) => {
return res.json();
})
@ -104,7 +129,10 @@ export function ConnectionProvider({ children = undefined as any }) {
}, [connection]);
useEffect(() => {
const id = sendConnection.onAccountChange(new Account().publicKey, () => {});
const id = sendConnection.onAccountChange(
new Account().publicKey,
() => {}
);
return () => {
sendConnection.removeAccountChangeListener(id);
};
@ -162,7 +190,7 @@ export function useSlippageConfig() {
const getErrorForTransaction = async (connection: Connection, txid: string) => {
// wait for all confirmation before geting transaction
await connection.confirmTransaction(txid, 'max');
await connection.confirmTransaction(txid, "max");
const tx = await connection.getParsedConfirmedTransaction(txid);
@ -196,7 +224,9 @@ export const sendTransaction = async (
) => {
let transaction = new Transaction();
instructions.forEach((instruction) => transaction.add(instruction));
transaction.recentBlockhash = (await connection.getRecentBlockhash('max')).blockhash;
transaction.recentBlockhash = (
await connection.getRecentBlockhash("max")
).blockhash;
transaction.setSigners(
// fee payied by the wallet owner
wallet.publicKey,
@ -209,30 +239,37 @@ export const sendTransaction = async (
const rawTransaction = transaction.serialize();
let options = {
skipPreflight: true,
commitment: 'singleGossip',
commitment: "singleGossip",
};
const txid = await connection.sendRawTransaction(rawTransaction, options);
if (awaitConfirmation) {
const status = (await connection.confirmTransaction(txid, options && (options.commitment as any))).value;
const status = (
await connection.confirmTransaction(
txid,
options && (options.commitment as any)
)
).value;
if (status?.err) {
const errors = await getErrorForTransaction(connection, txid);
notify({
message: 'Transaction failed...',
message: "Transaction failed...",
description: (
<>
{errors.map((err) => (
<div>{err}</div>
))}
<ExplorerLink address={txid} type='transaction' />
<ExplorerLink address={txid} type="transaction" />
</>
),
type: 'error',
type: "error",
});
throw new Error(`Raw transaction ${txid} failed (${JSON.stringify(status)})`);
throw new Error(
`Raw transaction ${txid} failed (${JSON.stringify(status)})`
);
}
}

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useConnection } from './connection';
import { LENDING_PROGRAM_ID } from './../utils/ids';
import React, { useCallback, useEffect, useState } from "react";
import { useConnection } from "./connection";
import { LENDING_PROGRAM_ID } from "./../utils/ids";
import {
LendingMarketParser,
isLendingReserve,
@ -9,12 +9,17 @@ import {
LendingReserve,
isLendingObligation,
LendingObligationParser,
} from './../models/lending';
import { cache, getMultipleAccounts, MintParser, ParsedAccount } from './accounts';
import { PublicKey, AccountInfo } from '@solana/web3.js';
import { DexMarketParser } from '../models/dex';
import { usePrecacheMarket } from './market';
import { useLendingReserves } from '../hooks';
} from "./../models/lending";
import {
cache,
getMultipleAccounts,
MintParser,
ParsedAccount,
} from "./accounts";
import { PublicKey, AccountInfo } from "@solana/web3.js";
import { DexMarketParser } from "../models/dex";
import { usePrecacheMarket } from "./market";
import { useLendingReserves } from "../hooks";
export interface LendingContextState {}
@ -41,21 +46,38 @@ export const useLending = () => {
// TODO: query for all the dex from reserves
const processAccount = useCallback((item : { pubkey: PublicKey, account: AccountInfo<Buffer> }) => {
if (isLendingReserve(item.account)) {
const reserve = cache.add(item.pubkey.toBase58(), item.account, LendingReserveParser);
const processAccount = useCallback(
(item: { pubkey: PublicKey; account: AccountInfo<Buffer> }) => {
if (isLendingReserve(item.account)) {
const reserve = cache.add(
item.pubkey.toBase58(),
item.account,
LendingReserveParser
);
return reserve;
} else if (isLendingMarket(item.account)) {
return cache.add(item.pubkey.toBase58(), item.account, LendingMarketParser);
} else if (isLendingObligation(item.account)) {
return cache.add(item.pubkey.toBase58(), item.account, LendingObligationParser);
}
}, []);
return reserve;
} else if (isLendingMarket(item.account)) {
return cache.add(
item.pubkey.toBase58(),
item.account,
LendingMarketParser
);
} else if (isLendingObligation(item.account)) {
return cache.add(
item.pubkey.toBase58(),
item.account,
LendingObligationParser
);
}
},
[]
);
useEffect(() => {
if (reserveAccounts.length > 0) {
precacheMarkets(reserveAccounts.map((reserve) => reserve.info.liquidityMint.toBase58()));
precacheMarkets(
reserveAccounts.map((reserve) => reserve.info.liquidityMint.toBase58())
);
}
}, [reserveAccounts, precacheMarkets]);
@ -64,21 +86,36 @@ export const useLending = () => {
setLendingAccounts([]);
const queryLendingAccounts = async () => {
const programAccounts = await connection.getProgramAccounts(LENDING_PROGRAM_ID);
const programAccounts = await connection.getProgramAccounts(
LENDING_PROGRAM_ID
);
const accounts = programAccounts.map(processAccount).filter((item) => item !== undefined);
const accounts = programAccounts
.map(processAccount)
.filter((item) => item !== undefined);
const lendingReserves = accounts
.filter((acc) => (acc?.info as LendingReserve).lendingMarket !== undefined)
.filter(
(acc) => (acc?.info as LendingReserve).lendingMarket !== undefined
)
.map((acc) => acc as ParsedAccount<LendingReserve>);
const toQuery = [
...lendingReserves.map((acc) => {
const result = [
cache.registerParser(acc?.info.collateralMint.toBase58(), MintParser),
cache.registerParser(acc?.info.liquidityMint.toBase58(), MintParser),
cache.registerParser(
acc?.info.collateralMint.toBase58(),
MintParser
),
cache.registerParser(
acc?.info.liquidityMint.toBase58(),
MintParser
),
// ignore dex if its not set
cache.registerParser(acc?.info.dexMarketOption ? acc?.info.dexMarket.toBase58() : '', DexMarketParser),
cache.registerParser(
acc?.info.dexMarketOption ? acc?.info.dexMarket.toBase58() : "",
DexMarketParser
),
].filter((_) => _);
return result;
}),
@ -86,13 +123,15 @@ export const useLending = () => {
// This will pre-cache all accounts used by pools
// All those accounts are updated whenever there is a change
await getMultipleAccounts(connection, toQuery, 'single').then(({ keys, array }) => {
return array.map((obj, index) => {
const address = keys[index];
cache.add(address, obj);
return obj;
}) as any[];
});
await getMultipleAccounts(connection, toQuery, "single").then(
({ keys, array }) => {
return array.map((obj, index) => {
const address = keys[index];
cache.add(address, obj);
return obj;
}) as any[];
}
);
// HACK: fix, force account refresh
programAccounts.map(processAccount).filter((item) => item !== undefined);
@ -116,7 +155,7 @@ export const useLending = () => {
};
processAccount(item);
},
'singleGossip'
"singleGossip"
);
return () => {

View File

@ -1,19 +1,26 @@
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { MINT_TO_MARKET } from './../models/marketOverrides';
import { POOLS_WITH_AIRDROP } from './../models/airdrops';
import { convert, fromLamports, getPoolName, getTokenName, KnownTokenMap, STABLE_COINS } from './../utils/utils';
import { useConnectionConfig } from './connection';
import { cache, getMultipleAccounts, ParsedAccount } from './accounts';
import { Market, MARKETS, Orderbook, TOKEN_MINTS } from '@project-serum/serum';
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
import { useMemo } from 'react';
import { EventEmitter } from './../utils/eventEmitter';
import React, { useCallback, useContext, useEffect, useState } from "react";
import { MINT_TO_MARKET } from "./../models/marketOverrides";
import { POOLS_WITH_AIRDROP } from "./../models/airdrops";
import {
convert,
fromLamports,
getPoolName,
getTokenName,
KnownTokenMap,
STABLE_COINS,
} from "./../utils/utils";
import { useConnectionConfig } from "./connection";
import { cache, getMultipleAccounts, ParsedAccount } from "./accounts";
import { Market, MARKETS, Orderbook, TOKEN_MINTS } from "@project-serum/serum";
import { AccountInfo, Connection, PublicKey } from "@solana/web3.js";
import { useMemo } from "react";
import { EventEmitter } from "./../utils/eventEmitter";
import { DexMarketParser } from './../models/dex';
import { LendingMarket, LendingReserve, PoolInfo } from '../models';
import { LIQUIDITY_PROVIDER_FEE, SERUM_FEE } from '../utils/pools';
import { DexMarketParser } from "./../models/dex";
import { LendingMarket, LendingReserve, PoolInfo } from "../models";
import { LIQUIDITY_PROVIDER_FEE, SERUM_FEE } from "../utils/pools";
const INITAL_LIQUIDITY_DATE = new Date('2020-10-27');
const INITAL_LIQUIDITY_DATE = new Date("2020-10-27");
export const BONFIDA_POOL_INTERVAL = 30 * 60_000; // 30 min
interface RecentPoolData {
@ -43,19 +50,27 @@ export function MarketProvider({ children = null as any }) {
const { endpoint } = useConnectionConfig();
const accountsToObserve = useMemo(() => new Map<string, number>(), []);
const [marketMints, setMarketMints] = useState<string[]>([]);
const [dailyVolume, setDailyVolume] = useState<Map<string, RecentPoolData>>(new Map());
const [dailyVolume, setDailyVolume] = useState<Map<string, RecentPoolData>>(
new Map()
);
const connection = useMemo(() => new Connection(endpoint, 'recent'), [endpoint]);
const connection = useMemo(() => new Connection(endpoint, "recent"), [
endpoint,
]);
const marketByMint = useMemo(() => {
return [...new Set(marketMints).values()].reduce((acc, key) => {
const mintAddress = key;
const SERUM_TOKEN = TOKEN_MINTS.find((a) => a.address.toBase58() === mintAddress);
const SERUM_TOKEN = TOKEN_MINTS.find(
(a) => a.address.toBase58() === mintAddress
);
const marketAddress = MINT_TO_MARKET[mintAddress];
const marketName = `${SERUM_TOKEN?.name}/USDC`;
const marketInfo = MARKETS.find((m) => m.name === marketName || m.address.toBase58() === marketAddress);
const marketInfo = MARKETS.find(
(m) => m.name === marketName || m.address.toBase58() === marketAddress
);
if (marketInfo) {
acc.set(mintAddress, {
@ -94,7 +109,7 @@ export function MarketProvider({ children = null as any }) {
connection,
// only query for markets that are not in cahce
allMarkets.filter((a) => cache.get(a) === undefined),
'single'
"single"
).then(({ keys, array }) => {
allMarkets.forEach(() => {});
@ -152,7 +167,10 @@ export function MarketProvider({ children = null as any }) {
const midPriceInUSD = useCallback(
(mintAddress: string) => {
return getMidPrice(marketByMint.get(mintAddress)?.marketInfo.address.toBase58(), mintAddress);
return getMidPrice(
marketByMint.get(mintAddress)?.marketInfo.address.toBase58(),
mintAddress
);
},
[marketByMint]
);
@ -160,7 +178,7 @@ export function MarketProvider({ children = null as any }) {
const subscribeToMarket = useCallback(
(mintAddress: string) => {
const info = marketByMint.get(mintAddress);
const market = cache.get(info?.marketInfo.address.toBase58() || '');
const market = cache.get(info?.marketInfo.address.toBase58() || "");
if (!market) {
return () => {};
}
@ -230,7 +248,7 @@ export const useEnrichedPools = (pools: PoolInfo[]) => {
const marketEmitter = context?.marketEmitter;
const marketsByMint = context?.marketByMint;
const dailyVolume = context?.dailyVolume;
const poolKeys = pools.map((p) => p.pubkeys.account.toBase58()).join(',');
const poolKeys = pools.map((p) => p.pubkeys.account.toBase58()).join(",");
useEffect(() => {
if (!marketEmitter || !subscribeToMarket || pools.length === 0) {
@ -242,7 +260,9 @@ export const useEnrichedPools = (pools: PoolInfo[]) => {
const subscriptions = mints.map((m) => subscribeToMarket(m));
const update = () => {
setEnriched(createEnrichedPools(pools, marketsByMint, dailyVolume, tokenMap));
setEnriched(
createEnrichedPools(pools, marketsByMint, dailyVolume, tokenMap)
);
};
const dispose = marketEmitter.onMarket(update);
@ -254,7 +274,14 @@ export const useEnrichedPools = (pools: PoolInfo[]) => {
subscriptions.forEach((dispose) => dispose && dispose());
};
// Do not add pools here, causes a really bad infinite rendering loop. Use poolKeys instead.
}, [tokenMap, dailyVolume, poolKeys, subscribeToMarket, marketEmitter, marketsByMint]);
}, [
tokenMap,
dailyVolume,
poolKeys,
subscribeToMarket,
marketEmitter,
marketsByMint,
]);
return enriched;
};
@ -279,20 +306,30 @@ function createEnrichedPools(
const result = pools
.filter((p) => p.pubkeys.holdingMints && p.pubkeys.holdingMints.length > 1)
.map((p, index) => {
const mints = (p.pubkeys.holdingMints || []).map((a) => a.toBase58()).sort();
const mints = (p.pubkeys.holdingMints || [])
.map((a) => a.toBase58())
.sort();
const mintA = cache.getMint(mints[0]);
const mintB = cache.getMint(mints[1]);
const account0 = cache.get(p.pubkeys.holdingAccounts[0]);
const account1 = cache.get(p.pubkeys.holdingAccounts[1]);
const accountA = account0?.info.mint.toBase58() === mints[0] ? account0 : account1;
const accountB = account1?.info.mint.toBase58() === mints[1] ? account1 : account0;
const accountA =
account0?.info.mint.toBase58() === mints[0] ? account0 : account1;
const accountB =
account1?.info.mint.toBase58() === mints[1] ? account1 : account0;
const baseMid = getMidPrice(marketByMint.get(mints[0])?.marketInfo.address.toBase58() || '', mints[0]);
const baseMid = getMidPrice(
marketByMint.get(mints[0])?.marketInfo.address.toBase58() || "",
mints[0]
);
const baseReserveUSD = baseMid * convert(accountA, mintA);
const quote = getMidPrice(marketByMint.get(mints[1])?.marketInfo.address.toBase58() || '', mints[1]);
const quote = getMidPrice(
marketByMint.get(mints[1])?.marketInfo.address.toBase58() || "",
mints[1]
);
const quoteReserveUSD = quote * convert(accountB, mintB);
const poolMint = cache.getMint(p.pubkeys.mint);
@ -300,10 +337,16 @@ function createEnrichedPools(
return undefined;
}
let airdropYield = calculateAirdropYield(p, marketByMint, baseReserveUSD, quoteReserveUSD);
let airdropYield = calculateAirdropYield(
p,
marketByMint,
baseReserveUSD,
quoteReserveUSD
);
let volume = 0;
let volume24h = baseMid * (poolData?.get(p.pubkeys.mint.toBase58())?.volume24hA || 0);
let volume24h =
baseMid * (poolData?.get(p.pubkeys.mint.toBase58())?.volume24hA || 0);
let fees24h = volume24h * (LIQUIDITY_PROVIDER_FEE - SERUM_FEE);
let fees = 0;
let apy = airdropYield;
@ -311,13 +354,18 @@ function createEnrichedPools(
if (p.pubkeys.feeAccount) {
const feeAccount = cache.get(p.pubkeys.feeAccount);
if (poolMint && feeAccount && feeAccount.info.mint.toBase58() === p.pubkeys.mint.toBase58()) {
if (
poolMint &&
feeAccount &&
feeAccount.info.mint.toBase58() === p.pubkeys.mint.toBase58()
) {
const feeBalance = feeAccount?.info.amount.toNumber();
const supply = poolMint?.supply.toNumber();
const ownedPct = feeBalance / supply;
const poolOwnerFees = ownedPct * baseReserveUSD + ownedPct * quoteReserveUSD;
const poolOwnerFees =
ownedPct * baseReserveUSD + ownedPct * quoteReserveUSD;
volume = poolOwnerFees / 0.0004;
fees = volume * LIQUIDITY_PROVIDER_FEE;
@ -327,16 +375,27 @@ function createEnrichedPools(
// Aproximation not true for all pools we need to fine a better way
const daysSinceInception = Math.floor(
(TODAY.getTime() - INITAL_LIQUIDITY_DATE.getTime()) / (24 * 3600 * 1000)
(TODAY.getTime() - INITAL_LIQUIDITY_DATE.getTime()) /
(24 * 3600 * 1000)
);
const apy0 =
parseFloat(((baseVolume / daysSinceInception) * LIQUIDITY_PROVIDER_FEE * 356) as any) / baseReserveUSD;
parseFloat(
((baseVolume / daysSinceInception) *
LIQUIDITY_PROVIDER_FEE *
356) as any
) / baseReserveUSD;
const apy1 =
parseFloat(((quoteVolume / daysSinceInception) * LIQUIDITY_PROVIDER_FEE * 356) as any) / quoteReserveUSD;
parseFloat(
((quoteVolume / daysSinceInception) *
LIQUIDITY_PROVIDER_FEE *
356) as any
) / quoteReserveUSD;
apy = apy + Math.max(apy0, apy1);
const apy24h0 = parseFloat((volume24h * LIQUIDITY_PROVIDER_FEE * 356) as any) / baseReserveUSD;
const apy24h0 =
parseFloat((volume24h * LIQUIDITY_PROVIDER_FEE * 356) as any) /
baseReserveUSD;
apy24h = apy24h + apy24h0;
}
}
@ -345,7 +404,10 @@ function createEnrichedPools(
const lpMint = cache.getMint(p.pubkeys.mint);
const name = getPoolName(tokenMap, p);
const link = `#/?pair=${getPoolName(tokenMap, p, false).replace('/', '-')}`;
const link = `#/?pair=${getPoolName(tokenMap, p, false).replace(
"/",
"-"
)}`;
return {
key: p.pubkeys.account.toBase58(),
@ -360,7 +422,11 @@ function createEnrichedPools(
liquidityAinUsd: baseReserveUSD,
liquidityB: convert(accountB, mintB),
liquidityBinUsd: quoteReserveUSD,
supply: lpMint && (lpMint?.supply.toNumber() / Math.pow(10, lpMint?.decimals || 0)).toFixed(9),
supply:
lpMint &&
(
lpMint?.supply.toNumber() / Math.pow(10, lpMint?.decimals || 0)
).toFixed(9),
fees,
fees24h,
liquidity: baseReserveUSD + quoteReserveUSD,
@ -384,7 +450,9 @@ function calculateAirdropYield(
quoteReserveUSD: number
) {
let airdropYield = 0;
let poolWithAirdrop = POOLS_WITH_AIRDROP.find((drop) => drop.pool.equals(p.pubkeys.mint));
let poolWithAirdrop = POOLS_WITH_AIRDROP.find((drop) =>
drop.pool.equals(p.pubkeys.mint)
);
if (poolWithAirdrop) {
airdropYield = poolWithAirdrop.airdrops.reduce((acc, item) => {
const market = marketByMint.get(item.mint.toBase58())?.marketInfo.address;
@ -394,7 +462,8 @@ function calculateAirdropYield(
acc =
acc +
// airdrop yield
((item.amount * midPrice) / (baseReserveUSD + quoteReserveUSD)) * (365 / 30);
((item.amount * midPrice) / (baseReserveUSD + quoteReserveUSD)) *
(365 / 30);
}
return acc;
@ -404,7 +473,9 @@ function calculateAirdropYield(
}
export const useMidPriceInUSD = (mint: string) => {
const { midPriceInUSD, subscribeToMarket, marketEmitter } = useContext(MarketsContext) as MarketsContextState;
const { midPriceInUSD, subscribeToMarket, marketEmitter } = useContext(
MarketsContext
) as MarketsContextState;
const [price, setPrice] = useState<number>(0);
useEffect(() => {
@ -432,7 +503,12 @@ export const usePrecacheMarket = () => {
return context.precacheMarkets;
};
export const simulateMarketOrderFill = (amount: number, reserve: LendingReserve, dex: PublicKey) => {
export const simulateMarketOrderFill = (
amount: number,
reserve: LendingReserve,
dex: PublicKey,
useBBO = false
) => {
const liquidityMint = cache.get(reserve.liquidityMint);
const collateralMint = cache.get(reserve.collateralMint);
if (!liquidityMint || !collateralMint) {
@ -445,35 +521,51 @@ export const simulateMarketOrderFill = (amount: number, reserve: LendingReserve,
}
const decodedMarket = marketInfo.info;
const baseMintDecimals = cache.get(decodedMarket.baseMint)?.info.decimals || 0;
const quoteMintDecimals = cache.get(decodedMarket.quoteMint)?.info.decimals || 0;
const baseMintDecimals =
cache.get(decodedMarket.baseMint)?.info.decimals || 0;
const quoteMintDecimals =
cache.get(decodedMarket.quoteMint)?.info.decimals || 0;
const lendingMarket = cache.get(reserve.lendingMarket) as ParsedAccount<LendingMarket>;
const lendingMarket = cache.get(reserve.lendingMarket) as ParsedAccount<
LendingMarket
>;
const dexMarket = new Market(decodedMarket, baseMintDecimals, quoteMintDecimals, undefined, decodedMarket.programId);
const dexMarket = new Market(
decodedMarket,
baseMintDecimals,
quoteMintDecimals,
undefined,
decodedMarket.programId
);
const bookAccount = lendingMarket.info.quoteMint.equals(reserve.liquidityMint)
? decodedMarket?.bids
: decodedMarket?.asks;
const bookInfo = cache.get(bookAccount)?.info;
if (!bookInfo) {
const bidInfo = cache.get(decodedMarket?.bids)?.info;
const askInfo = cache.get(decodedMarket?.asks)?.info;
if (!bidInfo || !askInfo) {
return 0;
}
const book = new Orderbook(dexMarket, bookInfo.accountFlags, bookInfo.slab);
const bids = new Orderbook(dexMarket, bidInfo.accountFlags, bidInfo.slab);
const asks = new Orderbook(dexMarket, askInfo.accountFlags, askInfo.slab);
const book = lendingMarket.info.quoteMint.equals(reserve.liquidityMint)
? bids
: asks;
let cost = 0;
let remaining = fromLamports(amount, liquidityMint.info);
if (book) {
const op = book.isBids
? (price: number, size: number) => size / price
: (price: number, size: number) => size * price;
if (useBBO) {
const price = bbo(bids, asks);
return op(price, remaining);
} else {
const depth = book.getL2(1000);
let price, sizeAtLevel: number;
const op = book.isBids
? (price: number, size: number) => size / price
: (price: number, size: number) => size * price;
for ([price, sizeAtLevel] of depth) {
let filled = remaining > sizeAtLevel ? sizeAtLevel : remaining;
cost = cost + op(price, filled);
@ -488,10 +580,23 @@ export const simulateMarketOrderFill = (amount: number, reserve: LendingReserve,
return cost;
};
const getMidPrice = (marketAddress?: string, mintAddress?: string) => {
const SERUM_TOKEN = TOKEN_MINTS.find((a) => a.address.toBase58() === mintAddress);
const bbo = (bidsBook: Orderbook, asksBook: Orderbook) => {
const bestBid = bidsBook.getL2(1);
const bestAsk = asksBook.getL2(1);
if (STABLE_COINS.has(SERUM_TOKEN?.name || '')) {
if (bestBid.length > 0 && bestAsk.length > 0) {
return (bestBid[0][0] + bestAsk[0][0]) / 2.0;
}
return 0;
};
const getMidPrice = (marketAddress?: string, mintAddress?: string) => {
const SERUM_TOKEN = TOKEN_MINTS.find(
(a) => a.address.toBase58() === mintAddress
);
if (STABLE_COINS.has(SERUM_TOKEN?.name || "")) {
return 1.0;
}
@ -506,10 +611,18 @@ const getMidPrice = (marketAddress?: string, mintAddress?: string) => {
const decodedMarket = marketInfo.info;
const baseMintDecimals = cache.get(decodedMarket.baseMint)?.info.decimals || 0;
const quoteMintDecimals = cache.get(decodedMarket.quoteMint)?.info.decimals || 0;
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 market = new Market(
decodedMarket,
baseMintDecimals,
quoteMintDecimals,
undefined,
decodedMarket.programId
);
const bids = cache.get(decodedMarket.bids)?.info;
const asks = cache.get(decodedMarket.asks)?.info;
@ -518,12 +631,7 @@ const getMidPrice = (marketAddress?: string, mintAddress?: string) => {
const bidsBook = new Orderbook(market, bids.accountFlags, bids.slab);
const asksBook = new Orderbook(market, asks.accountFlags, asks.slab);
const bestBid = bidsBook.getL2(1);
const bestAsk = asksBook.getL2(1);
if (bestBid.length > 0 && bestAsk.length > 0) {
return (bestBid[0][0] + bestAsk[0][0]) / 2.0;
}
return bbo(bidsBook, asksBook);
}
return 0;
@ -534,12 +642,14 @@ const refreshAccounts = async (connection: Connection, keys: string[]) => {
return [];
}
return getMultipleAccounts(connection, keys, 'single').then(({ keys, array }) => {
return array.map((item, index) => {
const address = keys[index];
return cache.add(new PublicKey(address), item);
});
});
return getMultipleAccounts(connection, keys, "single").then(
({ keys, array }) => {
return array.map((item, index) => {
const address = keys[index];
return cache.add(new PublicKey(address), item);
});
}
);
};
interface SerumMarket {

View File

@ -20,8 +20,8 @@ export function useBorrowedAmount(address?: string | PublicKey) {
borrowedLamports: 0,
borrowedInUSD: 0,
colateralInUSD: 0,
ltv: 0,
health: 0,
ltv: 0,
health: 0,
});
const reserve = useLendingReserve(address);
const liquidityMint = useMint(reserve?.info.liquidityMint);
@ -30,8 +30,8 @@ export function useBorrowedAmount(address?: string | PublicKey) {
setBorrowedInfo({
borrowedLamports: 0,
borrowedInUSD: 0,
colateralInUSD: 0,
ltv: 0,
colateralInUSD: 0,
ltv: 0,
health: 0,
});
@ -53,8 +53,8 @@ export function useBorrowedAmount(address?: string | PublicKey) {
const result = {
borrowedLamports: 0,
borrowedInUSD: 0,
colateralInUSD: 0,
ltv: 0,
colateralInUSD: 0,
ltv: 0,
health: 0,
};
@ -73,7 +73,8 @@ export function useBorrowedAmount(address?: string | PublicKey) {
item.obligation.info.tokenMint
) as ParsedAccount<MintInfo>;
result.borrowedLamports += borrowed * (owned / obligationMint?.info.supply.toNumber());
result.borrowedLamports +=
borrowed * (owned / obligationMint?.info.supply.toNumber());
result.borrowedInUSD += item.obligation.info.borrowedInQuote;
result.colateralInUSD += item.obligation.info.collateralInQuote;
liquidationThreshold = item.obligation.info.liquidationThreshold;
@ -83,8 +84,11 @@ export function useBorrowedAmount(address?: string | PublicKey) {
result.ltv = userObligationsByReserve[0].obligation.info.ltv;
result.health = userObligationsByReserve[0].obligation.info.health;
} else {
result.ltv = 100 * result.borrowedInUSD / result.colateralInUSD;
result.health = result.colateralInUSD * liquidationThreshold / 100 / result.borrowedInUSD;
result.ltv = (100 * result.borrowedInUSD) / result.colateralInUSD;
result.health =
(result.colateralInUSD * liquidationThreshold) /
100 /
result.borrowedInUSD;
result.health = Number.isFinite(result.health) ? result.health : 0;
}

View File

@ -8,8 +8,18 @@ import { useUserObligations } from "./useUserObligations";
// TODO: add option to decrease buying power by overcollateralization factor
// TODO: add support for balance in the wallet
export function useBorrowingPower(reserveAddress: string | PublicKey | undefined, includeWallet = false, overcollateralize = true) {
const key = useMemo(() => typeof reserveAddress === 'string' ? reserveAddress : reserveAddress?.toBase58() || '', [reserveAddress]);
export function useBorrowingPower(
reserveAddress: string | PublicKey | undefined,
includeWallet = false,
overcollateralize = true
) {
const key = useMemo(
() =>
typeof reserveAddress === "string"
? reserveAddress
: reserveAddress?.toBase58() || "",
[reserveAddress]
);
const reserve = useLendingReserve(key);
@ -20,21 +30,19 @@ export function useBorrowingPower(reserveAddress: string | PublicKey | undefined
const quoteMintAddess = market?.info?.quoteMint?.toBase58();
// TODO: remove once cross-collateral is supported
const onlyQuoteAllowed = liquidityMintAddress !==
quoteMintAddess;
const onlyQuoteAllowed = liquidityMintAddress !== quoteMintAddess;
const exclude = useMemo(() => new Set(
[key]),
[key]);
const exclude = useMemo(() => new Set([key]), [key]);
const inlcude = useMemo(() => {
const quoteReserve = getLendingReserves()
.find(r => r.info.liquidityMint.toBase58() === quoteMintAddess);
return onlyQuoteAllowed && quoteReserve ?
new Set([quoteReserve.pubkey.toBase58()]) :
undefined;
const quoteReserve = getLendingReserves().find(
(r) => r.info.liquidityMint.toBase58() === quoteMintAddess
);
return onlyQuoteAllowed && quoteReserve
? new Set([quoteReserve.pubkey.toBase58()])
: undefined;
}, [onlyQuoteAllowed, quoteMintAddess]);
const { totalInQuote } = useUserDeposits(exclude, inlcude)
const { totalInQuote } = useUserDeposits(exclude, inlcude);
const price = useMidPriceInUSD(liquidityMintAddress).price;
@ -56,6 +64,6 @@ export function useBorrowingPower(reserveAddress: string | PublicKey | undefined
return {
borrowingPower: totalInQuote / price,
totalInQuote,
utilization
utilization,
};
}

View File

@ -19,20 +19,25 @@ export function useUserCollateralBalance(
const [balanceInUSD, setBalanceInUSD] = useState(0);
const { marketEmitter, midPriceInUSD } = useMarkets();
const balanceLamports = useMemo(() => reserve &&
calculateCollateralBalance(reserve, userBalance),
[userBalance, reserve]);
const balanceLamports = useMemo(
() => reserve && calculateCollateralBalance(reserve, userBalance),
[userBalance, reserve]
);
const balance = useMemo(() => fromLamports(balanceLamports, mint),
[balanceLamports, mint]);
const balance = useMemo(() => fromLamports(balanceLamports, mint), [
balanceLamports,
mint,
]);
useEffect(() => {
const updateBalance = () => {
setBalanceInUSD(balance * midPriceInUSD(reserve?.liquidityMint?.toBase58() || ''));
}
setBalanceInUSD(
balance * midPriceInUSD(reserve?.liquidityMint?.toBase58() || "")
);
};
const dispose = marketEmitter.onMarket((args) => {
if(args.ids.has(reserve?.dexMarket.toBase58() || '')) {
if (args.ids.has(reserve?.dexMarket.toBase58() || "")) {
updateBalance();
}
});
@ -55,8 +60,10 @@ export function useUserCollateralBalance(
}
export function calculateCollateralBalance(
reserve: LendingReserve,
balanceLamports: number) {
return reserveMarketCap(reserve) *
(balanceLamports / (reserve?.state.collateralMintSupply.toNumber() || 1));
balanceLamports: number
) {
return (
reserveMarketCap(reserve) *
(balanceLamports / (reserve?.state.collateralMintSupply.toNumber() || 1))
);
}

View File

@ -77,14 +77,16 @@ export function useEnrichedLendingObligations() {
let collateralInQuote = 0;
if (liquidityMint) {
const collateralMint = cache.get(item.collateralReserve.info.liquidityMint);
const collateralMint = cache.get(
item.collateralReserve.info.liquidityMint
);
const collateral = fromLamports(
collateralToLiquidity(
obligation.info.depositedCollateral,
item.reserve.info
),
collateralMint?.info,
collateralMint?.info
);
const borrowed = wadToLamports(
@ -96,19 +98,26 @@ export function useEnrichedLendingObligations() {
item.reserve.info,
item.reserve.info.dexMarketOption
? item.reserve.info.dexMarket
: item.collateralReserve.info.dexMarket
: item.collateralReserve.info.dexMarket,
true
);
const liquidityMintAddress = item.reserve.info.liquidityMint.toBase58();
const liquidityMint = cache.get(liquidityMintAddress) as ParsedAccount<MintInfo>;
borrowedInQuote = fromLamports(borrowed, liquidityMint.info) * midPriceInUSD(liquidityMintAddress);;
collateralInQuote = collateral * midPriceInUSD(collateralMint?.pubkey.toBase58() || '');
const liquidityMint = cache.get(
liquidityMintAddress
) as ParsedAccount<MintInfo>;
borrowedInQuote =
fromLamports(borrowed, liquidityMint.info) *
midPriceInUSD(liquidityMintAddress);
collateralInQuote =
collateral *
midPriceInUSD(collateralMint?.pubkey.toBase58() || "");
ltv = (100 * borrowedAmount) / collateral;
const liquidationThreshold =
item.reserve.info.config.liquidationThreshold;
health = collateral * liquidationThreshold / 100 / borrowedAmount;
health = (collateral * liquidationThreshold) / 100 / borrowedAmount;
}
return {
@ -117,11 +126,15 @@ export function useEnrichedLendingObligations() {
...obligation.info,
ltv,
health,
borrowedInQuote,
borrowedInQuote,
collateralInQuote,
liquidationThreshold: item.reserve.info.config.liquidationThreshold,
liquidationThreshold:
item.reserve.info.config.liquidationThreshold,
repayName: getTokenName(tokenMap, reserve.liquidityMint),
collateralName: getTokenName(tokenMap, collateralReserve.liquidityMint)
collateralName: getTokenName(
tokenMap,
collateralReserve.liquidityMint
),
},
} as EnrichedLendingObligation;
})

View File

@ -43,12 +43,11 @@ export function useLendingReserve(address?: string | PublicKey) {
if (token) {
const account = reserveAccounts.filter(
(acc) => acc.info.liquidityMint.toBase58() === token.mintAddress
)[0]
)[0];
if (account) {
addressName = account.pubkey;
}
}
}
const id = useMemo(
() =>

View File

@ -5,8 +5,15 @@ import { useMarkets } from "../contexts/market";
import { fromLamports } from "../utils/utils";
import { useUserAccounts } from "./useUserAccounts";
export function useUserBalance(mintAddress?: PublicKey | string, account?: PublicKey) {
const mint = useMemo(() => typeof mintAddress === 'string' ? mintAddress : mintAddress?.toBase58(), [mintAddress]);
export function useUserBalance(
mintAddress?: PublicKey | string,
account?: PublicKey
) {
const mint = useMemo(
() =>
typeof mintAddress === "string" ? mintAddress : mintAddress?.toBase58(),
[mintAddress]
);
const { userAccounts } = useUserAccounts();
const [balanceInUSD, setBalanceInUSD] = useState(0);
const { marketEmitter, midPriceInUSD } = useMarkets();
@ -29,12 +36,15 @@ export function useUserBalance(mintAddress?: PublicKey | string, account?: Publi
);
}, [accounts]);
const balance = useMemo(() => fromLamports(balanceLamports, mintInfo), [mintInfo, balanceLamports]);
const balance = useMemo(() => fromLamports(balanceLamports, mintInfo), [
mintInfo,
balanceLamports,
]);
useEffect(() => {
const updateBalance = () => {
setBalanceInUSD(balance * midPriceInUSD(mint || ''));
}
setBalanceInUSD(balance * midPriceInUSD(mint || ""));
};
const dispose = marketEmitter.onMarket((args) => {
updateBalance();

View File

@ -45,19 +45,28 @@ export function useUserDeposits(exclude?: Set<string>, include?: Set<string>) {
}, [reserveAccounts, exclude, include]);
useEffect(() => {
const activeMarkets = new Set(reserveAccounts.map(r => r.info.dexMarket.toBase58()));
const activeMarkets = new Set(
reserveAccounts.map((r) => r.info.dexMarket.toBase58())
);
const userDepositsFactory = () => {
return userAccounts
.filter((acc) => reservesByCollateralMint.has(acc?.info.mint.toBase58()))
.filter((acc) =>
reservesByCollateralMint.has(acc?.info.mint.toBase58())
)
.map((item) => {
const reserve = reservesByCollateralMint.get(
item?.info.mint.toBase58()
) as ParsedAccount<LendingReserve>;
let collateralMint = cache.get(reserve.info.collateralMint) as ParsedAccount<MintInfo>;
let collateralMint = cache.get(
reserve.info.collateralMint
) as ParsedAccount<MintInfo>;
const amountLamports = calculateCollateralBalance(reserve.info, item?.info.amount.toNumber());
const amountLamports = calculateCollateralBalance(
reserve.info,
item?.info.amount.toNumber()
);
const amount = fromLamports(amountLamports, collateralMint?.info);
const price = midPriceInUSD(reserve.info.liquidityMint.toBase58());
const amountInQuote = price * amount;
@ -78,7 +87,7 @@ export function useUserDeposits(exclude?: Set<string>, include?: Set<string>) {
const dispose = marketEmitter.onMarket((args) => {
// ignore if none of the markets is used by the reserve
if ([...args.ids.values()].every(id => !activeMarkets.has(id))) {
if ([...args.ids.values()].every((id) => !activeMarkets.has(id))) {
return;
}
@ -90,10 +99,20 @@ export function useUserDeposits(exclude?: Set<string>, include?: Set<string>) {
return () => {
dispose();
};
}, [userAccounts, reserveAccounts, reservesByCollateralMint, tokenMap, midPriceInUSD, marketEmitter]);
}, [
userAccounts,
reserveAccounts,
reservesByCollateralMint,
tokenMap,
midPriceInUSD,
marketEmitter,
]);
return {
userDeposits,
totalInQuote: userDeposits.reduce((res, item) => res + item.info.amountInQuote, 0),
totalInQuote: userDeposits.reduce(
(res, item) => res + item.info.amountInQuote,
0
),
};
}

View File

@ -17,13 +17,14 @@ export function useUserObligationByReserve(
typeof collateralReserve === "string"
? collateralReserve
: collateralReserve?.toBase58();
return userObligations.filter(
(item) =>
borrowId && collateralId ?
item.obligation.info.borrowReserve.toBase58() === borrowId &&
item.obligation.info.collateralReserve.toBase58() === collateralId :
(borrowId && item.obligation.info.borrowReserve.toBase58() === borrowId) ||
(collateralId && item.obligation.info.collateralReserve.toBase58() === collateralId)
return userObligations.filter((item) =>
borrowId && collateralId
? item.obligation.info.borrowReserve.toBase58() === borrowId &&
item.obligation.info.collateralReserve.toBase58() === collateralId
: (borrowId &&
item.obligation.info.borrowReserve.toBase58() === borrowId) ||
(collateralId &&
item.obligation.info.collateralReserve.toBase58() === collateralId)
);
}, [borrowReserve, collateralReserve, userObligations]);

View File

@ -30,11 +30,17 @@ export function useUserObligations() {
userAccounts: [...accountsByMint.get(ob.info.tokenMint.toBase58())],
};
})
.sort((a, b) => b.obligation.info.borrowedInQuote - a.obligation.info.borrowedInQuote);
.sort(
(a, b) =>
b.obligation.info.borrowedInQuote - a.obligation.info.borrowedInQuote
);
}, [accountsByMint, obligations]);
return {
userObligations,
totalInQuote: userObligations.reduce((result, item) => result + item.obligation.info.borrowedInQuote, 0),
totalInQuote: userObligations.reduce(
(result, item) => result + item.obligation.info.borrowedInQuote,
0
),
};
}

View File

@ -1,4 +1,4 @@
import './wdyr';
import "./wdyr";
import React from "react";
import ReactDOM from "react-dom";

View File

@ -1,4 +1,3 @@
{
"name": "Oyster Lending",
"short_name": "Oyster Lending",
@ -20,4 +19,4 @@
"sizes": "512x512"
}
]
}
}

View File

@ -1,4 +1,9 @@
import { Account, AccountInfo, PublicKey, TransactionInstruction } from "@solana/web3.js";
import {
Account,
AccountInfo,
PublicKey,
TransactionInstruction,
} from "@solana/web3.js";
import { AccountInfo as TokenAccountInfo, Token } from "@solana/spl-token";
import { TOKEN_PROGRAM_ID } from "../utils/ids";
@ -17,7 +22,7 @@ export function approve(
amount: number,
// if delegate is not passed ephemeral transfer authority is used
delegate?: PublicKey,
delegate?: PublicKey
): Account {
const tokenProgram = TOKEN_PROGRAM_ID;
const transferAuthority = new Account();
@ -34,12 +39,8 @@ export function approve(
);
cleanupInstructions.push(
Token.createRevokeInstruction(
tokenProgram,
account,
owner,
[]),
Token.createRevokeInstruction(tokenProgram, account, owner, [])
);
return transferAuthority;
}
}

View File

@ -1,4 +1,4 @@
import { PublicKey } from '@solana/web3.js';
import { PublicKey } from "@solana/web3.js";
interface PoolAirdrop {
pool: PublicKey;

View File

@ -1,5 +1,5 @@
export * from './account';
export * from './lending';
export * from './tokenSwap';
export * from './pool';
export * from './totals';
export * from "./account";
export * from "./lending";
export * from "./tokenSwap";
export * from "./pool";
export * from "./totals";

View File

@ -1,10 +1,14 @@
import { PublicKey, SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY, TransactionInstruction } from '@solana/web3.js';
import BN from 'bn.js';
import * as BufferLayout from 'buffer-layout';
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from '../../utils/ids';
import * as Layout from './../../utils/layout';
import { LendingInstruction } from './lending';
import { calculateUtilizationRatio, LendingReserve } from './reserve';
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction,
} from "@solana/web3.js";
import BN from "bn.js";
import * as BufferLayout from "buffer-layout";
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from "../../utils/ids";
import * as Layout from "./../../utils/layout";
import { LendingInstruction } from "./lending";
import { calculateUtilizationRatio, LendingReserve } from "./reserve";
export enum BorrowAmountType {
LiquidityBorrowAmount = 0,
@ -17,23 +21,21 @@ export enum BorrowAmountType {
/// 0. `[writable]` Source collateral token account, minted by deposit reserve collateral mint,
/// $authority can transfer $collateral_amount
/// 1. `[writable]` Destination liquidity token account, minted by borrow reserve liquidity mint
/// 2. `[writable]` Deposit reserve account.
/// 2. `[]` Deposit reserve account.
/// 3. `[writable]` Deposit reserve collateral supply SPL Token account
/// 4. `[writable]` Borrow reserve account.
/// 5. `[writable]` Borrow reserve liquidity supply SPL Token account
/// 6. `[writable]` Obligation
/// 7. `[writable]` Obligation token mint
/// 8. `[writable]` Obligation token output
/// 9. `[]` Obligation token owner
/// 10 `[]` Lending market account.
/// 11 `[]` Derived lending market authority.
/// 12 `[]` User transfer authority ($authority).
/// 13 `[]` Dex market
/// 14 `[]` Dex market order book side
/// 15 `[]` Temporary memory
/// 16 `[]` Clock sysvar
/// 17 `[]` Rent sysvar
/// 18 '[]` Token program id
/// 8 `[]` Lending market account.
/// 10 `[]` Derived lending market authority.
/// 11 `[]` User transfer authority ($authority).
/// 12 `[]` Dex market
/// 13 `[]` Dex market order book side
/// 14 `[]` Temporary memory
/// 15 `[]` Clock sysvar
/// 16 '[]` Token program id
export const borrowInstruction = (
amount: number | BN,
amountType: BorrowAmountType,
@ -49,7 +51,6 @@ export const borrowInstruction = (
obligation: PublicKey,
obligationMint: PublicKey,
obligationTokenOutput: PublicKey,
obligationTokenOwner: PublicKey,
lendingMarket: PublicKey,
lendingMarketAuthority: PublicKey,
@ -60,12 +61,12 @@ export const borrowInstruction = (
memory: PublicKey,
hostFeeReceiver?: PublicKey,
hostFeeReceiver?: PublicKey
): TransactionInstruction => {
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
Layout.uint64('amount'),
BufferLayout.u8('amountType'),
BufferLayout.u8("instruction"),
Layout.uint64("amount"),
BufferLayout.u8("amountType"),
]);
const data = Buffer.alloc(dataLayout.span);
@ -81,7 +82,7 @@ export const borrowInstruction = (
const keys = [
{ pubkey: from, isSigner: false, isWritable: true },
{ pubkey: to, isSigner: false, isWritable: true },
{ pubkey: depositReserve, isSigner: false, isWritable: true },
{ pubkey: depositReserve, isSigner: false, isWritable: false },
{
pubkey: depositReserveCollateralSupply,
isSigner: false,
@ -98,7 +99,6 @@ export const borrowInstruction = (
{ pubkey: obligation, isSigner: false, isWritable: true },
{ pubkey: obligationMint, isSigner: false, isWritable: true },
{ pubkey: obligationTokenOutput, isSigner: false, isWritable: true },
{ pubkey: obligationTokenOwner, isSigner: false, isWritable: false },
{ pubkey: lendingMarket, isSigner: false, isWritable: false },
{ pubkey: lendingMarketAuthority, isSigner: false, isWritable: false },
@ -108,12 +108,11 @@ export const borrowInstruction = (
{ pubkey: dexOrderBookSide, isSigner: false, isWritable: false },
{ pubkey: memory, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
];
if(hostFeeReceiver) {
keys.push({ pubkey: hostFeeReceiver, isSigner: false, isWritable: true })
if (hostFeeReceiver) {
keys.push({ pubkey: hostFeeReceiver, isSigner: false, isWritable: true });
}
return new TransactionInstruction({
@ -134,12 +133,16 @@ export const calculateBorrowAPY = (reserve: LendingReserve) => {
const normalizedFactor = currentUtilization / optimalUtilization;
const optimalBorrowRate = reserve.config.optimalBorrowRate / 100;
const minBorrowRate = reserve.config.minBorrowRate / 100;
borrowAPY = normalizedFactor * (optimalBorrowRate - minBorrowRate) + minBorrowRate;
borrowAPY =
normalizedFactor * (optimalBorrowRate - minBorrowRate) + minBorrowRate;
} else {
const normalizedFactor = (currentUtilization - optimalUtilization) / (1 - optimalUtilization);
const normalizedFactor =
(currentUtilization - optimalUtilization) / (1 - optimalUtilization);
const optimalBorrowRate = reserve.config.optimalBorrowRate / 100;
const maxBorrowRate = reserve.config.maxBorrowRate / 100;
borrowAPY = normalizedFactor * (maxBorrowRate - optimalBorrowRate) + optimalBorrowRate;
borrowAPY =
normalizedFactor * (maxBorrowRate - optimalBorrowRate) +
optimalBorrowRate;
}
return borrowAPY;

View File

@ -1,11 +1,15 @@
import { PublicKey, SYSVAR_CLOCK_PUBKEY, TransactionInstruction } from '@solana/web3.js';
import BN from 'bn.js';
import * as BufferLayout from 'buffer-layout';
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from '../../utils/ids';
import * as Layout from './../../utils/layout';
import { calculateBorrowAPY } from './borrow';
import { LendingInstruction } from './lending';
import { calculateUtilizationRatio, LendingReserve } from './reserve';
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction,
} from "@solana/web3.js";
import BN from "bn.js";
import * as BufferLayout from "buffer-layout";
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from "../../utils/ids";
import * as Layout from "./../../utils/layout";
import { calculateBorrowAPY } from "./borrow";
import { LendingInstruction } from "./lending";
import { calculateUtilizationRatio, LendingReserve } from "./reserve";
/// Deposit liquidity into a reserve. The output is a collateral token representing ownership
/// of the reserve liquidity pool.
@ -32,8 +36,8 @@ export const depositInstruction = (
collateralMint: PublicKey
): TransactionInstruction => {
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
Layout.uint64('liquidityAmount')
BufferLayout.u8("instruction"),
Layout.uint64("liquidityAmount"),
]);
const data = Buffer.alloc(dataLayout.span);

View File

@ -1,9 +1,11 @@
export enum LendingInstruction {
InitLendingMarket = 0,
InitReserve = 1,
DepositReserveLiquidity = 2,
WithdrawReserveLiquidity = 3,
BorrowLiquidity = 4,
RepayOblogationLiquidity = 5,
LiquidateObligation = 6,
InitObligation = 2,
DepositReserveLiquidity = 3,
WithdrawReserveLiquidity = 4,
BorrowLiquidity = 5,
RepayOblogationLiquidity = 6,
LiquidateObligation = 7,
AccrueReserveInterest = 8,
}

View File

@ -1,16 +1,20 @@
import { PublicKey, SYSVAR_CLOCK_PUBKEY, TransactionInstruction } from '@solana/web3.js';
import BN from 'bn.js';
import { LendingInstruction } from './lending';
import * as BufferLayout from 'buffer-layout';
import * as Layout from './../../utils/layout';
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from '../../utils/ids';
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction,
} from "@solana/web3.js";
import BN from "bn.js";
import { LendingInstruction } from "./lending";
import * as BufferLayout from "buffer-layout";
import * as Layout from "./../../utils/layout";
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from "../../utils/ids";
/// Purchase collateral tokens at a discount rate if the chosen obligation is unhealthy.
///
/// 0. `[writable]` Source liquidity token account, minted by repay reserve liquidity mint
/// $authority can transfer $collateral_amount
/// 1. `[writable]` Destination collateral token account, minted by withdraw reserve collateral mint
/// 2. `[writable]` Repay reserve account.
/// 2. `[]` Repay reserve account.
/// 3. `[writable]` Repay reserve liquidity supply SPL Token account
/// 4. `[writable]` Withdraw reserve account.
/// 5. `[writable]` Withdraw reserve collateral supply SPL Token account
@ -39,7 +43,10 @@ export const liquidateInstruction = (
dexOrderBookSide: PublicKey,
memory: PublicKey
): TransactionInstruction => {
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction'), Layout.uint64('liquidityAmount')]);
const dataLayout = BufferLayout.struct([
BufferLayout.u8("instruction"),
Layout.uint64("liquidityAmount"),
]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
@ -57,7 +64,7 @@ export const liquidateInstruction = (
{ pubkey: repayReserveAccount, isSigner: false, isWritable: true },
{ pubkey: repayReserveLiquiditySupply, isSigner: false, isWritable: true },
{ pubkey: withdrawReserve, isSigner: false, isWritable: true },
{ pubkey: withdrawReserve, isSigner: false, isWritable: false },
{
pubkey: withdrawReserveCollateralSupply,
isSigner: false,

View File

@ -4,14 +4,15 @@ import * as Layout from "./../../utils/layout";
export const LendingMarketLayout: typeof BufferLayout.Structure = BufferLayout.struct(
[
BufferLayout.u8('version'),
BufferLayout.u8('bumpSeed'),
BufferLayout.u8("version"),
BufferLayout.u8("bumpSeed"),
Layout.publicKey("owner"),
Layout.publicKey("quoteMint"),
Layout.publicKey("tokenProgramId"),
// extra space for future contract changes
BufferLayout.blob(62, "padding"),
],
]
);
export interface LendingMarket {
@ -19,7 +20,7 @@ export interface LendingMarket {
isInitialized: boolean;
quoteMint: PublicKey;
tokenProgramId: PublicKey,
tokenProgramId: PublicKey;
}
export const isLendingMarket = (info: AccountInfo<Buffer>) => {

View File

@ -1,11 +1,19 @@
import { AccountInfo, PublicKey } from "@solana/web3.js";
import {
AccountInfo,
PublicKey,
SYSVAR_CLOCK_PUBKEY,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from "@solana/web3.js";
import BN from "bn.js";
import * as BufferLayout from "buffer-layout";
import { LendingInstruction } from ".";
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../utils/ids";
import * as Layout from "./../../utils/layout";
export const LendingObligationLayout: typeof BufferLayout.Structure = BufferLayout.struct(
[
BufferLayout.u8('version'),
BufferLayout.u8("version"),
/// Amount of collateral tokens deposited for this obligation
Layout.uint64("depositedCollateral"),
/// Reserve which collateral tokens were deposited into
@ -58,5 +66,47 @@ export const LendingObligationParser = (
};
export const healthFactorToRiskColor = (health: number) => {
return '';
}
return "";
};
export const initObligationInstruction = (
depositReserve: PublicKey,
borrowReserve: PublicKey,
obligation: PublicKey,
obligationMint: PublicKey,
obligationTokenOutput: PublicKey,
obligationTokenOwner: PublicKey,
lendingMarket: PublicKey,
lendingMarketAuthority: PublicKey
): TransactionInstruction => {
const dataLayout = BufferLayout.struct([BufferLayout.u8("instruction")]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: LendingInstruction.InitObligation,
},
data
);
const keys = [
{ pubkey: depositReserve, isSigner: false, isWritable: false },
{ pubkey: borrowReserve, isSigner: false, isWritable: false },
{ pubkey: obligation, isSigner: false, isWritable: true },
{ pubkey: obligationMint, isSigner: false, isWritable: true },
{ pubkey: obligationTokenOutput, isSigner: false, isWritable: true },
{ pubkey: obligationTokenOwner, isSigner: false, isWritable: false },
{ pubkey: lendingMarket, isSigner: false, isWritable: false },
{ pubkey: lendingMarketAuthority, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
];
return new TransactionInstruction({
keys,
programId: LENDING_PROGRAM_ID,
data,
});
};

View File

@ -1,9 +1,13 @@
import { PublicKey, SYSVAR_CLOCK_PUBKEY, TransactionInstruction } from '@solana/web3.js';
import BN from 'bn.js';
import { LendingInstruction } from './lending';
import * as BufferLayout from 'buffer-layout';
import * as Layout from './../../utils/layout';
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from '../../utils/ids';
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction,
} from "@solana/web3.js";
import BN from "bn.js";
import { LendingInstruction } from "./lending";
import * as BufferLayout from "buffer-layout";
import * as Layout from "./../../utils/layout";
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from "../../utils/ids";
/// Repay loaned tokens to a reserve and receive collateral tokens. The obligation balance
/// will be recalculated for interest.
@ -36,9 +40,12 @@ export const repayInstruction = (
obligationInput: PublicKey,
lendingMarket: PublicKey,
authority: PublicKey,
transferAuthority: PublicKey,
transferAuthority: PublicKey
): TransactionInstruction => {
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction'), Layout.uint64('liquidityAmount')]);
const dataLayout = BufferLayout.struct([
BufferLayout.u8("instruction"),
Layout.uint64("liquidityAmount"),
]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(

View File

@ -4,80 +4,82 @@ import {
SYSVAR_CLOCK_PUBKEY,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js';
import BN from 'bn.js';
import * as BufferLayout from 'buffer-layout';
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from '../../utils/ids';
import { wadToLamports } from '../../utils/utils';
import * as Layout from './../../utils/layout';
import { LendingInstruction } from './lending';
} from "@solana/web3.js";
import BN from "bn.js";
import * as BufferLayout from "buffer-layout";
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from "../../utils/ids";
import { wadToLamports } from "../../utils/utils";
import * as Layout from "./../../utils/layout";
import { LendingInstruction } from "./lending";
export const LendingReserveLayout: typeof BufferLayout.Structure = BufferLayout.struct([
BufferLayout.u8('version'),
Layout.uint64('lastUpdateSlot'),
export const LendingReserveLayout: typeof BufferLayout.Structure = BufferLayout.struct(
[
BufferLayout.u8("version"),
Layout.uint64("lastUpdateSlot"),
Layout.publicKey('lendingMarket'),
Layout.publicKey('liquidityMint'),
BufferLayout.u8('liquidityMintDecimals'),
Layout.publicKey('liquiditySupply'),
Layout.publicKey('collateralMint'),
Layout.publicKey('collateralSupply'),
Layout.publicKey("lendingMarket"),
Layout.publicKey("liquidityMint"),
BufferLayout.u8("liquidityMintDecimals"),
Layout.publicKey("liquiditySupply"),
Layout.publicKey("collateralMint"),
Layout.publicKey("collateralSupply"),
Layout.publicKey('collateralFeesReceiver'),
Layout.publicKey("collateralFeesReceiver"),
// TODO: replace u32 option with generic quivalent
BufferLayout.u32('dexMarketOption'),
Layout.publicKey('dexMarket'),
// TODO: replace u32 option with generic quivalent
BufferLayout.u32("dexMarketOption"),
Layout.publicKey("dexMarket"),
BufferLayout.struct(
[
/// 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'),
BufferLayout.struct(
[
/// 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"),
BufferLayout.struct(
[
/// Fee assessed on `BorrowReserveLiquidity`, expressed as a Wad.
/// Must be between 0 and 10^18, such that 10^18 = 1. A few examples for
/// clarity:
/// 1% = 10_000_000_000_000_000
/// 0.01% (1 basis point) = 100_000_000_000_000
/// 0.00001% (Aave borrow fee) = 100_000_000_000
Layout.uint64('borrowFeeWad'),
BufferLayout.struct(
[
/// Fee assessed on `BorrowReserveLiquidity`, expressed as a Wad.
/// Must be between 0 and 10^18, such that 10^18 = 1. A few examples for
/// clarity:
/// 1% = 10_000_000_000_000_000
/// 0.01% (1 basis point) = 100_000_000_000_000
/// 0.00001% (Aave borrow fee) = 100_000_000_000
Layout.uint64("borrowFeeWad"),
/// Amount of fee going to host account, if provided in liquidate and repay
BufferLayout.u8('hostFeePercentage'),
],
'fees'
),
],
'config'
),
/// Amount of fee going to host account, if provided in liquidate and repay
BufferLayout.u8("hostFeePercentage"),
],
"fees"
),
],
"config"
),
BufferLayout.struct(
[
Layout.uint128('cumulativeBorrowRateWad'),
Layout.uint128('borrowedLiquidityWad'),
Layout.uint64('availableLiquidity'),
Layout.uint64('collateralMintSupply'),
],
'state'
),
BufferLayout.struct(
[
Layout.uint128("cumulativeBorrowRateWad"),
Layout.uint128("borrowedLiquidityWad"),
Layout.uint64("availableLiquidity"),
Layout.uint64("collateralMintSupply"),
],
"state"
),
// extra space for future contract changes
BufferLayout.blob(300, "padding"),
]);
// extra space for future contract changes
BufferLayout.blob(300, "padding"),
]
);
export const isLendingReserve = (info: AccountInfo<Buffer>) => {
return info.data.length === LendingReserveLayout.span;
@ -108,7 +110,7 @@ export interface LendingReserve {
maxBorrowRate: number;
fees: {
borrowFeeWad: BN;
borrowFeeWad: BN;
hostFeePercentage: number;
};
};
@ -122,7 +124,10 @@ export interface LendingReserve {
};
}
export const LendingReserveParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
export const LendingReserveParser = (
pubKey: PublicKey,
info: AccountInfo<Buffer>
) => {
const buffer = Buffer.from(info.data);
const data = LendingReserveLayout.decode(buffer) as LendingReserve;
@ -138,8 +143,6 @@ export const LendingReserveParser = (pubKey: PublicKey, info: AccountInfo<Buffer
info: data,
};
return details;
};
@ -162,9 +165,9 @@ export const initReserveInstruction = (
dexMarket: PublicKey // TODO: optional
): TransactionInstruction => {
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
Layout.uint64('liquidityAmount'),
BufferLayout.u8('maxUtilizationRate'),
BufferLayout.u8("instruction"),
Layout.uint64("liquidityAmount"),
BufferLayout.u8("maxUtilizationRate"),
]);
const data = Buffer.alloc(dataLayout.span);
@ -204,31 +207,79 @@ export const initReserveInstruction = (
});
};
export const accrueInterestInstruction = (
...reserveAccount: PublicKey[]
): TransactionInstruction => {
const dataLayout = BufferLayout.struct([BufferLayout.u8("instruction")]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: LendingInstruction.AccrueReserveInterest,
},
data
);
const keys = [
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
...reserveAccount.map((reserve) => ({
pubkey: reserve,
isSigner: false,
isWritable: true,
})),
];
return new TransactionInstruction({
keys,
programId: LENDING_PROGRAM_ID,
data,
});
};
export const calculateUtilizationRatio = (reserve: LendingReserve) => {
const totalBorrows = wadToLamports(reserve.state.borrowedLiquidityWad).toNumber();
const currentUtilization = totalBorrows / (reserve.state.availableLiquidity.toNumber() + totalBorrows);
const totalBorrows = wadToLamports(
reserve.state.borrowedLiquidityWad
).toNumber();
const currentUtilization =
totalBorrows / (reserve.state.availableLiquidity.toNumber() + totalBorrows);
return currentUtilization;
};
export const reserveMarketCap = (reserve?: LendingReserve) => {
const available = reserve?.state.availableLiquidity.toNumber() || 0;
const borrowed = wadToLamports(reserve?.state.borrowedLiquidityWad).toNumber();
const borrowed = wadToLamports(
reserve?.state.borrowedLiquidityWad
).toNumber();
const total = available + borrowed;
return total;
};
export const collateralExchangeRate = (reserve?: LendingReserve) => {
return (reserve?.state.collateralMintSupply.toNumber() || 1) / reserveMarketCap(reserve);
return (
(reserve?.state.collateralMintSupply.toNumber() || 1) /
reserveMarketCap(reserve)
);
};
export const collateralToLiquidity = (collateralAmount: BN | number, reserve?: LendingReserve) => {
const amount = typeof collateralAmount === 'number' ? collateralAmount : collateralAmount.toNumber();
export const collateralToLiquidity = (
collateralAmount: BN | number,
reserve?: LendingReserve
) => {
const amount =
typeof collateralAmount === "number"
? collateralAmount
: collateralAmount.toNumber();
return Math.floor(amount / collateralExchangeRate(reserve));
};
export const liquidityToCollateral = (liquidityAmount: BN | number, reserve?: LendingReserve) => {
const amount = typeof liquidityAmount === 'number' ? liquidityAmount : liquidityAmount.toNumber();
export const liquidityToCollateral = (
liquidityAmount: BN | number,
reserve?: LendingReserve
) => {
const amount =
typeof liquidityAmount === "number"
? liquidityAmount
: liquidityAmount.toNumber();
return Math.floor(amount * collateralExchangeRate(reserve));
};

View File

@ -1,9 +1,13 @@
import { PublicKey, SYSVAR_CLOCK_PUBKEY, TransactionInstruction } from '@solana/web3.js';
import BN from 'bn.js';
import * as BufferLayout from 'buffer-layout';
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from '../../utils/ids';
import * as Layout from './../../utils/layout';
import { LendingInstruction } from './lending';
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction,
} from "@solana/web3.js";
import BN from "bn.js";
import * as BufferLayout from "buffer-layout";
import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from "../../utils/ids";
import * as Layout from "./../../utils/layout";
import { LendingInstruction } from "./lending";
export const withdrawInstruction = (
collateralAmount: number | BN,
@ -14,9 +18,12 @@ export const withdrawInstruction = (
reserveSupply: PublicKey,
lendingMarket: PublicKey,
authority: PublicKey,
transferAuthority: PublicKey,
transferAuthority: PublicKey
): TransactionInstruction => {
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction'), Layout.uint64('collateralAmount')]);
const dataLayout = BufferLayout.struct([
BufferLayout.u8("instruction"),
Layout.uint64("collateralAmount"),
]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(

View File

@ -1,5 +1,5 @@
import { PublicKey } from '@solana/web3.js';
import { TokenAccount } from './account';
import { PublicKey } from "@solana/web3.js";
import { TokenAccount } from "./account";
export const DEFAULT_DENOMINATOR = 10_000;

View File

@ -1,68 +1,84 @@
import * as BufferLayout from 'buffer-layout';
import { publicKey, uint64 } from '../utils/layout';
import * as BufferLayout from "buffer-layout";
import { publicKey, uint64 } from "../utils/layout";
export { TokenSwap } from '@solana/spl-token-swap';
export { TokenSwap } from "@solana/spl-token-swap";
const FEE_LAYOUT = BufferLayout.struct(
[
BufferLayout.nu64('tradeFeeNumerator'),
BufferLayout.nu64('tradeFeeDenominator'),
BufferLayout.nu64('ownerTradeFeeNumerator'),
BufferLayout.nu64('ownerTradeFeeDenominator'),
BufferLayout.nu64('ownerWithdrawFeeNumerator'),
BufferLayout.nu64('ownerWithdrawFeeDenominator'),
BufferLayout.nu64('hostFeeNumerator'),
BufferLayout.nu64('hostFeeDenominator'),
BufferLayout.nu64("tradeFeeNumerator"),
BufferLayout.nu64("tradeFeeDenominator"),
BufferLayout.nu64("ownerTradeFeeNumerator"),
BufferLayout.nu64("ownerTradeFeeDenominator"),
BufferLayout.nu64("ownerWithdrawFeeNumerator"),
BufferLayout.nu64("ownerWithdrawFeeDenominator"),
BufferLayout.nu64("hostFeeNumerator"),
BufferLayout.nu64("hostFeeDenominator"),
],
'fees'
"fees"
);
export const TokenSwapLayoutLegacyV0 = BufferLayout.struct([
BufferLayout.u8('isInitialized'),
BufferLayout.u8('nonce'),
publicKey('tokenAccountA'),
publicKey('tokenAccountB'),
publicKey('tokenPool'),
uint64('feesNumerator'),
uint64('feesDenominator'),
BufferLayout.u8("isInitialized"),
BufferLayout.u8("nonce"),
publicKey("tokenAccountA"),
publicKey("tokenAccountB"),
publicKey("tokenPool"),
uint64("feesNumerator"),
uint64("feesDenominator"),
]);
export const TokenSwapLayoutV1: typeof BufferLayout.Structure = BufferLayout.struct([
BufferLayout.u8('isInitialized'),
BufferLayout.u8('nonce'),
publicKey('tokenProgramId'),
publicKey('tokenAccountA'),
publicKey('tokenAccountB'),
publicKey('tokenPool'),
publicKey('mintA'),
publicKey('mintB'),
publicKey('feeAccount'),
BufferLayout.u8('curveType'),
uint64('tradeFeeNumerator'),
uint64('tradeFeeDenominator'),
uint64('ownerTradeFeeNumerator'),
uint64('ownerTradeFeeDenominator'),
uint64('ownerWithdrawFeeNumerator'),
uint64('ownerWithdrawFeeDenominator'),
BufferLayout.blob(16, 'padding'),
]);
export const TokenSwapLayoutV1: typeof BufferLayout.Structure = BufferLayout.struct(
[
BufferLayout.u8("isInitialized"),
BufferLayout.u8("nonce"),
publicKey("tokenProgramId"),
publicKey("tokenAccountA"),
publicKey("tokenAccountB"),
publicKey("tokenPool"),
publicKey("mintA"),
publicKey("mintB"),
publicKey("feeAccount"),
BufferLayout.u8("curveType"),
uint64("tradeFeeNumerator"),
uint64("tradeFeeDenominator"),
uint64("ownerTradeFeeNumerator"),
uint64("ownerTradeFeeDenominator"),
uint64("ownerWithdrawFeeNumerator"),
uint64("ownerWithdrawFeeDenominator"),
BufferLayout.blob(16, "padding"),
]
);
const CURVE_NODE = BufferLayout.union(BufferLayout.u8(), BufferLayout.blob(32), 'curve');
CURVE_NODE.addVariant(0, BufferLayout.struct([]), 'constantProduct');
CURVE_NODE.addVariant(1, BufferLayout.struct([BufferLayout.nu64('token_b_price')]), 'constantPrice');
CURVE_NODE.addVariant(2, BufferLayout.struct([]), 'stable');
CURVE_NODE.addVariant(3, BufferLayout.struct([BufferLayout.nu64('token_b_offset')]), 'offset');
const CURVE_NODE = BufferLayout.union(
BufferLayout.u8(),
BufferLayout.blob(32),
"curve"
);
CURVE_NODE.addVariant(0, BufferLayout.struct([]), "constantProduct");
CURVE_NODE.addVariant(
1,
BufferLayout.struct([BufferLayout.nu64("token_b_price")]),
"constantPrice"
);
CURVE_NODE.addVariant(2, BufferLayout.struct([]), "stable");
CURVE_NODE.addVariant(
3,
BufferLayout.struct([BufferLayout.nu64("token_b_offset")]),
"offset"
);
export const TokenSwapLayout: typeof BufferLayout.Structure = BufferLayout.struct([
BufferLayout.u8('isInitialized'),
BufferLayout.u8('nonce'),
publicKey('tokenProgramId'),
publicKey('tokenAccountA'),
publicKey('tokenAccountB'),
publicKey('tokenPool'),
publicKey('mintA'),
publicKey('mintB'),
publicKey('feeAccount'),
FEE_LAYOUT,
CURVE_NODE,
]);
export const TokenSwapLayout: typeof BufferLayout.Structure = BufferLayout.struct(
[
BufferLayout.u8("isInitialized"),
BufferLayout.u8("nonce"),
publicKey("tokenProgramId"),
publicKey("tokenAccountA"),
publicKey("tokenAccountB"),
publicKey("tokenPool"),
publicKey("mintA"),
publicKey("mintB"),
publicKey("feeAccount"),
FEE_LAYOUT,
CURVE_NODE,
]
);

View File

@ -1,11 +1,11 @@
import { HashRouter, Route, Switch } from 'react-router-dom';
import React from 'react';
import { WalletProvider } from './contexts/wallet';
import { ConnectionProvider } from './contexts/connection';
import { AccountsProvider } from './contexts/accounts';
import { MarketProvider } from './contexts/market';
import { LendingProvider } from './contexts/lending';
import { AppLayout } from './components/Layout';
import { HashRouter, Route, Switch } from "react-router-dom";
import React from "react";
import { WalletProvider } from "./contexts/wallet";
import { ConnectionProvider } from "./contexts/connection";
import { AccountsProvider } from "./contexts/accounts";
import { MarketProvider } from "./contexts/market";
import { LendingProvider } from "./contexts/lending";
import { AppLayout } from "./components/Layout";
import {
BorrowReserveView,
@ -21,13 +21,13 @@ import {
LiquidateView,
LiquidateReserveView,
MarginTrading,
} from './views';
import { NewPosition } from './views/margin/newPosition';
} from "./views";
import { NewPosition } from "./views/margin/newPosition";
export function Routes() {
return (
<>
<HashRouter basename={'/'}>
<HashRouter basename={"/"}>
<ConnectionProvider>
<WalletProvider>
<AccountsProvider>
@ -35,22 +35,53 @@ export function Routes() {
<LendingProvider>
<AppLayout>
<Switch>
<Route exact path='/' component={() => <HomeView />} />
<Route exact path='/dashboard' children={<DashboardView />} />
<Route path='/reserve/:id' children={<ReserveView />} />
<Route exact path='/deposit' component={() => <DepositView />} />
<Route path='/deposit/:id' children={<DepositReserveView />} />
<Route path='/withdraw/:id' children={<WithdrawView />} />
<Route exact path='/borrow' children={<BorrowView />} />
<Route path='/borrow/:id' children={<BorrowReserveView />} />
<Route path='/repay/loan/:obligation' children={<RepayReserveView />} />
<Route path='/repay/:reserve' children={<RepayReserveView />} />
<Route exact path='/liquidate' children={<LiquidateView />} />
<Route path='/liquidate/:id' children={<LiquidateReserveView />} />
<Route exact path='/margin' children={<MarginTrading />} />
<Route exact path="/" component={() => <HomeView />} />
<Route
exact
path="/dashboard"
children={<DashboardView />}
/>
<Route path="/reserve/:id" children={<ReserveView />} />
<Route
exact
path="/deposit"
component={() => <DepositView />}
/>
<Route
path="/deposit/:id"
children={<DepositReserveView />}
/>
<Route path="/withdraw/:id" children={<WithdrawView />} />
<Route exact path="/borrow" children={<BorrowView />} />
<Route
path="/borrow/:id"
children={<BorrowReserveView />}
/>
<Route
path="/repay/loan/:obligation"
children={<RepayReserveView />}
/>
<Route
path="/repay/:reserve"
children={<RepayReserveView />}
/>
<Route
exact
path="/liquidate"
children={<LiquidateView />}
/>
<Route
path="/liquidate/:id"
children={<LiquidateReserveView />}
/>
<Route
exact
path="/margin"
children={<MarginTrading />}
/>
<Route path='/margin/:id' children={<NewPosition />} />
<Route exact path='/faucet' children={<FaucetView />} />
<Route path="/margin/:id" children={<NewPosition />} />
<Route exact path="/faucet" children={<FaucetView />} />
</Switch>
</AppLayout>
</LendingProvider>

View File

@ -1,7 +1,7 @@
import { EventEmitter as Emitter } from 'eventemitter3';
import { EventEmitter as Emitter } from "eventemitter3";
export class CacheUpdateEvent {
static type = 'CacheUpdate';
static type = "CacheUpdate";
id: string;
parser: any;
isNew: boolean;
@ -13,7 +13,7 @@ export class CacheUpdateEvent {
}
export class CacheDeleteEvent {
static type = 'CacheUpdate';
static type = "CacheUpdate";
id: string;
constructor(id: string) {
this.id = id;
@ -21,7 +21,7 @@ export class CacheDeleteEvent {
}
export class MarketUpdateEvent {
static type = 'MarketUpdate';
static type = "MarketUpdate";
ids: Set<string>;
constructor(ids: Set<string>) {
this.ids = ids;
@ -48,7 +48,10 @@ export class EventEmitter {
}
raiseCacheUpdated(id: string, isNew: boolean, parser: any) {
this.emitter.emit(CacheUpdateEvent.type, new CacheUpdateEvent(id, isNew, parser));
this.emitter.emit(
CacheUpdateEvent.type,
new CacheUpdateEvent(id, isNew, parser)
);
}
raiseCacheDeleted(id: string) {

View File

@ -1,10 +1,16 @@
import { PublicKey } from '@solana/web3.js';
import { TokenSwapLayout, TokenSwapLayoutV1 } from '../models';
import { PublicKey } from "@solana/web3.js";
import { TokenSwapLayout, TokenSwapLayoutV1 } from "../models";
export const WRAPPED_SOL_MINT = new PublicKey('So11111111111111111111111111111111111111112');
export let TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
export const WRAPPED_SOL_MINT = new PublicKey(
"So11111111111111111111111111111111111111112"
);
export let TOKEN_PROGRAM_ID = new PublicKey(
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
);
export let LENDING_PROGRAM_ID = new PublicKey('TokenLending1111111111111111111111111111111');
export let LENDING_PROGRAM_ID = new PublicKey(
"TokenLending1111111111111111111111111111111"
);
let SWAP_PROGRAM_ID: PublicKey;
let SWAP_PROGRAM_LEGACY_IDS: PublicKey[];
@ -14,17 +20,17 @@ export const LEND_HOST_FEE_ADDRESS = process.env.REACT_APP_LEND_HOST_FEE_ADDRESS
? new PublicKey(`${process.env.REACT_APP_LEND_HOST_FEE_ADDRESS}`)
: undefined;
console.debug(`Lend host fee address: ${LEND_HOST_FEE_ADDRESS?.toBase58()}`);
console.debug(`Lend host fee address: ${LEND_HOST_FEE_ADDRESS?.toBase58()}`);
export const ENABLE_FEES_INPUT = false;
// legacy pools are used to show users contributions in those pools to allow for withdrawals of funds
export const PROGRAM_IDS = [
{
name: 'mainnet-beta',
name: "mainnet-beta",
swap: () => ({
current: {
pubkey: new PublicKey('9qvG1zUp8xF1Bi4m6UdRNby1BAAuaDrUxSpv4CmRRMjL'),
pubkey: new PublicKey("9qvG1zUp8xF1Bi4m6UdRNby1BAAuaDrUxSpv4CmRRMjL"),
layout: TokenSwapLayoutV1,
},
legacy: [
@ -34,30 +40,30 @@ export const PROGRAM_IDS = [
}),
},
{
name: 'testnet',
name: "testnet",
swap: () => ({
current: {
pubkey: new PublicKey('2n2dsFSgmPcZ8jkmBZLGUM2nzuFqcBGQ3JEEj6RJJcEg'),
pubkey: new PublicKey("2n2dsFSgmPcZ8jkmBZLGUM2nzuFqcBGQ3JEEj6RJJcEg"),
layout: TokenSwapLayoutV1,
},
legacy: [],
}),
},
{
name: 'devnet',
name: "devnet",
swap: () => ({
current: {
pubkey: new PublicKey('6Cust2JhvweKLh4CVo1dt21s2PJ86uNGkziudpkNPaCj'),
pubkey: new PublicKey("6Cust2JhvweKLh4CVo1dt21s2PJ86uNGkziudpkNPaCj"),
layout: TokenSwapLayout,
},
legacy: [new PublicKey('BSfTAcBdqmvX5iE2PW88WFNNp2DHhLUaBKk5WrnxVkcJ')],
legacy: [new PublicKey("BSfTAcBdqmvX5iE2PW88WFNNp2DHhLUaBKk5WrnxVkcJ")],
}),
},
{
name: 'localnet',
name: "localnet",
swap: () => ({
current: {
pubkey: new PublicKey('369YmCWHGxznT7GGBhcLZDRcRoGWmGKFWdmtiPy78yj7'),
pubkey: new PublicKey("369YmCWHGxznT7GGBhcLZDRcRoGWmGKFWdmtiPy78yj7"),
layout: TokenSwapLayoutV1,
},
legacy: [],
@ -77,8 +83,10 @@ export const setProgramIds = (envName: string) => {
SWAP_PROGRAM_LAYOUT = swap.current.layout;
SWAP_PROGRAM_LEGACY_IDS = swap.legacy;
if (envName === 'mainnet-beta') {
LENDING_PROGRAM_ID = new PublicKey('2KfJP7pZ6QSpXa26RmsN6kKVQteDEdQmizLSvuyryeiW');
if (envName === "mainnet-beta") {
LENDING_PROGRAM_ID = new PublicKey(
"2KfJP7pZ6QSpXa26RmsN6kKVQteDEdQmizLSvuyryeiW"
);
}
};

View File

@ -1,20 +1,23 @@
import { Connection, PublicKey } from '@solana/web3.js';
import { useEffect, useMemo, useState } from 'react';
import { MintLayout, AccountLayout } from '@solana/spl-token';
import { programIds } from './ids';
import { Connection, PublicKey } from "@solana/web3.js";
import { useEffect, useMemo, useState } from "react";
import { MintLayout, AccountLayout } from "@solana/spl-token";
import { programIds } from "./ids";
import {
PoolInfo,
TokenSwapLayout,
TokenSwapLayoutLegacyV0 as TokenSwapLayoutV0,
TokenSwapLayoutV1,
} from './../models';
import { useConnection } from '../contexts/connection';
import { cache, getMultipleAccounts, TokenAccountParser } from '../contexts/accounts';
} from "./../models";
import { useConnection } from "../contexts/connection";
import {
cache,
getMultipleAccounts,
TokenAccountParser,
} from "../contexts/accounts";
export const LIQUIDITY_PROVIDER_FEE = 0.003;
export const SERUM_FEE = 0.0005;
const getHoldings = (connection: Connection, accounts: string[]) => {
return accounts.map((acc) => cache.query(connection, new PublicKey(acc)));
};
@ -77,10 +80,16 @@ export const usePools = () => {
// TODO: this is not great
// Ideally SwapLayout stores hash of all the mints to make finding of pool for a pair easier
const holdings = await Promise.all(
getHoldings(connection, [result.data.tokenAccountA, result.data.tokenAccountB])
getHoldings(connection, [
result.data.tokenAccountA,
result.data.tokenAccountB,
])
);
pool.pubkeys.holdingMints = [holdings[0].info.mint, holdings[1].info.mint] as PublicKey[];
pool.pubkeys.holdingMints = [
holdings[0].info.mint,
holdings[1].info.mint,
] as PublicKey[];
} catch (err) {
console.log(err);
}
@ -91,7 +100,10 @@ export const usePools = () => {
let pool = toPoolInfo(result, swapId);
pool.legacy = isLegacy;
pool.pubkeys.feeAccount = result.data.feeAccount;
pool.pubkeys.holdingMints = [result.data.mintA, result.data.mintB] as PublicKey[];
pool.pubkeys.holdingMints = [
result.data.mintA,
result.data.mintB,
] as PublicKey[];
poolsArray.push(pool as PoolInfo);
}
@ -113,28 +125,31 @@ export const usePools = () => {
// This will pre-cache all accounts used by pools
// All those accounts are updated whenever there is a change
await getMultipleAccounts(connection, toQuery, 'single').then(({ keys, array }) => {
return array.map((obj, index) => {
const pubKey = keys[index];
if (obj.data.length === AccountLayout.span) {
return cache.add(pubKey, obj, TokenAccountParser);
} else if (obj.data.length === MintLayout.span) {
if (!cache.getMint(pubKey)) {
return cache.addMint(new PublicKey(pubKey), obj);
await getMultipleAccounts(connection, toQuery, "single").then(
({ keys, array }) => {
return array.map((obj, index) => {
const pubKey = keys[index];
if (obj.data.length === AccountLayout.span) {
return cache.add(pubKey, obj, TokenAccountParser);
} else if (obj.data.length === MintLayout.span) {
if (!cache.getMint(pubKey)) {
return cache.addMint(new PublicKey(pubKey), obj);
}
}
}
return obj;
}) as any[];
});
return obj;
}) as any[];
}
);
return poolsArray;
};
Promise.all([queryPools(programIds().swap), ...programIds().swap_legacy.map((leg) => queryPools(leg, true))]).then(
(all) => {
setPools(all.flat());
}
);
Promise.all([
queryPools(programIds().swap),
...programIds().swap_legacy.map((leg) => queryPools(leg, true)),
]).then((all) => {
setPools(all.flat());
});
}, [connection]);
useEffect(() => {
@ -150,7 +165,9 @@ export const usePools = () => {
pubkey: new PublicKey(id),
};
const index = pools && pools.findIndex((p) => p.pubkeys.account.toBase58() === id);
const index =
pools &&
pools.findIndex((p) => p.pubkeys.account.toBase58() === id);
if (index && index >= 0 && pools) {
// TODO: check if account is empty?
@ -160,13 +177,16 @@ export const usePools = () => {
let pool = toPoolInfo(updated, programIds().swap);
pool.pubkeys.feeAccount = updated.data.feeAccount;
pool.pubkeys.holdingMints = [updated.data.mintA, updated.data.mintB] as PublicKey[];
pool.pubkeys.holdingMints = [
updated.data.mintA,
updated.data.mintB,
] as PublicKey[];
setPools([...pools, pool]);
}
}
},
'singleGossip'
"singleGossip"
);
return () => {
@ -198,7 +218,10 @@ export const usePoolForBasket = (mints: (string | undefined)[]) => {
for (let i = 0; i < matchingPool.length; i++) {
const p = matchingPool[i];
const account = await cache.query(connection, p.pubkeys.holdingAccounts[0]);
const account = await cache.query(
connection,
p.pubkeys.holdingAccounts[0]
);
if (!account.info.amount.eqn(0)) {
setPool(p);
@ -216,7 +239,9 @@ function estimateProceedsFromInput(
proceedsQuantityInPool: number,
inputAmount: number
): number {
return (proceedsQuantityInPool * inputAmount) / (inputQuantityInPool + inputAmount);
return (
(proceedsQuantityInPool * inputAmount) / (inputQuantityInPool + inputAmount)
);
}
function estimateInputFromProceeds(
@ -225,10 +250,13 @@ function estimateInputFromProceeds(
proceedsAmount: number
): number | string {
if (proceedsAmount >= proceedsQuantityInPool) {
return 'Not possible';
return "Not possible";
}
return (inputQuantityInPool * proceedsAmount) / (proceedsQuantityInPool - proceedsAmount);
return (
(inputQuantityInPool * proceedsAmount) /
(proceedsQuantityInPool - proceedsAmount)
);
}
export enum PoolOperation {
@ -245,14 +273,20 @@ export async function calculateDependentAmount(
op: PoolOperation
): Promise<number | string | undefined> {
const poolMint = await cache.queryMint(connection, pool.pubkeys.mint);
const accountA = await cache.query(connection, pool.pubkeys.holdingAccounts[0]);
const accountA = await cache.query(
connection,
pool.pubkeys.holdingAccounts[0]
);
const amountA = accountA.info.amount.toNumber();
const accountB = await cache.query(connection, pool.pubkeys.holdingAccounts[1]);
const accountB = await cache.query(
connection,
pool.pubkeys.holdingAccounts[1]
);
let amountB = accountB.info.amount.toNumber();
if (!poolMint.mintAuthority) {
throw new Error('Mint doesnt have authority');
throw new Error("Mint doesnt have authority");
}
if (poolMint.supply.eqn(0)) {
@ -274,8 +308,14 @@ export async function calculateDependentAmount(
}
const isFirstIndependent = accountA.info.mint.toBase58() === independent;
const depPrecision = Math.pow(10, isFirstIndependent ? mintB.decimals : mintA.decimals);
const indPrecision = Math.pow(10, isFirstIndependent ? mintA.decimals : mintB.decimals);
const depPrecision = Math.pow(
10,
isFirstIndependent ? mintB.decimals : mintA.decimals
);
const indPrecision = Math.pow(
10,
isFirstIndependent ? mintA.decimals : mintB.decimals
);
const indAdjustedAmount = amount * indPrecision;
let indBasketQuantity = isFirstIndependent ? amountA : amountB;
@ -290,18 +330,27 @@ export async function calculateDependentAmount(
} else {
switch (+op) {
case PoolOperation.Add:
depAdjustedAmount = (depBasketQuantity / indBasketQuantity) * indAdjustedAmount;
depAdjustedAmount =
(depBasketQuantity / indBasketQuantity) * indAdjustedAmount;
break;
case PoolOperation.SwapGivenProceeds:
depAdjustedAmount = estimateInputFromProceeds(depBasketQuantity, indBasketQuantity, indAdjustedAmount);
depAdjustedAmount = estimateInputFromProceeds(
depBasketQuantity,
indBasketQuantity,
indAdjustedAmount
);
break;
case PoolOperation.SwapGivenInput:
depAdjustedAmount = estimateProceedsFromInput(indBasketQuantity, depBasketQuantity, indAdjustedAmount);
depAdjustedAmount = estimateProceedsFromInput(
indBasketQuantity,
depBasketQuantity,
indAdjustedAmount
);
break;
}
}
if (typeof depAdjustedAmount === 'string') {
if (typeof depAdjustedAmount === "string") {
return depAdjustedAmount;
}
if (depAdjustedAmount === undefined) {

View File

@ -9,7 +9,7 @@ export const BorrowView = () => {
const { reserveAccounts } = useLendingReserves();
return (
<div className="flexColumn">
<Card >
<Card>
<div className="borrow-item deposit-header">
<div>{LABELS.TABLE_TITLE_ASSET}</div>
<div>Serum Dex Price</div>

View File

@ -1,15 +1,18 @@
import React from 'react';
import { useTokenName, useBorrowingPower } from '../../hooks';
import { calculateBorrowAPY, LendingReserve } from '../../models/lending';
import { TokenIcon } from '../../components/TokenIcon';
import { formatNumber, formatPct } from '../../utils/utils';
import { Button } from 'antd';
import { Link } from 'react-router-dom';
import { PublicKey } from '@solana/web3.js';
import { LABELS } from '../../constants';
import { useMidPriceInUSD } from '../../contexts/market';
import React from "react";
import { useTokenName, useBorrowingPower } from "../../hooks";
import { calculateBorrowAPY, LendingReserve } from "../../models/lending";
import { TokenIcon } from "../../components/TokenIcon";
import { formatNumber, formatPct } from "../../utils/utils";
import { Button } from "antd";
import { Link } from "react-router-dom";
import { PublicKey } from "@solana/web3.js";
import { LABELS } from "../../constants";
import { useMidPriceInUSD } from "../../contexts/market";
export const BorrowItem = (props: { reserve: LendingReserve; address: PublicKey }) => {
export const BorrowItem = (props: {
reserve: LendingReserve;
address: PublicKey;
}) => {
const name = useTokenName(props.reserve.liquidityMint);
const price = useMidPriceInUSD(props.reserve.liquidityMint.toBase58()).price;
@ -19,8 +22,8 @@ export const BorrowItem = (props: { reserve: LendingReserve; address: PublicKey
return (
<Link to={`/borrow/${props.address.toBase58()}`}>
<div className='borrow-item'>
<span style={{ display: 'flex' }}>
<div className="borrow-item">
<span style={{ display: "flex" }}>
<TokenIcon mintAddress={props.reserve.liquidityMint} />
{name}
</span>
@ -30,12 +33,14 @@ export const BorrowItem = (props: { reserve: LendingReserve; address: PublicKey
<div>
<em>{formatNumber.format(borrowingPower)}</em> {name}
</div>
<div className='dashboard-amount-quote'>${formatNumber.format(totalInQuote)}</div>
<div className="dashboard-amount-quote">
${formatNumber.format(totalInQuote)}
</div>
</div>
</div>
<div>{formatPct.format(apr)}</div>
<div>
<Button type='primary'>
<Button type="primary">
<span>{LABELS.BORROW_ACTION}</span>
</Button>
</div>

View File

@ -1,5 +1,9 @@
import React from "react";
import { useBorrowingPower, useLendingReserve, useUserObligations } from "../../hooks";
import {
useBorrowingPower,
useLendingReserve,
useUserObligations,
} from "../../hooks";
import { useParams } from "react-router-dom";
import "./style.less";
@ -17,7 +21,7 @@ export const BorrowReserveView = () => {
const lendingReserve = useLendingReserve(id);
const { userObligations, totalInQuote: loansValue } = useUserObligations();
const { totalInQuote: borrowingPower, utilization } = useBorrowingPower(id)
const { totalInQuote: borrowingPower, utilization } = useBorrowingPower(id);
if (!lendingReserve) {
return null;
@ -62,17 +66,17 @@ export const BorrowReserveView = () => {
<BarChartStatistic
title="Your Loans"
items={userObligations}
getPct={(item) => item.obligation.info.borrowedInQuote / loansValue}
name={(item) => item.obligation.info.repayName} />
getPct={(item) =>
item.obligation.info.borrowedInQuote / loansValue
}
name={(item) => item.obligation.info.repayName}
/>
</Card>
</Col>
</Row>
<Row gutter={GUTTER} style={{ flex: 1 }}>
<Col xs={24} xl={15}>
<BorrowInput
className="card-fill"
reserve={lendingReserve}
/>
<BorrowInput className="card-fill" reserve={lendingReserve} />
</Col>
<Col xs={24} xl={9}>
<SideReserveOverview

View File

@ -9,24 +9,32 @@ import { DepositItem } from "./item";
export const DashboardDeposits = () => {
const { userDeposits, totalInQuote } = useUserDeposits();
return (<Card title={
<div className="dashboard-title">
<div>{LABELS.DASHBOARD_TITLE_DEPOSITS}</div>
<div><span>{LABELS.TOTAL_TITLE}: </span>${formatNumber.format(totalInQuote)}</div>
</div>}>
<BarChartStatistic
items={userDeposits}
getPct={(item) => item.info.amountInQuote / totalInQuote}
name={(item) => item.info.name} />
<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></div>
</div>
{userDeposits.map((deposit) => (
<DepositItem userDeposit={deposit} />
))}
</Card>
return (
<Card
title={
<div className="dashboard-title">
<div>{LABELS.DASHBOARD_TITLE_DEPOSITS}</div>
<div>
<span>{LABELS.TOTAL_TITLE}: </span>$
{formatNumber.format(totalInQuote)}
</div>
</div>
}
>
<BarChartStatistic
items={userDeposits}
getPct={(item) => item.info.amountInQuote / totalInQuote}
name={(item) => item.info.name}
/>
<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></div>
</div>
{userDeposits.map((deposit) => (
<DepositItem userDeposit={deposit} />
))}
</Card>
);
};

View File

@ -7,10 +7,8 @@ import { Button } from "antd";
import { Link } from "react-router-dom";
import { LABELS } from "../../../constants";
export const DepositItem = (props: {
userDeposit: UserDeposit;
}) => {
const {reserve, info} = props.userDeposit;
export const DepositItem = (props: { userDeposit: UserDeposit }) => {
const { reserve, info } = props.userDeposit;
const mintAddress = reserve.info.liquidityMint;
const name = useTokenName(mintAddress);
@ -19,30 +17,34 @@ export const DepositItem = (props: {
]);
return (
<div className="dashboard-item">
<span style={{ display: "flex" }}>
<TokenIcon mintAddress={mintAddress} />
{name}
</span>
<div className="dashboard-item">
<span style={{ display: "flex" }}>
<TokenIcon mintAddress={mintAddress} />
{name}
</span>
<div>
<div>
<div>
<div><em>{formatNumber.format(info.amount)}</em> {name}</div>
<div className="dashboard-amount-quote">${formatNumber.format(info.amountInQuote)}</div>
<em>{formatNumber.format(info.amount)}</em> {name}
</div>
<div className="dashboard-amount-quote">
${formatNumber.format(info.amountInQuote)}
</div>
</div>
<div>{formatPct.format(depositAPY)}</div>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<Link to={`/deposit/${reserve.pubkey.toBase58()}`}>
<Button type="primary">
<span>{LABELS.DEPOSIT_ACTION}</span>
</Button>
</Link>
<Link to={`/withdraw/${reserve.pubkey.toBase58()}`}>
<Button type="text">
<span>{LABELS.WITHDRAW_ACTION}</span>
</Button>
</Link>
</div>
</div>
<div>{formatPct.format(depositAPY)}</div>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<Link to={`/deposit/${reserve.pubkey.toBase58()}`}>
<Button type="primary">
<span>{LABELS.DEPOSIT_ACTION}</span>
</Button>
</Link>
<Link to={`/withdraw/${reserve.pubkey.toBase58()}`}>
<Button type="text">
<span>{LABELS.WITHDRAW_ACTION}</span>
</Button>
</Link>
</div>
</div>
);
};

View File

@ -14,32 +14,40 @@ export const DashboardView = () => {
return (
<div className="dashboard-container">
{!connected && (
{!connected && (
<div className="dashboard-info">
<img src="splash.svg" alt="connect your wallet" className="dashboard-splash"/>
<img
src="splash.svg"
alt="connect your wallet"
className="dashboard-splash"
/>
{LABELS.DASHBOARD_INFO}
</div>
</div>
)}
{connected && userDeposits.length === 0 && userObligations.length === 0 && (
<div className="dashboard-info">
<img
src="splash.svg"
alt="connect your wallet"
className="dashboard-splash"
/>
{LABELS.NO_LOANS_NO_DEPOSITS}
</div>
)}
{connected && (
<Row gutter={GUTTER}>
{userDeposits.length > 0 && (
<Col md={24} xl={12} span={24}>
<DashboardDeposits />
</Col>
)}
{userObligations.length > 0 && (
<Col md={24} xl={12} span={24}>
<DashboardObligations />
</Col>
)}
</Row>
)}
{connected &&
userDeposits.length === 0 &&
userObligations.length === 0 && (
<div className="dashboard-info">
<img src="splash.svg" alt="connect your wallet" className="dashboard-splash"/>
{LABELS.NO_LOANS_NO_DEPOSITS}
</div>
)}
{connected && <Row gutter={GUTTER} >
{userDeposits.length >0 && (
<Col md={24} xl={12} span={24}>
<DashboardDeposits />
</Col>
)}
{userObligations.length >0 && (
<Col md={24} xl={12} span={24}>
<DashboardObligations />
</Col>
)}
</Row>}
</div>
);
};

View File

@ -10,15 +10,22 @@ export const DashboardObligations = () => {
const { userObligations, totalInQuote } = useUserObligations();
return (
<Card title={
<div className="dashboard-title">
<div>{LABELS.DASHBOARD_TITLE_LOANS}</div>
<div><span>{LABELS.TOTAL_TITLE}: </span>${formatNumber.format(totalInQuote)}</div>
</div>}>
<Card
title={
<div className="dashboard-title">
<div>{LABELS.DASHBOARD_TITLE_LOANS}</div>
<div>
<span>{LABELS.TOTAL_TITLE}: </span>$
{formatNumber.format(totalInQuote)}
</div>
</div>
}
>
<BarChartStatistic
items={userObligations}
getPct={(item) => item.obligation.info.borrowedInQuote / totalInQuote}
name={(item) => item.obligation.info.repayName} />
name={(item) => item.obligation.info.repayName}
/>
<div className="dashboard-item dashboard-header">
<div>{LABELS.TABLE_TITLE_ASSET}</div>
<div>{LABELS.TABLE_TITLE_YOUR_LOAN_BALANCE}</div>

View File

@ -52,45 +52,55 @@ export const ObligationItem = (props: {
const collateralName = useTokenName(collateralReserve?.info.liquidityMint);
return (
<div className="dashboard-item">
<span style={{ display: "flex", marginLeft: 5 }}>
<div
style={{ display: "flex" }}
title={`${collateralName}${borrowName}`}
>
<TokenIcon
mintAddress={collateralReserve?.info.liquidityMint}
style={{ marginRight: "-0.5rem" }}
/>
<TokenIcon mintAddress={borrowReserve?.info.liquidityMint} />
</div>
</span>
<div className="dashboard-item">
<span style={{ display: "flex", marginLeft: 5 }}>
<div
style={{ display: "flex" }}
title={`${collateralName}${borrowName}`}
>
<TokenIcon
mintAddress={collateralReserve?.info.liquidityMint}
style={{ marginRight: "-0.5rem" }}
/>
<TokenIcon mintAddress={borrowReserve?.info.liquidityMint} />
</div>
</span>
<div>
<div>
<div>
<div><em>{formatNumber.format(borrowAmount)}</em> {borrowName}</div>
<div className="dashboard-amount-quote">${formatNumber.format(obligation.info.borrowedInQuote)}</div>
<em>{formatNumber.format(borrowAmount)}</em> {borrowName}
</div>
</div>
<div>
<div>
<div><em>{formatNumber.format(collateral)}</em> {collateralName}</div>
<div className="dashboard-amount-quote">${formatNumber.format(obligation.info.collateralInQuote)}</div>
<div className="dashboard-amount-quote">
${formatNumber.format(obligation.info.borrowedInQuote)}
</div>
</div>
<div>{formatPct.format(borrowAPY)}</div>
<div style={{ color: healthFactorToRiskColor(obligation.info.health)}}>{formatPct.format(obligation.info.ltv / 100)}</div>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<Link to={`/borrow/${borrowReserve.pubkey.toBase58()}`}>
<Button type="primary">
<span>Borrow</span>
</Button>
</Link>
<Link to={`/repay/loan/${obligation.account.pubkey.toBase58()}`}>
<Button type="text">
<span>Repay</span>
</Button>
</Link>
</div>
</div>
<div>
<div>
<div>
<em>{formatNumber.format(collateral)}</em> {collateralName}
</div>
<div className="dashboard-amount-quote">
${formatNumber.format(obligation.info.collateralInQuote)}
</div>
</div>
</div>
<div>{formatPct.format(borrowAPY)}</div>
<div style={{ color: healthFactorToRiskColor(obligation.info.health) }}>
{formatPct.format(obligation.info.ltv / 100)}
</div>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<Link to={`/borrow/${borrowReserve.pubkey.toBase58()}`}>
<Button type="primary">
<span>Borrow</span>
</Button>
</Link>
<Link to={`/repay/loan/${obligation.account.pubkey.toBase58()}`}>
<Button type="text">
<span>Repay</span>
</Button>
</Link>
</div>
</div>
);
};

View File

@ -8,7 +8,7 @@ export const DepositView = () => {
const { reserveAccounts } = useLendingReserves();
return (
<div className="flexColumn">
<Card >
<Card>
<div className="deposit-item deposit-header">
<div>Asset</div>
<div>Your wallet balance</div>

View File

@ -17,10 +17,14 @@ export const ReserveItem = (props: {
address: PublicKey;
}) => {
const name = useTokenName(props.reserve.liquidityMint);
const { balance: tokenBalance, balanceInUSD: tokenBalanceInUSD } = useUserBalance(props.reserve.liquidityMint);
const { balance: collateralBalance, balanceInUSD: collateralBalanceInUSD } = useUserCollateralBalance(
props.reserve
);
const {
balance: tokenBalance,
balanceInUSD: tokenBalanceInUSD,
} = useUserBalance(props.reserve.liquidityMint);
const {
balance: collateralBalance,
balanceInUSD: collateralBalanceInUSD,
} = useUserCollateralBalance(props.reserve);
const apy = calculateDepositAPY(props.reserve);
@ -33,14 +37,22 @@ export const ReserveItem = (props: {
</span>
<div>
<div>
<div><em>{formatNumber.format(tokenBalance)}</em> {name}</div>
<div className="dashboard-amount-quote">${formatNumber.format(tokenBalanceInUSD)}</div>
<div>
<em>{formatNumber.format(tokenBalance)}</em> {name}
</div>
<div className="dashboard-amount-quote">
${formatNumber.format(tokenBalanceInUSD)}
</div>
</div>
</div>
<div>
<div>
<div><em>{formatNumber.format(collateralBalance)}</em> {name}</div>
<div className="dashboard-amount-quote">${formatNumber.format(collateralBalanceInUSD)}</div>
<div>
<em>{formatNumber.format(collateralBalance)}</em> {name}
</div>
<div className="dashboard-amount-quote">
${formatNumber.format(collateralBalanceInUSD)}
</div>
</div>
</div>
<div>{formatPct.format(apy)}</div>

View File

@ -21,7 +21,7 @@ export const HomeView = () => {
borrowed: 0,
lentOutPct: 0,
items: [],
})
});
useEffect(() => {
const refreshTotal = () => {
@ -48,26 +48,29 @@ export const HomeView = () => {
let leaf = {
key: item.pubkey.toBase58(),
marketSize: fromLamports(marketCapLamports, liquidityMint?.info) *
price,
borrowed: fromLamports(
wadToLamports(item.info?.state.borrowedLiquidityWad).toNumber(),
liquidityMint.info
) *
price,
name: getTokenName(tokenMap, item.info.liquidityMint.toBase58())
}
marketSize:
fromLamports(marketCapLamports, liquidityMint?.info) * price,
borrowed:
fromLamports(
wadToLamports(item.info?.state.borrowedLiquidityWad).toNumber(),
liquidityMint.info
) * price,
name: getTokenName(tokenMap, item.info.liquidityMint.toBase58()),
};
newTotals.items.push(leaf);
newTotals.marketSize = newTotals.marketSize + leaf.marketSize;
newTotals.borrowed = newTotals.borrowed + leaf.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)
newTotals.lentOutPct = Number.isFinite(newTotals.lentOutPct)
? newTotals.lentOutPct
: 0;
newTotals.items = newTotals.items.sort(
(a, b) => b.marketSize - a.marketSize
);
setTotals(newTotals);
};
@ -85,9 +88,7 @@ export const HomeView = () => {
return (
<div className="flexColumn">
<Row
gutter={GUTTER}
className="home-info-row" >
<Row gutter={GUTTER} className="home-info-row">
<Col xs={24} xl={5}>
<Card>
<Statistic
@ -125,7 +126,8 @@ export const HomeView = () => {
title="Market composition"
name={(item) => item.name}
getPct={(item) => item.marketSize / totals.marketSize}
items={totals.items} />
items={totals.items}
/>
</Card>
</Col>
</Row>
@ -139,7 +141,13 @@ export const HomeView = () => {
<div>{LABELS.TABLE_TITLE_BORROW_APY}</div>
</div>
{reserveAccounts.map((account) => (
<LendingReserveItem reserve={account.info} address={account.pubkey} item={totals.items.find(item => item.key === account.pubkey.toBase58())} />
<LendingReserveItem
reserve={account.info}
address={account.pubkey}
item={totals.items.find(
(item) => item.key === account.pubkey.toBase58()
)}
/>
))}
</Card>
</div>

View File

@ -59,19 +59,25 @@ export const LendingReserveItem = (props: {
</span>
<div title={marketSize.toString()}>
<div>
<div><em>{formatNumber.format(marketSize)}</em> {name}</div>
<div className="dashboard-amount-quote">${formatNumber.format(props.item?.marketSize)}</div>
<div>
<em>{formatNumber.format(marketSize)}</em> {name}
</div>
<div className="dashboard-amount-quote">
${formatNumber.format(props.item?.marketSize)}
</div>
</div>
</div>
<div title={totalBorrows.toString()}>
<div>
<div><em>{formatNumber.format(totalBorrows)}</em> {name}</div>
<div className="dashboard-amount-quote">${formatNumber.format(props.item?.borrowed)}</div>
<div>
<em>{formatNumber.format(totalBorrows)}</em> {name}
</div>
<div className="dashboard-amount-quote">
${formatNumber.format(props.item?.borrowed)}
</div>
</div>
</div>
<div title={depositAPY.toString()}>
{formatPct.format(depositAPY)}
</div>
<div title={depositAPY.toString()}>{formatPct.format(depositAPY)}</div>
<div title={borrowAPY.toString()}>{formatPct.format(borrowAPY)}</div>
</div>
</Link>

View File

@ -9,17 +9,29 @@ import { BarChartStatistic } from "../../components/BarChartStatistic";
export const LiquidateView = () => {
const { obligations } = useEnrichedLendingObligations();
const atRisk = useMemo(() => obligations.filter(item => item.info.health < 1.0), [obligations]);
const atRisk = useMemo(
() => obligations.filter((item) => item.info.health < 1.0),
[obligations]
);
const valueAtRisk = useMemo(() => atRisk.reduce((acc, item) => acc + item.info.collateralInQuote, 0), [atRisk]);
const valueAtRisk = useMemo(
() => atRisk.reduce((acc, item) => acc + item.info.collateralInQuote, 0),
[atRisk]
);
const loansAtRiskCount = useMemo(() => atRisk.length, [atRisk]);
const pctAtRisk = useMemo(() => atRisk.length / obligations.length, [atRisk, obligations]);
const pctAtRisk = useMemo(() => atRisk.length / obligations.length, [
atRisk,
obligations,
]);
const groupedLoans = useMemo(() => {
return atRisk.reduce((acc, item) => {
acc.set(item.info.repayName, (acc.get(item.info.collateralName) || 0) + item.info.collateralInQuote);
acc.set(
item.info.repayName,
(acc.get(item.info.collateralName) || 0) + item.info.collateralInQuote
);
return acc;
}, new Map<string, number>())
}, new Map<string, number>());
}, [atRisk]);
const keys = useMemo(() => [...groupedLoans.keys()], [groupedLoans]);
@ -29,81 +41,79 @@ export const LiquidateView = () => {
{atRisk.length === 0 ? (
<div className="liquidate-info">{LABELS.LIQUIDATE_NO_LOANS}</div>
) : (
<div className="flexColumn">
<Row gutter={GUTTER}>
<Col span={24}>
<Card>
<Typography>
{LABELS.LIQUIDATION_INFO}
</Typography>
</Card>
</Col>
</Row>
<Row
gutter={GUTTER}>
<Col xs={24} xl={5}>
<Card>
<Statistic
title="Value at risk"
value={valueAtRisk}
precision={2}
valueStyle={{ color: "#3f8600" }}
prefix="$"
/>
</Card>
</Col>
<Col xs={24} xl={5}>
<Card>
<Statistic
title="Loans at risk"
value={loansAtRiskCount}
precision={0}
/>
</Card>
</Col>
<Col xs={24} xl={5}>
<Card>
<Statistic
title="% loans at risk"
value={pctAtRisk * 100}
precision={2}
suffix="%"
/>
</Card>
</Col>
<Col xs={24} xl={9}>
<Card>
<BarChartStatistic
title="At risk loan composition"
name={(item) => item}
getPct={(item) => (groupedLoans.get(item) || 0) / valueAtRisk}
items={keys} />
</Card>
</Col>
</Row>
<Row gutter={GUTTER}>
<Col span={24}>
<Card className="card-fill">
<div className="liquidate-item liquidate-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_LTV}</div>
<div>{LABELS.TABLE_TITLE_HEALTH}</div>
<div>{LABELS.TABLE_TITLE_ACTION}</div>
</div>
{atRisk.map((item) => (
<LiquidateItem
key={item.account.pubkey.toBase58()}
item={item}
></LiquidateItem>
))}
</Card>
</Col>
</Row>
</div>
)}
<div className="flexColumn">
<Row gutter={GUTTER}>
<Col span={24}>
<Card>
<Typography>{LABELS.LIQUIDATION_INFO}</Typography>
</Card>
</Col>
</Row>
<Row gutter={GUTTER}>
<Col xs={24} xl={5}>
<Card>
<Statistic
title="Value at risk"
value={valueAtRisk}
precision={2}
valueStyle={{ color: "#3f8600" }}
prefix="$"
/>
</Card>
</Col>
<Col xs={24} xl={5}>
<Card>
<Statistic
title="Loans at risk"
value={loansAtRiskCount}
precision={0}
/>
</Card>
</Col>
<Col xs={24} xl={5}>
<Card>
<Statistic
title="% loans at risk"
value={pctAtRisk * 100}
precision={2}
suffix="%"
/>
</Card>
</Col>
<Col xs={24} xl={9}>
<Card>
<BarChartStatistic
title="At risk loan composition"
name={(item) => item}
getPct={(item) => (groupedLoans.get(item) || 0) / valueAtRisk}
items={keys}
/>
</Card>
</Col>
</Row>
<Row gutter={GUTTER}>
<Col span={24}>
<Card className="card-fill">
<div className="liquidate-item liquidate-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_LTV}</div>
<div>{LABELS.TABLE_TITLE_HEALTH}</div>
<div>{LABELS.TABLE_TITLE_ACTION}</div>
</div>
{atRisk.map((item) => (
<LiquidateItem
key={item.account.pubkey.toBase58()}
item={item}
></LiquidateItem>
))}
</Card>
</Col>
</Row>
</div>
)}
</div>
);
};

View File

@ -1,6 +1,10 @@
import React, { useMemo } from "react";
import { cache, ParsedAccount, useMint } from "../../contexts/accounts";
import { LendingReserve, calculateBorrowAPY, collateralToLiquidity } from "../../models/lending";
import {
LendingReserve,
calculateBorrowAPY,
collateralToLiquidity,
} from "../../models/lending";
import { EnrichedLendingObligation, useTokenName } from "../../hooks";
import { Link } from "react-router-dom";
import { Button } from "antd";
@ -60,14 +64,22 @@ export const LiquidateItem = (props: { item: EnrichedLendingObligation }) => {
</span>
<div>
<div>
<div><em>{formatNumber.format(borrowAmount)}</em> {borrowName}</div>
<div className="dashboard-amount-quote">${formatNumber.format(obligation.borrowedInQuote)}</div>
<div>
<em>{formatNumber.format(borrowAmount)}</em> {borrowName}
</div>
<div className="dashboard-amount-quote">
${formatNumber.format(obligation.borrowedInQuote)}
</div>
</div>
</div>
<div>
<div>
<div><em>{formatNumber.format(collateral)}</em> {collateralName}</div>
<div className="dashboard-amount-quote">${formatNumber.format(obligation.collateralInQuote)}</div>
<div>
<em>{formatNumber.format(collateral)}</em> {collateralName}
</div>
<div className="dashboard-amount-quote">
${formatNumber.format(obligation.collateralInQuote)}
</div>
</div>
</div>
<div>{formatPct.format(borrowAPY)}</div>

View File

@ -15,9 +15,7 @@ export const LiquidateReserveView = () => {
const obligation = useEnrichedLendingObligation(id);
const repayReserve = useLendingReserve(obligation?.info.borrowReserve);
const withdrawReserve = useLendingReserve(
obligation?.info.collateralReserve
);
const withdrawReserve = useLendingReserve(obligation?.info.collateralReserve);
if (!obligation || !repayReserve) {
return null;

View File

@ -1,16 +1,16 @@
import React from 'react';
import { LABELS } from '../../constants';
import './itemStyle.less';
import { Card } from 'antd';
import { useLendingReserves } from '../../hooks/useLendingReserves';
import { MarginTradeItem } from './item';
import React from "react";
import { LABELS } from "../../constants";
import "./itemStyle.less";
import { Card } from "antd";
import { useLendingReserves } from "../../hooks/useLendingReserves";
import { MarginTradeItem } from "./item";
export const MarginTrading = () => {
const { reserveAccounts } = useLendingReserves();
return (
<div className='flexColumn'>
<div className="flexColumn">
<Card>
<div className='choose-margin-item choose-margin-header'>
<div className="choose-margin-item choose-margin-header">
<div>{LABELS.TABLE_TITLE_ASSET}</div>
<div>Serum Dex Price</div>
<div>{LABELS.TABLE_TITLE_BUYING_POWER}</div>

View File

@ -1,27 +1,34 @@
import React from 'react';
import { useBorrowingPower, useTokenName } from '../../hooks';
import { calculateBorrowAPY, LendingReserve } from '../../models/lending';
import { TokenIcon } from '../../components/TokenIcon';
import { formatNumber, formatPct } from '../../utils/utils';
import { Button } from 'antd';
import { Link } from 'react-router-dom';
import { PublicKey } from '@solana/web3.js';
import { LABELS } from '../../constants';
import { useMidPriceInUSD } from '../../contexts/market';
import React from "react";
import { useBorrowingPower, useTokenName } from "../../hooks";
import { calculateBorrowAPY, LendingReserve } from "../../models/lending";
import { TokenIcon } from "../../components/TokenIcon";
import { formatNumber, formatPct } from "../../utils/utils";
import { Button } from "antd";
import { Link } from "react-router-dom";
import { PublicKey } from "@solana/web3.js";
import { LABELS } from "../../constants";
import { useMidPriceInUSD } from "../../contexts/market";
export const MarginTradeItem = (props: { reserve: LendingReserve; address: PublicKey }) => {
export const MarginTradeItem = (props: {
reserve: LendingReserve;
address: PublicKey;
}) => {
const name = useTokenName(props.reserve.liquidityMint);
const price = useMidPriceInUSD(props.reserve.liquidityMint.toBase58()).price;
const apr = calculateBorrowAPY(props.reserve);
// TODO: specifc max leverage
const { totalInQuote, borrowingPower } = useBorrowingPower(props.address, false, false);
const { totalInQuote, borrowingPower } = useBorrowingPower(
props.address,
false,
false
);
return (
<Link to={`/margin/${props.address.toBase58()}`}>
<div className='choose-margin-item'>
<span style={{ display: 'flex' }}>
<div className="choose-margin-item">
<span style={{ display: "flex" }}>
<TokenIcon mintAddress={props.reserve.liquidityMint} />
{name}
</span>
@ -31,12 +38,14 @@ export const MarginTradeItem = (props: { reserve: LendingReserve; address: Publi
<div>
<em>{formatNumber.format(borrowingPower)}</em> {name}
</div>
<div className='dashboard-amount-quote'>${formatNumber.format(totalInQuote)}</div>
<div className="dashboard-amount-quote">
${formatNumber.format(totalInQuote)}
</div>
</div>
</div>
<div>{formatPct.format(apr)}</div>
<div>
<Button type='primary'>
<Button type="primary">
<span>{LABELS.MARGIN_TRADE_ACTION}</span>
</Button>
</div>

View File

@ -1,24 +1,32 @@
import { Progress, Slider, Card, Statistic } from 'antd';
import React, { useState } from 'react';
import { Position } from './interfaces';
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
import tokens from '../../../config/tokens.json';
import GainsChart from './GainsChart';
import { usePoolAndTradeInfoFrom } from './utils';
import { Progress, Slider, Card, Statistic } from "antd";
import React, { useState } from "react";
import { Position } from "./interfaces";
import { ArrowUpOutlined, ArrowDownOutlined } from "@ant-design/icons";
import tokens from "../../../config/tokens.json";
import GainsChart from "./GainsChart";
import { usePoolAndTradeInfoFrom } from "./utils";
export default function Breakdown({ item }: { item: Position }) {
const { enrichedPools, leverage } = usePoolAndTradeInfoFrom(item);
const exchangeRate = enrichedPools.length === 0 ? 1 : enrichedPools[0].liquidityB / enrichedPools[0].liquidityA;
const exchangeRate =
enrichedPools.length === 0
? 1
: enrichedPools[0].liquidityB / enrichedPools[0].liquidityA;
let myPart = item.collateral.value || 0;
const brokeragePart = (item.collateral.value || 0) * leverage - myPart;
const brokerageColor = 'brown';
const myColor = 'blue';
const gains = 'green';
const losses = 'red';
const token = tokens.find((t) => t.mintAddress === item.asset.type?.info?.liquidityMint?.toBase58());
const collateralToken = tokens.find((t) => t.mintAddress === item.collateral.type?.info?.liquidityMint?.toBase58());
const brokerageColor = "brown";
const myColor = "blue";
const gains = "green";
const losses = "red";
const token = tokens.find(
(t) => t.mintAddress === item.asset.type?.info?.liquidityMint?.toBase58()
);
const collateralToken = tokens.find(
(t) =>
t.mintAddress === item.collateral.type?.info?.liquidityMint?.toBase58()
);
const [myGain, setMyGain] = useState<number>(10);
const profitPart = (myPart + brokeragePart) * (myGain / 100);
@ -29,7 +37,10 @@ export default function Breakdown({ item }: { item: Position }) {
progressBar = (
<Progress
percent={(myPart / total) * 100 + (brokeragePart / total) * 100}
success={{ percent: (brokeragePart / total) * 100, strokeColor: brokerageColor }}
success={{
percent: (brokeragePart / total) * 100,
strokeColor: brokerageColor,
}}
strokeColor={myColor}
trailColor={gains}
showInfo={false}
@ -40,24 +51,36 @@ export default function Breakdown({ item }: { item: Position }) {
myPart += profitPart; // profit is negative
const total = myPart + brokeragePart;
if (myPart < 0) {
progressBar = <p>Your position has been liquidated at this price swing.</p>;
progressBar = (
<p>Your position has been liquidated at this price swing.</p>
);
} else
progressBar = (
<Progress
showInfo={false}
success={{ percent: (brokeragePart / total) * 100, strokeColor: brokerageColor }}
success={{
percent: (brokeragePart / total) * 100,
strokeColor: brokerageColor,
}}
trailColor={myColor}
/>
);
}
return (
<div className='new-position-item new-position-item-top-right'>
<Card className='new-position-item new-position-item-top-right'>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center' }}>
<div className="new-position-item new-position-item-top-right">
<Card className="new-position-item new-position-item-top-right">
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-around",
alignItems: "center",
}}
>
<Card>
<Statistic
title='Borrowed'
title="Borrowed"
value={brokeragePart * exchangeRate}
precision={2}
valueStyle={{ color: brokerageColor }}
@ -66,7 +89,7 @@ export default function Breakdown({ item }: { item: Position }) {
</Card>
<Card>
<Statistic
title='My Collateral'
title="My Collateral"
value={myPart}
precision={2}
valueStyle={{ color: myColor }}
@ -75,19 +98,21 @@ export default function Breakdown({ item }: { item: Position }) {
</Card>
<Card>
<Statistic
title='Profit/Loss'
title="Profit/Loss"
value={profitPart * exchangeRate}
precision={2}
valueStyle={{ color: profitPart > 0 ? gains : losses }}
suffix={token?.tokenSymbol}
prefix={profitPart > 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
prefix={
profitPart > 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />
}
/>
</Card>
</div>
<br />
{progressBar}
</Card>
<Card className='new-position-item new-position-item-bottom-right'>
<Card className="new-position-item new-position-item-bottom-right">
<GainsChart item={item} priceChange={myGain} />
<Slider
tooltipVisible={true}
@ -95,11 +120,11 @@ export default function Breakdown({ item }: { item: Position }) {
tipFormatter={(p) => <span>{p}%</span>}
max={100}
min={-100}
tooltipPlacement={'top'}
tooltipPlacement={"top"}
onChange={(v: number) => {
setMyGain(v);
}}
style={{ marginBottom: '20px' }}
style={{ marginBottom: "20px" }}
/>
</Card>
</div>

View File

@ -1,6 +1,6 @@
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useRef } from "react";
import Chart, { ChartPluginsOptions } from "chart.js";
import { Position } from './interfaces';
import { Position } from "./interfaces";
// Special thanks to
// https://github.com/bZxNetwork/fulcrum_ui/blob/development/packages/fulcrum-website/assets/js/trading.js
@ -63,34 +63,36 @@ const baseData = [
function getChartData() {
//the only way to create an immutable copy of array with objects inside.
const baseDashed = getBaseDashed();
const baseSolid = JSON.parse(JSON.stringify(baseData.slice(0, Math.floor(baseData.length) / 2 + 1)));
const baseSolid = JSON.parse(
JSON.stringify(baseData.slice(0, Math.floor(baseData.length) / 2 + 1))
);
return {
datasets: [
{
backgroundColor: 'transparent',
borderColor: 'rgb(39, 107, 251)',
backgroundColor: "transparent",
borderColor: "rgb(39, 107, 251)",
borderWidth: 4,
radius: 0,
data: baseSolid,
},
{
backgroundColor: 'transparent',
backgroundColor: "transparent",
borderWidth: 4,
radius: 0,
data: baseDashed,
borderDash: [15, 3],
label: 'LEVERAGE',
label: "LEVERAGE",
},
{
backgroundColor: 'transparent',
borderColor: 'rgb(86, 169, 255)',
backgroundColor: "transparent",
borderColor: "rgb(86, 169, 255)",
borderWidth: 2,
radius: 0,
data: baseDashed,
borderDash: [8, 4],
label: 'HOLD',
label: "HOLD",
},
],
};
@ -99,8 +101,10 @@ function getChartData() {
const labelPlugin: ChartPluginsOptions = {};
const getBaseDashed = () => {
return JSON.parse(JSON.stringify(baseData.slice(Math.floor(baseData.length) / 2))) as { x: number, y: number }[];
}
return JSON.parse(
JSON.stringify(baseData.slice(Math.floor(baseData.length) / 2))
) as { x: number; y: number }[];
};
function updateChartData({
item,
@ -116,21 +120,24 @@ function updateChartData({
}
labelPlugin.afterDraw = (instance: Chart) => {
drawLabels(instance, item.leverage, priceChange)
drawLabels(instance, item.leverage, priceChange);
};
const baseDashed = getBaseDashed();
const leverage = item.leverage;
var leverageData = baseDashed.map((item: { x: number; y: number }, index: number) => {
if (index === 0) {
return { x: item.x, y: item.y };
var leverageData = baseDashed.map(
(item: { x: number; y: number }, index: number) => {
if (index === 0) {
return { x: item.x, y: item.y };
}
const gain = (priceChange * leverage) / 100;
return { x: item.x, y: item.y * (1 + gain) };
}
const gain = (priceChange * leverage) / 100;
return { x: item.x, y: item.y * (1 + gain) };
});
);
chart.data.datasets[1].data = leverageData;
chart.data.datasets[1].borderColor = priceChange >= 0 ? 'rgb(51, 223, 204)' : 'rgb(255,79,79)';
chart.data.datasets[1].borderColor =
priceChange >= 0 ? "rgb(51, 223, 204)" : "rgb(255,79,79)";
baseDashed.forEach((item: { y: number; x: number }, index: number) => {
if (index !== 0) item.y += (item.y * priceChange) / 100;
@ -140,11 +147,16 @@ function updateChartData({
// chart.chartInstance.canvas.parentNode.style.width = '100%';
// chart.chartInstance.canvas.parentNode.style.height = 'auto';
chart?.update()
chart?.update();
}
function drawLabels(chart: Chart, leverage: number, priceChange: number) {
if (!chart.config || !chart.config.data || !chart.config.data.datasets || !chart.canvas) {
if (
!chart.config ||
!chart.config.data ||
!chart.config.data.datasets ||
!chart.canvas
) {
return;
}
@ -154,11 +166,11 @@ function drawLabels(chart: Chart, leverage: number, priceChange: number) {
}
ctx.save();
ctx.font = 'normal normal bold 15px /1.5 Muli';
ctx.textBaseline = 'bottom';
ctx.font = "normal normal bold 15px /1.5 Muli";
ctx.textBaseline = "bottom";
const datasets = chart.config.data.datasets;
const element = (chart?.canvas?.parentNode as HTMLElement);
const element = chart?.canvas?.parentNode as HTMLElement;
datasets.forEach((ds, index) => {
const label = ds.label;
ctx.fillStyle = ds.borderColor as string;
@ -171,7 +183,7 @@ function drawLabels(chart: Chart, leverage: number, priceChange: number) {
const y = meta.data[pointPostition]._model.y;
let yOffset;
if (label === 'HOLD') {
if (label === "HOLD") {
yOffset = leverage * priceChange > 0 ? y * 1.2 : y * 0.8;
} else {
yOffset = leverage * priceChange > 0 ? y * 0.8 : y * 1.2;
@ -187,7 +199,13 @@ function drawLabels(chart: Chart, leverage: number, priceChange: number) {
ctx.restore();
}
export default function GainsChart({ item, priceChange }: { item: Position; priceChange: number }) {
export default function GainsChart({
item,
priceChange,
}: {
item: Position;
priceChange: number;
}) {
const chartRef = useRef<Chart>();
const canvasRef = useRef<HTMLCanvasElement>();
@ -197,11 +215,9 @@ export default function GainsChart({ item, priceChange }: { item: Position; pric
}
chartRef.current = new Chart(canvasRef.current, {
type: 'line',
type: "line",
data: getChartData(),
plugins: [
labelPlugin
],
plugins: [labelPlugin],
options: {
responsive: true,
maintainAspectRatio: true,
@ -213,12 +229,12 @@ export default function GainsChart({ item, priceChange }: { item: Position; pric
},
},
labels: {
render: 'title',
fontColor: ['green', 'white', 'red'],
render: "title",
fontColor: ["green", "white", "red"],
precision: 2,
},
animation: {
easing: 'easeOutExpo',
easing: "easeOutExpo",
duration: 500,
},
scales: {
@ -228,8 +244,8 @@ export default function GainsChart({ item, priceChange }: { item: Position; pric
gridLines: {
display: false,
},
type: 'linear',
position: 'bottom',
type: "linear",
position: "bottom",
},
],
yAxes: [
@ -244,7 +260,7 @@ export default function GainsChart({ item, priceChange }: { item: Position; pric
legend: {
display: false,
},
} as any
} as any,
});
}, []);
@ -255,14 +271,21 @@ export default function GainsChart({ item, priceChange }: { item: Position; pric
}, [priceChange, item]);
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'stretch', justifyContent: 'center' }}>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "stretch",
justifyContent: "center",
}}
>
<canvas ref={canvasRef as any} />
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span>past</span>

View File

@ -1,16 +1,19 @@
import { Button, Card } from 'antd';
import React, { useState } from 'react';
import { ActionConfirmation } from '../../../components/ActionConfirmation';
import { LABELS } from '../../../constants';
import { cache, ParsedAccount } from '../../../contexts/accounts';
import { LendingReserve, LendingReserveParser } from '../../../models/lending/reserve';
import { Position } from './interfaces';
import { useLeverage } from './leverage';
import CollateralInput from '../../../components/CollateralInput';
import { usePoolAndTradeInfoFrom } from './utils';
import { UserDeposit } from '../../../hooks';
import { ArrowDownOutlined } from '@ant-design/icons';
import { useWallet } from '../../../contexts/wallet';
import { Button, Card } from "antd";
import React, { useState } from "react";
import { ActionConfirmation } from "../../../components/ActionConfirmation";
import { LABELS } from "../../../constants";
import { cache, ParsedAccount } from "../../../contexts/accounts";
import {
LendingReserve,
LendingReserveParser,
} from "../../../models/lending/reserve";
import { Position } from "./interfaces";
import { useLeverage } from "./leverage";
import CollateralInput from "../../../components/CollateralInput";
import { usePoolAndTradeInfoFrom } from "./utils";
import { UserDeposit } from "../../../hooks";
import { ArrowDownOutlined } from "@ant-design/icons";
import { useWallet } from "../../../contexts/wallet";
interface NewPositionFormProps {
lendingReserve: ParsedAccount<LendingReserve>;
@ -18,8 +21,15 @@ interface NewPositionFormProps {
setNewPosition: (pos: Position) => void;
}
export const generateActionLabel = (connected: boolean, newPosition: Position) => {
return !connected ? LABELS.CONNECT_LABEL : newPosition.error ? newPosition.error : LABELS.TRADING_ADD_POSITION;
export const generateActionLabel = (
connected: boolean,
newPosition: Position
) => {
return !connected
? LABELS.CONNECT_LABEL
: newPosition.error
? newPosition.error
: LABELS.TRADING_ADD_POSITION;
};
function onUserChangesLeverageOrCollateralValue({
@ -36,9 +46,14 @@ function onUserChangesLeverageOrCollateralValue({
setNewPosition(newPosition); // It has always changed, need to guarantee save
// if user changes leverage, we need to adjust the amount they desire up.
if (collateralDeposit && enrichedPools.length) {
const exchangeRate = enrichedPools[0].liquidityB / enrichedPools[0].liquidityA;
const convertedAmount = (newPosition.collateral.value || 0) * newPosition.leverage * exchangeRate;
setNewPosition({ ...newPosition, asset: { ...newPosition.asset, value: convertedAmount } });
const exchangeRate =
enrichedPools[0].liquidityB / enrichedPools[0].liquidityA;
const convertedAmount =
(newPosition.collateral.value || 0) * newPosition.leverage * exchangeRate;
setNewPosition({
...newPosition,
asset: { ...newPosition.asset, value: convertedAmount },
});
}
}
@ -55,44 +70,68 @@ function onUserChangesAssetValue({
}) {
setNewPosition(newPosition); // It has always changed, need to guarantee save
if (collateralDeposit && enrichedPools.length) {
const exchangeRate = enrichedPools[0].liquidityB / enrichedPools[0].liquidityA;
const convertedAmount = (newPosition.asset.value || 0) / (exchangeRate * newPosition.leverage);
setNewPosition({ ...newPosition, collateral: { ...newPosition.collateral, value: convertedAmount } });
const exchangeRate =
enrichedPools[0].liquidityB / enrichedPools[0].liquidityA;
const convertedAmount =
(newPosition.asset.value || 0) / (exchangeRate * newPosition.leverage);
setNewPosition({
...newPosition,
collateral: { ...newPosition.collateral, value: convertedAmount },
});
}
}
export default function NewPositionForm({ lendingReserve, newPosition, setNewPosition }: NewPositionFormProps) {
export default function NewPositionForm({
lendingReserve,
newPosition,
setNewPosition,
}: NewPositionFormProps) {
const bodyStyle: React.CSSProperties = {
display: 'flex',
display: "flex",
flex: 1,
justifyContent: 'center',
alignItems: 'center',
height: '100%',
justifyContent: "center",
alignItems: "center",
height: "100%",
};
const [showConfirmation, setShowConfirmation] = useState(false);
const { enrichedPools, collateralDeposit } = usePoolAndTradeInfoFrom(newPosition);
const { enrichedPools, collateralDeposit } = usePoolAndTradeInfoFrom(
newPosition
);
useLeverage({ newPosition, setNewPosition });
const { wallet, connected } = useWallet();
return (
<Card className='new-position-item new-position-item-top-left' bodyStyle={bodyStyle}>
<Card
className="new-position-item new-position-item-top-left"
bodyStyle={bodyStyle}
>
{showConfirmation ? (
<ActionConfirmation onClose={() => setShowConfirmation(false)} />
) : (
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-around',
display: "flex",
flexDirection: "column",
justifyContent: "space-around",
}}
>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-evenly', alignItems: 'center' }}>
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-evenly",
alignItems: "center",
}}
>
<CollateralInput
title='Collateral'
title="Collateral"
reserve={lendingReserve.info}
amount={newPosition.collateral.value}
onInputChange={(val: number | null) => {
const newPos = { ...newPosition, collateral: { ...newPosition.collateral, value: val } };
const newPos = {
...newPosition,
collateral: { ...newPosition.collateral, value: val },
};
onUserChangesLeverageOrCollateralValue({
newPosition: newPos,
setNewPosition,
@ -101,9 +140,18 @@ export default function NewPositionForm({ lendingReserve, newPosition, setNewPos
});
}}
onCollateralReserve={(key) => {
const id: string = cache.byParser(LendingReserveParser).find((acc) => acc === key) || '';
const id: string =
cache
.byParser(LendingReserveParser)
.find((acc) => acc === key) || "";
const parser = cache.get(id) as ParsedAccount<LendingReserve>;
const newPos = { ...newPosition, collateral: { value: newPosition.collateral.value, type: parser } };
const newPos = {
...newPosition,
collateral: {
value: newPosition.collateral.value,
type: parser,
},
};
onUserChangesLeverageOrCollateralValue({
newPosition: newPos,
setNewPosition,
@ -126,14 +174,24 @@ export default function NewPositionForm({ lendingReserve, newPosition, setNewPos
</div>
<ArrowDownOutlined />
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'stretch' }}>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "stretch",
}}
>
{newPosition.asset.type && (
<CollateralInput
title='Choose trade'
title="Choose trade"
reserve={newPosition.asset.type.info}
amount={newPosition.asset.value}
onInputChange={(val: number | null) => {
const newPos = { ...newPosition, asset: { ...newPosition.asset, value: val } };
const newPos = {
...newPosition,
asset: { ...newPosition.asset, value: val },
};
onUserChangesAssetValue({
newPosition: newPos,
setNewPosition,
@ -146,11 +204,11 @@ export default function NewPositionForm({ lendingReserve, newPosition, setNewPos
/>
)}
<Button
className='trade-button'
type='primary'
size='large'
className="trade-button"
type="primary"
size="large"
onClick={connected ? null : wallet.connect}
style={{ width: '100%' }}
style={{ width: "100%" }}
disabled={connected && !!newPosition.error}
>
<span>{generateActionLabel(connected, newPosition)}</span>

View File

@ -1,14 +1,14 @@
import Card from 'antd/lib/card';
import React from 'react';
import { PoolPrice } from '../../../components/PoolPrice';
import { SupplyOverview } from '../../../components/SupplyOverview';
import { Position } from './interfaces';
import { usePoolAndTradeInfoFrom } from './utils';
import Card from "antd/lib/card";
import React from "react";
import { PoolPrice } from "../../../components/PoolPrice";
import { SupplyOverview } from "../../../components/SupplyOverview";
import { Position } from "./interfaces";
import { usePoolAndTradeInfoFrom } from "./utils";
export default function PoolHealth({ newPosition }: { newPosition: Position }) {
const { pool } = usePoolAndTradeInfoFrom(newPosition);
return (
<Card className='new-position-item new-position-item-bottom-left'>
<Card className="new-position-item new-position-item-bottom-left">
{!pool && <span>Choose a CCY to see exchange rate information.</span>}
{pool && (
<>

View File

@ -1,12 +1,12 @@
import React, { useState } from 'react';
import { useLendingReserve } from '../../../hooks';
import { useParams } from 'react-router-dom';
import './style.less';
import React, { useState } from "react";
import { useLendingReserve } from "../../../hooks";
import { useParams } from "react-router-dom";
import "./style.less";
import NewPositionForm from './NewPositionForm';
import { Position } from './interfaces';
import Breakdown from './Breakdown';
import PoolHealth from './PoolHealth';
import NewPositionForm from "./NewPositionForm";
import { Position } from "./interfaces";
import Breakdown from "./Breakdown";
import PoolHealth from "./PoolHealth";
export const NewPosition = () => {
const { id } = useParams<{ id: string }>();
@ -23,14 +23,21 @@ export const NewPosition = () => {
}
if (!newPosition.asset.type) {
setNewPosition({ ...newPosition, asset: { value: newPosition.asset.value, type: lendingReserve } });
setNewPosition({
...newPosition,
asset: { value: newPosition.asset.value, type: lendingReserve },
});
}
return (
<div className='new-position'>
<div className='new-position-container'>
<div className='new-position-item-left'>
<NewPositionForm lendingReserve={lendingReserve} newPosition={newPosition} setNewPosition={setNewPosition} />
<div className="new-position">
<div className="new-position-container">
<div className="new-position-item-left">
<NewPositionForm
lendingReserve={lendingReserve}
newPosition={newPosition}
setNewPosition={setNewPosition}
/>
<PoolHealth newPosition={newPosition} />
</div>
<Breakdown item={newPosition} />

View File

@ -1,5 +1,5 @@
import { ParsedAccount } from '../../../contexts/accounts';
import { LendingReserve } from '../../../models/lending/reserve';
import { ParsedAccount } from "../../../contexts/accounts";
import { LendingReserve } from "../../../models/lending/reserve";
export interface Token {
mintAddress: string;

View File

@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { LABELS } from '../../../constants';
import { Position } from './interfaces';
import { usePoolAndTradeInfoFrom } from './utils';
import { useEffect } from "react";
import { LABELS } from "../../../constants";
import { Position } from "./interfaces";
import { usePoolAndTradeInfoFrom } from "./utils";
export function useLeverage({
newPosition,
@ -33,18 +33,28 @@ export function useLeverage({
return;
}
if (!desiredType || !newPosition.asset.value || !enrichedPools || enrichedPools.length === 0) {
if (
!desiredType ||
!newPosition.asset.value ||
!enrichedPools ||
enrichedPools.length === 0
) {
return;
}
// If there is more of A than B
const exchangeRate = enrichedPools[0].liquidityB / enrichedPools[0].liquidityA;
const exchangeRate =
enrichedPools[0].liquidityB / enrichedPools[0].liquidityA;
const leverageDesired = newPosition.leverage;
const amountAvailableInOysterForMargin = collateralDeposit.info.amount * exchangeRate;
const amountAvailableInOysterForMargin =
collateralDeposit.info.amount * exchangeRate;
const amountToDepositOnMargin = desiredValue / leverageDesired;
if (amountToDepositOnMargin > amountAvailableInOysterForMargin) {
setNewPosition({ ...newPosition, error: LABELS.NOT_ENOUGH_MARGIN_MESSAGE });
setNewPosition({
...newPosition,
error: LABELS.NOT_ENOUGH_MARGIN_MESSAGE,
});
return;
}
@ -71,6 +81,14 @@ export function useLeverage({
setNewPosition({ ...newPosition, error: LABELS.LEVERAGE_LIMIT_MESSAGE });
return;
}
setNewPosition({ ...newPosition, error: '' });
}, [collType, desiredType, desiredValue, leverage, enrichedPools, collValue, collateralDeposit]);
setNewPosition({ ...newPosition, error: "" });
}, [
collType,
desiredType,
desiredValue,
leverage,
enrichedPools,
collValue,
collateralDeposit,
]);
}

View File

@ -1,9 +1,9 @@
import { ParsedAccount } from '../../../contexts/accounts';
import { useEnrichedPools } from '../../../contexts/market';
import { UserDeposit, useUserDeposits } from '../../../hooks';
import { LendingReserve, PoolInfo } from '../../../models';
import { usePoolForBasket } from '../../../utils/pools';
import { Position } from './interfaces';
import { ParsedAccount } from "../../../contexts/accounts";
import { useEnrichedPools } from "../../../contexts/market";
import { UserDeposit, useUserDeposits } from "../../../hooks";
import { LendingReserve, PoolInfo } from "../../../models";
import { usePoolForBasket } from "../../../utils/pools";
import { Position } from "./interfaces";
export function usePoolAndTradeInfoFrom(
newPosition: Position
@ -29,7 +29,9 @@ export function usePoolAndTradeInfoFrom(
const userDeposits = useUserDeposits();
const collateralDeposit = userDeposits.userDeposits.find(
(u) => u.reserve.info.liquidityMint.toBase58() === collType?.info?.liquidityMint?.toBase58()
(u) =>
u.reserve.info.liquidityMint.toBase58() ===
collType?.info?.liquidityMint?.toBase58()
);
const enrichedPools = useEnrichedPools(pool ? [pool] : []);

View File

@ -1,5 +1,10 @@
import React from "react";
import { useBorrowingPower, useEnrichedLendingObligation, useLendingReserve, useUserObligations } from "../../hooks";
import {
useBorrowingPower,
useEnrichedLendingObligation,
useLendingReserve,
useUserObligations,
} from "../../hooks";
import { useParams } from "react-router-dom";
import { RepayInput } from "../../components/RepayInput";
@ -36,7 +41,6 @@ export const RepayReserveView = () => {
return null;
}
return (
<div className="repay-reserve">
<Row gutter={GUTTER}>
@ -76,8 +80,11 @@ export const RepayReserveView = () => {
<BarChartStatistic
title="Your Loans"
items={userObligations}
getPct={(item) => item.obligation.info.borrowedInQuote / loansValue}
name={(item) => item.obligation.info.repayName} />
getPct={(item) =>
item.obligation.info.borrowedInQuote / loansValue
}
name={(item) => item.obligation.info.repayName}
/>
</Card>
</Col>
</Row>
@ -98,5 +105,6 @@ export const RepayReserveView = () => {
/>
</Col>
</Row>
</div>);
}
</div>
);
};

View File

@ -21,9 +21,7 @@ export const ReserveView = () => {
<div className="flexColumn">
<Row gutter={GUTTER}>
<Col sm={24} md={12} lg={14} xl={15} xxl={18}>
<ReserveStatus
reserve={reserve}
address={lendingReserve.pubkey} />
<ReserveStatus reserve={reserve} address={lendingReserve.pubkey} />
</Col>
<Col sm={24} md={12} lg={10} xl={9} xxl={6}>
<UserLendingCard

View File

@ -1,8 +1,8 @@
import React from 'react';
import React from "react";
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
if (process.env.NODE_ENV === "development") {
const whyDidYouRender = require("@welldone-software/why-did-you-render");
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
}