Browse Source

examples: Pyth oracle (#287)

NorbertBodziony 4 years ago
parent
commit
37118e8c50

+ 2 - 0
.travis.yml

@@ -21,6 +21,7 @@ _examples: &examples
   - npm install -g mocha
   - npm install -g mocha
   - npm install -g ts-mocha
   - npm install -g ts-mocha
   - npm install -g typescript
   - npm install -g typescript
+  - npm install -g buffer
   - cd ts && yarn && yarn build && npm link && cd ../
   - cd ts && yarn && yarn build && npm link && cd ../
   - npm install -g @project-serum/serum
   - npm install -g @project-serum/serum
   - npm install -g @project-serum/common
   - npm install -g @project-serum/common
@@ -68,6 +69,7 @@ jobs:
         - pushd examples/chat && yarn && anchor test && popd
         - pushd examples/chat && yarn && anchor test && popd
         - pushd examples/ido-pool && 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/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-0 && anchor test && popd
         - pushd examples/tutorial/basic-1 && anchor test && popd
         - pushd examples/tutorial/basic-1 && anchor test && popd
         - pushd examples/tutorial/basic-2 && anchor test && popd
         - pushd examples/tutorial/basic-2 && anchor test && popd

+ 2 - 0
examples/pyth/Anchor.toml

@@ -0,0 +1,2 @@
+cluster = "localnet"
+wallet = "~/.config/solana/id.json"

+ 4 - 0
examples/pyth/Cargo.toml

@@ -0,0 +1,4 @@
+[workspace]
+members = [
+    "programs/*"
+]

+ 2 - 0
examples/pyth/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 20 - 0
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" }

+ 2 - 0
examples/pyth/programs/pyth/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 39 - 0
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<Initialize>, 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<SetPrice>, 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>,
+}

+ 110 - 0
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<RefMut<'a, Price>, 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::<u8, u8>(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 {}

+ 213 - 0
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,
+  }
+}

+ 39 - 0
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)
+  })
+})

+ 10 - 0
examples/pyth/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "types": ["mocha", "chai"],
+    "typeRoots": ["./node_modules/@types"],
+    "lib": ["es2015"],
+    "module": "commonjs",
+    "target": "es6",
+    "esModuleInterop": true
+  }
+}