diff --git a/content/courses/native-onchain-development/deserialize-instruction-data.md b/content/courses/native-onchain-development/deserialize-instruction-data.md
index 1c88e4e17..3a18f4a0a 100644
--- a/content/courses/native-onchain-development/deserialize-instruction-data.md
+++ b/content/courses/native-onchain-development/deserialize-instruction-data.md
@@ -5,7 +5,7 @@ objectives:
- Create and use Rust structs and enums
- Use Rust match statements
- Add implementations to Rust types
- - Deserialize instruction data into Rust data types
+ - Deserialize instruction data into native Rust data types
- Execute different program logic for different types of instructions
- Explain the structure of a smart contract on Solana
description:
@@ -15,16 +15,16 @@ description:
## Summary
- Most programs support **multiple discrete instruction handlers** (sometimes
- just referred to as 'instructions') - these are functions inside your program
-- Rust **enums** are often used to represent each instruction handler
-- You can use the `borsh` crate and the `derive` attribute to provide Borsh
- deserialization and serialization functionality to Rust structs
+ referred to as 'instructions') - these are functions within your program.
+- Rust **enums** are often used to represent each instruction handler.
+- You can use the `borsh` crate and the `derive` attribute to enable Borsh
+ deserialization and serialization functionality in Rust structs.
- Rust `match` expressions help create conditional code paths based on the
- provided instruction
+ provided instruction.
## Lesson
-One of the most basic elements of a Solana program is the logic for handling
+One of the fundamental elements of a Solana program is the logic for handling
instruction data. Most programs support multiple functions, also called
instruction handlers. For example, a program may have different instruction
handlers for creating a new piece of data versus deleting the same piece of
@@ -32,51 +32,51 @@ data. Programs use differences in instruction data to determine which
instruction handler to execute.
Since instruction data is provided to your program's entry point as a byte
-array, it's common to create a Rust data type to represent instructions in a way
-that's more usable throughout your code. This lesson will walk through how to
-set up such a type, how to deserialize the instruction data into this format,
-and how to execute the proper instruction handler based on the instruction
-passed into the program's entry point.
+array, it's common to create a Rust data type to represent instructions in a
+more usable format throughout your code. This lesson will guide you through
+setting up such a type, deserializing the instruction data into this format, and
+executing the appropriate instruction handler based on the instruction passed
+into the program's entry point.
-### Rust basics
+### Rust Basics
-Before we dive into the specifics of a basic Solana program, let's talk about
-the Rust basics we'll be using throughout this lesson.
+Before diving into the specifics of a basic Solana program, let's cover the Rust
+basics that will be used throughout this lesson.
#### Variables
-Variable assignment in Rust happens with the `let` keyword.
+Variable assignment in Rust is done using the `let` keyword.
```rust
let age = 33;
```
-By default, variables in Rust are immutable, meaning a variable's value cannot
-be changed once it has been set. To create a variable that we'd like to change
-at some point in the future, we use the `mut` keyword. Defining a variable with
-this keyword means that its stored value can change.
+By default, variables in Rust are immutable, meaning their value cannot be
+changed once set. To create a variable that can be changed later, use the `mut`
+keyword. Defining a variable with this keyword allows its stored value to
+change.
```rust
-// compiler will throw error
+// Compiler will throw an error
let age = 33;
age = 34;
-// this is allowed
+// This is allowed
let mut mutable_age = 33;
mutable_age = 34;
```
-The Rust compiler guarantees that immutable variables cannot change, so you
-don’t have to keep track of it yourself. This makes your code easier to reason
-through and simplifies debugging.
+The Rust compiler ensures that immutable variables cannot change, so you don’t
+have to track it yourself. This makes your code easier to reason through and
+simplifies debugging.
#### Structs
-A struct, or structure, is a custom data type that lets you package together and
-name multiple related values that make up a meaningful group. Each piece of data
-in a struct can be of different types, and each has a name associated with it.
-These pieces of data are called **fields**. They behave similarly to properties
-in other languages.
+A struct (short for structure) is a custom data type that lets you package
+together and name multiple related values that make up a meaningful group. Each
+piece of data in a struct can be of different types, and each has a name
+associated with it. These pieces of data are called fields and behave similarly
+to properties in other languages.
```rust
struct User {
@@ -86,7 +86,7 @@ struct User {
}
```
-To use a struct after we’ve defined it, we create an instance of that struct by
+To use a struct after it’s defined, create an instance of the struct by
specifying concrete values for each of the fields.
```rust
@@ -97,16 +97,20 @@ let mut user1 = User {
};
```
-To get or set a specific value from a struct, we use dot notation.
+To get or set a specific value from a struct, use dot notation.
```rust
user1.age = 37;
```
+You can check out the
+[struct examples](https://doc.rust-lang.org/rust-by-example/custom_types/structs.html)
+for in depth understanding.
+
#### Enumerations
-Enumerations (or Enums) are a data struct that allow you to define a type by
-enumerating its possible variants. An example of an enum may look like:
+Enumerations (or Enums) are a data struct that allows you to define a type by
+enumerating its possible variants. An example of an enum might look like:
```rust
enum LightStatus {
@@ -115,8 +119,7 @@ enum LightStatus {
}
```
-The `LightStatus` enum has two possible variants in this situation: it's
-either`On` or `Off`.
+The `LightStatus` enum has two possible variants in this example: `On` or `Off`.
You can also embed values into enum variants, similar to adding fields to a
struct.
@@ -133,15 +136,18 @@ let light_status = LightStatus::On { color: String::from("red") };
```
In this example, setting a variable to the `On` variant of `LightStatus`
-requires also setting the value of `color`.
+requires also setting the value of `color`. You can check out more examples of
+using enums in Rust by visiting
+[this Rust by Example page on enums](https://doc.rust-lang.org/rust-by-example/custom_types/enum.html).
#### Match statements
Match statements are very similar to `switch` statements in other languages. The
-`match` statement allows you to compare a value against a series of patterns and
-then execute code based on which pattern matches the value. Patterns can be made
-of literal values, variable names, wildcards, and more. The match statement must
-include all possible scenarios, otherwise the code will not compile.
+[`match`](https://doc.rust-lang.org/rust-by-example/flow_control/match.html)
+statement allows you to compare a value against a series of patterns and then
+execute code based on which pattern matches the value. Patterns can be made of
+literal values, variable names, wildcards, and more. The match statement must
+include all possible scenarios; otherwise, the code will not compile.
```rust
enum Coin {
@@ -163,8 +169,9 @@ fn value_in_cents(coin: Coin) -> u8 {
#### Implementations
-The `impl` keyword is used in Rust to define a type's implementations. Functions
-and constants can both be defined in an implementation.
+The [`impl`](https://doc.rust-lang.org/rust-by-example/trait/impl_trait.html)
+keyword is used in Rust to define a type's implementations. Functions and
+constants can both be defined in an implementation.
```rust
struct Example {
@@ -186,7 +193,7 @@ impl Example {
}
```
-The function `boo` here can only be called on the type itself rather than an
+The `boo` function here can only be called on the type itself rather than an
instance of the type, like so:
```rust
@@ -203,14 +210,15 @@ example.answer();
#### Traits and attributes
-You won't be creating your own traits or attributes at this stage, so we won't
-provide an in-depth explanation of either. However, you will be using the
-`derive` attribute macro and some traits provided by the `borsh` crate, so it's
-important you have a high-level understanding of each.
+You won't be creating your own traits or attributes at this stage, so an
+in-depth explanation isn't necessary. However, you will be using the `derive`
+attribute macro and some traits provided by the `borsh` crate, so it's important
+to have a high-level understanding of each.
-Traits describe an abstract interface that types can implement. If a trait
-defines a function `bark()` and a type then adopts that trait, the type must
-then implement the `bark()` function.
+[Traits](https://doc.rust-lang.org/rust-by-example/trait.html) describe an
+abstract interface that types can implement. If a trait defines a function
+`bark()` and a type adopts that trait, the type must implement the `bark()`
+function.
[Attributes](https://doc.rust-lang.org/rust-by-example/attribute.html) add
metadata to a type and can be used for many different purposes.
@@ -221,7 +229,7 @@ to a type and provide one or more supported traits, code is generated under the
hood to automatically implement the traits for that type. We'll provide a
concrete example of this shortly.
-### Representing instructions as a Rust data type
+### Representing Instructions as a Rust Data Type
Now that we've covered the Rust basics, let's apply them to Solana programs.
@@ -255,7 +263,7 @@ Notice that each variant of the `NoteInstruction` enum comes with embedded data
that will be used by the program to accomplish the tasks of creating, updating,
and deleting a note, respectively.
-### Deserialize instruction data
+### 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
@@ -305,25 +313,24 @@ impl NoteInstruction {
// determine which instruction handler to execute
let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;
// Use the temporary payload struct to deserialize
- let payload = NoteInstructionPayload::try_from_slice(rest).unwrap();
+ let payload = NoteInstructionPayload::try_from_slice(rest)
+ .map_err(|_| ProgramError::InvalidInstructionData)?;
// Match the variant to determine which data struct is expected by
// the function and return the TestStruct or an error
- Ok(match variant {
- 0 => Self::CreateNote {
+ match variant {
+ 0 => Ok(Self::CreateNote {
title: payload.title,
body: payload.body,
- id: payload.id
- },
- 1 => Self::UpdateNote {
+ id: payload.id,
+ }),
+ 1 => Ok(Self::UpdateNote {
title: payload.title,
body: payload.body,
- id: payload.id
- },
- 2 => Self::DeleteNote {
- id: payload.id
- },
- _ => return Err(ProgramError::InvalidInstructionData)
- })
+ id: payload.id,
+ }),
+ 2 => Ok(Self::DeleteNote { id: payload.id }),
+ _ => Err(ProgramError::InvalidInstructionData),
+ }
}
}
```
@@ -338,17 +345,33 @@ There's a lot in this example so let's take it one step at a time:
`NoteInstructionPayload` to deserialize the rest of the byte array into an
instance of `NoteInstructionPayload` called `payload`
3. Finally, the function uses a `match` statement on `variant` to create and
- return the appropriate enum instance using information from `payload`
+ return the appropriate enum instance using information from `payload`. Each
+ valid variant (0, 1, 2) corresponds to a specific NoteInstruction variant,
+ while any other value results in an error.
+
+
+
+There is Rust syntax in this function that we haven't explained yet. The
+`ok_or`, `map_err`, and `?` operators are used for error handling:
+
+- [`ok_or`](https://doc.rust-lang.org/std/option/enum.Option.html#method.ok_or):
+ Converts an `Option` to a `Result`. If the `Option` is `None`, it returns the
+ provided error. Otherwise, it returns the `Some` value as `Ok`.
-Note that there is Rust syntax in this function that we haven't explained yet.
-The `ok_or` and `unwrap` functions are used for error handling and will be
-discussed in detail in another lesson.
+- [`map_err`](https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err):
+ Transforms the error of a `Result` by applying a function to the error. It
+ leaves the `Ok` value unchanged.
+
+- [`?` operator](https://doc.rust-lang.org/rust-by-example/error/result/enter_question_mark.html):
+ Unwraps a `Result` or `Option`. If it’s `Ok` or `Some`, it returns the value.
+ If it’s an `Err` or `None`, it propagates the error up to the calling
+ function.
### Program logic
-With a way to deserialize instruction data into a custom Rust type, you can then
+With a method to deserialize instruction data into a custom Rust type, you can
use appropriate control flow to execute different code paths in your program
-based on which instruction is passed into your program's entry point.
+based on the instruction passed into the program's entry point.
```rust
entrypoint!(process_instruction);
@@ -356,59 +379,60 @@ entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
- instruction_data: &[u8]
+ instruction_data: &[u8],
) -> ProgramResult {
+ msg!("Note program entrypoint");
// Call unpack to deserialize instruction_data
let instruction = NoteInstruction::unpack(instruction_data)?;
// Match the returned data struct to what you expect
match instruction {
NoteInstruction::CreateNote { title, body, id } => {
+ msg!("Instruction: Create Note");
// Execute program code to create a note
},
NoteInstruction::UpdateNote { title, body, id } => {
+ msg!("Instruction: Update Note");
// Execute program code to update a note
},
NoteInstruction::DeleteNote { id } => {
+ msg!("Instruction: Delete Note");
// Execute program code to delete a note
}
}
}
```
-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 handler is written in a separate
-function and simply called from inside the `match` statement.
+For simple programs with one or two instructions, placing logic inside the
+`match` statement may suffice. However, for programs with many instructions, it
+is advisable to write the logic for each instruction handler in a separate
+function and call it from within the `match` statement.
-### Program file structure
+### Program File Structure
-The [Hello World lesson’s](hello-world-program) program was simple enough that
-it could be confined to one file. But as 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 as we've
-done so far. But it also involves grouping related code into separate files.
+The
+[Hello World lesson](/content/courses/native-onchain-development/hello-world-program.md)
+demonstrated a program simple enough to be confined to one file. As program
+complexity grows, maintaining a readable and extensible project structure
+becomes crucial. This involves encapsulating code into functions and data
+structures, and grouping related code into separate files.
-For example, a good portion of the code we've worked through so far involves
-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 two files, one with the program entry point and the other with
-the instruction handler:
+For instance, instruction definition and deserialization code should reside in
+its own file, separate from the entry point. This approach might result in two
+files: one for the program entry point and another for the instruction handler.
- **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.**
+When splitting your program into multiple files, register all files in a central
+location, typically in `lib.rs`. Each file must be registered this way.
-```rust
-// This would be inside lib.rs
+```rust filename="lib.rs"
+// Inside lib.rs
pub mod instruction;
```
-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:
+Additionally, use the `pub` keyword to make declarations available for `use`
+statements in other files.
```rust
pub enum NoteInstruction { ... }
@@ -416,24 +440,18 @@ pub enum NoteInstruction { ... }
## Lab
-For this lesson’s lab, we’ll be building out the first half of the Movie Review
-program that we worked with in Module 1. This program stores movie reviews
-submitted by users.
-
-For now, we'll focus on deserializing the instruction data. The following lesson
-will focus on the second half of this program.
+For this lesson’s lab, you'll build the first half of the Movie Review program
+from Module 1, focusing on deserializing instruction data. The next lesson will
+cover the remaining implementation.
-#### 1. Entry point
+### 1. Entry point
-We’ll be using [Solana Playground](https://beta.solpg.io/) again to build out
-this program. Solana Playground saves state in your browser, so everything you
-did in the previous lesson may still be there. If it is, let's clear everything
-out from the current `lib.rs` file.
+Using [Solana Playground](https://beta.solpg.io/), clear everything in the
+current `lib.rs` file if it's still populated from the previous lesson. Then,
+bring in the following crates and define the program's entry point using the
+entrypoint macro.
-Inside lib.rs, 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.
-
-```rust
+```rust filename="lib.rs"
use solana_program::{
entrypoint,
entrypoint::ProgramResult,
@@ -458,17 +476,15 @@ pub fn process_instruction(
#### 2. Deserialize instruction data
-Before we continue with the processor logic, we should define our supported
-instructions and implement our deserialization function.
-
-For readability, let's create a new file called `instruction.rs`. Inside this
-new file, add `use` statements for `BorshDeserialize` and `ProgramError`, then
-create a `MovieInstruction` enum with an `AddMovieReview` variant. This variant
-should have embedded values for `title,` `rating`, and `description`.
+Define your supported instructions and implement a deserialization function.
+Create a new file called `instruction.rs`, and add `use` statements for
+`BorshDeserialize` and `ProgramError`, and create a `MovieInstruction` enum with
+an `AddMovieReview` variant that includes `title`, `rating`, and `description`
+values.
-```rust
+```rust filename="instruction.rs"
use borsh::{BorshDeserialize};
-use solana_program::{program_error::ProgramError};
+use solana_program::program_error::ProgramError;
pub enum MovieInstruction {
AddMovieReview {
@@ -479,9 +495,9 @@ pub enum MovieInstruction {
}
```
-Next, define a `MovieReviewPayload` struct. This will act as an intermediary
-type for deserialization so it should use the `derive` attribute macro to
-provide a default implementation for the `BorshDeserialize` trait.
+Next, define a `MovieReviewPayload` struct as an intermediary type for
+deserialization. Use the derive attribute macro to provide a default
+implementation for the `BorshDeserialize` trait.
```rust
#[derive(BorshDeserialize)]
@@ -492,46 +508,43 @@ struct MovieReviewPayload {
}
```
-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:
+Finally, implement the `MovieInstruction` enum by defining a `unpack` function
+that takes a byte array and returns a `Result` type. This function should:
-1. Use the `split_first` function to split the first byte of the array from the
- rest of the array
-2. Deserialize the rest of the array into an instance of `MovieReviewPayload`
+1. Split the first byte from the array using `split_first`.
+2. Deserialize the remaining array into a `MovieReviewPayload` instance.
3. Use a `match` statement to return the `AddMovieReview` variant of
- `MovieInstruction` if the first byte of the array was a 0 or return a program
- error otherwise
+ `MovieInstruction` if the first byte is 0, otherwise return a program error.
```rust
impl MovieInstruction {
// Unpack inbound buffer to associated Instruction
// The expected format for input is a Borsh serialized vector
pub fn unpack(input: &[u8]) -> Result {
- // Split the first byte of data
- let (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;
- // `try_from_slice` is one of the implementations from the BorshDeserialization trait
- // Deserializes instruction byte data into the payload struct
- let payload = MovieReviewPayload::try_from_slice(rest).unwrap();
- // Match the first byte and return the AddMovieReview struct
- Ok(match variant {
- 0 => Self::AddMovieReview {
+ // Ensure the input is not empty and split off the first byte (instruction variant)
+ let (&variant, rest) = input.split_first()
+ .ok_or(ProgramError::InvalidInstructionData)?;
+ // Attempt to deserialize the remaining input into a MovieReviewPayload
+ let payload = MovieReviewPayload::try_from_slice(rest)
+ .map_err(|_| ProgramError::InvalidInstructionData)?;
+ // Match on the instruction variant to construct the appropriate MovieInstruction
+ match variant {
+ 0 => Ok(Self::AddMovieReview {
title: payload.title,
rating: payload.rating,
- description: payload.description },
- _ => return Err(ProgramError::InvalidInstructionData)
- })
+ description: payload.description,
+ }),
+ // If the variant doesn't match any known instruction, return an error
+ _ => Err(ProgramError::InvalidInstructionData),
+ }
}
}
```
#### 3. Program logic
-With the instruction deserialization handled, we can return to the `lib.rs` file
-to handle some of our program logic.
-
-Remember, since 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
+Return to `lib.rs` to handle the program logic now that instruction
+deserialization is set up. Register the `instruction.rs` file in `lib.rs` and
bring the `MovieInstruction` type into scope.
```rust
@@ -539,11 +552,10 @@ pub mod instruction;
use instruction::{MovieInstruction};
```
-Next, let's define a new function `add_movie_review` that takes the 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 and we'll revisit the rest of the implementation of the
-function in the next lesson.
+Next, define an `add_movie_review` function that takes `program_id`, `accounts`,
+`title`, `rating`, and `description` as arguments, and returns a
+`ProgramResult`. For now, log these values, and we'll revisit the function
+implementation in the next lesson.
```rust
pub fn add_movie_review(
@@ -564,11 +576,9 @@ pub fn add_movie_review(
}
```
-With that done, we can call `add_movie_review` from `process_instruction` (the
-function we set as our entry point). 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.
+Finally, call `add_movie_review` from `process_instruction`, unpack the
+instruction using the `unpack` method, and use a `match` statement to ensure the
+instruction is the `AddMovieReview` variant.
```rust
pub fn process_instruction(
@@ -588,51 +598,45 @@ pub fn process_instruction(
}
```
-And just like that, your program should be functional enough to log the
-instruction data passed in when a transaction is submitted!
+With this, your program should now log the instruction data when a transaction
+is submitted. Build and deploy your program from Solana Playground as in the
+last lesson. If your program ID hasn't changed, it will deploy to the same ID.
+To deploy to a different address, generate a new program ID before deploying.
-Build and deploy your program from Solana Program just like in the last lesson.
-If you haven't changed the program ID since going through the last lesson, it
-will automatically deploy to the same ID. If you'd like it to have a separate
-address, you can generate a new program ID from the playground before deploying.
-
-You can test your program by submitting a transaction with the right instruction
-data. For that, feel free to use
+Test your program by submitting a transaction with the correct instruction data.
+You can use
[this script](https://github.com/Unboxed-Software/solana-movie-client) or
[the frontend](https://github.com/Unboxed-Software/solana-movie-frontend) we
built in the
[Serialize Custom Instruction Data lesson](/content/courses/native-onchain-development/serialize-instruction-data-frontend.md).
-In both cases, make sure you copy and paste the program ID for your program into
-the appropriate area of the source code to make sure you're testing the right
+Ensure you update the program ID in the source code to match your deployed
program.
-If you need to spend some more time with this lab before moving on, please do!
-You can also have a look at the program
+Take your time with this lab before moving on, and feel free to reference the
[solution code](https://beta.solpg.io/62aa9ba3b5e36a8f6716d45b) if you get
stuck.
## Challenge
-For this lesson's challenge, try replicating the Student Intro program from
-Module 1. Recall that we created a frontend application that lets students
-introduce themselves! The program takes a user's name and a short message as the
-`instruction_data` and creates an account to store the data onchain.
+Replicate the Student Intro program from Module 1 for this lesson's challenge.
+The program takes a user's name and a short message as `instruction_data` and
+creates an account to store the data on-chain.
-Using what you've learned in this lesson, build the Student Intro program to the
-point where you can print the `name` and `message` provided by the user to the
-program logs when the program is invoked.
+Using what you've learned, build the Student Intro program to the point where it
+prints the `name` and `message` to the program logs when invoked.
You can test your program by building the
[frontend](https://github.com/Unboxed-Software/solana-student-intros-frontend/tree/solution-serialize-instruction-data)
we created in the
[Serialize Custom Instruction Data lesson](/content/courses/native-onchain-development/serialize-instruction-data-frontend.md)
-and then checking the program logs on Solana Explorer. Remember to replace the
-program ID in the frontend code with the one you've deployed.
+and checking the program logs on Solana Explorer. Replace the program ID in the
+frontend code with your deployed program ID.
Try to do this independently if you can! But if you get stuck, feel free to
reference the [solution code](https://beta.solpg.io/62b0ce53f6273245aca4f5b0).
+
Push your code to GitHub and
[tell us what you thought of this lesson](https://form.typeform.com/to/IPH0UGz7#answers-lesson=74a157dc-01a7-4b08-9a5f-27aa51a4346c)!