Merge pull request #15 from dummytester123/main

adding Margin Trading Scaffolding
This commit is contained in:
Bartosz Lipinski 2020-12-30 08:34:37 -06:00 committed by GitHub
commit 530397afdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 3979 additions and 855 deletions

View File

@ -1,5 +1,5 @@
## ⚠️ 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.
## TODO

35
package-lock.json generated
View File

@ -4311,6 +4311,32 @@
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
},
"chart.js": {
"version": "2.9.4",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz",
"integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==",
"requires": {
"chartjs-color": "^2.1.0",
"moment": "^2.10.2"
}
},
"chartjs-color": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",
"integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==",
"requires": {
"chartjs-color-string": "^0.6.0",
"color-convert": "^1.9.3"
}
},
"chartjs-color-string": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz",
"integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==",
"requires": {
"color-name": "^1.0.0"
}
},
"cheerio": {
"version": "0.22.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
@ -12566,6 +12592,15 @@
"whatwg-fetch": "^3.0.0"
}
},
"react-chartjs-2": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-2.11.1.tgz",
"integrity": "sha512-G7cNq/n2Bkh/v4vcI+GKx7Q1xwZexKYhOSj2HmrFXlvNeaURWXun6KlOUpEQwi1cv9Tgs4H3kGywDWMrX2kxfA==",
"requires": {
"lodash": "^4.17.19",
"prop-types": "^15.7.2"
}
},
"react-copy-to-clipboard": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz",

View File

@ -19,12 +19,15 @@
"bn.js": "^5.1.3",
"bs58": "^4.0.1",
"buffer-layout": "^1.2.0",
"chart.js": "^2.9.4",
"craco-less": "^1.17.0",
"echarts": "^4.9.0",
"eventemitter3": "^4.0.7",
"identicon.js": "^2.3.3",
"jazzicon": "^1.5.0",
"lodash": "^4.17.20",
"react": "^16.13.1",
"react-chartjs-2": "^2.11.1",
"react-dom": "^16.13.1",
"react-github-btn": "^1.2.0",
"react-intl": "^5.10.2",

View File

@ -1,17 +1,8 @@
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 "../constants/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[],
@ -25,31 +16,11 @@ 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;
}
@ -153,16 +124,9 @@ 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;
}
@ -196,25 +160,10 @@ 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

@ -8,7 +8,7 @@ import { sendTransaction } from "../contexts/connection";
import { notify } from "../utils/notifications";
import { LendingReserve } from "./../models/lending/reserve";
import { AccountLayout, MintInfo, MintLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID } from "../constants/ids";
import { LENDING_PROGRAM_ID } from "../utils/ids";
import {
createTempMemoryAccount,
createUninitializedAccount,
@ -16,8 +16,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,
@ -46,47 +46,38 @@ 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 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,
@ -100,21 +91,19 @@ export const borrow = async (
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 = [];
@ -135,19 +124,15 @@ 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;
}
@ -180,20 +165,12 @@ 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.bids
: dexMarket?.info.asks;
const memory = createTempMemoryAccount(
instructions,
wallet.publicKey,
signers
);
const memory = createTempMemoryAccount(instructions, wallet.publicKey, signers);
// deposit
instructions.push(
@ -221,17 +198,11 @@ export const borrow = 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 borrowed.",
type: "success",
message: 'Funds borrowed.',
type: 'success',
description: `Transaction - ${tx}`,
});
} catch {

View File

@ -12,7 +12,7 @@ import {
LendingReserve,
} from "./../models/lending";
import { AccountLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID } from "../constants/ids";
import { LENDING_PROGRAM_ID } from "../utils/ids";
import {
createUninitializedAccount,
ensureSplAccount,
@ -29,9 +29,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,9 +41,7 @@ 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
@ -82,12 +80,7 @@ export const deposit = async (
signers
);
} else {
toAccount = createUninitializedAccount(
instructions,
wallet.publicKey,
accountRentExempt,
signers
);
toAccount = createUninitializedAccount(instructions, wallet.publicKey, accountRentExempt, signers);
}
if (isInitalized) {
@ -125,17 +118,11 @@ 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

@ -9,13 +9,12 @@ import { notify } from "../utils/notifications";
import { LendingReserve } from "./../models/lending/reserve";
import { liquidateInstruction } from "./../models/lending/liquidate";
import { AccountLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID } from "../constants/ids";
import { LENDING_PROGRAM_ID } from "../utils/ids";
import { createTempMemoryAccount, ensureSplAccount, findOrCreateAccountByMint } from "./account";
import { approve, LendingMarket, LendingObligation, TokenAccount } from "../models";
import { cache, ParsedAccount } from "../contexts/accounts";
export const liquidate = async (
connection: Connection,
wallet: any,
from: TokenAccount, // liquidity account
@ -26,12 +25,12 @@ export const liquidate = async (
repayReserve: ParsedAccount<LendingReserve>,
withdrawReserve: ParsedAccount<LendingReserve>,
withdrawReserve: ParsedAccount<LendingReserve>
) => {
notify({
message: "Repaing funds...",
description: "Please review transactions to approve.",
type: "warn",
message: 'Repaing funds...',
description: 'Please review transactions to approve.',
type: 'warn',
});
// user from account
@ -39,9 +38,7 @@ 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()],
@ -87,24 +84,15 @@ 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.bids
: dexMarket?.info.asks;
console.log(dexMarketAddress.toBase58());
console.log(dexMarketAddress.toBase58())
const memory = createTempMemoryAccount(
instructions,
wallet.publicKey,
signers
);
const memory = createTempMemoryAccount(instructions, wallet.publicKey, signers);
instructions.push(
liquidateInstruction(
@ -123,17 +111,11 @@ 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

@ -9,7 +9,7 @@ import { notify } from "../utils/notifications";
import { LendingReserve } from "./../models/lending/reserve";
import { repayInstruction } from "./../models/lending/repay";
import { AccountLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID } from "../constants/ids";
import { LENDING_PROGRAM_ID } from "../utils/ids";
import { findOrCreateAccountByMint } from "./account";
import { approve, LendingObligation, TokenAccount } from "../models";
import { ParsedAccount } from "../contexts/accounts";
@ -31,9 +31,9 @@ export const repay = async (
wallet: any
) => {
notify({
message: "Repaing funds...",
description: "Please review transactions to approve.",
type: "warn",
message: 'Repaing funds...',
description: 'Please review transactions to approve.',
type: 'warn',
});
// user from account
@ -41,9 +41,7 @@ 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()],
@ -101,17 +99,11 @@ export const repay = 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 repaid.",
type: "success",
message: 'Funds repaid.',
type: 'success',
description: `Transaction - ${tx}`,
});
};

View File

@ -8,7 +8,7 @@ import { sendTransaction } from "../contexts/connection";
import { notify } from "../utils/notifications";
import { LendingReserve, withdrawInstruction } from "./../models/lending";
import { AccountLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID } from "../constants/ids";
import { LENDING_PROGRAM_ID } from "../utils/ids";
import { findOrCreateAccountByMint } from "./account";
import { approve, TokenAccount } from "../models";
@ -21,9 +21,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,14 +31,9 @@ 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;
@ -76,17 +71,11 @@ export const withdraw = 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

@ -0,0 +1,154 @@
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;
// User can choose a collateral they want to use, and then this will display the balance they have in Oyster's lending
// reserve for that collateral type.
export default function CollateralInput(props: {
title: string;
amount?: number | null;
reserve: LendingReserve;
disabled?: boolean;
onCollateralReserve?: (id: string) => void;
onLeverage?: (leverage: number) => void;
onInputChange: (value: number | null) => void;
hideBalance?: boolean;
showLeverageSelector?: boolean;
leverage?: number;
}) {
const { reserveAccounts } = useLendingReserves();
const { tokenMap } = useConnectionConfig();
const [collateralReserve, setCollateralReserve] = useState<string>();
const [balance, setBalance] = useState<number>(0);
const [lastAmount, setLastAmount] = useState<string>('');
const userDeposits = useUserDeposits();
useEffect(() => {
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()
);
if (collateralDeposit) setBalance(collateralDeposit.info.amount);
else setBalance(0);
}
}, [collateralReserve, userDeposits]);
const market = cache.get(props.reserve.lendingMarket) as ParsedAccount<LendingMarket>;
if (!market) return null;
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))
.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' }}>
<TokenIcon mintAddress={mint} />
{name}
</div>
</Option>
);
});
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>
{!props.hideBalance && (
<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' }}>
<NumericInput
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);
else props.onInputChange(parseFloat(val));
}
setLastAmount(val);
}}
style={{
fontSize: 20,
boxShadow: 'none',
borderColor: 'transparent',
outline: 'transparent',
}}
placeholder='0.00'
/>
<div className='ccy-input-header-right' style={{ display: 'flex' }}>
{props.showLeverageSelector && (
<Select
size='large'
showSearch
style={{ minWidth: 150 }}
placeholder='CCY'
value={props.leverage}
onChange={(item: number) => {
if (props.onLeverage) props.onLeverage(item);
}}
notFoundContent={null}
onSearch={(item: string) => {
if (props.onLeverage && item.match(/^\d+$/)) {
props.onLeverage(parseFloat(item));
}
}}
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'}
</div>
</Option>
))}
</Select>
)}
{!props.disabled ? (
<Select
size='large'
showSearch
style={{ minWidth: 150 }}
placeholder='CCY'
value={collateralReserve}
onChange={(item) => {
if (props.onCollateralReserve) props.onCollateralReserve(item);
setCollateralReserve(item);
}}
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())}
mintAddress={props.reserve.liquidityMint.toBase58()}
showBalance={false}
/>
)}
</div>
</div>
</Card>
);
}

View File

@ -0,0 +1,62 @@
.ccy-input {
margin-top: 10px;
margin-bottom: 10px;
.ant-select-selector,
.ant-select-selector:focus,
.ant-select-selector:active {
border-color: transparent !important;
box-shadow: none !important;
}
.ant-select-selection-item {
display: flex;
.token-balance {
display: none;
}
}
}
.token-balance {
color: grey;
}
.ccy-input-header {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 10px;
-webkit-box-pack: justify;
justify-content: space-between;
-webkit-box-align: center;
align-items: center;
flex-direction: row;
padding: 10px 20px 0px 20px;
}
.ccy-input-header-left {
width: 100%;
box-sizing: border-box;
margin: 0px;
min-width: 0px;
display: flex;
padding: 0px;
-webkit-box-align: center;
align-items: center;
width: fit-content;
}
.ccy-input-header-right {
width: 100%;
display: flex;
flex-direction: row;
-webkit-box-align: center;
align-items: center;
justify-self: flex-end;
justify-content: flex-end;
}
.ant-select-dropdown {
width: 150px !important;
}

View File

