0% found this document useful (0 votes)
9 views29 pages

Smart Contract

The document outlines the structure and functionality of on-chain and off-chain code for Solana programs, detailing the transaction object, account objects, and the process of creating and managing smart contracts. It emphasizes the use of Rust for coding, including the importance of serialization and deserialization using the Borsh crate, and explains how to handle program state through accounts. Additionally, it introduces the Anchor framework for simplifying Solana program development.

Uploaded by

prajwalv208
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
9 views29 pages

Smart Contract

The document outlines the structure and functionality of on-chain and off-chain code for Solana programs, detailing the transaction object, account objects, and the process of creating and managing smart contracts. It emphasizes the use of Rust for coding, including the importance of serialization and deserialization using the Borsh crate, and explains how to handle program state through accounts. Additionally, it introduces the Anchor framework for simplifying Solana program development.

Uploaded by

prajwalv208
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 29

On-chain and off-chain high level

Transaction Object:
Signature & Message:-

Break into Message :-


After saving the Smart-contract (program logic) on the Solana network, the Program ID will
generate

Account Objects:-


Data:- just serialized data that we send across the network. ​


Program id:- id of smart-contract.

Practical code:-
On-chain code:- ​

Instruction code

Off-chain code:-

Client-side:-

Rust on-chain code:-




Defining the account data type:-
Just data that we store on smart contracts in this instance we keep track of an account
value.

Entry point:- handle our transaction

Rust off-chain code:-


Deploying Smart-contract:

Hello World
Programs on Solana are a particular type of account that stores and executes
instruction logic.

Solana programs have a single entry point to process instructions.

A program processes an instruction using the program_id, list of accounts, and


instruction_data included with the instruction.

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.

Solana Program Entry Point:


Solana programs require a single entry point to process program instructions. The entry
point is declared using the entrypoint! Macro.

The entry point to a Solana program requires a process_instruction function with the
following arguments:

●​ program_id - the address of the account where the program is stored.


●​ accounts - the list of accounts required to process the instruction.
●​ instruction_data - the serialized, instruction-specific data.

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.

2.​ Entry Point


set up the entry point to our program using the entrypoint! macro and create the
process_instruction function. The msg! macro then allows us to print “Hello, world!”
to the program log when the program is invoked.
3.​ All together, the “Hello, world!” program will look like this:

Create a Basic Program, Part 1 - Handle Instruction Data

Most programs support multiple discrete instructions - you decide when writing your
program what these instructions are and what data must accompany them.

Rust enums are often used to represent discrete program instructions.

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.

Deserialize instruction data:-


Instruction data is passed to the program as a byte array, so you need a way to
deterministically convert that array into an instance of the instruction enum type.

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.

For now, we'll focus on deserializing the instruction data.

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.

Then define a MovieReviewPayload struct. This will act as an intermediary type


for deserializtion so it should use the derive attribute macro to provide a default
implementation for the BorshDeserialize trait.

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.

This function should:

●​ 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.

define a new function add_movie_review that takes as arguments program_id,


accounts, title, rating, and description. It should also return an instance of
ProgramResult Inside this function, let's simply log our values for now
With that done, we can call add_movie_review from process_instruction. (the
function we set as our entry point). In order to pass all the required arguments to the
function, we'll first need to call the unpack we created on MovieInstruction, then
use a match statement to ensure that the instruction we've received is the
AddMovieReview variant.

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.

Solana maintains speed, efficiency, and extensibility in part by making programs


stateless. Rather than having state stored on the program itself, programs use
Solana's account model to read state from and write state to separate PDA accounts.

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.

Represent state as a Rust type:-

Using Borsh for serialization and deserialization:-

Serialization is the process of converting an object into a byte array.

Deserialization is the process of reconstructing an object from a byte array.

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.

To create a new account within our program we must:

●​ Calculate the space and rent required for the account.


●​ Have an address to assign the new account.
●​ Invoke the system program to create the new 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.

