My FeedDiscussionsHeadless CMS
New
Sign in
Log inSign up
Learn more about Hashnode Headless CMSHashnode Headless CMS
Collaborate seamlessly with Hashnode Headless CMS for Enterprise.
Upgrade ✨Learn more

Anchor Framework simplified for new developers in Solana

Note: In this blog post I've only covered the rust part, in a further blog post I'll cover the Javascript RPC part to call our on chain Rust programs.

Harsh N. Patel's photo
Harsh N. Patel
·Sep 24, 2021·

10 min read

I'm trying to understand and compare Anchor with the vanilla Solana program architecture which uses Entrypoint, State, Processor, Instruction, Lib and Error files, one thing which was easy to spot was that most of the rust jargon is taken care of by attributes in anchor.

First of anchor has modules, modules are basically a way of grouping structs, functions, methods etc in a large program to make it more easier to export and reuse.

First let me explain what is serialization and deserialization. Serialization in simple terms basically means "the process of translating a data structure or object state into a format that can be stored or transmitted and reconstructed later.

Computers can recognise and process data as a stream of bytes in correct order and not in high level data structures (here we deal with serialization and deserialization of mostly accounts and structs). Serialization is basically a process of converting complex data structures into a stream of bytes for our computer. Deserialization as you may have guessed is the conversion of a stream of bytes back to its original structure.

There are many functions or pre-built programs for serialization and deserialization. In Solana Program Library they mostly used BorshSerialize and BorshDeserialize traits from the Borsh crate while in Anchor they use 4 traits AccountSerialize, Account Deserialise, AnchorSerialize and AnchorDeserialize which are just modified versions of Borsh traits.

Basic starter anchor specific imports:- 1) prelude 2) state

Intuitive observations that might be obvious but helpful for other "intemediate" noobs:-

  1. In anchor, there are no separate files for programs entrypoint, state, processor, instructions etc.

  2. the #[program] attribute initialises the instructions to be passed and should be enclosed in a module (mod{}).

  3. ‌Each instruction is nothing but a function which has a separate struct to define it's initial state, constraints, using the #[account(..)] attribute.

  4. the line ctx:Context<(your instruction struct name)> links your instruction with your struct for instructions as mentioned in the above point.

  5. ‌The #[derive(Accounts)] basically marks your instruction structs or simply enables them and the functions that come with it.

  6. the plain "#[account]" attribute macro implements serialization and deserialization of the accounts enabled by #[derive(Accounts) ] macro.

  7. #[account] attribute enables the final deserialized struct on which the instructions perform operations.

  8. #[access_control(<YOUR FUNCTION OR INSTRUCTION>)] -> "Executes the given access control method before running the decorated instruction handler. Any method in scope of the attribute can be invoked with any arguments from the associated instruction handler." ‌To simply explain this above point access control attribute is defined above the instruction function on which the "access control" condition is to be implemented. access_control checks and implements or executes a function or a condition before the actual instruction function no matter where it is defined in the program.

  9. #[error] attribute macro basically let's you create custom error enums which can include different error types. Comparing it with vanilla Solana program file configuration, #[error] basically helps us define the functions that we used to define in the error.rs files in our custom rust programs.

  10. #[account(init, payer = user, space = <size of the storage your account takes>)] attribute initializes the account with the current program as the owner of that account, it has to be passed with a payer argument which will be the user account which pays for the account initialization in lamports.

  11. #[account(mut)] marks the account code following this definition as mutable to register changes in its state (for eg. transferring tokens from Alice to Bob will change their account balance variable and hence change the account state).

  12. Brief comparison between Account<'info, "YOUR_CUSTOM_TYPE"> and AccountInfo<'info> structs. Account is an empty struct or can be treated as a container for other custom accounts or structs. It checks ownership on deserialization. AccountInfo struct is a predefined struct with different fields. Check here for more info. AccountInfo implements a ToAccountInfo trait. AccountInfo struct has a trait of its own which returns the AccountInfo struct. Account<'info, any account> does deserialization and reduces boilerplate. For example Account<'info, Mint>. Otherwise you'll have to add something like Mint::from() in your function You may want to use normal AccountInfo<'info> when:

  13. Account is a SOL wallet or signer: there's nothing to deserialize then
  14. Some 3rd party libraries don't support Account<'info, any account> deserialization yet, like Serum's Market struct, we have to deserialize inside our function.