@ -52,6 +52,7 @@ export const CollateralSelector = (props: {
const market = cache.get(props.reserve?.lendingMarket) as ParsedAccount<
LendingMarket
>;
if (!market) return null;
const quoteMintAddress = market?.info?.quoteMint?.toBase58();
@ -60,10 +61,10 @@ export const CollateralSelector = (props: {
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}
@ -72,23 +73,16 @@ 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}

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);
}
};
@ -14,26 +14,20 @@ export class NumericInput extends React.Component<any, any> {
onBlur = () => {
const { value, onBlur, onChange } = this.props;
let valueTemp = value;
if (value.charAt(value.length - 1) === "." || value === "-") {
if (value === undefined || value === null) return;
if (value.charAt && (value.charAt(value.length - 1) === '.' || value === '-')) {
valueTemp = value.slice(0, -1);
}
if (value.startsWith(".") || value.startsWith("-.")) {
valueTemp = valueTemp.replace(".", "0.");
if (value.startsWith && (value.startsWith('.') || value.startsWith('-.'))) {
valueTemp = valueTemp.replace('.', '0.');
}
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,6 +9,7 @@ import {
ShoppingOutlined,
HomeOutlined,
RocketOutlined,
LineChartOutlined
} from "@ant-design/icons";
import BasicLayout from "@ant-design/pro-layout";
@ -104,8 +105,17 @@ export const AppLayout = (props: any) => {
{LABELS.MENU_LIQUIDATE}
</Link>
</Menu.Item>
<Menu.Item key="6" icon={< LineChartOutlined/>}>
<Link
to={{
pathname: "/marginTrading",
}}
>
{LABELS.MARGIN_TRADING}
</Link>
</Menu.Item>
{env !== "mainnet-beta" && (
<Menu.Item key="6" icon={<RocketOutlined />}>
<Menu.Item key="7" icon={<RocketOutlined />}>
<Link
to={{
pathname: "/faucet",

View File

@ -0,0 +1,56 @@
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 enriched = useEnrichedPools(pools)[0];
const { userAccounts } = useUserAccounts();
const lpMint = useMint(pool.pubkeys.mint);
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);
if (!enriched) {
return null;
}
return (
<Card
className='ccy-input'
style={{ borderRadius: 20, width: '100%' }}
bodyStyle={{ padding: '7px' }}
size='small'
title='Prices and pool share'
>
<Row style={{ width: '100%' }}>
<Col span={8}>
{formatPriceNumber.format(parseFloat(enriched.liquidityA) / parseFloat(enriched.liquidityB))}
</Col>
<Col span={8}>
{formatPriceNumber.format(parseFloat(enriched.liquidityB) / parseFloat(enriched.liquidityA))}
</Col>
<Col span={8}>
{ratio * 100 < 0.001 && ratio > 0 ? '<' : ''}
&nbsp;{formatPriceNumber.format(ratio * 100)}%
</Col>
</Row>
<Row style={{ width: '100%' }}>
<Col span={8}>
{enriched.names[0]} per {enriched.names[1]}
</Col>
<Col span={8}>
{enriched.names[1]} per {enriched.names[0]}
</Col>
<Col span={8}>Share of pool</Col>
</Row>
</Card>
);
};

View File

@ -0,0 +1,100 @@
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;
const pools = useMemo(() => (pool ? [pool] : []), [pool]);
const enriched = useEnrichedPools(pools);
const chartDiv = useRef<HTMLDivElement>(null);
// dispose chart
useEffect(() => {
const div = chartDiv.current;
return () => {
let instance = div && echarts.getInstanceByDom(div);
instance && instance.dispose();
};
}, []);
useEffect(() => {
if (!chartDiv.current || enriched.length === 0) {
return;
}
let instance = echarts.getInstanceByDom(chartDiv.current);
if (!instance) {
instance = echarts.init(chartDiv.current as any);
}
const data = [
{
name: enriched[0].names[0],
value: enriched[0].liquidityAinUsd,
tokens: enriched[0].liquidityA,
},
{
name: enriched[0].names[1],
value: enriched[0].liquidityBinUsd,
tokens: enriched[0].liquidityB,
},
];
instance.setOption({
tooltip: {
trigger: 'item',
formatter: function (params: any) {
var val = formatUSD.format(params.value);
var tokenAmount = formatNumber.format(params.data.tokens);
return `${params.name}: \n${val}\n(${tokenAmount})`;
},
},
series: [
{
name: 'Liquidity',
type: 'pie',
top: 0,
bottom: 0,
left: 0,
right: 0,
animation: false,
label: {
fontSize: 14,
show: true,
formatter: function (params: any) {
var val = formatUSD.format(params.value);
var tokenAmount = formatNumber.format(params.data.tokens);
return `{c|${params.name}}\n{r|${tokenAmount}}\n{r|${val}}`;
},
rich: {
c: {
color: 'black',
lineHeight: 22,
align: 'center',
},
r: {
color: 'black',
align: 'right',
},
},
color: 'rgba(255, 255, 255, 0.5)',
},
itemStyle: {
normal: {
borderColor: '#000',
},
},
data,
},
],
});
}, [enriched]);
if (enriched.length === 0) {
return null;
}
return <div ref={chartDiv} style={{ height: 150, width: '100%' }} />;
};

View File

@ -0,0 +1,47 @@
import React from 'react';
import { useMint, useAccountByMint } from '../../contexts/accounts';
import { TokenIcon } from '../TokenIcon';
export const TokenDisplay = (props: {
name: string;
mintAddress: string;
icon?: JSX.Element;
showBalance?: boolean;
}) => {
const { showBalance, mintAddress, name, icon } = props;
const tokenMint = useMint(mintAddress);
const tokenAccount = useAccountByMint(mintAddress);
let balance: number = 0;
let hasBalance: boolean = false;
if (showBalance) {
if (tokenAccount && tokenMint) {
balance = tokenAccount.info.amount.toNumber() / Math.pow(10, tokenMint.decimals);
hasBalance = balance > 0;
}
}
return (
<>
<div
title={mintAddress}
key={mintAddress}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<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>
) : null}
</div>
</>
);
};

View File

@ -1,28 +0,0 @@
import { PublicKey } from "@solana/web3.js";
export const WRAPPED_SOL_MINT = new PublicKey(
"So11111111111111111111111111111111111111112"
);
export let TOKEN_PROGRAM_ID = new PublicKey(
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
);
export let LENDING_PROGRAM_ID = new PublicKey(
"TokenLend1ng1111111111111111111111111111111"
);
export const setProgramIds = (envName: string) => {
// Add dynamic program ids
if (envName === "mainnet-beta") {
LENDING_PROGRAM_ID = new PublicKey(
"2KfJP7pZ6QSpXa26RmsN6kKVQteDEdQmizLSvuyryeiW"
);
}
};
export const programIds = () => {
return {
token: TOKEN_PROGRAM_ID,
lending: LENDING_PROGRAM_ID,
};
};

View File

@ -1,5 +1,4 @@
export * from "./ids";
export * from "./labels";
export * from "./math";
export * from "./marks";
export * from "./style";
export * from "./style";

View File

@ -12,7 +12,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",
@ -57,4 +57,21 @@ 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',
};

View File

@ -1,17 +1,21 @@
import React, { useCallback, useContext, useEffect, useState } from "react";
import { useConnection } from "./connection";
import { useWallet } from "./wallet";
import { AccountInfo, Connection, PublicKey } from "@solana/web3.js";
import { programIds, WRAPPED_SOL_MINT } from "./../constants/ids";
import { AccountLayout, u64, MintInfo, MintLayout } from "@solana/spl-token";
import { TokenAccount } from "./../models";
import { chunks } from "./../utils/utils";
import { EventEmitter } from "./../utils/eventEmitter";
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 { usePools } from '../utils/pools';
import { WRAPPED_SOL_MINT, programIds } from '../utils/ids';
const AccountsContext = React.createContext<any>(null);
const pendingCalls = new Map<string, Promise<ParsedAccountBase>>();
const genericCache = new Map<string, ParsedAccountBase>();
const pendingMintCalls = new Map<string, Promise<MintInfo>>();
const mintCache = new Map<string, MintInfo>();
export interface ParsedAccountBase {
pubkey: PublicKey;
@ -19,15 +23,23 @@ 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;
}
const getMintInfo = async (connection: Connection, pubKey: PublicKey) => {
const info = await connection.getAccountInfo(pubKey);
if (info === null) {
throw new Error('Failed to find mint account');
}
const data = Buffer.from(info.data);
return deserializeMint(data);
};
export const MintParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
const buffer = Buffer.from(info.data);
@ -44,10 +56,7 @@ 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);
@ -62,10 +71,7 @@ export const TokenAccountParser = (
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 = {
@ -83,13 +89,9 @@ 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;
@ -110,7 +112,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);
@ -119,17 +121,11 @@ 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);
@ -147,7 +143,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;
@ -155,6 +151,22 @@ export const cache = {
return genericCache.get(key);
},
delete: (pubKey: string | PublicKey) => {
let key: string;
if (typeof pubKey !== 'string') {
key = pubKey.toBase58();
} else {
key = pubKey;
}
if (genericCache.get(key)) {
genericCache.delete(key);
cache.emitter.raiseCacheDeleted(key);
return true;
}
return false;
},
byParser: (parser: AccountParser) => {
const result: string[] = [];
for (const id of keyToAccountParser.keys()) {
@ -167,12 +179,57 @@ 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);
}
return pubkey;
},
queryMint: async (connection: Connection, pubKey: string | PublicKey) => {
let id: PublicKey;
if (typeof pubKey === 'string') {
id = new PublicKey(pubKey);
} else {
id = pubKey;
}
const address = id.toBase58();
let mint = mintCache.get(address);
if (mint) {
return mint;
}
let query = pendingMintCalls.get(address);
if (query) {
return query;
}
query = getMintInfo(connection, id).then((data) => {
pendingMintCalls.delete(address);
mintCache.set(address, data);
return data;
}) as Promise<MintInfo>;
pendingMintCalls.set(address, query as any);
return query;
},
getMint: (pubKey: string | PublicKey) => {
let key: string;
if (typeof pubKey !== 'string') {
key = pubKey.toBase58();
} else {
key = pubKey;
}
return mintCache.get(key);
},
addMint: (pubKey: PublicKey, obj: AccountInfo<Buffer>) => {
const mint = deserializeMint(obj.data);
const id = pubKey.toBase58();
mintCache.set(id, mint);
return mint;
},
};
export const useAccountsContext = () => {
@ -181,10 +238,7 @@ 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;
}
@ -207,6 +261,27 @@ function wrapNativeAccount(
};
}
export function useCachedPool(legacy = false) {
const context = useContext(AccountsContext);
const allPools = context.pools as PoolInfo[];
const pools = useMemo(() => {
return allPools.filter((p) => p.legacy === legacy);
}, [allPools, legacy]);
return {
pools,
};
}
export const getCachedAccount = (predicate: (account: TokenAccount) => boolean) => {
for (const account of genericCache.values()) {
if (predicate(account)) {
return account as TokenAccount;
}
}
};
const UseNativeAccount = () => {
const connection = useConnection();
const { wallet } = useWallet();
@ -249,10 +324,7 @@ 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;
}
@ -275,28 +347,25 @@ export function AccountsProvider({ children = null as any }) {
const [tokenAccounts, setTokenAccounts] = useState<TokenAccount[]>([]);
const [userAccounts, setUserAccounts] = useState<TokenAccount[]>([]);
const { nativeAccount } = UseNativeAccount();
const { pools } = usePools();
const selectUserAccounts = useCallback(() => {
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]);
useEffect(() => {
const subs: number[] = [];
cache.emitter.onCache((args) => {
if(args.isNew) {
if (args.isNew) {
let id = args.id;
let deserialize = args.parser;
connection.onAccountChange(new PublicKey(id), (info) => {
@ -306,8 +375,8 @@ export function AccountsProvider({ children = null as any }) {
});
return () => {
subs.forEach(id => connection.removeAccountChangeListener(id));
}
subs.forEach((id) => connection.removeAccountChangeListener(id));
};
}, [connection]);
const publicKey = wallet?.publicKey;
@ -320,7 +389,7 @@ export function AccountsProvider({ children = null as any }) {
});
// This can return different types of accounts: token-account, mint, multisig
// TODO: web3.js expose ability to filter.
// TODO: web3.js expose ability to filter.
// this should use only filter syntax to only get accounts that are owned by user
const tokenSubID = connection.onProgramAccountChange(
programIds().token,
@ -337,7 +406,7 @@ export function AccountsProvider({ children = null as any }) {
}
}
},
"singleGossip"
'singleGossip'
);
return () => {
@ -350,6 +419,7 @@ export function AccountsProvider({ children = null as any }) {
<AccountsContext.Provider
value={{
userAccounts,
pools,
nativeAccount,
}}
>
@ -365,15 +435,9 @@ 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
@ -388,7 +452,7 @@ export const getMultipleAccounts = async (
const { data, ...rest } = acc;
const obj = {
...rest,
data: Buffer.from(data[0], "base64"),
data: Buffer.from(data[0], 'base64'),
} as AccountInfo<Buffer>;
return obj;
})
@ -398,18 +462,12 @@ export const getMultipleAccounts = async (
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) {
@ -425,7 +483,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) {
@ -440,9 +498,7 @@ 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 () => {
@ -453,6 +509,17 @@ export function useMint(key?: string | PublicKey) {
return mint;
}
export const useAccountByMint = (mint: string) => {
const { userAccounts } = useUserAccounts();
const index = userAccounts.findIndex((acc) => acc.info.mint.toBase58() === mint);
if (index !== -1) {
return userAccounts[index];
}
return;
};
export function useAccount(pubKey?: PublicKey) {
const connection = useConnection();
const [account, setAccount] = useState<TokenAccount>();
@ -465,9 +532,7 @@ 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);
}
@ -530,7 +595,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,36 +1,25 @@
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 { setProgramIds } from "./../constants/ids";
import { notify } from "./../utils/notifications";
import { ExplorerLink } from "../components/ExplorerLink";
import LocalTokens from "../config/tokens.json";
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: "lending" as ENV,
endpoint: "https://tln.solana.com",
name: 'lending' as ENV,
endpoint: 'https://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;
@ -53,43 +42,29 @@ 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();
})
@ -125,10 +100,7 @@ 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);
};
@ -186,7 +158,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);
@ -220,9 +192,7 @@ 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,
@ -235,37 +205,30 @@ 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 "./../constants/ids";
import React, { useCallback, useEffect, useState } from 'react';
import { useConnection } from './connection';
import { LENDING_PROGRAM_ID } from './../utils/ids';
import {
LendingMarketParser,
isLendingReserve,
@ -9,17 +9,12 @@ import {
LendingReserve,
isLendingObligation,
LendingObligationParser,
} from "./../models/lending";
import {
cache,
getMultipleAccounts,
MintParser,
ParsedAccount,
} from "./accounts";
import { PublicKey } 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 } from '@solana/web3.js';
import { DexMarketParser } from '../models/dex';
import { usePrecacheMarket } from './market';
import { useLendingReserves } from '../hooks';
export interface LendingContextState {}
@ -48,33 +43,19 @@ export const useLending = () => {
const processAccount = useCallback((item) => {
if (isLendingReserve(item.account)) {
const reserve = cache.add(
item.pubkey.toBase58(),
item.account,
LendingReserveParser
);
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
);
return cache.add(item.pubkey.toBase58(), item.account, LendingMarketParser);
} else if (isLendingObligation(item.account)) {
return cache.add(
item.pubkey.toBase58(),
item.account,
LendingObligationParser
);
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]);
@ -83,36 +64,21 @@ 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;
}),
@ -120,15 +86,13 @@ 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);
@ -152,7 +116,7 @@ export const useLending = () => {
};
processAccount(item);
},
"singleGossip"
'singleGossip'
);
return () => {

View File

@ -1,15 +1,25 @@
import React, { useCallback, useContext, useEffect, useState } from "react";
import { MINT_TO_MARKET } from "./../models/marketOverrides";
import { fromLamports, 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 { useConnection, 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 } from "../models";
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');
export const BONFIDA_POOL_INTERVAL = 30 * 60_000; // 30 min
interface RecentPoolData {
pool_identifier: string;
volume24hA: number;
}
export interface MarketsContextState {
midPriceInUSD: (mint: string) => number;
@ -20,6 +30,7 @@ export interface MarketsContextState {
subscribeToMarket: (mint: string) => () => void;
precacheMarkets: (mints: string[]) => void;
dailyVolume: Map<string, RecentPoolData>;
}
const REFRESH_INTERVAL = 30_000;
@ -32,24 +43,19 @@ 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 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, {
@ -63,6 +69,7 @@ export function MarketProvider({ children = null as any }) {
useEffect(() => {
let timer = 0;
let bonfidaTimer = 0;
const updateData = async () => {
await refreshAccounts(connection, [...accountsToObserve.keys()]);
@ -71,6 +78,23 @@ export function MarketProvider({ children = null as any }) {
timer = window.setTimeout(() => updateData(), REFRESH_INTERVAL);
};
const bonfidaQuery = async () => {
try {
const resp = await window.fetch('https://serum-api.bonfida.com/pools-recent');
const data = await resp.json();
const map = (data?.data as RecentPoolData[]).reduce((acc, item) => {
acc.set(item.pool_identifier, item);
return acc;
}, new Map<string, RecentPoolData>());
setDailyVolume(map);
} catch {
// ignore
}
bonfidaTimer = window.setTimeout(() => bonfidaQuery(), BONFIDA_POOL_INTERVAL);
};
const initalQuery = async () => {
const reverseSerumMarketCache = new Map<string, string>();
[...marketByMint.keys()].forEach((mint) => {
@ -88,7 +112,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(() => {});
@ -141,15 +165,13 @@ export function MarketProvider({ children = null as any }) {
return () => {
window.clearTimeout(timer);
window.clearTimeout(bonfidaTimer);
};
}, [marketByMint, accountsToObserve, connection]);
const midPriceInUSD = useCallback(
(mintAddress: string) => {
return getMidPrice(
marketByMint.get(mintAddress)?.marketInfo.address.toBase58(),
mintAddress
);
return getMidPrice(marketByMint.get(mintAddress)?.marketInfo.address.toBase58(), mintAddress);
},
[marketByMint]
);
@ -157,7 +179,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 () => {};
}
@ -206,6 +228,7 @@ export function MarketProvider({ children = null as any }) {
marketByMint,
subscribeToMarket,
precacheMarkets,
dailyVolume,
}}
>
{children}
@ -218,10 +241,189 @@ export const useMarkets = () => {
return context as MarketsContextState;
};
export const useEnrichedPools = (pools: PoolInfo[]) => {
const context = useContext(MarketsContext);
const { tokenMap } = useConnectionConfig();
const [enriched, setEnriched] = useState<any[]>([]);
const subscribeToMarket = context?.subscribeToMarket;
const marketEmitter = context?.marketEmitter;
const marketsByMint = context?.marketByMint;
const dailyVolume = context?.dailyVolume;
const poolKeys = pools.map((p) => p.pubkeys.account.toBase58()).join(',');
useEffect(() => {
if (!marketEmitter || !subscribeToMarket || pools.length == 0) {
return;
}
//@ts-ignore
const mints = [...new Set([...marketsByMint?.keys()]).keys()];
const subscriptions = mints.map((m) => subscribeToMarket(m));
const update = () => {
setEnriched(createEnrichedPools(pools, marketsByMint, dailyVolume, tokenMap));
};
const dispose = marketEmitter.onMarket(update);
update();
return () => {
dispose && dispose();
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]);
return enriched;
};
// TODO:
// 1. useEnrichedPools
// combines market and pools and user info
// 2. ADD useMidPrice with event to refresh price
// that could subscribe to multiple markets and trigger refresh of those markets only when there is active subscription
function createEnrichedPools(
pools: PoolInfo[],
marketByMint: Map<string, SerumMarket> | undefined,
poolData: Map<string, RecentPoolData> | undefined,
tokenMap: KnownTokenMap
) {
const TODAY = new Date();
if (!marketByMint) {
return [];
}
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 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 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 quoteReserveUSD = quote * convert(accountB, mintB);
const poolMint = cache.getMint(p.pubkeys.mint);
if (poolMint?.supply.eqn(0)) {
return undefined;
}
let airdropYield = calculateAirdropYield(p, marketByMint, baseReserveUSD, quoteReserveUSD);
let volume = 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;
let apy24h = airdropYield;
if (p.pubkeys.feeAccount) {
const feeAccount = cache.get(p.pubkeys.feeAccount);
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;
volume = poolOwnerFees / 0.0004;
fees = volume * LIQUIDITY_PROVIDER_FEE;
if (fees !== 0) {
const baseVolume = (ownedPct * baseReserveUSD) / 0.0004;
const quoteVolume = (ownedPct * quoteReserveUSD) / 0.0004;
// 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)
);
const apy0 =
parseFloat(((baseVolume / daysSinceInception) * LIQUIDITY_PROVIDER_FEE * 356) as any) / baseReserveUSD;
const apy1 =
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;
apy24h = apy24h + apy24h0;
}
}
}
const lpMint = cache.getMint(p.pubkeys.mint);
const name = getPoolName(tokenMap, p);
const link = `#/?pair=${getPoolName(tokenMap, p, false).replace('/', '-')}`;
return {
key: p.pubkeys.account.toBase58(),
id: index,
name,
names: mints.map((m) => getTokenName(tokenMap, m)),
accounts: [accountA?.pubkey, accountB?.pubkey],
address: p.pubkeys.mint.toBase58(),
link,
mints,
liquidityA: convert(accountA, mintA),
liquidityAinUsd: baseReserveUSD,
liquidityB: convert(accountB, mintB),
liquidityBinUsd: quoteReserveUSD,
supply: lpMint && (lpMint?.supply.toNumber() / Math.pow(10, lpMint?.decimals || 0)).toFixed(9),
fees,
fees24h,
liquidity: baseReserveUSD + quoteReserveUSD,
volume,
volume24h,
apy: Number.isFinite(apy) ? apy : 0,
apy24h: Number.isFinite(apy24h) ? apy24h : 0,
map: poolData,
extra: poolData?.get(p.pubkeys.account.toBase58()),
raw: p,
};
})
.filter((p) => p !== undefined);
return result;
}
function calculateAirdropYield(
p: PoolInfo,
marketByMint: Map<string, SerumMarket>,
baseReserveUSD: number,
quoteReserveUSD: number
) {
let airdropYield = 0;
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;
if (market) {
const midPrice = getMidPrice(market?.toBase58(), item.mint.toBase58());
acc =
acc +
// airdrop yield
((item.amount * midPrice) / (baseReserveUSD + quoteReserveUSD)) * (365 / 30);
}
return acc;
}, 0);
}
return airdropYield;
}
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(() => {
@ -249,11 +451,7 @@ export const usePrecacheMarket = () => {
return context.precacheMarkets;
};
export const simulateMarketOrderFill = (
amount: number,
reserve: LendingReserve,
dex: PublicKey
) => {
export const simulateMarketOrderFill = (amount: number, reserve: LendingReserve, dex: PublicKey) => {
const liquidityMint = cache.get(reserve.liquidityMint);
const collateralMint = cache.get(reserve.collateralMint);
if (!liquidityMint || !collateralMint) {
@ -266,22 +464,12 @@ export const simulateMarketOrderFill = (
}
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
@ -320,11 +508,9 @@ export const simulateMarketOrderFill = (
};
const getMidPrice = (marketAddress?: string, mintAddress?: string) => {
const SERUM_TOKEN = TOKEN_MINTS.find(
(a) => a.address.toBase58() === mintAddress
);
const SERUM_TOKEN = TOKEN_MINTS.find((a) => a.address.toBase58() === mintAddress);
if (STABLE_COINS.has(SERUM_TOKEN?.name || "")) {
if (STABLE_COINS.has(SERUM_TOKEN?.name || '')) {
return 1.0;
}
@ -339,18 +525,10 @@ 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;
@ -375,14 +553,12 @@ 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 {

11
src/models/airdrops.ts Normal file
View File

@ -0,0 +1,11 @@
import { PublicKey } from '@solana/web3.js';
interface PoolAirdrop {
pool: PublicKey;
airdrops: {
mint: PublicKey;
amount: number;
}[];
}
export const POOLS_WITH_AIRDROP: PoolAirdrop[] = [];

View File

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

View File

@ -1,16 +1,11 @@
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 { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids";
import { wadToLamports } from "../../utils/utils";
import * as Layout from "./../../utils/layout";
import { LendingInstruction } from "./lending";
import { LendingReserve } from "./reserve";
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 { wadToLamports } from '../../utils/utils';
import * as Layout from './../../utils/layout';
import { LendingInstruction } from './lending';
import { LendingReserve } from './reserve';
export enum BorrowAmountType {
LiquidityBorrowAmount = 0,
@ -60,9 +55,9 @@ export const borrowInstruction = (
memory: 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);
@ -113,8 +108,7 @@ export const borrowInstruction = (
export const calculateBorrowAPY = (reserve: LendingReserve) => {
const totalBorrows = wadToLamports(reserve.borrowedLiquidityWad).toNumber();
const currentUtilization =
totalBorrows / (reserve.availableLiquidity.toNumber() + totalBorrows);
const currentUtilization = totalBorrows / (reserve.availableLiquidity.toNumber() + totalBorrows);
const optimalUtilization = reserve.config.optimalUtilizationRate / 100;
let borrowAPY;
@ -122,16 +116,12 @@ 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,16 +1,12 @@
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction,
} from "@solana/web3.js";
import BN from "bn.js";
import * as BufferLayout from "buffer-layout";
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids";
import { wadToLamports } from "../../utils/utils";
import * as Layout from "./../../utils/layout";
import { calculateBorrowAPY } from "./borrow";
import { LendingInstruction } from "./lending";
import { 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 { wadToLamports } from '../../utils/utils';
import * as Layout from './../../utils/layout';
import { calculateBorrowAPY } from './borrow';
import { LendingInstruction } from './lending';
import { LendingReserve } from './reserve';
/// Deposit liquidity into a reserve. The output is a collateral token representing ownership
/// of the reserve liquidity pool.
@ -32,10 +28,7 @@ export const depositInstruction = (
reserveSupply: PublicKey,
collateralMint: 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(
@ -65,8 +58,7 @@ export const depositInstruction = (
export const calculateDepositAPY = (reserve: LendingReserve) => {
const totalBorrows = wadToLamports(reserve.borrowedLiquidityWad).toNumber();
const currentUtilization =
totalBorrows / (reserve.availableLiquidity.toNumber() + totalBorrows);
const currentUtilization = totalBorrows / (reserve.availableLiquidity.toNumber() + totalBorrows);
const borrowAPY = calculateBorrowAPY(reserve);
return currentUtilization * borrowAPY;

View File

@ -1,13 +1,9 @@
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction,
} from "@solana/web3.js";
import BN from "bn.js";
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids";
import { LendingInstruction } from "./lending";
import * as BufferLayout from "buffer-layout";
import * as Layout from "./../../utils/layout";
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.
///
@ -38,10 +34,7 @@ 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(

View File

@ -1,13 +1,9 @@
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction,
} from "@solana/web3.js";
import BN from "bn.js";
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids";
import { LendingInstruction } from "./lending";
import * as BufferLayout from "buffer-layout";
import * as Layout from "./../../utils/layout";
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.
@ -37,10 +33,7 @@ export const repayInstruction = (
obligationInput: PublicKey,
authority: 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,54 +4,52 @@ import {
SYSVAR_CLOCK_PUBKEY,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from "@solana/web3.js";
import BN from "bn.js";
import * as BufferLayout from "buffer-layout";
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/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(
[
Layout.uint64("lastUpdateSlot"),
Layout.publicKey("lendingMarket"),
Layout.publicKey("liquidityMint"),
BufferLayout.u8("liquidityMintDecimals"),
Layout.publicKey("liquiditySupply"),
Layout.publicKey("collateralMint"),
Layout.publicKey("collateralSupply"),
// TODO: replace u32 option with generic quivalent
BufferLayout.u32("dexMarketOption"),
Layout.publicKey("dexMarket"),
export const LendingReserveLayout: typeof BufferLayout.Structure = BufferLayout.struct([
Layout.uint64('lastUpdateSlot'),
Layout.publicKey('lendingMarket'),
Layout.publicKey('liquidityMint'),
BufferLayout.u8('liquidityMintDecimals'),
Layout.publicKey('liquiditySupply'),
Layout.publicKey('collateralMint'),
Layout.publicKey('collateralSupply'),
// 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"),
],
"config"
),
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'),
],
'config'
),
Layout.uint128("cumulativeBorrowRateWad"),
Layout.uint128("borrowedLiquidityWad"),
Layout.uint128('cumulativeBorrowRateWad'),
Layout.uint128('borrowedLiquidityWad'),
Layout.uint64("availableLiquidity"),
Layout.uint64("collateralMintSupply"),
]
);
Layout.uint64('availableLiquidity'),
Layout.uint64('collateralMintSupply'),
]);
export const isLendingReserve = (info: AccountInfo<Buffer>) => {
return info.data.length === LendingReserveLayout.span;
@ -86,10 +84,7 @@ export interface LendingReserve {
collateralMintSupply: BN;
}
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);
if (data.lastUpdateSlot.toNumber() === 0) return;
@ -123,9 +118,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);
@ -165,13 +160,8 @@ export const initReserveInstruction = (
};
export const calculateUtilizationRatio = (reserve: LendingReserve) => {
let borrowedLiquidity = wadToLamports(
reserve.borrowedLiquidityWad
).toNumber();
return (
borrowedLiquidity /
(reserve.availableLiquidity.toNumber() + borrowedLiquidity)
);
let borrowedLiquidity = wadToLamports(reserve.borrowedLiquidityWad).toNumber();
return borrowedLiquidity / (reserve.availableLiquidity.toNumber() + borrowedLiquidity);
};
export const reserveMarketCap = (reserve?: LendingReserve) => {
@ -183,29 +173,15 @@ export const reserveMarketCap = (reserve?: LendingReserve) => {
};
export const collateralExchangeRate = (reserve?: LendingReserve) => {
return (
(reserve?.collateralMintSupply.toNumber() || 1) / reserveMarketCap(reserve)
);
return (reserve?.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,13 +1,9 @@
import {
PublicKey,
SYSVAR_CLOCK_PUBKEY,
TransactionInstruction,
} from "@solana/web3.js";
import BN from "bn.js";
import * as BufferLayout from "buffer-layout";
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/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,
@ -18,10 +14,7 @@ export const withdrawInstruction = (
reserveSupply: PublicKey,
authority: 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(

47
src/models/pool.ts Normal file
View File

@ -0,0 +1,47 @@
import { PublicKey } from '@solana/web3.js';
import { TokenAccount } from './account';
export const DEFAULT_DENOMINATOR = 10_000;
export interface PoolInfo {
pubkeys: {
program: PublicKey;
account: PublicKey;
holdingAccounts: PublicKey[];
holdingMints: PublicKey[];
mint: PublicKey;
feeAccount?: PublicKey;
};
legacy: boolean;
raw: any;
}
export interface LiquidityComponent {
amount: number;
account?: TokenAccount;
mintAddress: string;
}
export enum CurveType {
ConstantProduct = 0,
ConstantPrice = 1,
Stable = 2,
ConstantProductWithOffset = 3,
}
export interface PoolConfig {
curveType: CurveType;
fees: {
tradeFeeNumerator: number;
tradeFeeDenominator: number;
ownerTradeFeeNumerator: number;
ownerTradeFeeDenominator: number;
ownerWithdrawFeeNumerator: number;
ownerWithdrawFeeDenominator: number;
hostFeeNumerator: number;
hostFeeDenominator: number;
};
token_b_offset?: number;
token_b_price?: number;
}

438
src/models/tokenSwap.ts Normal file
View File

@ -0,0 +1,438 @@
import { Numberu64 } from '@solana/spl-token-swap';
import { PublicKey, Account, TransactionInstruction } from '@solana/web3.js';
import * as BufferLayout from 'buffer-layout';
import { programIds } from '../utils/ids';
import { publicKey, uint64 } from '../utils/layout';
import { CurveType, PoolConfig } from './pool';
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'),
],
'fees'
);
export const TokenSwapLayoutLegacyV0 = BufferLayout.struct([
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'),
]);
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 createInitSwapInstruction = (
tokenSwapAccount: Account,
authority: PublicKey,
tokenAccountA: PublicKey,
tokenAccountB: PublicKey,
tokenPool: PublicKey,
feeAccount: PublicKey,
destinationAccount: PublicKey,
tokenProgramId: PublicKey,
swapProgramId: PublicKey,
nonce: number,
config: PoolConfig
): TransactionInstruction => {
const keys = [
{ pubkey: tokenSwapAccount.publicKey, isSigner: false, isWritable: true },
{ pubkey: authority, isSigner: false, isWritable: false },
{ pubkey: tokenAccountA, isSigner: false, isWritable: false },
{ pubkey: tokenAccountB, isSigner: false, isWritable: false },
{ pubkey: tokenPool, isSigner: false, isWritable: true },
{ pubkey: feeAccount, isSigner: false, isWritable: false },
{ pubkey: destinationAccount, isSigner: false, isWritable: true },
{ pubkey: tokenProgramId, isSigner: false, isWritable: false },
];
let data = Buffer.alloc(1024);
{
const isLatestLayout = programIds().swapLayout === TokenSwapLayout;
if (isLatestLayout) {
const fields = [
BufferLayout.u8('instruction'),
BufferLayout.u8('nonce'),
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.u8('curveType'),
];
if (config.curveType === CurveType.ConstantProductWithOffset) {
fields.push(BufferLayout.nu64('token_b_offset'));
fields.push(BufferLayout.blob(24, 'padding'));
} else if (config.curveType === CurveType.ConstantPrice) {
fields.push(BufferLayout.nu64('token_b_price'));
fields.push(BufferLayout.blob(24, 'padding'));
} else {
fields.push(BufferLayout.blob(32, 'padding'));
}
const commandDataLayout = BufferLayout.struct(fields);
const { fees, ...rest } = config;
const encodeLength = commandDataLayout.encode(
{
instruction: 0, // InitializeSwap instruction
nonce,
...fees,
...rest,
},
data
);
data = data.slice(0, encodeLength);
} else {
const commandDataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
BufferLayout.u8('nonce'),
BufferLayout.u8('curveType'),
BufferLayout.nu64('tradeFeeNumerator'),
BufferLayout.nu64('tradeFeeDenominator'),
BufferLayout.nu64('ownerTradeFeeNumerator'),
BufferLayout.nu64('ownerTradeFeeDenominator'),
BufferLayout.nu64('ownerWithdrawFeeNumerator'),
BufferLayout.nu64('ownerWithdrawFeeDenominator'),
BufferLayout.blob(16, 'padding'),
]);
const encodeLength = commandDataLayout.encode(
{
instruction: 0, // InitializeSwap instruction
nonce,
curveType: config.curveType,
tradeFeeNumerator: config.fees.tradeFeeNumerator,
tradeFeeDenominator: config.fees.tradeFeeDenominator,
ownerTradeFeeNumerator: config.fees.ownerTradeFeeNumerator,
ownerTradeFeeDenominator: config.fees.ownerTradeFeeDenominator,
ownerWithdrawFeeNumerator: config.fees.ownerWithdrawFeeNumerator,
ownerWithdrawFeeDenominator: config.fees.ownerWithdrawFeeDenominator,
},
data
);
data = data.slice(0, encodeLength);
}
}
return new TransactionInstruction({
keys,
programId: swapProgramId,
data,
});
};
export const depositPoolInstruction = (
tokenSwap: PublicKey,
authority: PublicKey,
sourceA: PublicKey,
sourceB: PublicKey,
intoA: PublicKey,
intoB: PublicKey,
poolToken: PublicKey,
poolAccount: PublicKey,
swapProgramId: PublicKey,
tokenProgramId: PublicKey,
poolTokenAmount: number | Numberu64,
maximumTokenA: number | Numberu64,
maximumTokenB: number | Numberu64
): TransactionInstruction => {
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
uint64('poolTokenAmount'),
uint64('maximumTokenA'),
uint64('maximumTokenB'),
]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: 2, // Deposit instruction
poolTokenAmount: new Numberu64(poolTokenAmount).toBuffer(),
maximumTokenA: new Numberu64(maximumTokenA).toBuffer(),
maximumTokenB: new Numberu64(maximumTokenB).toBuffer(),
},
data
);
const keys = [
{ pubkey: tokenSwap, isSigner: false, isWritable: false },
{ pubkey: authority, isSigner: false, isWritable: false },
{ pubkey: sourceA, isSigner: false, isWritable: true },
{ pubkey: sourceB, isSigner: false, isWritable: true },
{ pubkey: intoA, isSigner: false, isWritable: true },
{ pubkey: intoB, isSigner: false, isWritable: true },
{ pubkey: poolToken, isSigner: false, isWritable: true },
{ pubkey: poolAccount, isSigner: false, isWritable: true },
{ pubkey: tokenProgramId, isSigner: false, isWritable: false },
];
return new TransactionInstruction({
keys,
programId: swapProgramId,
data,
});
};
export const depositExactOneInstruction = (
tokenSwap: PublicKey,
authority: PublicKey,
source: PublicKey,
intoA: PublicKey,
intoB: PublicKey,
poolToken: PublicKey,
poolAccount: PublicKey,
swapProgramId: PublicKey,
tokenProgramId: PublicKey,
sourceTokenAmount: number | Numberu64,
minimumPoolTokenAmount: number | Numberu64
): TransactionInstruction => {
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
uint64('sourceTokenAmount'),
uint64('minimumPoolTokenAmount'),
]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: 4, // DepositExactOne instruction
sourceTokenAmount: new Numberu64(sourceTokenAmount).toBuffer(),
minimumPoolTokenAmount: new Numberu64(minimumPoolTokenAmount).toBuffer(),
},
data
);
const keys = [
{ pubkey: tokenSwap, isSigner: false, isWritable: false },
{ pubkey: authority, isSigner: false, isWritable: false },
{ pubkey: source, isSigner: false, isWritable: true },
{ pubkey: intoA, isSigner: false, isWritable: true },
{ pubkey: intoB, isSigner: false, isWritable: true },
{ pubkey: poolToken, isSigner: false, isWritable: true },
{ pubkey: poolAccount, isSigner: false, isWritable: true },
{ pubkey: tokenProgramId, isSigner: false, isWritable: false },
];
return new TransactionInstruction({
keys,
programId: swapProgramId,
data,
});
};
export const withdrawPoolInstruction = (
tokenSwap: PublicKey,
authority: PublicKey,
poolMint: PublicKey,
feeAccount: PublicKey | undefined,
sourcePoolAccount: PublicKey,
fromA: PublicKey,
fromB: PublicKey,
userAccountA: PublicKey,
userAccountB: PublicKey,
swapProgramId: PublicKey,
tokenProgramId: PublicKey,
poolTokenAmount: number | Numberu64,
minimumTokenA: number | Numberu64,
minimumTokenB: number | Numberu64
): TransactionInstruction => {
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
uint64('poolTokenAmount'),
uint64('minimumTokenA'),
uint64('minimumTokenB'),
]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: 3, // Withdraw instruction
poolTokenAmount: new Numberu64(poolTokenAmount).toBuffer(),
minimumTokenA: new Numberu64(minimumTokenA).toBuffer(),
minimumTokenB: new Numberu64(minimumTokenB).toBuffer(),
},
data
);
const keys = [
{ pubkey: tokenSwap, isSigner: false, isWritable: false },
{ pubkey: authority, isSigner: false, isWritable: false },
{ pubkey: poolMint, isSigner: false, isWritable: true },
{ pubkey: sourcePoolAccount, isSigner: false, isWritable: true },
{ pubkey: fromA, isSigner: false, isWritable: true },
{ pubkey: fromB, isSigner: false, isWritable: true },
{ pubkey: userAccountA, isSigner: false, isWritable: true },
{ pubkey: userAccountB, isSigner: false, isWritable: true },
];
if (feeAccount) {
keys.push({ pubkey: feeAccount, isSigner: false, isWritable: true });
}
keys.push({ pubkey: tokenProgramId, isSigner: false, isWritable: false });
return new TransactionInstruction({
keys,
programId: swapProgramId,
data,
});
};
export const withdrawExactOneInstruction = (
tokenSwap: PublicKey,
authority: PublicKey,
poolMint: PublicKey,
sourcePoolAccount: PublicKey,
fromA: PublicKey,
fromB: PublicKey,
userAccount: PublicKey,
feeAccount: PublicKey | undefined,
swapProgramId: PublicKey,
tokenProgramId: PublicKey,
sourceTokenAmount: number | Numberu64,
maximumTokenAmount: number | Numberu64
): TransactionInstruction => {
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
uint64('sourceTokenAmount'),
uint64('maximumTokenAmount'),
]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: 5, // WithdrawExactOne instruction
sourceTokenAmount: new Numberu64(sourceTokenAmount).toBuffer(),
maximumTokenAmount: new Numberu64(maximumTokenAmount).toBuffer(),
},
data
);
const keys = [
{ pubkey: tokenSwap, isSigner: false, isWritable: false },
{ pubkey: authority, isSigner: false, isWritable: false },
{ pubkey: poolMint, isSigner: false, isWritable: true },
{ pubkey: sourcePoolAccount, isSigner: false, isWritable: true },
{ pubkey: fromA, isSigner: false, isWritable: true },
{ pubkey: fromB, isSigner: false, isWritable: true },
{ pubkey: userAccount, isSigner: false, isWritable: true },
];
if (feeAccount) {
keys.push({ pubkey: feeAccount, isSigner: false, isWritable: true });
}
keys.push({ pubkey: tokenProgramId, isSigner: false, isWritable: false });
return new TransactionInstruction({
keys,
programId: swapProgramId,
data,
});
};
export const swapInstruction = (
tokenSwap: PublicKey,
authority: PublicKey,
userSource: PublicKey,
poolSource: PublicKey,
poolDestination: PublicKey,
userDestination: PublicKey,
poolMint: PublicKey,
feeAccount: PublicKey,
swapProgramId: PublicKey,
tokenProgramId: PublicKey,
amountIn: number | Numberu64,
minimumAmountOut: number | Numberu64,
programOwner?: PublicKey
): TransactionInstruction => {
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
uint64('amountIn'),
uint64('minimumAmountOut'),
]);
const keys = [
{ pubkey: tokenSwap, isSigner: false, isWritable: false },
{ pubkey: authority, isSigner: false, isWritable: false },
{ pubkey: userSource, isSigner: false, isWritable: true },
{ pubkey: poolSource, isSigner: false, isWritable: true },
{ pubkey: poolDestination, isSigner: false, isWritable: true },
{ pubkey: userDestination, isSigner: false, isWritable: true },
{ pubkey: poolMint, isSigner: false, isWritable: true },
{ pubkey: feeAccount, isSigner: false, isWritable: true },
{ pubkey: tokenProgramId, isSigner: false, isWritable: false },
];
// optional depending on the build of token-swap program
if (programOwner) {
keys.push({ pubkey: programOwner, isSigner: false, isWritable: true });
}
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: 1, // Swap instruction
amountIn: new Numberu64(amountIn).toBuffer(),
minimumAmountOut: new Numberu64(minimumAmountOut).toBuffer(),
},
data
);
return new TransactionInstruction({
keys,
programId: swapProgramId,
data,
});
};

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,
@ -20,12 +20,14 @@ import {
WithdrawView,
LiquidateView,
LiquidateReserveView,
} from "./views";
MarginTrading,
} from './views';
import { NewPosition } from './views/marginTrading/newPosition';
export function Routes() {
return (
<>
<HashRouter basename={"/"}>
<HashRouter basename={'/'}>
<ConnectionProvider>
<WalletProvider>
<AccountsProvider>
@ -33,46 +35,22 @@ 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="/faucet" children={<FaucetView />} />
<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='/marginTrading' children={<MarginTrading />} />
<Route path='/marginTrading/: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;
@ -12,8 +12,16 @@ export class CacheUpdateEvent {
}
}
export class CacheDeleteEvent {
static type = 'CacheUpdate';
id: string;
constructor(id: string) {
this.id = id;
}
}
export class MarketUpdateEvent {
static type = "MarketUpdate";
static type = 'MarketUpdate';
ids: Set<string>;
constructor(ids: Set<string>) {
this.ids = ids;
@ -42,4 +50,8 @@ export class EventEmitter {
raiseCacheUpdated(id: string, isNew: boolean, parser: any) {
this.emitter.emit(CacheUpdateEvent.type, new CacheUpdateEvent(id, isNew, parser));
}
raiseCacheDeleted(id: string) {
this.emitter.emit(CacheDeleteEvent.type, new CacheDeleteEvent(id));
}
}

96
src/utils/ids.ts Normal file
View File

@ -0,0 +1,96 @@
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 let LENDING_PROGRAM_ID = new PublicKey('TokenLend1ng1111111111111111111111111111111');
let SWAP_PROGRAM_ID: PublicKey;
let SWAP_PROGRAM_LEGACY_IDS: PublicKey[];
let SWAP_PROGRAM_LAYOUT: any;
export const SWAP_PROGRAM_OWNER_FEE_ADDRESS = new PublicKey('HfoTxFR1Tm6kGmWgYWD6J7YHVy1UwqSULUGVLXkJqaKN');
export const SWAP_HOST_FEE_ADDRESS = process.env.REACT_APP_SWAP_HOST_FEE_ADDRESS
? new PublicKey(`${process.env.REACT_APP_SWAP_HOST_FEE_ADDRESS}`)
: SWAP_PROGRAM_OWNER_FEE_ADDRESS;
export const ENABLE_FEES_INPUT = false;
console.debug(`Host address: ${SWAP_HOST_FEE_ADDRESS?.toBase58()}`);
console.debug(`Owner address: ${SWAP_PROGRAM_OWNER_FEE_ADDRESS?.toBase58()}`);
// legacy pools are used to show users contributions in those pools to allow for withdrawals of funds
export const PROGRAM_IDS = [
{
name: 'mainnet-beta',
swap: () => ({
current: {
pubkey: new PublicKey('9qvG1zUp8xF1Bi4m6UdRNby1BAAuaDrUxSpv4CmRRMjL'),
layout: TokenSwapLayoutV1,
},
legacy: [
// TODO: uncomment to enable legacy contract
// new PublicKey("9qvG1zUp8xF1Bi4m6UdRNby1BAAuaDrUxSpv4CmRRMjL"),
],
}),
},
{
name: 'testnet',
swap: () => ({
current: {
pubkey: new PublicKey('2n2dsFSgmPcZ8jkmBZLGUM2nzuFqcBGQ3JEEj6RJJcEg'),
layout: TokenSwapLayoutV1,
},
legacy: [],
}),
},
{
name: 'devnet',
swap: () => ({
current: {
pubkey: new PublicKey('6Cust2JhvweKLh4CVo1dt21s2PJ86uNGkziudpkNPaCj'),
layout: TokenSwapLayout,
},
legacy: [new PublicKey('BSfTAcBdqmvX5iE2PW88WFNNp2DHhLUaBKk5WrnxVkcJ')],
}),
},
{
name: 'localnet',
swap: () => ({
current: {
pubkey: new PublicKey('369YmCWHGxznT7GGBhcLZDRcRoGWmGKFWdmtiPy78yj7'),
layout: TokenSwapLayoutV1,
},
legacy: [],
}),
},
];
export const setProgramIds = (envName: string) => {
let instance = PROGRAM_IDS.find((env) => env.name === envName);
if (!instance) {
return;
}
let swap = instance.swap();
SWAP_PROGRAM_ID = swap.current.pubkey;
SWAP_PROGRAM_LAYOUT = swap.current.layout;
SWAP_PROGRAM_LEGACY_IDS = swap.legacy;
if (envName === 'mainnet-beta') {
LENDING_PROGRAM_ID = new PublicKey('2KfJP7pZ6QSpXa26RmsN6kKVQteDEdQmizLSvuyryeiW');
}
};
export const programIds = () => {
return {
token: TOKEN_PROGRAM_ID,
swap: SWAP_PROGRAM_ID,
swapLayout: SWAP_PROGRAM_LAYOUT,
swap_legacy: SWAP_PROGRAM_LEGACY_IDS,
lending: LENDING_PROGRAM_ID,
};
};

1181
src/utils/pools.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
import { useCallback, useState } from "react";
import { MintInfo } from "@solana/spl-token";
import { useCallback, useState } from 'react';
import { MintInfo } from '@solana/spl-token';
import { TokenAccount } from "./../models";
import { PublicKey } from "@solana/web3.js";
import BN from "bn.js";
import { WAD, ZERO } from "../constants";
import { PoolInfo, TokenAccount } from './../models';
import { PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
import { WAD, ZERO } from '../constants';
export interface KnownToken {
tokenSymbol: string;
@ -15,6 +15,12 @@ export interface KnownToken {
export type KnownTokenMap = Map<string, KnownToken>;
export const formatPriceNumber = new Intl.NumberFormat('en-US', {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 8,
});
export function useLocalStorageState(key: string, defaultState?: string) {
const [state, setState] = useState(() => {
// NOTE: Not sure if this is ok
@ -49,15 +55,11 @@ export function shortenAddress(address: string, chars = 4): string {
return `${address.slice(0, chars)}...${address.slice(-chars)}`;
}
export function getTokenName(
map: KnownTokenMap,
mint?: string | PublicKey,
shorten = true
): string {
export function getTokenName(map: KnownTokenMap, mint?: string | PublicKey, shorten = true): string {
const mintAddress = typeof mint === 'string' ? mint : mint?.toBase58();
if (!mintAddress) {
return "N/A";
return 'N/A';
}
const knownSymbol = map.get(mintAddress)?.tokenSymbol;
@ -68,12 +70,8 @@ export function getTokenName(
return shorten ? `${mintAddress.substring(0, 5)}...` : mintAddress;
}
export function getTokenIcon(
map: KnownTokenMap,
mintAddress?: string | PublicKey
): string | undefined {
const address =
typeof mintAddress === "string" ? mintAddress : mintAddress?.toBase58();
export function getTokenIcon(map: KnownTokenMap, mintAddress?: string | PublicKey): string | undefined {
const address = typeof mintAddress === 'string' ? mintAddress : mintAddress?.toBase58();
if (!address) {
return;
}
@ -85,25 +83,20 @@ export function isKnownMint(map: KnownTokenMap, mintAddress: string) {
return !!map.get(mintAddress);
}
export const STABLE_COINS = new Set(["USDC", "wUSDC", "USDT"]);
export const STABLE_COINS = new Set(['USDC', 'wUSDC', 'USDT']);
export function chunks<T>(array: T[], size: number): T[][] {
return Array.apply<number, T[], T[][]>(
0,
new Array(Math.ceil(array.length / size))
).map((_, index) => array.slice(index * size, (index + 1) * size));
return Array.apply<number, T[], T[][]>(0, new Array(Math.ceil(array.length / size))).map((_, index) =>
array.slice(index * size, (index + 1) * size)
);
}
export function toLamports(
account?: TokenAccount | number,
mint?: MintInfo
): number {
export function toLamports(account?: TokenAccount | number, mint?: MintInfo): number {
if (!account) {
return 0;
}
const amount =
typeof account === "number" ? account : account.info.amount?.toNumber();
const amount = typeof account === 'number' ? account : account.info.amount?.toNumber();
const precision = Math.pow(10, mint?.decimals || 0);
return Math.floor(amount * precision);
@ -113,28 +106,20 @@ export function wadToLamports(amount?: BN): BN {
return amount?.div(WAD) || ZERO;
}
export function fromLamports(
account?: TokenAccount | number | BN,
mint?: MintInfo,
rate: number = 1.0
): number {
export function fromLamports(account?: TokenAccount | number | BN, mint?: MintInfo, rate: number = 1.0): number {
if (!account) {
return 0;
}
const amount = Math.floor(
typeof account === "number"
? account
: BN.isBN(account)
? account.toNumber()
: account.info.amount.toNumber()
typeof account === 'number' ? account : BN.isBN(account) ? account.toNumber() : account.info.amount.toNumber()
);
const precision = Math.pow(10, mint?.decimals || 0);
return (amount / precision) * rate;
}
var SI_SYMBOL = ["", "k", "M", "G", "T", "P", "E"];
var SI_SYMBOL = ['', 'k', 'M', 'G', 'T', 'P', 'E'];
const abbreviateNumber = (number: number, precision: number) => {
let tier = (Math.log10(number) / 3) | 0;
@ -155,13 +140,13 @@ export function formatTokenAmount(
account?: TokenAccount,
mint?: MintInfo,
rate: number = 1.0,
prefix = "",
suffix = "",
prefix = '',
suffix = '',
precision = 6,
abbr = false
): string {
if (!account) {
return "";
return '';
}
return `${[prefix]}${formatAmount(
@ -171,13 +156,13 @@ export function formatTokenAmount(
)}${suffix}`;
}
export const formatUSD = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
export const formatUSD = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
});
const numberFormater = new Intl.NumberFormat("en-US", {
style: "decimal",
const numberFormater = new Intl.NumberFormat('en-US', {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
@ -185,15 +170,33 @@ const numberFormater = new Intl.NumberFormat("en-US", {
export const formatNumber = {
format: (val?: number) => {
if (!val) {
return "--";
return '--';
}
return numberFormater.format(val);
},
};
export const formatPct = new Intl.NumberFormat("en-US", {
style: "percent",
export const formatPct = new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
export function convert(account?: TokenAccount | number, mint?: MintInfo, rate: number = 1.0): number {
if (!account) {
return 0;
}
const amount = typeof account === 'number' ? account : account.info.amount?.toNumber();
const precision = Math.pow(10, mint?.decimals || 0);
let result = (amount / precision) * rate;
return result;
}
export function getPoolName(map: KnownTokenMap, pool: PoolInfo, shorten = true) {
const sorted = pool.pubkeys.holdingMints.map((a) => a.toBase58()).sort();
return sorted.map((item) => getTokenName(map, item, shorten)).join('/');
}

View File

@ -1,42 +1,41 @@
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;
const { borrowingPower, totalInQuote } = useBorrowingPower(props.address)
const { borrowingPower, totalInQuote } = useBorrowingPower(props.address);
const apr = calculateBorrowAPY(props.reserve);
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>
<div>${formatNumber.format(price)}</div>
<div>
<div>
<div><em>{formatNumber.format(borrowingPower)}</em> {name}</div>
<div className="dashboard-amount-quote">${formatNumber.format(totalInQuote)}</div>
<div>
<em>{formatNumber.format(borrowingPower)}</em> {name}
</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

@ -10,3 +10,4 @@ export { FaucetView } from "./faucet";
export { RepayReserveView } from "./repayReserve";
export { LiquidateView } from "./liquidate";
export { LiquidateReserveView } from "./liquidateReserve";
export { MarginTrading } from "./marginTrading";

View File

@ -0,0 +1,26 @@
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'>
<Card>
<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>
<div>{LABELS.TABLE_TITLE_APY}</div>
<div></div>
</div>
{reserveAccounts.map((account) => (
<MarginTradeItem reserve={account.info} address={account.pubkey} />
))}
</Card>
</div>
);
};

View File

@ -0,0 +1,43 @@
import React from 'react';
import { 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 }) => {
const name = useTokenName(props.reserve.liquidityMint);
const price = useMidPriceInUSD(props.reserve.liquidityMint.toBase58()).price;
const apr = calculateBorrowAPY(props.reserve);
return (
<Link to={`/marginTrading/${props.address.toBase58()}`}>
<div className='choose-margin-item'>
<span style={{ display: 'flex' }}>
<TokenIcon mintAddress={props.reserve.liquidityMint} />
{name}
</span>
<div>${formatNumber.format(price)}</div>
<div>
<div>
<div>
<em>{formatNumber.format(200)}</em> {name}
</div>
<div className='dashboard-amount-quote'>${formatNumber.format(300)}</div>
</div>
</div>
<div>{formatPct.format(apr)}</div>
<div>
<Button type='primary'>
<span>{LABELS.MARGIN_TRADE_ACTION}</span>
</Button>
</div>
</div>
</Link>
);
};

View File

@ -0,0 +1,32 @@
@import '~antd/es/style/themes/default.less';
.choose-margin-item {
display: flex;
justify-content: space-between;
align-items: center;
color: @text-color;
& > :nth-child(n) {
flex: 20%;
text-align: right;
margin: 10px 0px;
}
& > :first-child {
flex: 80px;
}
border-bottom: 1px solid #eee;
}
.choose-margin-header {
& > div {
flex: 20%;
text-align: right;
}
& > :first-child {
text-align: left;
flex: 80px;
}
}

View File

@ -0,0 +1,115 @@
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;
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 [myGain, setMyGain] = useState<number>(10);
const profitPart = (myPart + brokeragePart) * (myGain / 100);
let progressBar = null;
if (profitPart > 0) {
// normalize...
const total = profitPart + myPart + brokeragePart;
progressBar = (
<Progress
percent={(myPart / total) * 100 + (brokeragePart / total) * 100}
success={{ percent: (brokeragePart / total) * 100, strokeColor: brokerageColor }}
strokeColor={myColor}
trailColor={gains}
showInfo={false}
/>
);
} else {
// now, we're eating away your profit...
myPart += profitPart; // profit is negative
const total = myPart + brokeragePart;
if (myPart < 0) {
progressBar = <p>Your position has been liquidated at this price swing.</p>;
} else
progressBar = (
<Progress
showInfo={false}
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' }}>
<Card>
<Statistic
title='Leverage'
value={brokeragePart * exchangeRate}
precision={2}
valueStyle={{ color: brokerageColor }}
suffix={token?.tokenSymbol}
/>
</Card>
<Card>
<Statistic
title='My Collateral Value'
value={myPart}
precision={2}
valueStyle={{ color: myColor }}
suffix={collateralToken?.tokenSymbol}
/>
</Card>
<Card>
<Statistic
title='Profit/Loss'
value={profitPart * exchangeRate}
precision={2}
valueStyle={{ color: profitPart > 0 ? gains : losses }}
suffix={token?.tokenSymbol}
prefix={profitPart > 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
/>
</Card>
</div>
<br />
{progressBar}
</Card>
<Card className='new-position-item new-position-item-bottom-right'>
<GainsChart item={item} priceChange={myGain} />
<Slider
tooltipVisible={true}
defaultValue={10}
tipFormatter={(p) => <span>{p}%</span>}
max={100}
min={-100}
tooltipPlacement={'top'}
onChange={(v: number) => {
setMyGain(v);
}}
style={{ marginBottom: '20px' }}
/>
<span style={{ float: 'right', fontSize: '9px' }}>
<a
href='https://github.com/bZxNetwork/fulcrum_ui/blob/development/packages/fulcrum-website/assets/js/trading.js'
target='blank'
>
credit
</a>
</span>
</Card>
</div>
);
}

View File

@ -0,0 +1,260 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Line } from 'react-chartjs-2';
import { Position } from './interfaces';
import { debounce } from 'lodash';
// Special thanks to
// https://github.com/bZxNetwork/fulcrum_ui/blob/development/packages/fulcrum-website/assets/js/trading.js
// For the basis of this code - I copied it directly from there and then modified it for our needs.
// You guys are real heroes - that is beautifully done.
const baseData = [
{ x: 0, y: 65 },
{ x: 1, y: 80 },
{ x: 2, y: 60 },
{ x: 3, y: 30 },
{ x: 4, y: 20 },
{ x: 5, y: 35 },
{ x: 6, y: 25 },
{ x: 7, y: 40 },
{ x: 8, y: 36 },
{ x: 9, y: 34 },
{ x: 10, y: 50 },
{ x: 11, y: 33 },
{ x: 12, y: 37 },
{ x: 13, y: 45 },
{ x: 14, y: 35 },
{ x: 15, y: 37 },
{ x: 16, y: 50 },
{ x: 17, y: 43 },
{ x: 18, y: 50 },
{ x: 19, y: 45 },
{ x: 20, y: 55 },
{ x: 21, y: 50 },
{ x: 22, y: 45 },
{ x: 23, y: 50 },
{ x: 24, y: 45 },
{ x: 25, y: 40 },
{ x: 26, y: 35 },
{ x: 27, y: 40 },
{ x: 28, y: 37 },
{ x: 29, y: 45 },
{ x: 30, y: 50 },
{ x: 31, y: 60 },
{ x: 32, y: 55 },
{ x: 33, y: 50 },
{ x: 34, y: 53 },
{ x: 35, y: 55 },
{ x: 36, y: 50 },
{ x: 37, y: 45 },
{ x: 38, y: 40 },
{ x: 39, y: 45 },
{ x: 40, y: 50 },
{ x: 41, y: 55 },
{ x: 42, y: 65 },
{ x: 43, y: 62 },
{ x: 44, y: 54 },
{ x: 45, y: 65 },
{ x: 46, y: 48 },
{ x: 47, y: 55 },
{ x: 48, y: 60 },
{ x: 49, y: 63 },
{ x: 50, y: 65 },
];
function getChartData({ item, priceChange }: { item: Position; priceChange: number }) {
//the only way to create an immutable copy of array with objects inside.
const baseDashed = JSON.parse(JSON.stringify(baseData.slice(Math.floor(baseData.length) / 2)));
const baseSolid = JSON.parse(JSON.stringify(baseData.slice(0, Math.floor(baseData.length) / 2 + 1)));
const leverage = item.leverage;
baseDashed.forEach((item: { y: number; x: number }, index: number) => {
if (index !== 0) item.y += (item.y * priceChange) / 100;
});
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) };
});
return {
datasets: [
{
backgroundColor: 'transparent',
borderColor: 'rgb(39, 107, 251)',
borderWidth: 4,
radius: 0,
data: baseSolid,
},
{
backgroundColor: 'transparent',
borderColor: priceChange >= 0 ? 'rgb(51, 223, 204)' : 'rgb(255,79,79)',
borderWidth: 4,
radius: 0,
data: leverageData,
borderDash: [15, 3],
label: 'LEVERAGE',
},
{
backgroundColor: 'transparent',
borderColor: 'rgb(86, 169, 255)',
borderWidth: 2,
radius: 0,
data: baseDashed,
borderDash: [8, 4],
label: 'HOLD',
},
],
};
}
function updateChartData({
item,
priceChange,
chartRef,
}: {
item: Position;
priceChange: number;
chartRef: React.RefObject<any>;
}) {
const data = getChartData({ item, priceChange });
chartRef.current.chartInstance.data = data;
chartRef.current.chartInstance.canvas.parentNode.style.width = '100%';
chartRef.current.chartInstance.canvas.parentNode.style.height = 'auto';
chartRef.current.chartInstance.update();
}
function drawLabels(t: any, ctx: any, leverage: number, priceChange: number) {
ctx.save();
ctx.font = 'normal normal bold 15px /1.5 Muli';
ctx.textBaseline = 'bottom';
const chartInstance = t.chart;
const datasets = chartInstance.config.data.datasets;
datasets.forEach(function (ds: { label: any; borderColor: any }, index: number) {
const label = ds.label;
ctx.fillStyle = ds.borderColor;
const meta = chartInstance.controller.getDatasetMeta(index);
const len = meta.data.length - 1;
const pointPostition = Math.floor(len / 2) - Math.floor(0.2 * len);
const x = meta.data[pointPostition]._model.x;
const xOffset = x;
const y = meta.data[pointPostition]._model.y;
let yOffset;
if (label === 'HOLD') {
yOffset = leverage * priceChange > 0 ? y * 1.2 : y * 0.8;
} else {
yOffset = leverage * priceChange > 0 ? y * 0.8 : y * 1.2;
}
if (yOffset > chartInstance.canvas.parentNode.offsetHeight) {
// yOffset = 295;
chartInstance.canvas.parentNode.style.height = `${yOffset * 1.3}px`;
}
if (yOffset < 0) yOffset = 5;
if (label) ctx.fillText(label, xOffset, yOffset);
});
ctx.restore();
}
const debouncedUpdateChartData = debounce(updateChartData, 200);
export default function GainsChart({ item, priceChange }: { item: Position; priceChange: number }) {
const chartRef = useRef<any>();
const [booted, setBooted] = useState<boolean>(false);
const [canvas, setCanvas] = useState<any>();
useEffect(() => {
if (chartRef.current.chartInstance) debouncedUpdateChartData({ item, priceChange, chartRef });
}, [priceChange, item.leverage]);
useEffect(() => {
if (chartRef.current && !booted && canvas) {
//@ts-ignore
const originalController = window.Chart.controllers.line;
//@ts-ignore
window.Chart.controllers.line = Chart.controllers.line.extend({
draw: function () {
originalController.prototype.draw.call(this, arguments);
drawLabels(this, canvas.getContext('2d'), item.leverage, priceChange);
},
});
setBooted(true);
}
}, [chartRef, canvas]);
const chart = useMemo(
() => (
<Line
ref={chartRef}
data={(canvas: any) => {
setCanvas(canvas);
return getChartData({ item, priceChange });
}}
options={{
responsive: true,
maintainAspectRatio: true,
scaleShowLabels: false,
layout: {
padding: {
top: 30,
bottom: 80,
},
},
labels: {
render: 'title',
fontColor: ['green', 'white', 'red'],
precision: 2,
},
animation: {
easing: 'easeOutExpo',
duration: 500,
},
scales: {
xAxes: [
{
display: false,
gridLines: {
display: false,
},
type: 'linear',
position: 'bottom',
},
],
yAxes: [
{
display: false,
gridLines: {
display: false,
},
},
],
},
legend: {
display: false,
},
}}
/>
),
[]
);
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'stretch', justifyContent: 'center' }}>
{chart}
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<span>past</span>
<span>today</span>
<span>future</span>
</div>
</div>
);
}

View File

@ -0,0 +1,163 @@
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>;
newPosition: Position;
setNewPosition: (pos: Position) => void;
}
export const generateActionLabel = (connected: boolean, newPosition: Position) => {
return !connected ? LABELS.CONNECT_LABEL : newPosition.error ? newPosition.error : LABELS.TRADING_ADD_POSITION;
};
function onUserChangesLeverageOrCollateralValue({
newPosition,
setNewPosition,
collateralDeposit,
enrichedPools,
}: {
newPosition: Position;
setNewPosition: (pos: Position) => void;
enrichedPools: any[];
collateralDeposit: UserDeposit | undefined;
}) {
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 } });
}
}
function onUserChangesAssetValue({
newPosition,
setNewPosition,
collateralDeposit,
enrichedPools,
}: {
newPosition: Position;
setNewPosition: (pos: Position) => void;
enrichedPools: any[];
collateralDeposit: UserDeposit | undefined;
}) {
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 } });
}
}
export default function NewPositionForm({ lendingReserve, newPosition, setNewPosition }: NewPositionFormProps) {
const bodyStyle: React.CSSProperties = {
display: 'flex',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
height: '100%',
};
const [showConfirmation, setShowConfirmation] = useState(false);
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}>
{showConfirmation ? (
<ActionConfirmation onClose={() => setShowConfirmation(false)} />
) : (
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-around',
}}
>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-evenly', alignItems: 'center' }}>
<CollateralInput
title='Collateral'
reserve={lendingReserve.info}
amount={newPosition.collateral.value}
onInputChange={(val: number | null) => {
const newPos = { ...newPosition, collateral: { ...newPosition.collateral, value: val } };
onUserChangesLeverageOrCollateralValue({
newPosition: newPos,
setNewPosition,
enrichedPools,
collateralDeposit,
});
}}
onCollateralReserve={(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 } };
onUserChangesLeverageOrCollateralValue({
newPosition: newPos,
setNewPosition,
enrichedPools,
collateralDeposit,
});
}}
showLeverageSelector={true}
onLeverage={(leverage: number) => {
const newPos = { ...newPosition, leverage };
onUserChangesLeverageOrCollateralValue({
newPosition: newPos,
setNewPosition,
enrichedPools,
collateralDeposit,
});
}}
leverage={newPosition.leverage}
/>
</div>
<ArrowDownOutlined />
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'stretch' }}>
{newPosition.asset.type && (
<CollateralInput
title='Choose trade'
reserve={newPosition.asset.type.info}
amount={newPosition.asset.value}
onInputChange={(val: number | null) => {
const newPos = { ...newPosition, asset: { ...newPosition.asset, value: val } };
onUserChangesAssetValue({
newPosition: newPos,
setNewPosition,
enrichedPools,
collateralDeposit,
});
}}
disabled
hideBalance={true}
/>
)}
<Button
className='trade-button'
type='primary'
size='large'
onClick={connected ? null : wallet.connect}
style={{ width: '100%' }}
disabled={connected && !!newPosition.error}
>
<span>{generateActionLabel(connected, newPosition)}</span>
</Button>
</div>
</div>
)}
</Card>
);
}

