Browse Source

client, lang: Add client support for state instructions (#248)

Armani Ferrante 4 years ago
parent
commit
d796cee091

+ 9 - 0
CHANGELOG.md

@@ -11,6 +11,15 @@ incremented for features.
 
 ## [Unreleased]
 
+## Features
+
+* client: Adds support for state instructions ([#248](https://github.com/project-serum/anchor/pull/248)).
+
+## Breaking
+
+* client: Renames `RequestBuilder::new` to `RequestBuilder::from` ([#248](https://github.com/project-serum/anchor/pull/248)).
+* lang: Renames the generated `instruction::state::Ctor` struct to `instruction::state::New` ([#248](https://github.com/project-serum/anchor/pull/248)).
+
 ## [0.4.5] - 2021-04-29
 
 * spl: Add serum DEX CPI client ([#224](https://github.com/project-serum/anchor/pull/224)).

+ 1 - 0
client/example/Cargo.toml

@@ -9,6 +9,7 @@ edition = "2018"
 [dependencies]
 anchor-client = { path = "../" }
 basic-2 = { path = "../../examples/tutorial/basic-2/programs/basic-2", features = ["no-entrypoint"] }
+basic-4 = { path = "../../examples/tutorial/basic-4/programs/basic-4", features = ["no-entrypoint"] }
 composite = { path = "../../examples/composite/programs/composite", features = ["no-entrypoint"] }
 events = { path = "../../examples/events/programs/events", features = ["no-entrypoint"] }
 shellexpand = "2.1.0"

+ 7 - 1
client/example/run-test.sh

@@ -38,15 +38,21 @@ main() {
     anchor deploy
     local basic_2_pid=$(cat target/idl/basic_2.json | jq -r .metadata.address)
     popd
+    pushd ../../examples/tutorial/basic-4/
+    anchor build
+    anchor deploy
+    local basic_4_pid=$(cat target/idl/basic_4.json | jq -r .metadata.address)
+    popd
     pushd ../../examples/events
     anchor build
     anchor deploy
     local events_pid=$(cat target/idl/events.json | jq -r .metadata.address)
     popd
+
     #
     # Run Test.
     #
-    cargo run -- --composite-pid $composite_pid --basic-2-pid $basic_2_pid --events-pid $events_pid
+    cargo run -- --composite-pid $composite_pid --basic-2-pid $basic_2_pid --basic-4-pid $basic_4_pid --events-pid $events_pid
 }
 
 cleanup() {

+ 40 - 3
client/example/src/main.rs

@@ -13,6 +13,10 @@ use basic_2::Counter;
 use events::instruction as events_instruction;
 use events::MyEvent;
 // The `accounts` and `instructions` modules are generated by the framework.
+use basic_4::accounts as basic_4_accounts;
+use basic_4::basic_4::Counter as CounterState;
+use basic_4::instruction as basic_4_instruction;
+// The `accounts` and `instructions` modules are generated by the framework.
 use clap::Clap;
 use composite::accounts::{Bar, CompositeUpdate, Foo, Initialize};
 use composite::instruction as composite_instruction;
@@ -27,12 +31,15 @@ pub struct Opts {
     #[clap(long)]
     basic_2_pid: Pubkey,
     #[clap(long)]
+    basic_4_pid: Pubkey,
+    #[clap(long)]
     events_pid: Pubkey,
 }
 
 // This example assumes a local validator is running with the programs
 // deployed at the addresses given by the CLI args.
 fn main() -> Result<()> {
+    println!("Starting test...");
     let opts = Opts::parse();
 
     // Wallet and cluster params.
@@ -49,6 +56,7 @@ fn main() -> Result<()> {
     // Run tests.
     composite(&client, opts.composite_pid)?;
     basic_2(&client, opts.basic_2_pid)?;
+    basic_4(&client, opts.basic_4_pid)?;
     events(&client, opts.events_pid)?;
 
     // Success.
@@ -122,7 +130,7 @@ fn composite(client: &Client, pid: Pubkey) -> Result<()> {
     assert_eq!(dummy_a_account.data, 1234);
     assert_eq!(dummy_b_account.data, 4321);
 
-    println!("Success!");
+    println!("Composite success!");
 
     Ok(())
 }
@@ -160,7 +168,7 @@ fn basic_2(client: &Client, pid: Pubkey) -> Result<()> {
     assert_eq!(counter_account.authority, authority);
     assert_eq!(counter_account.count, 0);
 
-    println!("Success!");
+    println!("Basic 2 success!");
 
     Ok(())
 }
@@ -192,7 +200,36 @@ fn events(client: &Client, pid: Pubkey) -> Result<()> {
         drop(handle);
     });
 
-    println!("Success!");
+    println!("Events success!");
+
+    Ok(())
+}
+
+pub fn basic_4(client: &Client, pid: Pubkey) -> Result<()> {
+    let program = client.program(pid);
+    let authority = program.payer();
+
+    // Invoke the state's `new` constructor.
+    program
+        .state_request()
+        .accounts(basic_4_accounts::Auth { authority })
+        .new(basic_4_instruction::state::New)
+        .send()?;
+    let counter_account: CounterState = program.state()?;
+    assert_eq!(counter_account.authority, authority);
+    assert_eq!(counter_account.count, 0);
+
+    // Call a state method.
+    program
+        .state_request()
+        .accounts(basic_4_accounts::Auth { authority })
+        .args(basic_4_instruction::state::Increment)
+        .send()?;
+    let counter_account: CounterState = program.state()?;
+    assert_eq!(counter_account.authority, authority);
+    assert_eq!(counter_account.count, 1);
+
+    println!("Basic 4 success!");
 
     Ok(())
 }

+ 70 - 3
client/src/lib.rs

@@ -4,6 +4,8 @@
 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::solana_program::system_program;
+use anchor_lang::solana_program::sysvar::rent;
 use anchor_lang::{AccountDeserialize, InstructionData, ToAccountMetas};
 use regex::Regex;
 use solana_client::client_error::ClientError as SolanaClientError;
@@ -87,11 +89,23 @@ impl Program {
 
     /// Returns a request builder.
     pub fn request(&self) -> RequestBuilder {
-        RequestBuilder::new(
+        RequestBuilder::from(
             self.program_id,
             &self.cfg.cluster.url(),
             Keypair::from_bytes(&self.cfg.payer.to_bytes()).unwrap(),
             self.cfg.options,
+            RequestNamespace::Global,
+        )
+    }
+
+    /// Returns a request builder for program state.
+    pub fn state_request(&self) -> RequestBuilder {
+        RequestBuilder::from(
+            self.program_id,
+            &self.cfg.cluster.url(),
+            Keypair::from_bytes(&self.cfg.payer.to_bytes()).unwrap(),
+            self.cfg.options,
+            RequestNamespace::State { new: false },
         )
     }
 
@@ -109,6 +123,10 @@ impl Program {
         T::try_deserialize(&mut data).map_err(Into::into)
     }
 
+    pub fn state<T: AccountDeserialize>(&self) -> Result<T, ClientError> {
+        self.account(anchor_lang::__private::state::address(&self.program_id))
+    }
+
     pub fn rpc(&self) -> RpcClient {
         RpcClient::new_with_commitment(
             self.cfg.cluster.url().to_string(),
@@ -304,14 +322,27 @@ pub struct RequestBuilder<'a> {
     // Serialized instruction data for the target RPC.
     instruction_data: Option<Vec<u8>>,
     signers: Vec<&'a dyn Signer>,
+    // True if the user is sending a state instruction.
+    namespace: RequestNamespace,
+}
+
+#[derive(PartialEq)]
+pub enum RequestNamespace {
+    Global,
+    State {
+        // True if the request is to the state's new ctor.
+        new: bool,
+    },
+    Interface,
 }
 
 impl<'a> RequestBuilder<'a> {
-    pub fn new(
+    pub fn from(
         program_id: Pubkey,
         cluster: &str,
         payer: Keypair,
         options: Option<CommitmentConfig>,
+        namespace: RequestNamespace,
     ) -> Self {
         Self {
             program_id,
@@ -322,6 +353,7 @@ impl<'a> RequestBuilder<'a> {
             instructions: Vec::new(),
             instruction_data: None,
             signers: Vec::new(),
+            namespace,
         }
     }
 
@@ -361,18 +393,53 @@ impl<'a> RequestBuilder<'a> {
         self
     }
 
+    /// Invokes the `#[state]`'s `new` constructor.
+    pub fn new(mut self, args: impl InstructionData) -> Self {
+        assert!(self.namespace == RequestNamespace::State { new: false });
+        self.namespace = RequestNamespace::State { new: true };
+        self.instruction_data = Some(args.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 accounts = match self.namespace {
+            RequestNamespace::State { new } => {
+                let mut accounts = match new {
+                    false => vec![AccountMeta::new(
+                        anchor_lang::__private::state::address(&self.program_id),
+                        false,
+                    )],
+                    true => vec![
+                        AccountMeta::new_readonly(self.payer.pubkey(), true),
+                        AccountMeta::new(
+                            anchor_lang::__private::state::address(&self.program_id),
+                            false,
+                        ),
+                        AccountMeta::new_readonly(
+                            Pubkey::find_program_address(&[], &self.program_id).0,
+                            false,
+                        ),
+                        AccountMeta::new_readonly(system_program::ID, false),
+                        AccountMeta::new_readonly(self.program_id, false),
+                        AccountMeta::new_readonly(rent::ID, false),
+                    ],
+                };
+                accounts.extend_from_slice(&self.accounts);
+                accounts
+            }
+            _ => self.accounts,
+        };
         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,
+                accounts,
             });
         }
 

+ 2 - 2
examples/tutorial/basic-4/programs/basic-4/src/lib.rs

@@ -7,8 +7,8 @@ pub mod basic_4 {
 
     #[state]
     pub struct Counter {
-        authority: Pubkey,
-        count: u64,
+        pub authority: Pubkey,
+        pub count: u64,
     }
 
     impl Counter {

+ 5 - 1
lang/src/lib.rs

@@ -232,7 +232,7 @@ pub mod prelude {
     pub use thiserror;
 }
 
-// Internal module used by macros.
+// Internal module used by macros and unstable apis.
 #[doc(hidden)]
 pub mod __private {
     use solana_program::program_error::ProgramError;
@@ -245,6 +245,10 @@ pub mod __private {
     pub use base64;
     pub use bytemuck;
 
+    pub mod state {
+        pub use crate::state::*;
+    }
+
     // Calculates the size of an account, which may be larger than the deserialized
     // data in it. This trait is currently only used for `#[state]` accounts.
     #[doc(hidden)]

+ 8 - 4
lang/src/state.rs

@@ -45,10 +45,7 @@ impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramState<'a, T> {
     }
 
     pub fn address(program_id: &Pubkey) -> Pubkey {
-        let (base, _nonce) = Pubkey::find_program_address(&[], program_id);
-        let seed = Self::seed();
-        let owner = program_id;
-        Pubkey::create_with_seed(&base, seed, owner).unwrap()
+        address(program_id)
     }
 }
 
@@ -145,3 +142,10 @@ impl<'info, T: AccountSerialize + AccountDeserialize + Clone> AccountsExit<'info
         Ok(())
     }
 }
+
+pub fn address(program_id: &Pubkey) -> Pubkey {
+    let (base, _nonce) = Pubkey::find_program_address(&[], program_id);
+    let seed = PROGRAM_STATE_SEED;
+    let owner = program_id;
+    Pubkey::create_with_seed(&base, seed, owner).unwrap()
+}

+ 13 - 4
lang/syn/src/codegen/program.rs

@@ -895,7 +895,7 @@ pub fn generate_ctor_variant(state: &State) -> proc_macro2::TokenStream {
 }
 
 pub fn generate_ctor_variant_name() -> String {
-    "Ctor".to_string()
+    "New".to_string()
 }
 
 fn generate_ctor_typed_args(state: &State) -> Vec<syn::PatType> {
@@ -1004,12 +1004,12 @@ pub fn generate_ixs(program: &Program) -> proc_macro2::TokenStream {
                 if ctor_args.is_empty() {
                     quote! {
                         #[derive(AnchorSerialize, AnchorDeserialize)]
-                        pub struct Ctor;
+                        pub struct New;
                     }
                 } else {
                     quote! {
                         #[derive(AnchorSerialize, AnchorDeserialize)]
-                        pub struct Ctor {
+                        pub struct New {
                             #(#ctor_args),*
                         }
                     }
@@ -1023,7 +1023,7 @@ pub fn generate_ixs(program: &Program) -> proc_macro2::TokenStream {
                 /// constructor.
                 #strct
 
-                impl anchor_lang::InstructionData for Ctor {
+                impl anchor_lang::InstructionData for New {
                     fn data(&self) -> Vec<u8> {
                         let mut d = #sighash_tts.to_vec();
                         d.append(&mut self.try_to_vec().expect("Should always serialize"));
@@ -1182,6 +1182,15 @@ fn generate_accounts(program: &Program) -> proc_macro2::TokenStream {
 
     // Go through state accounts.
     if let Some(state) = &program.state {
+        // Ctor.
+        if let Some((_ctor, ctor_accounts)) = &state.ctor_and_anchor {
+            let macro_name = format!(
+                "__client_accounts_{}",
+                ctor_accounts.to_string().to_snake_case()
+            );
+            accounts.insert(macro_name);
+        }
+        // Methods.
         if let Some((_impl_block, methods)) = &state.impl_block_and_methods {
             for ix in methods {
                 let anchor_ident = &ix.anchor_ident;