diff --git a/package.json b/package.json index 35ee08ab1..5c0aaeedf 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "validate": "npm run typecheck && npm run lint && npm run format" }, "devDependencies": { - "@jup-ag/core": "^1.0.0-beta.28", "@tsconfig/recommended": "^1.0.1", "@types/bs58": "^4.0.1", "@types/chai": "^4.3.0", diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index aee5892b0..c0d69f60f 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -59,6 +59,7 @@ export class Bank implements BankForHealth { public util1: I80F48; public _price: I80F48 | undefined; public _uiPrice: number | undefined; + public _oracleLastUpdatedSlot: number | undefined; public collectedFeesNative: I80F48; public loanFeeRate: I80F48; public loanOriginationFeeRate: I80F48; @@ -235,6 +236,7 @@ export class Bank implements BankForHealth { this.dust = I80F48.from(dust); this._price = undefined; this._uiPrice = undefined; + this._oracleLastUpdatedSlot = undefined; } toString(): string { @@ -351,6 +353,15 @@ export class Bank implements BankForHealth { return this._uiPrice; } + get oracleLastUpdatedSlot(): number { + if (!this._oracleLastUpdatedSlot) { + throw new Error( + `Undefined oracleLastUpdatedSlot for bank ${this.publicKey} with tokenIndex ${this.tokenIndex}!`, + ); + } + return this._oracleLastUpdatedSlot; + } + nativeDeposits(): I80F48 { return this.indexedDeposits.mul(this.depositIndex); } diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index 31cac1489..90bc67ba2 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -292,15 +292,17 @@ export class Group { throw new Error( `Undefined accountInfo object in reloadBankOraclePrices for ${bank.oracle}!`, ); - const { price, uiPrice } = await this.decodePriceFromOracleAi( - coder, - bank.oracle, - ai, - this.getMintDecimals(bank.mint), - client, - ); + const { price, uiPrice, lastUpdatedSlot } = + await this.decodePriceFromOracleAi( + coder, + bank.oracle, + ai, + this.getMintDecimals(bank.mint), + client, + ); bank._price = price; bank._uiPrice = uiPrice; + bank._oracleLastUpdatedSlot = lastUpdatedSlot; } } } @@ -324,15 +326,18 @@ export class Group { throw new Error( `Undefined ai object in reloadPerpMarketOraclePrices for ${perpMarket.oracle}!`, ); - const { price, uiPrice } = await this.decodePriceFromOracleAi( - coder, - perpMarket.oracle, - ai, - perpMarket.baseDecimals, - client, - ); + + const { price, uiPrice, lastUpdatedSlot } = + await this.decodePriceFromOracleAi( + coder, + perpMarket.oracle, + ai, + perpMarket.baseDecimals, + client, + ); perpMarket._price = price; perpMarket._uiPrice = uiPrice; + perpMarket._oracleLastUpdatedSlot = lastUpdatedSlot; }), ); } @@ -343,8 +348,8 @@ export class Group { ai: AccountInfo, baseDecimals: number, client: MangoClient, - ): Promise<{ price: I80F48; uiPrice: number }> { - let price, uiPrice; + ): Promise<{ price: I80F48; uiPrice: number; lastUpdatedSlot: number }> { + let price, uiPrice, lastUpdatedSlot; if ( !BorshAccountsCoder.accountDiscriminator('stubOracle').compare( ai.data.slice(0, 8), @@ -353,21 +358,26 @@ export class Group { const stubOracle = coder.decode('stubOracle', ai.data); price = new I80F48(stubOracle.price.val); uiPrice = this.toUiPrice(price, baseDecimals); + lastUpdatedSlot = stubOracle.lastUpdated.val; } else if (isPythOracle(ai)) { - uiPrice = parsePriceData(ai.data).previousPrice; + const priceData = parsePriceData(ai.data); + uiPrice = priceData.previousPrice; price = this.toNativePrice(uiPrice, baseDecimals); + lastUpdatedSlot = parseInt(priceData.lastSlot.toString()); } else if (isSwitchboardOracle(ai)) { - uiPrice = await parseSwitchboardOracle( + const priceData = await parseSwitchboardOracle( ai, client.program.provider.connection, ); + uiPrice = priceData.price; price = this.toNativePrice(uiPrice, baseDecimals); + lastUpdatedSlot = priceData.lastUpdatedSlot; } else { throw new Error( `Unknown oracle provider (parsing not implemented) for oracle ${oracle}, with owner ${ai.owner}!`, ); } - return { price, uiPrice }; + return { price, uiPrice, lastUpdatedSlot }; } public async reloadVaults(client: MangoClient): Promise { diff --git a/ts/client/src/accounts/oracle.ts b/ts/client/src/accounts/oracle.ts index 563c75036..0db7ed24e 100644 --- a/ts/client/src/accounts/oracle.ts +++ b/ts/client/src/accounts/oracle.ts @@ -48,21 +48,29 @@ export class StubOracle { } // https://gist.github.com/microwavedcola1/b741a11e6ee273a859f3ef00b35ac1f0 -export function parseSwitcboardOracleV1( - accountInfo: AccountInfo, -): number { - return accountInfo.data.readDoubleLE(1 + 32 + 4 + 4); +export function parseSwitchboardOracleV1(accountInfo: AccountInfo): { + price: number; + lastUpdatedSlot: number; +} { + const price = accountInfo.data.readDoubleLE(1 + 32 + 4 + 4); + const lastUpdatedSlot = parseInt( + accountInfo.data.readBigUInt64LE(1 + 32 + 4 + 4 + 8).toString(), + ); + return { price, lastUpdatedSlot }; } -export function parseSwitcboardOracleV2( +export function parseSwitchboardOracleV2( program: SwitchboardProgram, accountInfo: AccountInfo, -): number { - const aggregatorAccountData = - program.decodeLatestAggregatorValue(accountInfo); - if (!aggregatorAccountData) +): { price: number; lastUpdatedSlot: number } { + const price = program.decodeLatestAggregatorValue(accountInfo)!.toNumber(); + const lastUpdatedSlot = program + .decodeAggregator(accountInfo) + .latestConfirmedRound!.roundOpenSlot!.toNumber(); + + if (!price || !lastUpdatedSlot) throw new Error('Unable to parse Switchboard Oracle V2'); - return aggregatorAccountData?.toNumber(); + return { price, lastUpdatedSlot }; } /** @@ -73,26 +81,26 @@ export function parseSwitcboardOracleV2( export async function parseSwitchboardOracle( accountInfo: AccountInfo, connection: Connection, -): Promise { +): Promise<{ price: number; lastUpdatedSlot: number }> { if (accountInfo.owner.equals(SwitchboardProgram.devnetPid)) { if (!sbv2DevnetProgram) { sbv2DevnetProgram = await SwitchboardProgram.loadDevnet(connection); } - return parseSwitcboardOracleV2(sbv2DevnetProgram, accountInfo); + return parseSwitchboardOracleV2(sbv2DevnetProgram, accountInfo); } if (accountInfo.owner.equals(SwitchboardProgram.mainnetPid)) { if (!sbv2MainnetProgram) { sbv2MainnetProgram = await SwitchboardProgram.loadMainnet(connection); } - return parseSwitcboardOracleV2(sbv2MainnetProgram, accountInfo); + return parseSwitchboardOracleV2(sbv2MainnetProgram, accountInfo); } if ( accountInfo.owner.equals(SBV1_DEVNET_PID) || accountInfo.owner.equals(SBV1_MAINNET_PID) ) { - return parseSwitcboardOracleV1(accountInfo); + return parseSwitchboardOracleV1(accountInfo); } throw new Error(`Should not be reached!`); diff --git a/ts/client/src/accounts/perp.ts b/ts/client/src/accounts/perp.ts index 66d1213db..6bfe142c0 100644 --- a/ts/client/src/accounts/perp.ts +++ b/ts/client/src/accounts/perp.ts @@ -48,6 +48,7 @@ export class PerpMarket { public _price: I80F48; public _uiPrice: number; + public _oracleLastUpdatedSlot: number; private priceLotsToUiConverter: number; private baseLotsToUiConverter: number; @@ -246,6 +247,15 @@ export class PerpMarket { return this._uiPrice; } + get oracleLastUpdatedSlot(): number { + if (!this._oracleLastUpdatedSlot) { + throw new Error( + `Undefined oracleLastUpdatedSlot for perpMarket ${this.publicKey} with marketIndex ${this.perpMarketIndex}!`, + ); + } + return this._oracleLastUpdatedSlot; + } + get minOrderSize(): number { return this.baseLotsToUiConverter; }