Add oracleLastUpdatedSlot to Bank and PerpMarket (#395)

* Parse last updated slot from oracle

* Fix typo

* Remove unused package
This commit is contained in:
riordanp 2023-01-17 18:03:16 +00:00 committed by GitHub
parent 71b13efc95
commit 2f78bd336f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 72 additions and 34 deletions

View File

@ -32,7 +32,6 @@
"validate": "npm run typecheck && npm run lint && npm run format" "validate": "npm run typecheck && npm run lint && npm run format"
}, },
"devDependencies": { "devDependencies": {
"@jup-ag/core": "^1.0.0-beta.28",
"@tsconfig/recommended": "^1.0.1", "@tsconfig/recommended": "^1.0.1",
"@types/bs58": "^4.0.1", "@types/bs58": "^4.0.1",
"@types/chai": "^4.3.0", "@types/chai": "^4.3.0",

View File

@ -59,6 +59,7 @@ export class Bank implements BankForHealth {
public util1: I80F48; public util1: I80F48;
public _price: I80F48 | undefined; public _price: I80F48 | undefined;
public _uiPrice: number | undefined; public _uiPrice: number | undefined;
public _oracleLastUpdatedSlot: number | undefined;
public collectedFeesNative: I80F48; public collectedFeesNative: I80F48;
public loanFeeRate: I80F48; public loanFeeRate: I80F48;
public loanOriginationFeeRate: I80F48; public loanOriginationFeeRate: I80F48;
@ -235,6 +236,7 @@ export class Bank implements BankForHealth {
this.dust = I80F48.from(dust); this.dust = I80F48.from(dust);
this._price = undefined; this._price = undefined;
this._uiPrice = undefined; this._uiPrice = undefined;
this._oracleLastUpdatedSlot = undefined;
} }
toString(): string { toString(): string {
@ -351,6 +353,15 @@ export class Bank implements BankForHealth {
return this._uiPrice; 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 { nativeDeposits(): I80F48 {
return this.indexedDeposits.mul(this.depositIndex); return this.indexedDeposits.mul(this.depositIndex);
} }

View File

@ -292,15 +292,17 @@ export class Group {
throw new Error( throw new Error(
`Undefined accountInfo object in reloadBankOraclePrices for ${bank.oracle}!`, `Undefined accountInfo object in reloadBankOraclePrices for ${bank.oracle}!`,
); );
const { price, uiPrice } = await this.decodePriceFromOracleAi( const { price, uiPrice, lastUpdatedSlot } =
coder, await this.decodePriceFromOracleAi(
bank.oracle, coder,
ai, bank.oracle,
this.getMintDecimals(bank.mint), ai,
client, this.getMintDecimals(bank.mint),
); client,
);
bank._price = price; bank._price = price;
bank._uiPrice = uiPrice; bank._uiPrice = uiPrice;
bank._oracleLastUpdatedSlot = lastUpdatedSlot;
} }
} }
} }
@ -324,15 +326,18 @@ export class Group {
throw new Error( throw new Error(
`Undefined ai object in reloadPerpMarketOraclePrices for ${perpMarket.oracle}!`, `Undefined ai object in reloadPerpMarketOraclePrices for ${perpMarket.oracle}!`,
); );
const { price, uiPrice } = await this.decodePriceFromOracleAi(
coder, const { price, uiPrice, lastUpdatedSlot } =
perpMarket.oracle, await this.decodePriceFromOracleAi(
ai, coder,
perpMarket.baseDecimals, perpMarket.oracle,
client, ai,
); perpMarket.baseDecimals,
client,
);
perpMarket._price = price; perpMarket._price = price;
perpMarket._uiPrice = uiPrice; perpMarket._uiPrice = uiPrice;
perpMarket._oracleLastUpdatedSlot = lastUpdatedSlot;
}), }),
); );
} }
@ -343,8 +348,8 @@ export class Group {
ai: AccountInfo<Buffer>, ai: AccountInfo<Buffer>,
baseDecimals: number, baseDecimals: number,
client: MangoClient, client: MangoClient,
): Promise<{ price: I80F48; uiPrice: number }> { ): Promise<{ price: I80F48; uiPrice: number; lastUpdatedSlot: number }> {
let price, uiPrice; let price, uiPrice, lastUpdatedSlot;
if ( if (
!BorshAccountsCoder.accountDiscriminator('stubOracle').compare( !BorshAccountsCoder.accountDiscriminator('stubOracle').compare(
ai.data.slice(0, 8), ai.data.slice(0, 8),
@ -353,21 +358,26 @@ export class Group {
const stubOracle = coder.decode('stubOracle', ai.data); const stubOracle = coder.decode('stubOracle', ai.data);
price = new I80F48(stubOracle.price.val); price = new I80F48(stubOracle.price.val);
uiPrice = this.toUiPrice(price, baseDecimals); uiPrice = this.toUiPrice(price, baseDecimals);
lastUpdatedSlot = stubOracle.lastUpdated.val;
} else if (isPythOracle(ai)) { } else if (isPythOracle(ai)) {
uiPrice = parsePriceData(ai.data).previousPrice; const priceData = parsePriceData(ai.data);
uiPrice = priceData.previousPrice;
price = this.toNativePrice(uiPrice, baseDecimals); price = this.toNativePrice(uiPrice, baseDecimals);
lastUpdatedSlot = parseInt(priceData.lastSlot.toString());
} else if (isSwitchboardOracle(ai)) { } else if (isSwitchboardOracle(ai)) {
uiPrice = await parseSwitchboardOracle( const priceData = await parseSwitchboardOracle(
ai, ai,
client.program.provider.connection, client.program.provider.connection,
); );
uiPrice = priceData.price;
price = this.toNativePrice(uiPrice, baseDecimals); price = this.toNativePrice(uiPrice, baseDecimals);
lastUpdatedSlot = priceData.lastUpdatedSlot;
} else { } else {
throw new Error( throw new Error(
`Unknown oracle provider (parsing not implemented) for oracle ${oracle}, with owner ${ai.owner}!`, `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<void> { public async reloadVaults(client: MangoClient): Promise<void> {

View File

@ -48,21 +48,29 @@ export class StubOracle {
} }
// https://gist.github.com/microwavedcola1/b741a11e6ee273a859f3ef00b35ac1f0 // https://gist.github.com/microwavedcola1/b741a11e6ee273a859f3ef00b35ac1f0
export function parseSwitcboardOracleV1( export function parseSwitchboardOracleV1(accountInfo: AccountInfo<Buffer>): {
accountInfo: AccountInfo<Buffer>, price: number;
): number { lastUpdatedSlot: number;
return accountInfo.data.readDoubleLE(1 + 32 + 4 + 4); } {
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, program: SwitchboardProgram,
accountInfo: AccountInfo<Buffer>, accountInfo: AccountInfo<Buffer>,
): number { ): { price: number; lastUpdatedSlot: number } {
const aggregatorAccountData = const price = program.decodeLatestAggregatorValue(accountInfo)!.toNumber();
program.decodeLatestAggregatorValue(accountInfo); const lastUpdatedSlot = program
if (!aggregatorAccountData) .decodeAggregator(accountInfo)
.latestConfirmedRound!.roundOpenSlot!.toNumber();
if (!price || !lastUpdatedSlot)
throw new Error('Unable to parse Switchboard Oracle V2'); 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( export async function parseSwitchboardOracle(
accountInfo: AccountInfo<Buffer>, accountInfo: AccountInfo<Buffer>,
connection: Connection, connection: Connection,
): Promise<number> { ): Promise<{ price: number; lastUpdatedSlot: number }> {
if (accountInfo.owner.equals(SwitchboardProgram.devnetPid)) { if (accountInfo.owner.equals(SwitchboardProgram.devnetPid)) {
if (!sbv2DevnetProgram) { if (!sbv2DevnetProgram) {
sbv2DevnetProgram = await SwitchboardProgram.loadDevnet(connection); sbv2DevnetProgram = await SwitchboardProgram.loadDevnet(connection);
} }
return parseSwitcboardOracleV2(sbv2DevnetProgram, accountInfo); return parseSwitchboardOracleV2(sbv2DevnetProgram, accountInfo);
} }
if (accountInfo.owner.equals(SwitchboardProgram.mainnetPid)) { if (accountInfo.owner.equals(SwitchboardProgram.mainnetPid)) {
if (!sbv2MainnetProgram) { if (!sbv2MainnetProgram) {
sbv2MainnetProgram = await SwitchboardProgram.loadMainnet(connection); sbv2MainnetProgram = await SwitchboardProgram.loadMainnet(connection);
} }
return parseSwitcboardOracleV2(sbv2MainnetProgram, accountInfo); return parseSwitchboardOracleV2(sbv2MainnetProgram, accountInfo);
} }
if ( if (
accountInfo.owner.equals(SBV1_DEVNET_PID) || accountInfo.owner.equals(SBV1_DEVNET_PID) ||
accountInfo.owner.equals(SBV1_MAINNET_PID) accountInfo.owner.equals(SBV1_MAINNET_PID)
) { ) {
return parseSwitcboardOracleV1(accountInfo); return parseSwitchboardOracleV1(accountInfo);
} }
throw new Error(`Should not be reached!`); throw new Error(`Should not be reached!`);

View File

@ -48,6 +48,7 @@ export class PerpMarket {
public _price: I80F48; public _price: I80F48;
public _uiPrice: number; public _uiPrice: number;
public _oracleLastUpdatedSlot: number;
private priceLotsToUiConverter: number; private priceLotsToUiConverter: number;
private baseLotsToUiConverter: number; private baseLotsToUiConverter: number;
@ -246,6 +247,15 @@ export class PerpMarket {
return this._uiPrice; 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 { get minOrderSize(): number {
return this.baseLotsToUiConverter; return this.baseLotsToUiConverter;
} }