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