diff --git a/Cargo.lock b/Cargo.lock index 0372d5b9c73..cbc3ab83d1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3326,6 +3326,16 @@ dependencies = [ "gstd", ] +[[package]] +name = "demo-async-reply-hook" +version = "0.1.0" +dependencies = [ + "futures", + "gear-wasm-builder", + "gstd", + "parity-scale-codec", +] + [[package]] name = "demo-async-signal-entry" version = "0.1.0" @@ -10920,6 +10930,7 @@ dependencies = [ "demo-async-custom-entry", "demo-async-init", "demo-async-recursion", + "demo-async-reply-hook", "demo-async-signal-entry", "demo-async-tester", "demo-calc-hash", diff --git a/Cargo.toml b/Cargo.toml index 5bac5b7e299..e48a24d02c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ members = [ "examples/async-critical", "examples/async-custom-entry", "examples/async-init", + "examples/async-reply-hook", "examples/async-signal-entry", "examples/async-tester", "examples/autoreply", @@ -430,6 +431,7 @@ demo-async = { path = "examples/async" } demo-async-critical = { path = "examples/async-critical" } demo-async-custom-entry = { path = "examples/async-custom-entry" } demo-async-init = { path = "examples/async-init" } +demo-async-reply-hook = { path = "examples/async-reply-hook" } demo-async-recursion = { path = "examples/async-recursion" } demo-async-signal-entry = { path = "examples/async-signal-entry" } demo-async-tester = { path = "examples/async-tester" } diff --git a/examples/async-reply-hook/Cargo.toml b/examples/async-reply-hook/Cargo.toml new file mode 100644 index 00000000000..532aa46ecce --- /dev/null +++ b/examples/async-reply-hook/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "demo-async-reply-hook" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +gstd.workspace = true +parity-scale-codec.workspace = true +futures.workspace = true + +[build-dependencies] +gear-wasm-builder.workspace = true + +[features] +debug = ["gstd/debug"] +default = ["std"] +std = [] diff --git a/examples/async-reply-hook/build.rs b/examples/async-reply-hook/build.rs new file mode 100644 index 00000000000..44f0f822521 --- /dev/null +++ b/examples/async-reply-hook/build.rs @@ -0,0 +1,21 @@ +// This file is part of Gear. + +// Copyright (C) 2023-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +fn main() { + gear_wasm_builder::build(); +} diff --git a/examples/async-reply-hook/src/lib.rs b/examples/async-reply-hook/src/lib.rs new file mode 100644 index 00000000000..1a73dc5f23d --- /dev/null +++ b/examples/async-reply-hook/src/lib.rs @@ -0,0 +1,30 @@ +// This file is part of Gear. + +// Copyright (C) 2023-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#![no_std] + +#[cfg(feature = "std")] +mod code { + include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +} + +#[cfg(feature = "std")] +pub use code::WASM_BINARY_OPT as WASM_BINARY; + +#[cfg(target_arch = "wasm32")] +mod wasm; diff --git a/examples/async-reply-hook/src/wasm.rs b/examples/async-reply-hook/src/wasm.rs new file mode 100644 index 00000000000..dac4731fcd5 --- /dev/null +++ b/examples/async-reply-hook/src/wasm.rs @@ -0,0 +1,88 @@ +// This file is part of Gear. + +// Copyright (C) 2023-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use gstd::{critical, debug, exec, msg, prelude::*, ActorId}; + +#[gstd::async_main] +async fn main() { + let source = msg::source(); + + // Case 1: Message without reply hook + let m1 = gstd::msg::send_bytes_for_reply(source, b"for_reply_1", 0, 0) + .expect("Failed to send message"); + + // Case 2: Message with reply hook but we don't reply to it + let m2 = gstd::msg::send_bytes_for_reply(source, b"for_reply_2", 0, 1_000_000_000) + .expect("Failed to send message") + .up_to(Some(5)) + .expect("Failed to set timeout") + .handle_reply(|| { + // This should be called in gas / 100 blocks, but the program exits by that time + unreachable!("This should not be called"); + }) + .expect("Failed to set reply hook"); + + // Case 3: Message with reply hook and we reply to it + let for_reply_3 = gstd::rc::Rc::new(core::cell::RefCell::new(false)); + let for_reply_3_clone = for_reply_3.clone(); + let m3 = gstd::msg::send_bytes_for_reply(source, b"for_reply_3", 0, 1_000_000_000) + .expect("Failed to send message") + .handle_reply(move || { + debug!("reply message_id: {:?}", msg::id()); + debug!("reply payload: {:?}", msg::load_bytes()); + + assert_eq!(msg::load_bytes().unwrap(), [3]); + + msg::send_bytes(msg::source(), b"saw_reply_3", 0); + for_reply_3_clone.replace(true); + }) + .expect("Failed to set reply hook"); + + // Case 4: We reply to message after timeout + let m4 = gstd::msg::send_bytes_for_reply(source, b"for_reply_4", 0, 1_000_000_000) + .expect("Failed to send message") + .up_to(Some(5)) + .expect("Failed to set timeout") + .handle_reply(|| { + debug!("reply message_id: {:?}", msg::id()); + debug!("reply payload: {:?}", msg::load_bytes()); + + assert_eq!(msg::load_bytes().unwrap(), [4]); + + msg::send_bytes(msg::source(), b"saw_reply_4", 0); + }) + .expect("Failed to set reply hook"); + + m1.await.expect("Received error reply"); + + assert_eq!( + m2.await.expect_err("Should receive timeout"), + gstd::errors::Error::Timeout(8, 8) + ); + + m3.await.expect("Received error reply"); + // check for_reply_3 handle_reply executed + assert!(for_reply_3.replace(false)); + + assert_eq!( + m4.await.expect_err("Should receive timeout"), + gstd::errors::Error::Timeout(8, 8) + ); + + msg::send_bytes(source, b"completed", 0); +} diff --git a/examples/distributor/src/wasm.rs b/examples/distributor/src/wasm.rs index 2472ffa683d..a6dbc36e9a2 100644 --- a/examples/distributor/src/wasm.rs +++ b/examples/distributor/src/wasm.rs @@ -171,7 +171,7 @@ extern "C" fn handle() { #[no_mangle] extern "C" fn handle_reply() { - gstd::record_reply(); + gstd::handle_reply_with_hook(); } #[no_mangle] diff --git a/gstd/codegen/src/lib.rs b/gstd/codegen/src/lib.rs index a0746d01d3e..90a8b1b7b46 100644 --- a/gstd/codegen/src/lib.rs +++ b/gstd/codegen/src/lib.rs @@ -196,7 +196,7 @@ fn generate_handle_reply_if_required(mut code: TokenStream, attr: Option) let handle_reply: TokenStream = quote!( #[no_mangle] extern "C" fn handle_reply() { - gstd::record_reply(); + gstd::handle_reply_with_hook(); #attr (); } ) @@ -498,7 +498,7 @@ pub fn wait_for_reply(attr: TokenStream, item: TokenStream) -> TokenStream { // Registering signal. crate::async_runtime::signals().register_signal(waiting_reply_to); - Ok(crate::msg::MessageFuture { waiting_reply_to }) + Ok(crate::msg::MessageFuture { waiting_reply_to, reply_deposit }) } #[doc = #for_reply_as_docs] @@ -514,7 +514,7 @@ pub fn wait_for_reply(attr: TokenStream, item: TokenStream) -> TokenStream { // Registering signal. crate::async_runtime::signals().register_signal(waiting_reply_to); - Ok(crate::msg::CodecMessageFuture:: { waiting_reply_to, _marker: Default::default() }) + Ok(crate::msg::CodecMessageFuture:: { waiting_reply_to, reply_deposit, _marker: Default::default() }) } } .into() @@ -591,7 +591,7 @@ pub fn wait_create_program_for_reply(attr: TokenStream, item: TokenStream) -> To // Registering signal. crate::async_runtime::signals().register_signal(waiting_reply_to); - Ok(crate::msg::CreateProgramFuture { waiting_reply_to, program_id }) + Ok(crate::msg::CreateProgramFuture { waiting_reply_to, program_id, reply_deposit }) } #[doc = #for_reply_as_docs] @@ -607,7 +607,7 @@ pub fn wait_create_program_for_reply(attr: TokenStream, item: TokenStream) -> To // Registering signal. crate::async_runtime::signals().register_signal(waiting_reply_to); - Ok(crate::msg::CodecCreateProgramFuture:: { waiting_reply_to, program_id, _marker: Default::default() }) + Ok(crate::msg::CodecCreateProgramFuture:: { waiting_reply_to, program_id, reply_deposit, _marker: Default::default() }) } } .into() diff --git a/gstd/src/async_runtime/mod.rs b/gstd/src/async_runtime/mod.rs index c9e026552a7..bf8d2fc02dc 100644 --- a/gstd/src/async_runtime/mod.rs +++ b/gstd/src/async_runtime/mod.rs @@ -18,11 +18,13 @@ mod futures; mod locks; +mod reply_hooks; mod signals; mod waker; pub use self::futures::message_loop; pub(crate) use locks::Lock; +pub(crate) use reply_hooks::HooksMap; pub(crate) use signals::ReplyPoll; use self::futures::FuturesMap; @@ -49,9 +51,20 @@ pub(crate) fn locks() -> &'static mut LocksMap { unsafe { LOCKS.get_or_insert_with(LocksMap::default) } } +static mut REPLY_HOOKS: Option = None; + +pub(crate) fn reply_hooks() -> &'static mut HooksMap { + unsafe { REPLY_HOOKS.get_or_insert_with(HooksMap::new) } +} + /// Default reply handler. -pub fn record_reply() { +pub fn handle_reply_with_hook() { signals().record_reply(); + + // Execute reply hook (if it was registered) + let replied_to = + crate::msg::reply_to().expect("`gstd::handle_reply_with_hook()` called in wrong context"); + reply_hooks().execute_and_remove(replied_to); } /// Default signal handler. @@ -64,4 +77,5 @@ pub fn handle_signal() { futures().remove(&msg_id); locks().remove_message_entry(msg_id); + reply_hooks().remove(msg_id) } diff --git a/gstd/src/async_runtime/reply_hooks.rs b/gstd/src/async_runtime/reply_hooks.rs new file mode 100644 index 00000000000..570f6df51f6 --- /dev/null +++ b/gstd/src/async_runtime/reply_hooks.rs @@ -0,0 +1,49 @@ +// This file is part of Gear. + +// Copyright (C) 2023-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::MessageId; +use alloc::boxed::Box; +use hashbrown::HashMap; + +pub(crate) struct HooksMap(HashMap>); + +impl HooksMap { + pub fn new() -> Self { + Self(HashMap::new()) + } + + /// Register hook to be executed when a reply for message_id is received. + pub(crate) fn register(&mut self, mid: MessageId, f: F) { + if self.0.contains_key(&mid) { + panic!("handle_reply: reply hook for this message_id is already registered"); + } + self.0.insert(mid, Box::new(f)); + } + + /// Execute hook for message_id (if registered) + pub(crate) fn execute_and_remove(&mut self, message_id: MessageId) { + if let Some(f) = self.0.remove(&message_id) { + f(); + } + } + + /// Clear hook for message_id without executing it. + pub(crate) fn remove(&mut self, message_id: MessageId) { + self.0.remove(&message_id); + } +} diff --git a/gstd/src/common/errors.rs b/gstd/src/common/errors.rs index bccb8fbe021..f3cd3bf0dc3 100644 --- a/gstd/src/common/errors.rs +++ b/gstd/src/common/errors.rs @@ -172,6 +172,10 @@ pub enum UsageError { ZeroSystemReservationAmount, /// This error occurs when providing zero duration to mutex lock function ZeroMxLockDuration, + /// This error occurs when handle_reply is called without (or with zero) + /// reply deposit + /// (see [`MessageFuture::handle_reply`](crate::msg::MessageFuture::handle_reply)). + ZeroReplyDeposit, } impl fmt::Display for UsageError { @@ -182,6 +186,9 @@ impl fmt::Display for UsageError { write!(f, "System reservation amount can not be zero in config") } UsageError::ZeroMxLockDuration => write!(f, "Mutex lock duration can not be zero"), + UsageError::ZeroReplyDeposit => { + write!(f, "Reply deposit can not be zero when setting reply hook") + } } } } diff --git a/gstd/src/lib.rs b/gstd/src/lib.rs index 9fb61eab073..3e867de58e2 100644 --- a/gstd/src/lib.rs +++ b/gstd/src/lib.rs @@ -159,7 +159,7 @@ mod reservations; pub mod sync; pub mod util; -pub use async_runtime::{handle_signal, message_loop, record_reply}; +pub use async_runtime::{handle_reply_with_hook, handle_signal, message_loop}; pub use common::{errors, primitives_ext::*}; pub use config::Config; pub use gcore::{ diff --git a/gstd/src/msg/async.rs b/gstd/src/msg/async.rs index 69cc108f20a..0a50bb8975d 100644 --- a/gstd/src/msg/async.rs +++ b/gstd/src/msg/async.rs @@ -104,6 +104,9 @@ where pub struct CodecMessageFuture { /// A message identifier for an expected reply. pub waiting_reply_to: MessageId, + /// Reply deposit that was allocated for this message. Checked in + /// handle_reply. + pub(crate) reply_deposit: u64, /// Marker /// /// # Note @@ -161,6 +164,9 @@ pub struct CodecCreateProgramFuture { pub waiting_reply_to: MessageId, /// An identifier of a newly created program. pub program_id: ActorId, + /// Reply deposit that was allocated for this message. Checked in + /// handle_reply. + pub(crate) reply_deposit: u64, /// Marker /// /// # Note @@ -219,6 +225,9 @@ pub struct MessageFuture { /// This identifier is generated by the corresponding send function (e.g. /// [`send_bytes`](super::send_bytes)). pub waiting_reply_to: MessageId, + /// Reply deposit that was allocated for this message. Checked in + /// handle_reply. + pub(crate) reply_deposit: u64, } impl_futures!( @@ -266,6 +275,9 @@ pub struct CreateProgramFuture { pub waiting_reply_to: MessageId, /// An identifier of a newly created program. pub program_id: ActorId, + /// Reply deposit that was allocated for this message. Checked in + /// handle_reply. + pub(crate) reply_deposit: u64, } impl_futures!( diff --git a/gstd/src/msg/macros.rs b/gstd/src/msg/macros.rs index dca7795a24a..0748af62c3e 100644 --- a/gstd/src/msg/macros.rs +++ b/gstd/src/msg/macros.rs @@ -64,6 +64,52 @@ macro_rules! impl_futures { Ok(self) } + + /// Execute a function when the reply is received. + /// + /// This callback will be executed in reply context and consume reply gas, so + /// adequate `reply_deposit` should be supplied in `*_for_reply` call + /// that comes before this. Note that the hook will still be executed on reply + /// even after original future resolves in timeout. + /// + /// # Examples + /// + /// Send message to echo program and wait for reply. + /// + /// ``` + /// use gstd::{ActorId, msg, debug}; + /// + /// #[gstd::async_main] + /// async fn main() { + /// let dest = ActorId::from(1); // Replace with correct actor id + /// + /// msg::send_bytes_for_reply(dest, b"PING", 0, 1_000_000) + /// .expect("Unable to send") + /// .handle_reply(|| { + /// debug!("reply code: {:?}", msg::reply_code()); + /// + /// if msg::load_bytes().unwrap_or_default() == b"PONG" { + /// debug!("successfully received pong"); + /// } + /// }) + /// .expect("Error setting reply hook") + /// .await + /// .expect("Received error"); + /// } + /// # fn main() {} + /// ``` + /// + /// # Panics + /// + /// Panics if this is called second time. + pub fn handle_reply(self, f: F) -> Result { + if self.reply_deposit == 0 { + return Err(Error::Gstd(crate::errors::UsageError::ZeroReplyDeposit)); + } + async_runtime::reply_hooks().register(self.waiting_reply_to.clone(), f); + + Ok(self) + } } }; } diff --git a/pallets/gear/Cargo.toml b/pallets/gear/Cargo.toml index f6535852b6e..99284159692 100644 --- a/pallets/gear/Cargo.toml +++ b/pallets/gear/Cargo.toml @@ -117,6 +117,7 @@ demo-sync-duplicate.workspace = true demo-custom.workspace = true demo-delayed-reservation-sender = { workspace = true, features = ["debug"] } demo-async-critical = { workspace = true, features = ["debug"] } +demo-async-reply-hook = { workspace = true, features = ["debug"] } demo-create-program-reentrance = { workspace = true, features = ["debug"] } demo-value-sender.workspace = true test-syscalls = { workspace = true, features = ["debug"] } @@ -134,47 +135,47 @@ rand.workspace = true [features] default = ['std'] std = [ - "parity-scale-codec/std", - "env_logger", - "log/std", - "common/std", - "frame-benchmarking?/std", - "frame-support/std", - "frame-support-test/std", - "frame-system/std", - "gear-wasm-instrument/std", - "scopeguard/use_std", - "core-processor/std", - "gear-core-backend/std", - "gear-lazy-pages-interface/std", - "scale-info/std", - "sp-io/std", - "sp-std/std", - "sp-core/std", - "sp-runtime/std", - "sp-externalities/std", - "pallet-balances/std", - "pallet-authorship/std", - "pallet-gear-gas/std", - "pallet-gear-messenger/std", - "pallet-gear-scheduler/std", - "pallet-gear-program/std", - "pallet-gear-voucher/std", - "pallet-gear-bank/std", - "pallet-gear-proc-macro/full", - "primitive-types/std", - "serde/std", - "sp-consensus-babe/std", - "test-syscalls?/std", - "demo-read-big-state?/std", - "demo-proxy?/std", - "demo-reserve-gas?/std", - "demo-delayed-sender?/std", - "demo-constructor?/std", - "demo-waiter?/std", - "demo-init-wait?/std", - "demo-signal-entry?/std", - "gear-runtime-interface/std", + "parity-scale-codec/std", + "env_logger", + "log/std", + "common/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-support-test/std", + "frame-system/std", + "gear-wasm-instrument/std", + "scopeguard/use_std", + "core-processor/std", + "gear-core-backend/std", + "gear-lazy-pages-interface/std", + "scale-info/std", + "sp-io/std", + "sp-std/std", + "sp-core/std", + "sp-runtime/std", + "sp-externalities/std", + "pallet-balances/std", + "pallet-authorship/std", + "pallet-gear-gas/std", + "pallet-gear-messenger/std", + "pallet-gear-scheduler/std", + "pallet-gear-program/std", + "pallet-gear-voucher/std", + "pallet-gear-bank/std", + "pallet-gear-proc-macro/full", + "primitive-types/std", + "serde/std", + "sp-consensus-babe/std", + "test-syscalls?/std", + "demo-read-big-state?/std", + "demo-proxy?/std", + "demo-reserve-gas?/std", + "demo-delayed-sender?/std", + "demo-constructor?/std", + "demo-waiter?/std", + "demo-init-wait?/std", + "demo-signal-entry?/std", + "gear-runtime-interface/std", ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", @@ -198,7 +199,7 @@ runtime-benchmarks = [ "demo-waiter/wasm-wrapper", "demo-init-wait/wasm-wrapper", "demo-signal-entry/wasm-wrapper", - "core-processor/mock", + "core-processor/mock", ] runtime-benchmarks-checkers = [] try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/gear/src/tests.rs b/pallets/gear/src/tests.rs index 970b9810961..c6d6bd83088 100644 --- a/pallets/gear/src/tests.rs +++ b/pallets/gear/src/tests.rs @@ -15198,6 +15198,131 @@ fn critical_hook_in_handle_signal() { }); } +#[test] +fn handle_reply_hook() { + use demo_async_reply_hook::WASM_BINARY; + + init_logger(); + new_test_ext().execute_with(|| { + // Upload program + assert_ok!(Gear::upload_program( + RuntimeOrigin::signed(USER_1), + WASM_BINARY.to_vec(), + DEFAULT_SALT.to_vec(), + vec![], + 10_000_000_000, + 0, + false, + )); + let pid = get_last_program_id(); + + run_to_block(2, None); + + assert!(Gear::is_initialized(pid)); + assert!(utils::is_active(pid)); + + // Init conversation + assert_ok!(Gear::send_message( + RuntimeOrigin::signed(USER_1), + pid, + EMPTY_PAYLOAD.encode(), + 10_000_000_000, + 0, + false, + )); + + run_to_block(3, None); + + let messages = MailboxOf::::iter_key(USER_1).map(|(msg, _bn)| msg); + + let mut timeout_msg_id = None; + + for msg in messages { + match msg.payload_bytes() { + b"for_reply_1" => { + // Reply to the first message + assert_ok!(Gear::send_reply( + RuntimeOrigin::signed(USER_1), + msg.id(), + [1].to_vec(), + 1_000_000_000, + 0, + false, + )); + } + b"for_reply_2" => { + // Don't reply, message should time out + } + b"for_reply_3" => { + // Reply to the third message + assert_ok!(Gear::send_reply( + RuntimeOrigin::signed(USER_1), + msg.id(), + [3].to_vec(), + 1_000_000_000, + 0, + false, + )); + } + b"for_reply_4" => { + // reply later + timeout_msg_id = Some(msg.id()); + } + _ => unreachable!(), + } + } + + run_to_block(4, None); + + // Expect a reply back + let m = maybe_last_message(USER_1); + assert!(m.unwrap().payload_bytes() == b"saw_reply_3"); + + run_to_block(10, None); + + // Program finished + let m = maybe_last_message(USER_1); + assert!(m.unwrap().payload_bytes() == b"completed"); + + // Reply to a message that timed out + assert_ok!(Gear::send_reply( + RuntimeOrigin::signed(USER_1), + timeout_msg_id.unwrap(), + [4].to_vec(), + 1_000_000_000, + 0, + false, + )); + + run_to_block(11, None); + + let messages = all_user_messages(USER_1); + let vec: Vec> = messages + .iter() + .filter_map(|m| { + if m.details().is_some() { + None + } else { + Some(String::from_utf8_lossy(m.payload_bytes())) + } + }) + .collect(); + // Hook executed after completed + assert_eq!( + vec, + [ + "for_reply_1", + "for_reply_2", + "for_reply_3", + "for_reply_4", + "saw_reply_3", + "completed", + "saw_reply_4" + ] + ); + }); +} + #[test] fn program_with_large_indexes() { // There is a security problem in module deserialization found by @@ -16787,4 +16912,23 @@ pub(crate) mod utils { pub(super) fn gas_price(gas: u64) -> u128 { ::GasMultiplier::get().gas_to_value(gas) } + + // Collect all messages by account in chronological order (oldest first) + #[track_caller] + pub(super) fn all_user_messages(user_id: AccountId) -> Vec { + System::events() + .into_iter() + .filter_map(|e| { + if let MockRuntimeEvent::Gear(Event::UserMessageSent { message, .. }) = e.event { + if message.destination() == user_id.into() { + Some(message) + } else { + None + } + } else { + None + } + }) + .collect() + } }