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()
+ }
}