diff --git a/docs/03-intermediate/01-course-content.md b/docs/03-intermediate/01-course-content.md index 65b08b3..f3ab766 100644 --- a/docs/03-intermediate/01-course-content.md +++ b/docs/03-intermediate/01-course-content.md @@ -5,23 +5,23 @@ hide_table_of_contents: true # Course content -This course is a continuation of the basic course. Let's delve deeper into the implementation of programs using Gear technologies, in particular how programs can communicate with each other, receive and process requests, as well as how to handle cases when one of the parties does not respond +This course serves as a continuation of the basic course and delves deeper into the implementation of programs using Gear technologies. Specifically, it focuses on how programs can communicate with each other, receive and process requests, and handle scenarios where one party does not respond. The course material is structured into three sections: 1. Message Acquisition and Processing: -- Detailed examination of the handle_reply() function. -- Acquisition of skills in processing messages received as responses. +- Detailed examination of the `handle_reply()` function; +- Acquisition of skills in processing messages received as responses; - Testing of the developed program. 2. Asynchronous Logic Utilizing the Waiting List: -- Introduction to the wait() and wake() functions. -- Mastery of halting message processing and awakening it as necessary. -- Analysis of scenarios where responses are not received and resolution using the wait_for() function. +- Introduction to the `exec::wait()` and `exec::wake()` functions; +- Mastery of halting message processing and awakening it as necessary; +- Analysis of scenarios where responses are not received and resolution using the `exec::wait_for()` function; - Testing of the implemented programs. 3. Handling of Delayed Messages: -- In-depth exploration of delayed messages. -- Resolution of issues related to lack of response through the use of pending messages. -- Testing of the program featuring pending messages. +- In-depth exploration of delayed messages; +- Resolution of issues related to lack of response through the use of delayed messages; +- Testing of the program featuring delayed messages. diff --git a/docs/03-intermediate/02-message-receiving/handle_reply.md b/docs/03-intermediate/02-message-receiving/handle_reply.md index 224ac03..786b384 100644 --- a/docs/03-intermediate/02-message-receiving/handle_reply.md +++ b/docs/03-intermediate/02-message-receiving/handle_reply.md @@ -5,101 +5,157 @@ hide_table_of_contents: true # Message receiving -In this tutorial, you will acquire knowledge on how a program can effectively handle request messages. Let's illustrate this concept with an example of interaction between two programs: one program will act as an echo, responding with the received message, while the second program will initiate the communication by sending a message to the echo program and then receiving a response. - -Before delving into the analysis of program code, it is useful to illustrate the operation of our programs schematically: +In this lesson, you will learn how a program can efficiently handle request messages. We will illustrate this concept with an example of interaction between two programs. Before delving into the analysis of the program code, it is useful to illustrate the operation of our programs schematically. ![gif 1](../img/02/handle_reply.gif) -1. The user sends a "Hello" message to Program №1, which is processed by the handle() function; -2. This message is then forwarded to Program №2 (the echo program); -3. Program №1 sends a confirmation message to the user, indicating that the message has been successfully transmitted to Program №2; -4. Program №2 receives the message from Program №1 and responds with a reply message; -5. Program №1 receives the reply message from Program №2 through the handle_reply entrypoint; -6. Finally, the "Hello" message is relayed from the handle_reply function to the user. - +1. The user sends some `Action` message to program №1, which is processed by `handle()`; +2. This message is then passed to programme №2; +3. Programme №1 sends an `Event` message to the user indicating that the message was successfully passed to Programme №2; +4. Programme №2 receives the message from Programme №1 processes it and responds; +5. Programme №1 receives the reply message from Programme №2 via the `handle_reply()` entry point; +6. Finally, from the `handle_reply()` function, send the message to the user. -## Echo program +## First program -The echo program is very simple, using the `msg::reply_input()` it will reply with the same message that came to it: +The task of the first program is to communicate with the second program, so the following structure will be needed: ```rust -#[no_mangle] -extern "C" fn handle() { - // Reply to the incoming message - msg::reply_input(0, 0..msg::size()).expect("Error in sending a reply"); +struct Session { + second_program: ActorId, // second program address + msg_id_to_actor: (MessageId, ActorId), // tuple of message identifiers and message source address } ``` -## Main program - -The structure of the program is as follows: +The following actions and events will also be necessary to simulate a dialogue between programs: ```rust -struct Program { - echo_address: ActorId, // echo program address - msg_id_to_actor: (MessageId, ActorId), // tuple of message identifiers and message source address +#[derive(TypeInfo, Encode, Decode)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] +pub enum Action { + Hello, + HowAreYou, + MakeRandomNumber{ + range: u8, + }, +} + +#[derive(TypeInfo, Encode, Decode, Debug)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] +pub enum Event { + Hello, + Fine, + Number(u8), + MessageSent, } ``` -When the program is initialized, an echo address is sent: +During initialization, it is necessary to pass the address of the second program: ```rust #[no_mangle] -extern fn init() { - let echo_address = msg::load().expect("Unable to decode init"); +extern "C" fn init() { + let second_program = msg::load().expect("Unable to decode Init"); unsafe { - PROGRAM = Some(Program { - echo_address, + SESSION = Some(Session { + second_program, msg_id_to_actor: (MessageId::zero(), ActorId::zero()), }); } } ``` -Now let's look at sending messages using the `handle()` function: +Let's focus on processing requests in the `handle()` function: 1. Receive the message with the function `msg::load()`; -2. Send a message to the echo address using the `msg::send()`; -3. An important step is to store the identifier of the message that the `msg::send()` returns, so that the `handle_reply()` function can determine which message was responded to; -4. At the end send a reply message notifying that the message was sent to the echo address. +2. Send a message to the second program using the `msg::send()`; +3. An important step is to store the identifier of the message returned by `msg::send()`, allowing the `handle_reply()` function to identify which message received a response; +4. Finally, send a reply message indicating that the message was sent to the second program. ```rust #[no_mangle] extern "C" fn handle() { - let message: String = msg::load().expect("Unable to decode"); - let program = unsafe { PROGRAM.as_mut().expect("The program is not initialized")}; - let msg_id = msg::send(program.echo_address, message, 0).expect("Error in sending a message"); - program.msg_id_to_actor = (msg_id, msg::source()); - msg::reply("Sent to echo address", 0).expect("Error in sending a reply"); + let action: Action = msg::load().expect("Unable to decode "); + let session = unsafe { SESSION.as_mut().expect("The session is not initialized") }; + let msg_id = msg::send(session.second_program, action, 0).expect("Error in sending a message"); + session.msg_id_to_actor = (msg_id, msg::source()); + msg::reply(Event::MessageSent, 0).expect("Error in sending a reply"); } ``` -The Gear program processes the reply to the message using the `handle_reply` function, so let's now look at how to handle the response message from the "echo" program: +The Gear program handles the reply to the message using the `handle_reply()` function. Now, let's examine how to manage the response message from the second program: -1. Using the `msg::reply_to()` function to get the identifier of the message for which the `handle_reply` function is called; -2. Check that the message identifier is the same as the identifier of the message that was sent from the `handle()` function, in order to find out that the response came to that particular message; -3. At the end a reply message is sent to the sender's address(). +1. Utilize the `msg::reply_to()` function to obtain the identifier of the message for which the `handle_reply()` function is invoked. +2. Verify that the message identifier matches the identifier of the message sent from the `handle()` function, ensuring that the response corresponds to that specific message. +3. Finally, send a reply message to the sender's address. -It is important to emphasize that calling `msg::reply()` inside the `handle_reply` function is not allowed. +**It is important to emphasize that calling `msg::reply()` inside the `handle_reply` function is not allowed.** ```rust #[no_mangle] extern "C" fn handle_reply() { let reply_message_id = msg::reply_to().expect("Failed to query reply_to data"); - let program = unsafe { PROGRAM.as_mut().expect("The program is not initialized") }; - let (msg_id, actor) = program.msg_id_to_actor; - if reply_message_id == msg_id{ - let reply_message: String = msg::load().expect("Unable to decode "); - msg::send(actor, reply_message, 0).expect("Error in sending a message"); + let session = unsafe { SESSION.as_mut().expect("The session is not initialized") }; + let (msg_id, actor) = session.msg_id_to_actor; + if reply_message_id == msg_id { + let reply: Event = msg::load().expect("Unable to decode "); + msg::send(actor, reply, 0).expect("Error in sending a message"); } - } ``` -Just a reminder that the sender of the message will receive two messages: -- the first is the message that is sent from the `handle()` function that the message has been sent to the second program; -- the second message will come from `handle_reply` function with the response of the second program. +Just a reminder that the sender of the message will receive two messages: +- The first message is sent from the `handle()` function to indicate that the message has been sent to the second program. +- The second message will come from the `handle_reply()` function with the response from the second program. + +## Second program +The first program is straightforward; it can accept various variants of actions and respond to them with corresponding events. These responses can range from simple replies such as `Action::HowAreYou` and `Event::Fine` to more complex logic, such as generating a random number. +```rust +#![no_std] +use gstd::{exec, msg, Encode, Decode, TypeInfo}; + +static mut SEED: u8 = 0; + +#[derive(TypeInfo, Encode, Decode)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] +pub enum Action { + Hello, + HowAreYou, + MakeRandomNumber{ + range: u8, + }, +} + +#[derive(TypeInfo, Encode, Decode)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] +pub enum Event { + Hello, + Fine, + Number(u8), +} + +#[no_mangle] +extern "C" fn handle() { + let action: Action = msg::load().expect("Error in decode message"); + let reply = match action { + Action::Hello => Event::Hello, + Action::HowAreYou => Event::Fine, + Action::MakeRandomNumber {range} => { + let seed = unsafe { SEED }; + unsafe { SEED = SEED.wrapping_add(1) }; + let mut random_input: [u8; 32] = exec::program_id().into(); + random_input[0] = random_input[0].wrapping_add(seed); + let (random, _) = exec::random(random_input).expect("Error in getting random number"); + Event::Number(random[0] % range) + } + }; + msg::reply(reply, 0).expect("Error in sending a reply"); +} +``` diff --git a/docs/03-intermediate/02-message-receiving/testing_handle_reply.md b/docs/03-intermediate/02-message-receiving/testing_handle_reply.md index 5c8d806..1a6f1b5 100644 --- a/docs/03-intermediate/02-message-receiving/testing_handle_reply.md +++ b/docs/03-intermediate/02-message-receiving/testing_handle_reply.md @@ -12,38 +12,41 @@ The first thing to do is to create a testing environment: let system = System::new(); ``` -Get program of the root crate with provided `system` and `echo` program instance from wasm file. +Get first program of the root crate with provided `system` and the second program instance from wasm file. ```rust -let program = Program::current(&system); -let echo_program = Program::from_file(&system, "target/wasm32-unknown-unknown/debug/echo.opt.wasm"); +let first_program = Program::current(&system); +let second_program = Program::from_file(&system, "target/wasm32-unknown-unknown/release/second_program.opt.wasm"); ``` -Initialize the "echo" program by sending an empty message and then initialize the main program by passing the address of the echo program to it. +Initialize the second program by sending an empty message and then initialize the first program by passing the address of the second program to it. ```rust -let echo_result = echo.send_bytes(USER, []); -let echo_address: ActorId = ECHO_ADDRESS.into(); -let res = program.send(USER, echo_address); +let result = second_program.send_bytes(USER, []); +let second_program_address: ActorId = SECOND_PROGRAM_ADDRESS.into(); +let res = first_program.send(USER, second_program_address); ``` -Send the message "Hello" to the main program and check for the response "Sent to echo address", which means that the message was successfully sent to the echo program address; +Send the message with `Action::MakeRandomNumber {range: 1}` to the first program and check for the response `Event::MessageSent`, which means that the message was successfully sent to the second program address; ```rust -let result = program.send(USER, "Hello".to_string()); +let result = first_program.send(USER, Action::MakeRandomNumber {range: 1}); let log = Log::builder() .source(1) .dest(3) - .payload("Sent to echo address".to_string()); + .payload(Event::MessageSent); +assert!(result.contains(&log)); + ``` -Extract the user's mailbox with the specified identifier and check that the "Hello" reply message has been sent back to the user. +Retrieve the user's mailbox with the specified ID and verify that a reply message has been sent back to the user ```rust let mailbox = system.get_mailbox(USER); let log = Log::builder() .source(1) .dest(3) - .payload("HELLO".to_string()); + .payload(Event::Number(0)); +assert!(mailbox.contains(&log)); ``` The complete test code looks as follows: @@ -51,38 +54,39 @@ The complete test code looks as follows: ```rust use gstd::ActorId; use gtest::{Log, Program, System}; +use handle_reply_io::{Action, Event}; const USER: u64 = 3; -const ECHO_ADDRESS: u64 = 2; +const SECOND_PROGRAM_ADDRESS: u64 = 2; #[test] -fn test() { +fn success_test() { // Create a new testing environment. let system = System::new(); - // Get program of the root crate with provided system. - let program = Program::current(&system); - // Get "echo" program - let echo = Program::from_file(&system, "target/wasm32-unknown-unknown/debug/echo.opt.wasm"); - // The "echo" program is initialized with an empty payload message - let echo_result = echo.send_bytes(USER, []); - assert!(!echo_result.main_failed()); - - let echo_address: ActorId = ECHO_ADDRESS.into(); - // The program is initialized using echo_address in the payload message - let res = program.send(USER, echo_address); + // Get first program of the root crate with provided system. + let first_program = Program::current(&system); + // Get second program + let second_program = Program::from_file(&system, "target/wasm32-unknown-unknown/release/second_program.opt.wasm"); + // The second program is initialized with an empty payload message + let result = second_program.send_bytes(USER, []); + assert!(!result.main_failed()); + + let second_program_address: ActorId = SECOND_PROGRAM_ADDRESS.into(); + // The first program is initialized using second_program in the payload message + let res = first_program.send(USER, second_program_address); assert!(!res.main_failed()); - // Send the message wanted to receive in reply - let result = program.send(USER, "HELLO".to_string()); + // Send with the message we want to receive back + let result = first_program.send(USER, Action::MakeRandomNumber {range: 1}); assert!(!result.main_failed()); // check that the first message has arrived, - // which means that the message was successfully sent to the "echo" program + // which means that the message was successfully sent to the second program let log = Log::builder() .source(1) .dest(3) - .payload("Sent to echo address".to_string()); + .payload(Event::MessageSent); assert!(result.contains(&log)); // check that the second message has arrived at the mailbox, @@ -91,8 +95,10 @@ fn test() { let log = Log::builder() .source(1) .dest(3) - .payload("HELLO".to_string()); + .payload(Event::Number(0)); assert!(mailbox.contains(&log)); } ``` + +It will be good practice if you implement these programmes and test on your own. diff --git a/docs/03-intermediate/03-wait-wake-system/handle-reply-wait-wake.md b/docs/03-intermediate/03-wait-wake-system/handle-reply-wait-wake.md index 6d0b828..d1457fb 100644 --- a/docs/03-intermediate/03-wait-wake-system/handle-reply-wait-wake.md +++ b/docs/03-intermediate/03-wait-wake-system/handle-reply-wait-wake.md @@ -5,94 +5,97 @@ hide_table_of_contents: true # Handle reply with wait() and wake() -Now let's use the knowledge about `wait()`/`wake()` functions and try to improve the program that was shown in the previous lesson. +Now let's use the knowledge about `exec::wait()`/`exec::wake()` functions and try to improve the program that was shown in the previous lesson. ![gif 2](../img/03/wait_wake.gif) -As you may observe, the user will no longer receive two separate messages; instead, a single reply will be dispatched once the entire process is finalized. +As you can see, the user will no longer receive two separate messages; instead, a single reply will be sent at the end of the entire process. -## Main program +## First program -Since the "echo" program remains unchanged, let's proceed directly to the consideration of changes in the main program:: +As the second programme remains unchanged, let's proceed directly to examining the modifications in the first program: ```rust type MessageSentId = MessageId; type OriginalMessageId = MessageId; -struct Program { - echo_address: ActorId, +struct Session { + second_program: ActorId, msg_ids: (MessageSentId, OriginalMessageId), - status: Status, + message_status: MessageStatus, } ``` New fields have been created - `msg_ids` — a tuple consisting of two elements: MessageSentId and OriginalMessageId; - - `MessageSentId` - identifier of the message to be sent to the echo address; - - `OriginalMessageId` - identifier of the message to be sent to the main program (required for using the wake() function). -- `status` - program status (required to track program activity stages). + - `MessageSentId` - identifier of the message to be sent to the second program address; + - `OriginalMessageId` - identifier of the message to be sent to the first program (required for using the wake() function). +- `message_status` - session status (required to track session activity stages). ```rust -enum Status { +enum MessageStatus { Waiting, Sent, - Received(String), + Received(Event), } ``` -- `Waiting` — the program is waiting for a message; -- `Sent` - the program has sent a message to the "echo" program and has not yet received a response; -- `Received(String)` - the program received a reply to a message. +- `Waiting` — the session is in a waiting state; +- `Sent` - the intermediate state of the session in which the message was sent to the second program, but the response has not yet been received; +- `Received(String)` - the session state in which the reply message was received. Considering the new fields in the program structure, initialization appears as follows: ```rust #[no_mangle] -extern fn init() { - let echo_address = msg::load().expect("Unable to decode Init"); +extern "C" fn init() { + let second_program = msg::load().expect("Unable to decode Init"); unsafe { - PROGRAM = Some(Program { - echo_address, + SESSION = Some(Session { + second_program, msg_ids: (MessageId::zero(), MessageId::zero()), - status: Status::Waiting + message_status: MessageStatus::Waiting, }); } } ``` -This time let's include debugging in our program to understand the whole process of the program during testing +This time, let's incorporate debugging into our program to gain a comprehensive understanding of the entire process. ```rust #[no_mangle] extern "C" fn handle() { debug!("!!!! HANDLE !!!!"); debug!("Message ID: {:?}", msg::id()); - let message: String = msg::load().expect("Unable to decode "); - let program = unsafe { PROGRAM.as_mut().expect("The program is not initialized") }; - - // match status - match &program.status { - Status::Received(reply_message) => { - debug!("HANDLE: Status::Received"); - msg::reply(reply_message, 0).expect("Error in sending a reply"); - debug!("HANDLE: Status::Waiting"); - program.status = Status::Waiting; - } - Status::Waiting | Status::Sent => { - debug!("HANDLE: Status != Received"); - let msg_id = msg::send(program.echo_address, message.clone(), 0) + let action: Action = msg::load().expect("Unable to decode `Action`"); + debug!("Message payload: {:?}", action); + let session = unsafe { SESSION.as_mut().expect("The session is not initialized") }; + + // match message_status + match &session.message_status { + MessageStatus::Waiting => { + debug!("HANDLE: MessageStatus::Waiting"); + let msg_id = msg::send(session.second_program, action, 0) .expect("Error in sending a message"); - debug!("HANDLE: Status::Sent"); - program.status = Status::Sent; - program.msg_ids = (msg_id, msg::id()); + debug!("HANDLE: MessageStatus::Sent"); + session.message_status = MessageStatus::Sent; + session.msg_ids = (msg_id, msg::id()); debug!("HANDLE: WAIT"); exec::wait(); } + MessageStatus::Sent => { + debug!("HANDLE: MessageStatus::Sent"); + msg::reply(Event::MessageAlreadySent, 0).expect("Error in sending a reply"); + } + MessageStatus::Received(reply_message) => { + debug!("HANDLE: MessageStatus::Received({:?})", reply_message); + msg::reply(reply_message, 0).expect("Error in sending a reply"); + session.message_status = MessageStatus::Waiting; + } } debug!("HANDLE: END"); } - ``` -At the beginning, as you may have noticed, the program is in a `Status::Waiting` status, and when a `match` occurs, the code moves to the second variant. The program sends a message, sets the program status to `Status::Sent` and saves the identifiers of the current message and the sent message. After all this call the function `exec::wait()` function, which pauses the code and adds the current message to the waiting list until `exec::wake(message_id)` is called or the gas runs out. The message identifier is passed to the `exec::wake()` function, which is why `msg::id()` is stored in `program.msg_ids`. +At the very beginning, as you may have noticed, the session is in `MessageStatus::Waiting` state. When a match occurs, the code switches to the first option. The program sends a message, sets the session status to `MessageStatus::Sent` and records the identifiers of the current and sent message. Then `exec::wait()` is called, which pauses message processing and adds the current message to the waiting list until `exec::wake(message_id)` is called or the gas runs out. Subsequent waking of a message requires its id, so `msg::id()` is stored in `session.msg_ids`. Let's move on to the `handle_reply()` function: @@ -100,37 +103,37 @@ Let's move on to the `handle_reply()` function: #[no_mangle] extern "C" fn handle_reply() { debug!("HANDLE_REPLY"); - let reply_message: String = msg::load().expect("Unable to decode "); let reply_to = msg::reply_to().expect("Failed to query reply_to data"); - let program = unsafe { PROGRAM.as_mut().expect("The program is not initialized") }; + let session = unsafe { SESSION.as_mut().expect("The session is not initialized") }; - if reply_to == program.msg_ids.0 && program.status == Status::Sent { - debug!("HANDLE_REPLY: Status::Received"); - program.status = Status::Received(reply_message); - let original_message_id = program.msg_ids.1; + if reply_to == session.msg_ids.0 && session.message_status == MessageStatus::Sent { + let reply_message: Event = msg::load().expect("Unable to decode `Event`"); + debug!("HANDLE_REPLY: MessageStatus::Received {:?}", reply_message); + session.message_status = MessageStatus::Received(reply_message); + let original_message_id = session.msg_ids.1; debug!("HANDLE: WAKE"); exec::wake(original_message_id).expect("Failed to wake message"); } } ``` -Сondition `if reply_to == program.msg_ids.0 && program.status == Status::Sent` gives a guarantee that the expected message has arrived and arrived at the right moment, i.e. at the correct program status. -After that the status is set to `Status::Received(reply_message)` and the response message is saved; get the ID of the original message and call the `exec::wake()` function, which retrieves the message from the waiting list and the suspended message is restarted in `handle()`. +Сondition `if reply_to == session.msg_ids.0 && session.message_status == MessageStatus::Sent` gives a guarantee that the expected message has arrived and arrived at the right moment, i.e. at the correct session status. +After that the status is set to `MessageStatus::Received(reply_message)` and the response message is saved. The ID of the original message is retrieved, and the `exec::wake()` function is called. This function retrieves the message from the waiting list, and the suspended message is resumed in the `handle()` function. -*Important note*: when `exec::wake()` is called, the message is taken from the waiting list, it returns to the `handle()` entrypoint, and the program starts processing it from the beginning, i.e. the program code will get into the `match` again: +*Important note*: when `exec::wake()` is called, the message is taken from the waiting list, it returns to the `handle()` entrypoint, and message processing will start handling the message from the beginning, i.e. the program code will get into the `match` again: ```rust // ... -match &program.status { - Status::Received(reply_message) => { - debug!("HANDLE: Status::Received"); +match &session.message_status { + // ... + MessageStatus::Received(reply_message) => { + debug!("HANDLE: MessageStatus::Received({:?})", reply_message); msg::reply(reply_message, 0).expect("Error in sending a reply"); - debug!("HANDLE: Status::Waiting"); - program.status = Status::Waiting; + session.message_status = MessageStatus::Waiting; } // ... ``` -However, this time it will go into the first variant, send a response and set the status to `Status::Waiting`. +However, this time it will go into the third variant, send a response and set the status to `MessageStatus::Waiting`. Now, let's examine this process as a whole: diff --git a/docs/03-intermediate/03-wait-wake-system/testing_wait_for.md b/docs/03-intermediate/03-wait-wake-system/testing_wait_for.md index 03d7d57..63d461c 100644 --- a/docs/03-intermediate/03-wait-wake-system/testing_wait_for.md +++ b/docs/03-intermediate/03-wait-wake-system/testing_wait_for.md @@ -10,32 +10,41 @@ Let's use the function `system.spend_blocks()`, which allows to spend blocks and ```rust use gstd::ActorId; use gtest::{Log, Program, System}; +use wait_for_io::{Event, Action}; const USER: u64 = 3; -const ECHO_ADDRESS: u64 = 2; +const SECOND_PROGRAM_ADDRESS: u64 = 2; #[test] fn test() { let system = System::new(); system.init_logger(); - let program = Program::current(&system); - let echo_program = Program::from_file(&system, "target/wasm32-unknown-unknown/debug/echo.opt.wasm"); + let first_program = Program::current(&system); + let second_program = Program::from_file(&system, "target/wasm32-unknown-unknown/debug/second_program.opt.wasm"); - let result = echo_program.send_bytes(USER, []); + let result = second_program.send_bytes(USER, []); assert!(!result.main_failed()); - let echo_address: ActorId = ECHO_ADDRESS.into(); - let result = program.send(USER, echo_address); + let second_program_address: ActorId = SECOND_PROGRAM_ADDRESS.into(); + let result = first_program.send(USER, second_program_address); assert!(!result.main_failed()); - let result = program.send(USER, "Hello".to_string()); + let result = first_program.send(USER, Action::MakeRandomNumber{range: 1}); assert!(!result.main_failed()); + let result = first_program.send(USER, Action::MakeRandomNumber{range: 1}); + let log = Log::builder() + .source(1) + .dest(3) + .payload(Event::MessageAlreadySent); + assert!(result.contains(&log)); + let result = system.spend_blocks(3); + let log = Log::builder() .source(1) .dest(3) - .payload("No response was received".to_string()); + .payload(Event::NoReplyReceived); assert!(result[0].contains(&log)); } @@ -44,13 +53,21 @@ fn test() { Upon running the test, you will encounter the following debug messages. Examine them attentively to ensure that the program executed as intended. ```console -[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: !!!! START HANDLE !!!! +[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: !!!! HANDLE !!!! [DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: Message ID: MessageId([15, 200, 69, 247, 219, 197, 228, 169, 112, 34, 221, 58, 40, 159, 140, 193, 139, 19, 23, 77, 44, 107, 107, 94, 184, 209, 74, 155, 13, 80, 206, 217]) -[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: Status != Received -[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: Status::Sent +[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: Message payload: MakeRandomNumber { range: 1 } +[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: MessageStatus::Waiting +[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: MessageStatus::Sent [DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: WAIT -[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: !!!! START HANDLE !!!! +[DEBUG test] [handle(0xd560..12fb)] 0x0100..0000: !!!! HANDLE !!!! +[DEBUG test] [handle(0xd560..12fb)] 0x0100..0000: Message ID: MessageId([213, 96, 108, 153, 11, 175, 246, 203, 166, 249, 165, 69, 253, 140, 44, 138, 82, 194, 230, 50, 196, 117, 66, 218, 223, 197, 172, 150, 125, 82, 18, 251]) +[DEBUG test] [handle(0xd560..12fb)] 0x0100..0000: Message payload: MakeRandomNumber { range: 1 } +[DEBUG test] [handle(0xd560..12fb)] 0x0100..0000: HANDLE: Event::MessageAlreadySent +[DEBUG test] [handle(0xd560..12fb)] 0x0100..0000: HANDLE: END +[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: !!!! HANDLE !!!! [DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: Message ID: MessageId([15, 200, 69, 247, 219, 197, 228, 169, 112, 34, 221, 58, 40, 159, 140, 193, 139, 19, 23, 77, 44, 107, 107, 94, 184, 209, 74, 155, 13, 80, 206, 217]) +[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: Message payload: MakeRandomNumber { range: 1 } [DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: No response was received +[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: MessageStatus::Waiting [DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: END ``` diff --git a/docs/03-intermediate/03-wait-wake-system/testing_wait_wake.md b/docs/03-intermediate/03-wait-wake-system/testing_wait_wake.md index c512b47..21cc2ff 100644 --- a/docs/03-intermediate/03-wait-wake-system/testing_wait_wake.md +++ b/docs/03-intermediate/03-wait-wake-system/testing_wait_wake.md @@ -10,31 +10,34 @@ Let's use `system.init_logger()` to start the environment in debug mode and see ```rust use gstd::ActorId; use gtest::{Log, Program, System}; +use wait_wake_io::{Action, Event}; const USER: u64 = 3; -const ECHO_ADDRESS: u64 = 2; +const SECOND_PROGRAM_ADDRESS: u64 = 2; #[test] fn test() { let system = System::new(); system.init_logger(); - let program = Program::current(&system); - let echo_program = Program::from_file(&system, "target/wasm32-unknown-unknown/debug/echo.opt.wasm"); + let first_program = Program::current(&system); + let second_program = Program::from_file(&system, "target/wasm32-unknown-unknown/debug/second_program.opt.wasm"); - let result = echo_program.send_bytes(USER, []); + let result = second_program.send_bytes(USER, []); assert!(!result.main_failed()); - let echo_address: ActorId = ECHO_ADDRESS.into(); - let result = program.send(USER, echo_address); + + let second_program_address: ActorId = SECOND_PROGRAM_ADDRESS.into(); + let result = first_program.send(USER, second_program_address); assert!(!result.main_failed()); - let result = program.send(USER, "Hello".to_string()); + let result = first_program.send(USER, Action::MakeRandomNumber{range: 1}); assert!(!result.main_failed()); let log = Log::builder() .source(1) .dest(3) - .payload("Hello".to_string()); + .payload(Event::Number(0)); assert!(result.contains(&log)); + } ``` @@ -43,15 +46,16 @@ Upon running the test, you will encounter the following debug messages. Examine ```console [DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: !!!! HANDLE !!!! [DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: Message ID: MessageId([15, 200, 69, 247, 219, 197, 228, 169, 112, 34, 221, 58, 40, 159, 140, 193, 139, 19, 23, 77, 44, 107, 107, 94, 184, 209, 74, 155, 13, 80, 206, 217]) -[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: Status != Received -[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: Status::Sent +[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: Message payload: MakeRandomNumber { range: 1 } +[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: MessageStatus::Waiting +[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: MessageStatus::Sent [DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: WAIT [DEBUG test] [handle(0x0547..16ea)] 0x0100..0000: HANDLE_REPLY -[DEBUG test] [handle(0x0547..16ea)] 0x0100..0000: HANDLE_REPLY: Status::Received +[DEBUG test] [handle(0x0547..16ea)] 0x0100..0000: HANDLE_REPLY: MessageStatus::Received Number(0) [DEBUG test] [handle(0x0547..16ea)] 0x0100..0000: HANDLE: WAKE [DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: !!!! HANDLE !!!! [DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: Message ID: MessageId([15, 200, 69, 247, 219, 197, 228, 169, 112, 34, 221, 58, 40, 159, 140, 193, 139, 19, 23, 77, 44, 107, 107, 94, 184, 209, 74, 155, 13, 80, 206, 217]) -[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: Status::Received -[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: Status::Waiting +[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: Message payload: MakeRandomNumber { range: 1 } +[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: MessageStatus::Received(Number(0)) [DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: END ``` diff --git a/docs/03-intermediate/03-wait-wake-system/wait_for.md b/docs/03-intermediate/03-wait-wake-system/wait_for.md index 4f774f4..2b900db 100644 --- a/docs/03-intermediate/03-wait-wake-system/wait_for.md +++ b/docs/03-intermediate/03-wait-wake-system/wait_for.md @@ -10,60 +10,68 @@ Sometimes the second program may not answer for any reason and it is necessary t ## wait_for() Let's try to solve this problem using `wait_for(DURATION_IN_BLOCK)`. -Reminder: this function itself wakes up the message from the waiting list after a certain number of blocks, if `exec::wake()` was not called. +*Reminder: this function itself wakes up the message from the waiting list after a certain number of blocks, if `exec::wake()` was not called.* -## Echo program +## Second program -Let's add `exec::wait()` so that the echo program does not reply to incoming messages: +Let's add `exec::wait()` so that the second program does not reply to incoming messages: ```rust #[no_mangle] extern "C" fn handle() { exec::wait(); - msg::reply_input(0, 0..msg::size()).expect("Error in sending a reply"); -} + let action: Action = msg::load().expect("Error in decode message"); + // ... ``` -## Main program +## First program -In the main program `handle()` function will be modified instead of `exec::wait()` let's use `exec::wait_for(3)`, so if `exec::wake()`, which is in `handle_reply()`, is not called within 3 blocks, the program will wake itself up. +In the first program, the `handle()` function will be modified. Instead of `exec::wait()`, we will use `exec::wait_for(3)`. This ensures that if `exec::wake()` in `handle_reply()` is not called within 3 blocks, message processing will automatically resume. ```rust #[no_mangle] extern "C" fn handle() { debug!("!!!! HANDLE !!!!"); debug!("Message ID: {:?}", msg::id()); - let message: String = msg::load().expect("Unable to decode "); - let program = unsafe { PROGRAM.as_mut().expect("The program is not initialized") }; + let action: Action = msg::load().expect("Unable to decode `Action`"); + debug!("Message payload: {:?}", action); + let session = unsafe { SESSION.as_mut().expect("The session is not initialized") }; - // match status - match &program.status { - Status::Received(reply_message) => { - debug!("HANDLE: Status::Received"); + // match message_status + match &session.message_status { + MessageStatus::Received(reply_message) => { + debug!("HANDLE: MessageStatus::Received"); msg::reply(reply_message, 0).expect("Error in sending a reply"); - debug!("HANDLE: Status::Waiting"); - program.status = Status::Waiting; + debug!("HANDLE: MessageStatus::Waiting"); + session.message_status = MessageStatus::Waiting; } - Status::Waiting => { - debug!("HANDLE: Status != Received"); - let msg_id = msg::send(program.echo_address, message.clone(), 0) + MessageStatus::Waiting => { + debug!("HANDLE: MessageStatus::Waiting"); + let msg_id = msg::send(session.second_program, action, 0) .expect("Error in sending a message"); - debug!("HANDLE: Status::Sent"); - program.status = Status::Sent; - program.msg_ids = (msg_id, msg::id()); + debug!("HANDLE: MessageStatus::Sent"); + session.message_status = MessageStatus::Sent; + session.msg_ids = (msg_id, msg::id()); debug!("HANDLE: WAIT"); exec::wait_for(3); } - Status::Sent => { - debug!("HANDLE: No response was received"); - msg::reply("No response was received", 0).expect("Error in sending a reply"); + MessageStatus::Sent => { + if msg::id() == session.msg_ids.1 { + debug!("HANDLE: No response was received"); + msg::reply(Event::NoReplyReceived, 0).expect("Error in sending a reply"); + debug!("HANDLE: MessageStatus::Waiting"); + session.message_status = MessageStatus::Waiting; + } else { + debug!("HANDLE: Event::MessageAlreadySent"); + msg::reply(Event::MessageAlreadySent, 0).expect("Error in sending a reply"); + } } } debug!("HANDLE: END"); } ``` -In this case the program will go to `handle()` again, but this time the status will be `Status::Sent`, so a message will be sent to the user that there was no response and the status will be set to `Status::Waiting`. +In this case the program will go to `handle()` again, but this time the session status will be `MessageStatus::Sent`, so a message will be sent to the user that there was no response and the status will be set to `MessageStatus::Waiting`. diff --git a/docs/03-intermediate/03-wait-wake-system/wait_wake.md b/docs/03-intermediate/03-wait-wake-system/wait_wake.md index 45bd0a6..0165b7c 100644 --- a/docs/03-intermediate/03-wait-wake-system/wait_wake.md +++ b/docs/03-intermediate/03-wait-wake-system/wait_wake.md @@ -5,16 +5,17 @@ hide_table_of_contents: true # wait() and wake() -In order to improve the handling of messages in the Gear program, the use of the `exec::wait()` and `exec::wake()` functions can be beneficial: +In order to enhance message handling in the Gear program, utilizing the `exec::wait()` and `exec::wake()` functions can be advantageous: ## Wait -`exec::wait()`: pause the current message handling. It completes the current message handle execution with a special result and puts this message into the waiting queue. The message can then be awakened using the corresponding `exec::wake()` function at a later time. -When a message goes into the waiting state using the `exec::wait()` function, all the changes made by the program before the wait call are saved. These changes are stored in the program's persistent storage, so they are not lost when the current message handling is paused. -When a message is waiting, all gas that hasn't been spent yet is attributed to that message in the waiting queue. If the gas runs out while the message is waiting, it will not be awakened, and the program can be stuck in an intermediate state. This is very important to realize that each block of a message's presence in the queue is charged for in the form of some amount of gas. + +`exec::wait()`: Pauses the current message handling process, completing it with a special result and placing the message into the waiting queue. The message can later be awakened using the corresponding `exec::wake()` function. When a message enters the waiting state with `exec::wait()`, all changes made by the program before the wait call are preserved. These changes are stored in the program's persistent storage, ensuring they are not lost when message handling is paused. While a message is in the waiting state, all remaining gas is allocated to that message in the waiting queue. If the gas is depleted while the message is waiting, it will not be awakened, potentially causing the program to become stuck in an intermediate state. It is crucial to understand that each block of a message's presence in the queue incurs a charge in the form of gas. Therefore, Gear also provides the ability to enter a waiting state for a certain number of blocks using the `exec::wait_for` and `exec::wait_up_to` functions: -- `exec::wait_for(duration)`: The message waits for the specified number of blocks, and after that time, it wakes up itself if it hasn't been woken up before. If the message doesn't have enough gas to pay for the specified number of blocks, then the function panics; -- `exec::wait_up_to(duration)`: The message waits for the number of blocks that it has enough gas to pay for, and this number of blocks does not exceed the specified duration. + +- `exec::wait_for(duration)`: With this function, the message pauses and waits for a specific number of blocks to pass. After this time has elapsed, if the message hasn't already been awakened, it will wake up on its own. However, if the message doesn't have enough gas to cover the cost of waiting for the specified number of blocks, the function will cause an panic. + +- `exec::wait_up_to(duration)`: This function allows the message to wait for a duration of time, but it only waits for as many blocks as it has enough gas to pay for. It ensures that the waiting time does not exceed the specified duration. ## Wake `exec::exec::wake()`: Resume the execution of a message that was previously paused using the wait function. When the wake function is called with a valid message_id, it will take the message out of the waiting queue and put it back into the processing queue. diff --git a/docs/03-intermediate/04-delayed-message/delayed-message-example.md b/docs/03-intermediate/04-delayed-message/delayed-message-example.md index 34686ee..33be414 100644 --- a/docs/03-intermediate/04-delayed-message/delayed-message-example.md +++ b/docs/03-intermediate/04-delayed-message/delayed-message-example.md @@ -7,27 +7,49 @@ hide_table_of_contents: true Let's try to rewrite the program from the previous lesson using the acquired knowledge about delayed messages. -Add enum to distinguish between the two message types +In this case there will be two types of message: ```rust -#[derive(TypeInfo, Encode, Decode)] +#[derive(TypeInfo, Encode, Decode, Debug)] #[codec(crate = gstd::codec)] #[scale_info(crate = gstd::scale_info)] pub enum Action{ - SendMessage(String), - CheckReply, + SendMessage(MessageAction), // action to send a message to the second program + CheckReply, // action to check for a response +} + +#[derive(TypeInfo, Encode, Decode, Debug)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] +pub enum MessageAction { + Hello, + HowAreYou, + MakeRandomNumber{ + range: u8, + }, +} + +#[derive(TypeInfo, Encode, Decode, Debug, PartialEq)] +#[codec(crate = gstd::codec)] +#[scale_info(crate = gstd::scale_info)] +pub enum Event { + Hello, + Fine, + Number(u8), + MessageSent, + MessageAlreadySent, + WrongStatus, + NoReplyReceived, } ``` -- `SendMessage(String)` - action to send a message to the echo program; -- `CheckReply` - action to check for a response. Add `ActorId` to `msg_ids` to store the address of the sender of the message: ```rust -struct Program { - echo_address: ActorId, +struct Session { + second_program: ActorId, msg_ids: (MessageSentId, OriginalMessageId, ActorId), - status: Status, + message_status: MessageStatus, } ``` @@ -36,18 +58,18 @@ In this case the initialization will look as follows: ```rust #[no_mangle] extern "C" fn init() { - let echo_address = msg::load().expect("Unable to decode Init"); + let second_program = msg::load().expect("Unable to decode Init"); unsafe { - PROGRAM = Some(Program { - echo_address, + SESSION = Some(Session { + second_program, msg_ids: (MessageId::zero(), MessageId::zero(), ActorId::zero()), - status: Status::Waiting, + message_status: MessageStatus::Waiting, }); } } ``` -After sending an echo message to the program, send a delayed message `msg::send_delayed(exec::program_id(), Action::CheckReply, 0, 3)`, with a delay of three blocks. +After sending the message to the second program, send a delayed message `msg::send_delayed(exec::program_id(), Action::CheckReply, 0, 3)`, with a delay of three blocks. ```rust #[no_mangle] @@ -55,35 +77,37 @@ extern "C" fn handle() { debug!("!!!! HANDLE !!!!"); debug!("Message ID: {:?}", msg::id()); let action: Action = msg::load().expect("Unable to decode "); - let program = unsafe { PROGRAM.as_mut().expect("The program is not initialized") }; + debug!("Message payload: {:?}", action); + let session = unsafe { SESSION.as_mut().expect("The session is not initialized") }; match action { - Action::SendMessage(message) => { - if program.status == Status::Waiting { - debug!("HANDLE: Action::SendMessage -> Status::Waiting"); - let msg_id = msg::send(program.echo_address, message.clone(), 0) + Action::SendMessage(message_action) => { + if session.message_status == MessageStatus::Waiting { + debug!("HANDLE: Action::SendMessage and MessageStatus::Waiting"); + let msg_id = msg::send(session.second_program, message_action, 0) .expect("Error in sending a message"); - debug!("HANDLE: Status::Sent"); - program.status = Status::Sent; - program.msg_ids = (msg_id, msg::id(), msg::source()); + debug!("HANDLE: MessageStatus::Sent"); + session.message_status = MessageStatus::Sent; + session.msg_ids = (msg_id, msg::id(), msg::source()); msg::send_delayed(exec::program_id(), Action::CheckReply, 0, 3) .expect("Error in sending a message"); - msg::reply("Sent to echo address", 0).expect("Error in sending a reply"); + msg::reply(Event::MessageSent, 0).expect("Error in sending a reply"); } else { - panic!("Status is not Waiting"); + debug!("HANDLE: Event::WrongStatus"); + msg::reply(Event::WrongStatus, 0).expect("Error in sending a reply"); } } Action::CheckReply => { debug!("HANDLE: Action::CheckReply"); - if program.status == Status::Sent { + if session.message_status == MessageStatus::Sent && msg::source() == exec::program_id() { debug!("HANDLE: No response was received"); - msg::send(program.msg_ids.2, "No response was received", 0).expect("Error in sending a message"); - debug!("HANDLE: Status::Waiting"); - program.status = Status::Waiting; + msg::send(session.msg_ids.2, Event::NoReplyReceived, 0).expect("Error in sending a message"); + debug!("HANDLE: MessageStatus::Waiting"); + session.message_status = MessageStatus::Waiting; } } @@ -91,4 +115,4 @@ extern "C" fn handle() { debug!("HANDLE: END"); } ``` -When handle receives the same `Action::CheckReply` message, a check of the program status will be made. If the status is `Status::Sent`, which means that no response message has been received, a notification of this will be sent to the sender. +Upon receiving the `Action::CheckReply` message, the handler will inspect the session's status. If the message source is the program itself and the status is `Status::Sent`, a notification will be dispatched to the sender indicating the absence of a response message. diff --git a/docs/03-intermediate/04-delayed-message/delayed-message.md b/docs/03-intermediate/04-delayed-message/delayed-message.md index 14fe3c4..215ed2c 100644 --- a/docs/03-intermediate/04-delayed-message/delayed-message.md +++ b/docs/03-intermediate/04-delayed-message/delayed-message.md @@ -5,9 +5,9 @@ hide_table_of_contents: true # Delayed message -The usual way that smart contracts on other blockchains continue to function is by relying on external, centralized resources. This means that the code of these contracts will not run and make changes to the blockchain's state until it is triggered by an on-chain transaction. +The conventional approach adopted by programs on other blockchains involves dependency on external, centralized resources. Consequently, the execution of code within these programs and any corresponding alterations to the blockchain's state are contingent upon being triggered by an on-chain transaction. -The external transaction serves as a "poke" to activate the smart contract and initiate its logic. For instance, someone can start an auction by sending a message to the auction contract. When the auction time has passed, the contract will need to process the result of the auction. However, this will not happen until someone sends the appropriate message to the contract to trigger this action. +The external transaction functions as a trigger to activate the program and commence its logic. For example, an individual can initiate an auction by dispatching a message to the auction program. Following the expiration of the auction period, the program will require processing the auction's outcome. However, this processing will remain pending until an individual dispatches the appropriate message to the program, thereby initiating this action. Gear Protocol solves this issue by introducing delayed messaging functionality. The programs in Gear Networks are able to execute themselves an unlimited number of blocks, as long as enough gas for execution is kept available. As a result the need for including centralized components in dApps is eliminated, allowing them to function totally on-chain. diff --git a/docs/03-intermediate/04-delayed-message/testing-delayed-message.md b/docs/03-intermediate/04-delayed-message/testing-delayed-message.md index ed33047..de54553 100644 --- a/docs/03-intermediate/04-delayed-message/testing-delayed-message.md +++ b/docs/03-intermediate/04-delayed-message/testing-delayed-message.md @@ -10,32 +10,42 @@ Let's verify the functionality of the program discussed in the preceding section ```rust use gstd::ActorId; use gtest::{Log, Program, System}; -use delayed_message_io::Action; +use delayed_message_io::{Action, MessageAction, Event}; const USER: u64 = 3; -const ECHO_ADDRESS: u64 = 2; +const SECOND_PROGRAM_ADDRESS: u64 = 2; #[test] fn test() { let system = System::new(); system.init_logger(); - let program = Program::current(&system); - let echo_program = Program::from_file(&system, "target/wasm32-unknown-unknown/debug/echo.opt.wasm"); + let first_program = Program::current(&system); + let second_program = Program::from_file(&system, "target/wasm32-unknown-unknown/debug/second_program.opt.wasm"); - let result = echo_program.send_bytes(USER, []); + let result = second_program.send_bytes(USER, []); assert!(!result.main_failed()); - let echo_address: ActorId = ECHO_ADDRESS.into(); - let result = program.send(USER, echo_address); + let second_program_address: ActorId = SECOND_PROGRAM_ADDRESS.into(); + let result = first_program.send(USER, second_program_address); assert!(!result.main_failed()); - let result = program.send(USER, Action::SendMessage("Hello".to_string())); + let result = first_program.send(USER, Action::SendMessage(MessageAction::MakeRandomNumber{range: 1})); assert!(!result.main_failed()); let log = Log::builder() .source(1) .dest(3) - .payload("Sent to echo address".to_string()); + .payload(Event::MessageSent); + + assert!(result.contains(&log)); + + let result = first_program.send(USER, Action::SendMessage(MessageAction::MakeRandomNumber{range: 1})); + assert!(!result.main_failed()); + + let log = Log::builder() + .source(1) + .dest(3) + .payload(Event::WrongStatus); assert!(result.contains(&log)); @@ -45,7 +55,7 @@ fn test() { let log = Log::builder() .source(1) .dest(3) - .payload("No response was received".to_string()); + .payload(Event::NoReplyReceived); assert!(mailbox.contains(&log)); } @@ -56,14 +66,21 @@ The following debug messages will be displayed in the console: ```console [DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: !!!! HANDLE !!!! [DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: Message ID: MessageId([15, 200, 69, 247, 219, 197, 228, 169, 112, 34, 221, 58, 40, 159, 140, 193, 139, 19, 23, 77, 44, 107, 107, 94, 184, 209, 74, 155, 13, 80, 206, 217]) -[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: Action::SendMessage -> Status::Waiting -[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: Status::Sent +[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: Message payload: SendMessage(MakeRandomNumber { range: 1 }) +[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: Action::SendMessage and MessageStatus::Waiting +[DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: MessageStatus::Sent [DEBUG test] [handle(0x0fc8..ced9)] 0x0100..0000: HANDLE: END +[DEBUG test] [handle(0xd560..12fb)] 0x0100..0000: !!!! HANDLE !!!! +[DEBUG test] [handle(0xd560..12fb)] 0x0100..0000: Message ID: MessageId([213, 96, 108, 153, 11, 175, 246, 203, 166, 249, 165, 69, 253, 140, 44, 138, 82, 194, 230, 50, 196, 117, 66, 218, 223, 197, 172, 150, 125, 82, 18, 251]) +[DEBUG test] [handle(0xd560..12fb)] 0x0100..0000: Message payload: SendMessage(MakeRandomNumber { range: 1 }) +[DEBUG test] [handle(0xd560..12fb)] 0x0100..0000: HANDLE: Event::WrongStatus +[DEBUG test] [handle(0xd560..12fb)] 0x0100..0000: HANDLE: END [DEBUG test] [handle(0x058e..8c20)] 0x0100..0000: !!!! HANDLE !!!! [DEBUG test] [handle(0x058e..8c20)] 0x0100..0000: Message ID: MessageId([5, 142, 36, 249, 201, 59, 160, 201, 197, 250, 222, 94, 146, 206, 46, 134, 97, 7, 223, 108, 216, 36, 33, 151, 82, 49, 103, 150, 120, 114, 140, 32]) +[DEBUG test] [handle(0x058e..8c20)] 0x0100..0000: Message payload: CheckReply [DEBUG test] [handle(0x058e..8c20)] 0x0100..0000: HANDLE: Action::CheckReply [DEBUG test] [handle(0x058e..8c20)] 0x0100..0000: HANDLE: No response was received -[DEBUG test] [handle(0x058e..8c20)] 0x0100..0000: HANDLE: Status::Waiting +[DEBUG test] [handle(0x058e..8c20)] 0x0100..0000: HANDLE: MessageStatus::Waiting [DEBUG test] [handle(0x058e..8c20)] 0x0100..0000: HANDLE: END ``` diff --git a/docs/03-intermediate/05-homework/assignment.md b/docs/03-intermediate/05-homework/assignment.md index 0fdc29f..213115d 100644 --- a/docs/03-intermediate/05-homework/assignment.md +++ b/docs/03-intermediate/05-homework/assignment.md @@ -7,15 +7,39 @@ hide_table_of_contents: true ## Task Description -In this homework you are to write the Wordle Game. The games rules are the following: +In this homework you are to write the Wordle Game. -- There are two players: **User** and **Program**; -- The program generates a word, and the user must try to find the correct word; -- The program also indicates in which positions the letters are positioned correctly and in which positions they are not. +Wordle is an engaging word-guessing game that has gained popularity due to its simplicity and addictive gameplay. The objective of the game is to guess the hidden word within a limited number of attempts. + +The game board consists of six rows, each allowing the player to input a word. Upon entering a word, the player receives feedback indicating whether a letter is present in the hidden word and if it's in the correct position. If a letter is present but in the wrong position, it may be displayed in a different color or with a distinct symbol to indicate this. Players can use these hints to deduce which letters to include in the word and where to place them. + +Players must rely on their knowledge and intuition to guess the word within the fewest attempts possible. The time constraint and limited number of attempts make the game exhilarating and suspenseful. ## Project Structure -Let's divide the game program into two parts: one will handle word generation and indicating the correct positions of the letters, while the other will receive, process requests from the first part, and record the results for the user. +In order to create a dynamic and interactive word-guessing game, we propose dividing the gaming process into two distinct programs. The first program will manage the core functionalities of the game, including selecting a random word from a predetermined bank and evaluating user guesses. Meanwhile, the second program will act as a coordinator, handling user interactions, maintaining game state, and managing time constraints. By dividing the gameplay between two programs, we aim to create a modular and flexible system that enhances the overall gaming experience. Let's delve deeper into the functionalities and interactions of these two programs. + +1. **Description of the First Program**: + - The first program contains two main functions: "start the game" and "check the word." + - Inside the program, there's a word bank from which a random word is chosen at the beginning of the game. + - The "start the game" function initiates the beginning of the game and selects a random word from the word bank. + - The "check the word" function compares the word submitted by the user with the hidden word and provides information about the correct letter positions. + +2. **Description of the Second Program**: + - The second program is responsible for interacting with the first program and managing the gameplay. + - It should keep track of previous user responses and monitor the number of attempts. + - Additionally, the second program tracks time elapsed since the start of the game to manage time constraints and events. + +3. **Interaction between the Programs**: + - The user starts the game by sending the appropriate message to the second program. + - The second program calls the "start the game" function of the first program. + - The user submits their guesses to the second program, which passes them to the "check the word" function of the first program. + - The first program returns information about the correctness of the guess and the positions of letters. + - The second program analyzes the result and takes further actions, including tracking the number of attempts and game time. + + 4. **Key Implementation Aspects**: + - The second program must have mechanisms for storing data about previous user moves and tracking time. + - To manage the gameplay, the second program must effectively interact with the first program by exchanging data and receiving responses. ## First program @@ -41,6 +65,8 @@ The program will have two functions: - `StartGame` - action to start the game; generates a random word and returns the reply as `GameStarted{user: ActorId}`; - `CheckWord` - action to check the word and returns the reply as `WordChecked { user: ActorId, correct_positions: Vec,contained_in_word: Vec }`, where in the `correct_positions` returns the indices of letters that are in their place, and `contained_in_word` returns the indices of letters that are contained in the word but are in the wrong place. +The whole code of the first programme looks as follows: + ```rust pub enum Action { StartGame { @@ -64,42 +90,149 @@ pub enum Event { } ``` -For instance, if the secret word is "house" and the user submits "human" then `correct_positions` would be `[0]`, and `contained_in_word` would be `[1]`. +```rust +#![no_std] +use gstd::{collections::HashMap, exec, msg, prelude::*, ActorId}; +use wordle_io::*; + +static mut WORDLE: Option = None; + +const BANK_OF_WORDS: [&str; 3] = ["house", "human", "horse"]; + +#[derive(Default)] +struct Wordle { + games: HashMap, +} + +#[no_mangle] +extern "C" fn init() { + unsafe { + WORDLE = Some(Wordle { + games: HashMap::new(), + }); + } +} + +#[no_mangle] +extern "C" fn handle() { + let action: Action = msg::load().expect("Unable to decode "); + let wordle = unsafe { WORDLE.as_mut().expect("The program is not initialized") }; + + let reply = match action { + Action::StartGame { user } => { + let random_id = get_random_value(BANK_OF_WORDS.len() as u8); + let word = BANK_OF_WORDS[random_id as usize]; + wordle.games.insert(user, word.to_string()); + Event::GameStarted { user } + } + Action::CheckWord { user, word } => { + if word.len() != 5 { + panic!("The length of the word exceeds 5"); + } + let key_word = wordle + .games + .get(&user) + .expect("There is no game with this user"); + let mut matched_indices = Vec::with_capacity(5); + let mut key_indices = Vec::with_capacity(5); + for (i, (a, b)) in key_word.chars().zip(word.chars()).enumerate() { + if a == b { + matched_indices.push(i as u8); + } else if key_word.contains(b) { + key_indices.push(i as u8); + } + } + + Event::WordChecked { + user, + correct_positions: matched_indices, + contained_in_word: key_indices, + + } + + } + }; + + msg::reply(reply, 0).expect("Error in sending a reply"); +} + +static mut SEED: u8 = 0; + +pub fn get_random_value(range: u8) -> u8 { + let seed = unsafe { SEED }; + unsafe { SEED = SEED.wrapping_add(1) }; + let mut random_input: [u8; 32] = exec::program_id().into(); + random_input[0] = random_input[0].wrapping_add(seed); + let (random, _) = exec::random(random_input).expect("Error in getting random number"); + random[0] % range +} +``` + +For instance, consider a scenario where the secret word is *house*. If the user submits the word *human*, the correct_positions would be `[0]`, indicating that the letter "h" is in the correct position (at index 0) in the secret word. Also, `content_in_word` will be `[1]`, indicating that the letter "u" is present in the secret word, but not in the correct position. + +![Example Homework](../img/05/example_hw_1.png) + +Now, let's examine another scenario where the user inputs the word *horse*. In this case, correct_positions would be `[0, 1, 3, 4]`, indicating that the letters "h", "o", "s", and "e" are in the correct positions (at indices 0, 1, 3, and 4) in the secret word. However, `contained_in_word` would be empty. + +![Example Homework](../img/05/example_hw_2.png) + +Finally, if the user correctly guesses the secret word *house*, correct_positions would contain all indices from 0 to 4, indicating that all letters are in the correct positions. This signifies the end of the game. + +![Example Homework](../img/05/example_hw_3.png) + +Through these examples, we observe how the program evaluates user guesses and provides feedback based on the positions of correct letters in the secret word. ## The Homework Assignment -Your task is to write a second program that will link the user and the first program. +Your assignment is to create a second program that acts as an intermediary between the user and the first program. + +1. **Initialization Function (`init()`)**: + - This function should be created to receive the address of the first program and store it. + +2. **Handle Function (`handle()`)**: +`handle()` function must be able to handle three actions: `StartGame`, `CheckWord`, `CheckGameStatus`. +Let's examine the functionality of each action: + +- StartGame: + + - The program checks if a game already exists for the user; + - It sends a "StartGame" message to the first program; + - Utilizes the `exec::wait()` or `exec::wait_for()` function to await a response; + - Sends a delayed message with action "CheckGameStatus" to monitor the game's progress (its logic will be described below); + - A reply is sent to notify the user that the game has successfully started. + +- CheckWord: -1. Write `init()` function that: - - Receives the address of the first program and stores it (When initializing the program at `wss://testnet.vara.network` enter the address *0x963213d3eb00be79a84a74a74eb8bac21eee91aa387ce5d236e774291f2f2f8e16420*). + - Ensures that a game exists and is in the correct status; + - Validates that the submitted word length is five and is in lowercase; + - Sends a "CheckWord" message to the first program; + - Utilizes the `exec::wait()` or `exec::wait_for()` function to await a reply; + - Sends a reply to notify the user that the move was successful. -2. Write a `handle()` function that: -- Receives the action *Start* or *Check word*: - - In the case of *Start*, checks if the game exists; - - In the case of *Check word*, ensure that the submitted word contains 5 letters, convert the letters to lowercase, and save the check word in the program. -- Depending on the action sends the required message to the first program; -- Utilizes the `wait()` or `wait_for()` function to add the message to the waiting list and awaits `wake()`; -- Handles the cases when wake() will be called to send a reply to the user. +- CheckGameStatus: -3. Write a `handle_reply()` function that: -- Receives reply messages; -- Uses `msg::reply_to()` to determine the message identifier, i.e. to which message the reply was sent; -- saves the result depending on the reply: - - If a response of `GameStarted` is received, it sets the game status to indicate that the game has successfully commenced; - - If a response of `WordChecked` is received, it saves the response and checks if the word has been guessed. If the word has been guessed, it changes the game status accordingly. -- calls the `wake()` function with the received message identifier. + - The game should have a time limit from its start, so a delayed message is sent to check the game status. + If the game is not finished within the specified time limit, it ends the game by transitioning it to the desired status. + Specify a delay equal to 200 blocks (10 minutes) for the delayed message. -4. Include the logic for a delayed message: +3. **Handle Reply Function (`handle_reply()`)**: +- Receives reply messages. +- Utilizes `msg::reply_to()` to determine the message identifier, i.e., which message was replied to. +- Processes and stores the result depending on the reply: + - If a "GameStarted" response is received, it updates the game status to indicate that the game was successfully started. + - If a "WordChecked" response is received, it saves the response, increments the number of tries, and checks if the word was guessed. If the word has been guessed, it switches the game status to "GameOver(Win)". If all attempts are used up and the word is not guessed, it switches the game status to "GameOver(Lose)". +- Calls `wake()` with the identifier of the received message to acknowledge the response. -- Upon game start, send a delayed message about game completion with a delay of 200 blocks (10 minutes); -- The game should end with a loss if the word is not guessed in that time. +4. **State Function (`state()`)**: +- It is necessary to implement the state() function in order to get all the information about the game. ## Testing All program actions must be checked in tests using the [`gtest`](https://docs.gear.rs/gtest/) crate. -- Check all strategies of the program; -- Check negative scenarios and handle invalid input data. +- Check all strategies of the game program; +- Check delayed message logic; +- Check negative scenarios and handling of invalid inputs. ## Afterword diff --git a/docs/03-intermediate/img/02/handle_reply.gif b/docs/03-intermediate/img/02/handle_reply.gif index e20e308..c7553c5 100644 Binary files a/docs/03-intermediate/img/02/handle_reply.gif and b/docs/03-intermediate/img/02/handle_reply.gif differ diff --git a/docs/03-intermediate/img/03/wait_wake.gif b/docs/03-intermediate/img/03/wait_wake.gif index a18435c..09427b5 100644 Binary files a/docs/03-intermediate/img/03/wait_wake.gif and b/docs/03-intermediate/img/03/wait_wake.gif differ diff --git a/docs/03-intermediate/img/03/wait_wake_code.gif b/docs/03-intermediate/img/03/wait_wake_code.gif index 4b64cdd..9850dce 100644 Binary files a/docs/03-intermediate/img/03/wait_wake_code.gif and b/docs/03-intermediate/img/03/wait_wake_code.gif differ diff --git a/docs/03-intermediate/img/05/example_hw_1.png b/docs/03-intermediate/img/05/example_hw_1.png new file mode 100644 index 0000000..1be5244 Binary files /dev/null and b/docs/03-intermediate/img/05/example_hw_1.png differ diff --git a/docs/03-intermediate/img/05/example_hw_2.png b/docs/03-intermediate/img/05/example_hw_2.png new file mode 100644 index 0000000..40cb002 Binary files /dev/null and b/docs/03-intermediate/img/05/example_hw_2.png differ diff --git a/docs/03-intermediate/img/05/example_hw_3.png b/docs/03-intermediate/img/05/example_hw_3.png new file mode 100644 index 0000000..8efe4c6 Binary files /dev/null and b/docs/03-intermediate/img/05/example_hw_3.png differ