View File

@ -0,0 +1,21 @@
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'>
{!pool && <span>Choose a CCY to see exchange rate information.</span>}
{pool && (
<>
<PoolPrice pool={pool} />
<SupplyOverview pool={pool} />
</>
)}
</Card>
);
}

View File

@ -0,0 +1,40 @@
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';
export const NewPosition = () => {
const { id } = useParams<{ id: string }>();
const lendingReserve = useLendingReserve(id);
const [newPosition, setNewPosition] = useState<Position>({
id: null,
leverage: 1,
collateral: {},
asset: {},
});
if (!lendingReserve) {
return null;
}
if (!newPosition.asset.type) {
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} />
<PoolHealth newPosition={newPosition} />
</div>
<Breakdown item={newPosition} />
</div>
</div>
);
};

View File

@ -0,0 +1,22 @@
import { ParsedAccount } from '../../../contexts/accounts';
import { LendingReserve } from '../../../models/lending/reserve';
export interface Token {
mintAddress: string;
tokenName: string;
tokenSymbol: string;
}
export interface Position {
id?: number | null;
leverage: number;
collateral: {
type?: ParsedAccount<LendingReserve>;
value?: number | null;
};
asset: {
type?: ParsedAccount<LendingReserve>;
value?: number | null;
};
error?: string;
}

View File

@ -0,0 +1,76 @@
import { useEffect } from 'react';
import { LABELS } from '../../../constants';
import { Position } from './interfaces';
import { usePoolAndTradeInfoFrom } from './utils';
export function useLeverage({
newPosition,
setNewPosition,
}: {
newPosition: Position;
setNewPosition: (pos: Position) => void;
}) {
const {
enrichedPools,
collateralDeposit,
collType,
desiredType,
collValue,
desiredValue,
leverage,
} = usePoolAndTradeInfoFrom(newPosition);
// Leverage validation - if you choose this leverage, is it allowable, with your buying power and with
// the pool we have to cover you?
useEffect(() => {
if (!collType) {
setNewPosition({ ...newPosition, error: LABELS.NO_COLL_TYPE_MESSAGE });
return;
}
if (!collateralDeposit) {
setNewPosition({ ...newPosition, error: LABELS.NO_DEPOSIT_MESSAGE });
return;
}
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 leverageDesired = newPosition.leverage;
const amountAvailableInOysterForMargin = collateralDeposit.info.amount * exchangeRate;
const amountToDepositOnMargin = desiredValue / leverageDesired;
if (amountToDepositOnMargin > amountAvailableInOysterForMargin) {
setNewPosition({ ...newPosition, error: LABELS.NOT_ENOUGH_MARGIN_MESSAGE });
return;
}
if (amountToDepositOnMargin > collValue) {
setNewPosition({ ...newPosition, error: LABELS.SET_MORE_MARGIN_MESSAGE });
return;
}
const liqA = enrichedPools[0].liquidityA;
const liqB = enrichedPools[0].liquidityB;
const supplyRatio = liqA / liqB;
// change in liquidity is amount desired (in units of B) converted to collateral units(A)
const chgLiqA = desiredValue / exchangeRate;
const newLiqA = liqA - chgLiqA;
const newLiqB = liqB + desiredValue;
const newSupplyRatio = newLiqA / newLiqB;
const priceImpact = Math.abs(100 - 100 * (newSupplyRatio / supplyRatio));
const marginToLeverage = 100 / leverageDesired; // Would be 20% for 5x
if (marginToLeverage < priceImpact && leverageDesired != 1) {
// Obviously we allow 1x as edge case
// if their marginToLeverage ratio < priceImpact, we say hey ho no go
setNewPosition({ ...newPosition, error: LABELS.LEVERAGE_LIMIT_MESSAGE });
return;
}
setNewPosition({ ...newPosition, error: '' });
}, [collType, desiredType, desiredValue, leverage, enrichedPools, collValue, collateralDeposit?.info.amount]);
}

