diff --git a/.github/.ghaignore b/.github/.ghaignore index cf5190de..0c7d2136 100644 --- a/.github/.ghaignore +++ b/.github/.ghaignore @@ -1,11 +1,6 @@ # uses generated client from shank, can't rewrite to solana-bankrun tools/shank-and-solita/native -# not building: pyth-solana-receiver-sdk 1.1.0 pulls a borsh version that -# conflicts with Anchor 1.0 / Solana 3.x (PriceUpdateV2 fails BorshDeserialize). -# Blocked on an upstream SDK release compatible with solana 3.x. -basics/pyth/anchor - # not building compression/cutils/anchor compression/cnft-vault/anchor diff --git a/basics/pyth/anchor/README.md b/basics/pyth/anchor/README.md index c95755fc..0a3d38bd 100644 --- a/basics/pyth/anchor/README.md +++ b/basics/pyth/anchor/README.md @@ -4,6 +4,19 @@ Read a [Pyth](https://pyth.network/) price feed account and log price, confidenc See also: [Pyth overview](../README.md) and the [repository catalog](../../../README.md). +> [!NOTE] +> **The official `pyth-solana-receiver-sdk` is not Anchor 1.0 compatible (as of June 2026), so this example vendors the `PriceUpdateV2` account type instead of importing it.** +> +> The latest `pyth-solana-receiver-sdk` (1.2.0) builds against `anchor-lang` 0.32 and pulls `pythnet-sdk` (2.3.1), which still derives **borsh 0.10** on `PriceFeedMessage`. Anchor 0.32's `AnchorSerialize`/`AnchorDeserialize` derives require **borsh 1.x**, so `pyth-solana-receiver-sdk`'s own `PriceUpdateV2` fails to compile: +> +> ``` +> error[E0277]: the trait bound `pythnet_sdk::messages::PriceFeedMessage: BorshSerialize` is not satisfied +> ``` +> +> No published `pyth-solana-receiver-sdk` targets `anchor-lang` 1.0 (which this repo standardizes on), and no `pythnet-sdk` release has migrated to borsh 1.x — so the dependency can't simply be upgraded. Tracked upstream at [pyth-network/pyth-crosschain#3756](https://github.com/pyth-network/pyth-crosschain/issues/3756). +> +> As a workaround, `programs/pythexample/src/lib.rs` mirrors the on-chain `PriceUpdateV2` layout locally (same fields, same 8-byte discriminator, owned by the Pyth Receiver program) so accounts written by Pyth deserialize unchanged. Replace the vendored type with the SDK import once an Anchor 1.0 / borsh 1.x compatible release ships. + ## Major concepts - Oracle price accounts diff --git a/basics/pyth/anchor/programs/pythexample/Cargo.toml b/basics/pyth/anchor/programs/pythexample/Cargo.toml index a62ad14a..2acfd580 100644 --- a/basics/pyth/anchor/programs/pythexample/Cargo.toml +++ b/basics/pyth/anchor/programs/pythexample/Cargo.toml @@ -22,7 +22,6 @@ custom-panic = [] [dependencies] anchor-lang = "1.0.0" -pyth-solana-receiver-sdk = "1.1.0" [dev-dependencies] litesvm = "0.11.0" diff --git a/basics/pyth/anchor/programs/pythexample/src/lib.rs b/basics/pyth/anchor/programs/pythexample/src/lib.rs index c3ccced2..8416108c 100644 --- a/basics/pyth/anchor/programs/pythexample/src/lib.rs +++ b/basics/pyth/anchor/programs/pythexample/src/lib.rs @@ -1,8 +1,10 @@ -use pyth_solana_receiver_sdk::price_update::PriceUpdateV2; use anchor_lang::prelude::*; declare_id!("GUkjQmrLPFXXNK1bFLKt8XQi6g3TjxcHVspbjDoHvMG2"); +/// The Pyth Receiver program that owns `PriceUpdateV2` accounts on devnet/mainnet. +pub const PYTH_RECEIVER_PROGRAM_ID: Pubkey = pubkey!("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ"); + #[program] pub mod anchor_test { use super::*; @@ -13,7 +15,10 @@ pub mod anchor_test { msg!("Price: {:?}", price_update.price_message.price); msg!("Confidence: {:?}", price_update.price_message.conf); msg!("Exponent: {:?}", price_update.price_message.exponent); - msg!("Publish Time: {:?}", price_update.price_message.publish_time); + msg!( + "Publish Time: {:?}", + price_update.price_message.publish_time + ); Ok(()) } } @@ -22,3 +27,103 @@ pub mod anchor_test { pub struct ReadPrice<'info> { pub price_update: Account<'info, PriceUpdateV2>, } + +// --------------------------------------------------------------------------- +// Pyth `PriceUpdateV2` account, vendored from `pyth-solana-receiver-sdk`. +// +// The official `pyth-solana-receiver-sdk` is NOT Anchor 1.0 compatible (as of +// June 2026), so this example mirrors the `PriceUpdateV2` account type locally +// instead of importing it. +// +// Details: the latest `pyth-solana-receiver-sdk` (1.2.0) builds against +// `anchor-lang` 0.32 and pulls `pythnet-sdk` (2.3.1), which still derives +// borsh 0.10 on `PriceFeedMessage`. Anchor 0.32's `AnchorSerialize` / +// `AnchorDeserialize` derives require borsh 1.x, so the SDK's own +// `PriceUpdateV2` fails to compile: +// +// error[E0277]: the trait bound +// `pythnet_sdk::messages::PriceFeedMessage: BorshSerialize` is not satisfied +// +// No published `pyth-solana-receiver-sdk` targets `anchor-lang` 1.0 (which this +// repo standardizes on) and no `pythnet-sdk` release has migrated to borsh 1.x, +// so the dependency can't simply be upgraded. Tracked upstream at +// https://github.com/pyth-network/pyth-crosschain/issues/3756 +// +// The fields, order, and 8-byte +// discriminator below match the on-chain account exactly, and it is owned by +// the Pyth Receiver program (see the `Owner` impl), so accounts written by Pyth +// deserialize unchanged. Replace this with the SDK type once an Anchor 1.0 / +// borsh 1.x compatible `pyth-solana-receiver-sdk` release ships. +// --------------------------------------------------------------------------- + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)] +pub enum VerificationLevel { + /// Partially verified: only `num_signatures` of the Wormhole guardians + /// were checked against the price update. + Partial { num_signatures: u8 }, + /// Fully verified against the full guardian set. + Full, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)] +pub struct PriceFeedMessage { + pub feed_id: [u8; 32], + 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, Clone, Debug, PartialEq, Eq)] +pub struct PriceUpdateV2 { + pub write_authority: Pubkey, + pub verification_level: VerificationLevel, + pub price_message: PriceFeedMessage, + pub posted_slot: u64, +} + +// Anchor's 8-byte discriminator: sha256("account:PriceUpdateV2")[..8]. +impl anchor_lang::Discriminator for PriceUpdateV2 { + const DISCRIMINATOR: &'static [u8] = &[34, 241, 35, 99, 157, 126, 244, 205]; +} + +// The account is created and owned by the Pyth Receiver program. +impl anchor_lang::Owner for PriceUpdateV2 { + fn owner() -> Pubkey { + PYTH_RECEIVER_PROGRAM_ID + } +} + +impl anchor_lang::AccountSerialize for PriceUpdateV2 { + fn try_serialize(&self, writer: &mut W) -> Result<()> { + writer + .write_all(::DISCRIMINATOR) + .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotSerialize)?; + AnchorSerialize::serialize(self, writer) + .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotSerialize)?; + Ok(()) + } +} + +impl anchor_lang::AccountDeserialize for PriceUpdateV2 { + fn try_deserialize(buf: &mut &[u8]) -> Result { + let disc = ::DISCRIMINATOR; + if buf.len() < disc.len() { + return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorNotFound.into()); + } + if &buf[..disc.len()] != disc { + return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch.into()); + } + Self::try_deserialize_unchecked(buf) + } + + fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result { + let disc = ::DISCRIMINATOR; + let mut data: &[u8] = &buf[disc.len()..]; + AnchorDeserialize::deserialize(&mut data) + .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into()) + } +}