use std::path::PathBuf;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use sha2::{Digest, Sha256};
use syn::{parse2, Ident, LitStr, Token};
struct Input {
module_name: Ident,
manifest_path: LitStr,
}
impl syn::parse::Parse for Input {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let module_name: Ident = input.parse()?;
let _: Token![,] = input.parse()?;
let manifest_path: LitStr = input.parse()?;
Ok(Self {
module_name,
manifest_path,
})
}
}
pub fn expand(input: TokenStream) -> syn::Result<TokenStream> {
let Input {
module_name,
manifest_path,
} = parse2(input)?;
let candidate = PathBuf::from(manifest_path.value());
let resolved = if candidate.is_absolute() {
candidate
} else {
let root = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
PathBuf::from(root).join(candidate)
};
let manifest_bytes = std::fs::read(&resolved).map_err(|e| {
syn::Error::new_spanned(
&manifest_path,
format!(
"declare_program!: could not read `{}`: {e}",
resolved.display()
),
)
})?;
let fingerprint: [u8; 32] = {
let mut h = Sha256::new();
h.update(&manifest_bytes);
h.finalize().into()
};
let fingerprint_bytes: Vec<u8> = fingerprint.to_vec();
let manifest_json: serde_json::Value =
serde_json::from_slice(&manifest_bytes).map_err(|e| {
syn::Error::new_spanned(
&manifest_path,
format!("declare_program!: invalid JSON: {e}"),
)
})?;
let program_name_lit = manifest_json
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("(unknown)");
let program_id_str = manifest_json
.get("program_id")
.and_then(|v| v.as_str())
.unwrap_or("");
let instructions = manifest_json
.get("instructions")
.and_then(|v| v.as_array())
.ok_or_else(|| {
syn::Error::new_spanned(
&manifest_path,
"declare_program!: manifest has no `instructions` array",
)
})?;
let mut instruction_items: Vec<TokenStream> = Vec::new();
for ix in instructions {
instruction_items.push(build_instruction(ix, &manifest_path)?);
}
let expanded = quote! {
#[doc = concat!("Typed CPI surface for the `", #program_name_lit, "` Hopper program.")]
pub mod #module_name {
#![allow(dead_code, non_snake_case)]
pub const FINGERPRINT: [u8; 32] = [#( #fingerprint_bytes ),*];
pub const PROGRAM_NAME: &str = #program_name_lit;
pub const PROGRAM_ID_STR: &str = #program_id_str;
#( #instruction_items )*
}
};
Ok(expanded)
}
fn build_instruction(ix: &serde_json::Value, manifest_span: &LitStr) -> syn::Result<TokenStream> {
let name = ix
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| syn::Error::new_spanned(manifest_span, "instruction missing `name`"))?;
let tag = ix.get("tag").and_then(|v| v.as_u64()).ok_or_else(|| {
syn::Error::new_spanned(manifest_span, format!("instruction `{name}` missing `tag`"))
})? as u8;
let name_ident = format_ident!("{}", camel_to_snake(name));
let args_struct_ident = format_ident!("{}Args", name);
let accounts_struct_ident = format_ident!("{}Accounts", name);
let args = ix
.get("args")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let accounts = ix
.get("accounts")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let mut args_fields: Vec<TokenStream> = Vec::new();
let mut args_serialize: Vec<TokenStream> = Vec::new();
for a in &args {
let aname = a
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| syn::Error::new_spanned(manifest_span, "arg missing `name`"))?;
let size = a.get("size").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
let field = format_ident!("{}", aname);
let (ty, serialize_stmt) = arg_type_for_size(size, &field);
args_fields.push(quote! { pub #field: #ty, });
args_serialize.push(serialize_stmt);
}
let mut account_fields: Vec<TokenStream> = Vec::new();
let mut account_metas: Vec<TokenStream> = Vec::new();
for acct in &accounts {
let aname = acct
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| syn::Error::new_spanned(manifest_span, "account missing `name`"))?;
let writable = acct
.get("writable")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let signer = acct
.get("signer")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let field = format_ident!("{}", aname);
account_fields.push(quote! { pub #field: [u8; 32], });
account_metas.push(quote! {
::hopper::__runtime::InstructionAccount {
pubkey: ::hopper::__runtime::Address::from(__acct.#field),
is_writable: #writable,
is_signer: #signer,
}
});
}
let tag_byte: u8 = tag;
let accounts_count = account_fields.len();
let args_size: usize = args
.iter()
.map(|a| a.get("size").and_then(|v| v.as_u64()).unwrap_or(0) as usize)
.sum();
Ok(quote! {
#[derive(Clone, Copy, Debug)]
pub struct #accounts_struct_ident {
#( #account_fields )*
}
#[derive(Clone, Copy, Debug)]
pub struct #args_struct_ident {
#( #args_fields )*
}
pub fn #name_ident(
__program_id: ::hopper::__runtime::Address,
__acct: #accounts_struct_ident,
__args: #args_struct_ident,
) -> (
::hopper::__runtime::Address,
[::hopper::__runtime::InstructionAccount; #accounts_count],
[u8; 1 + #args_size],
) {
let accounts = [ #( #account_metas ),* ];
let mut data = [0u8; 1 + #args_size];
data[0] = #tag_byte;
let mut __offset: usize = 1;
#( #args_serialize )*
(__program_id, accounts, data)
}
})
}
fn arg_type_for_size(size: usize, field: &Ident) -> (TokenStream, TokenStream) {
match size {
1 => (
quote!(u8),
quote! {
data[__offset] = __args.#field;
__offset += 1;
},
),
2 => (
quote!(u16),
quote! {
data[__offset..__offset + 2]
.copy_from_slice(&__args.#field.to_le_bytes());
__offset += 2;
},
),
4 => (
quote!(u32),
quote! {
data[__offset..__offset + 4]
.copy_from_slice(&__args.#field.to_le_bytes());
__offset += 4;
},
),
8 => (
quote!(u64),
quote! {
data[__offset..__offset + 8]
.copy_from_slice(&__args.#field.to_le_bytes());
__offset += 8;
},
),
16 => (
quote!(u128),
quote! {
data[__offset..__offset + 16]
.copy_from_slice(&__args.#field.to_le_bytes());
__offset += 16;
},
),
n => (
quote!([u8; #n]),
quote! {
data[__offset..__offset + #n]
.copy_from_slice(&__args.#field);
__offset += #n;
},
),
}
}
fn camel_to_snake(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 4);
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() && i > 0 {
out.push('_');
}
out.push(c.to_ascii_lowercase());
}
out
}