Merge remote-tracking branch 'origin/deploy' into dev

This commit is contained in:
Christian Kamm 2023-08-10 13:32:06 +02:00
commit b2e578bc61
17 changed files with 1280 additions and 227 deletions

View File

@ -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": {

View File

@ -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);
}

View File

@ -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

View File

@ -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();

View File

@ -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';

View File

@ -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')!,

View File

@ -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 {

View File

@ -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 {

View File

@ -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) {}
}

View File

@ -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 {

View File

@ -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];

View File

@ -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',
);

View File

@ -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';

View File

@ -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 () {

View File

@ -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 {

View File

@ -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) => ({

View File

@ -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> {