From bafaf737459e899f5a53cce77eb8573687c0921d Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Fri, 30 Sep 2022 15:07:43 +0200 Subject: [PATCH] Mc/ts numbers - cleanup usage of all numbers (#259) * ts: a higher error tolerance is sufficient Signed-off-by: microwavedcola1 * ts: move stuff around Signed-off-by: microwavedcola1 * ts: string representation while printing Signed-off-by: microwavedcola1 * ts: number cleanup Signed-off-by: microwavedcola1 * ts: fix tsc errors Signed-off-by: microwavedcola1 * ts: cleanup creation of I80F48 from BN Signed-off-by: microwavedcola1 * ts: fixes from review Signed-off-by: microwavedcola1 * ts: fixed from review Signed-off-by: microwavedcola1 * revert Signed-off-by: microwavedcola1 * ts: fix from call Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- ts/client/src/accounts/bank.ts | 58 +++--- ts/client/src/accounts/group.ts | 73 +++----- ts/client/src/accounts/healthCache.spec.ts | 24 +-- ts/client/src/accounts/healthCache.ts | 54 +++--- ts/client/src/accounts/mangoAccount.ts | 102 +++++------ ts/client/src/accounts/oracle.ts | 6 +- ts/client/src/accounts/perp.ts | 36 ++-- ts/client/src/accounts/serum3.ts | 2 +- ts/client/src/client.ts | 21 +-- ts/client/src/development.ts | 76 ++++++++ ts/client/src/index.ts | 2 +- ts/client/src/{accounts => numbers}/I80F48.ts | 2 +- ts/client/src/numbers/numbers.spec.ts | 37 ++++ ts/client/src/utils.ts | 165 +++++------------- 14 files changed, 321 insertions(+), 337 deletions(-) create mode 100644 ts/client/src/development.ts rename ts/client/src/{accounts => numbers}/I80F48.ts (99%) create mode 100644 ts/client/src/numbers/numbers.spec.ts diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index 0bd410447..73f239bac 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -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(); + ); } /** diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index 9f746a461..86e757b18 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -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, public mintInfosMapByTokenIndex: Map, public mintInfosMapByMint: Map, - public vaultAmountsMap: Map, + public vaultAmountsMap: Map, ) {} public async reloadAll(client: MangoClient): Promise { @@ -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 { diff --git a/ts/client/src/accounts/healthCache.spec.ts b/ts/client/src/accounts/healthCache.spec.ts index e0beb5252..22ad523f3 100644 --- a/ts/client/src/accounts/healthCache.spec.ts +++ b/ts/client/src/accounts/healthCache.spec.ts @@ -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'); }); }); diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index b312c5f50..e3dc93af8 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -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)); } diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 9ef7b979b..f38d0eded 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -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, ) {} } diff --git a/ts/client/src/accounts/oracle.ts b/ts/client/src/accounts/oracle.ts index 10420caea..0117a04c3 100644 --- a/ts/client/src/accounts/oracle.ts +++ b/ts/client/src/accounts/oracle.ts @@ -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; } } diff --git a/ts/client/src/accounts/perp.ts b/ts/client/src/accounts/perp.ts index 5bf8fa2d8..986b79918 100644 --- a/ts/client/src/accounts/perp.ts +++ b/ts/client/src/accounts/perp.ts @@ -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() ); } } diff --git a/ts/client/src/accounts/serum3.ts b/ts/client/src/accounts/serum3.ts index 56324e064..38cbc83eb 100644 --- a/ts/client/src/accounts/serum3.ts +++ b/ts/client/src/accounts/serum3.ts @@ -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'>; diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index de4fc78a9..2d5e762a2 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -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 { 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 { 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 { - 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 { 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 */, diff --git a/ts/client/src/development.ts b/ts/client/src/development.ts new file mode 100644 index 000000000..a433f0857 --- /dev/null +++ b/ts/client/src/development.ts @@ -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())); + } + }); +} diff --git a/ts/client/src/index.ts b/ts/client/src/index.ts index f906bec88..c732f4396 100644 --- a/ts/client/src/index.ts +++ b/ts/client/src/index.ts @@ -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, diff --git a/ts/client/src/accounts/I80F48.ts b/ts/client/src/numbers/I80F48.ts similarity index 99% rename from ts/client/src/accounts/I80F48.ts rename to ts/client/src/numbers/I80F48.ts index 978a98848..ee4ce316a 100644 --- a/ts/client/src/accounts/I80F48.ts +++ b/ts/client/src/numbers/I80F48.ts @@ -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); } diff --git a/ts/client/src/numbers/numbers.spec.ts b/ts/client/src/numbers/numbers.spec.ts new file mode 100644 index 000000000..ecb2d25fa --- /dev/null +++ b/ts/client/src/numbers/numbers.spec.ts @@ -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); + }); +}); diff --git a/ts/client/src/utils.ts b/ts/client/src/utils.ts index aa7e18cdf..16cf3a205 100644 --- a/ts/client/src/utils.ts +++ b/ts/client/src/utils.ts @@ -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 { - private static readonly $as$: unique symbol; - private [As.$as$]: Record; +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( - entityName: string, - findMethod: (...x: any) => any, - findArgs: any[], - createMethod: (...x: any) => any, - createArgs: any[], -): Promise { - 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 { + private static readonly $as$: unique symbol; + private [As.$as$]: Record; +}