View File

@ -0,0 +1,46 @@
.new-position {
display: flex;
flex-direction: column;
flex: 1;
}
.new-position-item {
margin: 4px;
}
.new-position-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
flex: 1;
}
.new-position-item-left {
flex: 1;
flex-direction: column;
}
.new-position-item-top-left {
flex: 1;
}
.new-position-item-bottom-left {
flex: 1;
}
.new-position-item-top-right {
flex: 1;
}
.new-position-item-bottom-right {
flex: 1;
}
.new-position-item-right {
flex: 1;
flex-direction: column;
}
/* Responsive layout - makes a one column layout instead of a two-column layout */
@media (max-width: 600px) {
.new-position-item-right,
.new-position-item-left {
flex: 100%;
}
}

View File

@ -0,0 +1,48 @@
import { useEffect } from 'react';
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
): {
enrichedPools: any[];
collateralDeposit: UserDeposit | undefined;
collType: ParsedAccount<LendingReserve> | undefined;
desiredType: ParsedAccount<LendingReserve> | undefined;
collValue: number;
desiredValue: number;
leverage: number;
pool: PoolInfo | undefined;
} {
const collType = newPosition.collateral.type;
const desiredType = newPosition.asset.type;
const collValue = newPosition.collateral.value || 0;
const desiredValue = newPosition.asset.value || 0;
const pool = usePoolForBasket([
collType?.info?.liquidityMint?.toBase58(),
desiredType?.info?.liquidityMint?.toBase58(),
]);
const userDeposits = useUserDeposits();
const collateralDeposit = userDeposits.userDeposits.find(
(u) => u.reserve.info.liquidityMint.toBase58() == collType?.info?.liquidityMint?.toBase58()
);
const enrichedPools = useEnrichedPools(pool ? [pool] : []);
return {
enrichedPools,
collateralDeposit,
collType,
desiredType,
collValue,
desiredValue,
leverage: newPosition.leverage,
pool,
};
}

View File

@ -0,0 +1,46 @@
@import '~antd/es/style/themes/default.less';
.trading-item {
display: flex;
justify-content: space-between;
align-items: center;
color: @text-color;
& > :nth-child(n) {
flex: 20%;
text-align: left;
margin: 10px 0px;
}
& > :first-child {
flex: 300px;
}
& > :nth-child(2) {
flex: 300px;
}
border-bottom: 1px solid #eee;
}
.trading-header {
justify-content: space-between;
& > div {
flex: 20%;
margin: 10px 0px;
text-align: left;
}
}
.trading-info {
display: flex;
align-self: center;
justify-content: center;
flex: 1;
}
.trading-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
flex: 1;
}