Program Derived Addresses (PDA):-

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.

Cross Program Invocation (CPI):-

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.

Serializing and deserializing account data:-

Deserialize account data:-

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.

Serialize account data:-

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.

1. Create struct to represent account data:-

Create a new file named state.rs.

This file will:

●​ 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:

●​ is_initialized - shows whether or not the account has been initialized.


●​ rating - user’s rating of the movie.
●​ description - user’s description of the movie.
●​ title - title of the movie the user is reviewing.

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.

3. Iterate through accounts:- let’s continue building out our add_movie_review


function. Recall that an array of accounts is passed into the add_movie_review
function through a single accounts argument. To process our instruction, we
will need to iterate through accounts and assign the AccountInfo for each
account to its own variable.

4. Derive PDA:- within our add_movie_review function, let’s independently derive


the PDA we expect the user to have passed in. We'll need to provide the bump seed
for the derivation later, so even though pda_account should reference the same
account, we still need to call find_program_address.

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

let (pda, bump_seed) = Pubkey::find_program_address(&[initializer.key.as_ref(),


title.as_bytes().as_ref(),], program_id);

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

to “sign” the instruction.

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

Anchor is a development framework that makes writing Solana programs


easier, faster, and more secure. It makes it easier to organize your code,
implements common security checks automatically, and abstracts away a
significant amount of boilerplate associated with writing a Solana program.
SUBCOMMANDS:

account Fetch and deserialize an account using the IDL provided

build Builds the workspace

cluster Cluster commands

deploy Deploys each program in the workspace

expand Expands the macros of a program or the workspace

help Prints this message or the help of the given subcommand(s)

idl Commands for interacting with interface definitions

init Initializes a workspace

migrate Runs the deploy migration script

new Creates a new program

shell Starts a node shell with an Anchor client setup according to the
local config

test Runs integration tests against a localnetwork

upgrade Upgrades a single program. The configured wallet must be the


upgrade authority

verify Verifies the on-chain bytecode matches the locally compiled


artifact. Run this

command inside a program subdirectory, i.e., in the dir containing


the program's

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.

The main high level macros and attributes are:

●​ declare_id - a macro for declaring the program’s onchain address.


●​ #[program] - an attribute macro used to denote the module containing the
program’s instruction logic.
●​ Accounts - a trait applied to structs representing the list of accounts required
for an instruction.
●​ #[account] - an attribute macro used to define custom account types for the
program.

Declare your program ID:-

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>).

Through this context argument, the instruction can then access:

●​ The accounts passed into the instruction (ctx.accounts)


●​ The program ID (ctx.program_id) of the executing program
●​ The remaining accounts (ctx.remaining_accounts). The
remaining_accounts is a vector that contains all accounts that were
passed into the instruction but are not declared in the Accounts struct.
●​ The bumps for any PDA accounts in the Accounts struct (ctx.bumps)

Define instruction accounts (program module “account data types”): The


Accounts trait defines a data structure of validated accounts. Structs adopting
this trait define the list of accounts required for a given instruction. These accounts
are then exposed through an instruction's Context so that manual account
iteration and deserialization is no longer necessary.
Implementations of the Accounts trait are responsible for performing all requisite
constraint checks to ensure the accounts meet conditions required for the program
to run securely. Constraints are provided for each field using the #account(..)
attribute (more on that shortly).

When instruction_one is invoked, the program:

●​ 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

If any accounts passed into instruction_one fail the account validation or


security checks specified in the InstructionAccounts struct, then the instruction
fails before even reaching the program logic.

Account validation:

Anchor provides a number of account types that can be used to represent accounts.
Each type implements different account validation.

common types you may encounter:

Account: Account is a wrapper around AccountInfo that verifies program


ownership and deserializes the underlying data into a Rust type.
let (pda, bump_seed) = Pubkey::find_program_address(&[initializer.key.as_ref(),
title.as_bytes().as_ref(),], program_id);

You might also like