examples: Pyth oracle (#287)
This commit is contained in:
parent
95c248406b
commit
37118e8c50
|
@ -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
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
cluster = "localnet"
|
||||
wallet = "~/.config/solana/id.json"
|
|
@ -0,0 +1,4 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"programs/*"
|
||||
]
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -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" }
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -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>,
|
||||
}
|
|
@ -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 {}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"types": ["mocha", "chai"],
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"lib": ["es2015"],
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue