Armani Ferrante 4 роки тому
батько
коміт
e42668279a

+ 10 - 0
Cargo.lock

@@ -120,6 +120,16 @@ dependencies = [
  "toml",
 ]
 
+[[package]]
+name = "anchor-client"
+version = "0.1.0"
+dependencies = [
+ "anchor-lang",
+ "solana-client",
+ "solana-sdk",
+ "thiserror",
+]
+
 [[package]]
 name = "anchor-derive-accounts"
 version = "0.0.0-alpha.0"

+ 1 - 0
Cargo.toml

@@ -25,6 +25,7 @@ thiserror = "1.0.20"
 [workspace]
 members = [
     "cli",
+    "client",
     "syn",
     "attribute/*",
     "derive/*",

+ 3 - 1
cli/src/main.rs

@@ -500,7 +500,9 @@ fn deploy(url: Option<String>, keypair: Option<String>) -> Result<()> {
     }
 
     // Run migration script.
-    migrate(&url)?;
+    if Path::new("migrations/deploy.js").exists() {
+        migrate(&url)?;
+    }
 
     Ok(())
 }

+ 11 - 0
client/Cargo.toml

@@ -0,0 +1,11 @@
+[package]
+name = "anchor-client"
+version = "0.1.0"
+authors = ["Armani Ferrante <armaniferrante@gmail.com>"]
+edition = "2018"
+
+[dependencies]
+anchor-lang = { path = "../" }
+solana-client = "1.5.0"
+solana-sdk = "1.5.0"
+thiserror = "1.0.20"

+ 15 - 0
client/example/Cargo.toml

@@ -0,0 +1,15 @@
+[package]
+name = "example"
+version = "0.1.0"
+authors = ["Armani Ferrante <armaniferrante@gmail.com>"]
+edition = "2018"
+
+[workspace]
+
+[dependencies]
+anchor-client = { path = "../" }
+basic-2 = { path = "../../examples/tutorial/basic-2/programs/basic-2", features = ["no-entrypoint"] }
+composite = { path = "../../examples/composite/programs/composite", features = ["no-entrypoint"] }
+shellexpand = "2.1.0"
+anyhow = "1.0.32"
+rand = "0.7.3"

+ 158 - 0
client/example/src/main.rs

@@ -0,0 +1,158 @@
+use anchor_client::solana_sdk::commitment_config::CommitmentConfig;
+use anchor_client::solana_sdk::signature::read_keypair_file;
+use anchor_client::solana_sdk::signature::{Keypair, Signer};
+use anchor_client::solana_sdk::system_instruction;
+use anchor_client::solana_sdk::sysvar;
+use anchor_client::Client;
+use anyhow::Result;
+// The `accounts` and `instructions` modules are generated by the framework.
+use basic_2::accounts::CreateAuthor;
+use basic_2::instruction::Basic2Instruction;
+use basic_2::Author;
+// The `accounts` and `instructions` modules are generated by the framework.
+use composite::accounts::{Bar, CompositeUpdate, Foo, Initialize};
+use composite::instruction::CompositeInstruction;
+use composite::{DummyA, DummyB};
+
+use rand::rngs::OsRng;
+
+fn main() -> Result<()> {
+    // Wallet and cluster params.
+    let payer = read_keypair_file(&shellexpand::tilde("~/.config/solana/id.json"))
+        .expect("Example requires a keypair file");
+    let url = "http://localhost:8899";
+    let opts = CommitmentConfig::recent();
+
+    // Client.
+    let client = Client::new_with_options(url, payer, opts);
+
+    // Run tests.
+    composite(&client)?;
+    basic_2(&client)?;
+
+    // Success.
+    Ok(())
+}
+
+// Runs a client for examples/tutorial/composite.
+//
+// Make sure to run a localnet with the program deploy to run this example.
+fn composite(client: &Client) -> Result<()> {
+    // Deployed program to execute.
+    let pid = "75TykCe6b1oBa8JWVvfkXsFbZydgqi3QfRjgBEJJwy2g"
+        .parse()
+        .unwrap();
+
+    // Program client.
+    let program = client.program(pid);
+
+    // `Initialize` parameters.
+    let dummy_a = Keypair::generate(&mut OsRng);
+    let dummy_b = Keypair::generate(&mut OsRng);
+
+    // Build and send a transaction.
+    program
+        .request()
+        .instruction(system_instruction::create_account(
+            &program.payer(),
+            &dummy_a.pubkey(),
+            program.rpc().get_minimum_balance_for_rent_exemption(500)?,
+            500,
+            &program.id(),
+        ))
+        .instruction(system_instruction::create_account(
+            &program.payer(),
+            &dummy_b.pubkey(),
+            program.rpc().get_minimum_balance_for_rent_exemption(500)?,
+            500,
+            &program.id(),
+        ))
+        .signer(&dummy_a)
+        .signer(&dummy_b)
+        .accounts(Initialize {
+            dummy_a: dummy_a.pubkey(),
+            dummy_b: dummy_b.pubkey(),
+            rent: sysvar::rent::ID,
+        })
+        .args(CompositeInstruction::Initialize)
+        .send()?;
+
+    // Assert the transaction worked.
+    let dummy_a_account: DummyA = program.account(dummy_a.pubkey())?;
+    let dummy_b_account: DummyB = program.account(dummy_b.pubkey())?;
+    assert_eq!(dummy_a_account.data, 0);
+    assert_eq!(dummy_b_account.data, 0);
+
+    // Build and send another transaction, using composite account parameters.
+    program
+        .request()
+        .accounts(CompositeUpdate {
+            foo: Foo {
+                dummy_a: dummy_a.pubkey(),
+            },
+            bar: Bar {
+                dummy_b: dummy_b.pubkey(),
+            },
+        })
+        .args(CompositeInstruction::CompositeUpdate {
+            dummy_a: 1234,
+            dummy_b: 4321,
+        })
+        .send()?;
+
+    // Assert the transaction worked.
+    let dummy_a_account: DummyA = program.account(dummy_a.pubkey())?;
+    let dummy_b_account: DummyB = program.account(dummy_b.pubkey())?;
+    assert_eq!(dummy_a_account.data, 1234);
+    assert_eq!(dummy_b_account.data, 4321);
+
+    println!("Success!");
+
+    Ok(())
+}
+
+// Runs a client for examples/tutorial/basic-2.
+//
+// Make sure to run a localnet with the program deploy to run this example.
+fn basic_2(client: &Client) -> Result<()> {
+    // Deployed program to execute.
+    let program_id = "FU3yvTEGTFUdMa6qAjVyKfNcDU6hb4yXbPhz8f5iFyvE"
+        .parse()
+        .unwrap();
+
+    let program = client.program(program_id);
+
+    // `CreateAuthor` parameters.
+    let author = Keypair::generate(&mut OsRng);
+    let authority = program.payer();
+
+    // Build and send a transaction.
+    program
+        .request()
+        .instruction(system_instruction::create_account(
+            &authority,
+            &author.pubkey(),
+            program.rpc().get_minimum_balance_for_rent_exemption(500)?,
+            500,
+            &program_id,
+        ))
+        .signer(&author)
+        .accounts(CreateAuthor {
+            author: author.pubkey(),
+            rent: sysvar::rent::ID,
+        })
+        .args(Basic2Instruction::CreateAuthor {
+            authority,
+            name: "My Book Name".to_string(),
+        })
+        .send()?;
+
+    let author_account: Author = program.account(author.pubkey())?;
+
+    assert_eq!(author_account.authority, authority);
+    assert_eq!(author_account.name, "My Book Name".to_string());
+
+    println!("Success!");
+
+    Ok(())
+}

+ 228 - 0
client/src/lib.rs

@@ -0,0 +1,228 @@
+//! `anchor_client` provides an RPC client to send transactions and fetch
+//! deserialized accounts from Solana programs written in `anchor_lang`.
+
+use anchor_lang::solana_program::instruction::{AccountMeta, Instruction};
+use anchor_lang::solana_program::program_error::ProgramError;
+use anchor_lang::solana_program::pubkey::Pubkey;
+use anchor_lang::{AccountDeserialize, AnchorSerialize, ToAccountMetas};
+use solana_client::client_error::ClientError as SolanaClientError;
+use solana_client::rpc_client::RpcClient;
+use solana_sdk::commitment_config::CommitmentConfig;
+use solana_sdk::signature::{Keypair, Signature, Signer};
+use solana_sdk::transaction::Transaction;
+use std::convert::Into;
+use thiserror::Error;
+
+pub use anchor_lang;
+pub use solana_client;
+pub use solana_sdk;
+
+/// Client defines the base configuration for building RPC clients to
+/// communitcate with Anchor programs running on a Solana cluster. It's
+/// primary use is to build a `Program` client via the `program` method.
+pub struct Client {
+    cfg: Config,
+}
+
+impl Client {
+    pub fn new(cluster: &str, payer: Keypair) -> Self {
+        Self {
+            cfg: Config {
+                cluster: cluster.to_string(),
+                payer,
+                options: None,
+            },
+        }
+    }
+
+    pub fn new_with_options(cluster: &str, payer: Keypair, options: CommitmentConfig) -> Self {
+        Self {
+            cfg: Config {
+                cluster: cluster.to_string(),
+                payer,
+                options: Some(options),
+            },
+        }
+    }
+
+    pub fn program(&self, program_id: Pubkey) -> Program {
+        Program {
+            program_id,
+            cfg: Config {
+                cluster: self.cfg.cluster.clone(),
+                options: self.cfg.options.clone(),
+                payer: Keypair::from_bytes(&self.cfg.payer.to_bytes()).unwrap(),
+            },
+        }
+    }
+}
+
+// Internal configuration for a client.
+struct Config {
+    cluster: String,
+    payer: Keypair,
+    options: Option<CommitmentConfig>,
+}
+
+/// Program is the primary client handle to be used to build and send requests.
+pub struct Program {
+    program_id: Pubkey,
+    cfg: Config,
+}
+
+impl Program {
+    pub fn payer(&self) -> Pubkey {
+        self.cfg.payer.pubkey()
+    }
+
+    /// Returns a request builder.
+    pub fn request(&self) -> RequestBuilder {
+        RequestBuilder::new(
+            self.program_id,
+            &self.cfg.cluster,
+            Keypair::from_bytes(&self.cfg.payer.to_bytes()).unwrap(),
+            self.cfg.options.clone(),
+        )
+    }
+
+    /// Returns the account at the given address.
+    pub fn account<T: AccountDeserialize>(&self, address: Pubkey) -> Result<T, ClientError> {
+        let rpc_client = RpcClient::new_with_commitment(
+            self.cfg.cluster.clone(),
+            self.cfg.options.unwrap_or(Default::default()),
+        );
+        let account = rpc_client
+            .get_account_with_commitment(&address, CommitmentConfig::recent())?
+            .value
+            .ok_or(ClientError::AccountNotFound)?;
+        let mut data: &[u8] = &account.data;
+        T::try_deserialize(&mut data).map_err(Into::into)
+    }
+
+    pub fn rpc(&self) -> RpcClient {
+        RpcClient::new_with_commitment(
+            self.cfg.cluster.clone(),
+            self.cfg.options.unwrap_or(Default::default()),
+        )
+    }
+
+    pub fn id(&self) -> Pubkey {
+        self.program_id
+    }
+}
+
+#[derive(Debug, Error)]
+pub enum ClientError {
+    #[error("Account not found")]
+    AccountNotFound,
+    #[error("{0}")]
+    ProgramError(#[from] ProgramError),
+    #[error("{0}")]
+    SolanaClientError(#[from] SolanaClientError),
+}
+
+/// `RequestBuilder` provides a builder interface to create and send
+/// transactions to a cluster.
+pub struct RequestBuilder<'a> {
+    cluster: String,
+    program_id: Pubkey,
+    accounts: Vec<AccountMeta>,
+    options: CommitmentConfig,
+    instructions: Vec<Instruction>,
+    payer: Keypair,
+    // Serialized instruction data for the target RPC.
+    instruction_data: Option<Vec<u8>>,
+    signers: Vec<&'a dyn Signer>,
+}
+
+impl<'a> RequestBuilder<'a> {
+    pub fn new(
+        program_id: Pubkey,
+        cluster: &str,
+        payer: Keypair,
+        options: Option<CommitmentConfig>,
+    ) -> Self {
+        Self {
+            program_id,
+            payer,
+            cluster: cluster.to_string(),
+            accounts: Vec::new(),
+            options: options.unwrap_or(Default::default()),
+            instructions: Vec::new(),
+            instruction_data: None,
+            signers: Vec::new(),
+        }
+    }
+
+    pub fn payer(mut self, payer: Keypair) -> Self {
+        self.payer = payer;
+        self
+    }
+
+    pub fn cluster(mut self, url: &str) -> Self {
+        self.cluster = url.to_string();
+        self
+    }
+
+    pub fn instruction(mut self, ix: Instruction) -> Self {
+        self.instructions.push(ix);
+        self
+    }
+
+    pub fn program(mut self, program_id: Pubkey) -> Self {
+        self.program_id = program_id;
+        self
+    }
+
+    pub fn accounts(mut self, accounts: impl ToAccountMetas) -> Self {
+        let mut metas = accounts.to_account_metas(None);
+        self.accounts.append(&mut metas);
+        self
+    }
+
+    pub fn options(mut self, options: CommitmentConfig) -> Self {
+        self.options = options;
+        self
+    }
+
+    pub fn args(mut self, args: impl AnchorSerialize) -> Self {
+        let data = args.try_to_vec().expect("Should always serialize");
+        self.instruction_data = Some(data);
+        self
+    }
+
+    pub fn signer(mut self, signer: &'a dyn Signer) -> Self {
+        self.signers.push(signer);
+        self
+    }
+
+    pub fn send(self) -> Result<Signature, ClientError> {
+        let mut instructions = self.instructions;
+        if let Some(ix_data) = self.instruction_data {
+            instructions.push(Instruction {
+                program_id: self.program_id,
+                data: ix_data,
+                accounts: self.accounts,
+            });
+        }
+
+        let mut signers = self.signers;
+        signers.push(&self.payer);
+
+        let rpc_client = RpcClient::new_with_commitment(self.cluster, self.options);
+
+        let tx = {
+            let (recent_hash, _fee_calc) = rpc_client.get_recent_blockhash()?;
+            Transaction::new_signed_with_payer(
+                &instructions,
+                Some(&self.payer.pubkey()),
+                &signers,
+                recent_hash,
+            )
+        };
+
+        rpc_client
+            .send_and_confirm_transaction(&tx)
+            .map_err(Into::into)
+    }
+}

+ 109 - 0
syn/src/codegen/accounts.rs

@@ -3,6 +3,7 @@ use crate::{
     ConstraintLiteral, ConstraintOwner, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner,
     Field, Ty,
 };
+use heck::SnakeCase;
 use quote::quote;
 
 pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
@@ -138,7 +139,115 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
         }
     };
 