Cross Program Invocations and PDAs

CpiContext is a struct similar to Context struct we previously discussed, it works for invoking instructions from a different program thus implementing cross-program invocation or Cpi. More on Cpis with anchor here.. Cpis can be used with program derived addresses or PDAs which are basically public keys that can be used to sign transactions without seed phrases. More on Cpis and PDAs here. What I observed after going through a lot of programs was:-

  • Pubkey::create_program_address(program_id, seeds) and Pubkey::find_program_address(program_id, seeds) both functions are used to create PDAs in vanilla Solana programs and invoke() and Invoke_signed() functions are used to invoke instructions of a different program by passing in the instructions and the accounts to be operated on. The core difference between invoke and invoke_signed functions is that invoke only needs instructions and accounts to be passed in it while invoke_signed needs an additional seeds array or program IDs in it as if the PDA itself is signing the transaction and invoking instructions. Now anchor again changes the rules, instead of using invoke() and Invoke_signed() functions,it uses CpiContext::new("YOUR_PROGRAM_ID", "YOUR ACCOUNTS TO BE PASSED") and CpiContext::new_with_signer("YOUR_PROGRAM_ID", "YOUR ACCOUNTS TO BE PASSED", "YOUR SEEDS") respectively. CpiContext struct similar to ProgramAccount struct in vanilla Solana programs, although ProgramAccount struct is also used in older anchor versions now replaced with Program struct. To invoke an instruction on another program, just use the cpi module on the crate, here, <YOUR_PROGRAM_TO_BE_INVOKED>::cpi::<INVOKED INSTRUCTION> check the anchor docs. .
impl<'info> From<&mut InitializeEscrow<'info>>
    for CpiContext<'_, '_, '_, 'info, SetAuthority<'info>>
{
    fn from(accounts: &mut InitializeEscrow<'info>) -> Self {
        let cpi_accounts = SetAuthority {
            account_or_mint: accounts
                .initializer_deposit_token_account
                .to_account_info()
                .clone(),
            current_authority: accounts.initializer.clone(),
        };
 //here the token account is called and stored in AccountInfo struct.
        let cpi_program = accounts.token_program.to_account_info();
 //here the CpiContext's new function is accessed then cpi_program and cpi_accounts is passed into the new function for Cross-Program Invocation.    
        CpiContext::new(cpi_program, cpi_accounts)
    }
}
  • Anchor based rust attributes help create accounts which have a specific format or a structure as simple as I can get this structure is described as follows:- 8 byte discriminator || serialised data (using borsh crate) The 8 byte discriminator as the name suggests helps us differentiate between different types of accounts so that each account can be uniquely identified. This 8 byte discriminator is generated by passing the type of the account into a famous hashing function called SHA-256. Since you know the function of a discriminator let me explain how discriminators are implemented gradually.

  • the account marked with #[account(init)] attribute does not have a discriminator as the account storage is zeroed.

  • when the account is marked with #[account(mut)] attribute, a unique 8 byte discriminator is generated by the hashing function and prepended into the account storage implicitly by the program.

#[account()] attributes

