Smart Contract
Smart Contract
Transaction Object:
Signature & Message:-
Account Objects:-
Practical code:-
On-chain code:-
Instruction code
Off-chain code:-
Client-side:-
Hello World
Programs on Solana are a particular type of account that stores and executes
instruction logic.
Solana Programs:
all data stored on the Solana network are contained in what are referred to as accounts.
Each account has its own unique address which is used to identify and access the account
data. Solana programs are just a particular type of Solana account that store and execute
instructions.
For a basic program we will need to bring into scope the following items from the
solana_program crate:
● AccountInfo - a struct within the account_info module that allows us to access
account information.
● entrypoint - a macro that declares the entry point of the program.
● ProgramResult - a type within the entrypoint module that returns either a Result or
ProgramError.
● Pubkey - a struct within the pubkey module that allows us to access addresses as a
public key.
● msg - a macro that allows us to print messages to the program log.
The entry point to a Solana program requires a process_instruction function with the
following arguments:
Note : Recall that Solana program accounts only store the logic to process instructions. This
means program accounts are "read-only" and “stateless”. The “state” (the set of data)
that a program requires in order to process an instruction is stored in data accounts
(separate from the program account).
In order to process an instruction, the data accounts that an instruction requires must
be explicitly passed into the program through the accounts argument. Any additional
inputs must be passed in through the instruction_data argument.
LAB:-
1. Solana Program Crate
bring into scope everything we’ll need from the solana_program crate.
Most programs support multiple discrete instructions - you decide when writing your
program what these instructions are and what data must accompany them.
You can use the Borsh crate and the derive attribute to provide Borsh deserialization and
serialization functionality to Rust structs.
Rust match expressions help create conditional code paths based on the provided
instruction.
used Borsh for client-side serialization and deserialization. To use Borsh program-side, we
use the borsh crate. This crate provides traits for BorshDeserialize and BorshSerialize that
you can apply to your types using the derive attribute.
To make deserializing instruction data simple, you can create a struct representing the data
and use the derive attribute to apply the BorshDeserialize trait to the struct. This implements
the methods defined in BorshDeserialize, including the try_from_slice method that we'll be
using to deserialize the instruction data.
Remember, the struct itself needs to match the structure of the data in the byte array.
Once this struct has been created, you can create an implementation for your instruction
enum to handle the logic associated with deserializing instruction data. It's common to see
this done inside a function called unpack that accepts the instruction data as an argument
and returns the appropriate instance of the enum with the deserialized data.
Program logic:-
With a way to deserialize instruction data into a custom Rust type, you can then use
appropriate control flow to execute different code paths in your program based on which
instruction is passed into your program's entry point.
For simple programs where there are only one or two instructions to execute, it may
be fine to write the logic inside the match statement. For programs with many different
possible instructions to match against, your code will be much more readable if the logic for
each instruction is written in a separate function and simply called from inside the match
statement.
Program file structure:-
the complexity of a program grows, it's important to maintain a project structure that remains
readable and extensible. This involves encapsulating code into functions and data structures
But it also involves grouping related code into separate files.
For example, a good portion of the code we've worked through so far has to do with defining
and deserializing instructions. That code should live in its own file rather than be written in
the same file as the entry point. By doing so, we would then have 2 files, one with the
program entry point and the other with the instruction code:
● lib.rs
● instruction.rs
Once you start splitting your program up like this you will need to make sure you
register all of the files in one central location. We’ll be doing this in lib.rs. You must
register every file in your program like this.
Additionally, any declarations that you would like to be available through use
statements in other files will need to be prefaced with the pub keyword:
LAB:-
building out the first half of the Movie Review program:- This program stores
movie reviews submitted by users.
1. Entry point:-
Inside the lib.rs file, we’re going to bring in the following crates and define where
we’d like our entry point to the program to be with the entrypoint macro.
2. Deserialize instruction data:
Before we continue with the processor logic, we should define our supported
instructions and implement our deserialization function.
instruction.rs. Inside this new file, add use statements for BorshDeserialize and
ProgramError, then create a Movie Instruction enum with an AddMovieReview
variant. This variant should have embedded values for title, rating, and description.
Finally, create an implementation for the MovieInstruction enum that defines and
implements a function called unpack that takes a byte array as an argument and
returns a Result type.
● Use the split_first function to split the first byte of the array from the rest of
the array.
● Deserialize the rest of the array into an instance of MovieReviewPayload.
● Use a match statement to return the AddMovieReview variant of Movie
Instruction if the first byte of the array was a 0 or return a program error
otherwise.
3. Program logic:- With the instruction deserialization handled, we can return to the
lib.rs file to handle some of our program logic. we added code to a different file, we
need to register it in the lib.rs file using pub mod instruction; Then we can add a use
statement to bring the MovieInstruction type into scope.
your program should be functional enough to log the instruction data passed in when
a transaction is submitted!.
Create a Basic Program, Part 2 - State Management
The program state is stored in other accounts rather than in the program itself because
the Saolana program is stateless.
A Program Derived Address (PDA) is derived from a program ID and an optional list of
seeds. Once derived, PDAs are subsequently used as the address for a storage
account.
Creating an account requires that we calculate the space required and the corresponding
rent to allocate for the new account.
Creating a new account requires a Cross Program Invocation (CPI) to the create_account
instruction on the System Program.
Updating the data field on an account requires that we serialize (convert to byte array)
the data into the account.
Program state:-
All Solana accounts have a data field that holds a byte array. This makes accounts as
flexible as files on a computer. You can store literally anything in an account (so long as the
account has the storage space for it).
the data stored in a Solana account needs to follow some kind of pattern so that the data
can be retrieved and deserialized into something usable.
use Borsh for serialization and deserialization. In Rust, we can use the borsh crate to get
access to the BorshSerialize and BorshDeserialize traits.
Creating accounts:-
Before we can update the data field of an account, we have to first create that account.
Space and rent:- Recall that storing data on the Solana network requires users to allocate
rent in the form of lamports. The amount of rent required by a new account depends on the
amount of space you would like allocated to that account. That means we need to know
before creating the account how much space to allocate.
Before creating an account, we also need to have an address to assign the account. For
program owned accounts, this will be a program derived address (PDA) found using the
find_program_address function. PDAs are derived using the program ID (address of the
program creating the account) and an optional list of “seeds”. Optional seeds are additional
inputs used in the find_program_address function to derive the PDA. function to derive the
PDA.The function used to derive PDAs will return the same address every time when
given the same inputs. This gives us the ability to create any number of PDA accounts and a
deterministic way to find each account.
Once we’ve calculated the rent required for our account and found a valid PDA to assign as
the address of the new account, we are finally ready to create the account. Creating a new
account within our program requires a Cross Program Invocation (CPI). A CPI is when
one program invokes an instruction on another program.
Iterators:-
Iterators are used in Solana programs to safely iterate over the list of accounts passed
into the program entry point through the accounts argument.
Solana accounts iterator:- the AccountInfo for all accounts required by an instruction are
passing through a single accounts argument. In order to parse through the accounts and
use them within our instruction, we will need to create an iterator with a mutable reference to
the accounts. At that point, instead of using the iterator directly, we pass it to the
next_account_info function from the account_info module provided by the
solana_program crate.
For example, the instruction to create a new note in a note-taking program would at
minimum require the accounts for the user creating the note, a PDA to store the note, and
the system_program to initialize a new account. All three accounts would be passed into the
program entry point through the accounts argument. An iterator of accounts is then used to
separate out the AccountInfo associated with each account to process the instruction.
The first step to updating an account's data is to deserialize its data byte array into its Rust
type.to do this by first borrowing the data field on the account. This allows you to access the
data without taking ownership.
Once the Rust instance representing the account's data has been updated with the
appropriate values, you can "save" the data on the account.
Lab:-
Let’s now update our program to create new accounts to store the user’s movie
review.
● Define the struct our program uses to populate the data field of a new
account.
● Add BorshSerialize and BorshDeserialize traits to this struct.
First, let’s bring into scope everything we’ll need from the borsh crate.
let’s create our MovieAccountState struct. This struct will define the parameters
that each new movie review account will store in its data field. Our
MovieAccountState struct will require the following parameters:
2. Update lib.rs:- let’s update our lib.rs file. First, we’ll bring into scope everything
we will need to complete our Movie Review program.
we derive the PDA for each new account using the initializer’s public key and the
movie title as optional seeds.
Setting up the PDA this way restricts each user to only one review for any one movie
title. However, it still allows the same user to review movies with different titles and
different users to review movies with the same title.
// Derive PDA
5. Calculate space and rent:- calculate the rent that our new account will need.
Recall that rent is the amount of lamports a user must allocate to an account for
storing data on the Solana network. To calculate rent, we must first calculate the
amount of space our new account requires.
The MovieAccountState struct has four fields. We will allocate 1 byte each for
rating and is_initialized. For both title and description we will allocate space equal to
4 bytes plus the length of the string.
6. Create a new account:- Once we’ve calculated the rent and verified the PDA,
we are ready to create our new account. In order to create a new account, we must
call the create_account instruction from the system program. We do this with a
Cross Program Invocation (CPI) using the invoke_signed function. We use
invoke_signed because we are creating the account using a PDA and need the
Movie Review program
7. Update account data:- we’ve created a new account, we are ready to update the
data field of the new account using the format of the MovieAccountState struct from
our state.rs file. We first deserialize the account data from pda_account using
try_from_slice_unchecked, then set the values of each field.
// Derive PDA
Lastly, we serialize the updated account_data into the data field of our pda_account.
Intro to Anchor development
shell Starts a node shell with an Anchor client setup according to the
local config
Cargo.toml
● Anchor is a framework for building Solana programs
● Anchor macros speed up the process of building Solana programs by
abstracting away a significant amount of boilerplate code
● Anchor allows you to build secure programs more easily by performing
certain security checks, requiring account validation, and providing a
simple way to implement additional checks.
Anchor program structure: Anchor uses macros and traits to generate boilerplate
Rust code for you. These provide a clear structure to your program so you can more
easily reason about your code.
declare_id macro is used to specify the onchain address of the program (i.e. the
programId). When you build an Anchor program for the first time, the framework
will generate a new keypair. This becomes the default keypair used to deploy the
program unless specified otherwise. The corresponding public key should be used
as the programId specified in the declare_id! Macro.
Define instruction logic:- The #[program] attribute macro defines the module
containing all of your program's instructions. This is where you implement the
business logic for each instruction in your program.
Each public function in the module with the #[program] attribute will be treated as
a separate instruction. Each instruction function requires a parameter of type
Context and can optionally include additional function parameters representing
instruction data. Anchor will automatically handle instruction data
deserialization so that you can work with instruction data as Rust types.
Instruction Context:
The Context type exposes instruction metadata and accounts to your instruction
logic.
Context is a generic type where T defines the list of accounts an instruction
requires. When you use Context, you specify the concrete type of T as a struct that
adopts the Accounts trait (e.g. Context<AddMovieReviewAccounts>).
● Checks that the accounts passed into the instruction match the account types
specified in the InstructionAccounts struct
● Checks the accounts against any additional constraints specified
Account validation:
Anchor provides a number of account types that can be used to represent accounts.
Each type implements different account validation.