Merge remote-tracking branch 'origin/deploy' into dev
This commit is contained in:
commit
b2e578bc61
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@blockworks-foundation/mango-v4",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.14",
|
||||
"description": "Typescript Client for mango-v4 program.",
|
||||
"repository": "https://github.com/blockworks-foundation/mango-v4",
|
||||
"author": {
|
||||
|
|
|
@ -1,107 +1,106 @@
|
|||
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { TokenIndex } from '../../src/accounts/bank';
|
||||
import { MangoClient } from '../../src/client';
|
||||
import { MANGO_V4_ID } from '../../src/constants';
|
||||
|
||||
const USER_KEYPAIR =
|
||||
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
|
||||
const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK || '';
|
||||
const CLUSTER: Cluster =
|
||||
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
|
||||
const CLUSTER_URL =
|
||||
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
|
||||
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(process.env.CLUSTER_URL!, options);
|
||||
const connection = new Connection(CLUSTER_URL!, options);
|
||||
|
||||
const user = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(fs.readFileSync(process.env.USER_KEYPAIR!, 'utf-8')),
|
||||
),
|
||||
Buffer.from(JSON.parse(fs.readFileSync(USER_KEYPAIR!, 'utf-8'))),
|
||||
);
|
||||
const userWallet = new Wallet(user);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
|
||||
//
|
||||
// mainnet
|
||||
//
|
||||
|
||||
const client = await MangoClient.connect(
|
||||
userProvider,
|
||||
'mainnet-beta',
|
||||
MANGO_V4_ID['mainnet-beta'],
|
||||
CLUSTER,
|
||||
MANGO_V4_ID[CLUSTER],
|
||||
{
|
||||
idsSource: 'get-program-accounts',
|
||||
},
|
||||
);
|
||||
|
||||
const group = await client.getGroup(
|
||||
new PublicKey('78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX'),
|
||||
);
|
||||
console.log(
|
||||
await client.getMangoAccountForOwner(
|
||||
group,
|
||||
new PublicKey('v3mmtZ8JjXkaAbRRMBiNsjJF1rnN3qsMQqRLMk7Nz2C'),
|
||||
3,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
await client.getMangoAccountsForDelegate(
|
||||
group,
|
||||
new PublicKey('5P9rHX22jb3MDq46VgeaHZ2TxQDKezPxsxNX3MaXyHwT'),
|
||||
),
|
||||
|
||||
let account = await client.getMangoAccount(new PublicKey(MANGO_ACCOUNT_PK));
|
||||
await Promise.all(
|
||||
account.tokenConditionalSwaps.map((tcs, i) => {
|
||||
if (!tcs.hasData) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
client.tokenConditionalSwapCancel(group, account, tcs.id);
|
||||
}),
|
||||
);
|
||||
|
||||
//
|
||||
// devnet
|
||||
//
|
||||
|
||||
// const client = await MangoClient.connect(
|
||||
// userProvider,
|
||||
// 'devnet',
|
||||
// MANGO_V4_ID['devnet'],
|
||||
// {
|
||||
// idsSource: 'get-program-accounts',
|
||||
// },
|
||||
// const sig = await client.tcsTakeProfitOnDeposit(
|
||||
// group,
|
||||
// account,
|
||||
// group.getFirstBankByTokenIndex(4 as TokenIndex),
|
||||
// group.getFirstBankByTokenIndex(0 as TokenIndex),
|
||||
// group.getFirstBankByTokenIndex(4 as TokenIndex).uiPrice + 1,
|
||||
// false,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// );
|
||||
|
||||
// const admin = Keypair.fromSecretKey(
|
||||
// Buffer.from(
|
||||
// JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
|
||||
// ),
|
||||
// const sig = await client.tcsStopLossOnDeposit(
|
||||
// group,
|
||||
// account,
|
||||
// group.getFirstBankByTokenIndex(4 as TokenIndex),
|
||||
// group.getFirstBankByTokenIndex(0 as TokenIndex),
|
||||
// group.getFirstBankByTokenIndex(4 as TokenIndex).uiPrice - 1,
|
||||
// false,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// );
|
||||
// const group = await client.getGroupForCreator(admin.publicKey, 23);
|
||||
// const mangoAccount = (await client.getMangoAccountForOwner(
|
||||
// group,
|
||||
// user.publicKey,
|
||||
// 0,
|
||||
// )) as MangoAccount;
|
||||
// console.log(mangoAccount.tokenConditionalSwaps.length);
|
||||
// console.log(mangoAccount.tokenConditionalSwaps);
|
||||
// console.log(mangoAccount.tokenConditionalSwaps[1]);
|
||||
// console.log(mangoAccount.tokenConditionalSwaps[0]);
|
||||
|
||||
// let sig = await client.accountExpandV2(
|
||||
// const sig = await client.tcsTakeProfitOnBorrow(
|
||||
// group,
|
||||
// mangoAccount,
|
||||
// 16,
|
||||
// 8,
|
||||
// 8,
|
||||
// 32,
|
||||
// 8,
|
||||
// );
|
||||
// console.log(sig);
|
||||
// mangoAccount = await client.getOrCreateMangoAccount(group);
|
||||
|
||||
// let sig = await client.tokenConditionalSwapCreate(
|
||||
// group,
|
||||
// mangoAccount,
|
||||
// 0 as TokenIndex,
|
||||
// 1 as TokenIndex,
|
||||
// 0,
|
||||
// 73,
|
||||
// 81,
|
||||
// TokenConditionalSwapPriceThresholdType.priceOverThreshold,
|
||||
// 99,
|
||||
// 101,
|
||||
// true,
|
||||
// account,
|
||||
// group.getFirstBankByTokenIndex(0 as TokenIndex),
|
||||
// group.getFirstBankByTokenIndex(4 as TokenIndex),
|
||||
// group.getFirstBankByTokenIndex(4 as TokenIndex).uiPrice - 1,
|
||||
// true,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// );
|
||||
// console.log(sig);
|
||||
|
||||
const sig = await client.tcsStopLossOnBorrow(
|
||||
group,
|
||||
account,
|
||||
group.getFirstBankByTokenIndex(0 as TokenIndex),
|
||||
group.getFirstBankByTokenIndex(4 as TokenIndex),
|
||||
group.getFirstBankByTokenIndex(4 as TokenIndex).uiPrice + 1,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
|
||||
console.log(sig);
|
||||
|
||||
account = await client.getMangoAccount(new PublicKey(MANGO_ACCOUNT_PK));
|
||||
console.log(account.tokenConditionalSwaps[0].toString(group));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { AnchorProvider, Wallet } from '@coral-xyz/anchor';
|
||||
import { Connection, Keypair } from '@solana/web3.js';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { HealthType, MangoAccount } from '../../src/accounts/mangoAccount';
|
||||
import {
|
||||
|
@ -33,7 +33,9 @@ async function main() {
|
|||
);
|
||||
console.log(`Admin ${admin.publicKey.toBase58()}`);
|
||||
|
||||
const group = await client.getGroupForCreator(admin.publicKey, 2);
|
||||
const group = await client.getGroup(
|
||||
new PublicKey('78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX'),
|
||||
);
|
||||
console.log(`${group.toString()}`);
|
||||
|
||||
// create + fetch account
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
import {
|
||||
createApproveInstruction,
|
||||
createCloseAccountInstruction,
|
||||
createSyncNativeInstruction,
|
||||
createTransferInstruction,
|
||||
getAccount,
|
||||
getAssociatedTokenAddress,
|
||||
NATIVE_MINT,
|
||||
} from '@solana/spl-token';
|
||||
import {
|
||||
Connection,
|
||||
Keypair,
|
||||
sendAndConfirmTransaction,
|
||||
SystemProgram,
|
||||
Transaction,
|
||||
} from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
let sig;
|
||||
const conn = new Connection(process.env.MB_CLUSTER_URL!);
|
||||
|
||||
// load wallet 1
|
||||
const w1 = Keypair.fromSecretKey(
|
||||
Buffer.from(JSON.parse(fs.readFileSync(process.env.wallet1!, 'utf-8'))),
|
||||
);
|
||||
|
||||
// load wallet 2
|
||||
const w2 = Keypair.fromSecretKey(
|
||||
Buffer.from(JSON.parse(fs.readFileSync(process.env.wallet2!, 'utf-8'))),
|
||||
);
|
||||
|
||||
const w1WsolTA = await getAssociatedTokenAddress(NATIVE_MINT, w1.publicKey);
|
||||
// const ataTransaction1 = new Transaction().add(
|
||||
// createAssociatedTokenAccountInstruction(
|
||||
// w1.publicKey,
|
||||
// w1WsolTA,
|
||||
// w1.publicKey,
|
||||
// NATIVE_MINT,
|
||||
// ),
|
||||
// );
|
||||
// await sendAndConfirmTransaction(conn, ataTransaction1, [w1]);
|
||||
|
||||
const w2WsolTA = await getAssociatedTokenAddress(NATIVE_MINT, w2.publicKey);
|
||||
// const ataTransaction2 = new Transaction().add(
|
||||
// createAssociatedTokenAccountInstruction(
|
||||
// w2.publicKey,
|
||||
// w2WsolTA,
|
||||
// w2.publicKey,
|
||||
// NATIVE_MINT,
|
||||
// ),
|
||||
// );
|
||||
// await sendAndConfirmTransaction(conn, ataTransaction2, [w2]);
|
||||
|
||||
// wallet 1 wrap sol to wsol
|
||||
const solTransferTransaction = new Transaction().add(
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: w1.publicKey,
|
||||
toPubkey: w1WsolTA,
|
||||
lamports: 1,
|
||||
}),
|
||||
createSyncNativeInstruction(w1WsolTA),
|
||||
);
|
||||
sig = await sendAndConfirmTransaction(conn, solTransferTransaction, [w1]);
|
||||
console.log(
|
||||
`sig w1 wrapped some sol https://explorer.solana.com/tx/${sig}`,
|
||||
);
|
||||
|
||||
// wallet 1 approve wallet 2 for some wsol
|
||||
const tokenApproveTx = new Transaction().add(
|
||||
createApproveInstruction(w1WsolTA, w2.publicKey, w1.publicKey, 1),
|
||||
);
|
||||
sig = await sendAndConfirmTransaction(conn, tokenApproveTx, [w1]);
|
||||
console.log(
|
||||
`sig w1 token approve w2 https://explorer.solana.com/tx/${sig}`,
|
||||
);
|
||||
|
||||
// log delegate amount
|
||||
let w2WsolAtaInfo = await getAccount(conn, w1WsolTA);
|
||||
console.log(
|
||||
`- delegate ${w2WsolAtaInfo.delegate}, amount ${w2WsolAtaInfo.delegatedAmount}`,
|
||||
);
|
||||
|
||||
// wallet 2 transfer wsol from wallet 1 to wallet 2
|
||||
const tokenTransferTx = new Transaction().add(
|
||||
createTransferInstruction(w1WsolTA, w2WsolTA, w2.publicKey, 1),
|
||||
);
|
||||
sig = await sendAndConfirmTransaction(conn, tokenTransferTx, [w2], {
|
||||
skipPreflight: true,
|
||||
});
|
||||
console.log(
|
||||
`sig w1 transfer wsol to w2 https://explorer.solana.com/tx/${sig}`,
|
||||
);
|
||||
|
||||
// log delegate amount
|
||||
w2WsolAtaInfo = await getAccount(conn, w1WsolTA, 'finalized');
|
||||
console.log(
|
||||
`- delegate ${w2WsolAtaInfo.delegate}, amount ${w2WsolAtaInfo.delegatedAmount}`,
|
||||
);
|
||||
|
||||
// wallet 2 unwrap all wsol
|
||||
const closeAtaIx = new Transaction().add(
|
||||
createCloseAccountInstruction(w2WsolTA, w2.publicKey, w2.publicKey),
|
||||
);
|
||||
sig = await sendAndConfirmTransaction(conn, closeAtaIx, [w2], {
|
||||
skipPreflight: true,
|
||||
});
|
||||
console.log(`sig w2 unwrap wsol https://explorer.solana.com/tx/${sig}`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
|
@ -1,6 +1,5 @@
|
|||
import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor';
|
||||
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import { assert } from 'console';
|
||||
import fs from 'fs';
|
||||
import { Bank } from '../../src/accounts/bank';
|
||||
import { MangoAccount } from '../../src/accounts/mangoAccount';
|
||||
|
|
|
@ -175,7 +175,7 @@ async function main() {
|
|||
accounts2.find((account) => account.name == 'LIQTEST, LIQEE1'),
|
||||
);
|
||||
await client.accountExpandV2(group, account, 4, 4, 4, 4, 4);
|
||||
await client.tokenConditionalSwapCreate(
|
||||
await client.tokenConditionalSwapCreateRaw(
|
||||
group,
|
||||
account,
|
||||
MINTS.get('SOL')!,
|
||||
|
@ -199,7 +199,7 @@ async function main() {
|
|||
accounts2.find((account) => account.name == 'LIQTEST, LIQEE2'),
|
||||
);
|
||||
await client.accountExpandV2(group, account, 4, 4, 4, 4, 4);
|
||||
await client.tokenConditionalSwapCreate(
|
||||
await client.tokenConditionalSwapCreateRaw(
|
||||
group,
|
||||
account,
|
||||
MINTS.get('SOL')!,
|
||||
|
@ -223,7 +223,7 @@ async function main() {
|
|||
accounts2.find((account) => account.name == 'LIQTEST, LIQEE3'),
|
||||
);
|
||||
await client.accountExpandV2(group, account, 4, 4, 4, 4, 4);
|
||||
await client.tokenConditionalSwapCreate(
|
||||
await client.tokenConditionalSwapCreateRaw(
|
||||
group,
|
||||
account,
|
||||
MINTS.get('SOL')!,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { BN } from '@coral-xyz/anchor';
|
||||
import { utf8 } from '@coral-xyz/anchor/dist/cjs/utils/bytes';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import { I80F48, I80F48Dto, ZERO_I80F48 } from '../numbers/I80F48';
|
||||
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
|
||||
import { As, toUiDecimals } from '../utils';
|
||||
import { OracleProvider } from './oracle';
|
||||
|
||||
|
@ -210,8 +210,8 @@ export class Bank implements BankForHealth {
|
|||
initLiabWeight: I80F48Dto,
|
||||
liquidationFee: I80F48Dto,
|
||||
dust: I80F48Dto,
|
||||
flashLoanTokenAccountInitial: BN,
|
||||
flashLoanApprovedAmount: BN,
|
||||
public flashLoanTokenAccountInitial: BN,
|
||||
public flashLoanApprovedAmount: BN,
|
||||
public tokenIndex: TokenIndex,
|
||||
public mintDecimals: number,
|
||||
public bankNum: number,
|
||||
|
@ -364,6 +364,14 @@ export class Bank implements BankForHealth {
|
|||
);
|
||||
}
|
||||
|
||||
getAssetPrice(): I80F48 {
|
||||
return this.price.min(I80F48.fromNumber(this.stablePriceModel.stablePrice));
|
||||
}
|
||||
|
||||
getLiabPrice(): I80F48 {
|
||||
return this.price.max(I80F48.fromNumber(this.stablePriceModel.stablePrice));
|
||||
}
|
||||
|
||||
get price(): I80F48 {
|
||||
if (this._price === undefined) {
|
||||
throw new Error(
|
||||
|
@ -409,17 +417,11 @@ export class Bank implements BankForHealth {
|
|||
}
|
||||
|
||||
uiDeposits(): number {
|
||||
return toUiDecimals(
|
||||
this.indexedDeposits.mul(this.depositIndex),
|
||||
this.mintDecimals,
|
||||
);
|
||||
return toUiDecimals(this.nativeDeposits(), this.mintDecimals);
|
||||
}
|
||||
|
||||
uiBorrows(): number {
|
||||
return toUiDecimals(
|
||||
this.indexedBorrows.mul(this.borrowIndex),
|
||||
this.mintDecimals,
|
||||
);
|
||||
return toUiDecimals(this.nativeBorrows(), this.mintDecimals);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -493,6 +495,48 @@ export class Bank implements BankForHealth {
|
|||
getDepositRateUi(): number {
|
||||
return this.getDepositRate().toNumber() * 100;
|
||||
}
|
||||
|
||||
getNetBorrowLimitPerWindow(): I80F48 {
|
||||
return I80F48.fromI64(this.netBorrowLimitPerWindowQuote).div(this.price);
|
||||
}
|
||||
|
||||
getBorrowLimitLeftInWindow(): I80F48 {
|
||||
return this.getNetBorrowLimitPerWindow()
|
||||
.sub(I80F48.fromI64(this.netBorrowsInWindow))
|
||||
.max(ZERO_I80F48());
|
||||
}
|
||||
|
||||
getNetBorrowLimitPerWindowUi(): number {
|
||||
return toUiDecimals(this.getNetBorrowLimitPerWindow(), this.mintDecimals);
|
||||
}
|
||||
|
||||
getMaxWithdraw(vaultBalance: BN, userDeposits = ZERO_I80F48()): I80F48 {
|
||||
userDeposits = userDeposits.max(ZERO_I80F48());
|
||||
|
||||
// any borrow must respect the minVaultToDepositsRatio
|
||||
const minVaultBalanceRequired = this.nativeDeposits().mul(
|
||||
I80F48.fromNumber(this.minVaultToDepositsRatio),
|
||||
);
|
||||
const maxBorrowFromVault = I80F48.fromI64(vaultBalance)
|
||||
.sub(minVaultBalanceRequired)
|
||||
.max(ZERO_I80F48());
|
||||
// User deposits can exceed maxWithdrawFromVault
|
||||
let maxBorrow = maxBorrowFromVault.sub(userDeposits).max(ZERO_I80F48());
|
||||
// any borrow must respect the limit left in window
|
||||
maxBorrow = maxBorrow.min(this.getBorrowLimitLeftInWindow());
|
||||
// borrows would be applied a fee
|
||||
maxBorrow = maxBorrow.div(ONE_I80F48().add(this.loanOriginationFeeRate));
|
||||
|
||||
// user deposits can always be withdrawn
|
||||
// even if vaults can be depleted
|
||||
return maxBorrow.add(userDeposits).min(I80F48.fromI64(vaultBalance));
|
||||
}
|
||||
|
||||
getTimeToNextBorrowLimitWindowStartsTs(): number {
|
||||
return this.netBorrowLimitWindowSizeTs
|
||||
.sub(new BN(Date.now() / 1000).sub(this.lastNetBorrowsWindowStartTs))
|
||||
.toNumber();
|
||||
}
|
||||
}
|
||||
|
||||
export class MintInfo {
|
||||
|
|
|
@ -14,7 +14,8 @@ import { MangoClient } from '../client';
|
|||
import { OPENBOOK_PROGRAM_ID } from '../constants';
|
||||
import { Id } from '../ids';
|
||||
import { I80F48, ONE_I80F48 } from '../numbers/I80F48';
|
||||
import { toNative, toNativeI80F48, toUiDecimals } from '../utils';
|
||||
import { PriceImpact, computePriceImpactOnJup } from '../risk';
|
||||
import { buildFetch, toNative, toNativeI80F48, toUiDecimals } from '../utils';
|
||||
import { Bank, MintInfo, TokenIndex } from './bank';
|
||||
import {
|
||||
OracleProvider,
|
||||
|
@ -80,6 +81,7 @@ export class Group {
|
|||
new Map(), // mintInfosMapByTokenIndex
|
||||
new Map(), // mintInfosMapByMint
|
||||
new Map(), // vaultAmountsMap
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -115,6 +117,7 @@ export class Group {
|
|||
public mintInfosMapByTokenIndex: Map<TokenIndex, MintInfo>,
|
||||
public mintInfosMapByMint: Map<string, MintInfo>,
|
||||
public vaultAmountsMap: Map<string, BN>,
|
||||
public pis: PriceImpact[],
|
||||
) {}
|
||||
|
||||
public async reloadAll(client: MangoClient): Promise<void> {
|
||||
|
@ -122,6 +125,7 @@ export class Group {
|
|||
|
||||
// console.time('group.reload');
|
||||
await Promise.all([
|
||||
this.reloadPriceImpactData(),
|
||||
this.reloadAlts(client),
|
||||
this.reloadBanks(client, ids).then(() =>
|
||||
Promise.all([
|
||||
|
@ -140,6 +144,27 @@ export class Group {
|
|||
// console.timeEnd('group.reload');
|
||||
}
|
||||
|
||||
public async reloadPriceImpactData(): Promise<void> {
|
||||
try {
|
||||
this.pis = await (
|
||||
await (
|
||||
await buildFetch()
|
||||
)(
|
||||
`https://api.mngo.cloud/data/v4/risk/listed-tokens-one-week-price-impacts`,
|
||||
{
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
},
|
||||
)
|
||||
).json();
|
||||
} catch (error) {
|
||||
console.log(`Error while loading price impact: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async reloadAlts(client: MangoClient): Promise<void> {
|
||||
const alts = await Promise.all(
|
||||
this.addressLookupTables
|
||||
|
@ -480,6 +505,19 @@ export class Group {
|
|||
return banks[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a price impact in percentage, between 0 to 100 for a token,
|
||||
* returns -1 if data is bad
|
||||
*/
|
||||
public getPriceImpactByTokenIndex(
|
||||
tokenIndex: TokenIndex,
|
||||
usdcAmountUi: number,
|
||||
): number {
|
||||
const bank = this.getFirstBankByTokenIndex(tokenIndex);
|
||||
const pisBps = computePriceImpactOnJup(this.pis, usdcAmountUi, bank.name);
|
||||
return (pisBps * 100) / 10000;
|
||||
}
|
||||
|
||||
public getFirstBankForMngo(): Bank {
|
||||
return this.getFirstBankByTokenIndex(this.mngoTokenIndex);
|
||||
}
|
||||
|
@ -488,12 +526,7 @@ export class Group {
|
|||
return this.getFirstBankByTokenIndex(0 as TokenIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param mintPk
|
||||
* @returns sum of ui balances of vaults for all banks for a token
|
||||
*/
|
||||
public getTokenVaultBalanceByMintUi(mintPk: PublicKey): number {
|
||||
public getTokenVaultBalanceByMint(mintPk: PublicKey): BN {
|
||||
const banks = this.banksMapByMint.get(mintPk.toBase58());
|
||||
if (!banks) {
|
||||
throw new Error(`No bank found for mint ${mintPk}!`);
|
||||
|
@ -509,7 +542,19 @@ export class Group {
|
|||
totalAmount.iadd(amount);
|
||||
}
|
||||
|
||||
return toUiDecimals(totalAmount, this.getMintDecimals(mintPk));
|
||||
return totalAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param mintPk
|
||||
* @returns sum of ui balances of vaults for all banks for a token
|
||||
*/
|
||||
public getTokenVaultBalanceByMintUi(mintPk: PublicKey): number {
|
||||
return toUiDecimals(
|
||||
this.getTokenVaultBalanceByMint(mintPk),
|
||||
this.getMintDecimals(mintPk),
|
||||
);
|
||||
}
|
||||
|
||||
public getSerum3MarketByMarketIndex(marketIndex: MarketIndex): Serum3Market {
|
||||
|
|
|
@ -9,12 +9,16 @@ import {
|
|||
ONE_I80F48,
|
||||
ZERO_I80F48,
|
||||
} from '../numbers/I80F48';
|
||||
import { toNativeI80F48ForQuote } from '../utils';
|
||||
import {
|
||||
toNativeI80F48ForQuote,
|
||||
toUiDecimals,
|
||||
toUiDecimalsForQuote,
|
||||
} from '../utils';
|
||||
import { Bank, BankForHealth, TokenIndex } from './bank';
|
||||
import { Group } from './group';
|
||||
|
||||
import { HealthType, MangoAccount, PerpPosition } from './mangoAccount';
|
||||
import { PerpMarket, PerpOrderSide } from './perp';
|
||||
import { PerpMarket, PerpMarketIndex, PerpOrderSide } from './perp';
|
||||
import { MarketIndex, Serum3Market, Serum3Side } from './serum3';
|
||||
|
||||
// ░░░░
|
||||
|
@ -235,6 +239,48 @@ export class HealthCache {
|
|||
return tokenBalances;
|
||||
}
|
||||
|
||||
effectiveTokenBalancesInternalDisplay(
|
||||
group: Group,
|
||||
healthType: HealthType | undefined,
|
||||
ignoreNegativePerp: boolean,
|
||||
): TokenBalanceDisplay[] {
|
||||
const tokenBalances = new Array(this.tokenInfos.length)
|
||||
.fill(null)
|
||||
.map((ignored) => new TokenBalanceDisplay(ZERO_I80F48(), 0, []));
|
||||
|
||||
for (const perpInfo of this.perpInfos) {
|
||||
const settleTokenIndex = this.findTokenInfoIndex(
|
||||
perpInfo.settleTokenIndex,
|
||||
);
|
||||
const perpSettleToken = tokenBalances[settleTokenIndex];
|
||||
const healthUnsettled = perpInfo.healthUnsettledPnl(healthType);
|
||||
perpSettleToken.perpMarketContributions.push({
|
||||
market: group.getPerpMarketByMarketIndex(
|
||||
perpInfo.perpMarketIndex as PerpMarketIndex,
|
||||
).name,
|
||||
contributionUi: toUiDecimals(
|
||||
healthUnsettled,
|
||||
group.getMintDecimalsByTokenIndex(perpInfo.settleTokenIndex),
|
||||
),
|
||||
});
|
||||
if (!ignoreNegativePerp || healthUnsettled.gt(ZERO_I80F48())) {
|
||||
perpSettleToken.spotAndPerp.iadd(healthUnsettled);
|
||||
}
|
||||
}
|
||||
|
||||
for (const index of this.tokenInfos.keys()) {
|
||||
const tokenInfo = this.tokenInfos[index];
|
||||
const tokenBalance = tokenBalances[index];
|
||||
tokenBalance.spotAndPerp.iadd(tokenInfo.balanceSpot);
|
||||
tokenBalance.spotUi += toUiDecimals(
|
||||
tokenInfo.balanceSpot,
|
||||
group.getMintDecimalsByTokenIndex(tokenInfo.tokenIndex),
|
||||
);
|
||||
}
|
||||
|
||||
return tokenBalances;
|
||||
}
|
||||
|
||||
healthSum(healthType: HealthType, tokenBalances: TokenBalance[]): I80F48 {
|
||||
const health = ZERO_I80F48();
|
||||
for (const index of this.tokenInfos.keys()) {
|
||||
|
@ -262,6 +308,70 @@ export class HealthCache {
|
|||
return health;
|
||||
}
|
||||
|
||||
healthContributionPerAssetUi(
|
||||
group: Group,
|
||||
healthType: HealthType,
|
||||
): {
|
||||
asset: string;
|
||||
contribution: number;
|
||||
contributionDetails:
|
||||
| {
|
||||
spotUi: number;
|
||||
perpMarketContributions: { market: string; contributionUi: number }[];
|
||||
}
|
||||
| undefined;
|
||||
}[] {
|
||||
const tokenBalancesDisplay: TokenBalanceDisplay[] =
|
||||
this.effectiveTokenBalancesInternalDisplay(group, healthType, false);
|
||||
|
||||
const ret = new Array<{
|
||||
asset: string;
|
||||
contribution: number;
|
||||
contributionDetails:
|
||||
| {
|
||||
spotUi: number;
|
||||
perpMarketContributions: {
|
||||
market: string;
|
||||
contributionUi: number;
|
||||
}[];
|
||||
}
|
||||
| undefined;
|
||||
}>();
|
||||
for (const index of this.tokenInfos.keys()) {
|
||||
const tokenInfo = this.tokenInfos[index];
|
||||
const tokenBalance = tokenBalancesDisplay[index];
|
||||
const contrib = tokenInfo.healthContribution(
|
||||
healthType,
|
||||
tokenBalance.spotAndPerp,
|
||||
);
|
||||
ret.push({
|
||||
asset: group.getFirstBankByTokenIndex(tokenInfo.tokenIndex).name,
|
||||
contribution: toUiDecimalsForQuote(contrib),
|
||||
contributionDetails: {
|
||||
spotUi: tokenBalance.spotUi,
|
||||
perpMarketContributions: tokenBalance.perpMarketContributions,
|
||||
},
|
||||
});
|
||||
}
|
||||
const res = this.computeSerum3Reservations(healthType);
|
||||
for (const [index, serum3Info] of this.serum3Infos.entries()) {
|
||||
const contrib = serum3Info.healthContribution(
|
||||
healthType,
|
||||
this.tokenInfos,
|
||||
tokenBalancesDisplay,
|
||||
res.tokenMaxReserved,
|
||||
res.serum3Reserved[index],
|
||||
);
|
||||
ret.push({
|
||||
asset: group.getSerum3MarketByMarketIndex(serum3Info.marketIndex).name,
|
||||
contribution: toUiDecimalsForQuote(contrib),
|
||||
contributionDetails: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public health(healthType: HealthType): I80F48 {
|
||||
const tokenBalances = this.effectiveTokenBalancesInternal(
|
||||
healthType,
|
||||
|
@ -1389,6 +1499,17 @@ class TokenBalance {
|
|||
constructor(public spotAndPerp: I80F48) {}
|
||||
}
|
||||
|
||||
class TokenBalanceDisplay {
|
||||
constructor(
|
||||
public spotAndPerp: I80F48,
|
||||
public spotUi: number,
|
||||
public perpMarketContributions: {
|
||||
market: string;
|
||||
contributionUi: number;
|
||||
}[],
|
||||
) {}
|
||||
}
|
||||
|
||||
class TokenMaxReserved {
|
||||
constructor(public maxSerumReserved: I80F48) {}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,14 @@ import { AccountInfo, PublicKey, TransactionSignature } from '@solana/web3.js';
|
|||
import { MangoClient } from '../client';
|
||||
import { OPENBOOK_PROGRAM_ID, RUST_I64_MAX, RUST_I64_MIN } from '../constants';
|
||||
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
|
||||
import { toNativeI80F48, toUiDecimals, toUiDecimalsForQuote } from '../utils';
|
||||
import {
|
||||
U64_MAX_BN,
|
||||
roundTo5,
|
||||
toNativeI80F48,
|
||||
toUiDecimals,
|
||||
toUiDecimalsForQuote,
|
||||
toUiSellPerBuyTokenPrice,
|
||||
} from '../utils';
|
||||
import { Bank, TokenIndex } from './bank';
|
||||
import { Group } from './group';
|
||||
import { HealthCache } from './healthCache';
|
||||
|
@ -182,6 +189,10 @@ export class MangoAccount {
|
|||
return this.serum3.filter((serum3) => serum3.isActive());
|
||||
}
|
||||
|
||||
public tokenConditionalSwapsActive(): TokenConditionalSwap[] {
|
||||
return this.tokenConditionalSwaps.filter((tcs) => tcs.hasData);
|
||||
}
|
||||
|
||||
public perpPositionExistsForMarket(perpMarket: PerpMarket): boolean {
|
||||
return this.perps.some(
|
||||
(pp) => pp.isActive() && pp.marketIndex == perpMarket.perpMarketIndex,
|
||||
|
@ -198,10 +209,6 @@ export class MangoAccount {
|
|||
return this.perps.filter((perp) => perp.isActive());
|
||||
}
|
||||
|
||||
public tokenConditionalSwapsActive(): TokenConditionalSwap[] {
|
||||
return this.tokenConditionalSwaps.filter((tcs) => tcs.hasData);
|
||||
}
|
||||
|
||||
public perpOrdersActive(): PerpOo[] {
|
||||
return this.perpOpenOrders.filter(
|
||||
(oo) => oo.orderMarket !== PerpOo.OrderMarketUnset,
|
||||
|
@ -344,6 +351,23 @@ export class MangoAccount {
|
|||
return hc.health(healthType);
|
||||
}
|
||||
|
||||
public getHealthContributionPerAssetUi(
|
||||
group: Group,
|
||||
healthType: HealthType,
|
||||
): {
|
||||
asset: string;
|
||||
contribution: number;
|
||||
contributionDetails:
|
||||
| {
|
||||
spotUi: number;
|
||||
perpMarketContributions: { market: string; contributionUi: number }[];
|
||||
}
|
||||
| undefined;
|
||||
}[] {
|
||||
const hc = HealthCache.fromMangoAccount(group, this);
|
||||
return hc.healthContributionPerAssetUi(group, healthType);
|
||||
}
|
||||
|
||||
public perpMaxSettle(
|
||||
group: Group,
|
||||
perpMarketSettleTokenIndex: TokenIndex,
|
||||
|
@ -422,9 +446,9 @@ export class MangoAccount {
|
|||
* Sum of all positive assets.
|
||||
* @returns assets, in native quote
|
||||
*/
|
||||
public getAssetsValue(group: Group): I80F48 {
|
||||
public getAssetsValue(group: Group, healthType?: HealthType): I80F48 {
|
||||
const hc = HealthCache.fromMangoAccount(group, this);
|
||||
return hc.healthAssetsAndLiabs(undefined, false).assets;
|
||||
return hc.healthAssetsAndLiabs(healthType, false).assets;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -513,8 +537,8 @@ export class MangoAccount {
|
|||
let existingPositionHealthContrib = ZERO_I80F48();
|
||||
if (existingTokenDeposits.gt(ZERO_I80F48())) {
|
||||
existingPositionHealthContrib = existingTokenDeposits
|
||||
.mul(tokenBank.price)
|
||||
.imul(tokenBank.initAssetWeight);
|
||||
.mul(tokenBank.getAssetPrice())
|
||||
.imul(tokenBank.scaledInitAssetWeight(tokenBank.getAssetPrice()));
|
||||
}
|
||||
|
||||
// Case 2: token deposits have higher contribution than initHealth,
|
||||
|
@ -522,8 +546,8 @@ export class MangoAccount {
|
|||
if (existingPositionHealthContrib.gt(initHealth)) {
|
||||
const withdrawAbleExistingPositionHealthContrib = initHealth;
|
||||
return withdrawAbleExistingPositionHealthContrib
|
||||
.div(tokenBank.initAssetWeight)
|
||||
.div(tokenBank.price);
|
||||
.div(tokenBank.scaledInitAssetWeight(tokenBank.getAssetPrice()))
|
||||
.div(tokenBank.getAssetPrice());
|
||||
}
|
||||
|
||||
// Case 3: withdraw = withdraw existing deposits + borrows until initHealth reaches 0
|
||||
|
@ -597,12 +621,11 @@ export class MangoAccount {
|
|||
),
|
||||
);
|
||||
const sourceBalance = this.getEffectiveTokenBalance(group, sourceBank);
|
||||
if (maxSource.gt(sourceBalance)) {
|
||||
const sourceBorrow = maxSource.sub(sourceBalance);
|
||||
maxSource = sourceBalance.add(
|
||||
sourceBorrow.div(ONE_I80F48().add(sourceBank.loanOriginationFeeRate)),
|
||||
);
|
||||
}
|
||||
const maxWithdrawNative = sourceBank.getMaxWithdraw(
|
||||
group.getTokenVaultBalanceByMint(sourceBank.mint),
|
||||
sourceBalance,
|
||||
);
|
||||
maxSource = maxSource.min(maxWithdrawNative);
|
||||
return toUiDecimals(maxSource, group.getMintDecimals(sourceMintPk));
|
||||
}
|
||||
|
||||
|
@ -733,12 +756,11 @@ export class MangoAccount {
|
|||
// If its a bid then the reserved fund and potential loan is in base
|
||||
// also keep some buffer for fees, use taker fees for worst case simulation.
|
||||
const quoteBalance = this.getEffectiveTokenBalance(group, quoteBank);
|
||||
if (quoteAmount.gt(quoteBalance)) {
|
||||
const quoteBorrow = quoteAmount.sub(quoteBalance);
|
||||
quoteAmount = quoteBalance.add(
|
||||
quoteBorrow.div(ONE_I80F48().add(quoteBank.loanOriginationFeeRate)),
|
||||
);
|
||||
}
|
||||
const maxWithdrawNative = quoteBank.getMaxWithdraw(
|
||||
group.getTokenVaultBalanceByMint(quoteBank.mint),
|
||||
quoteBalance,
|
||||
);
|
||||
quoteAmount = quoteAmount.min(maxWithdrawNative);
|
||||
quoteAmount = quoteAmount.div(
|
||||
ONE_I80F48().add(I80F48.fromNumber(serum3Market.getFeeRates(true))),
|
||||
);
|
||||
|
@ -775,12 +797,11 @@ export class MangoAccount {
|
|||
// If its a ask then the reserved fund and potential loan is in base
|
||||
// also keep some buffer for fees, use taker fees for worst case simulation.
|
||||
const baseBalance = this.getEffectiveTokenBalance(group, baseBank);
|
||||
if (baseAmount.gt(baseBalance)) {
|
||||
const baseBorrow = baseAmount.sub(baseBalance);
|
||||
baseAmount = baseBalance.add(
|
||||
baseBorrow.div(ONE_I80F48().add(baseBank.loanOriginationFeeRate)),
|
||||
);
|
||||
}
|
||||
const maxWithdrawNative = baseBank.getMaxWithdraw(
|
||||
group.getTokenVaultBalanceByMint(baseBank.mint),
|
||||
baseBalance,
|
||||
);
|
||||
baseAmount = baseAmount.min(maxWithdrawNative);
|
||||
baseAmount = baseAmount.div(
|
||||
ONE_I80F48().add(I80F48.fromNumber(serum3Market.getFeeRates(true))),
|
||||
);
|
||||
|
@ -954,6 +975,7 @@ export class MangoAccount {
|
|||
group: Group,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
size: number,
|
||||
healthType: HealthType = HealthType.init,
|
||||
): number {
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
|
||||
|
@ -967,7 +989,7 @@ export class MangoAccount {
|
|||
PerpOrderSide.bid,
|
||||
perpMarket.uiBaseToLots(size),
|
||||
perpMarket.price,
|
||||
HealthType.init,
|
||||
healthType,
|
||||
)
|
||||
.toNumber();
|
||||
}
|
||||
|
@ -976,6 +998,7 @@ export class MangoAccount {
|
|||
group: Group,
|
||||
perpMarketIndex: PerpMarketIndex,
|
||||
size: number,
|
||||
healthType: HealthType = HealthType.init,
|
||||
): number {
|
||||
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
|
||||
const pp = this.getPerpPosition(perpMarket.perpMarketIndex);
|
||||
|
@ -989,7 +1012,7 @@ export class MangoAccount {
|
|||
PerpOrderSide.ask,
|
||||
perpMarket.uiBaseToLots(size),
|
||||
perpMarket.price,
|
||||
HealthType.init,
|
||||
healthType,
|
||||
)
|
||||
.toNumber();
|
||||
}
|
||||
|
@ -1826,6 +1849,116 @@ export class TokenConditionalSwap {
|
|||
public priceDisplayStyle: TokenConditionalSwapDisplayPriceStyle,
|
||||
public intention: TokenConditionalSwapIntention,
|
||||
) {}
|
||||
|
||||
getMaxBuyUi(group: Group): number {
|
||||
const buyBank = this.getBuyToken(group);
|
||||
return toUiDecimals(this.maxBuy, buyBank.mintDecimals);
|
||||
}
|
||||
|
||||
getMaxSellUi(group: Group): number {
|
||||
const sellBank = this.getSellToken(group);
|
||||
return toUiDecimals(this.maxSell, sellBank.mintDecimals);
|
||||
}
|
||||
|
||||
getBoughtUi(group: Group): number {
|
||||
const buyBank = this.getBuyToken(group);
|
||||
return toUiDecimals(this.bought, buyBank.mintDecimals);
|
||||
}
|
||||
|
||||
getSoldUi(group: Group): number {
|
||||
const sellBank = this.getSellToken(group);
|
||||
return toUiDecimals(this.sold, sellBank.mintDecimals);
|
||||
}
|
||||
|
||||
getExpiryTimestampInEpochSeconds(): number {
|
||||
return this.expiryTimestamp.toNumber();
|
||||
}
|
||||
|
||||
private priceLimitToUi(
|
||||
group: Group,
|
||||
sellTokenPerBuyTokenNative: number,
|
||||
): number {
|
||||
const buyBank = this.getBuyToken(group);
|
||||
const sellBank = this.getSellToken(group);
|
||||
const sellTokenPerBuyTokenUi = toUiSellPerBuyTokenPrice(
|
||||
sellTokenPerBuyTokenNative,
|
||||
sellBank,
|
||||
buyBank,
|
||||
);
|
||||
|
||||
// Below are workarounds to know when to show an inverted price in ui
|
||||
// We want to identify if the pair user is wanting to trade is
|
||||
// buytoken/selltoken or selltoken/buytoken
|
||||
|
||||
// Buy limit / close short
|
||||
if (this.maxSell.eq(U64_MAX_BN)) {
|
||||
return roundTo5(sellTokenPerBuyTokenUi);
|
||||
}
|
||||
|
||||
// Stop loss / take profit
|
||||
const buyTokenPerSellTokenUi = 1 / sellTokenPerBuyTokenUi;
|
||||
return roundTo5(buyTokenPerSellTokenUi);
|
||||
}
|
||||
|
||||
getPriceLowerLimitUi(group: Group): number {
|
||||
return this.priceLimitToUi(group, this.priceLowerLimit);
|
||||
}
|
||||
|
||||
getPriceUpperLimitUi(group: Group): number {
|
||||
return this.priceLimitToUi(group, this.priceUpperLimit);
|
||||
}
|
||||
|
||||
getThresholdPriceUi(group: Group): number {
|
||||
const a = I80F48.fromNumber(this.priceLowerLimit);
|
||||
const b = I80F48.fromNumber(this.priceUpperLimit);
|
||||
|
||||
const buyBank = this.getBuyToken(group);
|
||||
const sellBank = this.getSellToken(group);
|
||||
const o = buyBank.price.div(sellBank.price);
|
||||
|
||||
// Choose the price closest to oracle
|
||||
if (o.sub(a).abs().lt(o.sub(b).abs())) {
|
||||
return this.getPriceLowerLimitUi(group);
|
||||
}
|
||||
return this.getPriceUpperLimitUi(group);
|
||||
}
|
||||
|
||||
// in percent
|
||||
getPricePremium(): number {
|
||||
return this.pricePremiumRate * 100;
|
||||
}
|
||||
|
||||
getBuyToken(group: Group): Bank {
|
||||
return group.getFirstBankByTokenIndex(this.buyTokenIndex);
|
||||
}
|
||||
|
||||
getSellToken(group: Group): Bank {
|
||||
return group.getFirstBankByTokenIndex(this.sellTokenIndex);
|
||||
}
|
||||
|
||||
getAllowCreatingDeposits(): boolean {
|
||||
return this.allowCreatingDeposits;
|
||||
}
|
||||
|
||||
getAllowCreatingBorrows(): boolean {
|
||||
return this.allowCreatingBorrows;
|
||||
}
|
||||
|
||||
toString(group: Group): string {
|
||||
return `getMaxBuy ${this.getMaxBuyUi(
|
||||
group,
|
||||
)}, getMaxSell ${this.getMaxSellUi(group)}, bought ${this.getBoughtUi(
|
||||
group,
|
||||
)}, sold ${this.getSoldUi(
|
||||
group,
|
||||
)}, getPriceLowerLimitUi ${this.getPriceLowerLimitUi(
|
||||
group,
|
||||
)}, getPriceUpperLimitUi ${this.getPriceUpperLimitUi(
|
||||
group,
|
||||
)}, getThresholdPriceUi ${this.getThresholdPriceUi(
|
||||
group,
|
||||
)}, getPricePremium ${this.getPricePremium()}, expiry ${this.expiryTimestamp.toString()}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class TokenConditionalSwapDto {
|
||||
|
|
|
@ -79,10 +79,13 @@ import {
|
|||
createAssociatedTokenAccountIdempotentInstruction,
|
||||
getAssociatedTokenAddress,
|
||||
toNative,
|
||||
toNativeSellPerBuyTokenPrice,
|
||||
} from './utils';
|
||||
import { sendTransaction } from './utils/rpc';
|
||||
import { NATIVE_MINT, TOKEN_PROGRAM_ID } from './utils/spl';
|
||||
|
||||
export const DEFAULT_TOKEN_CONDITIONAL_SWAP_COUNT = 8;
|
||||
|
||||
export enum AccountRetriever {
|
||||
Scanning,
|
||||
Fixed,
|
||||
|
@ -161,6 +164,46 @@ export class MangoClient {
|
|||
});
|
||||
}
|
||||
|
||||
public async adminTokenWithdrawFees(
|
||||
group: Group,
|
||||
bank: Bank,
|
||||
tokenAccountPk: PublicKey,
|
||||
): Promise<TransactionSignature> {
|
||||
const admin = (this.program.provider as AnchorProvider).wallet.publicKey;
|
||||
const ix = await this.program.methods
|
||||
.adminTokenWithdrawFees()
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
bank: bank.publicKey,
|
||||
vault: bank.vault,
|
||||
tokenAccount: tokenAccountPk,
|
||||
admin,
|
||||
})
|
||||
.instruction();
|
||||
return await this.sendAndConfirmTransaction([ix]);
|
||||
}
|
||||
|
||||
public async adminPerpWithdrawFees(
|
||||
group: Group,
|
||||
perpMarket: PerpMarket,
|
||||
tokenAccountPk: PublicKey,
|
||||
): Promise<TransactionSignature> {
|
||||
const bank = group.getFirstBankByTokenIndex(perpMarket.settleTokenIndex);
|
||||
const admin = (this.program.provider as AnchorProvider).wallet.publicKey;
|
||||
const ix = await this.program.methods
|
||||
.adminPerpWithdrawFees()
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
perpMarket: perpMarket.publicKey,
|
||||
bank: bank.publicKey,
|
||||
vault: bank.vault,
|
||||
tokenAccount: tokenAccountPk,
|
||||
admin,
|
||||
})
|
||||
.instruction();
|
||||
return await this.sendAndConfirmTransaction([ix]);
|
||||
}
|
||||
|
||||
// Group
|
||||
public async groupCreate(
|
||||
groupNum: number,
|
||||
|
@ -720,7 +763,28 @@ export class MangoClient {
|
|||
perpOoCount: number,
|
||||
tokenConditionalSwapCount: number,
|
||||
): Promise<TransactionSignature> {
|
||||
const ix = await this.program.methods
|
||||
const ix = await this.accountExpandV2Ix(
|
||||
group,
|
||||
account,
|
||||
tokenCount,
|
||||
serum3Count,
|
||||
perpCount,
|
||||
perpOoCount,
|
||||
tokenConditionalSwapCount,
|
||||
);
|
||||
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
|
||||
}
|
||||
|
||||
public async accountExpandV2Ix(
|
||||
group: Group,
|
||||
account: MangoAccount,
|
||||
tokenCount: number,
|
||||
serum3Count: number,
|
||||
perpCount: number,
|
||||
perpOoCount: number,
|
||||
tokenConditionalSwapCount: number,
|
||||
): Promise<TransactionInstruction> {
|
||||
return await this.program.methods
|
||||
.accountExpandV2(
|
||||
tokenCount,
|
||||
serum3Count,
|
||||
|
@ -735,7 +799,6 @@ export class MangoClient {
|
|||
payer: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
})
|
||||
.instruction();
|
||||
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
|
||||
}
|
||||
|
||||
public async editMangoAccount(
|
||||
|
@ -825,7 +888,7 @@ export class MangoClient {
|
|||
);
|
||||
}
|
||||
|
||||
private async getMangoAccountFromAi(
|
||||
public async getMangoAccountFromAi(
|
||||
mangoAccountPk: PublicKey,
|
||||
ai: AccountInfo<Buffer>,
|
||||
): Promise<MangoAccount> {
|
||||
|
@ -1249,6 +1312,7 @@ export class MangoClient {
|
|||
const tokenAccountPk = await getAssociatedTokenAddress(
|
||||
mintPk,
|
||||
mangoAccount.owner,
|
||||
true,
|
||||
);
|
||||
|
||||
let wrappedSolAccount: Keypair | undefined;
|
||||
|
@ -1352,6 +1416,7 @@ export class MangoClient {
|
|||
const tokenAccountPk = await getAssociatedTokenAddress(
|
||||
bank.mint,
|
||||
mangoAccount.owner,
|
||||
true,
|
||||
);
|
||||
|
||||
// ensure withdraws don't fail with missing ATAs
|
||||
|
@ -2335,6 +2400,56 @@ export class MangoClient {
|
|||
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
|
||||
}
|
||||
|
||||
public async perpCloseAll(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
slippage = 0.01, // 1%, 100bps
|
||||
): Promise<TransactionSignature> {
|
||||
if (mangoAccount.perpActive().length == 0) {
|
||||
throw new Error(`No perp positions found.`);
|
||||
}
|
||||
|
||||
if (mangoAccount.perpActive().length > 8) {
|
||||
// Technically we can fit in 16, 1.6M CU, 100k CU per ix, but lets be conservative
|
||||
throw new Error(
|
||||
`Can't close more than 8 positions in one tx, due to compute usage limit.`,
|
||||
);
|
||||
}
|
||||
|
||||
const hrix1 = await this.healthRegionBeginIx(group, mangoAccount);
|
||||
const ixs = await Promise.all(
|
||||
mangoAccount.perpActive().map(async (pa) => {
|
||||
const pm = group.getPerpMarketByMarketIndex(pa.marketIndex);
|
||||
const isLong = pa.basePositionLots.gt(new BN(0));
|
||||
|
||||
return await this.perpPlaceOrderV2Ix(
|
||||
group,
|
||||
mangoAccount,
|
||||
pa.marketIndex,
|
||||
isLong ? PerpOrderSide.ask : PerpOrderSide.bid,
|
||||
pm.uiPrice * (isLong ? 1 - slippage : 1 + slippage), // Try to cross the spread to guarantee matching
|
||||
pa.getBasePositionUi(pm) * 1.01, // Send a larger size to ensure full order is closed
|
||||
undefined,
|
||||
Date.now(),
|
||||
PerpOrderType.immediateOrCancel,
|
||||
PerpSelfTradeBehavior.decrementTake,
|
||||
true, // Reduce only
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
}),
|
||||
);
|
||||
const hrix2 = await this.healthRegionEndIx(group, mangoAccount);
|
||||
|
||||
return await this.sendAndConfirmTransactionForGroup(
|
||||
group,
|
||||
[hrix1, ...ixs, hrix2],
|
||||
{
|
||||
prioritizationFee: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// perpPlaceOrder ix returns an optional, custom order id,
|
||||
// but, since we use a customer tx sender, this method
|
||||
// doesn't return it
|
||||
|
@ -2712,6 +2827,84 @@ export class MangoClient {
|
|||
.instruction();
|
||||
}
|
||||
|
||||
async settleAll(
|
||||
client: MangoClient,
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
allMangoAccounts?: MangoAccount[],
|
||||
): Promise<TransactionSignature> {
|
||||
if (!allMangoAccounts) {
|
||||
allMangoAccounts = await client.getAllMangoAccounts(group, true);
|
||||
}
|
||||
|
||||
const ixs1 = new Array<TransactionInstruction>();
|
||||
// This is optimistic, since we might find the same opponent candidate for all markets,
|
||||
// and they have might not be able to settle at some point due to safety limits
|
||||
// Future: correct way to do is, to apply the settlement on a copy and then move to next position
|
||||
for (const pa of mangoAccount.perpActive()) {
|
||||
const pm = group.getPerpMarketByMarketIndex(pa.marketIndex);
|
||||
const candidates = await pm.getSettlePnlCandidates(
|
||||
client,
|
||||
group,
|
||||
allMangoAccounts,
|
||||
pa.getUnsettledPnlUi(pm) > 0 ? 'negative' : 'positive',
|
||||
2,
|
||||
);
|
||||
if (candidates.length == 0) {
|
||||
continue;
|
||||
}
|
||||
ixs1.push(
|
||||
// Takes ~130k CU
|
||||
await this.perpSettlePnlIx(
|
||||
group,
|
||||
pa.getUnsettledPnlUi(pm) > 0 ? mangoAccount : candidates[0].account,
|
||||
pa.getUnsettledPnlUi(pm) < 0 ? candidates[0].account : mangoAccount,
|
||||
mangoAccount,
|
||||
pm.perpMarketIndex,
|
||||
),
|
||||
);
|
||||
ixs1.push(
|
||||
// Takes ~20k CU
|
||||
await this.perpSettleFeesIx(
|
||||
group,
|
||||
mangoAccount,
|
||||
pm.perpMarketIndex,
|
||||
undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const ixs2 = await Promise.all(
|
||||
mangoAccount.serum3Active().map((s) => {
|
||||
const serum3Market = group.getSerum3MarketByMarketIndex(s.marketIndex);
|
||||
// Takes ~65k CU
|
||||
return this.serum3SettleFundsV2Ix(
|
||||
group,
|
||||
mangoAccount,
|
||||
serum3Market.serumMarketExternal,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
mangoAccount.perpActive().length * 150 +
|
||||
mangoAccount.serum3Active().length * 65 >
|
||||
1600
|
||||
) {
|
||||
throw new Error(
|
||||
`Too many perp positions and serum open orders to settle in one tx! Please try settling individually!`,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.sendAndConfirmTransactionForGroup(
|
||||
group,
|
||||
[...ixs1, ...ixs2],
|
||||
{
|
||||
prioritizationFee: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async perpSettlePnlAndFees(
|
||||
group: Group,
|
||||
profitableAccount: MangoAccount,
|
||||
|
@ -2983,6 +3176,7 @@ export class MangoClient {
|
|||
const inputTokenAccountPk = await getAssociatedTokenAddress(
|
||||
inputBank.mint,
|
||||
swapExecutingWallet,
|
||||
true,
|
||||
);
|
||||
const inputTokenAccExists =
|
||||
await this.program.provider.connection.getAccountInfo(
|
||||
|
@ -3002,6 +3196,7 @@ export class MangoClient {
|
|||
const outputTokenAccountPk = await getAssociatedTokenAddress(
|
||||
outputBank.mint,
|
||||
swapExecutingWallet,
|
||||
true,
|
||||
);
|
||||
const outputTokenAccExists =
|
||||
await this.program.provider.connection.getAccountInfo(
|
||||
|
@ -3191,7 +3386,266 @@ export class MangoClient {
|
|||
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
|
||||
}
|
||||
|
||||
public async tcsTakeProfitOnDeposit(
|
||||
group: Group,
|
||||
account: MangoAccount,
|
||||
sellBank: Bank,
|
||||
buyBank: Bank,
|
||||
thresholdPriceUi: number,
|
||||
thresholdPriceInSellPerBuyToken: boolean,
|
||||
maxSellUi: number | null,
|
||||
pricePremium: number | null,
|
||||
expiryTimestamp: number | null,
|
||||
): Promise<TransactionSignature> {
|
||||
if (account.getTokenBalanceUi(sellBank) < 0) {
|
||||
throw new Error(
|
||||
`Only allowed to take profits on deposits! Current balance ${account.getTokenBalanceUi(
|
||||
sellBank,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.tokenConditionalSwapCreate(
|
||||
group,
|
||||
account,
|
||||
sellBank,
|
||||
buyBank,
|
||||
thresholdPriceUi,
|
||||
thresholdPriceInSellPerBuyToken,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
maxSellUi ?? account.getTokenBalanceUi(sellBank),
|
||||
'TakeProfitOnDeposit',
|
||||
pricePremium,
|
||||
true,
|
||||
false,
|
||||
expiryTimestamp,
|
||||
);
|
||||
}
|
||||
|
||||
public async tcsStopLossOnDeposit(
|
||||
group: Group,
|
||||
account: MangoAccount,
|
||||
sellBank: Bank,
|
||||
buyBank: Bank,
|
||||
thresholdPriceUi: number,
|
||||
thresholdPriceInSellPerBuyToken: boolean,
|
||||
maxSellUi: number | null,
|
||||
pricePremium: number | null,
|
||||
expiryTimestamp: number | null,
|
||||
): Promise<TransactionSignature> {
|
||||
if (account.getTokenBalanceUi(sellBank) < 0) {
|
||||
throw new Error(
|
||||
`Only allowed to set a stop loss on deposits! Current balance ${account.getTokenBalanceUi(
|
||||
sellBank,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.tokenConditionalSwapCreate(
|
||||
group,
|
||||
account,
|
||||
sellBank,
|
||||
buyBank,
|
||||
thresholdPriceUi,
|
||||
thresholdPriceInSellPerBuyToken,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
maxSellUi ?? account.getTokenBalanceUi(sellBank),
|
||||
'StopLossOnDeposit',
|
||||
pricePremium,
|
||||
true,
|
||||
false,
|
||||
expiryTimestamp,
|
||||
);
|
||||
}
|
||||
|
||||
public async tcsTakeProfitOnBorrow(
|
||||
group: Group,
|
||||
account: MangoAccount,
|
||||
sellBank: Bank,
|
||||
buyBank: Bank,
|
||||
thresholdPriceUi: number,
|
||||
thresholdPriceInSellPerBuyToken: boolean,
|
||||
maxBuyUi: number | null,
|
||||
pricePremium: number | null,
|
||||
allowMargin: boolean | null,
|
||||
expiryTimestamp: number | null,
|
||||
): Promise<TransactionSignature> {
|
||||
if (account.getTokenBalanceUi(buyBank) > 0) {
|
||||
throw new Error(
|
||||
`Only allowed to take profits on borrows! Current balance ${account.getTokenBalanceUi(
|
||||
buyBank,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.tokenConditionalSwapCreate(
|
||||
group,
|
||||
account,
|
||||
sellBank,
|
||||
buyBank,
|
||||
thresholdPriceUi,
|
||||
thresholdPriceInSellPerBuyToken,
|
||||
maxBuyUi ?? -account.getTokenBalanceUi(buyBank),
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
'TakeProfitOnBorrow',
|
||||
pricePremium,
|
||||
false,
|
||||
allowMargin ?? false,
|
||||
expiryTimestamp,
|
||||
);
|
||||
}
|
||||
|
||||
public async tcsStopLossOnBorrow(
|
||||
group: Group,
|
||||
account: MangoAccount,
|
||||
sellBank: Bank,
|
||||
buyBank: Bank,
|
||||
thresholdPriceUi: number,
|
||||
thresholdPriceInSellPerBuyToken: boolean,
|
||||
maxBuyUi: number | null,
|
||||
pricePremium: number | null,
|
||||
allowMargin: boolean | null,
|
||||
expiryTimestamp: number | null,
|
||||
): Promise<TransactionSignature> {
|
||||
if (account.getTokenBalanceUi(buyBank) > 0) {
|
||||
throw new Error(
|
||||
`Only allowed to set stop loss on borrows! Current balance ${account.getTokenBalanceUi(
|
||||
buyBank,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.tokenConditionalSwapCreate(
|
||||
group,
|
||||
account,
|
||||
sellBank,
|
||||
buyBank,
|
||||
thresholdPriceUi,
|
||||
thresholdPriceInSellPerBuyToken,
|
||||
maxBuyUi ?? -account.getTokenBalanceUi(buyBank),
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
'StopLossOnBorrow',
|
||||
pricePremium,
|
||||
false,
|
||||
allowMargin ?? false,
|
||||
expiryTimestamp,
|
||||
);
|
||||
}
|
||||
|
||||
public async tokenConditionalSwapCreate(
|
||||
group: Group,
|
||||
account: MangoAccount,
|
||||
sellBank: Bank,
|
||||
buyBank: Bank,
|
||||
thresholdPriceUi: number,
|
||||
thresholdPriceInSellPerBuyToken: boolean,
|
||||
maxBuyUi: number,
|
||||
maxSellUi: number,
|
||||
tcsIntention:
|
||||
| 'TakeProfitOnDeposit'
|
||||
| 'StopLossOnDeposit'
|
||||
| 'TakeProfitOnBorrow'
|
||||
| 'StopLossOnBorrow'
|
||||
| null,
|
||||
pricePremium: number | null,
|
||||
allowCreatingDeposits: boolean,
|
||||
allowCreatingBorrows: boolean,
|
||||
expiryTimestamp: number | null,
|
||||
): Promise<TransactionSignature> {
|
||||
const maxBuy =
|
||||
maxBuyUi == Number.MAX_SAFE_INTEGER
|
||||
? U64_MAX_BN
|
||||
: toNative(maxBuyUi, buyBank.mintDecimals);
|
||||
const maxSell =
|
||||
maxSellUi == Number.MAX_SAFE_INTEGER
|
||||
? U64_MAX_BN
|
||||
: toNative(maxSellUi, sellBank.mintDecimals);
|
||||
|
||||
if (!thresholdPriceInSellPerBuyToken) {
|
||||
thresholdPriceUi = 1 / thresholdPriceUi;
|
||||
}
|
||||
|
||||
let lowerLimit, upperLimit;
|
||||
const thresholdPrice = toNativeSellPerBuyTokenPrice(
|
||||
thresholdPriceUi,
|
||||
sellBank,
|
||||
buyBank,
|
||||
);
|
||||
const sellTokenPerBuyTokenPrice = buyBank.price
|
||||
.div(sellBank.price)
|
||||
.toNumber();
|
||||
|
||||
if (
|
||||
tcsIntention == 'TakeProfitOnDeposit' ||
|
||||
tcsIntention == 'StopLossOnBorrow' ||
|
||||
(tcsIntention == null && thresholdPrice > sellTokenPerBuyTokenPrice)
|
||||
) {
|
||||
lowerLimit = thresholdPrice;
|
||||
upperLimit = Number.MAX_SAFE_INTEGER;
|
||||
} else {
|
||||
lowerLimit = 0;
|
||||
upperLimit = thresholdPrice;
|
||||
}
|
||||
|
||||
const expiryTimestampBn =
|
||||
expiryTimestamp !== null ? new BN(expiryTimestamp) : U64_MAX_BN;
|
||||
|
||||
if (!pricePremium) {
|
||||
const buyTokenPriceImpact = group.getPriceImpactByTokenIndex(
|
||||
buyBank.tokenIndex,
|
||||
5000,
|
||||
);
|
||||
const sellTokenPriceImpact = group.getPriceImpactByTokenIndex(
|
||||
sellBank.tokenIndex,
|
||||
5000,
|
||||
);
|
||||
pricePremium =
|
||||
((1 + buyTokenPriceImpact / 100) * (1 + sellTokenPriceImpact / 100) -
|
||||
1) *
|
||||
100;
|
||||
}
|
||||
const pricePremiumRate = pricePremium > 0 ? pricePremium / 100 : 0.03;
|
||||
|
||||
const tcsIx = await this.program.methods
|
||||
.tokenConditionalSwapCreate(
|
||||
maxBuy,
|
||||
maxSell,
|
||||
expiryTimestampBn,
|
||||
lowerLimit,
|
||||
upperLimit,
|
||||
pricePremiumRate,
|
||||
allowCreatingDeposits,
|
||||
allowCreatingBorrows,
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
account: account.publicKey,
|
||||
authority: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
buyBank: buyBank.publicKey,
|
||||
sellBank: sellBank.publicKey,
|
||||
})
|
||||
.instruction();
|
||||
|
||||
const ixs: TransactionInstruction[] = [];
|
||||
if (account.tokenConditionalSwaps.length == 0) {
|
||||
ixs.push(
|
||||
await this.accountExpandV2Ix(
|
||||
group,
|
||||
account,
|
||||
account.tokens.length,
|
||||
account.serum3.length,
|
||||
account.perps.length,
|
||||
account.perpOpenOrders.length,
|
||||
DEFAULT_TOKEN_CONDITIONAL_SWAP_COUNT,
|
||||
),
|
||||
);
|
||||
}
|
||||
ixs.push(tcsIx);
|
||||
|
||||
return await this.sendAndConfirmTransactionForGroup(group, ixs);
|
||||
}
|
||||
|
||||
public async tokenConditionalSwapCreateRaw(
|
||||
group: Group,
|
||||
account: MangoAccount,
|
||||
buyMintPk: PublicKey,
|
||||
|
@ -3231,21 +3685,36 @@ export class MangoClient {
|
|||
})
|
||||
.instruction();
|
||||
|
||||
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
|
||||
const ixs = [ix];
|
||||
if (account.tokenConditionalSwaps.length == 0) {
|
||||
ixs.push(
|
||||
await this.accountExpandV2Ix(
|
||||
group,
|
||||
account,
|
||||
account.tokens.length,
|
||||
account.serum3.length,
|
||||
account.perps.length,
|
||||
account.perpOpenOrders.length,
|
||||
8,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return await this.sendAndConfirmTransactionForGroup(group, ixs);
|
||||
}
|
||||
|
||||
public async tokenConditionalSwapCancel(
|
||||
group: Group,
|
||||
account: MangoAccount,
|
||||
tokenConditionalSwapIndex: number,
|
||||
tokenConditionalSwapId: BN,
|
||||
): Promise<TransactionSignature> {
|
||||
const tcs = account
|
||||
.tokenConditionalSwapsActive()
|
||||
.find((tcs) => tcs.id.eq(tokenConditionalSwapId));
|
||||
if (!tcs) {
|
||||
const tokenConditionalSwapIndex = account.tokenConditionalSwaps.findIndex(
|
||||
(tcs) => tcs.id.eq(tokenConditionalSwapId),
|
||||
);
|
||||
if (tokenConditionalSwapIndex == -1) {
|
||||
throw new Error('tcs with id not found');
|
||||
}
|
||||
const tcs = account.tokenConditionalSwaps[tokenConditionalSwapIndex];
|
||||
|
||||
const buyBank = group.banksMapByTokenIndex.get(tcs.buyTokenIndex)![0];
|
||||
const sellBank = group.banksMapByTokenIndex.get(tcs.sellTokenIndex)![0];
|
||||
|
@ -3267,21 +3736,50 @@ export class MangoClient {
|
|||
return await this.sendAndConfirmTransactionForGroup(group, [ix]);
|
||||
}
|
||||
|
||||
public async tokenConditionalSwapCancelAll(
|
||||
group: Group,
|
||||
account: MangoAccount,
|
||||
): Promise<TransactionSignature> {
|
||||
const ixs = await Promise.all(
|
||||
account.tokenConditionalSwaps
|
||||
.filter((tcs) => tcs.hasData)
|
||||
.map(async (tcs, i) => {
|
||||
const buyBank = group.banksMapByTokenIndex.get(tcs.buyTokenIndex)![0];
|
||||
const sellBank = group.banksMapByTokenIndex.get(
|
||||
tcs.sellTokenIndex,
|
||||
)![0];
|
||||
return await this.program.methods
|
||||
.tokenConditionalSwapCancel(i, new BN(tcs.id))
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
account: account.publicKey,
|
||||
authority: (this.program.provider as AnchorProvider).wallet
|
||||
.publicKey,
|
||||
buyBank: buyBank.publicKey,
|
||||
sellBank: sellBank.publicKey,
|
||||
})
|
||||
.instruction();
|
||||
}),
|
||||
);
|
||||
|
||||
return await this.sendAndConfirmTransactionForGroup(group, ixs);
|
||||
}
|
||||
|
||||
public async tokenConditionalSwapTrigger(
|
||||
group: Group,
|
||||
liqee: MangoAccount,
|
||||
liqor: MangoAccount,
|
||||
tokenConditionalSwapIndex: number,
|
||||
tokenConditionalSwapId: BN,
|
||||
maxBuyTokenToLiqee: number,
|
||||
maxSellTokenToLiqor: number,
|
||||
): Promise<TransactionSignature> {
|
||||
const tcs = liqee
|
||||
.tokenConditionalSwapsActive()
|
||||
.find((tcs) => tcs.id.eq(tokenConditionalSwapId));
|
||||
if (!tcs) {
|
||||
const tokenConditionalSwapIndex = liqee.tokenConditionalSwaps.findIndex(
|
||||
(tcs) => tcs.id.eq(tokenConditionalSwapId),
|
||||
);
|
||||
if (tokenConditionalSwapIndex == -1) {
|
||||
throw new Error('tcs with id not found');
|
||||
}
|
||||
const tcs = liqee.tokenConditionalSwaps[tokenConditionalSwapIndex];
|
||||
|
||||
const buyBank = group.banksMapByTokenIndex.get(tcs.buyTokenIndex)![0];
|
||||
const sellBank = group.banksMapByTokenIndex.get(tcs.sellTokenIndex)![0];
|
||||
|
|
|
@ -21,3 +21,7 @@ export const MANGO_V4_ID = {
|
|||
devnet: new PublicKey('4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg'),
|
||||
'mainnet-beta': new PublicKey('4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg'),
|
||||
};
|
||||
|
||||
export const USDC_MINT = new PublicKey(
|
||||
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
||||
);
|
||||
|
|
|
@ -19,6 +19,7 @@ export {
|
|||
buildIxGate,
|
||||
} from './clientIxParamBuilder';
|
||||
export * from './constants';
|
||||
export * from './mango_v4';
|
||||
export * from './numbers/I80F48';
|
||||
export * from './risk';
|
||||
export * from './router';
|
||||
|
|
|
@ -1,9 +1,24 @@
|
|||
import BN from 'bn.js';
|
||||
import { expect } from 'chai';
|
||||
import { U64_MAX_BN } from '../utils';
|
||||
import { U64_MAX_BN, roundTo5 } from '../utils';
|
||||
import { I80F48 } from './I80F48';
|
||||
|
||||
describe('Math', () => {
|
||||
it('round to accuracy 5', () => {
|
||||
expect(roundTo5(0.012)).equals(0.012);
|
||||
expect(roundTo5(0.0123456789)).equals(0.012345);
|
||||
expect(roundTo5(0.123456789)).equals(0.12345);
|
||||
expect(roundTo5(1.23456789)).equals(1.2345);
|
||||
expect(roundTo5(12.3456789)).equals(12.345);
|
||||
expect(roundTo5(123.456789)).equals(123.45);
|
||||
expect(roundTo5(1234.56789)).equals(1234.5);
|
||||
expect(roundTo5(12345.6789)).equals(12346);
|
||||
expect(roundTo5(123456.789)).equals(123457);
|
||||
|
||||
expect(roundTo5(1.23)).equals(1.2299);
|
||||
expect(roundTo5(1.2)).equals(1.1999);
|
||||
});
|
||||
|
||||
it('js number to BN and I80F48', () => {
|
||||
// BN can be only be created from js numbers which are <=2^53
|
||||
expect(function () {
|
||||
|
|
|
@ -6,20 +6,7 @@ import { Group } from './accounts/group';
|
|||
import { HealthType, MangoAccount } from './accounts/mangoAccount';
|
||||
import { MangoClient } from './client';
|
||||
import { I80F48, ONE_I80F48, ZERO_I80F48 } from './numbers/I80F48';
|
||||
import { toUiDecimals, toUiDecimalsForQuote } from './utils';
|
||||
|
||||
async function buildFetch(): Promise<
|
||||
(
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit | undefined,
|
||||
) => Promise<Response>
|
||||
> {
|
||||
let fetch = globalThis?.fetch;
|
||||
if (!fetch && process?.versions?.node) {
|
||||
fetch = (await import('node-fetch')).default;
|
||||
}
|
||||
return fetch;
|
||||
}
|
||||
import { buildFetch, toUiDecimals, toUiDecimalsForQuote } from './utils';
|
||||
|
||||
export interface LiqorPriceImpact {
|
||||
Coin: { val: string; highlight: boolean };
|
||||
|
@ -56,33 +43,42 @@ export interface Risk {
|
|||
liqorEquity: { title: string; data: AccountEquity[] };
|
||||
}
|
||||
|
||||
export async function computePriceImpactOnJup(
|
||||
amount: string,
|
||||
inputMint: string,
|
||||
outputMint: string,
|
||||
): Promise<{ outAmount: number; priceImpactPct: number }> {
|
||||
const url = `https://quote-api.jup.ag/v4/quote?inputMint=${inputMint}&outputMint=${outputMint}&amount=${amount}&swapMode=ExactIn&slippageBps=10000&onlyDirectRoutes=false&asLegacyTransaction=false`;
|
||||
const response = await (await buildFetch())(url, { mode: 'no-cors' });
|
||||
export type PriceImpact = {
|
||||
symbol: string;
|
||||
side: 'bid' | 'ask';
|
||||
target_amount: number;
|
||||
avg_price_impact_percent: number;
|
||||
min_price_impact_percent: number;
|
||||
max_price_impact_percent: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns price impact in bps i.e. 0 to 10,000
|
||||
* returns -1 if data is missing
|
||||
*/
|
||||
export function computePriceImpactOnJup(
|
||||
pis: PriceImpact[],
|
||||
usdcAmount: number,
|
||||
tokenName: string,
|
||||
): number {
|
||||
try {
|
||||
const res = await response.json();
|
||||
if (res['data'] && res.data.length > 0 && res.data[0].outAmount) {
|
||||
return {
|
||||
outAmount: parseFloat(res.data[0].outAmount),
|
||||
priceImpactPct: parseFloat(res.data[0].priceImpactPct),
|
||||
};
|
||||
const closestTo = [1000, 5000, 20000, 100000].reduce((prev, curr) =>
|
||||
Math.abs(curr - usdcAmount) < Math.abs(prev - usdcAmount) ? curr : prev,
|
||||
);
|
||||
// Workaround api
|
||||
if (tokenName == 'ETH (Portal)') {
|
||||
tokenName = 'ETH';
|
||||
}
|
||||
const filteredPis: PriceImpact[] = pis.filter(
|
||||
(pi) => pi.symbol == tokenName && pi.target_amount == closestTo,
|
||||
);
|
||||
if (filteredPis.length > 0) {
|
||||
return (filteredPis[0].max_price_impact_percent * 10000) / 100;
|
||||
} else {
|
||||
return {
|
||||
outAmount: -1 / 10000,
|
||||
priceImpactPct: -1 / 10000,
|
||||
};
|
||||
return -1;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return {
|
||||
outAmount: -1 / 10000,
|
||||
priceImpactPct: -1 / 10000,
|
||||
};
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,6 +103,7 @@ export async function getOnChainPriceForMints(
|
|||
|
||||
export async function getPriceImpactForLiqor(
|
||||
group: Group,
|
||||
pis: PriceImpact[],
|
||||
mangoAccounts: MangoAccount[],
|
||||
): Promise<LiqorPriceImpact[]> {
|
||||
const mangoAccountsWithHealth = mangoAccounts.map((a: MangoAccount) => {
|
||||
|
@ -232,25 +229,24 @@ export async function getPriceImpactForLiqor(
|
|||
return sum.add(maxAsset);
|
||||
}, ZERO_I80F48());
|
||||
|
||||
const [pi1, pi2] = await Promise.all([
|
||||
const pi1 =
|
||||
!liabsInUsdc.eq(ZERO_I80F48()) &&
|
||||
usdcMint.toBase58() !== bank.mint.toBase58()
|
||||
? computePriceImpactOnJup(
|
||||
liabsInUsdc.toString(),
|
||||
usdcMint.toBase58(),
|
||||
bank.mint.toBase58(),
|
||||
pis,
|
||||
toUiDecimalsForQuote(liabsInUsdc),
|
||||
bank.name,
|
||||
)
|
||||
: Promise.resolve({ priceImpactPct: 0, outAmount: 0 }),
|
||||
|
||||
: 0;
|
||||
const pi2 =
|
||||
!assets.eq(ZERO_I80F48()) &&
|
||||
usdcMint.toBase58() !== bank.mint.toBase58()
|
||||
? computePriceImpactOnJup(
|
||||
assets.floor().toString(),
|
||||
bank.mint.toBase58(),
|
||||
usdcMint.toBase58(),
|
||||
pis,
|
||||
toUiDecimals(assets.mul(bank.price), bank.mintDecimals),
|
||||
bank.name,
|
||||
)
|
||||
: Promise.resolve({ priceImpactPct: 0, outAmount: 0 }),
|
||||
]);
|
||||
: 0;
|
||||
|
||||
return {
|
||||
Coin: { val: bank.name, highlight: false },
|
||||
|
@ -277,9 +273,9 @@ export async function getPriceImpactForLiqor(
|
|||
highlight: Math.round(toUiDecimalsForQuote(liabsInUsdc)) > 5000,
|
||||
},
|
||||
'Liabs Slippage': {
|
||||
val: Math.round(pi1.priceImpactPct * 10000),
|
||||
val: Math.round(pi1),
|
||||
highlight:
|
||||
Math.round(pi1.priceImpactPct * 10000) >
|
||||
Math.round(pi1) >
|
||||
Math.round(bank.liquidationFee.toNumber() * 10000),
|
||||
},
|
||||
Assets: {
|
||||
|
@ -292,9 +288,9 @@ export async function getPriceImpactForLiqor(
|
|||
) > 5000,
|
||||
},
|
||||
'Assets Slippage': {
|
||||
val: Math.round(pi2.priceImpactPct * 10000),
|
||||
val: Math.round(pi2),
|
||||
highlight:
|
||||
Math.round(pi2.priceImpactPct * 10000) >
|
||||
Math.round(pi2) >
|
||||
Math.round(bank.liquidationFee.toNumber() * 10000),
|
||||
},
|
||||
};
|
||||
|
@ -374,23 +370,14 @@ export async function getPerpPositionsToBeLiquidated(
|
|||
export async function getEquityForMangoAccounts(
|
||||
client: MangoClient,
|
||||
group: Group,
|
||||
mangoAccounts: PublicKey[],
|
||||
mangoAccountPks: PublicKey[],
|
||||
allMangoAccounts: MangoAccount[],
|
||||
): Promise<AccountEquity[]> {
|
||||
// Filter mango accounts which might be closed
|
||||
const liqors = (
|
||||
await client.connection.getMultipleAccountsInfo(mangoAccounts)
|
||||
)
|
||||
.map((ai, i) => {
|
||||
return { ai: ai, pk: mangoAccounts[i] };
|
||||
})
|
||||
.filter((val) => val.ai)
|
||||
.map((val) => val.pk);
|
||||
|
||||
const liqorMangoAccounts = await Promise.all(
|
||||
liqors.map((liqor) => client.getMangoAccount(liqor, true)),
|
||||
const mangoAccounts = allMangoAccounts.filter((a) =>
|
||||
mangoAccountPks.find((pk) => pk.equals(a.publicKey)),
|
||||
);
|
||||
|
||||
const accountsWithEquity = liqorMangoAccounts.map((a: MangoAccount) => {
|
||||
const accountsWithEquity = mangoAccounts.map((a: MangoAccount) => {
|
||||
return {
|
||||
Account: { val: a.publicKey, highlight: false },
|
||||
Equity: {
|
||||
|
@ -408,6 +395,26 @@ export async function getRiskStats(
|
|||
group: Group,
|
||||
change = 0.4, // simulates 40% price rally and price drop on tokens and markets
|
||||
): Promise<Risk> {
|
||||
let pis;
|
||||
try {
|
||||
pis = await (
|
||||
await (
|
||||
await buildFetch()
|
||||
)(
|
||||
`https://api.mngo.cloud/data/v4/risk/listed-tokens-one-week-price-impacts`,
|
||||
{
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
},
|
||||
)
|
||||
).json();
|
||||
} catch (error) {
|
||||
pis = [];
|
||||
}
|
||||
|
||||
// Get known liqors
|
||||
let liqors: PublicKey[];
|
||||
try {
|
||||
|
@ -417,7 +424,13 @@ export async function getRiskStats(
|
|||
await buildFetch()
|
||||
)(
|
||||
`https://api.mngo.cloud/data/v4/stats/liqors-over_period?over_period=1MONTH`,
|
||||
{ mode: 'no-cors' },
|
||||
{
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
},
|
||||
)
|
||||
).json()
|
||||
).map((data) => new PublicKey(data['liqor']));
|
||||
|
@ -435,12 +448,6 @@ export async function getRiskStats(
|
|||
|
||||
// Get all mango accounts
|
||||
const mangoAccounts = await client.getAllMangoAccounts(group, true);
|
||||
// const mangoAccounts = [
|
||||
// await client.getMangoAccount(
|
||||
// new PublicKey('5G9XriaoqQy1V4s9RmnbczWAozzbv6h2RuEeAHk4R6Lb'), // https://app.mango.markets/stats?token=SOL
|
||||
// true,
|
||||
// ),
|
||||
// ];
|
||||
|
||||
// Get on chain prices
|
||||
const mints = [
|
||||
|
@ -532,14 +539,14 @@ export async function getRiskStats(
|
|||
liqorEquity,
|
||||
marketMakerEquity,
|
||||
] = await Promise.all([
|
||||
getPriceImpactForLiqor(groupDrop, mangoAccounts),
|
||||
getPriceImpactForLiqor(groupRally, mangoAccounts),
|
||||
getPriceImpactForLiqor(groupUsdcDepeg, mangoAccounts),
|
||||
getPriceImpactForLiqor(groupUsdtDepeg, mangoAccounts),
|
||||
getPriceImpactForLiqor(groupDrop, pis, mangoAccounts),
|
||||
getPriceImpactForLiqor(groupRally, pis, mangoAccounts),
|
||||
getPriceImpactForLiqor(groupUsdcDepeg, pis, mangoAccounts),
|
||||
getPriceImpactForLiqor(groupUsdtDepeg, pis, mangoAccounts),
|
||||
getPerpPositionsToBeLiquidated(groupDrop, mangoAccounts),
|
||||
getPerpPositionsToBeLiquidated(groupRally, mangoAccounts),
|
||||
getEquityForMangoAccounts(client, group, liqors),
|
||||
getEquityForMangoAccounts(client, group, mms),
|
||||
getEquityForMangoAccounts(client, group, liqors, mangoAccounts),
|
||||
getEquityForMangoAccounts(client, group, mms, mangoAccounts),
|
||||
]);
|
||||
|
||||
return {
|
||||
|
|
|
@ -39,8 +39,12 @@ export async function getLargestPerpPositions(
|
|||
|
||||
allPps.sort(
|
||||
(a, b) =>
|
||||
b.getNotionalValueUi(group.getPerpMarketByMarketIndex(b.marketIndex)) -
|
||||
a.getNotionalValueUi(group.getPerpMarketByMarketIndex(a.marketIndex)),
|
||||
Math.abs(
|
||||
b.getNotionalValueUi(group.getPerpMarketByMarketIndex(b.marketIndex)),
|
||||
) -
|
||||
Math.abs(
|
||||
a.getNotionalValueUi(group.getPerpMarketByMarketIndex(a.marketIndex)),
|
||||
),
|
||||
);
|
||||
|
||||
return allPps.map((pp) => ({
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
VersionedTransaction,
|
||||
} from '@solana/web3.js';
|
||||
import BN from 'bn.js';
|
||||
import { Bank } from './accounts/bank';
|
||||
import { I80F48 } from './numbers/I80F48';
|
||||
import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from './utils/spl';
|
||||
|
||||
|
@ -38,6 +39,22 @@ export function toNative(uiAmount: number, decimals: number): BN {
|
|||
return new BN((uiAmount * Math.pow(10, decimals)).toFixed(0));
|
||||
}
|
||||
|
||||
export function toNativeSellPerBuyTokenPrice(
|
||||
price: number,
|
||||
sellBank: Bank,
|
||||
buyBank: Bank,
|
||||
): number {
|
||||
return price * Math.pow(10, sellBank.mintDecimals - buyBank.mintDecimals);
|
||||
}
|
||||
|
||||
export function toUiSellPerBuyTokenPrice(
|
||||
price: number,
|
||||
sellBank: Bank,
|
||||
buyBank: Bank,
|
||||
): number {
|
||||
return toUiDecimals(price, sellBank.mintDecimals - buyBank.mintDecimals);
|
||||
}
|
||||
|
||||
export function toUiDecimals(
|
||||
nativeAmount: BN | I80F48 | number,
|
||||
decimals: number,
|
||||
|
@ -66,6 +83,55 @@ export function toUiI80F48(nativeAmount: I80F48, decimals: number): I80F48 {
|
|||
return nativeAmount.div(I80F48.fromNumber(Math.pow(10, decimals)));
|
||||
}
|
||||
|
||||
export function roundTo5(number): number {
|
||||
if (number < 1) {
|
||||
const numString = number.toString();
|
||||
const nonZeroIndex = numString.search(/[1-9]/);
|
||||
if (nonZeroIndex === -1 || nonZeroIndex >= numString.length - 5) {
|
||||
return number;
|
||||
}
|
||||
return Number(numString.slice(0, nonZeroIndex + 5));
|
||||
} else if (number < 10) {
|
||||
return (
|
||||
Math.floor(number) +
|
||||
Number((number % 1).toString().padEnd(10, '0').slice(0, 6))
|
||||
);
|
||||
} else if (number < 100) {
|
||||
return (
|
||||
Math.floor(number) +
|
||||
Number((number % 1).toString().padEnd(10, '0').slice(0, 5))
|
||||
);
|
||||
} else if (number < 1000) {
|
||||
return (
|
||||
Math.floor(number) +
|
||||
Number((number % 1).toString().padEnd(10, '0').slice(0, 4))
|
||||
);
|
||||
} else if (number < 10000) {
|
||||
return (
|
||||
Math.floor(number) +
|
||||
Number((number % 1).toString().padEnd(10, '0').slice(0, 3))
|
||||
);
|
||||
}
|
||||
return Math.round(number);
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
export async function buildFetch(): Promise<
|
||||
(
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit | undefined,
|
||||
) => Promise<Response>
|
||||
> {
|
||||
let fetch = globalThis?.fetch;
|
||||
if (!fetch && process?.versions?.node) {
|
||||
fetch = (await import('node-fetch')).default;
|
||||
}
|
||||
return fetch;
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
///
|
||||
/// web3js extensions
|
||||
///
|
||||
|
@ -84,7 +150,7 @@ export function toUiI80F48(nativeAmount: I80F48, decimals: number): I80F48 {
|
|||
export async function getAssociatedTokenAddress(
|
||||
mint: PublicKey,
|
||||
owner: PublicKey,
|
||||
allowOwnerOffCurve = false,
|
||||
allowOwnerOffCurve = true,
|
||||
programId = TOKEN_PROGRAM_ID,
|
||||
associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
): Promise<PublicKey> {
|
||||
|
|
Loading…
Reference in New Issue