Skip to content

Commit 7f41bad

Browse files
committed
fix(v2): Port anchor-client to anchor-lang-v2
1 parent 6d62cba commit 7f41bad

7 files changed

Lines changed: 123 additions & 27 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ debug = []
1818
mock = []
1919

2020
[dependencies]
21-
anchor-lang = { path = "../lang", version = "2.0.0" }
21+
anchor-lang-v2 = { path = "../lang-v2", version = "2.0.0" }
2222
anyhow = "1"
23+
base64 = "0.22"
24+
borsh = "1"
2325
futures = "0.3"
2426
regex = "1"
2527
serde = { version = "1", features = ["derive"] }

client/src/blocking.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use {
55
ClientError, Config, EventContext, EventUnsubscriber, Program, ProgramAccountsIterator,
66
RequestBuilder,
77
},
8-
anchor_lang::{prelude::Pubkey, AccountDeserialize, Discriminator},
8+
anchor_lang_v2::{AccountDeserialize, Discriminator},
9+
solana_program::pubkey::Pubkey,
910
solana_commitment_config::CommitmentConfig,
1011
solana_rpc_client::nonblocking::rpc_client::RpcClient as AsyncRpcClient,
1112
solana_rpc_client_api::{config::RpcSendTransactionConfig, filter::RpcFilterType},
@@ -98,7 +99,7 @@ impl<C: Deref<Target = impl Signer> + Clone> Program<C> {
9899
self.rt.block_on(self.accounts_lazy_internal(filters))
99100
}
100101

