Mc/ts numbers - cleanup usage of all numbers (#259)

* ts: a higher error tolerance is sufficient

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: move stuff around

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: string representation while printing

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: number cleanup

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: fix tsc errors

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: cleanup creation of I80F48 from BN

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: fixed from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* revert

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* ts: fix from call

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2022-09-30 15:07:43 +02:00 committed by GitHub
parent d1079bb1b9
commit bafaf73745
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 321 additions and 337 deletions

View File

@ -1,8 +1,8 @@
import { BN } from '@project-serum/anchor';
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { PublicKey } from '@solana/web3.js';
import { As, nativeI80F48ToUi } from '../utils';
import { I80F48, I80F48Dto, ZERO_I80F48 } from './I80F48';
import { I80F48, I80F48Dto, ZERO_I80F48 } from '../numbers/I80F48';
import { As, toUiDecimals } from '../utils';
export const QUOTE_DECIMALS = 6;
@ -209,61 +209,61 @@ export class Bank implements BankForHealth {
'\n oracle - ' +
this.oracle.toBase58() +
'\n price - ' +
this._price?.toNumber() +
this._price?.toString() +
'\n uiPrice - ' +
this._uiPrice +
'\n deposit index - ' +
this.depositIndex.toNumber() +
this.depositIndex.toString() +
'\n borrow index - ' +
this.borrowIndex.toNumber() +
this.borrowIndex.toString() +
'\n indexedDeposits - ' +
this.indexedDeposits.toNumber() +
this.indexedDeposits.toString() +
'\n indexedBorrows - ' +
this.indexedBorrows.toNumber() +
this.indexedBorrows.toString() +
'\n cachedIndexedTotalDeposits - ' +
this.cachedIndexedTotalDeposits.toNumber() +
this.cachedIndexedTotalDeposits.toString() +
'\n cachedIndexedTotalBorrows - ' +
this.cachedIndexedTotalBorrows.toNumber() +
this.cachedIndexedTotalBorrows.toString() +
'\n indexLastUpdated - ' +
new Date(this.indexLastUpdated.toNumber() * 1000) +
'\n bankRateLastUpdated - ' +
new Date(this.bankRateLastUpdated.toNumber() * 1000) +
'\n avgUtilization - ' +
this.avgUtilization.toNumber() +
this.avgUtilization.toString() +
'\n adjustmentFactor - ' +
this.adjustmentFactor.toNumber() +
this.adjustmentFactor.toString() +
'\n maxRate - ' +
this.maxRate.toNumber() +
this.maxRate.toString() +
'\n util0 - ' +
this.util0.toNumber() +
this.util0.toString() +
'\n rate0 - ' +
this.rate0.toNumber() +
this.rate0.toString() +
'\n util1 - ' +
this.util1.toNumber() +
this.util1.toString() +
'\n rate1 - ' +
this.rate1.toNumber() +
this.rate1.toString() +
'\n loanFeeRate - ' +
this.loanFeeRate.toNumber() +
this.loanFeeRate.toString() +
'\n loanOriginationFeeRate - ' +
this.loanOriginationFeeRate.toNumber() +
this.loanOriginationFeeRate.toString() +
'\n maintAssetWeight - ' +
this.maintAssetWeight.toNumber() +
this.maintAssetWeight.toString() +
'\n initAssetWeight - ' +
this.initAssetWeight.toNumber() +
this.initAssetWeight.toString() +
'\n maintLiabWeight - ' +
this.maintLiabWeight.toNumber() +
this.maintLiabWeight.toString() +
'\n initLiabWeight - ' +
this.initLiabWeight.toNumber() +
this.initLiabWeight.toString() +
'\n liquidationFee - ' +
this.liquidationFee.toNumber() +
this.liquidationFee.toString() +
'\n uiDeposits() - ' +
this.uiDeposits() +
'\n uiBorrows() - ' +
this.uiBorrows() +
'\n getDepositRate() - ' +
this.getDepositRate().toNumber() +
this.getDepositRate().toString() +
'\n getBorrowRate() - ' +
this.getBorrowRate().toNumber()
this.getBorrowRate().toString()
);
}
@ -294,17 +294,17 @@ export class Bank implements BankForHealth {
}
uiDeposits(): number {
return nativeI80F48ToUi(
return toUiDecimals(
this.indexedDeposits.mul(this.depositIndex),
this.mintDecimals,
).toNumber();
);
}
uiBorrows(): number {
return nativeI80F48ToUi(
return toUiDecimals(
this.indexedBorrows.mul(this.borrowIndex),
this.mintDecimals,
).toNumber();
);
}
/**

View File

@ -16,9 +16,9 @@ import BN from 'bn.js';
import { MangoClient } from '../client';
import { SERUM3_PROGRAM_ID } from '../constants';
import { Id } from '../ids';
import { toNativeDecimals, toUiDecimals } from '../utils';
import { I80F48, ONE_I80F48 } from '../numbers/I80F48';
import { toNative, toNativeI80F48, toUiDecimals } from '../utils';
import { Bank, MintInfo, TokenIndex } from './bank';
import { I80F48, ONE_I80F48 } from './I80F48';
import {
isPythOracle,
isSwitchboardOracle,
@ -92,7 +92,7 @@ export class Group {
public perpMarketsMapByName: Map<string, PerpMarket>,
public mintInfosMapByTokenIndex: Map<TokenIndex, MintInfo>,
public mintInfosMapByMint: Map<string, MintInfo>,
public vaultAmountsMap: Map<string, number>,
public vaultAmountsMap: Map<string, BN>,
) {}
public async reloadAll(client: MangoClient): Promise<void> {
@ -382,9 +382,10 @@ export class Group {
if (!vaultAi) {
throw new Error(`Undefined vaultAi for ${vaultPks[i]}`!);
}
const vaultAmount = coder()
.accounts.decode('token', vaultAi.data)
.amount.toNumber();
const vaultAmount = coder().accounts.decode(
'token',
vaultAi.data,
).amount;
return [vaultPks[i].toBase58(), vaultAmount];
}),
);
@ -412,34 +413,28 @@ export class Group {
return banks[0];
}
/**
*
* @param mintPk
* @returns sum of native balances of vaults for all banks for a token (fetched from vaultAmountsMap cache)
*/
public getTokenVaultBalanceByMint(mintPk: PublicKey): I80F48 {
const banks = this.banksMapByMint.get(mintPk.toBase58());
if (!banks) throw new Error(`No bank found for mint ${mintPk}!`);
let totalAmount = 0;
for (const bank of banks) {
const amount = this.vaultAmountsMap.get(bank.vault.toBase58());
if (amount) {
totalAmount += amount;
}
}
return I80F48.fromNumber(totalAmount);
}
/**
*
* @param mintPk
* @returns sum of ui balances of vaults for all banks for a token
*/
public getTokenVaultBalanceByMintUi(mintPk: PublicKey): number {
const vaultBalance = this.getTokenVaultBalanceByMint(mintPk);
const mintDecimals = this.getMintDecimals(mintPk);
const banks = this.banksMapByMint.get(mintPk.toBase58());
if (!banks) {
throw new Error(`No bank found for mint ${mintPk}!`);
}
const totalAmount = new BN(0);
for (const bank of banks) {
const amount = this.vaultAmountsMap.get(bank.vault.toBase58());
if (!amount) {
throw new Error(
`Vault balance not found for bank ${bank.name} ${bank.bankNum}!`,
);
}
totalAmount.iadd(amount);
}
return toUiDecimals(vaultBalance, mintDecimals);
return toUiDecimals(totalAmount, this.getMintDecimals(mintPk));
}
public getSerum3MarketByMarketIndex(marketIndex: MarketIndex): Serum3Market {
@ -575,31 +570,21 @@ export class Group {
}
public toUiPrice(price: I80F48, baseDecimals: number): number {
return price
.mul(
I80F48.fromNumber(
Math.pow(10, baseDecimals - this.getInsuranceMintDecimals()),
),
)
.toNumber();
return toUiDecimals(price, baseDecimals - this.getInsuranceMintDecimals());
}
public toNativePrice(uiPrice: number, baseDecimals: number): I80F48 {
return I80F48.fromNumber(uiPrice).mul(
I80F48.fromNumber(
Math.pow(
10,
// note: our oracles are quoted in USD and our insurance mint is USD
// please update when these assumptions change
this.getInsuranceMintDecimals() - baseDecimals,
),
),
return toNativeI80F48(
uiPrice,
// note: our oracles are quoted in USD and our insurance mint is USD
// please update when these assumptions change
this.getInsuranceMintDecimals() - baseDecimals,
);
}
public toNativeDecimals(uiAmount: number, mintPk: PublicKey): BN {
const decimals = this.getMintDecimals(mintPk);
return toNativeDecimals(uiAmount, decimals);
return toNative(uiAmount, decimals);
}
toString(): string {

View File

@ -1,10 +1,10 @@
import { BN } from '@project-serum/anchor';
import { OpenOrders } from '@project-serum/serum';
import { expect } from 'chai';
import { I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { toUiDecimalsForQuote } from '../utils';
import { BankForHealth, TokenIndex } from './bank';
import { HealthCache, PerpInfo, Serum3Info, TokenInfo } from './healthCache';
import { I80F48, ZERO_I80F48 } from './I80F48';
import { HealthType, PerpPosition } from './mangoAccount';
import { PerpMarket } from './perp';
import { MarketIndex } from './serum3';
@ -81,12 +81,12 @@ describe('Health Cache', () => {
const pM = mockPerpMarket(9, 0.1, 0.2, targetBank.price);
const pp = new PerpPosition(
pM.perpMarketIndex,
3,
new BN(3),
I80F48.fromNumber(-310),
7,
11,
1,
2,
new BN(7),
new BN(11),
new BN(1),
new BN(2),
I80F48.fromNumber(0),
I80F48.fromNumber(0),
);
@ -182,12 +182,12 @@ describe('Health Cache', () => {
const pM = mockPerpMarket(9, 0.1, 0.2, bank2.price);
const pp = new PerpPosition(
pM.perpMarketIndex,
fixture.perp1[0],
new BN(fixture.perp1[0]),
I80F48.fromNumber(fixture.perp1[1]),
fixture.perp1[2],
fixture.perp1[3],
0,
0,
new BN(fixture.perp1[2]),
new BN(fixture.perp1[3]),
new BN(0),
new BN(0),
I80F48.fromNumber(0),
I80F48.fromNumber(0),
);
@ -430,6 +430,6 @@ describe('Health Cache', () => {
I80F48.fromNumber(0.95),
),
).toFixed(3),
).equals('90.477');
).equals('90.176');
});
});

View File

@ -2,15 +2,15 @@ import { BN } from '@project-serum/anchor';
import { OpenOrders } from '@project-serum/serum';
import { PublicKey } from '@solana/web3.js';
import _ from 'lodash';
import { Bank, BankForHealth, TokenIndex } from './bank';
import { Group } from './group';
import {
HUNDRED_I80F48,
I80F48,
I80F48Dto,
MAX_I80F48,
ZERO_I80F48,
} from './I80F48';
} from '../numbers/I80F48';
import { Bank, BankForHealth, TokenIndex } from './bank';
import { Group } from './group';
import { HealthType, MangoAccount, PerpPosition } from './mangoAccount';
import { PerpMarket, PerpOrderSide } from './perp';
@ -735,7 +735,7 @@ export class HealthCache {
const perpInfoIndex = this.getOrCreatePerpInfoIndex(perpMarket);
const perpInfo = this.perpInfos[perpInfoIndex];
const oraclePrice = perpInfo.oraclePrice;
const baseLotSize = I80F48.fromString(perpMarket.baseLotSize.toString());
const baseLotSize = I80F48.fromI64(perpMarket.baseLotSize);
// If the price is sufficiently good then health will just increase from trading
const finalHealthSlope =
@ -940,21 +940,21 @@ export class Serum3Info {
oo: OpenOrders,
): Serum3Info {
// add the amounts that are freely settleable
const baseFree = I80F48.fromString(oo.baseTokenFree.toString());
const baseFree = I80F48.fromI64(oo.baseTokenFree);
// NOTE: referrerRebatesAccrued is not declared on oo class, but the layout
// is aware of it
const quoteFree = I80F48.fromString(
oo.quoteTokenFree.add((oo as any).referrerRebatesAccrued).toString(),
const quoteFree = I80F48.fromI64(
oo.quoteTokenFree.add((oo as any).referrerRebatesAccrued),
);
baseInfo.balance.iadd(baseFree.mul(baseInfo.oraclePrice));
quoteInfo.balance.iadd(quoteFree.mul(quoteInfo.oraclePrice));
// add the reserved amount to both sides, to have the worst-case covered
const reservedBase = I80F48.fromString(
oo.baseTokenTotal.sub(oo.baseTokenFree).toString(),
const reservedBase = I80F48.fromI64(
oo.baseTokenTotal.sub(oo.baseTokenFree),
);
const reservedQuote = I80F48.fromString(
oo.quoteTokenTotal.sub(oo.quoteTokenFree).toString(),
const reservedQuote = I80F48.fromI64(
oo.quoteTokenTotal.sub(oo.quoteTokenFree),
);
const reservedBalance = reservedBase
.mul(baseInfo.oraclePrice)
@ -1059,21 +1059,17 @@ export class PerpInfo {
perpMarket: PerpMarket,
perpPosition: PerpPosition,
): PerpInfo {
const baseLotSize = I80F48.fromString(perpMarket.baseLotSize.toString());
const baseLots = I80F48.fromNumber(
perpPosition.basePositionLots + perpPosition.takerBaseLots,
const baseLotSize = I80F48.fromI64(perpMarket.baseLotSize);
const baseLots = I80F48.fromI64(
perpPosition.basePositionLots.add(perpPosition.takerBaseLots),
);
const unsettledFunding = perpPosition.unsettledFunding(perpMarket);
const takerQuote = I80F48.fromString(
new BN(perpPosition.takerQuoteLots)
.mul(perpMarket.quoteLotSize)
.toString(),
const takerQuote = I80F48.fromI64(
new BN(perpPosition.takerQuoteLots).mul(perpMarket.quoteLotSize),
);
const quoteCurrent = I80F48.fromString(
perpPosition.quotePositionNative.toString(),
)
const quoteCurrent = perpPosition.quotePositionNative
.sub(unsettledFunding)
.add(takerQuote);
@ -1118,26 +1114,18 @@ export class PerpInfo {
// scenario1 < scenario2
// iff abs(bidsNetLots) > abs(asksNetLots)
const bidsNetLots = baseLots.add(
I80F48.fromNumber(perpPosition.bidsBaseLots),
);
const asksNetLots = baseLots.sub(
I80F48.fromNumber(perpPosition.asksBaseLots),
);
const bidsNetLots = baseLots.add(I80F48.fromI64(perpPosition.bidsBaseLots));
const asksNetLots = baseLots.sub(I80F48.fromI64(perpPosition.asksBaseLots));
const lotsToQuote = baseLotSize.mul(perpMarket.price);
let base, quote;
if (bidsNetLots.abs().gt(asksNetLots.abs())) {
const bidsBaseLots = I80F48.fromString(
perpPosition.bidsBaseLots.toString(),
);
const bidsBaseLots = I80F48.fromI64(perpPosition.bidsBaseLots);
base = bidsNetLots.mul(lotsToQuote);
quote = quoteCurrent.sub(bidsBaseLots.mul(lotsToQuote));
} else {
const asksBaseLots = I80F48.fromString(
perpPosition.asksBaseLots.toString(),
);
const asksBaseLots = I80F48.fromI64(perpPosition.asksBaseLots);
base = asksNetLots.mul(lotsToQuote);
quote = quoteCurrent.add(asksBaseLots.mul(lotsToQuote));
}

View File

@ -4,16 +4,11 @@ import { OpenOrders, Order, Orderbook } from '@project-serum/serum/lib/market';
import { PublicKey } from '@solana/web3.js';
import { MangoClient } from '../client';
import { SERUM3_PROGRAM_ID } from '../constants';
import {
nativeI80F48ToUi,
toNative,
toUiDecimals,
toUiDecimalsForQuote,
} from '../utils';
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { toNativeI80F48, toUiDecimals, toUiDecimalsForQuote } from '../utils';
import { Bank, TokenIndex } from './bank';
import { Group } from './group';
import { HealthCache } from './healthCache';
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48';
import { PerpMarket, PerpMarketIndex, PerpOrder, PerpOrderSide } from './perp';
import { MarketIndex, Serum3Side } from './serum3';
export class MangoAccount {
@ -262,13 +257,9 @@ export class MangoAccount {
* @param healthType
* @returns health ratio, in percentage form, capped to 100
*/
getHealthRatioUi(group: Group, healthType: HealthType): number | undefined {
getHealthRatioUi(group: Group, healthType: HealthType): number {
const ratio = this.getHealthRatio(group, healthType).toNumber();
if (ratio) {
return ratio > 100 ? 100 : Math.trunc(ratio);
} else {
return undefined;
}
return ratio > 100 ? 100 : Math.trunc(ratio);
}
/**
@ -287,19 +278,15 @@ export class MangoAccount {
const baseBank = group.getFirstBankByTokenIndex(sp.baseTokenIndex);
tokensMap
.get(baseBank.tokenIndex)!
.iadd(
I80F48.fromString(oo.baseTokenTotal.toString()).mul(baseBank.price),
);
.iadd(I80F48.fromI64(oo.baseTokenTotal).mul(baseBank.price));
const quoteBank = group.getFirstBankByTokenIndex(sp.quoteTokenIndex);
// NOTE: referrerRebatesAccrued is not declared on oo class, but the layout
// is aware of it
tokensMap
.get(baseBank.tokenIndex)!
.iadd(
I80F48.fromString(
oo.quoteTokenTotal
.add((oo as any).referrerRebatesAccrued)
.toString(),
I80F48.fromI64(
oo.quoteTokenTotal.add((oo as any).referrerRebatesAccrued),
).mul(quoteBank.price),
);
}
@ -477,7 +464,7 @@ export class MangoAccount {
): number | undefined {
const nativeTokenChanges = uiTokenChanges.map((tokenChange) => {
return {
nativeTokenAmount: toNative(
nativeTokenAmount: toNativeI80F48(
tokenChange.uiTokenAmount,
group.getMintDecimals(tokenChange.mintPk),
),
@ -634,7 +621,7 @@ export class MangoAccount {
.simHealthRatioWithSerum3BidChanges(
baseBank,
quoteBank,
toNative(
toNativeI80F48(
uiQuoteAmount,
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex)
.mintDecimals,
@ -672,7 +659,7 @@ export class MangoAccount {
.simHealthRatioWithSerum3AskChanges(
baseBank,
quoteBank,
toNative(
toNativeI80F48(
uiBaseAmount,
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
.mintDecimals,
@ -703,11 +690,9 @@ export class MangoAccount {
I80F48.fromNumber(2),
group.toNativePrice(uiPrice, perpMarket.baseDecimals),
);
const nativeBase = baseLots.mul(
I80F48.fromString(perpMarket.baseLotSize.toString()),
);
const nativeBase = baseLots.mul(I80F48.fromI64(perpMarket.baseLotSize));
const nativeQuote = nativeBase.mul(perpMarket.price);
return toUiDecimalsForQuote(nativeQuote.toNumber());
return toUiDecimalsForQuote(nativeQuote);
}
/**
@ -856,7 +841,7 @@ export class TokenPosition {
* @returns UI balance, is signed
*/
public balanceUi(bank: Bank): number {
return nativeI80F48ToUi(this.balance(bank), bank.mintDecimals).toNumber();
return toUiDecimals(this.balance(bank), bank.mintDecimals);
}
/**
@ -864,7 +849,7 @@ export class TokenPosition {
* @returns UI deposits, 0 if position has borrows
*/
public depositsUi(bank: Bank): number {
return nativeI80F48ToUi(this.deposits(bank), bank.mintDecimals).toNumber();
return toUiDecimals(this.deposits(bank), bank.mintDecimals);
}
/**
@ -872,7 +857,7 @@ export class TokenPosition {
* @returns UI borrows, 0 if position has deposits
*/
public borrowsUi(bank: Bank): number {
return nativeI80F48ToUi(this.borrows(bank), bank.mintDecimals).toNumber();
return toUiDecimals(this.borrows(bank), bank.mintDecimals);
}
public toString(group?: Group, index?: number): string {
@ -947,12 +932,12 @@ export class PerpPosition {
static from(dto: PerpPositionDto): PerpPosition {
return new PerpPosition(
dto.marketIndex as PerpMarketIndex,
dto.basePositionLots.toNumber(),
dto.basePositionLots,
I80F48.from(dto.quotePositionNative),
dto.bidsBaseLots.toNumber(),
dto.asksBaseLots.toNumber(),
dto.takerBaseLots.toNumber(),
dto.takerQuoteLots.toNumber(),
dto.bidsBaseLots,
dto.asksBaseLots,
dto.takerBaseLots,
dto.takerQuoteLots,
I80F48.from(dto.longSettledFunding),
I80F48.from(dto.shortSettledFunding),
);
@ -960,12 +945,12 @@ export class PerpPosition {
constructor(
public marketIndex: PerpMarketIndex,
public basePositionLots: number,
public basePositionLots: BN,
public quotePositionNative: I80F48,
public bidsBaseLots: number,
public asksBaseLots: number,
public takerBaseLots: number,
public takerQuoteLots: number,
public bidsBaseLots: BN,
public asksBaseLots: BN,
public takerBaseLots: BN,
public takerQuoteLots: BN,
public longSettledFunding: I80F48,
public shortSettledFunding: I80F48,
) {}
@ -975,32 +960,32 @@ export class PerpPosition {
}
public unsettledFunding(perpMarket: PerpMarket): I80F48 {
if (this.basePositionLots > 0) {
if (this.basePositionLots.gt(new BN(0))) {
return perpMarket.longFunding
.sub(this.longSettledFunding)
.mul(I80F48.fromString(this.basePositionLots.toString()));
} else if (this.basePositionLots < 0) {
.mul(I80F48.fromI64(this.basePositionLots));
} else if (this.basePositionLots.lt(new BN(0))) {
return perpMarket.shortFunding
.sub(this.shortSettledFunding)
.mul(I80F48.fromString(this.basePositionLots.toString()));
.mul(I80F48.fromI64(this.basePositionLots));
}
return ZERO_I80F48();
}
public getEquity(perpMarket: PerpMarket): I80F48 {
const lotsToQuote = I80F48.fromString(
perpMarket.baseLotSize.toString(),
).mul(perpMarket.price);
const lotsToQuote = I80F48.fromI64(perpMarket.baseLotSize).mul(
perpMarket.price,
);
const baseLots = I80F48.fromNumber(
this.basePositionLots + this.takerBaseLots,
const baseLots = I80F48.fromI64(
this.basePositionLots.add(this.takerBaseLots),
);
const unsettledFunding = this.unsettledFunding(perpMarket);
const takerQuote = I80F48.fromString(
new BN(this.takerQuoteLots).mul(perpMarket.quoteLotSize).toString(),
const takerQuote = I80F48.fromI64(
new BN(this.takerQuoteLots).mul(perpMarket.quoteLotSize),
);
const quoteCurrent = I80F48.fromString(this.quotePositionNative.toString())
const quoteCurrent = this.quotePositionNative
.sub(unsettledFunding)
.add(takerQuote);
@ -1008,11 +993,12 @@ export class PerpPosition {
}
public hasOpenOrders(): boolean {
const zero = new BN(0);
return (
this.asksBaseLots != 0 ||
this.bidsBaseLots != 0 ||
this.takerBaseLots != 0 ||
this.takerQuoteLots != 0
!this.asksBaseLots.eq(zero) ||
!this.bidsBaseLots.eq(zero) ||
!this.takerBaseLots.eq(zero) ||
!this.takerQuoteLots.eq(zero)
);
}
}
@ -1038,7 +1024,7 @@ export class PerpOo {
return new PerpOo(
dto.orderSide,
dto.orderMarket,
dto.clientOrderId.toNumber(),
dto.clientOrderId,
dto.orderId,
);
}
@ -1046,7 +1032,7 @@ export class PerpOo {
constructor(
public orderSide: any,
public orderMarket: 0,
public clientOrderId: number,
public clientOrderId: BN,
public orderId: BN,
) {}
}

View File

@ -7,7 +7,7 @@ import {
SwitchboardDecimal,
} from '@switchboard-xyz/switchboard-v2';
import BN from 'bn.js';
import { I80F48, I80F48Dto } from './I80F48';
import { I80F48, I80F48Dto } from '../numbers/I80F48';
const SBV1_DEVNET_PID = new PublicKey(
'7azgmy1pFXHikv36q1zZASvFq5vFa39TT9NweVugKKTU',
@ -20,7 +20,7 @@ let sbv2MainnetProgram;
export class StubOracle {
public price: I80F48;
public lastUpdated: number;
public lastUpdated: BN;
static from(
publicKey: PublicKey,
@ -48,7 +48,7 @@ export class StubOracle {
lastUpdated: BN,
) {
this.price = I80F48.from(price);
this.lastUpdated = lastUpdated.toNumber();
this.lastUpdated = lastUpdated;
}
}

View File

@ -3,9 +3,9 @@ import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { PublicKey } from '@solana/web3.js';
import Big from 'big.js';
import { MangoClient } from '../client';
import { As, U64_MAX_BN } from '../utils';
import { I80F48, I80F48Dto } from '../numbers/I80F48';
import { As, toNative, U64_MAX_BN } from '../utils';
import { OracleConfig, QUOTE_DECIMALS } from './bank';
import { I80F48, I80F48Dto } from './I80F48';
export type PerpMarketIndex = number & As<'perp-market-index'>;
@ -22,8 +22,8 @@ export class PerpMarket {
public maxFunding: I80F48;
public longFunding: I80F48;
public shortFunding: I80F48;
public openInterest: number;
public seqNum: number;
public openInterest: BN;
public seqNum: BN;
public feesAccrued: I80F48;
priceLotsToUiConverter: number;
baseLotsToUiConverter: number;
@ -146,8 +146,8 @@ export class PerpMarket {
this.maxFunding = I80F48.from(maxFunding);
this.longFunding = I80F48.from(longFunding);
this.shortFunding = I80F48.from(shortFunding);
this.openInterest = openInterest.toNumber();
this.seqNum = seqNum.toNumber();
this.openInterest = openInterest;
this.seqNum = seqNum;
this.feesAccrued = I80F48.from(feesAccrued);
this.priceLotsToUiConverter = new Big(10)
@ -241,21 +241,17 @@ export class PerpMarket {
}
public uiPriceToLots(price: number): BN {
return new BN(price * Math.pow(10, QUOTE_DECIMALS))
return toNative(price, QUOTE_DECIMALS)
.mul(this.baseLotSize)
.div(this.quoteLotSize.mul(new BN(Math.pow(10, this.baseDecimals))));
}
public uiBaseToLots(quantity: number): BN {
return new BN(quantity * Math.pow(10, this.baseDecimals)).div(
this.baseLotSize,
);
return toNative(quantity, this.baseDecimals).div(this.baseLotSize);
}
public uiQuoteToLots(uiQuote: number): BN {
return new BN(uiQuote * Math.pow(10, QUOTE_DECIMALS)).div(
this.quoteLotSize,
);
return toNative(uiQuote, QUOTE_DECIMALS).div(this.quoteLotSize);
}
public priceLotsToUi(price: BN): number {
@ -276,19 +272,19 @@ export class PerpMarket {
'\n perpMarketIndex -' +
this.perpMarketIndex +
'\n maintAssetWeight -' +
this.maintAssetWeight.toNumber() +
this.maintAssetWeight.toString() +
'\n initAssetWeight -' +
this.initAssetWeight.toNumber() +
this.initAssetWeight.toString() +
'\n maintLiabWeight -' +
this.maintLiabWeight.toNumber() +
this.maintLiabWeight.toString() +
'\n initLiabWeight -' +
this.initLiabWeight.toNumber() +
this.initLiabWeight.toString() +
'\n liquidationFee -' +
this.liquidationFee.toNumber() +
this.liquidationFee.toString() +
'\n makerFee -' +
this.makerFee.toNumber() +
this.makerFee.toString() +
'\n takerFee -' +
this.takerFee.toNumber()
this.takerFee.toString()
);
}
}

View File

@ -7,7 +7,7 @@ import { SERUM3_PROGRAM_ID } from '../constants';
import { As } from '../utils';
import { TokenIndex } from './bank';
import { Group } from './group';
import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from './I80F48';
import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
export type MarketIndex = number & As<'market-index'>;

View File

@ -26,7 +26,6 @@ import {
import bs58 from 'bs58';
import { Bank, MintInfo, TokenIndex } from './accounts/bank';
import { Group } from './accounts/group';
import { I80F48 } from './accounts/I80F48';
import {
MangoAccount,
PerpPosition,
@ -52,12 +51,13 @@ import {
import { SERUM3_PROGRAM_ID } from './constants';
import { Id } from './ids';
import { IDL, MangoV4 } from './mango_v4';
import { I80F48 } from './numbers/I80F48';
import { FlashLoanType, InterestRateParams } from './types';
import {
createAssociatedTokenAccountIdempotentInstruction,
getAssociatedTokenAddress,
I64_MAX_BN,
toNativeDecimals,
toNative,
} from './utils';
import { sendTransaction } from './utils/rpc';
@ -741,7 +741,7 @@ export class MangoClient {
amount: number,
): Promise<TransactionSignature> {
const decimals = group.getMintDecimals(mintPk);
const nativeAmount = toNativeDecimals(amount, decimals).toNumber();
const nativeAmount = toNative(amount, decimals);
return await this.tokenDepositNative(
group,
mangoAccount,
@ -754,7 +754,7 @@ export class MangoClient {
group: Group,
mangoAccount: MangoAccount,
mintPk: PublicKey,
nativeAmount: number,
nativeAmount: BN,
): Promise<TransactionSignature> {
const bank = group.getFirstBankByMint(mintPk);
@ -769,13 +769,13 @@ export class MangoClient {
const additionalSigners: Signer[] = [];
if (mintPk.equals(WRAPPED_SOL_MINT)) {
wrappedSolAccount = new Keypair();
const lamports = nativeAmount + 1e7;
const lamports = nativeAmount.add(new BN(1e7));
preInstructions = [
SystemProgram.createAccount({
fromPubkey: mangoAccount.owner,
newAccountPubkey: wrappedSolAccount.publicKey,
lamports,
lamports: lamports.toNumber(),
space: 165,
programId: TOKEN_PROGRAM_ID,
}),
@ -841,10 +841,7 @@ export class MangoClient {
amount: number,
allowBorrow: boolean,
): Promise<TransactionSignature> {
const nativeAmount = toNativeDecimals(
amount,
group.getMintDecimals(mintPk),
).toNumber();
const nativeAmount = toNative(amount, group.getMintDecimals(mintPk));
return await this.tokenWithdrawNative(
group,
mangoAccount,
@ -858,7 +855,7 @@ export class MangoClient {
group: Group,
mangoAccount: MangoAccount,
mintPk: PublicKey,
nativeAmount: number,
nativeAmount: BN,
allowBorrow: boolean,
): Promise<TransactionSignature> {
const bank = group.getFirstBankByMint(mintPk);
@ -1852,7 +1849,7 @@ export class MangoClient {
const flashLoanBeginIx = await this.program.methods
.flashLoanBegin([
toNativeDecimals(amountIn, inputBank.mintDecimals),
toNative(amountIn, inputBank.mintDecimals),
new BN(
0,
) /* we don't care about borrowing the target amount, this is just a dummy */,

View File

@ -0,0 +1,76 @@
///
/// debugging
///
import { AccountMeta, PublicKey } from '@solana/web3.js';
import { Bank } from './accounts/bank';
import { Group } from './accounts/group';
import { MangoAccount, Serum3Orders } from './accounts/mangoAccount';
import { PerpMarket } from './accounts/perp';
export function debugAccountMetas(ams: AccountMeta[]): void {
for (const am of ams) {
console.log(
`${am.pubkey.toBase58()}, isSigner: ${am.isSigner
.toString()
.padStart(5, ' ')}, isWritable - ${am.isWritable
.toString()
.padStart(5, ' ')}`,
);
}
}
export function debugHealthAccounts(
group: Group,
mangoAccount: MangoAccount,
publicKeys: PublicKey[],
): void {
const banks = new Map(
Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [
banks[0].publicKey.toBase58(),
`${banks[0].name} bank`,
]),
);
const oracles = new Map(
Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [
banks[0].oracle.toBase58(),
`${banks[0].name} oracle`,
]),
);
const serum3 = new Map(
mangoAccount.serum3Active().map((serum3: Serum3Orders) => {
const serum3Market = Array.from(
group.serum3MarketsMapByExternal.values(),
).find((serum3Market) => serum3Market.marketIndex === serum3.marketIndex);
if (!serum3Market) {
throw new Error(
`Serum3Orders for non existent market with market index ${serum3.marketIndex}`,
);
}
return [serum3.openOrders.toBase58(), `${serum3Market.name} spot oo`];
}),
);
const perps = new Map(
Array.from(group.perpMarketsMapByName.values()).map(
(perpMarket: PerpMarket) => [
perpMarket.publicKey.toBase58(),
`${perpMarket.name} perp market`,
],
),
);
publicKeys.map((pk) => {
if (banks.get(pk.toBase58())) {
console.log(banks.get(pk.toBase58()));
}
if (oracles.get(pk.toBase58())) {
console.log(oracles.get(pk.toBase58()));
}
if (serum3.get(pk.toBase58())) {
console.log(serum3.get(pk.toBase58()));
}
if (perps.get(pk.toBase58())) {
console.log(perps.get(pk.toBase58()));
}
});
}

View File

@ -4,7 +4,7 @@ import { MangoClient } from './client';
import { MANGO_V4_ID } from './constants';
export * from './accounts/bank';
export * from './accounts/I80F48';
export * from './numbers/I80F48';
export * from './accounts/mangoAccount';
export {
Serum3Market,

View File

@ -45,7 +45,7 @@ export class I80F48 {
}
static fromNumber(x: number): I80F48 {
const int_part = Math.trunc(x);
const v = new BN(int_part).iushln(48);
const v = new BN(int_part.toFixed(0)).iushln(48);
v.iadd(new BN((x - int_part) * I80F48.MULTIPLIER_NUMBER));
return new I80F48(v);
}

View File

@ -0,0 +1,37 @@
import BN from 'bn.js';
import { expect } from 'chai';
import { U64_MAX_BN } from '../utils';
import { I80F48 } from './I80F48';
describe('Math', () => {
it('js number to BN and I80F48', () => {
// BN can be only be created from js numbers which are <=2^53
expect(function () {
new BN(0x1fffffffffffff);
}).to.not.throw('Assertion failed');
expect(function () {
new BN(0x20000000000000);
}).to.throw('Assertion failed');
// max BN cant be converted to a number
expect(function () {
U64_MAX_BN.toNumber();
}).to.throw('Number can only safely store up to 53 bits');
// max I80F48 can be converted to a number
// though, the number is represented in scientific notation
// anything above ^20 gets represented with scientific notation
expect(
I80F48.fromString('604462909807314587353087.999999999999996')
.toNumber()
.toString(),
).equals('6.044629098073146e+23');
// I80F48 constructor takes a BN, but it doesnt do what one might think it does
expect(new I80F48(new BN(10)).toNumber()).not.equals(10);
expect(I80F48.fromI64(new BN(10)).toNumber()).equals(10);
// BN treats input as whole integer
expect(new BN(1.5).toNumber()).equals(1);
});
});

View File

@ -4,7 +4,6 @@ import {
TOKEN_PROGRAM_ID,
} from '@solana/spl-token';
import {
AccountMeta,
AddressLookupTableAccount,
MessageV0,
PublicKey,
@ -14,107 +13,51 @@ import {
VersionedTransaction,
} from '@solana/web3.js';
import BN from 'bn.js';
import { Bank, QUOTE_DECIMALS } from './accounts/bank';
import { Group } from './accounts/group';
import { I80F48 } from './accounts/I80F48';
import { MangoAccount, Serum3Orders } from './accounts/mangoAccount';
import { PerpMarket } from './accounts/perp';
import { QUOTE_DECIMALS } from './accounts/bank';
import { I80F48 } from './numbers/I80F48';
///
/// numeric helpers
///
export const U64_MAX_BN = new BN('18446744073709551615');
export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64);
// https://stackoverflow.com/questions/70261755/user-defined-type-guard-function-and-type-narrowing-to-more-specific-type/70262876#70262876
export declare abstract class As<Tag extends keyof never> {
private static readonly $as$: unique symbol;
private [As.$as$]: Record<Tag, true>;
export function toNativeI80F48(uiAmount: number, decimals: number): I80F48 {
return I80F48.fromNumber(uiAmount * Math.pow(10, decimals));
}
export function debugAccountMetas(ams: AccountMeta[]): void {
for (const am of ams) {
console.log(
`${am.pubkey.toBase58()}, isSigner: ${am.isSigner
.toString()
.padStart(5, ' ')}, isWritable - ${am.isWritable
.toString()
.padStart(5, ' ')}`,
);
export function toNative(uiAmount: number, decimals: number): BN {
return new BN((uiAmount * Math.pow(10, decimals)).toFixed(0));
}
export function toUiDecimals(
nativeAmount: BN | I80F48 | number,
decimals: number,
): number {
if (nativeAmount instanceof BN) {
return nativeAmount.div(new BN(Math.pow(10, decimals))).toNumber();
} else if (nativeAmount instanceof I80F48) {
return nativeAmount
.div(I80F48.fromNumber(Math.pow(10, decimals)))
.toNumber();
}
return nativeAmount / Math.pow(10, decimals);
}
export function debugHealthAccounts(
group: Group,
mangoAccount: MangoAccount,
publicKeys: PublicKey[],
): void {
const banks = new Map(
Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [
banks[0].publicKey.toBase58(),
`${banks[0].name} bank`,
]),
);
const oracles = new Map(
Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [
banks[0].oracle.toBase58(),
`${banks[0].name} oracle`,
]),
);
const serum3 = new Map(
mangoAccount.serum3Active().map((serum3: Serum3Orders) => {
const serum3Market = Array.from(
group.serum3MarketsMapByExternal.values(),
).find((serum3Market) => serum3Market.marketIndex === serum3.marketIndex);
if (!serum3Market) {
throw new Error(
`Serum3Orders for non existent market with market index ${serum3.marketIndex}`,
);
}
return [serum3.openOrders.toBase58(), `${serum3Market.name} spot oo`];
}),
);
const perps = new Map(
Array.from(group.perpMarketsMapByName.values()).map(
(perpMarket: PerpMarket) => [
perpMarket.publicKey.toBase58(),
`${perpMarket.name} perp market`,
],
),
);
publicKeys.map((pk) => {
if (banks.get(pk.toBase58())) {
console.log(banks.get(pk.toBase58()));
}
if (oracles.get(pk.toBase58())) {
console.log(oracles.get(pk.toBase58()));
}
if (serum3.get(pk.toBase58())) {
console.log(serum3.get(pk.toBase58()));
}
if (perps.get(pk.toBase58())) {
console.log(perps.get(pk.toBase58()));
}
});
export function toUiDecimalsForQuote(
nativeAmount: BN | I80F48 | number,
): number {
return toUiDecimals(nativeAmount, QUOTE_DECIMALS);
}
export async function findOrCreate<T>(
entityName: string,
findMethod: (...x: any) => any,
findArgs: any[],
createMethod: (...x: any) => any,
createArgs: any[],
): Promise<T> {
let many: T[] = await findMethod(...findArgs);
let one: T;
if (many.length > 0) {
one = many[0];
return one;
}
await createMethod(...createArgs);
many = await findMethod(...findArgs);
one = many[0];
return one;
export function toUiI80F48(nativeAmount: I80F48, decimals: number): I80F48 {
return nativeAmount.div(I80F48.fromNumber(Math.pow(10, decimals)));
}
///
/// web3js extensions
///
/**
* Get the address of the associated token account for a given mint and owner
*
@ -168,40 +111,6 @@ export async function createAssociatedTokenAccountIdempotentInstruction(
});
}
export function toNative(uiAmount: number, decimals: number): I80F48 {
return I80F48.fromNumber(uiAmount).mul(
I80F48.fromNumber(Math.pow(10, decimals)),
);
}
export function toNativeDecimals(amount: number, decimals: number): BN {
return new BN(Math.trunc(amount * Math.pow(10, decimals)));
}
export function toUiDecimals(
amount: I80F48 | number,
decimals: number,
): number {
amount = amount instanceof I80F48 ? amount.toNumber() : amount;
return amount / Math.pow(10, decimals);
}
export function toUiDecimalsForQuote(amount: I80F48 | number): number {
amount = amount instanceof I80F48 ? amount.toNumber() : amount;
return amount / Math.pow(10, QUOTE_DECIMALS);
}
export function toU64(amount: number, decimals: number): BN {
const bn = toNativeDecimals(amount, decimals).toString();
console.log('bn', bn);
return new BN(bn);
}
export function nativeI80F48ToUi(amount: I80F48, decimals: number): I80F48 {
return amount.div(I80F48.fromNumber(Math.pow(10, decimals)));
}
export async function buildVersionedTx(
provider: AnchorProvider,
ix: TransactionInstruction[],
@ -222,3 +131,13 @@ export async function buildVersionedTx(
]);
return vTx;
}
///
/// ts extension
///
// https://stackoverflow.com/questions/70261755/user-defined-type-guard-function-and-type-narrowing-to-more-specific-type/70262876#70262876
export declare abstract class As<Tag extends keyof never> {
private static readonly $as$: unique symbol;
private [As.$as$]: Record<Tag, true>;
}