diff --git a/.travis.yml b/.travis.yml index 5bd953744..e4f6fd71f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,6 +49,7 @@ jobs: - pushd examples/errors && anchor test && popd - pushd examples/spl/token-proxy && anchor test && popd - pushd examples/multisig && anchor test && popd + - pushd examples/interface && anchor test && popd - pushd examples/tutorial/basic-0 && anchor test && popd - pushd examples/tutorial/basic-1 && anchor test && popd - pushd examples/tutorial/basic-2 && anchor test && popd diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d26c3369..0f0d5fd19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ incremented for features. ## [Unreleased] +### Features + +* lang: Adds the ability to create and use CPI program interfaces [(#66)](https://github.com/project-serum/anchor/pull/66/files?file-filters%5B%5D=). + +### Breaking Changes + * lang, client, ts: Migrate from rust enum based method dispatch to a variant of sighash [(#64)](https://github.com/project-serum/anchor/pull/64). ## [0.1.0] - 2021-01-31 diff --git a/Cargo.lock b/Cargo.lock index fc4693f82..c59e133dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,18 @@ dependencies = [ "syn 1.0.57", ] +[[package]] +name = "anchor-attribute-interface" +version = "0.1.0" +dependencies = [ + "anchor-syn", + "anyhow", + "heck", + "proc-macro2 1.0.24", + "quote 1.0.8", + "syn 1.0.57", +] + [[package]] name = "anchor-attribute-program" version = "0.1.0" @@ -155,6 +167,7 @@ dependencies = [ "anchor-attribute-access-control", "anchor-attribute-account", "anchor-attribute-error", + "anchor-attribute-interface", "anchor-attribute-program", "anchor-attribute-state", "anchor-derive-accounts", diff --git a/examples/interface/Anchor.toml b/examples/interface/Anchor.toml new file mode 100644 index 000000000..2ebd5af99 --- /dev/null +++ b/examples/interface/Anchor.toml @@ -0,0 +1,2 @@ +cluster = "localnet" +wallet = "~/.config/solana/id.json" diff --git a/examples/interface/Cargo.toml b/examples/interface/Cargo.toml new file mode 100644 index 000000000..a60de986d --- /dev/null +++ b/examples/interface/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "programs/*" +] diff --git a/examples/interface/programs/counter-auth/Cargo.toml b/examples/interface/programs/counter-auth/Cargo.toml new file mode 100644 index 000000000..fc05df182 --- /dev/null +++ b/examples/interface/programs/counter-auth/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "counter-auth" +version = "0.1.0" +description = "Created with Anchor" +edition = "2018" + +[lib] +crate-type = ["cdylib", "lib"] +name = "counter_auth" + +[features] +no-entrypoint = [] +no-idl = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = { git = "https://github.com/project-serum/anchor" } +counter = { path = "../counter", features = ["cpi"] } diff --git a/examples/interface/programs/counter-auth/Xargo.toml b/examples/interface/programs/counter-auth/Xargo.toml new file mode 100644 index 000000000..1744f098a --- /dev/null +++ b/examples/interface/programs/counter-auth/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/examples/interface/programs/counter-auth/src/lib.rs b/examples/interface/programs/counter-auth/src/lib.rs new file mode 100644 index 000000000..8057455fe --- /dev/null +++ b/examples/interface/programs/counter-auth/src/lib.rs @@ -0,0 +1,43 @@ +//! counter-auth is an example of a program *implementing* an external program +//! interface. Here the `counter::Auth` trait, where we only allow a count +//! to be incremented if it changes the counter from odd -> even or even -> odd. +//! Creative, I know. :P. + +#![feature(proc_macro_hygiene)] + +use anchor_lang::prelude::*; +use counter::Auth; + +#[program] +pub mod counter_auth { + use super::*; + + #[state] + pub struct CounterAuth {} + + // TODO: remove this impl block after addressing + // https://github.com/project-serum/anchor/issues/71. + impl CounterAuth { + pub fn new(_ctx: Context) -> Result { + Ok(Self {}) + } + } + + impl<'info> Auth<'info, Empty> for CounterAuth { + fn is_authorized(_ctx: Context, current: u64, new: u64) -> ProgramResult { + if current % 2 == 0 { + if new % 2 == 0 { + return Err(ProgramError::Custom(50)); // Arbitrary error code. + } + } else { + if new % 2 == 1 { + return Err(ProgramError::Custom(60)); // Arbitrary error code. + } + } + Ok(()) + } + } +} + +#[derive(Accounts)] +pub struct Empty {} diff --git a/examples/interface/programs/counter/Cargo.toml b/examples/interface/programs/counter/Cargo.toml new file mode 100644 index 000000000..fcb187759 --- /dev/null +++ b/examples/interface/programs/counter/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "counter" +version = "0.1.0" +description = "Created with Anchor" +edition = "2018" + +[lib] +crate-type = ["cdylib", "lib"] +name = "counter" + +[features] +no-entrypoint = [] +no-idl = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = { git = "https://github.com/project-serum/anchor" } diff --git a/examples/interface/programs/counter/Xargo.toml b/examples/interface/programs/counter/Xargo.toml new file mode 100644 index 000000000..1744f098a --- /dev/null +++ b/examples/interface/programs/counter/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/examples/interface/programs/counter/src/lib.rs b/examples/interface/programs/counter/src/lib.rs new file mode 100644 index 000000000..fea7c3f96 --- /dev/null +++ b/examples/interface/programs/counter/src/lib.rs @@ -0,0 +1,73 @@ +//! counter is an example program that depends on an external interface +//! that another program must implement. This allows our program to depend +//! on another program, without knowing anything about it other than the fact +//! that it implements the `Auth` trait. +//! +//! Here, we have a counter, where, in order to set the count, the `Auth` +//! program must first approve the transaction. + +#![feature(proc_macro_hygiene)] + +use anchor_lang::prelude::*; + +#[program] +pub mod counter { + use super::*; + + #[state] + pub struct Counter { + pub count: u64, + pub auth_program: Pubkey, + } + + impl Counter { + pub fn new(_ctx: Context, auth_program: Pubkey) -> Result { + Ok(Self { + count: 0, + auth_program, + }) + } + + #[access_control(SetCount::accounts(&self, &ctx))] + pub fn set_count(&mut self, ctx: Context, 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 {}); + 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> { + // Auxiliary account validation requiring program inputs. As a convention, + // we separate it from the business logic of the instruction handler itself. + pub fn accounts(counter: &Counter, ctx: &Context) -> Result<()> { + if ctx.accounts.auth_program.key != &counter.auth_program { + return Err(ErrorCode::InvalidAuthProgram.into()); + } + Ok(()) + } +} + +#[interface] +pub trait Auth<'info, T: Accounts<'info>> { + fn is_authorized(ctx: Context, current: u64, new: u64) -> ProgramResult; +} + +#[error] +pub enum ErrorCode { + #[msg("Invalid auth program.")] + InvalidAuthProgram, +} diff --git a/examples/interface/tests/interface.js b/examples/interface/tests/interface.js new file mode 100644 index 000000000..7696062cd --- /dev/null +++ b/examples/interface/tests/interface.js @@ -0,0 +1,45 @@ +const anchor = require('@project-serum/anchor'); +const assert = require("assert"); + +describe("interface", () => { + // Configure the client to use the local cluster. + anchor.setProvider(anchor.Provider.env()); + + const counter = anchor.workspace.Counter; + const counterAuth = anchor.workspace.CounterAuth; + it("Is initialized!", async () => { + await counter.state.rpc.new(counterAuth.programId); + + const stateAccount = await counter.state(); + assert.ok(stateAccount.count.eq(new anchor.BN(0))); + assert.ok(stateAccount.authProgram.equals(counterAuth.programId)); + }); + + it("Should fail to go from even to event", async () => { + await assert.rejects( + async () => { + await counter.state.rpc.setCount(new anchor.BN(4), { + accounts: { + authProgram: counterAuth.programId, + }, + }); + }, + (err) => { + if (err.toString().split("custom program error: 0x32").length !== 2) { + return false; + } + return true; + } + ); + }); + + it("Shold succeed to go from even to odd", async () => { + await counter.state.rpc.setCount(new anchor.BN(3), { + accounts: { + authProgram: counterAuth.programId, + }, + }); + const stateAccount = await counter.state(); + assert.ok(stateAccount.count.eq(new anchor.BN(3))); + }); +}); diff --git a/lang/Cargo.toml b/lang/Cargo.toml index 9184664bb..a6eead8ae 100644 --- a/lang/Cargo.toml +++ b/lang/Cargo.toml @@ -17,6 +17,7 @@ anchor-attribute-account = { path = "./attribute/account", version = "0.1.0" } anchor-attribute-error = { path = "./attribute/error", version = "0.1.0" } anchor-attribute-program = { path = "./attribute/program", version = "0.1.0" } anchor-attribute-state = { path = "./attribute/state", version = "0.1.0" } +anchor-attribute-interface = { path = "./attribute/interface", version = "0.1.0" } anchor-derive-accounts = { path = "./derive/accounts", version = "0.1.0" } serum-borsh = "0.8.1-serum.1" solana-program = "=1.5.0" diff --git a/lang/attribute/interface/Cargo.toml b/lang/attribute/interface/Cargo.toml new file mode 100644 index 000000000..07f6cb986 --- /dev/null +++ b/lang/attribute/interface/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "anchor-attribute-interface" +version = "0.1.0" +authors = ["Serum Foundation "] +repository = "https://github.com/project-serum/anchor" +license = "Apache-2.0" +description = "Attribute for defining a program interface trait" +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "=1.0.57", features = ["full"] } +anyhow = "1.0.32" +anchor-syn = { path = "../../syn", version = "0.1.0" } +heck = "0.3.2" diff --git a/lang/attribute/interface/src/lib.rs b/lang/attribute/interface/src/lib.rs new file mode 100644 index 000000000..659d0dcd7 --- /dev/null +++ b/lang/attribute/interface/src/lib.rs @@ -0,0 +1,120 @@ +extern crate proc_macro; + +use anchor_syn::parser; +use heck::SnakeCase; +use quote::quote; +use syn::parse_macro_input; + +#[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 = 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_map(|pat_ty: &syn::PatType| { + let mut ty = parser::tts_to_string(&pat_ty.ty); + ty.retain(|s| !s.is_whitespace()); + if ty.starts_with("Context<") { + None + } else { + Some(pat_ty) + } + }) + .collect(); + let args_no_tys: Vec<&Box> = args + .iter() + .map(|arg| { + &arg.pat + }) + .collect(); + let args_struct = { + if args.len() == 0 { + 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::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::ToAccountMetas + anchor_lang::ToAccountInfos<'info>>( + ctx: anchor_lang::CpiContext<'a, 'b, 'c, 'info, T>, + #(#args),* + ) -> anchor_lang::solana_program::entrypoint::ProgramResult { + #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::solana_program::program_error::ProgramError::InvalidInstructionData)?; + let mut data = #sighash_tts.to_vec(); + data.append(&mut ix_data); + let accounts = ctx.accounts.to_account_metas(None); + anchor_lang::solana_program::instruction::Instruction { + program_id: *ctx.program.key, + accounts, + data, + } + }; + let mut acc_infos = ctx.accounts.to_account_infos(); + acc_infos.push(ctx.program.clone()); + anchor_lang::solana_program::program::invoke_signed( + &ix, + &acc_infos, + ctx.signer_seeds, + ) + } + } + }) + .collect(); + + proc_macro::TokenStream::from(quote! { + #item_trait + + mod #mod_name { + use super::*; + #(#methods)* + } + }) +} diff --git a/lang/attribute/program/src/lib.rs b/lang/attribute/program/src/lib.rs index 45f62b9b2..6deec4bfc 100644 --- a/lang/attribute/program/src/lib.rs +++ b/lang/attribute/program/src/lib.rs @@ -4,8 +4,8 @@ use anchor_syn::codegen::program as program_codegen; use anchor_syn::parser::program as program_parser; use syn::parse_macro_input; -/// The module containing all instruction handlers defining all entries to the -/// Solana program. +/// The `#[program]` attribute defines the module containing all instruction +/// handlers defining all entries into a Solana program. #[proc_macro_attribute] pub fn program( _args: proc_macro::TokenStream, diff --git a/lang/src/context.rs b/lang/src/context.rs index 4b204a3ff..5000250b0 100644 --- a/lang/src/context.rs +++ b/lang/src/context.rs @@ -1,4 +1,4 @@ -use crate::Accounts; +use crate::{Accounts, ToAccountInfos, ToAccountMetas}; use solana_program::account_info::AccountInfo; use solana_program::pubkey::Pubkey; @@ -27,13 +27,19 @@ impl<'a, 'b, 'c, 'info, T: Accounts<'info>> Context<'a, 'b, 'c, 'info, T> { } /// Context speciying non-argument inputs for cross-program-invocations. -pub struct CpiContext<'a, 'b, 'c, 'info, T: Accounts<'info>> { +pub struct CpiContext<'a, 'b, 'c, 'info, T> +where + T: ToAccountMetas + ToAccountInfos<'info>, +{ pub accounts: T, pub program: AccountInfo<'info>, pub signer_seeds: &'a [&'b [&'c [u8]]], } -impl<'a, 'b, 'c, 'info, T: Accounts<'info>> CpiContext<'a, 'b, 'c, 'info, T> { +impl<'a, 'b, 'c, 'info, T> CpiContext<'a, 'b, 'c, 'info, T> +where + T: ToAccountMetas + ToAccountInfos<'info>, +{ pub fn new(program: AccountInfo<'info>, accounts: T) -> Self { Self { accounts, diff --git a/lang/src/lib.rs b/lang/src/lib.rs index d7b6d6309..d04f80de2 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -39,6 +39,7 @@ pub mod idl; mod program_account; mod state; mod sysvar; +mod vec; pub use crate::context::{Context, CpiContext}; pub use crate::cpi_account::CpiAccount; @@ -49,6 +50,7 @@ pub use crate::sysvar::Sysvar; pub use anchor_attribute_access_control::access_control; pub use anchor_attribute_account::account; pub use anchor_attribute_error::error; +pub use anchor_attribute_interface::interface; pub use anchor_attribute_program::program; pub use anchor_attribute_state::state; pub use anchor_derive_accounts::Accounts; @@ -68,8 +70,8 @@ pub trait Accounts<'info>: ToAccountMetas + ToAccountInfos<'info> + Sized { /// program dependent. However, users of these types should never have to /// worry about account substitution attacks. For example, if a program /// expects a `Mint` account from the SPL token program in a particular - /// field, then it should be impossible for this method to return `Ok` if any - /// other account type is given--from the SPL token program or elsewhere. + /// field, then it should be impossible for this method to return `Ok` if + /// any other account type is given--from the SPL token program or elsewhere. /// /// `program_id` is the currently executing program. `accounts` is the /// set of accounts to construct the type from. For every account used, @@ -171,9 +173,9 @@ pub trait InstructionData: AnchorSerialize { /// All programs should include it via `anchor_lang::prelude::*;`. pub mod prelude { pub use super::{ - access_control, account, error, program, state, AccountDeserialize, AccountSerialize, - Accounts, AccountsExit, AccountsInit, AnchorDeserialize, AnchorSerialize, Context, - CpiAccount, CpiContext, Ctor, ProgramAccount, ProgramState, Sysvar, ToAccountInfo, + access_control, account, error, interface, program, state, AccountDeserialize, + AccountSerialize, Accounts, AccountsExit, AccountsInit, AnchorDeserialize, AnchorSerialize, + Context, CpiAccount, CpiContext, Ctor, ProgramAccount, ProgramState, Sysvar, ToAccountInfo, ToAccountInfos, ToAccountMetas, }; diff --git a/lang/src/vec.rs b/lang/src/vec.rs new file mode 100644 index 000000000..4da643f23 --- /dev/null +++ b/lang/src/vec.rs @@ -0,0 +1,19 @@ +use crate::{ToAccountInfos, ToAccountMetas}; +use solana_program::account_info::AccountInfo; +use solana_program::instruction::AccountMeta; + +impl<'info, T: ToAccountInfos<'info>> ToAccountInfos<'info> for Vec { + fn to_account_infos(&self) -> Vec> { + self.iter() + .flat_map(|item| item.to_account_infos()) + .collect() + } +} + +impl ToAccountMetas for Vec { + fn to_account_metas(&self, is_signer: Option) -> Vec { + self.iter() + .flat_map(|item| (*item).to_account_metas(is_signer)) + .collect() + } +} diff --git a/lang/syn/src/codegen/error.rs b/lang/syn/src/codegen/error.rs index 4f73645e8..ebabe1c0e 100644 --- a/lang/syn/src/codegen/error.rs +++ b/lang/syn/src/codegen/error.rs @@ -37,5 +37,12 @@ pub fn generate(error: Error) -> proc_macro2::TokenStream { } } } + + impl std::convert::From<#enum_name> for ProgramError { + fn from(e: #enum_name) -> ProgramError { + let err: Error = e.into(); + err.into() + } + } } } diff --git a/lang/syn/src/codegen/program.rs b/lang/syn/src/codegen/program.rs index ce3f8a52a..2b068ed21 100644 --- a/lang/syn/src/codegen/program.rs +++ b/lang/syn/src/codegen/program.rs @@ -41,7 +41,7 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream { if cfg!(not(feature = "no-idl")) { if sighash == anchor_lang::idl::IDL_IX_TAG.to_le_bytes() { - return __private::__idl(program_id, accounts, &instruction_data[8..]); + return __private::__idl(program_id, accounts, &instruction_data); } } @@ -66,6 +66,7 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream { } pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream { + // Dispatch the state constructor. let ctor_state_dispatch_arm = match &program.state { None => quote! { /* no-op */ }, Some(state) => { @@ -85,6 +86,8 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream { } } }; + + // Dispatch the state impl instructions. let state_dispatch_arms: Vec = match &program.state { None => vec![], Some(s) => s @@ -112,6 +115,63 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream { }) .collect(), }; + + // Dispatch all trait interface implementations. + let trait_dispatch_arms: Vec = match &program.state { + None => vec![], + Some(s) => s + .interfaces + .iter() + .flat_map(|iface: &crate::StateInterface| { + iface + .methods + .iter() + .map(|m: &crate::StateRpc| { + let rpc_arg_names: Vec<&syn::Ident> = + m.args.iter().map(|arg| &arg.name).collect(); + let name = &m.raw_method.sig.ident.to_string(); + let rpc_name: proc_macro2::TokenStream = format!("__{}_{}", iface.trait_name, name).parse().unwrap(); + let raw_args: Vec<&syn::PatType> = m + .args + .iter() + .map(|arg: &crate::RpcArg| &arg.raw_arg) + .collect(); + let sighash_arr = sighash(&iface.trait_name, &m.ident.to_string()); + let sighash_tts: proc_macro2::TokenStream = + format!("{:?}", sighash_arr).parse().unwrap(); + let args_struct = { + if m.args.len() == 0 { + quote! { + #[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] + struct Args; + } + } else { + quote! { + #[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] + struct Args { + #(#raw_args),* + } + } + } + }; + quote! { + #sighash_tts => { + #args_struct + let ix = Args::deserialize(&mut instruction_data) + .map_err(|_| ProgramError::Custom(1))?; // todo: error code + let Args { + #(#rpc_arg_names),* + } = ix; + __private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*) + } + } + }) + .collect::>() + }) + .collect(), + }; + + // Dispatch all global instructions. let dispatch_arms: Vec = program .rpcs .iter() @@ -139,6 +199,7 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream { match sighash { #ctor_state_dispatch_arm #(#state_dispatch_arms)* + #(#trait_dispatch_arms)* #(#dispatch_arms)* _ => { msg!("Fallback functions are not supported. If you have a use case, please file an issue."); @@ -166,7 +227,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr let mut data: &[u8] = idl_ix_data; let ix = anchor_lang::idl::IdlInstruction::deserialize(&mut data) - .map_err(|_| ProgramError::Custom(1))?; // todo + .map_err(|_| ProgramError::Custom(2))?; // todo match ix { anchor_lang::idl::IdlInstruction::Create { data_len } => { @@ -419,6 +480,101 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr }) .collect(), }; + let non_inlined_state_trait_handlers: Vec = match &program.state { + None => Vec::new(), + Some(state) => state + .interfaces + .iter() + .flat_map(|iface: &crate::StateInterface| { + iface + .methods + .iter() + .map(|rpc| { + let rpc_params: Vec<_> = rpc.args.iter().map(|arg| &arg.raw_arg).collect(); + let rpc_arg_names: Vec<&syn::Ident> = + rpc.args.iter().map(|arg| &arg.name).collect(); + let private_rpc_name: proc_macro2::TokenStream = { + let n = format!("__{}_{}", iface.trait_name, &rpc.raw_method.sig.ident.to_string()); + n.parse().unwrap() + }; + let rpc_name = &rpc.raw_method.sig.ident; + let state_ty: proc_macro2::TokenStream = state.name.parse().unwrap(); + let anchor_ident = &rpc.anchor_ident; + + if rpc.has_receiver { + quote! { + #[inline(never)] + pub fn #private_rpc_name( + program_id: &Pubkey, + accounts: &[AccountInfo], + #(#rpc_params),* + ) -> ProgramResult { + + let mut remaining_accounts: &[AccountInfo] = accounts; + if remaining_accounts.len() == 0 { + return Err(ProgramError::Custom(1)); // todo + } + + // Deserialize the program state account. + let state_account = &remaining_accounts[0]; + let mut state: #state_ty = { + let data = state_account.try_borrow_data()?; + let mut sliced: &[u8] = &data; + anchor_lang::AccountDeserialize::try_deserialize(&mut sliced)? + }; + + remaining_accounts = &remaining_accounts[1..]; + + // Deserialize the program's execution context. + let mut accounts = #anchor_ident::try_accounts( + program_id, + &mut remaining_accounts, + )?; + let ctx = Context::new(program_id, &mut accounts, remaining_accounts); + + // Execute user defined function. + state.#rpc_name( + ctx, + #(#rpc_arg_names),* + )?; + + // Serialize the state and save it to storage. + accounts.exit(program_id)?; + let mut data = state_account.try_borrow_mut_data()?; + let dst: &mut [u8] = &mut data; + let mut cursor = std::io::Cursor::new(dst); + state.try_serialize(&mut cursor)?; + + Ok(()) + } + } + } else { + let state_name: proc_macro2::TokenStream = state.name.parse().unwrap(); + quote! { + #[inline(never)] + pub fn #private_rpc_name( + program_id: &Pubkey, + accounts: &[AccountInfo], + #(#rpc_params),* + ) -> ProgramResult { + let mut remaining_accounts: &[AccountInfo] = accounts; + let mut accounts = #anchor_ident::try_accounts( + program_id, + &mut remaining_accounts, + )?; + #state_name::#rpc_name( + Context::new(program_id, &mut accounts, remaining_accounts), + #(#rpc_arg_names),* + )?; + accounts.exit(program_id) + } + } + } + }) + .collect::>() + }) + .collect(), + }; let non_inlined_handlers: Vec = program .rpcs .iter() @@ -451,6 +607,7 @@ pub fn generate_non_inlined_handlers(program: &Program) -> proc_macro2::TokenStr #non_inlined_idl #non_inlined_ctor #(#non_inlined_state_handlers)* + #(#non_inlined_state_trait_handlers)* #(#non_inlined_handlers)* } } @@ -479,7 +636,14 @@ pub fn generate_ctor_typed_variant_with_semi(program: &Program) -> proc_macro2:: match &program.state { None => quote! {}, Some(state) => { - let ctor_args = generate_ctor_typed_args(state); + let ctor_args: Vec = generate_ctor_typed_args(state) + .iter() + .map(|arg| { + format!("pub {}", parser::tts_to_string(&arg)) + .parse() + .unwrap() + }) + .collect(); if ctor_args.len() == 0 { quote! { #[derive(AnchorSerialize, AnchorDeserialize)] @@ -490,7 +654,7 @@ pub fn generate_ctor_typed_variant_with_semi(program: &Program) -> proc_macro2:: #[derive(AnchorSerialize, AnchorDeserialize)] pub struct __Ctor { #(#ctor_args),* - }; + } } } } @@ -821,7 +985,7 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream { // Rust doesn't have method overloading so no need to use the arguments. // However, we do namespace methods in the preeimage so that we can use // different traits with the same method name. -fn sighash(namespace: &str, name: &str) -> [u8; 8] { +pub fn sighash(namespace: &str, name: &str) -> [u8; 8] { let preimage = format!("{}::{}", namespace, name); let mut sighash = [0u8; 8]; diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index 6c76706e3..33f0d989e 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -32,6 +32,7 @@ pub struct State { pub strct: syn::ItemStruct, pub impl_block: syn::ItemImpl, pub methods: Vec, + pub interfaces: Vec, pub ctor: syn::ImplItemMethod, pub ctor_anchor: syn::Ident, // TODO: consolidate this with ctor above. } @@ -42,6 +43,14 @@ pub struct StateRpc { pub ident: syn::Ident, pub args: Vec, pub anchor_ident: syn::Ident, + // True if there exists a &self on the method. + pub has_receiver: bool, +} + +#[derive(Debug)] +pub struct StateInterface { + pub trait_name: String, + pub methods: Vec, } #[derive(Debug)] diff --git a/lang/syn/src/parser/program.rs b/lang/syn/src/parser/program.rs index 4cf3d5365..4c92fca40 100644 --- a/lang/syn/src/parser/program.rs +++ b/lang/syn/src/parser/program.rs @@ -1,5 +1,5 @@ use crate::parser; -use crate::{Program, Rpc, RpcArg, State, StateRpc}; +use crate::{Program, Rpc, RpcArg, State, StateInterface, StateRpc}; pub fn parse(program_mod: syn::ItemMod) -> Program { let mod_ident = &program_mod.ident; @@ -28,12 +28,15 @@ pub fn parse(program_mod: syn::ItemMod) -> Program { .next(); let impl_block: Option<&syn::ItemImpl> = strct.map(|strct| { - let item_impl = mod_content + let item_impls = mod_content .iter() .filter_map(|item| match item { syn::Item::Impl(item_impl) => { let impl_ty_str = parser::tts_to_string(&item_impl.self_ty); let strct_name = strct.ident.to_string(); + if item_impl.trait_.is_some() { + return None; + } if strct_name != impl_ty_str { return None; } @@ -41,9 +44,39 @@ pub fn parse(program_mod: syn::ItemMod) -> Program { } _ => None, }) - .next() - .expect("Must provide an implementation"); - item_impl + .collect::>(); + item_impls[0] + }); + + // All program interface implementations. + let trait_impls: Option> = strct.map(|_strct| { + mod_content + .iter() + .filter_map(|item| match item { + syn::Item::Impl(item_impl) => { + let trait_name = match &item_impl.trait_ { + None => return None, + Some((_, path, _)) => path + .segments + .iter() + .next() + .expect("Must have one segmeent in a path") + .ident + .clone() + .to_string(), + }; + if item_impl.trait_.is_none() { + return None; + } + let methods = parse_state_trait_methods(item_impl); + Some(StateInterface { + trait_name, + methods, + }) + } + _ => None, + }) + .collect::>() }); strct.map(|strct| { @@ -112,6 +145,7 @@ pub fn parse(program_mod: syn::ItemMod) -> Program { ident: m.sig.ident.clone(), args, anchor_ident, + has_receiver: true, }) } }, @@ -122,6 +156,7 @@ pub fn parse(program_mod: syn::ItemMod) -> Program { State { name: strct.ident.to_string(), strct: strct.clone(), + interfaces: trait_impls.expect("Some if state exists"), impl_block, ctor, ctor_anchor, @@ -206,3 +241,52 @@ fn extract_ident(path_ty: &syn::PatType) -> &proc_macro2::Ident { }; &path.segments[0].ident } + +fn parse_state_trait_methods(item_impl: &syn::ItemImpl) -> Vec { + item_impl + .items + .iter() + .filter_map(|item: &syn::ImplItem| match item { + syn::ImplItem::Method(m) => match m.sig.inputs.first() { + None => None, + Some(_arg) => { + let mut has_receiver = false; + let mut args = m + .sig + .inputs + .iter() + .filter_map(|arg| match arg { + syn::FnArg::Receiver(_) => { + has_receiver = true; + None + } + syn::FnArg::Typed(arg) => Some(arg), + }) + .map(|raw_arg| { + let ident = match &*raw_arg.pat { + syn::Pat::Ident(ident) => &ident.ident, + _ => panic!("invalid syntax"), + }; + RpcArg { + name: ident.clone(), + raw_arg: raw_arg.clone(), + } + }) + .collect::>(); + // Remove the Anchor accounts argument + let anchor = args.remove(0); + let anchor_ident = extract_ident(&anchor.raw_arg).clone(); + + Some(StateRpc { + raw_method: m.clone(), + ident: m.sig.ident.clone(), + args, + anchor_ident, + has_receiver, + }) + } + }, + _ => None, + }) + .collect() +} diff --git a/ts/package.json b/ts/package.json index 1c377a591..c140adb5c 100644 --- a/ts/package.json +++ b/ts/package.json @@ -1,6 +1,6 @@ { "name": "@project-serum/anchor", - "version": "0.1.0", + "version": "0.2.0-beta.1", "description": "Anchor client", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -27,11 +27,13 @@ "@solana/web3.js": "^0.90.4", "@types/bn.js": "^4.11.6", "@types/bs58": "^4.0.1", + "@types/crypto-hash": "^1.1.2", "@types/pako": "^1.0.1", "bn.js": "^5.1.2", "bs58": "^4.0.1", "buffer-layout": "^1.2.0", "camelcase": "^5.3.1", + "crypto-hash": "^1.3.0", "eventemitter3": "^4.0.7", "find": "^0.3.0", "js-sha256": "^0.9.0", diff --git a/ts/src/coder.ts b/ts/src/coder.ts index b562d30d2..385b91921 100644 --- a/ts/src/coder.ts +++ b/ts/src/coder.ts @@ -356,7 +356,7 @@ export async function stateDiscriminator(name: string): Promise { // Returns the size of the type in bytes. For variable length types, just return // 1. Users should override this value in such cases. -export function typeSize(idl: Idl, ty: IdlType): number { +function typeSize(idl: Idl, ty: IdlType): number { switch (ty) { case "bool": return 1; @@ -386,7 +386,7 @@ export function typeSize(idl: Idl, ty: IdlType): number { // @ts-ignore if (ty.option !== undefined) { // @ts-ignore - return 1 + typeSize(ty.option); + return 1 + typeSize(idl, ty.option); } // @ts-ignore if (ty.defined !== undefined) { diff --git a/ts/yarn.lock b/ts/yarn.lock index 5025bbb9f..ea7ce29d6 100644 --- a/ts/yarn.lock +++ b/ts/yarn.lock @@ -753,6 +753,13 @@ dependencies: "@types/node" "*" +"@types/crypto-hash@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@types/crypto-hash/-/crypto-hash-1.1.2.tgz#5a993deb0e6ba7c42f86eaa65d9bf563378f4569" + integrity sha512-sOmi+4Go2XKodLV4+lfP+5QMQ+6ZYqRJhK8D/n6xsxIUvlerEulmU9S4Lo02pXCH3qPBeJXEy+g8ZERktDJLSg== + dependencies: + crypto-hash "*" + "@types/express-serve-static-core@^4.17.9": version "4.17.18" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.18.tgz#8371e260f40e0e1ca0c116a9afcd9426fa094c40" @@ -1687,7 +1694,7 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" -crypto-hash@^1.2.2: +crypto-hash@*, crypto-hash@^1.2.2, crypto-hash@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247" integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==