244 lines
8.3 KiB
Rust
244 lines
8.3 KiB
Rust
extern crate proc_macro;
|
|
|
|
use anchor_syn::parser;
|
|
use heck::SnakeCase;
|
|
use quote::quote;
|
|
use syn::parse_macro_input;
|
|
|
|
/// The `#[interface]` attribute allows one to define an external program
|
|
/// dependency, without having any knowledge about the program, other than
|
|
/// the fact that it implements the given trait.
|
|
///
|
|
/// Additionally, the attribute generates a client that can be used to perform
|
|
/// CPI to these external dependencies.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// In the following example, we have a counter program, where the count
|
|
/// can only be set if the configured external program authorizes it.
|
|
///
|
|
/// ## Defining an `#[interface]`
|
|
///
|
|
/// First we define the program that depends on an external interface.
|
|
///
|
|
/// ```ignore
|
|
/// use anchor_lang::prelude::*;
|
|
///
|
|
/// #[interface]
|
|
/// pub trait Auth<'info, T: Accounts<'info>> {
|
|
/// fn is_authorized(ctx: Context<T>, current: u64, new: u64) -> anchor_lang::Result<()>;
|
|
/// }
|
|
///
|
|
/// #[program]
|
|
/// pub mod counter {
|
|
/// use super::*;
|
|
///
|
|
/// #[state]
|
|
/// pub struct Counter {
|
|
/// pub count: u64,
|
|
/// pub auth_program: Pubkey,
|
|
/// }
|
|
///
|
|
/// impl Counter {
|
|
/// pub fn new(_ctx: Context<Empty>, auth_program: Pubkey) -> Result<Self> {
|
|
/// Ok(Self {
|
|
/// count: 0,
|
|
/// auth_program,
|
|
/// })
|
|
/// }
|
|
///
|
|
/// #[access_control(SetCount::accounts(&self, &ctx))]
|
|
/// pub fn set_count(&mut self, ctx: Context<SetCount>, new_count: u64) -> Result<()> {
|
|
/// // Ask the auth program if we should approve the transaction.
|
|
/// let cpi_program = ctx.accounts.auth_program.clone();
|
|
/// let cpi_ctx = CpiContext::new(cpi_program, Empty {});
|
|
///
|
|
/// // This is the client generated by the `#[interface]` attribute.
|
|
/// auth::is_authorized(cpi_ctx, self.count, new_count)?;
|
|
///
|
|
/// // Approved, so update.
|
|
/// self.count = new_count;
|
|
/// Ok(())
|
|
/// }
|
|
/// }
|
|
/// }
|
|
///
|
|
/// #[derive(Accounts)]
|
|
/// pub struct Empty {}
|
|
///
|
|
/// #[derive(Accounts)]
|
|
/// pub struct SetCount<'info> {
|
|
/// auth_program: AccountInfo<'info>,
|
|
/// }
|
|
///
|
|
/// impl<'info> SetCount<'info> {
|
|
/// pub fn accounts(counter: &Counter, ctx: &Context<SetCount>) -> Result<()> {
|
|
/// if ctx.accounts.auth_program.key != &counter.auth_program {
|
|
/// return Err(error!(ErrorCode::InvalidAuthProgram));
|
|
/// }
|
|
/// Ok(())
|
|
/// }
|
|
/// }
|
|
///
|
|
/// #[error_code]
|
|
/// pub enum ErrorCode {
|
|
/// #[msg("Invalid auth program.")]
|
|
/// InvalidAuthProgram,
|
|
/// }
|
|
///```
|
|
///
|
|
/// ## Defining an implementation
|
|
///
|
|
/// Now we define the program that implements the interface, which the above
|
|
/// program will call.
|
|
///
|
|
/// ```ignore
|
|
/// use anchor_lang::prelude::*;
|
|
/// use counter::Auth;
|
|
///
|
|
/// #[program]
|
|
/// pub mod counter_auth {
|
|
/// use super::*;
|
|
///
|
|
/// #[state]
|
|
/// pub struct CounterAuth;
|
|
///
|
|
/// impl<'info> Auth<'info, Empty> for CounterAuth {
|
|
/// fn is_authorized(_ctx: Context<Empty>, current: u64, new: u64) -> Result<()> {
|
|
/// if current % 2 == 0 {
|
|
/// if new % 2 == 0 {
|
|
/// return Err(ProgramError::Custom(50).into()); // Arbitrary error code.
|
|
/// }
|
|
/// } else {
|
|
/// if new % 2 == 1 {
|
|
/// return Err(ProgramError::Custom(60).into()); // Arbitrary error code.
|
|
/// }
|
|
/// }
|
|
/// Ok(())
|
|
/// }
|
|
/// }
|
|
/// }
|
|
/// #[derive(Accounts)]
|
|
/// pub struct Empty {}
|
|
/// ```
|
|
///
|
|
/// # Returning Values Across CPI
|
|
///
|
|
/// The caller above uses a `Result` to act as a boolean. However, in order
|
|
/// for this feature to be maximally useful, we need a way to return values from
|
|
/// interfaces. For now, one can do this by writing to a shared account, e.g.,
|
|
/// with the SPL's [Shared Memory Program](https://github.com/solana-labs/solana-program-library/tree/master/shared-memory).
|
|
/// In the future, Anchor will add the ability to return values across CPI
|
|
/// without having to worry about the details of shared memory accounts.
|
|
#[proc_macro_attribute]
|
|
pub fn interface(
|
|
_args: proc_macro::TokenStream,
|
|
input: proc_macro::TokenStream,
|
|
) -> proc_macro::TokenStream {
|
|
let item_trait = parse_macro_input!(input as syn::ItemTrait);
|
|
|
|
let trait_name = item_trait.ident.to_string();
|
|
let mod_name: proc_macro2::TokenStream = item_trait
|
|
.ident
|
|
.to_string()
|
|
.to_snake_case()
|
|
.parse()
|
|
.unwrap();
|
|
|
|
let methods: Vec<proc_macro2::TokenStream> = item_trait
|
|
.items
|
|
.iter()
|
|
.filter_map(|trait_item: &syn::TraitItem| match trait_item {
|
|
syn::TraitItem::Method(m) => Some(m),
|
|
_ => None,
|
|
})
|
|
.map(|method: &syn::TraitItemMethod| {
|
|
let method_name = &method.sig.ident;
|
|
let args: Vec<&syn::PatType> = method
|
|
.sig
|
|
.inputs
|
|
.iter()
|
|
.filter_map(|arg: &syn::FnArg| match arg {
|
|
syn::FnArg::Typed(pat_ty) => Some(pat_ty),
|
|
// TODO: just map this to None once we allow this feature.
|
|
_ => panic!("Invalid syntax. No self allowed."),
|
|
})
|
|
.filter(|pat_ty| {
|
|
let mut ty = parser::tts_to_string(&pat_ty.ty);
|
|
ty.retain(|s| !s.is_whitespace());
|
|
!ty.starts_with("Context<")
|
|
})
|
|
.collect();
|
|
let args_no_tys: Vec<&Box<syn::Pat>> = args
|
|
.iter()
|
|
.map(|arg| {
|
|
&arg.pat
|
|
})
|
|
.collect();
|
|
let args_struct = {
|
|
if args.is_empty() {
|
|
quote! {
|
|
use anchor_lang::prelude::borsh;
|
|
#[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)]
|
|
struct Args;
|
|
}
|
|
} else {
|
|
quote! {
|
|
use anchor_lang::prelude::borsh;
|
|
#[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)]
|
|
struct Args {
|
|
#(#args),*
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
let sighash_arr = anchor_syn::codegen::program::common::sighash(&trait_name, &method_name.to_string());
|
|
let sighash_tts: proc_macro2::TokenStream =
|
|
format!("{:?}", sighash_arr).parse().unwrap();
|
|
quote! {
|
|
pub fn #method_name<'a,'b, 'c, 'info, T: anchor_lang::Accounts<'info> + anchor_lang::ToAccountMetas + anchor_lang::ToAccountInfos<'info>>(
|
|
ctx: anchor_lang::context::CpiContext<'a, 'b, 'c, 'info, T>,
|
|
#(#args),*
|
|
) -> anchor_lang::Result<()> {
|
|
#args_struct
|
|
|
|
let ix = {
|
|
let ix = Args {
|
|
#(#args_no_tys),*
|
|
};
|
|
let mut ix_data = anchor_lang::AnchorSerialize::try_to_vec(&ix)
|
|
.map_err(|_| anchor_lang::error::ErrorCode::InstructionDidNotSerialize)?;
|
|
let mut data = #sighash_tts.to_vec();
|
|
data.append(&mut ix_data);
|
|
let accounts = ctx.to_account_metas(None);
|
|
anchor_lang::solana_program::instruction::Instruction {
|
|
program_id: *ctx.program.key,
|
|
accounts,
|
|
data,
|
|
}
|
|
};
|
|
let mut acc_infos = ctx.to_account_infos();
|
|
acc_infos.push(ctx.program.clone());
|
|
anchor_lang::solana_program::program::invoke_signed(
|
|
&ix,
|
|
&acc_infos,
|
|
ctx.signer_seeds,
|
|
).map_err(Into::into)
|
|
}
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
proc_macro::TokenStream::from(quote! {
|
|
#item_trait
|
|
|
|
/// Anchor generated module for invoking programs implementing an
|
|
/// `#[interface]` via CPI.
|
|
mod #mod_name {
|
|
use super::*;
|
|
#(#methods)*
|
|
}
|
|
})
|
|
}
|