Skip to content

Commit 9c1ca6d

Browse files
committed
fix(pyth/anchor): vendor PriceUpdateV2 to build on Anchor 1.0; re-enable in CI
basics/pyth/anchor was excluded because pyth-solana-receiver-sdk can't be built against Anchor 1.0 / borsh 1.x. The latest 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, while Anchor's AnchorSerialize/AnchorDeserialize derives need borsh 1.x — so the SDK's own PriceUpdateV2 fails to compile (`PriceFeedMessage: BorshSerialize is not satisfied`). No SDK release targets anchor-lang 1.0 and no pythnet-sdk release has moved to borsh 1.x, so the dep can't be upgraded. Drop the pyth-solana-receiver-sdk dependency and vendor the PriceUpdateV2 / PriceFeedMessage / VerificationLevel layout locally with Anchor 1.0 derives and manual Discriminator/Owner/AccountSerialize/AccountDeserialize impls. The fields, order, and 8-byte discriminator match the on-chain account, and it is owned by the Pyth Receiver program, so accounts written by Pyth deserialize unchanged. Document the situation in the README (GitHub note callout) and a matching comment in lib.rs, including the "as of June 2026" caveat to revisit once an Anchor 1.0 / borsh 1.x SDK ships. The existing LiteSVM test (builds a mock PriceUpdateV2 and calls read_price) passes. Drop basics/pyth/anchor from .github/.ghaignore.
1 parent 3db55d3 commit 9c1ca6d

4 files changed

Lines changed: 115 additions & 8 deletions

File tree

.github/.ghaignore

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
# uses generated client from shank, can't rewrite to solana-bankrun
22
tools/shank-and-solita/native
33

4-
# not building: pyth-solana-receiver-sdk 1.1.0 pulls a borsh version that
5-
# conflicts with Anchor 1.0 / Solana 3.x (PriceUpdateV2 fails BorshDeserialize).
6-
# Blocked on an upstream SDK release compatible with solana 3.x.
7-
basics/pyth/anchor
8-
94
# not building
105
compression/cutils/anchor
116
compression/cnft-vault/anchor

basics/pyth/anchor/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ Read a [Pyth](https://pyth.network/) price feed account and log price, confidenc
44

55
See also: [Pyth overview](../README.md) and the [repository catalog](../../../README.md).
66

7+
> [!NOTE]
8+
> **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.**
9+
>
10+
> 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:
11+
>
12+
> ```
13+
> error[E0277]: the trait bound `pythnet_sdk::messages::PriceFeedMessage: BorshSerialize` is not satisfied
14+
> ```
15+
>
16+
> 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. 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.
17+
718
## Major concepts
819
920
- Oracle price accounts

basics/pyth/anchor/programs/pythexample/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ custom-panic = []
2222

2323
[dependencies]
2424
anchor-lang = "1.0.0"
25-
pyth-solana-receiver-sdk = "1.1.0"
2625

2726
[dev-dependencies]
2827
litesvm = "0.11.0"

basics/pyth/anchor/programs/pythexample/src/lib.rs

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
use pyth_solana_receiver_sdk::price_update::PriceUpdateV2;
21
use anchor_lang::prelude::*;
32

43
declare_id!("GUkjQmrLPFXXNK1bFLKt8XQi6g3TjxcHVspbjDoHvMG2");
54

5+
/// The Pyth Receiver program that owns `PriceUpdateV2` accounts on devnet/mainnet.
6+
pub const PYTH_RECEIVER_PROGRAM_ID: Pubkey = pubkey!("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ");
7+
68
#[program]
79
pub mod anchor_test {
810
use super::*;
@@ -13,7 +15,10 @@ pub mod anchor_test {
1315
msg!("Price: {:?}", price_update.price_message.price);
1416
msg!("Confidence: {:?}", price_update.price_message.conf);
1517
msg!("Exponent: {:?}", price_update.price_message.exponent);
16-
msg!("Publish Time: {:?}", price_update.price_message.publish_time);
18+
msg!(
19+
"Publish Time: {:?}",
20+
price_update.price_message.publish_time
21+
);
1722
Ok(())
1823
}
1924
}
@@ -22,3 +27,100 @@ pub mod anchor_test {
2227
pub struct ReadPrice<'info> {
2328
pub price_update: Account<'info, PriceUpdateV2>,
2429
}
30+
31+
// ---------------------------------------------------------------------------
32+
// Pyth `PriceUpdateV2` account, vendored from `pyth-solana-receiver-sdk`.
33+
//
34+
// The official `pyth-solana-receiver-sdk` is NOT Anchor 1.0 compatible (as of
35+
// June 2026), so this example mirrors the `PriceUpdateV2` account type locally
36+
// instead of importing it.
37+
//
38+
// Details: the latest `pyth-solana-receiver-sdk` (1.2.0) builds against
39+
// `anchor-lang` 0.32 and pulls `pythnet-sdk` (2.3.1), which still derives
40+
// borsh 0.10 on `PriceFeedMessage`. Anchor 0.32's `AnchorSerialize` /
41+
// `AnchorDeserialize` derives require borsh 1.x, so the SDK's own
42+
// `PriceUpdateV2` fails to compile:
43+
//
44+
// error[E0277]: the trait bound
45+
// `pythnet_sdk::messages::PriceFeedMessage: BorshSerialize` is not satisfied
46+
//
47+
// No published `pyth-solana-receiver-sdk` targets `anchor-lang` 1.0 (which this
48+
// repo standardizes on) and no `pythnet-sdk` release has migrated to borsh 1.x,
49+
// so the dependency can't simply be upgraded. The fields, order, and 8-byte
50+
// discriminator below match the on-chain account exactly, and it is owned by
51+
// the Pyth Receiver program (see the `Owner` impl), so accounts written by Pyth
52+
// deserialize unchanged. Replace this with the SDK type once an Anchor 1.0 /
53+
// borsh 1.x compatible `pyth-solana-receiver-sdk` release ships.
54+
// ---------------------------------------------------------------------------
55+
56+
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)]
57+
pub enum VerificationLevel {
58+
/// Partially verified: only `num_signatures` of the Wormhole guardians
59+
/// were checked against the price update.
60+
Partial { num_signatures: u8 },
61+
/// Fully verified against the full guardian set.
62+
Full,
63+
}
64+
65+
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)]
66+
pub struct PriceFeedMessage {
67+
pub feed_id: [u8; 32],
68+
pub price: i64,
69+
pub conf: u64,
70+
pub exponent: i32,
71+
pub publish_time: i64,
72+
pub prev_publish_time: i64,
73+
pub ema_price: i64,
74+
pub ema_conf: u64,
75+
}
76+
77+
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)]
78+
pub struct PriceUpdateV2 {
79+
pub write_authority: Pubkey,
80+
pub verification_level: VerificationLevel,
81+
pub price_message: PriceFeedMessage,
82+
pub posted_slot: u64,
83+
}
84+
85+
// Anchor's 8-byte discriminator: sha256("account:PriceUpdateV2")[..8].
86+
impl anchor_lang::Discriminator for PriceUpdateV2 {
87+
const DISCRIMINATOR: &'static [u8] = &[34, 241, 35, 99, 157, 126, 244, 205];
88+
}
89+
90+
// The account is created and owned by the Pyth Receiver program.
91+
impl anchor_lang::Owner for PriceUpdateV2 {
92+
fn owner() -> Pubkey {
93+
PYTH_RECEIVER_PROGRAM_ID
94+
}
95+
}
96+
97+
impl anchor_lang::AccountSerialize for PriceUpdateV2 {
98+
fn try_serialize<W: std::io::Write>(&self, writer: &mut W) -> Result<()> {
99+
writer
100+
.write_all(<Self as anchor_lang::Discriminator>::DISCRIMINATOR)
101+
.map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotSerialize)?;
102+
AnchorSerialize::serialize(self, writer)
103+
.map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotSerialize)?;
104+
Ok(())
105+
}
106+
}
107+
108+
impl anchor_lang::AccountDeserialize for PriceUpdateV2 {
109+
fn try_deserialize(buf: &mut &[u8]) -> Result<Self> {
110+
let disc = <Self as anchor_lang::Discriminator>::DISCRIMINATOR;
111+
if buf.len() < disc.len() {
112+
return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorNotFound.into());
113+
}
114+
if &buf[..disc.len()] != disc {
115+
return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch.into());
116+
}
117+
Self::try_deserialize_unchecked(buf)
118+
}
119+
120+
fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self> {
121+
let disc = <Self as anchor_lang::Discriminator>::DISCRIMINATOR;
122+
let mut data: &[u8] = &buf[disc.len()..];
123+
AnchorDeserialize::deserialize(&mut data)
124+
.map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into())
125+
}
126+
}

0 commit comments

Comments
 (0)