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 ## ⚠️ 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. 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 ## TODO

35
package-lock.json generated
View File

@ -4311,6 +4311,32 @@
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" "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": { "cheerio": {
"version": "0.22.0", "version": "0.22.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
@ -12566,6 +12592,15 @@
"whatwg-fetch": "^3.0.0" "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": { "react-copy-to-clipboard": {
"version": "5.0.2", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz", "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", "bn.js": "^5.1.3",
"bs58": "^4.0.1", "bs58": "^4.0.1",
"buffer-layout": "^1.2.0", "buffer-layout": "^1.2.0",
"chart.js": "^2.9.4",
"craco-less": "^1.17.0", "craco-less": "^1.17.0",
"echarts": "^4.9.0", "echarts": "^4.9.0",
"eventemitter3": "^4.0.7", "eventemitter3": "^4.0.7",
"identicon.js": "^2.3.3", "identicon.js": "^2.3.3",
"jazzicon": "^1.5.0", "jazzicon": "^1.5.0",
"lodash": "^4.17.20",
"react": "^16.13.1", "react": "^16.13.1",
"react-chartjs-2": "^2.11.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-github-btn": "^1.2.0", "react-github-btn": "^1.2.0",
"react-intl": "^5.10.2", "react-intl": "^5.10.2",

View File

@ -1,17 +1,8 @@
import { AccountLayout, MintLayout, Token } from "@solana/spl-token"; import { AccountLayout, MintLayout, Token } from '@solana/spl-token';
import { import { Account, PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js';
Account, import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT } from '../utils/ids';
PublicKey, import { LendingObligationLayout, TokenAccount } from '../models';
SystemProgram, import { cache, TokenAccountParser } from './../contexts/accounts';
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";
export function ensureSplAccount( export function ensureSplAccount(
instructions: TransactionInstruction[], instructions: TransactionInstruction[],
@ -25,31 +16,11 @@ export function ensureSplAccount(
return toCheck.pubkey; return toCheck.pubkey;
} }
const account = createUninitializedAccount( const account = createUninitializedAccount(instructions, payer, amount, signers);
instructions,
payer,
amount,
signers
);
instructions.push( instructions.push(Token.createInitAccountInstruction(TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT, account, payer));
Token.createInitAccountInstruction(
TOKEN_PROGRAM_ID,
WRAPPED_SOL_MINT,
account,
payer
)
);
cleanupInstructions.push( cleanupInstructions.push(Token.createCloseAccountInstruction(TOKEN_PROGRAM_ID, account, payer, payer, []));
Token.createCloseAccountInstruction(
TOKEN_PROGRAM_ID,
account,
payer,
payer,
[]
)
);
return account; return account;
} }
@ -153,16 +124,9 @@ export function createTokenAccount(
owner: PublicKey, owner: PublicKey,
signers: Account[] signers: Account[]
) { ) {
const account = createUninitializedAccount( const account = createUninitializedAccount(instructions, payer, accountRentExempt, signers);
instructions,
payer,
accountRentExempt,
signers
);
instructions.push( instructions.push(Token.createInitAccountInstruction(TOKEN_PROGRAM_ID, mint, account, owner));
Token.createInitAccountInstruction(TOKEN_PROGRAM_ID, mint, account, owner)
);
return account; return account;
} }
@ -196,25 +160,10 @@ export function findOrCreateAccountByMint(
toAccount = account.pubkey; toAccount = account.pubkey;
} else { } else {
// creating depositor pool account // creating depositor pool account
toAccount = createTokenAccount( toAccount = createTokenAccount(instructions, payer, accountRentExempt, mint, owner, signers);
instructions,
payer,
accountRentExempt,
mint,
owner,
signers
);
if (isWrappedSol) { if (isWrappedSol) {
cleanupInstructions.push( cleanupInstructions.push(Token.createCloseAccountInstruction(TOKEN_PROGRAM_ID, toAccount, payer, payer, []));
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 { notify } from "../utils/notifications";
import { LendingReserve } from "./../models/lending/reserve"; import { LendingReserve } from "./../models/lending/reserve";
import { AccountLayout, MintInfo, MintLayout } from "@solana/spl-token"; import { AccountLayout, MintInfo, MintLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID } from "../constants/ids"; import { LENDING_PROGRAM_ID } from "../utils/ids";
import { import {
createTempMemoryAccount, createTempMemoryAccount,
createUninitializedAccount, createUninitializedAccount,
@ -16,8 +16,8 @@ import {
createUninitializedObligation, createUninitializedObligation,
ensureSplAccount, ensureSplAccount,
findOrCreateAccountByMint, findOrCreateAccountByMint,
} from "./account"; } from './account';
import { cache, MintParser, ParsedAccount } from "../contexts/accounts"; import { cache, MintParser, ParsedAccount } from '../contexts/accounts';
import { import {
TokenAccount, TokenAccount,
LendingObligationLayout, LendingObligationLayout,
@ -46,47 +46,38 @@ export const borrow = async (
obligationAccount?: PublicKey obligationAccount?: PublicKey
) => { ) => {
notify({ notify({
message: "Borrowing funds...", message: 'Borrowing funds...',
description: "Please review transactions to approve.", description: 'Please review transactions to approve.',
type: "warn", type: 'warn',
}); });
let signers: Account[] = []; let signers: Account[] = [];
let instructions: TransactionInstruction[] = []; let instructions: TransactionInstruction[] = [];
let cleanupInstructions: TransactionInstruction[] = []; let cleanupInstructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption( const accountRentExempt = await connection.getMinimumBalanceForRentExemption(AccountLayout.span);
AccountLayout.span
);
const obligation = existingObligation const obligation = existingObligation
? existingObligation.pubkey ? existingObligation.pubkey
: createUninitializedObligation( : createUninitializedObligation(
instructions, instructions,
wallet.publicKey, wallet.publicKey,
await connection.getMinimumBalanceForRentExemption( await connection.getMinimumBalanceForRentExemption(LendingObligationLayout.span),
LendingObligationLayout.span signers
), );
signers
);
const obligationMint = existingObligation const obligationMint = existingObligation
? existingObligation.info.tokenMint ? existingObligation.info.tokenMint
: createUninitializedMint( : createUninitializedMint(
instructions, instructions,
wallet.publicKey, wallet.publicKey,
await connection.getMinimumBalanceForRentExemption(MintLayout.span), await connection.getMinimumBalanceForRentExemption(MintLayout.span),
signers signers
); );
const obligationTokenOutput = obligationAccount const obligationTokenOutput = obligationAccount
? obligationAccount ? obligationAccount
: createUninitializedAccount( : createUninitializedAccount(instructions, wallet.publicKey, accountRentExempt, signers);
instructions,
wallet.publicKey,
accountRentExempt,
signers
);
let toAccount = await findOrCreateAccountByMint( let toAccount = await findOrCreateAccountByMint(
wallet.publicKey, wallet.publicKey,
@ -100,21 +91,19 @@ export const borrow = async (
if (instructions.length > 0) { if (instructions.length > 0) {
// create all accounts in one transaction // create all accounts in one transaction
let tx = await sendTransaction(connection, wallet, instructions, [ let tx = await sendTransaction(connection, wallet, instructions, [...signers]);
...signers,
]);
notify({ notify({
message: "Obligation accounts created", message: 'Obligation accounts created',
description: `Transaction ${tx}`, description: `Transaction ${tx}`,
type: "success", type: 'success',
}); });
} }
notify({ notify({
message: "Borrowing funds...", message: 'Borrowing funds...',
description: "Please review transactions to approve.", description: 'Please review transactions to approve.',
type: "warn", type: 'warn',
}); });
signers = []; signers = [];
@ -135,19 +124,15 @@ export const borrow = async (
fromLamports = approvedAmount - accountRentExempt; fromLamports = approvedAmount - accountRentExempt;
const mint = (await cache.query( const mint = (await cache.query(connection, borrowReserve.info.liquidityMint, MintParser)) as ParsedAccount<
connection, MintInfo
borrowReserve.info.liquidityMint, >;
MintParser
)) as ParsedAccount<MintInfo>;
amountLamports = toLamports(amount, mint?.info); amountLamports = toLamports(amount, mint?.info);
} else if (amountType === BorrowAmountType.CollateralDepositAmount) { } else if (amountType === BorrowAmountType.CollateralDepositAmount) {
const mint = (await cache.query( const mint = (await cache.query(connection, depositReserve.info.collateralMint, MintParser)) as ParsedAccount<
connection, MintInfo
depositReserve.info.collateralMint, >;
MintParser
)) as ParsedAccount<MintInfo>;
amountLamports = toLamports(amount, mint?.info); amountLamports = toLamports(amount, mint?.info);
fromLamports = amountLamports; fromLamports = amountLamports;
} }
@ -180,20 +165,12 @@ export const borrow = async (
throw new Error(`Dex market doesn't exist.`); throw new Error(`Dex market doesn't exist.`);
} }
const market = cache.get(depositReserve.info.lendingMarket) as ParsedAccount< const market = cache.get(depositReserve.info.lendingMarket) as ParsedAccount<LendingMarket>;
LendingMarket const dexOrderBookSide = market.info.quoteMint.equals(depositReserve.info.liquidityMint)
>;
const dexOrderBookSide = market.info.quoteMint.equals(
depositReserve.info.liquidityMint
)
? dexMarket?.info.bids ? dexMarket?.info.bids
: dexMarket?.info.asks; : dexMarket?.info.asks;
const memory = createTempMemoryAccount( const memory = createTempMemoryAccount(instructions, wallet.publicKey, signers);
instructions,
wallet.publicKey,
signers
);
// deposit // deposit
instructions.push( instructions.push(
@ -221,17 +198,11 @@ export const borrow = async (
) )
); );
try { try {
let tx = await sendTransaction( let tx = await sendTransaction(connection, wallet, instructions.concat(cleanupInstructions), signers, true);
connection,
wallet,
instructions.concat(cleanupInstructions),
signers,
true
);
notify({ notify({
message: "Funds borrowed.", message: 'Funds borrowed.',
type: "success", type: 'success',
description: `Transaction - ${tx}`, description: `Transaction - ${tx}`,
}); });
} catch { } catch {

View File

@ -12,7 +12,7 @@ import {
LendingReserve, LendingReserve,
} from "./../models/lending"; } from "./../models/lending";
import { AccountLayout } from "@solana/spl-token"; import { AccountLayout } from "@solana/spl-token";
import { LENDING_PROGRAM_ID } from "../constants/ids"; import { LENDING_PROGRAM_ID } from "../utils/ids";
import { import {
createUninitializedAccount, createUninitializedAccount,
ensureSplAccount, ensureSplAccount,
@ -29,9 +29,9 @@ export const deposit = async (
wallet: any wallet: any
) => { ) => {
notify({ notify({
message: "Depositing funds...", message: 'Depositing funds...',
description: "Please review transactions to approve.", description: 'Please review transactions to approve.',
type: "warn", type: 'warn',
}); });
const isInitalized = true; // TODO: finish reserve init const isInitalized = true; // TODO: finish reserve init
@ -41,9 +41,7 @@ export const deposit = async (
const instructions: TransactionInstruction[] = []; const instructions: TransactionInstruction[] = [];
const cleanupInstructions: TransactionInstruction[] = []; const cleanupInstructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption( const accountRentExempt = await connection.getMinimumBalanceForRentExemption(AccountLayout.span);
AccountLayout.span
);
const [authority] = await PublicKey.findProgramAddress( const [authority] = await PublicKey.findProgramAddress(
[reserve.lendingMarket.toBuffer()], // which account should be authority [reserve.lendingMarket.toBuffer()], // which account should be authority
@ -82,12 +80,7 @@ export const deposit = async (
signers signers
); );
} else { } else {
toAccount = createUninitializedAccount( toAccount = createUninitializedAccount(instructions, wallet.publicKey, accountRentExempt, signers);
instructions,
wallet.publicKey,
accountRentExempt,
signers
);
} }
if (isInitalized) { if (isInitalized) {
@ -125,17 +118,11 @@ export const deposit = async (
} }
try { try {
let tx = await sendTransaction( let tx = await sendTransaction(connection, wallet, instructions.concat(cleanupInstructions), signers, true);
connection,
wallet,
instructions.concat(cleanupInstructions),
signers,
true
);
notify({ notify({
message: "Funds deposited.", message: 'Funds deposited.',
type: "success", type: 'success',
description: `Transaction - ${tx}`, description: `Transaction - ${tx}`,
}); });
} catch { } catch {

View File

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

View File

@ -9,7 +9,7 @@ import { notify } from "../utils/notifications";
import { LendingReserve } from "./../models/lending/reserve"; import { LendingReserve } from "./../models/lending/reserve";
import { repayInstruction } from "./../models/lending/repay"; import { repayInstruction } from "./../models/lending/repay";
import { AccountLayout } from "@solana/spl-token"; 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 { findOrCreateAccountByMint } from "./account";
import { approve, LendingObligation, TokenAccount } from "../models"; import { approve, LendingObligation, TokenAccount } from "../models";
import { ParsedAccount } from "../contexts/accounts"; import { ParsedAccount } from "../contexts/accounts";
@ -31,9 +31,9 @@ export const repay = async (
wallet: any wallet: any
) => { ) => {
notify({ notify({
message: "Repaing funds...", message: 'Repaing funds...',
description: "Please review transactions to approve.", description: 'Please review transactions to approve.',
type: "warn", type: 'warn',
}); });
// user from account // user from account
@ -41,9 +41,7 @@ export const repay = async (
const instructions: TransactionInstruction[] = []; const instructions: TransactionInstruction[] = [];
const cleanupInstructions: TransactionInstruction[] = []; const cleanupInstructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption( const accountRentExempt = await connection.getMinimumBalanceForRentExemption(AccountLayout.span);
AccountLayout.span
);
const [authority] = await PublicKey.findProgramAddress( const [authority] = await PublicKey.findProgramAddress(
[repayReserve.info.lendingMarket.toBuffer()], [repayReserve.info.lendingMarket.toBuffer()],
@ -101,17 +99,11 @@ export const repay = async (
) )
); );
let tx = await sendTransaction( let tx = await sendTransaction(connection, wallet, instructions.concat(cleanupInstructions), signers, true);
connection,
wallet,
instructions.concat(cleanupInstructions),
signers,
true
);
notify({ notify({
message: "Funds repaid.", message: 'Funds repaid.',
type: "success", type: 'success',
description: `Transaction - ${tx}`, description: `Transaction - ${tx}`,
}); });
}; };

View File

@ -8,7 +8,7 @@ import { sendTransaction } from "../contexts/connection";
import { notify } from "../utils/notifications"; import { notify } from "../utils/notifications";
import { LendingReserve, withdrawInstruction } from "./../models/lending"; import { LendingReserve, withdrawInstruction } from "./../models/lending";
import { AccountLayout } from "@solana/spl-token"; 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 { findOrCreateAccountByMint } from "./account";
import { approve, TokenAccount } from "../models"; import { approve, TokenAccount } from "../models";
@ -21,9 +21,9 @@ export const withdraw = async (
wallet: any wallet: any
) => { ) => {
notify({ notify({
message: "Withdrawing funds...", message: 'Withdrawing funds...',
description: "Please review transactions to approve.", description: 'Please review transactions to approve.',
type: "warn", type: 'warn',
}); });
// user from account // user from account
@ -31,14 +31,9 @@ export const withdraw = async (
const instructions: TransactionInstruction[] = []; const instructions: TransactionInstruction[] = [];
const cleanupInstructions: TransactionInstruction[] = []; const cleanupInstructions: TransactionInstruction[] = [];
const accountRentExempt = await connection.getMinimumBalanceForRentExemption( const accountRentExempt = await connection.getMinimumBalanceForRentExemption(AccountLayout.span);
AccountLayout.span
);
const [authority] = await PublicKey.findProgramAddress( const [authority] = await PublicKey.findProgramAddress([reserve.lendingMarket.toBuffer()], LENDING_PROGRAM_ID);
[reserve.lendingMarket.toBuffer()],
LENDING_PROGRAM_ID
);
const fromAccount = from.pubkey; const fromAccount = from.pubkey;
@ -76,17 +71,11 @@ export const withdraw = async (
); );
try { try {
let tx = await sendTransaction( let tx = await sendTransaction(connection, wallet, instructions.concat(cleanupInstructions), signers, true);
connection,
wallet,
instructions.concat(cleanupInstructions),
signers,
true
);
notify({ notify({
message: "Funds deposited.", message: 'Funds deposited.',
type: "success", type: 'success',
description: `Transaction - ${tx}`, description: `Transaction - ${tx}`,
}); });
} catch { } 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< const market = cache.get(props.reserve?.lendingMarket) as ParsedAccount<
LendingMarket LendingMarket
>; >;
if (!market) return null;
const quoteMintAddress = market?.info?.quoteMint?.toBase58(); const quoteMintAddress = market?.info?.quoteMint?.toBase58();
@ -60,10 +61,10 @@ export const CollateralSelector = (props: {
return ( return (
<Select <Select
size="large" size='large'
showSearch showSearch
style={{ minWidth: 300, margin: "5px 0px" }} style={{ minWidth: 300, margin: '5px 0px' }}
placeholder="Collateral" placeholder='Collateral'
value={props.collateralReserve} value={props.collateralReserve}
disabled={props.disabled} disabled={props.disabled}
defaultValue={props.collateralReserve} defaultValue={props.collateralReserve}
@ -72,23 +73,16 @@ export const CollateralSelector = (props: {
props.onCollateralReserve(item); props.onCollateralReserve(item);
} }
}} }}
filterOption={(input, option) => filterOption={(input, option) => option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0}
option?.name?.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
> >
{reserveAccounts {reserveAccounts
.filter((reserve) => reserve.info !== props.reserve) .filter((reserve) => reserve.info !== props.reserve)
.filter( .filter((reserve) => !onlyQuoteAllowed || reserve.info.liquidityMint.equals(market.info.quoteMint))
(reserve) =>
!onlyQuoteAllowed ||
reserve.info.liquidityMint.equals(market.info.quoteMint)
)
.map((reserve) => { .map((reserve) => {
const mint = reserve.info.liquidityMint.toBase58(); const mint = reserve.info.liquidityMint.toBase58();
const address = reserve.pubkey.toBase58(); const address = reserve.pubkey.toBase58();
const name = getTokenName(tokenMap, mint); const name = getTokenName(tokenMap, mint);
return <Option key={address} value={address} name={name} title={address}> return <Option key={address} value={address} name={name} title={address}>
<CollateralItem <CollateralItem
reserve={reserve} reserve={reserve}

View File

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

View File

@ -9,6 +9,7 @@ import {
ShoppingOutlined, ShoppingOutlined,
HomeOutlined, HomeOutlined,
RocketOutlined, RocketOutlined,
LineChartOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import BasicLayout from "@ant-design/pro-layout"; import BasicLayout from "@ant-design/pro-layout";
@ -104,8 +105,17 @@ export const AppLayout = (props: any) => {
{LABELS.MENU_LIQUIDATE} {LABELS.MENU_LIQUIDATE}
</Link> </Link>
</Menu.Item> </Menu.Item>
<Menu.Item key="6" icon={< LineChartOutlined/>}>
<Link
to={{
pathname: "/marginTrading",
}}
>
{LABELS.MARGIN_TRADING}
</Link>
</Menu.Item>
{env !== "mainnet-beta" && ( {env !== "mainnet-beta" && (
<Menu.Item key="6" icon={<RocketOutlined />}> <Menu.Item key="7" icon={<RocketOutlined />}>
<Link <Link
to={{ to={{
pathname: "/faucet", 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 "./labels";
export * from "./math"; export * from "./math";
export * from "./marks"; export * from "./marks";
export * from "./style"; export * from "./style";

View File

@ -12,7 +12,7 @@ export const LABELS = {
REPAY_ACTION: "Repay", REPAY_ACTION: "Repay",
RESERVE_STATUS_TITLE: "Reserve Status & Configuration", RESERVE_STATUS_TITLE: "Reserve Status & Configuration",
AUDIT_WARNING: 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: 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.', '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", MENU_HOME: "Home",
@ -57,4 +57,21 @@ export const LABELS = {
GO_BACK_ACTION: "Go back", GO_BACK_ACTION: "Go back",
DEPOSIT_ACTION: "Deposit", DEPOSIT_ACTION: "Deposit",
TOTAL_TITLE: "Total", 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 React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useConnection } from "./connection"; import { useConnection } from './connection';
import { useWallet } from "./wallet"; import { useWallet } from './wallet';
import { AccountInfo, Connection, PublicKey } from "@solana/web3.js"; 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 { AccountLayout, u64, MintInfo, MintLayout } from "@solana/spl-token"; import { PoolInfo, TokenAccount } from './../models';
import { TokenAccount } from "./../models"; import { chunks } from './../utils/utils';
import { chunks } from "./../utils/utils"; import { EventEmitter } from './../utils/eventEmitter';
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 AccountsContext = React.createContext<any>(null);
const pendingCalls = new Map<string, Promise<ParsedAccountBase>>(); const pendingCalls = new Map<string, Promise<ParsedAccountBase>>();
const genericCache = new Map<string, ParsedAccountBase>(); const genericCache = new Map<string, ParsedAccountBase>();
const pendingMintCalls = new Map<string, Promise<MintInfo>>();
const mintCache = new Map<string, MintInfo>();
export interface ParsedAccountBase { export interface ParsedAccountBase {
pubkey: PublicKey; pubkey: PublicKey;
@ -19,15 +23,23 @@ export interface ParsedAccountBase {
info: any; // TODO: change to unkown info: any; // TODO: change to unkown
} }
export type AccountParser = ( export type AccountParser = (pubkey: PublicKey, data: AccountInfo<Buffer>) => ParsedAccountBase | undefined;
pubkey: PublicKey,
data: AccountInfo<Buffer>
) => ParsedAccountBase | undefined;
export interface ParsedAccount<T> extends ParsedAccountBase { export interface ParsedAccount<T> extends ParsedAccountBase {
info: T; 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>) => { export const MintParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
const buffer = Buffer.from(info.data); const buffer = Buffer.from(info.data);
@ -44,10 +56,7 @@ export const MintParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
return details; return details;
}; };
export const TokenAccountParser = ( export const TokenAccountParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
pubKey: PublicKey,
info: AccountInfo<Buffer>
) => {
const buffer = Buffer.from(info.data); const buffer = Buffer.from(info.data);
const data = deserializeAccount(buffer); const data = deserializeAccount(buffer);
@ -62,10 +71,7 @@ export const TokenAccountParser = (
return details; return details;
}; };
export const GenericAccountParser = ( export const GenericAccountParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
pubKey: PublicKey,
info: AccountInfo<Buffer>
) => {
const buffer = Buffer.from(info.data); const buffer = Buffer.from(info.data);
const details = { const details = {
@ -83,13 +89,9 @@ export const keyToAccountParser = new Map<string, AccountParser>();
export const cache = { export const cache = {
emitter: new EventEmitter(), emitter: new EventEmitter(),
query: async ( query: async (connection: Connection, pubKey: string | PublicKey, parser?: AccountParser) => {
connection: Connection,
pubKey: string | PublicKey,
parser?: AccountParser
) => {
let id: PublicKey; let id: PublicKey;
if (typeof pubKey === "string") { if (typeof pubKey === 'string') {
id = new PublicKey(pubKey); id = new PublicKey(pubKey);
} else { } else {
id = pubKey; id = pubKey;
@ -110,7 +112,7 @@ export const cache = {
// TODO: refactor to use multiple accounts query with flush like behavior // TODO: refactor to use multiple accounts query with flush like behavior
query = connection.getAccountInfo(id).then((data) => { query = connection.getAccountInfo(id).then((data) => {
if (!data) { if (!data) {
throw new Error("Account not found"); throw new Error('Account not found');
} }
return cache.add(id, data, parser); return cache.add(id, data, parser);
@ -119,17 +121,11 @@ export const cache = {
return query; return query;
}, },
add: ( add: (id: PublicKey | string, obj: AccountInfo<Buffer>, parser?: AccountParser) => {
id: PublicKey | string, const address = typeof id === 'string' ? id : id?.toBase58();
obj: AccountInfo<Buffer>,
parser?: AccountParser
) => {
const address = typeof id === "string" ? id : id?.toBase58();
const deserialize = parser ? parser : keyToAccountParser.get(address); const deserialize = parser ? parser : keyToAccountParser.get(address);
if (!deserialize) { if (!deserialize) {
throw new Error( throw new Error('Deserializer needs to be registered or passed as a parameter');
"Deserializer needs to be registered or passed as a parameter"
);
} }
cache.registerParser(id, deserialize); cache.registerParser(id, deserialize);
@ -147,7 +143,7 @@ export const cache = {
}, },
get: (pubKey: string | PublicKey) => { get: (pubKey: string | PublicKey) => {
let key: string; let key: string;
if (typeof pubKey !== "string") { if (typeof pubKey !== 'string') {
key = pubKey.toBase58(); key = pubKey.toBase58();
} else { } else {
key = pubKey; key = pubKey;
@ -155,6 +151,22 @@ export const cache = {
return genericCache.get(key); 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) => { byParser: (parser: AccountParser) => {
const result: string[] = []; const result: string[] = [];
for (const id of keyToAccountParser.keys()) { for (const id of keyToAccountParser.keys()) {
@ -167,12 +179,57 @@ export const cache = {
}, },
registerParser: (pubkey: PublicKey | string, parser: AccountParser) => { registerParser: (pubkey: PublicKey | string, parser: AccountParser) => {
if (pubkey) { if (pubkey) {
const address = typeof pubkey === "string" ? pubkey : pubkey?.toBase58(); const address = typeof pubkey === 'string' ? pubkey : pubkey?.toBase58();
keyToAccountParser.set(address, parser); keyToAccountParser.set(address, parser);
} }
return pubkey; 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 = () => { export const useAccountsContext = () => {
@ -181,10 +238,7 @@ export const useAccountsContext = () => {
return context; return context;
}; };
function wrapNativeAccount( function wrapNativeAccount(pubkey: PublicKey, account?: AccountInfo<Buffer>): TokenAccount | undefined {
pubkey: PublicKey,
account?: AccountInfo<Buffer>
): TokenAccount | undefined {
if (!account) { if (!account) {
return undefined; 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 UseNativeAccount = () => {
const connection = useConnection(); const connection = useConnection();
const { wallet } = useWallet(); const { wallet } = useWallet();
@ -249,10 +324,7 @@ const UseNativeAccount = () => {
}; };
const PRECACHED_OWNERS = new Set<string>(); const PRECACHED_OWNERS = new Set<string>();
const precacheUserTokenAccounts = async ( const precacheUserTokenAccounts = async (connection: Connection, owner?: PublicKey) => {
connection: Connection,
owner?: PublicKey
) => {
if (!owner) { if (!owner) {
return; return;
} }
@ -275,28 +347,25 @@ export function AccountsProvider({ children = null as any }) {
const [tokenAccounts, setTokenAccounts] = useState<TokenAccount[]>([]); const [tokenAccounts, setTokenAccounts] = useState<TokenAccount[]>([]);
const [userAccounts, setUserAccounts] = useState<TokenAccount[]>([]); const [userAccounts, setUserAccounts] = useState<TokenAccount[]>([]);
const { nativeAccount } = UseNativeAccount(); const { nativeAccount } = UseNativeAccount();
const { pools } = usePools();
const selectUserAccounts = useCallback(() => { const selectUserAccounts = useCallback(() => {
return cache return cache
.byParser(TokenAccountParser) .byParser(TokenAccountParser)
.map((id) => cache.get(id)) .map((id) => cache.get(id))
.filter( .filter((a) => a && a.info.owner.toBase58() === wallet.publicKey?.toBase58())
(a) => a && a.info.owner.toBase58() === wallet.publicKey?.toBase58()
)
.map((a) => a as TokenAccount); .map((a) => a as TokenAccount);
}, [wallet]); }, [wallet]);
useEffect(() => { useEffect(() => {
const accounts = selectUserAccounts().filter( const accounts = selectUserAccounts().filter((a) => a !== undefined) as TokenAccount[];
(a) => a !== undefined
) as TokenAccount[];
setUserAccounts(accounts); setUserAccounts(accounts);
}, [nativeAccount, wallet, tokenAccounts, selectUserAccounts]); }, [nativeAccount, wallet, tokenAccounts, selectUserAccounts]);
useEffect(() => { useEffect(() => {
const subs: number[] = []; const subs: number[] = [];
cache.emitter.onCache((args) => { cache.emitter.onCache((args) => {
if(args.isNew) { if (args.isNew) {
let id = args.id; let id = args.id;
let deserialize = args.parser; let deserialize = args.parser;
connection.onAccountChange(new PublicKey(id), (info) => { connection.onAccountChange(new PublicKey(id), (info) => {
@ -306,8 +375,8 @@ export function AccountsProvider({ children = null as any }) {
}); });
return () => { return () => {
subs.forEach(id => connection.removeAccountChangeListener(id)); subs.forEach((id) => connection.removeAccountChangeListener(id));
} };
}, [connection]); }, [connection]);
const publicKey = wallet?.publicKey; 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 // 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 // this should use only filter syntax to only get accounts that are owned by user
const tokenSubID = connection.onProgramAccountChange( const tokenSubID = connection.onProgramAccountChange(
programIds().token, programIds().token,
@ -337,7 +406,7 @@ export function AccountsProvider({ children = null as any }) {
} }
} }
}, },
"singleGossip" 'singleGossip'
); );
return () => { return () => {
@ -350,6 +419,7 @@ export function AccountsProvider({ children = null as any }) {
<AccountsContext.Provider <AccountsContext.Provider
value={{ value={{
userAccounts, userAccounts,
pools,
nativeAccount, nativeAccount,
}} }}
> >
@ -365,15 +435,9 @@ export function useNativeAccount() {
}; };
} }
export const getMultipleAccounts = async ( export const getMultipleAccounts = async (connection: any, keys: string[], commitment: string) => {
connection: any,
keys: string[],
commitment: string
) => {
const result = await Promise.all( const result = await Promise.all(
chunks(keys, 99).map((chunk) => chunks(keys, 99).map((chunk) => getMultipleAccountsCore(connection, chunk, commitment))
getMultipleAccountsCore(connection, chunk, commitment)
)
); );
const array = result const array = result
@ -388,7 +452,7 @@ export const getMultipleAccounts = async (
const { data, ...rest } = acc; const { data, ...rest } = acc;
const obj = { const obj = {
...rest, ...rest,
data: Buffer.from(data[0], "base64"), data: Buffer.from(data[0], 'base64'),
} as AccountInfo<Buffer>; } as AccountInfo<Buffer>;
return obj; return obj;
}) })
@ -398,18 +462,12 @@ export const getMultipleAccounts = async (
return { keys, array }; return { keys, array };
}; };
const getMultipleAccountsCore = async ( const getMultipleAccountsCore = async (connection: any, keys: string[], commitment: string) => {
connection: any, const args = connection._buildArgs([keys], commitment, 'base64');
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) { if (unsafeRes.error) {
throw new Error( throw new Error('failed to get info about account ' + unsafeRes.error.message);
"failed to get info about account " + unsafeRes.error.message
);
} }
if (unsafeRes.result.value) { if (unsafeRes.result.value) {
@ -425,7 +483,7 @@ export function useMint(key?: string | PublicKey) {
const connection = useConnection(); const connection = useConnection();
const [mint, setMint] = useState<MintInfo>(); const [mint, setMint] = useState<MintInfo>();
const id = typeof key === "string" ? key : key?.toBase58(); const id = typeof key === 'string' ? key : key?.toBase58();
useEffect(() => { useEffect(() => {
if (!id) { if (!id) {
@ -440,9 +498,7 @@ export function useMint(key?: string | PublicKey) {
const dispose = cache.emitter.onCache((e) => { const dispose = cache.emitter.onCache((e) => {
const event = e; const event = e;
if (event.id === id) { if (event.id === id) {
cache cache.query(connection, id, MintParser).then((mint) => setMint(mint.info as any));
.query(connection, id, MintParser)
.then((mint) => setMint(mint.info as any));
} }
}); });
return () => { return () => {
@ -453,6 +509,17 @@ export function useMint(key?: string | PublicKey) {
return mint; 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) { export function useAccount(pubKey?: PublicKey) {
const connection = useConnection(); const connection = useConnection();
const [account, setAccount] = useState<TokenAccount>(); const [account, setAccount] = useState<TokenAccount>();
@ -465,9 +532,7 @@ export function useAccount(pubKey?: PublicKey) {
return; return;
} }
const acc = await cache const acc = await cache.query(connection, key, TokenAccountParser).catch((err) => console.log(err));
.query(connection, key, TokenAccountParser)
.catch((err) => console.log(err));
if (acc) { if (acc) {
setAccount(acc); setAccount(acc);
} }
@ -530,7 +595,7 @@ const deserializeAccount = (data: Buffer) => {
// TODO: expose in spl package // TODO: expose in spl package
const deserializeMint = (data: Buffer) => { const deserializeMint = (data: Buffer) => {
if (data.length !== MintLayout.span) { if (data.length !== MintLayout.span) {
throw new Error("Not a valid Mint"); throw new Error('Not a valid Mint');
} }
const mintInfo = MintLayout.decode(data); const mintInfo = MintLayout.decode(data);

View File

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

View File

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

View File

@ -1,15 +1,25 @@
import React, { useCallback, useContext, useEffect, useState } from "react"; import React, { useCallback, useContext, useEffect, useState } from 'react';
import { MINT_TO_MARKET } from "./../models/marketOverrides"; import { MINT_TO_MARKET } from './../models/marketOverrides';
import { fromLamports, STABLE_COINS } from "./../utils/utils"; import { POOLS_WITH_AIRDROP } from './../models/airdrops';
import { useConnectionConfig } from "./connection"; import { convert, fromLamports, getPoolName, getTokenName, KnownTokenMap, STABLE_COINS } from './../utils/utils';
import { cache, getMultipleAccounts, ParsedAccount } from "./accounts"; import { useConnection, useConnectionConfig } from './connection';
import { Market, MARKETS, Orderbook, TOKEN_MINTS } from "@project-serum/serum"; import { cache, getMultipleAccounts, ParsedAccount } from './accounts';
import { AccountInfo, Connection, PublicKey } from "@solana/web3.js"; import { Market, MARKETS, Orderbook, TOKEN_MINTS } from '@project-serum/serum';
import { useMemo } from "react"; import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
import { EventEmitter } from "./../utils/eventEmitter"; import { useMemo } from 'react';
import { EventEmitter } from './../utils/eventEmitter';
import { DexMarketParser } from "./../models/dex"; import { DexMarketParser } from './../models/dex';
import { LendingMarket, LendingReserve } from "../models"; 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 { export interface MarketsContextState {
midPriceInUSD: (mint: string) => number; midPriceInUSD: (mint: string) => number;
@ -20,6 +30,7 @@ export interface MarketsContextState {
subscribeToMarket: (mint: string) => () => void; subscribeToMarket: (mint: string) => () => void;
precacheMarkets: (mints: string[]) => void; precacheMarkets: (mints: string[]) => void;
dailyVolume: Map<string, RecentPoolData>;
} }
const REFRESH_INTERVAL = 30_000; const REFRESH_INTERVAL = 30_000;
@ -32,24 +43,19 @@ export function MarketProvider({ children = null as any }) {
const { endpoint } = useConnectionConfig(); const { endpoint } = useConnectionConfig();
const accountsToObserve = useMemo(() => new Map<string, number>(), []); const accountsToObserve = useMemo(() => new Map<string, number>(), []);
const [marketMints, setMarketMints] = useState<string[]>([]); const [marketMints, setMarketMints] = useState<string[]>([]);
const [dailyVolume, setDailyVolume] = useState<Map<string, RecentPoolData>>(new Map());
const connection = useMemo(() => new Connection(endpoint, "recent"), [ const connection = useMemo(() => new Connection(endpoint, 'recent'), [endpoint]);
endpoint,
]);
const marketByMint = useMemo(() => { const marketByMint = useMemo(() => {
return [...new Set(marketMints).values()].reduce((acc, key) => { return [...new Set(marketMints).values()].reduce((acc, key) => {
const mintAddress = key; const mintAddress = key;
const SERUM_TOKEN = TOKEN_MINTS.find( const SERUM_TOKEN = TOKEN_MINTS.find((a) => a.address.toBase58() === mintAddress);
(a) => a.address.toBase58() === mintAddress
);
const marketAddress = MINT_TO_MARKET[mintAddress]; const marketAddress = MINT_TO_MARKET[mintAddress];
const marketName = `${SERUM_TOKEN?.name}/USDC`; const marketName = `${SERUM_TOKEN?.name}/USDC`;
const marketInfo = MARKETS.find( const marketInfo = MARKETS.find((m) => m.name === marketName || m.address.toBase58() === marketAddress);
(m) => m.name === marketName || m.address.toBase58() === marketAddress
);
if (marketInfo) { if (marketInfo) {
acc.set(mintAddress, { acc.set(mintAddress, {
@ -63,6 +69,7 @@ export function MarketProvider({ children = null as any }) {
useEffect(() => { useEffect(() => {
let timer = 0; let timer = 0;
let bonfidaTimer = 0;
const updateData = async () => { const updateData = async () => {
await refreshAccounts(connection, [...accountsToObserve.keys()]); await refreshAccounts(connection, [...accountsToObserve.keys()]);
@ -71,6 +78,23 @@ export function MarketProvider({ children = null as any }) {
timer = window.setTimeout(() => updateData(), REFRESH_INTERVAL); 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 initalQuery = async () => {
const reverseSerumMarketCache = new Map<string, string>(); const reverseSerumMarketCache = new Map<string, string>();
[...marketByMint.keys()].forEach((mint) => { [...marketByMint.keys()].forEach((mint) => {
@ -88,7 +112,7 @@ export function MarketProvider({ children = null as any }) {
connection, connection,
// only query for markets that are not in cahce // only query for markets that are not in cahce
allMarkets.filter((a) => cache.get(a) === undefined), allMarkets.filter((a) => cache.get(a) === undefined),
"single" 'single'
).then(({ keys, array }) => { ).then(({ keys, array }) => {
allMarkets.forEach(() => {}); allMarkets.forEach(() => {});
@ -141,15 +165,13 @@ export function MarketProvider({ children = null as any }) {
return () => { return () => {
window.clearTimeout(timer); window.clearTimeout(timer);
window.clearTimeout(bonfidaTimer);
}; };
}, [marketByMint, accountsToObserve, connection]); }, [marketByMint, accountsToObserve, connection]);
const midPriceInUSD = useCallback( const midPriceInUSD = useCallback(
(mintAddress: string) => { (mintAddress: string) => {
return getMidPrice( return getMidPrice(marketByMint.get(mintAddress)?.marketInfo.address.toBase58(), mintAddress);
marketByMint.get(mintAddress)?.marketInfo.address.toBase58(),
mintAddress
);
}, },
[marketByMint] [marketByMint]
); );
@ -157,7 +179,7 @@ export function MarketProvider({ children = null as any }) {
const subscribeToMarket = useCallback( const subscribeToMarket = useCallback(
(mintAddress: string) => { (mintAddress: string) => {
const info = marketByMint.get(mintAddress); const info = marketByMint.get(mintAddress);
const market = cache.get(info?.marketInfo.address.toBase58() || ""); const market = cache.get(info?.marketInfo.address.toBase58() || '');
if (!market) { if (!market) {
return () => {}; return () => {};
} }
@ -206,6 +228,7 @@ export function MarketProvider({ children = null as any }) {
marketByMint, marketByMint,
subscribeToMarket, subscribeToMarket,
precacheMarkets, precacheMarkets,
dailyVolume,
}} }}
> >
{children} {children}
@ -218,10 +241,189 @@ export const useMarkets = () => {
return context as MarketsContextState; 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) => { export const useMidPriceInUSD = (mint: string) => {
const { midPriceInUSD, subscribeToMarket, marketEmitter } = useContext( const { midPriceInUSD, subscribeToMarket, marketEmitter } = useContext(MarketsContext) as MarketsContextState;
MarketsContext
) as MarketsContextState;
const [price, setPrice] = useState<number>(0); const [price, setPrice] = useState<number>(0);
useEffect(() => { useEffect(() => {
@ -249,11 +451,7 @@ export const usePrecacheMarket = () => {
return context.precacheMarkets; return context.precacheMarkets;
}; };
export const simulateMarketOrderFill = ( export const simulateMarketOrderFill = (amount: number, reserve: LendingReserve, dex: PublicKey) => {
amount: number,
reserve: LendingReserve,
dex: PublicKey
) => {
const liquidityMint = cache.get(reserve.liquidityMint); const liquidityMint = cache.get(reserve.liquidityMint);
const collateralMint = cache.get(reserve.collateralMint); const collateralMint = cache.get(reserve.collateralMint);
if (!liquidityMint || !collateralMint) { if (!liquidityMint || !collateralMint) {
@ -266,22 +464,12 @@ export const simulateMarketOrderFill = (
} }
const decodedMarket = marketInfo.info; const decodedMarket = marketInfo.info;
const baseMintDecimals = const baseMintDecimals = cache.get(decodedMarket.baseMint)?.info.decimals || 0;
cache.get(decodedMarket.baseMint)?.info.decimals || 0; const quoteMintDecimals = cache.get(decodedMarket.quoteMint)?.info.decimals || 0;
const quoteMintDecimals =
cache.get(decodedMarket.quoteMint)?.info.decimals || 0;
const lendingMarket = cache.get(reserve.lendingMarket) as ParsedAccount< const lendingMarket = cache.get(reserve.lendingMarket) as ParsedAccount<LendingMarket>;
LendingMarket
>;
const dexMarket = new Market( const dexMarket = new Market(decodedMarket, baseMintDecimals, quoteMintDecimals, undefined, decodedMarket.programId);
decodedMarket,
baseMintDecimals,
quoteMintDecimals,
undefined,
decodedMarket.programId
);
const bookAccount = lendingMarket.info.quoteMint.equals(reserve.liquidityMint) const bookAccount = lendingMarket.info.quoteMint.equals(reserve.liquidityMint)
? decodedMarket?.bids ? decodedMarket?.bids
@ -320,11 +508,9 @@ export const simulateMarketOrderFill = (
}; };
const getMidPrice = (marketAddress?: string, mintAddress?: string) => { const getMidPrice = (marketAddress?: string, mintAddress?: string) => {
const SERUM_TOKEN = TOKEN_MINTS.find( const SERUM_TOKEN = TOKEN_MINTS.find((a) => a.address.toBase58() === mintAddress);
(a) => a.address.toBase58() === mintAddress
);
if (STABLE_COINS.has(SERUM_TOKEN?.name || "")) { if (STABLE_COINS.has(SERUM_TOKEN?.name || '')) {
return 1.0; return 1.0;
} }
@ -339,18 +525,10 @@ const getMidPrice = (marketAddress?: string, mintAddress?: string) => {
const decodedMarket = marketInfo.info; const decodedMarket = marketInfo.info;
const baseMintDecimals = const baseMintDecimals = cache.get(decodedMarket.baseMint)?.info.decimals || 0;
cache.get(decodedMarket.baseMint)?.info.decimals || 0; const quoteMintDecimals = cache.get(decodedMarket.quoteMint)?.info.decimals || 0;
const quoteMintDecimals =
cache.get(decodedMarket.quoteMint)?.info.decimals || 0;
const market = new Market( const market = new Market(decodedMarket, baseMintDecimals, quoteMintDecimals, undefined, decodedMarket.programId);
decodedMarket,
baseMintDecimals,
quoteMintDecimals,
undefined,
decodedMarket.programId
);
const bids = cache.get(decodedMarket.bids)?.info; const bids = cache.get(decodedMarket.bids)?.info;
const asks = cache.get(decodedMarket.asks)?.info; const asks = cache.get(decodedMarket.asks)?.info;
@ -375,14 +553,12 @@ const refreshAccounts = async (connection: Connection, keys: string[]) => {
return []; return [];
} }
return getMultipleAccounts(connection, keys, "single").then( return getMultipleAccounts(connection, keys, 'single').then(({ keys, array }) => {
({ keys, array }) => { return array.map((item, index) => {
return array.map((item, index) => { const address = keys[index];
const address = keys[index]; return cache.add(new PublicKey(address), item);
return cache.add(new PublicKey(address), item); });
}); });
}
);
}; };
interface SerumMarket { 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 './account';
export * from "./lending"; export * from './lending';
export * from "./totals"; export * from './tokenSwap';
export * from './pool';
export * from './totals';

View File

@ -1,16 +1,11 @@
import { import { PublicKey, SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY, TransactionInstruction } from '@solana/web3.js';
PublicKey, import BN from 'bn.js';
SYSVAR_CLOCK_PUBKEY, import * as BufferLayout from 'buffer-layout';
SYSVAR_RENT_PUBKEY, import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from '../../utils/ids';
TransactionInstruction, import { wadToLamports } from '../../utils/utils';
} from "@solana/web3.js"; import * as Layout from './../../utils/layout';
import BN from "bn.js"; import { LendingInstruction } from './lending';
import * as BufferLayout from "buffer-layout"; import { LendingReserve } from './reserve';
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";
export enum BorrowAmountType { export enum BorrowAmountType {
LiquidityBorrowAmount = 0, LiquidityBorrowAmount = 0,
@ -60,9 +55,9 @@ export const borrowInstruction = (
memory: PublicKey memory: PublicKey
): TransactionInstruction => { ): TransactionInstruction => {
const dataLayout = BufferLayout.struct([ const dataLayout = BufferLayout.struct([
BufferLayout.u8("instruction"), BufferLayout.u8('instruction'),
Layout.uint64("amount"), Layout.uint64('amount'),
BufferLayout.u8("amountType"), BufferLayout.u8('amountType'),
]); ]);
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
@ -113,8 +108,7 @@ export const borrowInstruction = (
export const calculateBorrowAPY = (reserve: LendingReserve) => { export const calculateBorrowAPY = (reserve: LendingReserve) => {
const totalBorrows = wadToLamports(reserve.borrowedLiquidityWad).toNumber(); const totalBorrows = wadToLamports(reserve.borrowedLiquidityWad).toNumber();
const currentUtilization = const currentUtilization = totalBorrows / (reserve.availableLiquidity.toNumber() + totalBorrows);
totalBorrows / (reserve.availableLiquidity.toNumber() + totalBorrows);
const optimalUtilization = reserve.config.optimalUtilizationRate / 100; const optimalUtilization = reserve.config.optimalUtilizationRate / 100;
let borrowAPY; let borrowAPY;
@ -122,16 +116,12 @@ export const calculateBorrowAPY = (reserve: LendingReserve) => {
const normalizedFactor = currentUtilization / optimalUtilization; const normalizedFactor = currentUtilization / optimalUtilization;
const optimalBorrowRate = reserve.config.optimalBorrowRate / 100; const optimalBorrowRate = reserve.config.optimalBorrowRate / 100;
const minBorrowRate = reserve.config.minBorrowRate / 100; const minBorrowRate = reserve.config.minBorrowRate / 100;
borrowAPY = borrowAPY = normalizedFactor * (optimalBorrowRate - minBorrowRate) + minBorrowRate;
normalizedFactor * (optimalBorrowRate - minBorrowRate) + minBorrowRate;
} else { } else {
const normalizedFactor = const normalizedFactor = (currentUtilization - optimalUtilization) / (1 - optimalUtilization);
(currentUtilization - optimalUtilization) / (1 - optimalUtilization);
const optimalBorrowRate = reserve.config.optimalBorrowRate / 100; const optimalBorrowRate = reserve.config.optimalBorrowRate / 100;
const maxBorrowRate = reserve.config.maxBorrowRate / 100; const maxBorrowRate = reserve.config.maxBorrowRate / 100;
borrowAPY = borrowAPY = normalizedFactor * (maxBorrowRate - optimalBorrowRate) + optimalBorrowRate;
normalizedFactor * (maxBorrowRate - optimalBorrowRate) +
optimalBorrowRate;
} }
return borrowAPY; return borrowAPY;

View File

@ -1,16 +1,12 @@
import { import { PublicKey, SYSVAR_CLOCK_PUBKEY, TransactionInstruction } from '@solana/web3.js';
PublicKey, import BN from 'bn.js';
SYSVAR_CLOCK_PUBKEY, import * as BufferLayout from 'buffer-layout';
TransactionInstruction, import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from '../../utils/ids';
} from "@solana/web3.js"; import { wadToLamports } from '../../utils/utils';
import BN from "bn.js"; import * as Layout from './../../utils/layout';
import * as BufferLayout from "buffer-layout"; import { calculateBorrowAPY } from './borrow';
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids"; import { LendingInstruction } from './lending';
import { wadToLamports } from "../../utils/utils"; import { LendingReserve } from './reserve';
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 /// Deposit liquidity into a reserve. The output is a collateral token representing ownership
/// of the reserve liquidity pool. /// of the reserve liquidity pool.
@ -32,10 +28,7 @@ export const depositInstruction = (
reserveSupply: PublicKey, reserveSupply: PublicKey,
collateralMint: PublicKey collateralMint: PublicKey
): TransactionInstruction => { ): TransactionInstruction => {
const dataLayout = BufferLayout.struct([ const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction'), Layout.uint64('liquidityAmount')]);
BufferLayout.u8("instruction"),
Layout.uint64("liquidityAmount"),
]);
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
dataLayout.encode( dataLayout.encode(
@ -65,8 +58,7 @@ export const depositInstruction = (
export const calculateDepositAPY = (reserve: LendingReserve) => { export const calculateDepositAPY = (reserve: LendingReserve) => {
const totalBorrows = wadToLamports(reserve.borrowedLiquidityWad).toNumber(); const totalBorrows = wadToLamports(reserve.borrowedLiquidityWad).toNumber();
const currentUtilization = const currentUtilization = totalBorrows / (reserve.availableLiquidity.toNumber() + totalBorrows);
totalBorrows / (reserve.availableLiquidity.toNumber() + totalBorrows);
const borrowAPY = calculateBorrowAPY(reserve); const borrowAPY = calculateBorrowAPY(reserve);
return currentUtilization * borrowAPY; return currentUtilization * borrowAPY;

View File

@ -1,13 +1,9 @@
import { import { PublicKey, SYSVAR_CLOCK_PUBKEY, TransactionInstruction } from '@solana/web3.js';
PublicKey, import BN from 'bn.js';
SYSVAR_CLOCK_PUBKEY, import { LendingInstruction } from './lending';
TransactionInstruction, import * as BufferLayout from 'buffer-layout';
} from "@solana/web3.js"; import * as Layout from './../../utils/layout';
import BN from "bn.js"; import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from '../../utils/ids';
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";
/// Purchase collateral tokens at a discount rate if the chosen obligation is unhealthy. /// Purchase collateral tokens at a discount rate if the chosen obligation is unhealthy.
/// ///
@ -38,10 +34,7 @@ export const liquidateInstruction = (
dexOrderBookSide: PublicKey, dexOrderBookSide: PublicKey,
memory: PublicKey memory: PublicKey
): TransactionInstruction => { ): TransactionInstruction => {
const dataLayout = BufferLayout.struct([ const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction'), Layout.uint64('liquidityAmount')]);
BufferLayout.u8("instruction"),
Layout.uint64("liquidityAmount"),
]);
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
dataLayout.encode( dataLayout.encode(

View File

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

View File

@ -4,54 +4,52 @@ import {
SYSVAR_CLOCK_PUBKEY, SYSVAR_CLOCK_PUBKEY,
SYSVAR_RENT_PUBKEY, SYSVAR_RENT_PUBKEY,
TransactionInstruction, TransactionInstruction,
} from "@solana/web3.js"; } from '@solana/web3.js';
import BN from "bn.js"; import BN from 'bn.js';
import * as BufferLayout from "buffer-layout"; import * as BufferLayout from 'buffer-layout';
import { LENDING_PROGRAM_ID, TOKEN_PROGRAM_ID } from "../../constants/ids"; import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from '../../utils/ids';
import { wadToLamports } from "../../utils/utils"; import { wadToLamports } from '../../utils/utils';
import * as Layout from "./../../utils/layout"; import * as Layout from './../../utils/layout';
import { LendingInstruction } from "./lending"; import { LendingInstruction } from './lending';
export const LendingReserveLayout: typeof BufferLayout.Structure = BufferLayout.struct( export const LendingReserveLayout: typeof BufferLayout.Structure = BufferLayout.struct([
[ Layout.uint64('lastUpdateSlot'),
Layout.uint64("lastUpdateSlot"), Layout.publicKey('lendingMarket'),
Layout.publicKey("lendingMarket"), Layout.publicKey('liquidityMint'),
Layout.publicKey("liquidityMint"), BufferLayout.u8('liquidityMintDecimals'),
BufferLayout.u8("liquidityMintDecimals"), Layout.publicKey('liquiditySupply'),
Layout.publicKey("liquiditySupply"), Layout.publicKey('collateralMint'),
Layout.publicKey("collateralMint"), Layout.publicKey('collateralSupply'),
Layout.publicKey("collateralSupply"), // TODO: replace u32 option with generic quivalent
// TODO: replace u32 option with generic quivalent BufferLayout.u32('dexMarketOption'),
BufferLayout.u32("dexMarketOption"), Layout.publicKey('dexMarket'),
Layout.publicKey("dexMarket"),
BufferLayout.struct( BufferLayout.struct(
[ [
/// Optimal utilization rate as a percent /// Optimal utilization rate as a percent
BufferLayout.u8("optimalUtilizationRate"), BufferLayout.u8('optimalUtilizationRate'),
/// The ratio of the loan to the value of the collateral as a percent /// The ratio of the loan to the value of the collateral as a percent
BufferLayout.u8("loanToValueRatio"), BufferLayout.u8('loanToValueRatio'),
/// The percent discount the liquidator gets when buying collateral for an unhealthy obligation /// The percent discount the liquidator gets when buying collateral for an unhealthy obligation
BufferLayout.u8("liquidationBonus"), BufferLayout.u8('liquidationBonus'),
/// The percent at which an obligation is considered unhealthy /// The percent at which an obligation is considered unhealthy
BufferLayout.u8("liquidationThreshold"), BufferLayout.u8('liquidationThreshold'),
/// Min borrow APY /// Min borrow APY
BufferLayout.u8("minBorrowRate"), BufferLayout.u8('minBorrowRate'),
/// Optimal (utilization) borrow APY /// Optimal (utilization) borrow APY
BufferLayout.u8("optimalBorrowRate"), BufferLayout.u8('optimalBorrowRate'),
/// Max borrow APY /// Max borrow APY
BufferLayout.u8("maxBorrowRate"), BufferLayout.u8('maxBorrowRate'),
], ],
"config" 'config'
), ),
Layout.uint128("cumulativeBorrowRateWad"), Layout.uint128('cumulativeBorrowRateWad'),
Layout.uint128("borrowedLiquidityWad"), Layout.uint128('borrowedLiquidityWad'),
Layout.uint64("availableLiquidity"), Layout.uint64('availableLiquidity'),
Layout.uint64("collateralMintSupply"), Layout.uint64('collateralMintSupply'),
] ]);
);
export const isLendingReserve = (info: AccountInfo<Buffer>) => { export const isLendingReserve = (info: AccountInfo<Buffer>) => {
return info.data.length === LendingReserveLayout.span; return info.data.length === LendingReserveLayout.span;
@ -86,10 +84,7 @@ export interface LendingReserve {
collateralMintSupply: BN; collateralMintSupply: BN;
} }
export const LendingReserveParser = ( export const LendingReserveParser = (pubKey: PublicKey, info: AccountInfo<Buffer>) => {
pubKey: PublicKey,
info: AccountInfo<Buffer>
) => {
const buffer = Buffer.from(info.data); const buffer = Buffer.from(info.data);
const data = LendingReserveLayout.decode(buffer); const data = LendingReserveLayout.decode(buffer);
if (data.lastUpdateSlot.toNumber() === 0) return; if (data.lastUpdateSlot.toNumber() === 0) return;
@ -123,9 +118,9 @@ export const initReserveInstruction = (
dexMarket: PublicKey // TODO: optional dexMarket: PublicKey // TODO: optional
): TransactionInstruction => { ): TransactionInstruction => {
const dataLayout = BufferLayout.struct([ const dataLayout = BufferLayout.struct([
BufferLayout.u8("instruction"), BufferLayout.u8('instruction'),
Layout.uint64("liquidityAmount"), Layout.uint64('liquidityAmount'),
BufferLayout.u8("maxUtilizationRate"), BufferLayout.u8('maxUtilizationRate'),
]); ]);
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
@ -165,13 +160,8 @@ export const initReserveInstruction = (
}; };
export const calculateUtilizationRatio = (reserve: LendingReserve) => { export const calculateUtilizationRatio = (reserve: LendingReserve) => {
let borrowedLiquidity = wadToLamports( let borrowedLiquidity = wadToLamports(reserve.borrowedLiquidityWad).toNumber();
reserve.borrowedLiquidityWad return borrowedLiquidity / (reserve.availableLiquidity.toNumber() + borrowedLiquidity);
).toNumber();
return (
borrowedLiquidity /
(reserve.availableLiquidity.toNumber() + borrowedLiquidity)
);
}; };
export const reserveMarketCap = (reserve?: LendingReserve) => { export const reserveMarketCap = (reserve?: LendingReserve) => {
@ -183,29 +173,15 @@ export const reserveMarketCap = (reserve?: LendingReserve) => {
}; };
export const collateralExchangeRate = (reserve?: LendingReserve) => { export const collateralExchangeRate = (reserve?: LendingReserve) => {
return ( return (reserve?.collateralMintSupply.toNumber() || 1) / reserveMarketCap(reserve);
(reserve?.collateralMintSupply.toNumber() || 1) / reserveMarketCap(reserve)
);
}; };
export const collateralToLiquidity = ( export const collateralToLiquidity = (collateralAmount: BN | number, reserve?: LendingReserve) => {
collateralAmount: BN | number, const amount = typeof collateralAmount === 'number' ? collateralAmount : collateralAmount.toNumber();
reserve?: LendingReserve
) => {
const amount =
typeof collateralAmount === "number"
? collateralAmount
: collateralAmount.toNumber();
return Math.floor(amount / collateralExchangeRate(reserve)); return Math.floor(amount / collateralExchangeRate(reserve));
}; };
export const liquidityToCollateral = ( export const liquidityToCollateral = (liquidityAmount: BN | number, reserve?: LendingReserve) => {
liquidityAmount: BN | number, const amount = typeof liquidityAmount === 'number' ? liquidityAmount : liquidityAmount.toNumber();
reserve?: LendingReserve
) => {
const amount =
typeof liquidityAmount === "number"
? liquidityAmount
: liquidityAmount.toNumber();
return Math.floor(amount * collateralExchangeRate(reserve)); return Math.floor(amount * collateralExchangeRate(reserve));
}; };

View File

@ -1,13 +1,9 @@
import { import { PublicKey, SYSVAR_CLOCK_PUBKEY, TransactionInstruction } from '@solana/web3.js';
PublicKey, import BN from 'bn.js';
SYSVAR_CLOCK_PUBKEY, import * as BufferLayout from 'buffer-layout';
TransactionInstruction, import { TOKEN_PROGRAM_ID, LENDING_PROGRAM_ID } from '../../utils/ids';
} from "@solana/web3.js"; import * as Layout from './../../utils/layout';
import BN from "bn.js"; import { LendingInstruction } from './lending';
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";
export const withdrawInstruction = ( export const withdrawInstruction = (
collateralAmount: number | BN, collateralAmount: number | BN,
@ -18,10 +14,7 @@ export const withdrawInstruction = (
reserveSupply: PublicKey, reserveSupply: PublicKey,
authority: PublicKey authority: PublicKey
): TransactionInstruction => { ): TransactionInstruction => {
const dataLayout = BufferLayout.struct([ const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction'), Layout.uint64('collateralAmount')]);
BufferLayout.u8("instruction"),
Layout.uint64("collateralAmount"),
]);
const data = Buffer.alloc(dataLayout.span); const data = Buffer.alloc(dataLayout.span);
dataLayout.encode( 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 { HashRouter, Route, Switch } from 'react-router-dom';
import React from "react"; import React from 'react';
import { WalletProvider } from "./contexts/wallet"; import { WalletProvider } from './contexts/wallet';
import { ConnectionProvider } from "./contexts/connection"; import { ConnectionProvider } from './contexts/connection';
import { AccountsProvider } from "./contexts/accounts"; import { AccountsProvider } from './contexts/accounts';
import { MarketProvider } from "./contexts/market"; import { MarketProvider } from './contexts/market';
import { LendingProvider } from "./contexts/lending"; import { LendingProvider } from './contexts/lending';
import { AppLayout } from "./components/Layout"; import { AppLayout } from './components/Layout';
import { import {
BorrowReserveView, BorrowReserveView,
@ -20,12 +20,14 @@ import {
WithdrawView, WithdrawView,
LiquidateView, LiquidateView,
LiquidateReserveView, LiquidateReserveView,
} from "./views"; MarginTrading,
} from './views';
import { NewPosition } from './views/marginTrading/newPosition';
export function Routes() { export function Routes() {
return ( return (
<> <>
<HashRouter basename={"/"}> <HashRouter basename={'/'}>
<ConnectionProvider> <ConnectionProvider>
<WalletProvider> <WalletProvider>
<AccountsProvider> <AccountsProvider>
@ -33,46 +35,22 @@ export function Routes() {
<LendingProvider> <LendingProvider>
<AppLayout> <AppLayout>
<Switch> <Switch>
<Route exact path="/" component={() => <HomeView />} /> <Route exact path='/' component={() => <HomeView />} />
<Route <Route exact path='/dashboard' children={<DashboardView />} />
exact <Route path='/reserve/:id' children={<ReserveView />} />
path="/dashboard" <Route exact path='/deposit' component={() => <DepositView />} />
children={<DashboardView />} <Route path='/deposit/:id' children={<DepositReserveView />} />
/> <Route path='/withdraw/:id' children={<WithdrawView />} />
<Route path="/reserve/:id" children={<ReserveView />} /> <Route exact path='/borrow' children={<BorrowView />} />
<Route <Route path='/borrow/:id' children={<BorrowReserveView />} />
exact <Route path='/repay/loan/:obligation' children={<RepayReserveView />} />
path="/deposit" <Route path='/repay/:reserve' children={<RepayReserveView />} />
component={() => <DepositView />} <Route exact path='/liquidate' children={<LiquidateView />} />
/> <Route path='/liquidate/:id' children={<LiquidateReserveView />} />
<Route <Route exact path='/marginTrading' children={<MarginTrading />} />
path="/deposit/:id"
children={<DepositReserveView />} <Route path='/marginTrading/:id' children={<NewPosition />} />
/> <Route exact path='/faucet' children={<FaucetView />} />
<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 />} />
</Switch> </Switch>
</AppLayout> </AppLayout>
</LendingProvider> </LendingProvider>

View File

@ -1,7 +1,7 @@
import { EventEmitter as Emitter } from "eventemitter3"; import { EventEmitter as Emitter } from 'eventemitter3';
export class CacheUpdateEvent { export class CacheUpdateEvent {
static type = "CacheUpdate"; static type = 'CacheUpdate';
id: string; id: string;
parser: any; parser: any;
isNew: boolean; 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 { export class MarketUpdateEvent {
static type = "MarketUpdate"; static type = 'MarketUpdate';
ids: Set<string>; ids: Set<string>;
constructor(ids: Set<string>) { constructor(ids: Set<string>) {
this.ids = ids; this.ids = ids;
@ -42,4 +50,8 @@ export class EventEmitter {
raiseCacheUpdated(id: string, isNew: boolean, parser: any) { raiseCacheUpdated(id: string, isNew: boolean, parser: any) {
this.emitter.emit(CacheUpdateEvent.type, new CacheUpdateEvent(id, isNew, parser)); this.emitter.emit(CacheUpdateEvent.type, new CacheUpdateEvent(id, isNew, parser));
} }
raiseCacheDeleted(id: string) {
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 { useCallback, useState } from 'react';
import { MintInfo } from "@solana/spl-token"; import { MintInfo } from '@solana/spl-token';
import { TokenAccount } from "./../models"; import { PoolInfo, TokenAccount } from './../models';
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from '@solana/web3.js';
import BN from "bn.js"; import BN from 'bn.js';
import { WAD, ZERO } from "../constants"; import { WAD, ZERO } from '../constants';
export interface KnownToken { export interface KnownToken {
tokenSymbol: string; tokenSymbol: string;
@ -15,6 +15,12 @@ export interface KnownToken {
export type KnownTokenMap = Map<string, 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) { export function useLocalStorageState(key: string, defaultState?: string) {
const [state, setState] = useState(() => { const [state, setState] = useState(() => {
// NOTE: Not sure if this is ok // 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)}`; return `${address.slice(0, chars)}...${address.slice(-chars)}`;
} }
export function getTokenName( export function getTokenName(map: KnownTokenMap, mint?: string | PublicKey, shorten = true): string {
map: KnownTokenMap,
mint?: string | PublicKey,
shorten = true
): string {
const mintAddress = typeof mint === 'string' ? mint : mint?.toBase58(); const mintAddress = typeof mint === 'string' ? mint : mint?.toBase58();
if (!mintAddress) { if (!mintAddress) {
return "N/A"; return 'N/A';
} }
const knownSymbol = map.get(mintAddress)?.tokenSymbol; const knownSymbol = map.get(mintAddress)?.tokenSymbol;
@ -68,12 +70,8 @@ export function getTokenName(
return shorten ? `${mintAddress.substring(0, 5)}...` : mintAddress; return shorten ? `${mintAddress.substring(0, 5)}...` : mintAddress;
} }
export function getTokenIcon( export function getTokenIcon(map: KnownTokenMap, mintAddress?: string | PublicKey): string | undefined {
map: KnownTokenMap, const address = typeof mintAddress === 'string' ? mintAddress : mintAddress?.toBase58();
mintAddress?: string | PublicKey
): string | undefined {
const address =
typeof mintAddress === "string" ? mintAddress : mintAddress?.toBase58();
if (!address) { if (!address) {
return; return;
} }
@ -85,25 +83,20 @@ export function isKnownMint(map: KnownTokenMap, mintAddress: string) {
return !!map.get(mintAddress); 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[][] { export function chunks<T>(array: T[], size: number): T[][] {
return Array.apply<number, T[], T[][]>( return Array.apply<number, T[], T[][]>(0, new Array(Math.ceil(array.length / size))).map((_, index) =>
0, array.slice(index * size, (index + 1) * size)
new Array(Math.ceil(array.length / size)) );
).map((_, index) => array.slice(index * size, (index + 1) * size));
} }
export function toLamports( export function toLamports(account?: TokenAccount | number, mint?: MintInfo): number {
account?: TokenAccount | number,
mint?: MintInfo
): number {
if (!account) { if (!account) {
return 0; return 0;
} }
const amount = const amount = typeof account === 'number' ? account : account.info.amount?.toNumber();
typeof account === "number" ? account : account.info.amount?.toNumber();
const precision = Math.pow(10, mint?.decimals || 0); const precision = Math.pow(10, mint?.decimals || 0);
return Math.floor(amount * precision); return Math.floor(amount * precision);
@ -113,28 +106,20 @@ export function wadToLamports(amount?: BN): BN {
return amount?.div(WAD) || ZERO; return amount?.div(WAD) || ZERO;
} }
export function fromLamports( export function fromLamports(account?: TokenAccount | number | BN, mint?: MintInfo, rate: number = 1.0): number {
account?: TokenAccount | number | BN,
mint?: MintInfo,
rate: number = 1.0
): number {
if (!account) { if (!account) {
return 0; return 0;
} }
const amount = Math.floor( const amount = Math.floor(
typeof account === "number" typeof account === 'number' ? account : BN.isBN(account) ? account.toNumber() : account.info.amount.toNumber()
? account
: BN.isBN(account)
? account.toNumber()
: account.info.amount.toNumber()
); );
const precision = Math.pow(10, mint?.decimals || 0); const precision = Math.pow(10, mint?.decimals || 0);
return (amount / precision) * rate; 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) => { const abbreviateNumber = (number: number, precision: number) => {
let tier = (Math.log10(number) / 3) | 0; let tier = (Math.log10(number) / 3) | 0;
@ -155,13 +140,13 @@ export function formatTokenAmount(
account?: TokenAccount, account?: TokenAccount,
mint?: MintInfo, mint?: MintInfo,
rate: number = 1.0, rate: number = 1.0,
prefix = "", prefix = '',
suffix = "", suffix = '',
precision = 6, precision = 6,
abbr = false abbr = false
): string { ): string {
if (!account) { if (!account) {
return ""; return '';
} }
return `${[prefix]}${formatAmount( return `${[prefix]}${formatAmount(
@ -171,13 +156,13 @@ export function formatTokenAmount(
)}${suffix}`; )}${suffix}`;
} }
export const formatUSD = new Intl.NumberFormat("en-US", { export const formatUSD = new Intl.NumberFormat('en-US', {
style: "currency", style: 'currency',
currency: "USD", currency: 'USD',
}); });
const numberFormater = new Intl.NumberFormat("en-US", { const numberFormater = new Intl.NumberFormat('en-US', {
style: "decimal", style: 'decimal',
minimumFractionDigits: 2, minimumFractionDigits: 2,
maximumFractionDigits: 2, maximumFractionDigits: 2,
}); });
@ -185,15 +170,33 @@ const numberFormater = new Intl.NumberFormat("en-US", {
export const formatNumber = { export const formatNumber = {
format: (val?: number) => { format: (val?: number) => {
if (!val) { if (!val) {
return "--"; return '--';
} }
return numberFormater.format(val); return numberFormater.format(val);
}, },
}; };
export const formatPct = new Intl.NumberFormat("en-US", { export const formatPct = new Intl.NumberFormat('en-US', {
style: "percent", style: 'percent',
minimumFractionDigits: 2, minimumFractionDigits: 2,
maximumFractionDigits: 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 React from 'react';
import { useTokenName, useBorrowingPower } from "../../hooks"; import { useTokenName, useBorrowingPower } from '../../hooks';
import { calculateBorrowAPY, LendingReserve } from "../../models/lending"; import { calculateBorrowAPY, LendingReserve } from '../../models/lending';
import { TokenIcon } from "../../components/TokenIcon"; import { TokenIcon } from '../../components/TokenIcon';
import { formatNumber, formatPct } from "../../utils/utils"; import { formatNumber, formatPct } from '../../utils/utils';
import { Button } from "antd"; import { Button } from 'antd';
import { Link } from "react-router-dom"; import { Link } from 'react-router-dom';
import { PublicKey } from "@solana/web3.js"; import { PublicKey } from '@solana/web3.js';
import { LABELS } from "../../constants"; import { LABELS } from '../../constants';
import { useMidPriceInUSD } from "../../contexts/market"; import { useMidPriceInUSD } from '../../contexts/market';
export const BorrowItem = (props: { export const BorrowItem = (props: { reserve: LendingReserve; address: PublicKey }) => {
reserve: LendingReserve;
address: PublicKey;
}) => {
const name = useTokenName(props.reserve.liquidityMint); const name = useTokenName(props.reserve.liquidityMint);
const price = useMidPriceInUSD(props.reserve.liquidityMint.toBase58()).price; 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); const apr = calculateBorrowAPY(props.reserve);
return ( return (
<Link to={`/borrow/${props.address.toBase58()}`}> <Link to={`/borrow/${props.address.toBase58()}`}>
<div className="borrow-item"> <div className='borrow-item'>
<span style={{ display: "flex" }}> <span style={{ display: 'flex' }}>
<TokenIcon mintAddress={props.reserve.liquidityMint} /> <TokenIcon mintAddress={props.reserve.liquidityMint} />
{name} {name}
</span> </span>
<div>${formatNumber.format(price)}</div> <div>${formatNumber.format(price)}</div>
<div> <div>
<div> <div>
<div><em>{formatNumber.format(borrowingPower)}</em> {name}</div> <div>
<div className="dashboard-amount-quote">${formatNumber.format(totalInQuote)}</div> <em>{formatNumber.format(borrowingPower)}</em> {name}
</div>
<div className='dashboard-amount-quote'>${formatNumber.format(totalInQuote)}</div>
</div> </div>
</div> </div>
<div>{formatPct.format(apr)}</div> <div>{formatPct.format(apr)}</div>
<div> <div>
<Button type="primary"> <Button type='primary'>
<span>{LABELS.BORROW_ACTION}</span> <span>{LABELS.BORROW_ACTION}</span>
</Button> </Button>
</div> </div>

View File

@ -10,3 +10,4 @@ export { FaucetView } from "./faucet";
export { RepayReserveView } from "./repayReserve"; export { RepayReserveView } from "./repayReserve";
export { LiquidateView } from "./liquidate"; export { LiquidateView } from "./liquidate";
export { LiquidateReserveView } from "./liquidateReserve"; 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;
}