From 37118e8c50f7f46bc34003864626487e633f48cb Mon Sep 17 00:00:00 2001 From: NorbertBodziony Date: Mon, 17 May 2021 08:43:50 +0200 Subject: [PATCH] examples: Pyth oracle (#287) --- .travis.yml | 2 + examples/pyth/Anchor.toml | 2 + examples/pyth/Cargo.toml | 4 + examples/pyth/Xargo.toml | 2 + examples/pyth/programs/pyth/Cargo.toml | 20 +++ examples/pyth/programs/pyth/Xargo.toml | 2 + examples/pyth/programs/pyth/src/lib.rs | 39 +++++ examples/pyth/programs/pyth/src/pc.rs | 110 +++++++++++++ examples/pyth/tests/oracleUtils.ts | 213 +++++++++++++++++++++++++ examples/pyth/tests/pyth.spec.ts | 39 +++++ examples/pyth/tsconfig.json | 10 ++ 11 files changed, 443 insertions(+) create mode 100644 examples/pyth/Anchor.toml create mode 100644 examples/pyth/Cargo.toml create mode 100644 examples/pyth/Xargo.toml create mode 100644 examples/pyth/programs/pyth/Cargo.toml create mode 100644 examples/pyth/programs/pyth/Xargo.toml create mode 100644 examples/pyth/programs/pyth/src/lib.rs create mode 100644 examples/pyth/programs/pyth/src/pc.rs create mode 100644 examples/pyth/tests/oracleUtils.ts create mode 100644 examples/pyth/tests/pyth.spec.ts create mode 100644 examples/pyth/tsconfig.json diff --git a/.travis.yml b/.travis.yml index 1536d6d1..83583838 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,7 @@ _examples: &examples - npm install -g mocha - npm install -g ts-mocha - npm install -g typescript + - npm install -g buffer - cd ts && yarn && yarn build && npm link && cd ../ - npm install -g @project-serum/serum - npm install -g @project-serum/common @@ -68,6 +69,7 @@ jobs: - pushd examples/chat && yarn && anchor test && popd - pushd examples/ido-pool && yarn && anchor test && popd - pushd examples/swap/deps/serum-dex/dex && cargo build-bpf && cd ../../../ && anchor test && popd + - pushd examples/pyth && yarn && anchor test && popd - pushd examples/tutorial/basic-0 && anchor test && popd - pushd examples/tutorial/basic-1 && anchor test && popd - pushd examples/tutorial/basic-2 && anchor test && popd diff --git a/examples/pyth/Anchor.toml b/examples/pyth/Anchor.toml new file mode 100644 index 00000000..2ebd5af9 --- /dev/null +++ b/examples/pyth/Anchor.toml @@ -0,0 +1,2 @@ +cluster = "localnet" +wallet = "~/.config/solana/id.json" diff --git a/examples/pyth/Cargo.toml b/examples/pyth/Cargo.toml new file mode 100644 index 00000000..a60de986 --- /dev/null +++ b/examples/pyth/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "programs/*" +] diff --git a/examples/pyth/Xargo.toml b/examples/pyth/Xargo.toml new file mode 100644 index 00000000..1744f098 --- /dev/null +++ b/examples/pyth/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/examples/pyth/programs/pyth/Cargo.toml b/examples/pyth/programs/pyth/Cargo.toml new file mode 100644 index 00000000..38fa8f54 --- /dev/null +++ b/examples/pyth/programs/pyth/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "pyth" +version = "0.1.0" +description = "Created with Anchor" +edition = "2018" + +[lib] +crate-type = ["cdylib", "lib"] +name = "pyth" + +[features] +no-entrypoint = [] +no-idl = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = "0.5.0" +arrayref = "0.3.6" +bytemuck = { version = "1.4.0" } diff --git a/examples/pyth/programs/pyth/Xargo.toml b/examples/pyth/programs/pyth/Xargo.toml new file mode 100644 index 00000000..1744f098 --- /dev/null +++ b/examples/pyth/programs/pyth/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/examples/pyth/programs/pyth/src/lib.rs b/examples/pyth/programs/pyth/src/lib.rs new file mode 100644 index 00000000..06689093 --- /dev/null +++ b/examples/pyth/programs/pyth/src/lib.rs @@ -0,0 +1,39 @@ +use anchor_lang::prelude::*; +mod pc; +use pc::Price; + +#[program] +pub mod pyth { + use super::*; + + pub fn initialize(ctx: Context, price: i64, expo: i32, conf: u64) -> ProgramResult { + let oracle = &ctx.accounts.price; + + let mut price_oracle = Price::load(&oracle).unwrap(); + + price_oracle.agg.price = price; + price_oracle.agg.conf = conf; + price_oracle.expo = expo; + price_oracle.ptype = pc::PriceType::Price; + Ok(()) + } + + pub fn set_price(ctx: Context, price: i64) -> ProgramResult { + let oracle = &ctx.accounts.price; + let mut price_oracle = Price::load(&oracle).unwrap(); + price_oracle.agg.price = price as i64; + Ok(()) + } +} + +#[derive(Accounts)] +pub struct SetPrice<'info> { + #[account(mut)] + pub price: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct Initialize<'info> { + #[account(mut)] + pub price: AccountInfo<'info>, +} diff --git a/examples/pyth/programs/pyth/src/pc.rs b/examples/pyth/programs/pyth/src/pc.rs new file mode 100644 index 00000000..62be731e --- /dev/null +++ b/examples/pyth/programs/pyth/src/pc.rs @@ -0,0 +1,110 @@ +use crate::*; +use anchor_lang::prelude::AccountInfo; +use bytemuck::{cast_slice_mut, from_bytes_mut, try_cast_slice_mut, Pod, Zeroable}; +use std::cell::RefMut; + +#[derive(Default, Copy, Clone)] +#[repr(C)] +pub struct AccKey { + pub val: [u8; 32], +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub enum PriceStatus { + Unknown, + Trading, + Halted, + Auction, +} + +impl Default for PriceStatus { + fn default() -> Self { + PriceStatus::Trading + } +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub enum CorpAction { + NoCorpAct, +} + +impl Default for CorpAction { + fn default() -> Self { + CorpAction::NoCorpAct + } +} + +#[derive(Default, Copy, Clone)] +#[repr(C)] +pub struct PriceInfo { + pub price: i64, + pub conf: u64, + pub status: PriceStatus, + pub corp_act: CorpAction, + pub pub_slot: u64, +} +#[derive(Default, Copy, Clone)] +#[repr(C)] +pub struct PriceComp { + publisher: AccKey, + agg: PriceInfo, + latest: PriceInfo, +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub enum PriceType { + Unknown, + Price, + TWAP, + Volatility, +} + +impl Default for PriceType { + fn default() -> Self { + PriceType::Price + } +} + +#[derive(Default, Copy, Clone)] +#[repr(C)] +pub struct Price { + pub magic: u32, // Pyth magic number. + pub ver: u32, // Program version. + pub atype: u32, // Account type. + pub size: u32, // Price account size. + pub ptype: PriceType, // Price or calculation type. + pub expo: i32, // Price exponent. + pub num: u32, // Number of component prices. + pub unused: u32, + pub curr_slot: u64, // Currently accumulating price slot. + pub valid_slot: u64, // Valid slot-time of agg price. + pub prod: AccKey, + pub next: AccKey, + pub agg_pub: AccKey, + pub agg: PriceInfo, + pub comp: [PriceComp; 16], +} + +impl Price { + #[inline] + pub fn load<'a>(price_feed: &'a AccountInfo) -> Result, ProgramError> { + let account_data: RefMut<'a, [u8]>; + let state: RefMut<'a, Self>; + + account_data = RefMut::map(price_feed.try_borrow_mut_data().unwrap(), |data| *data); + + state = RefMut::map(account_data, |data| { + from_bytes_mut(cast_slice_mut::(try_cast_slice_mut(data).unwrap())) + }); + Ok(state) + } +} + +#[cfg(target_endian = "little")] +unsafe impl Zeroable for Price {} + +#[cfg(target_endian = "little")] +unsafe impl Pod for Price {} diff --git a/examples/pyth/tests/oracleUtils.ts b/examples/pyth/tests/oracleUtils.ts new file mode 100644 index 00000000..b6ace3d3 --- /dev/null +++ b/examples/pyth/tests/oracleUtils.ts @@ -0,0 +1,213 @@ +import { Buffer } from 'buffer' +import { BN, Program, web3 } from '@project-serum/anchor' + +export const Magic = 0xa1b2c3d4 +export const Version1 = 1 +export const Version = Version1 +export const PriceStatus = ['Unknown', 'Trading', 'Halted', 'Auction'] +export const CorpAction = ['NoCorpAct'] +export const PriceType = ['Unknown', 'Price', 'TWAP', 'Volatility'] + +const empty32Buffer = Buffer.alloc(32) +const PKorNull = (data: Buffer) => (data.equals(empty32Buffer) ? null : new web3.PublicKey(data)) + +interface ICreatePriceFeed { + oracleProgram: Program + initPrice: number + confidence?: BN + expo?: number +} +export const createPriceFeed = async ({ + oracleProgram, + initPrice, + confidence, + expo = -4, +}: ICreatePriceFeed) => { + const conf = confidence || new BN((initPrice / 10) * 10 ** -expo) + const collateralTokenFeed = new web3.Account() + await oracleProgram.rpc.initialize(new BN(initPrice * 10 ** -expo), expo, conf, { + accounts: { price: collateralTokenFeed.publicKey }, + signers: [collateralTokenFeed], + instructions: [ + web3.SystemProgram.createAccount({ + fromPubkey: oracleProgram.provider.wallet.publicKey, + newAccountPubkey: collateralTokenFeed.publicKey, + space: 1712, + lamports: await oracleProgram.provider.connection.getMinimumBalanceForRentExemption(1712), + programId: oracleProgram.programId, + }), + ], + }) + return collateralTokenFeed.publicKey +} +export const setFeedPrice = async ( + oracleProgram: Program, + newPrice: number, + priceFeed: web3.PublicKey +) => { + const info = await oracleProgram.provider.connection.getAccountInfo(priceFeed) + const data = parsePriceData(info.data) + await oracleProgram.rpc.setPrice(new BN(newPrice * 10 ** -data.exponent), { + accounts: { price: priceFeed }, + }) +} +export const getFeedData = async (oracleProgram: Program, priceFeed: web3.PublicKey) => { + const info = await oracleProgram.provider.connection.getAccountInfo(priceFeed) + return parsePriceData(info.data) +} + +export const parseMappingData = (data: Buffer) => { + // Pyth magic number. + const magic = data.readUInt32LE(0) + // Program version. + const version = data.readUInt32LE(4) + // Account type. + const type = data.readUInt32LE(8) + // Account used size. + const size = data.readUInt32LE(12) + // Number of product accounts. + const numProducts = data.readUInt32LE(16) + // Unused. + // const unused = accountInfo.data.readUInt32LE(20) + // TODO: check and use this. + // Next mapping account (if any). + const nextMappingAccount = PKorNull(data.slice(24, 56)) + // Read each symbol account. + let offset = 56 + const productAccountKeys = [] + for (let i = 0; i < numProducts; i++) { + const productAccountBytes = data.slice(offset, offset + 32) + const productAccountKey = new web3.PublicKey(productAccountBytes) + offset += 32 + productAccountKeys.push(productAccountKey) + } + return { + magic, + version, + type, + size, + nextMappingAccount, + productAccountKeys, + } +} + +interface ProductAttributes { + [index: string]: string +} + +export const parseProductData = (data: Buffer) => { + // Pyth magic number. + const magic = data.readUInt32LE(0) + // Program version. + const version = data.readUInt32LE(4) + // Account type. + const type = data.readUInt32LE(8) + // Price account size. + const size = data.readUInt32LE(12) + // First price account in list. + const priceAccountBytes = data.slice(16, 48) + const priceAccountKey = new web3.PublicKey(priceAccountBytes) + const product: ProductAttributes = {} + let idx = 48 + while (idx < data.length) { + const keyLength = data[idx] + idx++ + if (keyLength) { + const key = data.slice(idx, idx + keyLength).toString() + idx += keyLength + const valueLength = data[idx] + idx++ + const value = data.slice(idx, idx + valueLength).toString() + idx += valueLength + product[key] = value + } + } + return { magic, version, type, size, priceAccountKey, product } +} + +const parsePriceInfo = (data: Buffer, exponent: number) => { + // Aggregate price. + const priceComponent = data.readBigUInt64LE(0) + const price = Number(priceComponent) * 10 ** exponent + // Aggregate confidence. + const confidenceComponent = data.readBigUInt64LE(8) + const confidence = Number(confidenceComponent) * 10 ** exponent + // Aggregate status. + const status = data.readUInt32LE(16) + // Aggregate corporate action. + const corporateAction = data.readUInt32LE(20) + // Aggregate publish slot. + const publishSlot = data.readBigUInt64LE(24) + return { + priceComponent, + price, + confidenceComponent, + confidence, + status, + corporateAction, + publishSlot, + } +} + +export const parsePriceData = (data: Buffer) => { + // Pyth magic number. + const magic = data.readUInt32LE(0) + // Program version. + const version = data.readUInt32LE(4) + // Account type. + const type = data.readUInt32LE(8) + // Price account size. + const size = data.readUInt32LE(12) + // Price or calculation type. + const priceType = data.readUInt32LE(16) + // Price exponent. + const exponent = data.readInt32LE(20) + // Number of component prices. + const numComponentPrices = data.readUInt32LE(24) + // Unused. + // const unused = accountInfo.data.readUInt32LE(28) + // Currently accumulating price slot. + const currentSlot = data.readBigUInt64LE(32) + // Valid on-chain slot of aggregate price. + const validSlot = data.readBigUInt64LE(40) + // Product id / reference account. + const productAccountKey = new web3.PublicKey(data.slice(48, 80)) + // Next price account in list. + const nextPriceAccountKey = new web3.PublicKey(data.slice(80, 112)) + // Aggregate price updater. + const aggregatePriceUpdaterAccountKey = new web3.PublicKey(data.slice(112, 144)) + const aggregatePriceInfo = parsePriceInfo(data.slice(144, 176), exponent) + // Urice components - up to 16. + const priceComponents = [] + let offset = 176 + let shouldContinue = true + while (offset < data.length && shouldContinue) { + const publisher = PKorNull(data.slice(offset, offset + 32)) + offset += 32 + if (publisher) { + const aggregate = parsePriceInfo(data.slice(offset, offset + 32), exponent) + offset += 32 + const latest = parsePriceInfo(data.slice(offset, offset + 32), exponent) + offset += 32 + priceComponents.push({ publisher, aggregate, latest }) + } else { + shouldContinue = false + } + } + return { + magic, + version, + type, + size, + priceType, + exponent, + numComponentPrices, + currentSlot, + validSlot, + productAccountKey, + nextPriceAccountKey, + aggregatePriceUpdaterAccountKey, + ...aggregatePriceInfo, + priceComponents, + } +} diff --git a/examples/pyth/tests/pyth.spec.ts b/examples/pyth/tests/pyth.spec.ts new file mode 100644 index 00000000..72320348 --- /dev/null +++ b/examples/pyth/tests/pyth.spec.ts @@ -0,0 +1,39 @@ +import * as anchor from '@project-serum/anchor' +import { BN, Program, web3 } from '@project-serum/anchor' +import assert from 'assert' +import { createPriceFeed, setFeedPrice, getFeedData } from './oracleUtils' + +describe('pyth-oracle', () => { + anchor.setProvider(anchor.Provider.env()) + const program = anchor.workspace.Pyth as Program + + it('initialize', async () => { + const price = 50000 + const priceFeedAddress = await createPriceFeed({ + oracleProgram: program, + initPrice: price, + expo: -6, + }) + const feedData = await getFeedData(program, priceFeedAddress) + assert.ok(feedData.price === price) + }) + + it('change feed price', async () => { + const price = 50000 + const expo = -7 + const priceFeedAddress = await createPriceFeed({ + oracleProgram: program, + initPrice: price, + expo: expo, + }) + const feedDataBefore = await getFeedData(program, priceFeedAddress) + assert.ok(feedDataBefore.price === price) + assert.ok(feedDataBefore.exponent === expo) + + const newPrice = 55000 + await setFeedPrice(program, newPrice, priceFeedAddress) + const feedDataAfter = await getFeedData(program, priceFeedAddress) + assert.ok(feedDataAfter.price === newPrice) + assert.ok(feedDataAfter.exponent === expo) + }) +}) diff --git a/examples/pyth/tsconfig.json b/examples/pyth/tsconfig.json new file mode 100644 index 00000000..cd5d2e3d --- /dev/null +++ b/examples/pyth/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } +}