From 89fd0358208485502dcf83ce88499998a23e18ef Mon Sep 17 00:00:00 2001 From: "Jean Marchand (Exotic Markets)" Date: Tue, 3 Sep 2024 09:18:26 +0200 Subject: [PATCH] chore: Fix pyth sdk (#59) * chore: Fix pyth sdk * Fix lints --- Cargo.lock | 72 +------ Cargo.toml | 2 +- plugin/Cargo.toml | 1 - plugin/src/builders/thread_exec.rs | 4 +- plugin/src/events.rs | 4 +- plugin/src/observers/thread.rs | 4 +- plugin/src/utils.rs | 10 - programs/thread/Cargo.toml | 1 - .../thread/src/instructions/thread_kickoff.rs | 8 +- utils/Cargo.toml | 3 +- utils/src/lib.rs | 1 + utils/src/pyth.rs | 186 ++++++++++++++++++ utils/src/thread.rs | 3 +- 13 files changed, 205 insertions(+), 94 deletions(-) create mode 100644 utils/src/pyth.rs diff --git a/Cargo.lock b/Cargo.lock index 83471615..7649ebaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1832,15 +1832,6 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" -[[package]] -name = "fast-math" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2465292146cdfc2011350fe3b1c616ac83cf0faeedb33463ba1c332ed8948d66" -dependencies = [ - "ieee754", -] - [[package]] name = "fastrand" version = "2.1.0" @@ -2146,15 +2137,6 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] - [[package]] name = "histogram" version = "0.6.9" @@ -2321,12 +2303,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "ieee754" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9007da9cacbd3e6343da136e98b0d2df013f553d35bdec8b518f07bea768e19c" - [[package]] name = "im" version = "15.1.0" @@ -3186,40 +3162,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "pyth-solana-receiver-sdk" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b7854c4176470c8d86de301dc5b57ac84227dabb9527328b585fc332962d60b" -dependencies = [ - "anchor-lang", - "hex", - "pythnet-sdk", - "solana-program", -] - -[[package]] -name = "pythnet-sdk" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bbbc0456f9f27c9ad16b6c3bf1b2a7fea61eebf900f4d024a0468b9a84fe0c1" -dependencies = [ - "anchor-lang", - "bincode", - "borsh 0.10.3", - "bytemuck", - "byteorder", - "fast-math", - "hex", - "proc-macro2", - "rustc_version", - "serde", - "sha3 0.10.8", - "slow_primes", - "solana-program", - "thiserror", -] - [[package]] name = "qstring" version = "0.7.2" @@ -3780,7 +3722,6 @@ version = "1.0.1" dependencies = [ "anchor-lang", "chrono", - "pyth-solana-receiver-sdk", "sablier-cron", "sablier-network-program", "sablier-utils", @@ -3793,9 +3734,10 @@ version = "1.0.1" dependencies = [ "anchor-lang", "base64 0.21.7", - "pyth-solana-receiver-sdk", + "borsh 0.10.3", "sablier-macros", "serde", + "solana-program", "solana-sdk", "static-pubkey", ] @@ -3818,7 +3760,6 @@ dependencies = [ "chrono", "futures", "log", - "pyth-solana-receiver-sdk", "reqwest", "rustc_version", "sablier-cron", @@ -4106,15 +4047,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "slow_primes" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58267dd2fbaa6dceecba9e3e106d2d90a2b02497c0e8b01b8759beccf5113938" -dependencies = [ - "num", -] - [[package]] name = "smallvec" version = "1.13.2" diff --git a/Cargo.toml b/Cargo.toml index 5e6bd65c..b95a8e95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ anchor-spl = "=0.29.0" anyhow = "1.0" base64 = "~0.21" bincode = "1.3" +borsh = "0.10.3" bytemuck = "1.17.1" bzip2 = "0.4" cargo_metadata = "=0.18.1" @@ -40,7 +41,6 @@ indicatif = "0.17" log = "0.4" nom = "~7" proc-macro2 = "1.0" -pyth-solana-receiver-sdk = "0.3.1" quote = "1.0" rayon = "1.10.0" regex = "1.10.6" diff --git a/plugin/Cargo.toml b/plugin/Cargo.toml index 69fdcb5e..830bb306 100644 --- a/plugin/Cargo.toml +++ b/plugin/Cargo.toml @@ -30,7 +30,6 @@ sablier-thread-program = { workspace = true, features = ["no-entrypoint"] } sablier-webhook-program = { workspace = true, features = ["no-entrypoint"] } sablier-utils.workspace = true log.workspace = true -pyth-solana-receiver-sdk.workspace = true reqwest.workspace = true solana-account-decoder.workspace = true solana-client.workspace = true diff --git a/plugin/src/builders/thread_exec.rs b/plugin/src/builders/thread_exec.rs index c213b00a..e015ec56 100644 --- a/plugin/src/builders/thread_exec.rs +++ b/plugin/src/builders/thread_exec.rs @@ -4,7 +4,7 @@ use anchor_lang::{InstructionData, ToAccountMetas}; use log::info; use sablier_network_program::state::Worker; use sablier_thread_program::state::{Trigger, VersionedThread}; -use sablier_utils::thread::PAYER_PUBKEY; +use sablier_utils::{pyth::get_oracle_key, thread::PAYER_PUBKEY}; use solana_account_decoder::UiAccountEncoding; use solana_client::{ nonblocking::rpc_client::RpcClient, @@ -21,7 +21,7 @@ use solana_sdk::{ transaction::Transaction, }; -use crate::{error::PluginError, utils::get_oracle_key}; +use crate::error::PluginError; /// Max byte size of a serialized transaction. static TRANSACTION_MESSAGE_SIZE_LIMIT: usize = 1_232; diff --git a/plugin/src/events.rs b/plugin/src/events.rs index 4ff4a096..9983cf7d 100644 --- a/plugin/src/events.rs +++ b/plugin/src/events.rs @@ -1,6 +1,6 @@ use anchor_lang::{AccountDeserialize, Discriminator}; -use pyth_solana_receiver_sdk::price_update::{PriceFeedMessage, PriceUpdateV2}; use sablier_thread_program::state::{Thread, VersionedThread}; +use sablier_utils::pyth::{self, PriceFeedMessage, PriceUpdateV2}; use sablier_webhook_program::state::Webhook; use solana_geyser_plugin_interface::geyser_plugin_interface::ReplicaAccountInfoVersions; use solana_sdk::{clock::Clock, pubkey::Pubkey, sysvar}; @@ -59,7 +59,7 @@ fn parse_event( } } - if owner == pyth_solana_receiver_sdk::ID { + if owner == pyth::ID { return Ok(Some(AccountUpdateEvent::PriceFeed { price_feed: PriceUpdateV2::try_deserialize(&mut data)?.price_message, })); diff --git a/plugin/src/observers/thread.rs b/plugin/src/observers/thread.rs index be648d0d..c4c3e201 100644 --- a/plugin/src/observers/thread.rs +++ b/plugin/src/observers/thread.rs @@ -7,12 +7,12 @@ use std::{ use chrono::DateTime; use log::info; -use pyth_solana_receiver_sdk::price_update::PriceFeedMessage; use sablier_cron::Schedule; use sablier_thread_program::state::{Equality, Trigger, TriggerContext, VersionedThread}; +use sablier_utils::pyth::{get_oracle_key, PriceFeedMessage}; use solana_program::{clock::Clock, pubkey::Pubkey}; -use crate::{error::PluginError, observers::state::PythThread, utils::get_oracle_key}; +use crate::{error::PluginError, observers::state::PythThread}; use super::state::{ AccountThreads, Clocks, CronThreads, EpochThreads, NowThreads, PythThreads, SlotThreads, diff --git a/plugin/src/utils.rs b/plugin/src/utils.rs index ff83d257..f996c60d 100644 --- a/plugin/src/utils.rs +++ b/plugin/src/utils.rs @@ -1,15 +1,5 @@ -use pyth_solana_receiver_sdk::price_update::FeedId; -use solana_program::pubkey::Pubkey; use solana_sdk::signature::{read_keypair_file, Keypair}; -pub fn get_oracle_key(shard_id: u16, feed_id: FeedId) -> Pubkey { - let (pubkey, _) = Pubkey::find_program_address( - &[&shard_id.to_be_bytes(), &feed_id], - &pyth_solana_receiver_sdk::PYTH_PUSH_ORACLE_ID, - ); - pubkey -} - pub fn read_or_new_keypair(keypath: Option) -> Keypair { match keypath { Some(keypath) => read_keypair_file(keypath).unwrap(), diff --git a/programs/thread/Cargo.toml b/programs/thread/Cargo.toml index 7e67a7ad..5ba84617 100644 --- a/programs/thread/Cargo.toml +++ b/programs/thread/Cargo.toml @@ -27,5 +27,4 @@ chrono = { workspace = true, features = ["alloc"] } sablier-cron.workspace = true sablier-network-program = { features = ["cpi"], workspace = true } sablier-utils.workspace = true -pyth-solana-receiver-sdk.workspace = true version.workspace = true diff --git a/programs/thread/src/instructions/thread_kickoff.rs b/programs/thread/src/instructions/thread_kickoff.rs index 8fb3359b..59c688c7 100644 --- a/programs/thread/src/instructions/thread_kickoff.rs +++ b/programs/thread/src/instructions/thread_kickoff.rs @@ -6,10 +6,12 @@ use std::{ use anchor_lang::prelude::*; use chrono::DateTime; -use pyth_solana_receiver_sdk::price_update::PriceUpdateV2; use sablier_cron::Schedule; use sablier_network_program::state::{Worker, WorkerAccount}; -use sablier_utils::thread::Trigger; +use sablier_utils::{ + pyth::{self, PriceUpdateV2}, + thread::Trigger, +}; use crate::{constants::*, errors::*, state::*}; @@ -207,7 +209,7 @@ pub fn handler(ctx: Context) -> Result<()> { Some(account_info) => { require_keys_eq!( *account_info.owner, - pyth_solana_receiver_sdk::ID, + pyth::ID, SablierError::TriggerConditionFailed ); const STALENESS_THRESHOLD: u64 = 60; // staleness threshold in seconds diff --git a/utils/Cargo.toml b/utils/Cargo.toml index e65fe9ea..eee17754 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -16,10 +16,11 @@ name = "sablier_utils" [dependencies] anchor-lang.workspace = true base64.workspace = true +borsh.workspace = true serde = { workspace = true, features = ["derive"] } static-pubkey.workspace = true sablier-macros.workspace = true -pyth-solana-receiver-sdk.workspace = true +solana-program.workspace = true [dev-dependencies] solana-sdk.workspace = true diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 109c0b9f..f13e165a 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -1,6 +1,7 @@ pub mod account; pub mod explorer; pub mod pubkey; +pub mod pyth; pub mod space; pub mod thread; diff --git a/utils/src/pyth.rs b/utils/src/pyth.rs new file mode 100644 index 00000000..738cfebf --- /dev/null +++ b/utils/src/pyth.rs @@ -0,0 +1,186 @@ +use anchor_lang::prelude::*; +use solana_program::{clock::Clock, pubkey as key, pubkey::Pubkey}; + +pub const PYTH_PUSH_ORACLE_ID: Pubkey = key!("pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT"); +pub const ID: Pubkey = key!("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ"); + +pub type FeedId = [u8; 32]; + +#[error_code] +#[derive(PartialEq)] +pub enum GetPriceError { + #[msg("This price feed update's age exceeds the requested maximum age")] + PriceTooOld = 10000, // Big number to avoid conflicts with the SDK user's program error codes + #[msg("The price feed update doesn't match the requested feed id")] + MismatchedFeedId, + #[msg("This price feed update has a lower verification level than the one requested")] + InsufficientVerificationLevel, + #[msg("Feed id must be 32 Bytes, that's 64 hex characters or 66 with a 0x prefix")] + FeedIdMustBe32Bytes, + #[msg("Feed id contains non-hex characters")] + FeedIdNonHexCharacter, +} + +macro_rules! check { + ($cond:expr, $err:expr) => { + if !$cond { + return Err($err); + } + }; +} + +#[derive(AnchorSerialize, AnchorDeserialize, Debug)] +pub struct PriceFeedMessage { + pub feed_id: FeedId, + pub price: i64, + pub conf: u64, + pub exponent: i32, + pub publish_time: i64, + pub prev_publish_time: i64, + pub ema_price: i64, + pub ema_conf: u64, +} + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub enum VerificationLevel { + Partial { num_signatures: u8 }, + Full, +} + +impl VerificationLevel { + pub fn gte(&self, other: VerificationLevel) -> bool { + match self { + VerificationLevel::Full => true, + VerificationLevel::Partial { num_signatures } => match other { + VerificationLevel::Full => false, + VerificationLevel::Partial { + num_signatures: other_num_signatures, + } => *num_signatures >= other_num_signatures, + }, + } + } +} + +pub struct Price { + pub price: i64, + pub conf: u64, + pub exponent: i32, + pub publish_time: i64, +} + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct PriceUpdateV2 { + pub write_authority: Pubkey, + pub verification_level: VerificationLevel, + pub price_message: PriceFeedMessage, + pub posted_slot: u64, +} + +impl PriceUpdateV2 { + pub fn get_price_unchecked( + &self, + feed_id: &FeedId, + ) -> std::result::Result { + check!( + self.price_message.feed_id == *feed_id, + GetPriceError::MismatchedFeedId + ); + Ok(Price { + price: self.price_message.price, + conf: self.price_message.conf, + exponent: self.price_message.exponent, + publish_time: self.price_message.publish_time, + }) + } + + pub fn get_price_no_older_than_with_custom_verification_level( + &self, + clock: &Clock, + maximum_age: u64, + feed_id: &FeedId, + verification_level: VerificationLevel, + ) -> std::result::Result { + check!( + self.verification_level.gte(verification_level), + GetPriceError::InsufficientVerificationLevel + ); + let price = self.get_price_unchecked(feed_id)?; + check!( + price + .publish_time + .saturating_add(maximum_age.try_into().unwrap()) + >= clock.unix_timestamp, + GetPriceError::PriceTooOld + ); + Ok(price) + } + + pub fn get_price_no_older_than( + &self, + clock: &Clock, + maximum_age: u64, + feed_id: &FeedId, + ) -> std::result::Result { + self.get_price_no_older_than_with_custom_verification_level( + clock, + maximum_age, + feed_id, + VerificationLevel::Full, + ) + } +} + +impl anchor_lang::AccountDeserialize for PriceUpdateV2 { + fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result { + if buf.len() < 8 { + return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorNotFound.into()); + } + let given_disc = &buf[..8]; + if [34, 241, 35, 99, 157, 126, 244, 205] != given_disc { + return Err(anchor_lang::error::Error::from( + anchor_lang::error::AnchorError { + error_name: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch.name(), + error_code_number: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch + .into(), + error_msg: anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch + .to_string(), + error_origin: Some(anchor_lang::error::ErrorOrigin::AccountName( + "PriceUpdateV2".to_string(), + )), + compared_values: None, + }, + )); + } + Self::try_deserialize_unchecked(buf) + } + fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { + let mut data: &[u8] = &buf[8..]; + AnchorDeserialize::deserialize(&mut data) + .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into()) + } +} + +pub fn get_oracle_key(shard_id: u16, feed_id: FeedId) -> Pubkey { + let (pubkey, _) = + Pubkey::find_program_address(&[&shard_id.to_be_bytes(), &feed_id], &PYTH_PUSH_ORACLE_ID); + pubkey +} + +#[cfg(test)] +mod tests { + use base64::Engine; + + use super::*; + + #[test] + fn test_price_update_v2() { + let data = base64::engine::general_purpose::STANDARD.decode("IvEjY51+9M1gMUcENA3t3zcf1CRyFI8kjp0abRpesqw6zYt/1dayQwHvDYtv2izrpB2hXUCV0do5Kg0vjtDGx7wPTPrIwoC1bdYkWB4DAAAAPA/bAAAAAAD4////rbTWZgAAAACttNZmAAAAAOx1oSEDAAAAtpvRAAAAAADMB0YTAAAAAAA=").unwrap(); + + let price_update = PriceUpdateV2::try_deserialize(&mut data.as_slice()).unwrap(); + + assert_eq!( + price_update.write_authority, + key!("7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE") + ); + } +} diff --git a/utils/src/thread.rs b/utils/src/thread.rs index d93abb54..2fb675b4 100644 --- a/utils/src/thread.rs +++ b/utils/src/thread.rs @@ -5,12 +5,13 @@ use anchor_lang::{ solana_program::{self, instruction::Instruction}, AnchorDeserialize, }; -use pyth_solana_receiver_sdk::price_update::FeedId; use sablier_macros::MinSpace; use serde::{Deserialize, Serialize}; use static_pubkey::static_pubkey; use std::{convert::TryFrom, fmt::Debug, hash::Hash}; +use crate::pyth::FeedId; + /// The stand-in pubkey for delegating a payer address to a worker. All workers are re-imbursed by the user for lamports spent during this delegation. pub static PAYER_PUBKEY: Pubkey = static_pubkey!("Sab1ierPayer1111111111111111111111111111111");