Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Intermediate course clearance #228

Merged
merged 2 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 33 additions & 31 deletions docs/03-intermediate/02-message-receiving/handle_reply.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,37 @@ hide_table_of_contents: true

In this lesson, you will learn how a program can efficiently handle request messages. This concept will be illustrated through an example of interaction between two programs.

Before analyzing the program code in detail, it will be helpful to first present a schematic overview of how Gear programs operate:
Imagine you have dApp with specific functionality, and you need to update this application by uploading a new version to the blockchain. Consequently, the application's address on the blockchain will change. To make this transition seamless for users, it's essential to maintain a consistent address that users can interact with. This issue can be addressed by using a Proxy program. The Proxy program will act as an intermediary: it will receive messages from users, forward them to the updated version of the application, and relay responses back to the users. Each time you're updating the Target program, you just nedd to update the Target's program address in the Proxy Program.

Before analyzing program codes in detail, it will be helpful to first present a schematic overview of how Gear programs operate:

![gif 1](../img/02/handle_reply.gif)

1. The user sends an `Action` message to Program #1, which is processed by the `handle()` function.
2. This message is then passed to Program #2.
3. Program #1 sends an `Event` message to the user, indicating that the message was successfully passed to Program #2.
4. Program #2 receives the message from Program #1, processes it, and responds.
5. Program #1 receives the reply message from Program #2 via the `handle_reply()` entry point.
6. Finally, from the `handle_reply()` function, Program #1 sends the response to the user.
1. The user sends an `Action` message to `Proxy Program`, which is processed by the `handle()` function.
2. This message is then passed to `Target Program`.
3. `Proxy Program` sends an `Event:MessageSent` message to the user, indicating that the action message was successfully passed to `Target Program`.
4. `Target Program` receives the message containing a proxied `Action` from `Proxy Program`, processes it, and replies with event corresponding to desired action.
5. `Proxy Program` receives the reply message from `Target Program` via the `handle_reply()` entry point.
6. Finally, from the `handle_reply()` function, `Proxy Program` resends received event from the `Target Program` to the user.

## First program
## Proxy program

The primary task of the first program is to communicate with the second program, requiring the following structure:
The primary task of the Proxy program is to proxy user's actions to the Target program and resend replies from the Target program back to the User. Here is the structure in the Proxy program that enables handling of single flow of interaction between user and Target program:

```rust
struct Session {
second_program: ActorId, // second program address
msg_id_to_actor: (MessageId, ActorId), // tuple of message identifiers and message source address
target_program_id: ActorId, // target program address
msg_id_to_actor_id: (MessageId, ActorId), // tuple containing identifier of a message sent to a Target program and Id of a User initiated the action
}
```

The following actions and events will be necessary to simulate a dialogue between the programs:
The following actions and events will be necessary to simulate a dialogue between the user and programs:

```rust
#[derive(TypeInfo, Encode, Decode)]
#[codec(crate = gstd::codec)]
#[scale_info(crate = gstd::scale_info)]
pub enum Action {
pub enum Action { // arbitrary actions should be supported in the dApp (defined by dApp author)
Hello,
HowAreYou,
MakeRandomNumber{
Expand All @@ -46,24 +48,24 @@ pub enum Action {
#[derive(TypeInfo, Encode, Decode, Debug)]
#[codec(crate = gstd::codec)]
#[scale_info(crate = gstd::scale_info)]
pub enum Event {
pub enum Event { // arbitrary replies to the action
Hello,
Fine,
Number(u8),
MessageSent,
MessageSent, // event cofirming succesfull message sent from Proxy to Target
}
```

During initialization, it is necessary to pass the address of the second program.
During initialization of the Proxy program, it is necessary to pass the address of the Target program.

```rust
#[no_mangle]
extern "C" fn init() {
let second_program = msg::load().expect("Unable to decode Init");
let target_program_id = msg::load().expect("Unable to decode Init");
unsafe {
SESSION = Some(Session {
second_program,
msg_id_to_actor: (MessageId::zero(), ActorId::zero()),
target_program_id,
msg_id_to_actor_id: (MessageId::zero(), ActorId::zero()),
});
}
}
Expand All @@ -72,35 +74,35 @@ extern "C" fn init() {
Let's focus on processing requests in the `handle()` function:

1. Receive the message with the `msg::load()` function.
2. Send a message to the second program using `msg::send()`.
2. Send a message to the Target program using `msg::send()`.
3. An important step is to store the identifier of the message returned by `msg::send()`. This allows 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.
4. Finally, send a reply message indicating that the message was sent to the Target program.

```rust
#[no_mangle]
extern "C" fn handle() {
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());
let msg_id = msg::send(session.target_program_id, action, 0).expect("Error in sending a message");
session.msg_id_to_actor_id = (msg_id, msg::source());
msg::reply(Event::MessageSent, 0).expect("Error in sending a reply");
}
```

The Gear program utilizes the `handle_reply()` function to handle replies to messages. Let’s delve into managing the response message from the second program:
The Gear program utilizes the `handle_reply()` function to handle replies to messages. Let’s delve into processing the response message from the second program:

1. Use the `msg::reply_to()` function to retrieve the identifier of the message for which the `handle_reply()` function was invoked.
2. Ensure that the message identifier matches the identifier of the message sent from the `handle()` function. This step verifies that the response corresponds to the specific message sent earlier.
3. Finally, send a reply message to the original sender’s address.
3. Finally, resend a message content from the Target program to the original sender’s address.

**It is crucial to note that calling `msg::reply()` inside the `handle_reply()` function is not permitted.**
**It is crucial to note that calling `msg::reply()` inside the `handle_reply()` function is not permitted. Instead, use `msg::send()` to proxy reply from the Target program to a User**

```rust
#[no_mangle]
extern "C" fn handle_reply() {
let reply_message_id = msg::reply_to().expect("Failed to query reply_to data");
let session = unsafe { SESSION.as_mut().expect("The session is not initialized") };
let (msg_id, actor) = session.msg_id_to_actor;
let (msg_id, actor) = session.msg_id_to_actor_id;
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");
Expand All @@ -109,12 +111,12 @@ extern "C" fn handle_reply() {
```

Just a reminder: the sender of the message will receive two messages:
- The first message, originating from the `handle()` function, indicates that the message has been forwarded to the second program.
- The second message, sent by the `handle_reply()` function, contains the response from the second program.
- The first message, originating from the `handle()` function, indicates that the original message with action has been succesfully forwarded to the Target program.
- The second message, sent by the `handle_reply()` function, contains the response from the Target program.

## Second Program
## Target Program

The first program is straightforward; it can accept various types of actions and respond 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.
The Target program is straightforward; it can accept various types of actions and respond 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]
Expand Down
46 changes: 23 additions & 23 deletions docs/03-intermediate/02-message-receiving/testing_handle_reply.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,25 @@ Let's verify the functionality of the programs discussed in the preceding sectio
let system = System::new();
```

2. Retrieve the first program from the root crate using the provided `system` and the second program instance from the wasm file.
2. Retrieve the Proxy program from the root crate using the provided `system` and the Target program instance from the wasm file.

```rust
let first_program = Program::current(&system);
let second_program = Program::from_file(&system, "target/wasm32-unknown-unknown/release/second_program.opt.wasm");
let proxy_program = Program::current(&system);
let target_program = Program::from_file(&system, "target/wasm32-unknown-unknown/release/target_program.opt.wasm");
```

3. 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.
3. Initialize the Target program by sending an empty message, and then initialize the Proxy program by passing the address of the Target program to it.

```rust
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);
let result = target_program.send_bytes(USER, []); // initialize Target program
let target_program_address: ActorId = TARGET_PROGRAM_ADDRESS.into();
let res = proxy_program.send(USER, target_program_address); // initialize Proxy program
```

4. Send a message with `Action::MakeRandomNumber { range: 1 }` to the first program and check for the response `Event::MessageSent`, indicating that the message was successfully sent to the second program's address.
4. Send a message with `Action::MakeRandomNumber { range: 1 }` to the Proxy program and check for the response `Event::MessageSent`, indicating that the message was successfully sent to the Target program's address.

```rust
let result = first_program.send(USER, Action::MakeRandomNumber {range: 1});
let result = proxy_program.send(USER, Action::MakeRandomNumber {range: 1});
let log = Log::builder()
.source(1)
.dest(3)
Expand Down Expand Up @@ -58,39 +58,39 @@ use gtest::{Log, Program, System};
use handle_reply_io::{Action, Event};

const USER: u64 = 3;
const SECOND_PROGRAM_ADDRESS: u64 = 2;
const TARGET_PROGRAM_ADDRESS: u64 = 2;

#[test]
fn success_test() {
// Create a new testing environment.
let system = System::new();

// 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, []);
// Get proxy program of the root crate with provided system.
let proxy_program = Program::current(&system);
// Get target program
let target_program = Program::from_file(&system, "target/wasm32-unknown-unknown/release/target_program.opt.wasm");
// The target program is initialized with an empty payload message
let result = target_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);
let target_program_address: ActorId = TARGET_PROGRAM_ADDRESS.into();
// The proxy program is initialized using target_program in the payload message
let res = proxy_program.send(USER, target_program_address);
assert!(!res.main_failed());

// Send with the message we want to receive back
let result = first_program.send(USER, Action::MakeRandomNumber {range: 1});
let result = proxy_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 second program
// check that the proxy message has arrived,
// which means that the message was successfully sent to the target program
let log = Log::builder()
.source(1)
.dest(3)
.payload(Event::MessageSent);
assert!(result.contains(&log));

// check that the second message has arrived at the mailbox,
// check that the target message has arrived at the mailbox,
// which means that a reply has been received.
let mailbox = system.get_mailbox(USER);
let log = Log::builder()
Expand Down
Loading