examples: Pyth oracle (#287)

This commit is contained in:
NorbertBodziony 2021-05-17 08:43:50 +02:00 committed by GitHub
parent 95c248406b
commit 37118e8c50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 443 additions and 0 deletions

View File

@ -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

View File

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

4
examples/pyth/Cargo.toml Normal file
View File

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

2
examples/pyth/Xargo.toml Normal file
View File

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

View File

@ -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" }

View File

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

View File

@ -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>,
}

View File

@ -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 {}

View File

@ -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,
}
}

View File

@ -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)
})
})

View File

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