+    let account_mod_name: proc_macro2::TokenStream = format!(
+        "__client_accounts_{}",
+        accs.ident.to_string().to_snake_case()
+    )
+    .parse()
+    .unwrap();
+
+    let account_struct_fields: Vec<proc_macro2::TokenStream> = accs
+        .fields
+        .iter()
+        .map(|f: &AccountField| match f {
+            AccountField::AccountsStruct(s) => {
+                let name = &s.ident;
+                let symbol: proc_macro2::TokenStream = format!(
+                    "__client_accounts_{0}::{1}",
+                    s.symbol.to_snake_case(),
+                    s.symbol,
+                )
+                .parse()
+                .unwrap();
+                quote! {
+                    pub #name: #symbol
+                }
+            }
+            AccountField::Field(f) => {
+                let name = &f.ident;
+                quote! {
+                    pub #name: anchor_lang::solana_program::pubkey::Pubkey
+                }
+            }
+        })
+        .collect();
+
+    let account_struct_metas: Vec<proc_macro2::TokenStream> = accs
+        .fields
+        .iter()
+        .map(|f: &AccountField| match f {
+            AccountField::AccountsStruct(s) => {
+                let name = &s.ident;
+                quote! {
+                    account_metas.extend(self.#name.to_account_metas(None));
+                }
+            }
+            AccountField::Field(f) => {
+                let is_signer = match f.is_signer {
+                    false => quote! {false},
+                    true => quote! {true},
+                };
+                let meta = match f.is_mut {
+                    false => quote! { anchor_lang::solana_program::instruction::AccountMeta::new_readonly },
+                    true => quote! { anchor_lang::solana_program::instruction::AccountMeta::new },
+                };
+                let name = &f.ident;
+                quote! {
+                    account_metas.push(#meta(self.#name, #is_signer));
+                }
+            }
+        })
+        .collect();
+
+    // Re-export all composite account structs (i.e. other structs deriving
+    // accounts embedded into this struct. Required because, these embedded
+    // structs are *not* visible from the #[program] macro, which is responsible
+    // for generating the `accounts` mod, which aggregates all the the generated
+    // accounts used for structs.
+    let re_exports: Vec<proc_macro2::TokenStream> = accs
+        .fields
+        .iter()
+        .filter_map(|f: &AccountField| match f {
+            AccountField::AccountsStruct(s) => Some(s),
+            AccountField::Field(_) => None,
+        })
+        .map(|f: &CompositeField| {
+            let symbol: proc_macro2::TokenStream = format!(
+                "__client_accounts_{0}::{1}",
+                f.symbol.to_snake_case(),
+                f.symbol,
+            )
+            .parse()
+            .unwrap();
+            quote! {
+                pub use #symbol;
+            }
+        })
+        .collect();
+
     quote! {
+
+        mod #account_mod_name {
+            use super::*;
+            use anchor_lang::prelude::borsh;
+            #(#re_exports)*
+
+            #[derive(anchor_lang::AnchorSerialize)]
+            pub struct #name {
+                #(#account_struct_fields),*
+            }
+
+            impl anchor_lang::ToAccountMetas for #name {
+                fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<anchor_lang::solana_program::instruction::AccountMeta> {
+                    let mut account_metas = vec![];
+
+                    #(#account_struct_metas)*
+
+                    account_metas
+                }
+            }
+        }
+
         impl#combined_generics anchor_lang::Accounts#trait_generics for #name#strct_generics {
             #[inline(never)]
             fn try_accounts(program_id: &anchor_lang::solana_program::pubkey::Pubkey, accounts: &mut &[anchor_lang::solana_program::account_info::AccountInfo<'info>]) -> std::result::Result<Self, anchor_lang::solana_program::program_error::ProgramError> {

+ 69 - 11
syn/src/codegen/program.rs

@@ -1,6 +1,6 @@
 use crate::parser;
 use crate::{Program, RpcArg, State};
-use heck::CamelCase;
+use heck::{CamelCase, SnakeCase};
 use quote::quote;
 
 pub fn generate(program: Program) -> proc_macro2::TokenStream {
@@ -11,12 +11,13 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
     let methods = generate_methods(&program);
     let instruction = generate_instruction(&program);
     let cpi = generate_cpi(&program);
+    let accounts = generate_accounts(&program);
 
     quote! {
-        // Import everything in the mod, in case the user wants to put types
-        // in there.
+        // TODO: remove once we allow segmented paths in `Accounts` structs.
         use #mod_name::*;
 
+
         #[cfg(not(feature = "no-entrypoint"))]
         anchor_lang::solana_program::entrypoint!(entry);
         #[cfg(not(feature = "no-entrypoint"))]
@@ -29,10 +30,10 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
                 }
             }
             let mut data: &[u8] = instruction_data;
-            let ix = __private::instruction::#instruction_name::deserialize(&mut data)
+            let ix = instruction::#instruction_name::deserialize(&mut data)
                 .map_err(|_| ProgramError::Custom(1))?; // todo: error code
 
-                #dispatch
+            #dispatch
         }
 
         // Create a private module to not clutter the program's namespace.
@@ -40,10 +41,12 @@ pub fn generate(program: Program) -> proc_macro2::TokenStream {
             use super::*;
 
             #handlers_non_inlined
-
-            #instruction
         }
 
+        #accounts
+
+        #instruction
+
         #methods
 
         #cpi
@@ -57,7 +60,7 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
             let variant_arm = generate_ctor_variant(program, state);
             let ctor_args = generate_ctor_args(state);
             quote! {
-                __private::instruction::#variant_arm => __private::__ctor(program_id, accounts, #(#ctor_args),*),
+                instruction::#variant_arm => __private::__ctor(program_id, accounts, #(#ctor_args),*),
             }
         }
     };
