mirror of https://github.com/certusone/oyster.git
Merge pull request #15 from dummytester123/main
adding Margin Trading Scaffolding
This commit is contained in:
commit
530397afdb
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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}`,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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}`,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 ? '<' : ''}
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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%' }} />;
|
||||||
|
};
|
|
@ -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'>
|
||||||
|
{hasBalance ? (balance < 0.001 ? '<0.001' : balance.toFixed(3)) : '-'}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,4 +1,3 @@
|
||||||
export * from "./ids";
|
|
||||||
export * from "./labels";
|
export * from "./labels";
|
||||||
export * from "./math";
|
export * from "./math";
|
||||||
export * from "./marks";
|
export * from "./marks";
|
||||||
|
|
|
@ -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',
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
|
|
@ -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)})`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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[] = [];
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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));
|
||||||
};
|
};
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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,
|
||||||
|
});
|
||||||
|
};
|
|
@ -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>
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
File diff suppressed because it is too large
Load Diff
|
@ -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('/');
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
|
@ -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]);
|
||||||
|
}
|
|
@ -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%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue