Skip to content

Commit 9bc01d2

Browse files
authored
Merge pull request #56 from quicknode/claude/pyth-anchor-fix
fix(pyth/anchor): vendor PriceUpdateV2 to build on Anchor 1.0; re-enable in CI
2 parents 3db55d3 + 925c91d commit 9bc01d2

4 files changed

Lines changed: 120 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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ 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. Tracked upstream at [pyth-network/pyth-crosschain#3756](https://github.com/pyth-network/pyth-crosschain/issues/3756).
17+
>
18+
> 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.
19+
720
## Major concepts
821
922
- 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: 107 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,103 @@ 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. Tracked upstream at
50+
// https://github.com/pyth-network/pyth-crosschain/issues/3756
51+
//
52+
// The fields, order, and 8-byte
53+
// discriminator below match the on-chain account exactly, and it is owned by
54+
// the Pyth Receiver program (see the `Owner` impl), so accounts written by Pyth
55+
// deserialize unchanged. Replace this with the SDK type once an Anchor 1.0 /
56+
// borsh 1.x compatible `pyth-solana-receiver-sdk` release ships.
57+
// ---------------------------------------------------------------------------
58+
59+
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)]
60+
pub enum VerificationLevel {
61+
/// Partially verified: only `num_signatures` of the Wormhole guardians
62+
/// were checked against the price update.
63+
Partial { num_signatures: u8 },
64+
/// Fully verified against the full guardian set.
65+
Full,
66+
}
67+
68+
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)]
69+
pub struct PriceFeedMessage {
70+
pub feed_id: [u8; 32],
71+
pub price: i64,
72+
pub conf: u64,
73+
pub exponent: i32,
74+
pub publish_time: i64,
75+
pub prev_publish_time: i64,
76+
pub ema_price: i64,
77+
pub ema_conf: u64,
78+
}
79+
80+
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)]
81+
pub struct PriceUpdateV2 {
82+
pub write_authority: Pubkey,
83+
pub verification_level: VerificationLevel,
84+
pub price_message: PriceFeedMessage,
85+
pub posted_slot: u64,
86+
}
87+
88+
// Anchor's 8-byte discriminator: sha256("account:PriceUpdateV2")[..8].
89+
impl anchor_lang::Discriminator for PriceUpdateV2 {
90+
const DISCRIMINATOR: &'static [u8] = &[34, 241, 35, 99, 157, 126, 244, 205];
91+
}
92+
93+
// The account is created and owned by the Pyth Receiver program.
94+
impl anchor_lang::Owner for PriceUpdateV2 {
95+
fn owner() -> Pubkey {
96+
PYTH_RECEIVER_PROGRAM_ID
97+
}
98+
}
99+
100+
impl anchor_lang::AccountSerialize for PriceUpdateV2 {
101+
fn try_serialize<W: std::io::Write>(&self, writer: &mut W) -> Result<()> {
102+
writer
103+
.write_all(<Self as anchor_lang::Discriminator>::DISCRIMINATOR)
104+
.map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotSerialize)?;
105+
AnchorSerialize::serialize(self, writer)
106+
.map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotSerialize)?;
107+
Ok(())
108+
}
109+
}
110+
111+
impl anchor_lang::AccountDeserialize for PriceUpdateV2 {
112+
fn try_deserialize(buf: &mut &[u8]) -> Result<Self> {
113+
let disc = <Self as anchor_lang::Discriminator>::DISCRIMINATOR;
114+
if buf.len() < disc.len() {
115+
return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorNotFound.into());
116+
}
117+
if &buf[..disc.len()] != disc {
118+
return Err(anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch.into());
119+
}
120+
Self::try_deserialize_unchecked(buf)
121+
}
122+
123+
fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self> {
124+
let disc = <Self as anchor_lang::Discriminator>::DISCRIMINATOR;
125+
let mut data: &[u8] = &buf[disc.len()..];
126+
AnchorDeserialize::deserialize(&mut data)
127+
.map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into())
128+
}
129+
}

0 commit comments

Comments
 (0)