use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields, Expr, Lit, ItemImpl, ImplItem, ItemStruct, Type};
fn parse_variant_accounts(attrs: &[syn::Attribute]) -> Vec<(String, bool, bool, bool)> {
let mut accounts = Vec::new();
for attr in attrs {
if attr.path().is_ident("accounts") {
let content = attr.meta.require_list()
.expect("#[accounts(...)] requires a list");
let tokens_str = content.tokens.to_string();
for part in tokens_str.split(',') {
let part = part.trim();
if part.is_empty() { continue; }
let (name, constraint) = if let Some(colon_pos) = part.find(':') {
let name = part[..colon_pos].trim().to_string();
let constraint = part[colon_pos + 1..].trim().to_string();
(name, constraint)
} else {
(part.to_string(), String::new())
};
let is_signer = constraint == "signer" || constraint == "mut_signer";
let is_writable = constraint == "mut" || constraint == "mut_signer";
let is_program = constraint == "program";
accounts.push((name, is_signer, is_writable, is_program));
}
}
}
accounts
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn SolzempicEntrypoint(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
let enum_name = &input.ident;
let vis = &input.vis;
let attrs = &input.attrs;
let program_id_tokens: proc_macro2::TokenStream = if attr.is_empty() {
panic!("SolzempicEntrypoint requires a program ID, e.g. #[SolzempicEntrypoint(\"Your111...\")]");
} else {
let attr_str = attr.to_string();
let trimmed = attr_str.trim();
if trimmed.starts_with('"') && trimmed.ends_with('"') {
let pubkey_str = &trimmed[1..trimmed.len()-1];
let pubkey_str_lit = syn::LitStr::new(pubkey_str, proc_macro2::Span::call_site());
quote! { ::pinocchio_pubkey::pubkey!(#pubkey_str_lit) }
} else {
let ident: syn::Ident = syn::parse(attr.clone())
.expect("SolzempicEntrypoint attribute must be a string literal or identifier");
quote! { #ident }
}
};
let variants = match &input.data {
Data::Enum(data_enum) => &data_enum.variants,
_ => panic!("SolzempicEntrypoint can only be applied to enums"),
};
let variant_info: Vec<_> = variants.iter().map(|variant| {
let variant_name = &variant.ident;
let discriminant = variant.discriminant.as_ref()
.expect("SolzempicEntrypoint requires explicit discriminant values");
let disc_expr = &discriminant.1;
let accounts = parse_variant_accounts(&variant.attrs);
(variant_name, disc_expr, accounts)
}).collect();
let try_from_arms = variant_info.iter().map(|(name, disc, _)| {
quote! { #disc => Ok(#enum_name::#name), }
});
let dispatch_arms = variant_info.iter().map(|(name, _, _)| {
quote! {
#enum_name::#name => <#name<'_> as ::solzempic::Instruction<'_>>::process(program_id, accounts, data),
}
});
let process_arms = variant_info.iter().map(|(name, disc, _)| {
quote! {
#disc => <#name<'_> as ::solzempic::Instruction<'_>>::process(program_id, accounts, &data[1..]),
}
});
let variant_defs = variant_info.iter().map(|(name, disc, accounts)| {
let account_attrs: Vec<proc_macro2::TokenStream> = accounts.iter().enumerate().map(|(idx, (acc_name, is_signer, is_writable, _is_program))| {
let mut constraints = Vec::new();
if *is_writable { constraints.push(quote! { writable }); }
if *is_signer { constraints.push(quote! { signer }); }
let idx_lit = syn::LitInt::new(&idx.to_string(), proc_macro2::Span::call_site());
let name_lit = syn::LitStr::new(acc_name, proc_macro2::Span::call_site());
if constraints.is_empty() {
quote! { #[account(#idx_lit, name = #name_lit)] }
} else {
quote! { #[account(#idx_lit, #(#constraints),*, name = #name_lit)] }
}
}).collect();
quote! {
#(#account_attrs)*
#name = #disc
}
});
let expanded = quote! {
pub const ID: ::solana_address::Address = ::solana_address::Address::new_from_array(#program_id_tokens);
pub struct Solzempic;
impl ::solzempic::Framework for Solzempic {
const PROGRAM_ID: ::solana_address::Address = ID;
}
pub type AccountRef<'a, T> = ::solzempic::AccountRef<'a, T, Solzempic>;
pub type AccountRefMut<'a, T> = ::solzempic::AccountRefMut<'a, T, Solzempic>;
pub type ShardRefContext<'a, T> = ::solzempic::ShardRefContext<'a, T, Solzempic>;
#[inline]
pub fn id() -> &'static ::solana_address::Address {
&ID
}
#(#attrs)*
#[repr(u8)]
#[cfg_attr(feature = "shank", derive(::shank::ShankInstruction))]
#vis enum #enum_name {
#(#variant_defs),*
}
impl ::core::convert::TryFrom<u8> for #enum_name {
type Error = ::pinocchio::error::ProgramError;
#[inline]
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
#(#try_from_arms)*
_ => Err(::pinocchio::error::ProgramError::InvalidInstructionData),
}
}
}
impl #enum_name {
#[inline]
pub fn dispatch(
self,
program_id: &::solana_address::Address,
accounts: &[::pinocchio::AccountView],
data: &[u8],
) -> ::pinocchio::ProgramResult {
match self {
#(#dispatch_arms)*
}
}
#[inline]
pub fn process(
program_id: &::solana_address::Address,
accounts: &[::pinocchio::AccountView],
data: &[u8],
) -> ::pinocchio::ProgramResult {
let discriminator = *data.first()
.ok_or(::pinocchio::error::ProgramError::InvalidInstructionData)?;
match discriminator {
#(#process_arms)*
_ => Err(::pinocchio::error::ProgramError::InvalidInstructionData),
}
}
}
#[inline]
pub fn process_instruction(
program_id: &::solana_address::Address,
accounts: &[::pinocchio::AccountView],
instruction_data: &[u8],
) -> ::pinocchio::ProgramResult {
#enum_name::process(program_id, accounts, instruction_data)
}
#[cfg(not(feature = "no-entrypoint"))]
::pinocchio::entrypoint!(process_instruction);
};
TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn instruction(attr: TokenStream, item: TokenStream) -> TokenStream {
let item_clone = item.clone();
if let Ok(input) = syn::parse::<ItemStruct>(item_clone) {
return instruction_struct_impl(attr, input);
}
let params_type: syn::Path = syn::parse(attr)
.expect("instruction macro on impl requires params type, e.g. #[instruction(MyParams)]");
let input = parse_macro_input!(item as ItemImpl);
let struct_type = &input.self_ty;
let struct_name = match struct_type.as_ref() {
syn::Type::Path(type_path) => &type_path.path.segments.last().unwrap().ident,
_ => panic!("instruction macro requires a struct type"),
};
let methods: Vec<_> = input.items.iter().filter_map(|item| {
if let ImplItem::Fn(method) = item {
Some(method)
} else {
None
}
}).collect();
let expanded = quote! {
impl ::solzempic::InstructionParams for #struct_name<'_> {
type Params = #params_type;
}
impl<'a> ::solzempic::Instruction<'a> for #struct_name<'a> {
#(#methods)*
}
};
TokenStream::from(expanded)
}
fn instruction_struct_impl(attr: TokenStream, input: ItemStruct) -> TokenStream {
let struct_name = &input.ident;
let vis = &input.vis;
let attrs = &input.attrs;
let generics = &input.generics;
let start_index: usize = if attr.is_empty() {
0
} else {
syn::parse::<syn::LitInt>(attr)
.map(|lit| lit.base10_parse::<usize>().unwrap_or(0))
.unwrap_or(0)
};
let fields = match &input.fields {
Fields::Named(fields_named) => &fields_named.named,
_ => panic!("instruction macro on struct only supports named fields"),
};
let mut account_metas: Vec<proc_macro2::TokenStream> = Vec::new();
let mut shank_attr_strings: Vec<String> = Vec::new();
let mut current_idx = start_index;
for field in fields.iter() {
let field_name = field.ident.as_ref().expect("Named field required");
let field_name_str = field_name.to_string();
let field_ty = &field.ty;
let (is_signer, is_writable, is_program, expand_count) = analyze_field_type(field_ty);
if expand_count > 1 {
let shard_names = ["left_shard", "current_shard", "right_shard"];
for (i, shard_name) in shard_names.iter().enumerate() {
let idx = current_idx + i;
account_metas.push(quote! {
::solzempic::ShankAccountMeta {
index: #idx,
name: #shard_name,
is_signer: false,
is_writable: true,
is_program: false,
}
});
shank_attr_strings.push(format!("#[account({}, writable, name=\"{}\")]", idx, shard_name));
}
current_idx += expand_count;
} else {
let mut constraints = Vec::new();
if is_writable { constraints.push("writable"); }
if is_signer { constraints.push("signer"); }
let constraints_str = if constraints.is_empty() {
String::new()
} else {
format!(", {}", constraints.join(", "))
};
shank_attr_strings.push(format!("#[account({}{}, name=\"{}\")]", current_idx, constraints_str, field_name_str));
account_metas.push(quote! {
::solzempic::ShankAccountMeta {
index: #current_idx,
name: #field_name_str,
is_signer: #is_signer,
is_writable: #is_writable,
is_program: #is_program,
}
});
current_idx += 1;
}
}
let num_accounts = account_metas.len();
let shank_output = shank_attr_strings.join("\n ");
let field_defs = fields.iter().map(|f| {
let field_name = &f.ident;
let field_ty = &f.ty;
let field_vis = &f.vis;
let field_attrs = &f.attrs;
quote! {
#(#field_attrs)*
#field_vis #field_name: #field_ty
}
});
let expanded = quote! {
#(#attrs)*
#vis struct #struct_name #generics {
#(#field_defs),*
}
impl #struct_name<'_> {
pub const NUM_ACCOUNTS: usize = #num_accounts;
pub const SHANK_ACCOUNTS: [::solzempic::ShankAccountMeta; #num_accounts] = [
#(#account_metas),*
];
pub fn shank_accounts() -> &'static str {
#shank_output
}
}
};
TokenStream::from(expanded)
}
#[proc_macro_derive(Account, attributes(account))]
pub fn derive_account(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let vis = &input.vis;
let discriminator = extract_discriminator(&input.attrs)
.expect("Account derive requires #[account(discriminator = N)] attribute");
let fields = match &input.data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Named(fields_named) => &fields_named.named,
_ => panic!("Account derive only supports structs with named fields"),
},
_ => panic!("Account derive only supports structs"),
};
let field_defs = fields.iter().map(|f| {
let field_name = &f.ident;
let field_ty = &f.ty;
let field_vis = &f.vis;
let attrs = &f.attrs;
quote! {
#(#attrs)*
#field_vis #field_name: #field_ty
}
});
let expanded = quote! {
#[repr(C)]
#[derive(Clone, Copy, ::bytemuck::Pod, ::bytemuck::Zeroable)]
#[cfg_attr(feature = "shank", derive(::shank::ShankAccount))]
#vis struct #name {
pub discriminator: [u8; 8],
#(#field_defs),*
}
impl #name {
pub const DISCRIMINATOR_VALUE: u8 = #discriminator;
pub const DISCRIMINATOR_BYTES: [u8; 8] = [#discriminator, 0, 0, 0, 0, 0, 0, 0];
#[inline]
pub fn check_discriminator(data: &[u8]) -> bool {
!data.is_empty() && data[0] == #discriminator
}
}
impl ::solzempic::Loadable for #name {
const DISCRIMINATOR: u8 = #discriminator;
}
};
TokenStream::from(expanded)
}
fn analyze_field_type(ty: &Type) -> (bool, bool, bool, usize) {
match ty {
Type::Path(type_path) => {
if let Some(segment) = type_path.path.segments.last() {
let type_name = segment.ident.to_string();
match type_name.as_str() {
"Signer" => (true, false, false, 1),
"MutSigner" => (true, true, false, 1),
"AccountRefMut" => (false, true, false, 1),
"TokenAccountRefMut" => (false, true, false, 1),
"Writable" => (false, true, false, 1),
"AccountRef" => (false, false, false, 1),
"TokenAccountRef" => (false, false, false, 1),
"Mint" => (false, false, false, 1),
"ValidatedAccount" => (false, false, false, 1),
"ReadOnly" => (false, false, false, 1),
"SystemProgram" => (false, false, true, 1),
"TokenProgram" => (false, false, true, 1),
"AtaProgram" => (false, false, true, 1),
"Token2022Program" => (false, false, true, 1),
"ShardRefContext" => (false, true, false, 3),
_ => (false, false, false, 1),
}
} else {
(false, false, false, 1)
}
}
Type::Reference(_) => {
(false, false, false, 1)
}
_ => (false, false, false, 1),
}
}
#[proc_macro_attribute]
pub fn account(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemStruct);
let name = &input.ident;
let vis = &input.vis;
let attrs = &input.attrs;
let generics = &input.generics;
let discriminator_expr: Option<syn::Expr> = if attr.is_empty() {
None
} else {
let attr_str = attr.to_string();
if let Some(eq_pos) = attr_str.find('=') {
let expr_str = attr_str[eq_pos + 1..].trim();
syn::parse_str(expr_str).ok()
} else {
None
}
};
let fields = match &input.fields {
Fields::Named(fields_named) => &fields_named.named,
_ => panic!("account macro only supports structs with named fields"),
};
let field_defs = fields.iter().map(|f| {
let field_name = &f.ident;
let field_ty = &f.ty;
let field_vis = &f.vis;
let field_attrs = &f.attrs;
quote! {
#(#field_attrs)*
#field_vis #field_name: #field_ty
}
});
let has_discriminator_field = fields.iter().any(|f| {
f.ident.as_ref().map(|i| i == "discriminator").unwrap_or(false)
});
let loadable_impl = discriminator_expr.map(|disc| {
let account_impl = if has_discriminator_field {
quote! {
impl ::solzempic::traits::Account for #name {
const DISCRIMINATOR: u8 = #disc as u8;
const LEN: usize = ::core::mem::size_of::<Self>();
#[inline]
fn discriminator(&self) -> &[u8; 8] {
&self.discriminator
}
}
}
} else {
quote! {}
};
quote! {
impl ::solzempic::Loadable for #name {
const DISCRIMINATOR: u8 = #disc as u8;
}
#account_impl
}
});
let expanded = quote! {
#[repr(C)]
#[derive(Clone, Copy)]
#[cfg_attr(feature = "shank", derive(::shank::ShankAccount))]
#(#attrs)*
#vis struct #name #generics {
#(#field_defs),*
}
unsafe impl ::bytemuck::Pod for #name {}
unsafe impl ::bytemuck::Zeroable for #name {}
#loadable_impl
};
TokenStream::from(expanded)
}
fn extract_discriminator(attrs: &[syn::Attribute]) -> Option<u8> {
for attr in attrs {
if attr.path().is_ident("account") {
let nested = attr.parse_args_with(
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated
).ok()?;
for meta in nested {
if let syn::Meta::NameValue(nv) = meta {
if nv.path.is_ident("discriminator") {
if let Expr::Lit(expr_lit) = &nv.value {
if let Lit::Int(lit_int) = &expr_lit.lit {
return lit_int.base10_parse::<u8>().ok();
}
}
}
}
}
}
}
None
}