Lets talk about #[account] attributes implemented by #[derive(Accounts)] macro. As we know that #[derive(Accounts)] macro enables or marks our instruction handler structs, i.e. the structs which are passed in the Context<'info, ..> struct in the parameters of our instruction functions, we can also implement conditions to the accounts passed in our instruction handler structs by using these #[account] attributes. I'll explain each attribute with an example of how it can be implemented from the examples I went through.I'll try to explain in simple terms the best I can breaking down each line in the examples.

  • #[account(signer)]: This attribute works on AccountInfo structs and is basically an abstracted version of conditions given in rust, it basically checks if the account which is marked with this attribute has signed the transaction or not. As I've already discussed #[account(init)] and #[account(mut)] attributes above I won't discuss them here.
  • #[account(has_one = <any account>)]: Checks the target field on the account matches the target field in the struct deriving Accounts.

  • #[account(seeds = [<seeds>], bump? = <target>, payer? = <target>, space? = <target>, owner? = <target>)]: This attribute operates on AccountInfo structs. Seeds for the program derived address an AccountInfo struct represents. If bump is provided, then appends it to the seeds. On initialization, validates the given bump is the bump provided by Pubkey::find_program_address. Bump seed is a type of seed which pushes off a certain addresses off the ed2559 curve and they won't have a private key associated with them and also won't need them to sign a transaction (this usually works with PDAs where we may need the program to sign the transaction internally to execute the instructions i.e. a graphical representation of program addresses which will surely have a private key associated with them.) the payer parameter ensures that the payer is the owner of the program and space parameter needs a predefined size of the program in bytes to be passed. Owner parameter checks if the account owner matches the target owner specified. The following examples explain it all.

  • #[account(executable)]: It works on all AccountInfo structs and checks if the given account is an executable program or not.

  • #[account(close = <target>)]: It works on ProgramAccount/ Program structs and CpiAccount structs. This attribute marks the end of an account or terminates it after the end of an Instruction's execution.

  • #[account(zero)]: It works on ProgramAccount / Program structs and it asserts(checks if the condition equals to true) the account discriminator as zero. Here I've covered most of the important attributes on how each of them works, for more info check the docs.rs

Ex.

#[derive(Accounts)]
pub struct ExecuteTransaction<'info> {
     multisig: ProgramAccount<'info, Multisig>,
     #[account(seeds = [multisig.to_account_info().key.as_ref()],
 //look at how bump and seed parameters are used
        bump = multisig.nonce,
        )]
        multisig_signer: AccountInfo<'info>,
        #[account(mut, has_one = multisig)] **// refer the has_one function here, checks if the multisig account authority is actually the multisig authority key.**
        transaction: ProgramAccount<'info, Transaction>,
    }

Ex.

...
       #[derive(Accounts)] **//marks or enables Exchange instruction**
    pub struct Exchange<'info> {
        #[account(signer)]  **//checks if the taker account has indeed signed the transaction.**
        pub taker: AccountInfo<'info>,
        #[account(mut)]   **// marks the "taker_deposit_token_account" mutable to include changes**
        pub taker_deposit_token_account: Account<'info, TokenAccount>, 
...

Ex.

    ...
    pub struct InitializeEscrow<'info> {
        #[account(signer)]
        pub initializer: AccountInfo<'info>,
        #[account(mut,constraint = initializer_deposit_token_account.amount >= initializer_amount )] 

**// constraint as the name suggests implements a condition that initializer_deposit_token_account is >= to initializer_amount**
        pub initializer_deposit_token_account: Account<'info, TokenAccount>,
        pub initializer_receive_token_account: Account<'info, TokenAccount>,

        #[account(init, payer = initializer, space = 8 + EscrowAccount::LEN)] 
**//checks if the payer for the transaction is the initialiser itself, also the predefined size of the program is defined here.**
        pub escrow_account: Account<'info, EscrowAccount>,
        pub system_program: Program<'info, System>,
        pub token_program: Program<'info, Token>,
    }
    ....

Some useful links for more examples on anchor programs:-

.

Useful Tips:

  • use Cargo expand command to reveal the underlying rust code for each abstracted code like macros and attributes. More of it explained on Twitch stream by Armani Ferrante.
  • Try learning rust tutorials and join the Solana and Project Serum's discord channels for more resources.
  • PaulX's escrow program was very helpful for learning solana rust programming from scratch. Anchor has a lot of abstraction and comparing it with Anchor's escrow program you'll have an understanding as to why Anchor is so smooth. I highly recommend you to compare both of the code side by side if you were familiar with normal Solana rust programming and are new to Anchor.

This blog was inspired by the problems I faced while trying to learn anchor from scratch. I hope it'll help other intermediate devs and newbies like me, if you like this let me know or hit me up on my Twitter @harsh4786 . Special thanks and a shoutout to Shardul from cyclos.io and my friend Aadhi for helping me out. This blog is written by a noob who was trying to learn anchor, feel free to correct me or make any suggestions.