diff --git a/package.json b/package.json index 51bc348..443a00c 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@project-serum/common": "^0.0.1-beta.3", "@project-serum/serum": "^0.13.20", "@project-serum/sol-wallet-adapter": "^0.1.4", + "@pythnetwork/client": "^2.7.2", "@solana/spl-token": "0.0.13", "@solana/web3.js": "^0.95.0", "bn.js": "^5.1.2", diff --git a/src/client.ts b/src/client.ts index 70dd482..4b485b4 100644 --- a/src/client.ts +++ b/src/client.ts @@ -28,6 +28,7 @@ import { awaitTransactionSignatureConfirmation, createAccountInstruction, getFilteredProgramAccounts, + getOraclePrice, getUnixTs, nativeToUi, parseTokenAccountData, @@ -105,15 +106,10 @@ export class MangoGroup { } async getPrices(connection: Connection): Promise { - const aggs = await Promise.all( - this.oracles.map((pk) => Aggregator.loadWithConnection(pk, connection)), + const oraclePrices = await Promise.all( + this.oracles.map((pk) => getOraclePrice(connection, pk)), ); - return aggs - .map( - (agg) => - agg.answer.median.toNumber() / Math.pow(10, agg.config.decimals), - ) - .concat(1.0); + return oraclePrices.concat([1.0]); } getMarketIndex(spotMarket: Market): number { diff --git a/src/utils.ts b/src/utils.ts index 81b3b70..26ba43d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -14,6 +14,11 @@ import { } from '@solana/web3.js'; import BN from 'bn.js'; import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions'; +import { + parseBaseData, + parsePriceData, + AccountType, +} from '@pythnetwork/client'; import { bits, blob, struct, u8, u32, nu64 } from 'buffer-layout'; import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; import { AccountLayout } from './layout'; @@ -25,6 +30,7 @@ import { u64, zeros, } from '@project-serum/serum/lib/layout'; +import { Aggregator } from './schema'; export const zeroKey = new PublicKey(new Uint8Array(32)); @@ -418,3 +424,24 @@ export function decodeRecentEvents(buffer: Buffer, lastSeenSeqNum?: number) { return { header, nodes }; } + +const PYTH_MAGIC = Buffer.from([0xa1, 0xb2, 0xc3, 0xd4]); + +export async function getOraclePrice( + connection: Connection, + oracle: PublicKey, +): Promise { + const info = await connection.getAccountInfo(oracle); + if (!info || !info.data.length) { + throw new Error('account does not exist'); + } + + const pythBase = parseBaseData(info.data); + if (pythBase?.type == AccountType.Price) { + const price = parsePriceData(info.data); + return price.aggregate.price; + } else { + const agg = Aggregator.deserialize(info.data); + return agg.answer.median.toNumber() / Math.pow(10, agg.config.decimals); + } +} diff --git a/tests/oracle.test.ts b/tests/oracle.test.ts new file mode 100644 index 0000000..938c3ef --- /dev/null +++ b/tests/oracle.test.ts @@ -0,0 +1,23 @@ +import { Connection, PublicKey } from '@solana/web3.js'; +import { expect } from 'chai'; +import { getOraclePrice } from '../src/utils'; + +const conn = new Connection('https://api.mainnet-beta.solana.com/'); + +describe('getOraclePrice', async () => { + it('should parse flux aggregator', async () => { + const p = await getOraclePrice( + conn, + new PublicKey('HxrRDnjj2Ltj9LMmtcN6PDuFqnDe3FqXDHPvs2pwmtYF'), + ); + expect(p).to.be.within(5000, 80000); + }); + + it('should parse pyth', async () => { + const p = await getOraclePrice( + conn, + new PublicKey('GVXRSBjFk6e6J3NbVPXohDJetcTjaeeuykUpbQF8UoMU'), + ); + expect(p).to.be.within(5000, 80000); + }); +}); diff --git a/yarn.lock b/yarn.lock index 369d1a6..23242d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -583,6 +583,13 @@ bs58 "^4.0.1" eventemitter3 "^4.0.4" +"@pythnetwork/client@^2.7.2": + version "2.7.2" + resolved "https://registry.yarnpkg.com/@pythnetwork/client/-/client-2.7.2.tgz#eca3a59e8f222aa1b67c8e4653e2484079f5fb9a" + integrity sha512-Hx/GLaZH0evm0tT0gsO1S7r3liiDzUeqDfMaV1HH7a5yuQGH9zgUrmdEMEtkgRceLa2nGhEGnRtISqY/X96XtA== + dependencies: + buffer "^6.0.1" + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"