@@ -80,7 +83,7 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
                     format!("__{}", name).parse().unwrap()
                 };
                 quote! {
-                    __private::instruction::#variant_arm => {
+                    instruction::#variant_arm => {
                         __private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*)
                     }
                 }
@@ -100,7 +103,7 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream {
             );
             let rpc_name = &rpc.raw_method.sig.ident;
             quote! {
-                __private::instruction::#variant_arm => {
+                instruction::#variant_arm => {
                     __private::#rpc_name(program_id, accounts, #(#rpc_arg_names),*)
                 }
             }
@@ -594,6 +597,10 @@ pub fn generate_instruction(program: &Program) -> proc_macro2::TokenStream {
         .collect();
 
     quote! {
+        /// `instruction` is a macro generated module containing the program's
+        /// instruction enum, where each variant is created from each method
+        /// handler in the `#[program]` mod. These should be used directly, when
+        /// specifying instructions on a client.
         pub mod instruction {
             use super::*;
             #[derive(AnchorSerialize, AnchorDeserialize)]
@@ -613,6 +620,57 @@ fn instruction_enum_name(program: &Program) -> proc_macro2::Ident {
     )
 }
 
+fn generate_accounts(program: &Program) -> proc_macro2::TokenStream {
+    let mut accounts = std::collections::HashSet::new();
+
+    // Got through state accounts.
+    if let Some(state) = &program.state {
+        for rpc in &state.methods {
+            let anchor_ident = &rpc.anchor_ident;
+            // TODO: move to fn and share with accounts.rs.
+            let macro_name = format!(
+                "__client_accounts_{}",
+                anchor_ident.to_string().to_snake_case()
+            );
+            accounts.insert(macro_name);
+        }
+    }
+
+    // Go through instruction accounts.
+    for rpc in &program.rpcs {
+        let anchor_ident = &rpc.anchor_ident;
+        // TODO: move to fn and share with accounts.rs.
+        let macro_name = format!(
+            "__client_accounts_{}",
+            anchor_ident.to_string().to_snake_case()
+        );
+        accounts.insert(macro_name);
+    }
+
+    // Build the tokens from all accounts
+    let account_structs: Vec<proc_macro2::TokenStream> = accounts
+        .iter()
+        .map(|macro_name: &String| {
+            let macro_name: proc_macro2::TokenStream = macro_name.parse().unwrap();
+            quote! {
+                pub use crate::#macro_name::*;
+            }
+        })
+        .collect();
+
+    // TODO: calculate the account size and add it as a constant field to
+    //       each struct here. This is convenient for Rust clients.
+
+    quote! {
+        /// `accounts` is a macro generated module, providing a set of structs
+        /// mirroring the structs deriving `Accounts`, where each field is
+        /// a `Pubkey`. This is useful for specifying accounts for a client.
+        pub mod accounts {
+            #(#account_structs)*
+        }
+    }
+}
+
 fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
     let cpi_methods: Vec<proc_macro2::TokenStream> = program
         .rpcs
@@ -634,7 +692,7 @@ fn generate_cpi(program: &Program) -> proc_macro2::TokenStream {
                         #(#args),*
                     ) -> ProgramResult {
                         let ix = {
-                            let ix = __private::instruction::#ix_variant;
+                            let ix = instruction::#ix_variant;
                             let data = AnchorSerialize::try_to_vec(&ix)
                                 .map_err(|_| ProgramError::InvalidInstructionData)?;
                             let accounts = ctx.accounts.to_account_metas(None);