101-
pub fn on<T: anchor_lang::Event + anchor_lang::AnchorDeserialize>(
102+
pub fn on<T: anchor_lang_v2::Event + anchor_lang_v2::AnchorDeserialize>(
102103
&self,
103104
f: impl FnMut(&EventContext, T) + Send + 'static,
104105
) -> Result<EventUnsubscriber<'_>, ClientError> {

client/src/lib.rs

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
#[cfg(feature = "async")]
6868
pub use nonblocking::ThreadSafeSigner;
6969
pub use {
70-
anchor_lang,
70+
anchor_lang_v2,
7171
cluster::Cluster,
7272
solana_commitment_config::CommitmentConfig,
7373
solana_instruction::Instruction,
@@ -81,10 +81,8 @@ pub use {
8181
solana_transaction::Transaction,
8282
};
8383
use {
84-
anchor_lang::{
85-
solana_program::{program_error::ProgramError, pubkey::Pubkey},
86-
AccountDeserialize, Discriminator, InstructionData, ToAccountMetas,
87-
},
84+
anchor_lang_v2::{AccountDeserialize, Discriminator, InstructionData, ToAccountMetas},
85+
solana_program::{program_error::ProgramError, pubkey::Pubkey},
8886
futures::{Future, StreamExt},
8987
regex::Regex,
9088
solana_account_decoder::{UiAccount, UiAccountEncoding},
@@ -295,7 +293,7 @@ impl<C: Deref<Target = impl Signer> + Clone> Program<C> {
295293
})
296294
}
297295

298-
async fn on_internal<T: anchor_lang::Event + anchor_lang::AnchorDeserialize>(
296+
async fn on_internal<T: anchor_lang_v2::Event + anchor_lang_v2::AnchorDeserialize>(
299297
&self,
300298
mut f: impl FnMut(&EventContext, T) + Send + 'static,
301299
) -> Result<
@@ -370,14 +368,11 @@ impl<T> Iterator for ProgramAccountsIterator<T> {
370368
}
371369
}
372370

373-
pub fn handle_program_log<T: anchor_lang::Event + anchor_lang::AnchorDeserialize>(
371+
pub fn handle_program_log<T: anchor_lang_v2::Event + anchor_lang_v2::AnchorDeserialize>(
374372
self_program_str: &str,
375373
l: &str,
376374
) -> Result<(Option<T>, Option<String>, bool), ClientError> {
377-
use {
378-
anchor_lang::__private::base64,
379-
base64::{engine::general_purpose::STANDARD, Engine},
380-
};
375+
use base64::{engine::general_purpose::STANDARD, Engine};
381376

382377
// Log emitted from the current program.
383378
if let Some(log) = l
@@ -477,8 +472,6 @@ pub enum ClientError {
477472
#[error("Account not found")]
478473
AccountNotFound,
479474
#[error("{0}")]
480-
AnchorError(#[from] anchor_lang::error::Error),
481-
#[error("{0}")]
482475
ProgramError(#[from] ProgramError),
483476
#[error("{0}")]
484477
SolanaClientError(#[from] Box<SolanaClientError>),
@@ -673,7 +666,7 @@ impl<C: Deref<Target = impl Signer> + Clone, S: AsSigner> RequestBuilder<'_, C,
673666
}
674667
}
675668

676-
fn parse_logs_response<T: anchor_lang::Event + anchor_lang::AnchorDeserialize>(
669+
fn parse_logs_response<T: anchor_lang_v2::Event + anchor_lang_v2::AnchorDeserialize>(
677670
logs: RpcResponse<RpcLogsResponse>,
678671
program_id_str: &str,
679672
) -> Result<Vec<T>, ClientError> {
@@ -733,18 +726,29 @@ fn parse_logs_response<T: anchor_lang::Event + anchor_lang::AnchorDeserialize>(
733726

734727
#[cfg(test)]
735728
mod tests {
736-
// Creating a mock struct that implements `anchor_lang::events`
737-
// for type inference in `test_logs`
729+
// Mock event: minimal manual impl avoiding the `#[event]` macro, which
730+
// depends on the `wincode` derive (anchor-lang-v2 transitively pulls it
731+
// in but the re-exported derive's generated code references the bare
732+
// `wincode` path, not visible from this crate). The test only needs
733+
// `Event + AnchorDeserialize + Discriminator` for type inference inside
734+
// `parse_logs_response::<MockEvent>`.
738735
use {
739-
anchor_lang::prelude::*,
736+
anchor_lang_v2::{AnchorDeserialize, AnchorSerialize, Discriminator, Event},
740737
futures::{SinkExt, StreamExt},
741738
solana_rpc_client_api::response::RpcResponseContext,
742739
std::sync::atomic::{AtomicU64, Ordering},
743740
tokio_tungstenite::tungstenite::Message,
744741
};
745-
#[derive(Debug, Clone, Copy)]
746-
#[event]
742+
#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize)]
747743
pub struct MockEvent {}
744+
impl Discriminator for MockEvent {
745+
const DISCRIMINATOR: &'static [u8] = &[0; 8];
746+
}
747+
impl Event for MockEvent {
748+
fn data(&self) -> Vec<u8> {
749+
Vec::new()
750+
}
751+
}
748752

749753
use super::*;
750754
#[test]
@@ -775,7 +779,7 @@ mod tests {
775779
}
776780

777781
#[test]
778-
fn test_parse_logs_response() -> Result<()> {
782+
fn test_parse_logs_response() -> anyhow::Result<()> {
779783
// Mock logs received within an `RpcResponse`. These are based on a Jupiter transaction.
780784
let logs = vec![
781785
"Program VeryCoolProgram invoke [1]", // Outer instruction #1 starts
@@ -892,7 +896,7 @@ mod tests {
892896
}
893897

894898
#[test]
895-
fn test_parse_logs_response_fake_pop() -> Result<()> {
899+
fn test_parse_logs_response_fake_pop() -> anyhow::Result<()> {
896900
let logs = [
897901
"Program fake111111111111111111111111111111111111112 invoke [1]",
898902
"Program log: i logged success",

client/src/nonblocking.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ use {
33
AsSigner, ClientError, Config, EventContext, EventUnsubscriber, Program,
44
ProgramAccountsIterator, RequestBuilder,
55
},
6-
anchor_lang::{prelude::Pubkey, AccountDeserialize, Discriminator},
6+
anchor_lang_v2::{AccountDeserialize, Discriminator},
7+
solana_program::pubkey::Pubkey,
78
solana_commitment_config::CommitmentConfig,
89
solana_rpc_client::nonblocking::rpc_client::RpcClient as AsyncRpcClient,
910
solana_rpc_client_api::{config::RpcSendTransactionConfig, filter::RpcFilterType},
@@ -107,7 +108,7 @@ impl<C: Deref<Target = impl Signer> + Clone> Program<C> {
107108
/// Subscribe to program logs.
108109
///
109110
/// Returns an [`EventUnsubscriber`] to unsubscribe and close connection gracefully.
110-
pub async fn on<T: anchor_lang::Event + anchor_lang::AnchorDeserialize>(
111+
pub async fn on<T: anchor_lang_v2::Event + anchor_lang_v2::AnchorDeserialize>(
111112
&self,
112113
f: impl FnMut(&EventContext, T) + Send + 'static,
113114
) -> Result<EventUnsubscriber<'_>, ClientError> {

lang-v2/derive/src/lib.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,67 @@ pub fn account(attr: TokenStream, item: TokenStream) -> TokenStream {
10891089
Fields::Unit => Vec::new(),
10901090
};
10911091

1092+
// Client-side `AccountDeserialize` impl. Mode-dependent: borsh accounts
1093+
// run `BorshDeserialize` over the post-disc tail; pod accounts do a
1094+
// `bytemuck::pod_read_unaligned` on a sized slice. Both share the disc-
1095+
// check shape so a wrong-type fetch surfaces as `InvalidAccountData`.
1096+
let account_deserialize_impl = if is_borsh {
1097+
quote! {
1098+
impl anchor_lang_v2::AccountDeserialize for #name {
1099+
fn try_deserialize(
1100+
buf: &mut &[u8],
1101+
) -> ::core::result::Result<Self, anchor_lang_v2::Error> {
1102+
use anchor_lang_v2::Discriminator as _;
1103+
if buf.len() < <Self as anchor_lang_v2::Discriminator>::DISCRIMINATOR.len() {
1104+
return Err(anchor_lang_v2::Error::AccountDataTooSmall);
1105+
}
1106+
let (disc, rest) = buf.split_at(<Self as anchor_lang_v2::Discriminator>::DISCRIMINATOR.len());
1107+
if disc != <Self as anchor_lang_v2::Discriminator>::DISCRIMINATOR {
1108+
return Err(anchor_lang_v2::Error::InvalidAccountData);
1109+
}
1110+
*buf = rest;
1111+
Self::try_deserialize_unchecked(buf)
1112+
}
1113+
fn try_deserialize_unchecked(
1114+
buf: &mut &[u8],
1115+
) -> ::core::result::Result<Self, anchor_lang_v2::Error> {
1116+
<Self as anchor_lang_v2::borsh::BorshDeserialize>::deserialize(buf)
1117+
.map_err(|_| anchor_lang_v2::Error::InvalidAccountData)
1118+
}
1119+
}
1120+
}
1121+
} else {
1122+
quote! {
1123+
impl anchor_lang_v2::AccountDeserialize for #name {
1124+
fn try_deserialize(
1125+
buf: &mut &[u8],
1126+
) -> ::core::result::Result<Self, anchor_lang_v2::Error> {
1127+
use anchor_lang_v2::Discriminator as _;
1128+
if buf.len() < <Self as anchor_lang_v2::Discriminator>::DISCRIMINATOR.len() {
1129+
return Err(anchor_lang_v2::Error::AccountDataTooSmall);
1130+
}
1131+
let (disc, rest) = buf.split_at(<Self as anchor_lang_v2::Discriminator>::DISCRIMINATOR.len());
1132+
if disc != <Self as anchor_lang_v2::Discriminator>::DISCRIMINATOR {
1133+
return Err(anchor_lang_v2::Error::InvalidAccountData);
1134+
}
1135+
*buf = rest;
1136+
Self::try_deserialize_unchecked(buf)
1137+
}
1138+
fn try_deserialize_unchecked(
1139+
buf: &mut &[u8],
1140+
) -> ::core::result::Result<Self, anchor_lang_v2::Error> {
1141+
let n = ::core::mem::size_of::<Self>();
1142+
if buf.len() < n {
1143+
return Err(anchor_lang_v2::Error::AccountDataTooSmall);
1144+
}
1145+
let value: Self = anchor_lang_v2::bytemuck::pod_read_unaligned(&buf[..n]);
1146+
*buf = &buf[n..];
1147+
Ok(value)
1148+
}
1149+
}
1150+
}
1151+
};
1152+
10921153
let (struct_attrs, pod_impls) = if is_borsh {
10931154
(
10941155
quote! { #[derive(anchor_lang_v2::borsh::BorshSerialize, anchor_lang_v2::borsh::BorshDeserialize, Default)] },
@@ -1159,6 +1220,7 @@ pub fn account(attr: TokenStream, item: TokenStream) -> TokenStream {
11591220
impl anchor_lang_v2::Discriminator for #name {
11601221
const DISCRIMINATOR: &'static [u8] = &[#(#disc_literals),*];
11611222
}
1223+
#account_deserialize_impl
11621224
#[cfg(feature = "idl-build")]
11631225
impl anchor_lang_v2::IdlAccountType for #name {
11641226
const __IDL_TYPE: Option<&'static str> = Some(#idl_type_json);

lang-v2/src/traits.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,30 @@ pub trait Discriminator {
204204
const DISCRIMINATOR: &'static [u8];
205205
}
206206

207+
/// Client-side account deserialization. Mirrors v1 anchor-lang's trait so
208+
/// `anchor-client` can fetch raw account bytes and decode them into the
209+
/// user's `#[account]` struct. The `#[account]` macro emits two impl
210+
/// bodies:
211+
///
212+
/// - Borsh mode (`#[account(borsh)]`): check disc, run `BorshDeserialize`.
213+
/// - Pod mode (default): check disc, `bytemuck::pod_read_unaligned` on
214+
/// the post-disc bytes.
215+
///
216+
/// Not used by the on-chain account wrappers (`BorshAccount` / `Slab`),
217+
/// which read directly from `AccountView` borrows; this is purely the
218+
/// off-chain client helper.
219+
pub trait AccountDeserialize: Sized {
220+
/// Verify the leading discriminator and decode. Default implementation
221+
/// strips the disc and forwards to `try_deserialize_unchecked`.
222+
fn try_deserialize(buf: &mut &[u8]) -> Result<Self, ProgramError> {
223+
Self::try_deserialize_unchecked(buf)
224+
}
225+
226+
/// Decode without verifying the discriminator. Used during initialization
227+
/// when the bytes are zero or otherwise not yet stamped with the disc.
228+
fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self, ProgramError>;
229+
}
230+
207231
/// Wrapper-level init: creates the on-chain account and returns a loaded
208232
/// `Self`. `Slab<H, T>` and `BorshAccount<T>` get this automatically;
209233
/// custom wrappers implement it directly.

0 commit comments

Comments
 (0)