diff --git a/Cargo.toml b/Cargo.toml index 5a217f9..8a7e76b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["contracts/*", "packages/ibcmail", "tests"] resolver = "2" [workspace.package] -version = "0.2.0" +version = "0.3.0" [workspace.dependencies] cosmwasm-std = "1.5.3" @@ -12,6 +12,7 @@ cw-controllers = "1.1.2" cw-storage-plus = "1.2.0" thiserror = "1.0.50" cw-paginate = "0.2.1" +cw-item-set = "0.7.1" schemars = "0.8" cw-asset = "3.0.0" cw-semver = { version = "1.0", features = ["serde"] } @@ -20,14 +21,16 @@ cw-orch = "0.24.1" ibcmail = { path = "packages/ibcmail", package = "ibcmail" } client = { path = "contracts/client", package = "ibcmail-client" } server = { path = "contracts/server", package = "ibcmail-server" } -abstract-client = "0.23.0" -abstract-app = "0.23.0" -abstract-adapter = "0.23.0" -abstract-interface = "0.23.0" + +abstract-client = "=0.23.0" +abstract-app = "=0.23.0" +abstract-adapter = "=0.23.0" +abstract-interface = "0.23.1" + speculoos = "0.11.0" semver = "1.0" dotenv = "0.15.0" -env_logger = "0.10.0" +env_logger = "0.11.3" clap = "4.3.7" const_format = "0.2.32" diff --git a/contracts/client/examples/publish.rs b/contracts/client/examples/publish.rs index 31afd35..114912f 100644 --- a/contracts/client/examples/publish.rs +++ b/contracts/client/examples/publish.rs @@ -8,7 +8,6 @@ //! $ just publish uni-6 osmo-test-5 //! ``` -use abstract_app::objects::module::ModuleVersion; use abstract_app::objects::namespace::Namespace; use abstract_client::{AbstractClient, Publisher}; use clap::Parser; diff --git a/contracts/client/src/contract.rs b/contracts/client/src/contract.rs index d38dbf9..1cf98ee 100644 --- a/contracts/client/src/contract.rs +++ b/contracts/client/src/contract.rs @@ -8,6 +8,7 @@ use crate::{dependencies::MAIL_SERVER_DEP, error::ClientError, handlers, APP_VER pub type ClientResult = Result; const APP: App = App::new(IBCMAIL_CLIENT_ID, APP_VERSION, None) + .with_instantiate(handlers::instantiate_handler) .with_execute(handlers::execute_handler) .with_query(handlers::query_handler) .with_migrate(handlers::migrate_handler) diff --git a/contracts/client/src/dependencies.rs b/contracts/client/src/dependencies.rs index 9546150..a3cc4d2 100644 --- a/contracts/client/src/dependencies.rs +++ b/contracts/client/src/dependencies.rs @@ -4,7 +4,7 @@ use abstract_app::{objects::module::ModuleInfo, std::manager::ModuleInstallConfi use ibcmail::IBCMAIL_SERVER_ID; pub const MAIL_SERVER_DEP: StaticDependency = - StaticDependency::new(IBCMAIL_SERVER_ID, &[">=0.0.1"]); + StaticDependency::new(IBCMAIL_SERVER_ID, &[">=0.3.0"]); #[cfg(feature = "interface")] impl abstract_app::abstract_interface::DependencyCreation @@ -21,6 +21,10 @@ impl abstract_app::abstract_interface::Depen None, ); - Ok(vec![adapter_install_config]) + // The IBC client is depended upon by the server + let ibc_client = + ModuleInstallConfig::new(ModuleInfo::from_id_latest("abstract:ibc-client")?, None); + + Ok(vec![adapter_install_config, ibc_client]) } } diff --git a/contracts/client/src/handlers/execute.rs b/contracts/client/src/handlers/execute.rs index fae994e..188d298 100644 --- a/contracts/client/src/handlers/execute.rs +++ b/contracts/client/src/handlers/execute.rs @@ -1,23 +1,24 @@ +use crate::{ + contract::{App, ClientResult}, + error::ClientError, + msg::ClientExecuteMsg, +}; use abstract_app::objects::TruncatedChainId; use abstract_app::{ sdk::ModuleRegistryInterface, traits::{AbstractResponse, AccountIdentification}, }; use base64::prelude::*; -use cosmwasm_std::{ensure_eq, CosmosMsg, Deps, DepsMut, Env, MessageInfo}; +use cosmwasm_std::{ensure_eq, Addr, CosmosMsg, Deps, DepsMut, Env, MessageInfo}; +use ibcmail::client::state::SENT_STATUS; use ibcmail::{ client::{ state::{RECEIVED, SENT}, ClientApp, }, server::api::{MailServer, ServerInterface}, - IbcMailMessage, Message, Recipient, Route, Sender, IBCMAIL_SERVER_ID, -}; - -use crate::{ - contract::{App, ClientResult}, - error::ClientError, - msg::ClientExecuteMsg, + ClientMetadata, DeliveryStatus, Header, MailMessage, MessageHash, ReceivedMessage, Recipient, + Sender, IBCMAIL_SERVER_ID, }; // # ANCHOR: execute_handler @@ -29,10 +30,15 @@ pub fn execute_handler( msg: ClientExecuteMsg, ) -> ClientResult { match msg { - ClientExecuteMsg::SendMessage { message, route } => { - send_msg(deps, env, info, message, route, app) + ClientExecuteMsg::SendMessage { + message, + recipient, + metadata, + } => send_msg(deps, env, info, app, message, recipient, metadata), + ClientExecuteMsg::ReceiveMessage(message) => receive_msg(deps, info, app, message), + ClientExecuteMsg::UpdateDeliveryStatus { id, status } => { + update_delivery_status(deps, info, app, id, status) } - ClientExecuteMsg::ReceiveMessage(message) => receive_msg(deps, info, message, app), } } // # ANCHOR_END: execute_handler @@ -42,33 +48,39 @@ fn send_msg( deps: DepsMut, env: Env, _info: MessageInfo, - msg: Message, - route: Option, app: ClientApp, + message: MailMessage, + recipient: Recipient, + metadata: Option, ) -> ClientResult { // validate basic fields of message, construct message to send to server - let to_hash = format!("{:?}{:?}{:?}", env.block.time, msg.subject, msg.recipient); + let to_hash = format!("{:?}{:?}{:?}", env.block.time, message.subject, recipient); let hash = ::digest(to_hash); let base_64_hash = BASE64_STANDARD.encode(hash); - let to_send = IbcMailMessage { + + let sender = Sender::account( + app.account_id(deps.as_ref()).unwrap(), + Some(TruncatedChainId::new(&env)), + ); + let version = app.version().to_string(); + + let client_header = Header { + sender, + recipient, id: base_64_hash, - sender: Sender::account( - app.account_id(deps.as_ref()).unwrap(), - Some(TruncatedChainId::new(&env)), - ), - message: Message { - recipient: msg.recipient, - subject: msg.subject, - body: msg.body, - }, + version, timestamp: env.block.time, - version: app.version().to_string(), + reply_to: None, }; - SENT.save(deps.storage, to_send.id.clone(), &to_send)?; + SENT.save( + deps.storage, + client_header.id.clone(), + &(message.clone(), client_header.clone()), + )?; let server: MailServer<_> = app.mail_server(deps.as_ref()); - let route_msg: CosmosMsg = server.process_msg(to_send, route)?; + let route_msg: CosmosMsg = server.process_msg(message, client_header, metadata)?; Ok(app.response("send").add_message(route_msg)) } @@ -76,26 +88,57 @@ fn send_msg( /// Receive a message from the server // # ANCHOR: receive_msg -fn receive_msg(deps: DepsMut, info: MessageInfo, msg: IbcMailMessage, app: App) -> ClientResult { +fn receive_msg( + deps: DepsMut, + info: MessageInfo, + app: App, + received: ReceivedMessage, +) -> ClientResult { + ensure_server_sender(deps.as_ref(), &app, info.sender)?; + ensure_correct_recipient(deps.as_ref(), &received.header.recipient, &app)?; + + let msg_id = received.header.id.clone(); + RECEIVED.save(deps.storage, msg_id.clone(), &received)?; + + Ok(app + .response("received") + .add_attribute("message_id", &msg_id)) +} +// # ANCHOR_END: receive_msg + +fn update_delivery_status( + deps: DepsMut, + info: MessageInfo, + app: App, + id: MessageHash, + status: DeliveryStatus, +) -> ClientResult { + ensure_server_sender(deps.as_ref(), &app, info.sender)?; + + // ensure that the message exists + SENT.load(deps.storage, id.clone()) + .map_err(|_| ClientError::MessageNotFound(id.clone()))?; + SENT_STATUS.save(deps.storage, id.clone(), &status)?; + + Ok(app + .response("update_msg_status") + .add_attribute("message_id", &id) + .add_attribute("status", status.to_string())) +} + +fn ensure_server_sender(deps: Deps, app: &ClientApp, sender: Addr) -> Result<(), ClientError> { let sender_module = app - .module_registry(deps.as_ref())? - .module_info(info.sender) + .module_registry(deps)? + .module_info(sender) .map_err(|_| ClientError::NotMailServer {})?; + ensure_eq!( sender_module.info.id(), IBCMAIL_SERVER_ID, ClientError::NotMailServer {} ); - - ensure_correct_recipient(deps.as_ref(), &msg.message.recipient, &app)?; - - RECEIVED.save(deps.storage, msg.id.clone(), &msg)?; - - Ok(app - .response("received") - .add_attribute("message_id", &msg.id)) + Ok(()) } -// # ANCHOR_END: receive_msg fn ensure_correct_recipient( deps: Deps, diff --git a/contracts/client/src/handlers/instantiate.rs b/contracts/client/src/handlers/instantiate.rs new file mode 100644 index 0000000..f8fe31f --- /dev/null +++ b/contracts/client/src/handlers/instantiate.rs @@ -0,0 +1,24 @@ +use crate::{ + contract::{App, ClientResult}, + CLIENT_FEATURES, +}; +use abstract_app::traits::AbstractResponse; +use cosmwasm_std::{DepsMut, Env, MessageInfo}; +use ibcmail::client::msg::ClientInstantiateMsg; +use ibcmail::client::state::FEATURES; + +pub fn instantiate_handler( + deps: DepsMut, + _env: Env, + info: MessageInfo, + app: App, + _msg: ClientInstantiateMsg, +) -> ClientResult { + for feature in CLIENT_FEATURES { + FEATURES.save(deps.storage, feature.to_string(), &true)?; + } + + Ok(app + .response("instantiate") + .add_attribute("features", CLIENT_FEATURES.join(","))) +} diff --git a/contracts/client/src/handlers/mod.rs b/contracts/client/src/handlers/mod.rs index 0f15788..bd14bb0 100644 --- a/contracts/client/src/handlers/mod.rs +++ b/contracts/client/src/handlers/mod.rs @@ -1,7 +1,9 @@ pub mod execute; +pub mod instantiate; pub mod migrate; pub mod query; pub use crate::handlers::{ - execute::execute_handler, migrate::migrate_handler, query::query_handler, + execute::execute_handler, instantiate::instantiate_handler, migrate::migrate_handler, + query::query_handler, }; diff --git a/contracts/client/src/handlers/query.rs b/contracts/client/src/handlers/query.rs index 9b38d3f..af7472c 100644 --- a/contracts/client/src/handlers/query.rs +++ b/contracts/client/src/handlers/query.rs @@ -1,18 +1,19 @@ +use crate::{ + contract::{App, ClientResult}, + msg::ClientQueryMsg, +}; use abstract_app::sdk::cw_helpers::load_many; use cosmwasm_std::{to_json_binary, Binary, Deps, Env}; use cw_storage_plus::Bound; +use ibcmail::client::msg::SentMessagesResponse; +use ibcmail::client::state::SENT_STATUS; use ibcmail::{ client::{ error::ClientError, - msg::{MessageFilter, MessagesResponse}, + msg::{MessageFilter, ReceivedMessagesResponse}, state::{RECEIVED, SENT}, }, - MessageHash, MessageStatus, -}; - -use crate::{ - contract::{App, ClientResult}, - msg::ClientQueryMsg, + MessageHash, SentMessage, }; pub fn query_handler( @@ -22,17 +23,20 @@ pub fn query_handler( msg: ClientQueryMsg, ) -> ClientResult { match msg { - ClientQueryMsg::Messages { status, ids } => { - to_json_binary(&query_messages(deps, status, ids)?) + ClientQueryMsg::ReceivedMessages { ids } => { + to_json_binary(&query_received_messages(deps, ids)?) } - ClientQueryMsg::ListMessages { - status, + ClientQueryMsg::ListSentMessages { filter, start_after, limit, - } => to_json_binary(&query_messages_list( + } => to_json_binary(&query_sent_messages_list(deps, filter, start_after, limit)?), + ClientQueryMsg::ListReceivedMessages { + filter, + start_after, + limit, + } => to_json_binary(&query_received_messages_list( deps, - status, filter, start_after, limit, @@ -41,43 +45,56 @@ pub fn query_handler( .map_err(Into::into) } -fn query_messages( +fn query_received_messages( deps: Deps, - status: MessageStatus, ids: Vec, -) -> ClientResult { - let map = match status { - MessageStatus::Received => RECEIVED, - MessageStatus::Sent => SENT, - _ => return Err(ClientError::NotImplemented("message type".to_string())), - }; - - let messages = load_many(map, deps.storage, ids)?; +) -> ClientResult { + let messages = load_many(RECEIVED, deps.storage, ids)?; let messages = messages.into_iter().map(|(_, m)| m).collect(); - Ok(MessagesResponse { messages }) + Ok(ReceivedMessagesResponse { messages }) } -fn query_messages_list( +fn query_sent_messages_list( deps: Deps, - status: MessageStatus, _filter: Option, start: Option, limit: Option, -) -> ClientResult { - let map = match status { - MessageStatus::Received => RECEIVED, - MessageStatus::Sent => SENT, - _ => return Err(ClientError::NotImplemented("message type".to_string())), - }; +) -> ClientResult { + let messages = cw_paginate::paginate_map( + &SENT, + deps.storage, + start.as_ref().map(Bound::exclusive), + limit, + |_id, message| Ok::<_, ClientError>(message), + )?; + let mut result = vec![]; + for (message, header) in messages { + let status = SENT_STATUS.load(deps.storage, header.id.clone())?; + result.push(SentMessage { + message, + header, + status, + }); + } + + Ok(SentMessagesResponse { messages: result }) +} + +fn query_received_messages_list( + deps: Deps, + _filter: Option, + start: Option, + limit: Option, +) -> ClientResult { let messages = cw_paginate::paginate_map( - &map, + &RECEIVED, deps.storage, start.as_ref().map(Bound::exclusive), limit, |_id, message| Ok::<_, ClientError>(message), )?; - Ok(MessagesResponse { messages }) + Ok(ReceivedMessagesResponse { messages }) } diff --git a/contracts/client/src/lib.rs b/contracts/client/src/lib.rs index dc5ddc3..f4c64ef 100644 --- a/contracts/client/src/lib.rs +++ b/contracts/client/src/lib.rs @@ -4,9 +4,11 @@ mod handlers; /// The version of your app pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const CLIENT_FEATURES: &[&str] = &[DELIVERY_STATUS_FEATURE]; #[cfg(feature = "interface")] pub use contract::interface::ClientInterface; #[cfg(feature = "interface")] pub use ibcmail::client::msg::{ClientExecuteMsgFns, ClientQueryMsgFns}; pub use ibcmail::client::{error, msg, state}; +use ibcmail::features::DELIVERY_STATUS_FEATURE; diff --git a/contracts/server/src/contract.rs b/contracts/server/src/contract.rs index 75b44af..e6c9297 100644 --- a/contracts/server/src/contract.rs +++ b/contracts/server/src/contract.rs @@ -3,19 +3,24 @@ pub use ibcmail::server::ServerAdapter as Adapter; use ibcmail::{server::error::ServerError, IBCMAIL_SERVER_ID}; use ibcmail::{server::msg::ServerInstantiateMsg, IBCMAIL_CLIENT_ID}; -use crate::{handlers, APP_VERSION}; +use crate::{handlers, replies, APP_VERSION}; +use crate::replies::DELIVER_MESSAGE_REPLY; use abstract_adapter::objects::dependency::StaticDependency; pub const MAIL_CLIENT: StaticDependency = StaticDependency::new(IBCMAIL_CLIENT_ID, &[]); +pub const IBC_CLIENT: StaticDependency = StaticDependency::new("abstract:ibc-client", &[]); /// The type of the result returned by your client's entry points. pub type ServerResult = Result; const ADAPTER: Adapter = Adapter::new(IBCMAIL_SERVER_ID, APP_VERSION, None) + .with_instantiate(handlers::instantiate_handler) .with_execute(handlers::execute_handler) .with_module_ibc(handlers::module_ibc_handler) - .with_dependencies(&[MAIL_CLIENT]); + .with_ibc_callback(handlers::ibc_callback_handler) + .with_replies(&[(DELIVER_MESSAGE_REPLY, replies::deliver_message_reply)]) + .with_dependencies(&[MAIL_CLIENT, IBC_CLIENT]); // Export handlers #[cfg(feature = "export")] diff --git a/contracts/server/src/handlers/execute.rs b/contracts/server/src/handlers/execute.rs index 8c0e94f..d2bc33a 100644 --- a/contracts/server/src/handlers/execute.rs +++ b/contracts/server/src/handlers/execute.rs @@ -1,28 +1,33 @@ -use abstract_adapter::objects::TruncatedChainId; -use abstract_adapter::sdk::{ - features::ModuleIdentification, AccountVerification, ModuleRegistryInterface, -}; -use abstract_adapter::std::version_control::AccountBase; -use abstract_adapter::std::{ - ibc_client, - objects::{account::AccountTrace, module::ModuleInfo}, - version_control::NamespaceResponse, - IBC_CLIENT, +use abstract_adapter::{ + objects::TruncatedChainId, + sdk::{features::ModuleIdentification, AccountVerification, ModuleRegistryInterface}, + std::ibc::Callback, + std::version_control::AccountBase, + std::{ + ibc_client, + objects::{account::AccountTrace, module::ModuleInfo}, + IBC_CLIENT, + }, + traits::{AbstractResponse, AccountIdentification}, }; -use abstract_adapter::traits::AbstractResponse; use cosmwasm_std::{ - to_json_binary, wasm_execute, Addr, CosmosMsg, Deps, DepsMut, Env, MessageInfo, + ensure_eq, to_json_binary, wasm_execute, Addr, CosmosMsg, Deps, DepsMut, Empty, Env, + MessageInfo, StdResult, SubMsg, }; -use ibcmail::client::api::MailClient; + use ibcmail::{ - client::api::ClientInterface, + client::{api::ClientInterface, api::MailClient, state::FEATURES}, + features::DELIVERY_STATUS_FEATURE, server::{ - msg::{ServerExecuteMsg, ServerIbcMessage}, + msg::{ServerExecuteMsg, ServerIbcMessage, ServerMessage}, + state::{AWAITING, AWAITING_DELIVERY}, ServerAdapter, }, - Header, IbcMailMessage, Recipient, Route, + ClientMetadata, DeliveryFailure, DeliveryStatus, Header, MailMessage, Recipient, Route, Sender, + ServerMetadata, }; +use crate::replies::DELIVER_MESSAGE_REPLY; use crate::{ contract::{Adapter, ServerResult}, error::ServerError, @@ -37,26 +42,72 @@ pub fn execute_handler( msg: ServerExecuteMsg, ) -> ServerResult { match msg { - ServerExecuteMsg::ProcessMessage { msg, route } => { - process_message(deps, env, info, msg, route, app) - } + ServerExecuteMsg::ProcessMessage { + message, + header, + metadata, + } => process_message(deps, env, info, app, message, header, metadata), } } // ANCHOR_END: execute_handler +fn check_sender( + deps: Deps, + module: &Adapter, + current_chain: &TruncatedChainId, + sender_to_check: Sender, +) -> ServerResult { + let expected_sender = module + .account_id(deps) + .map_err(|_| ServerError::NoSenderAccount)?; + + match sender_to_check { + Sender::Account { id, chain } => { + ensure_eq!( + id, + expected_sender, + ServerError::MismatchedSender { + expected: expected_sender, + actual: id, + } + ); + + Ok(Sender::account(id, Some(current_chain.clone()))) + } + Sender::Server { address, chain } => { + panic!("Server sender not implemented"); + } + _ => Err(ServerError::NotImplemented( + "Non-account senders not supported".to_string(), + )), + } +} + fn process_message( - deps: DepsMut, + mut deps: DepsMut, env: Env, _info: MessageInfo, - msg: IbcMailMessage, - route: Option, - mut app: Adapter, + mut module: Adapter, + message: MailMessage, + header: Header, + metadata: Option, ) -> ServerResult { - println!("processing message: {:?} with route {:?}", msg, route); + println!( + "processing message: {:?} with header: {:?}, metadata {:?}", + message, header, metadata + ); let current_chain = TruncatedChainId::new(&env); + let checked_sender = check_sender( + deps.as_ref(), + &module, + ¤t_chain, + header.sender.clone(), + )?; - let route: Route = if let Some(route) = route { + let client_metadata = metadata.unwrap_or_default(); + + let route: Route = if let Some(route) = client_metadata.route { Ok::<_, ServerError>(match route { Route::Local => Route::Local, Route::Remote(mut chains) => { @@ -69,20 +120,21 @@ fn process_message( Route::Remote(chains) } } else { - chains.insert(0, current_chain); + chains.insert(0, current_chain.clone()); Route::Remote(chains) } } }) } else { - println!("processing message recipient: {:?}", msg.message.recipient); - match msg.message.recipient.clone() { + // We weren't provided a route + println!("processing message recipient: {:?}", header.recipient); + match header.recipient.clone() { // TODO: add smarter routing Recipient::Account { id: _, chain } => Ok(chain.map_or(AccountTrace::Local, |chain| { if chain == current_chain { AccountTrace::Local } else { - AccountTrace::Remote(vec![current_chain, chain.clone()]) + AccountTrace::Remote(vec![current_chain.clone(), chain.clone()]) } })), Recipient::Namespace { @@ -92,7 +144,7 @@ fn process_message( if chain == current_chain { AccountTrace::Local } else { - AccountTrace::Remote(vec![current_chain, chain.clone()]) + AccountTrace::Remote(vec![current_chain.clone(), chain.clone()]) } })), _ => { @@ -103,106 +155,250 @@ fn process_message( } }?; - let metadata = Header { - current_hop: 0, - route, + let header = Header { + recipient: header.recipient.clone(), + id: header.id.clone(), + version: header.version.clone(), + sender: checked_sender, + timestamp: header.timestamp, + reply_to: None, }; - let msg = route_msg(deps, msg, metadata, &mut app)?; + let server_metadata = ServerMetadata { route }; + + let msgs = route_message( + deps.branch(), + &env, + &mut module, + ¤t_chain, + header.clone(), + server_metadata.clone(), + ServerMessage::mail(message), + )?; - Ok(app.response("route").add_message(msg)) + Ok(module.response("route").add_submessages(msgs)) } -pub(crate) fn route_msg( - deps: DepsMut, - msg: IbcMailMessage, +pub(crate) fn route_message( + mut deps: DepsMut, + env: &Env, + module: &mut ServerAdapter, + current_chain: &TruncatedChainId, header: Header, - app: &mut ServerAdapter, -) -> ServerResult { - println!("routing message: {:?}, metadata: {:?}", msg, header); + metadata: ServerMetadata, + message: ServerMessage, +) -> ServerResult> { + println!("routing message: {:?}, metadata: {:?}", message, header); + + let current_hop = metadata.current_hop(current_chain)?; - match header.route { - AccountTrace::Local => route_to_local_account(deps.as_ref(), msg, header, app), + let branched_deps = deps.branch(); + + let routing = match metadata.route { + AccountTrace::Local => route_to_local_account( + branched_deps, + module, + message, + header.clone(), + metadata.clone(), + ), AccountTrace::Remote(ref chains) => { println!("routing to chains: {:?}", chains); // check index of hop. If we are on the final hop, route to local account - if header.current_hop == (chains.len() - 1) as u32 { - println!("routing to local account: {:?}", chains); - return route_to_local_account(deps.as_ref(), msg.clone(), header, app); + if current_hop == (chains.len() - 1) as u32 { + println!("routing to local account: {:?}", chains.last().unwrap()); + route_to_local_account( + branched_deps, + module, + message.clone(), + header.clone(), + metadata.clone(), + ) + } else { + // TODO verify that the chain is a valid chain + let dest_chain = + chains + .get(current_hop as usize + 1) + .ok_or(ServerError::InvalidRoute { + route: metadata.route.clone(), + hop: current_hop, + })?; + + // Awaiting callback + // Save that we're awaiting callbacks from dest chain onwards. + AWAITING.save(branched_deps.storage, &header.id, dest_chain)?; + + let msg = remote_server_msg( + branched_deps, + module, + &ServerIbcMessage::RouteMessage { + msg: message, + header: header.clone(), + metadata: metadata.clone(), + }, + dest_chain, + )?; + Ok::, ServerError>(vec![SubMsg::new(msg)]) } - // TODO verify that the chain is a valid chain - - let current_module_info = ModuleInfo::from_id(app.module_id(), app.version().into())?; - - let dest_chain = - chains - .get(header.current_hop as usize + 1) - .ok_or(ServerError::InvalidRoute { - route: header.route.clone(), - hop: header.current_hop, - })?; - - // ANCHOR: ibc_client - // Call IBC client - let ibc_client_msg = ibc_client::ExecuteMsg::ModuleIbcAction { - host_chain: dest_chain.clone(), - target_module: current_module_info, - msg: to_json_binary(&ServerIbcMessage::RouteMessage { msg, header })?, - callback: None, - }; - - let ibc_client_addr: Addr = app - .module_registry(deps.as_ref())? - .query_module(ModuleInfo::from_id_latest(IBC_CLIENT)?)? - .reference - .unwrap_native()?; - - let msg: CosmosMsg = wasm_execute(ibc_client_addr, &ibc_client_msg, vec![])?.into(); - // ANCHOR_END: ibc_client - Ok::(msg) } + }; + + // ensure that the route is valid, otherwise send a status update + match routing { + Ok(msgs) => Ok(msgs), + Err(e) => match e { + ServerError::DeliveryFailure(delivery_failure) => send_delivery_status( + deps, + env, + module, + current_chain, + header, + metadata, + delivery_failure.into(), + ), + _ => Err(e), + }, } } +/// Route a mail message to an account on the local chain fn route_to_local_account( - deps: Deps, - msg: IbcMailMessage, - header: Header, + deps: DepsMut, app: &mut ServerAdapter, -) -> ServerResult { - println!("routing to local account: {:?}", msg.message.recipient); + msg: ServerMessage, + header: Header, + metadata: ServerMetadata, +) -> ServerResult> { + println!("routing to local account: {:?}", header.recipient); // This is a local message + match msg { + ServerMessage::Mail { message } => { + AWAITING_DELIVERY.update(deps.storage, |mut awaiting| -> StdResult> { + awaiting.push((header.clone(), metadata.clone())); + Ok(awaiting) + })?; - let recipient = msg.message.recipient.clone(); - - let account_id = match recipient { - Recipient::Account { id: account_id, .. } => Ok(account_id), - Recipient::Namespace { namespace, .. } => { - // TODO: this only allows for addressing recipients via namespace of their email account directly. - // If they have the email application installed on a sub-account, this will not be able to identify the sub-account. - let namespace_status = app - .module_registry(deps)? - .query_namespace(namespace.clone())?; - match namespace_status { - NamespaceResponse::Claimed(info) => Ok(info.account_id), - NamespaceResponse::Unclaimed {} => { - return Err(ServerError::UnclaimedNamespace(namespace)); - } + let mail_client = get_recipient_mail_client(deps.as_ref(), app, &header.recipient) + .map_err(|e| DeliveryFailure::RecipientNotFound)?; + + Ok(vec![SubMsg::reply_always( + mail_client.receive_msg(message, header, metadata)?, + DELIVER_MESSAGE_REPLY, + )]) + } + ServerMessage::DeliveryStatus { id, status } => { + println!( + "updating local delivery message status: recipient: {:?} status: {:?}", + header.recipient, status + ); + + let mail_client = get_recipient_mail_client(deps.as_ref(), app, &header.recipient)?; + let is_delivery_enabled = FEATURES + .query( + &deps.querier, + mail_client.module_address()?, + DELIVERY_STATUS_FEATURE.to_string(), + ) + .is_ok_and(|f| f.is_some_and(|f| f)); + + if is_delivery_enabled { + Ok(vec![SubMsg::new( + mail_client.update_msg_status(id, status)?, + )]) + } else { + Ok(vec![]) } } _ => Err(ServerError::NotImplemented( - "Non-account recipients not supported".to_string(), + "Unknown message type".to_string(), )), - }?; + } +} + +/// Set the target account for the message and get the mail client for the recipient +fn get_recipient_mail_client<'a>( + deps: Deps<'a>, + app: &'a mut ServerAdapter, + recipient: &Recipient, +) -> ServerResult> { + let account_id = recipient.resolve_account_id(app.module_registry(deps)?)?; // ANCHOR: set_acc_and_send // Set target account for actions, is used by APIs to retrieve mail client address. let recipient_acc: AccountBase = app.account_registry(deps)?.account_base(&account_id)?; app.target_account = Some(recipient_acc); - - let mail_client: MailClient<_> = app.mail_client(deps); - let msg: CosmosMsg = mail_client.receive_msg(msg, header)?; + Ok(app.mail_client::<'a>(deps)) // ANCHOR_END: set_acc_and_send +} + +/// Build a message to send to a server on the destination chain +fn remote_server_msg( + deps: DepsMut, + module: &ServerAdapter, + msg: &ServerIbcMessage, + dest_chain: &TruncatedChainId, +) -> ServerResult { + // ANCHOR: ibc_client + // Call IBC client + let current_module_info = ModuleInfo::from_id(module.module_id(), module.version().into())?; + + let ibc_client_msg = ibc_client::ExecuteMsg::ModuleIbcAction { + host_chain: dest_chain.clone(), + target_module: current_module_info, + msg: to_json_binary(msg)?, + callback: Some(Callback::new(&Empty {})?), + }; + + let ibc_client_addr: Addr = module + .module_registry(deps.as_ref())? + .query_module(ModuleInfo::from_id_latest(IBC_CLIENT)?)? + .reference + .unwrap_native()?; + + let msg: CosmosMsg = wasm_execute(ibc_client_addr, &ibc_client_msg, vec![])?.into(); + // ANCHOR_END: ibc_client + Ok(msg) +} + +/// Send a delivery status update to the consumer +pub(crate) fn send_delivery_status( + deps: DepsMut, + env: &Env, + module: &mut ServerAdapter, + current_chain: &TruncatedChainId, + origin_header: Header, + origin_metadata: ServerMetadata, + delivery_status: DeliveryStatus, +) -> Result, ServerError> { + let message_id = origin_header.id.clone(); + let delivery_message = ServerMessage::delivery_status(message_id.clone(), delivery_status); + + let delivery_header = Header { + sender: Sender::Server { + address: env.contract.address.to_string(), + chain: TruncatedChainId::new(env), + }, + recipient: origin_header.sender.try_into()?, + // TODO: new ID? + id: message_id.clone(), + // TODO: version? + version: origin_header.version, + timestamp: env.block.time, + reply_to: None, + }; + + let delivery_metadata = ServerMetadata { + route: origin_metadata.reverse_route()?, + }; + let msg = route_message( + deps, + env, + module, + current_chain, + delivery_header, + delivery_metadata, + delivery_message, + )?; Ok(msg) } diff --git a/contracts/server/src/handlers/ibc_callback.rs b/contracts/server/src/handlers/ibc_callback.rs new file mode 100644 index 0000000..a796269 --- /dev/null +++ b/contracts/server/src/handlers/ibc_callback.rs @@ -0,0 +1,156 @@ +use abstract_adapter::objects::TruncatedChainId; +use abstract_adapter::std::ibc::{Callback, IbcResult}; +use cosmwasm_std::{from_json, DepsMut, Env, Response, SubMsg}; + +use ibcmail::server::msg::ServerMessage; +use ibcmail::{ + server::{msg::ServerIbcMessage, ServerAdapter}, + DeliveryFailure, Header, Route, Sender, ServerMetadata, +}; + +use crate::contract::ServerResult; +use crate::handlers::execute; + +// ANCHOR: ibc_callback_handler +/// Handler for message callbacks. +/// We use this handler for sending message delivery updates to our clients. +pub fn ibc_callback_handler( + deps: DepsMut, + _env: Env, + mut app: ServerAdapter, + callback: Callback, + ibc_result: IbcResult, +) -> ServerResult { + // panic!("ibc_callback_handler: {:?}", callback); + println!( + "ibc_callback_handler callback: {:?} result, env: {:?}", + callback, _env + ); + + let msgs: Vec = match ibc_result { + // The destination server successfully processed the message + IbcResult::Execute { + result: Ok(_response), + initiator_msg, + } => { + println!("ibc_callback_handler execute success"); + let origin_msg: ServerIbcMessage = from_json(initiator_msg)?; + + match origin_msg { + // We successfully routed a packet, and need to send an update to the sender + ServerIbcMessage::RouteMessage { + msg, + header, + metadata, + } => { + println!( + "ibc_callback_handler success route_msg id: {:?}, header: {:?}, metadata: {:?}", + header.id, + header, + metadata + ); + vec![] + // vec![execute::update_message_status(deps, &mut app, msg.id, header, MessageStatus::Received)?] + } + _ => { + println!("Unknown message"); + vec![] + } + } + } + // The destination server failed to process the message + IbcResult::Execute { + result: Err(e), + initiator_msg, + } => { + println!("ibc_callback_handler execute error"); + // println!("ibc_callback_handler execute error: {:?}", e); + let origin_msg: ServerIbcMessage = from_json(initiator_msg)?; + match origin_msg { + ServerIbcMessage::RouteMessage { + msg, + header, + metadata, + } => { + println!( + "ibc_callback_handler execute error route_msg id: {:?}, header: {:?}, metadata: {:?}", + header.id, + header, + metadata + ); + let current_chain = TruncatedChainId::new(&_env); + let current_hop = metadata.current_hop(¤t_chain)?; + match msg { + // We failed to deliver a message, we send a failed status update to the sender + ServerMessage::Mail { ref message } => { + // archway juno neutron + // juno -> neutron failed current hop 1 + // expected: juno archway + // need to remove anything after the current hop + let status_header = Header { + sender: Sender::Server { + chain: current_chain.clone(), + address: _env.contract.address.to_string(), + }, + recipient: header.sender.clone().try_into()?, + // TODO: new message id + id: header.id.clone(), + version: header.version.clone(), + timestamp: _env.block.time, + reply_to: None, + }; + + let status_metadata = ServerMetadata { + route: match metadata.route { + Route::Remote(mut chains) => { + // keep the current hop but remove everything after it + chains.truncate(current_hop as usize + 1); + chains.reverse(); + Route::Remote(chains) + } + _ => Route::Local, + }, + }; + + let status_message = ServerMessage::delivery_status( + header.id, + DeliveryFailure::Unknown(e).into(), + ); + execute::route_message( + deps, + &_env, + &mut app, + ¤t_chain, + status_header, + status_metadata, + status_message, + )? + } + _ => { + println!( + "ibc_callback_handler execute error route_msg unknown message" + ); + vec![] + } + } + // We failed to route a message, we send a failed status update to the sender + } + _ => { + println!("unknown message"); + vec![] + } + } + } + IbcResult::FatalError(e) => { + println!("ibc_callback_handler fatal error: {:?}", e); + vec![] + } + _ => { + println!("unexpected callback result"); + vec![] + } + }; + + Ok(Response::default().add_submessages(msgs)) +} +// ANCHOR_END: ibc_callback_handler diff --git a/contracts/server/src/handlers/instantiate.rs b/contracts/server/src/handlers/instantiate.rs new file mode 100644 index 0000000..1c91530 --- /dev/null +++ b/contracts/server/src/handlers/instantiate.rs @@ -0,0 +1,19 @@ +use abstract_adapter::sdk::AbstractResponse; +use cosmwasm_std::{DepsMut, Env, MessageInfo}; + +use ibcmail::server::msg::ServerInstantiateMsg; +use ibcmail::server::state::AWAITING_DELIVERY; +use ibcmail::server::ServerAdapter; + +use crate::contract::ServerResult; + +pub fn instantiate_handler( + deps: DepsMut, + _env: Env, + info: MessageInfo, + app: ServerAdapter, + _msg: ServerInstantiateMsg, +) -> ServerResult { + AWAITING_DELIVERY.save(deps.storage, &vec![])?; + Ok(app.response("instantiate")) +} diff --git a/contracts/server/src/handlers/mod.rs b/contracts/server/src/handlers/mod.rs index efe2c62..16b92d6 100644 --- a/contracts/server/src/handlers/mod.rs +++ b/contracts/server/src/handlers/mod.rs @@ -1,4 +1,9 @@ pub mod execute; +pub mod ibc_callback; +pub mod instantiate; pub mod module_ibc; -pub use crate::handlers::{execute::execute_handler, module_ibc::module_ibc_handler}; +pub use crate::handlers::{ + execute::execute_handler, ibc_callback::ibc_callback_handler, instantiate::instantiate_handler, + module_ibc::module_ibc_handler, +}; diff --git a/contracts/server/src/handlers/module_ibc.rs b/contracts/server/src/handlers/module_ibc.rs index 7d9ea07..f98d1e5 100644 --- a/contracts/server/src/handlers/module_ibc.rs +++ b/contracts/server/src/handlers/module_ibc.rs @@ -1,3 +1,4 @@ +use abstract_adapter::objects::TruncatedChainId; use abstract_adapter::sdk::AbstractResponse; use abstract_adapter::std::ibc::ModuleIbcInfo; use cosmwasm_std::{from_json, Binary, DepsMut, Env}; @@ -7,12 +8,12 @@ use ibcmail::{ IBCMAIL_SERVER_ID, }; -use crate::{contract::ServerResult, handlers::execute::route_msg}; +use crate::{contract::ServerResult, handlers::execute::route_message}; // ANCHOR: module_ibc_handler pub fn module_ibc_handler( deps: DepsMut, - _env: Env, + env: Env, mut app: ServerAdapter, module_info: ModuleIbcInfo, msg: Binary, @@ -25,14 +26,26 @@ pub fn module_ibc_handler( let server_msg: ServerIbcMessage = from_json(msg)?; match server_msg { - ServerIbcMessage::RouteMessage { msg, mut header } => { - header.current_hop += 1; + ServerIbcMessage::RouteMessage { + msg, + header, + metadata, + } => { + let msgs = route_message( + deps, + &env, + &mut app, + &TruncatedChainId::new(&env), + header, + metadata, + msg, + )?; - let msg = route_msg(deps, msg, header, &mut app)?; - - Ok(app.response("module_ibc").add_message(msg)) + Ok(app + .response("module_ibc") + .add_attribute("method", "route") + .add_submessages(msgs)) } _ => Err(ServerError::UnauthorizedIbcMessage {}), } } -// ANCHOR_END: module_ibc_handler diff --git a/contracts/server/src/lib.rs b/contracts/server/src/lib.rs index ba08141..18232c9 100644 --- a/contracts/server/src/lib.rs +++ b/contracts/server/src/lib.rs @@ -1,5 +1,6 @@ pub mod contract; mod handlers; +mod replies; #[cfg(feature = "interface")] pub use contract::interface::ServerInterface; diff --git a/contracts/server/src/replies/deliver_msg.rs b/contracts/server/src/replies/deliver_msg.rs new file mode 100644 index 0000000..6ebd3c3 --- /dev/null +++ b/contracts/server/src/replies/deliver_msg.rs @@ -0,0 +1,44 @@ +use abstract_adapter::objects::TruncatedChainId; +use abstract_adapter::sdk::AbstractResponse; +use cosmwasm_std::{DepsMut, Env, Reply, SubMsgResult}; + +use ibcmail::server::state::AWAITING_DELIVERY; +use ibcmail::server::ServerAdapter; +use ibcmail::{DeliveryFailure, DeliveryStatus}; + +use crate::contract::ServerResult; +use crate::handlers::execute; +pub fn deliver_message_reply( + deps: DepsMut, + env: Env, + mut app: ServerAdapter, + reply: Reply, +) -> ServerResult { + let current_chain = TruncatedChainId::new(&env); + let delivery_status = match reply.result { + SubMsgResult::Ok(_) => DeliveryStatus::Delivered, + SubMsgResult::Err(error) => DeliveryFailure::Unknown(error).into(), + }; + + // Load the awaiting message + let mut awaiting_msgs = AWAITING_DELIVERY.load(deps.storage)?; + let (origin_header, origin_metadata) = awaiting_msgs.remove(0); + AWAITING_DELIVERY.save(deps.storage, &awaiting_msgs)?; + + let message_id = origin_header.id.clone(); + + let msg = execute::send_delivery_status( + deps, + &env, + &mut app, + ¤t_chain, + origin_header, + origin_metadata, + delivery_status, + )?; + + Ok(app + .response("deliver_message_reply") + .add_attribute("message_id", &message_id) + .add_submessages(msg)) +} diff --git a/contracts/server/src/replies/mod.rs b/contracts/server/src/replies/mod.rs new file mode 100644 index 0000000..3e3d582 --- /dev/null +++ b/contracts/server/src/replies/mod.rs @@ -0,0 +1,7 @@ +mod deliver_msg; + +pub use deliver_msg::deliver_message_reply; + +pub const DELIVER_MESSAGE_REPLY: u64 = 1u64; + +pub const STATUS_UPDATE_REPLY: u64 = 2u64; diff --git a/packages/ibcmail/src/client/api.rs b/packages/ibcmail/src/client/api.rs index 13408a1..d6e1fdb 100644 --- a/packages/ibcmail/src/client/api.rs +++ b/packages/ibcmail/src/client/api.rs @@ -1,11 +1,10 @@ use abstract_adapter::{sdk::AbstractSdkResult, std::objects::module::ModuleId}; - use abstract_app::sdk::AppInterface; - -use cosmwasm_std::{CosmosMsg, Deps}; +use cosmwasm_std::{Addr, CosmosMsg, Deps}; use crate::{ - client::msg::ClientExecuteMsg, Header, IbcMailMessage, Message, Route, IBCMAIL_CLIENT_ID, + client::msg::ClientExecuteMsg, ClientMetadata, DeliveryStatus, Header, MailMessage, + MessageHash, ReceivedMessage, Recipient, ServerMetadata, IBCMAIL_CLIENT_ID, }; // API for Abstract SDK users @@ -35,6 +34,12 @@ impl<'a, T: ClientInterface> MailClient<'a, T> { self.module_id } + pub fn module_address(&self) -> AbstractSdkResult { + self.base + .modules(self.deps) + .module_address(self.module_id()) + } + // Execute a request on the ibc mail client fn request(&self, msg: ClientExecuteMsg) -> AbstractSdkResult { let apps = self.base.apps(self.deps); @@ -42,16 +47,39 @@ impl<'a, T: ClientInterface> MailClient<'a, T> { } /// Send message - pub fn send_msg(&self, message: Message, route: Option) -> AbstractSdkResult { - self.request(ClientExecuteMsg::SendMessage { message, route }) + pub fn send_msg( + &self, + recipient: Recipient, + message: MailMessage, + metadata: Option, + ) -> AbstractSdkResult { + self.request(ClientExecuteMsg::SendMessage { + recipient, + message, + metadata, + }) } /// Receive message pub fn receive_msg( &self, - message: IbcMailMessage, - _header: Header, + message: MailMessage, + header: Header, + metadata: ServerMetadata, + ) -> AbstractSdkResult { + self.request(ClientExecuteMsg::ReceiveMessage(ReceivedMessage { + message, + header, + metadata, + })) + } + + /// Receive message + pub fn update_msg_status( + &self, + id: MessageHash, + status: DeliveryStatus, ) -> AbstractSdkResult { - self.request(ClientExecuteMsg::ReceiveMessage(message)) + self.request(ClientExecuteMsg::UpdateDeliveryStatus { id, status }) } } diff --git a/packages/ibcmail/src/client/error.rs b/packages/ibcmail/src/client/error.rs index 125f673..f93795c 100644 --- a/packages/ibcmail/src/client/error.rs +++ b/packages/ibcmail/src/client/error.rs @@ -1,3 +1,4 @@ +use crate::MessageHash; use abstract_app::{sdk::AbstractSdkError, std::AbstractError, AppError as AbstractAppError}; use cosmwasm_std::StdError; use cw_asset::AssetError; @@ -30,6 +31,9 @@ pub enum ClientError { #[error("Recipient is not the current account")] NotRecipient {}, + #[error("Message not found: {0}")] + MessageNotFound(MessageHash), + #[error("{0} is not implemented")] NotImplemented(String), } diff --git a/packages/ibcmail/src/client/msg.rs b/packages/ibcmail/src/client/msg.rs index 585aa60..f97f08d 100644 --- a/packages/ibcmail/src/client/msg.rs +++ b/packages/ibcmail/src/client/msg.rs @@ -1,7 +1,8 @@ use cosmwasm_schema::QueryResponses; use crate::{ - client::ClientApp, IbcMailMessage, Message, MessageHash, MessageStatus, Route, Sender, + client::ClientApp, ClientMetadata, DeliveryStatus, MailMessage, MessageHash, ReceivedMessage, + Recipient, Sender, SentMessage, }; // This is used for type safety and re-exporting the contract endpoint structs. @@ -17,12 +18,18 @@ pub struct ClientInstantiateMsg {} #[derive(cw_orch::ExecuteFns)] #[cw_orch(impl_into(ExecuteMsg))] pub enum ClientExecuteMsg { - /// Receive a message from the server - ReceiveMessage(IbcMailMessage), + /// Receive a message from the server. + ReceiveMessage(ReceivedMessage), + /// Update the status of a message. only callable by the server + UpdateDeliveryStatus { + id: MessageHash, + status: DeliveryStatus, + }, /// Send a message SendMessage { - message: Message, - route: Option, + recipient: Recipient, + message: MailMessage, + metadata: Option, }, } // # ANCHOR_END: execute_msg @@ -33,18 +40,20 @@ pub enum ClientExecuteMsg { #[cw_orch(impl_into(QueryMsg))] #[derive(QueryResponses)] pub enum ClientQueryMsg { - #[returns(MessagesResponse)] - ListMessages { - status: MessageStatus, + #[returns(SentMessagesResponse)] + ListSentMessages { filter: Option, limit: Option, start_after: Option, }, - #[returns(MessagesResponse)] - Messages { - status: MessageStatus, - ids: Vec, + #[returns(ReceivedMessagesResponse)] + ListReceivedMessages { + filter: Option, + limit: Option, + start_after: Option, }, + #[returns(ReceivedMessagesResponse)] + ReceivedMessages { ids: Vec }, } #[cosmwasm_schema::cw_serde] @@ -59,6 +68,16 @@ pub struct AppMigrateMsg {} pub struct ConfigResponse {} #[cosmwasm_schema::cw_serde] -pub struct MessagesResponse { - pub messages: Vec, +pub struct SentMessagesResponse { + pub messages: Vec, +} + +#[cosmwasm_schema::cw_serde] +pub struct ReceivedMessagesResponse { + pub messages: Vec, +} + +#[cosmwasm_schema::cw_serde] +pub struct MessageStatusesResponse { + pub statuses: Vec<(MessageHash, DeliveryStatus)>, } diff --git a/packages/ibcmail/src/client/state.rs b/packages/ibcmail/src/client/state.rs index ae07a43..c6f5582 100644 --- a/packages/ibcmail/src/client/state.rs +++ b/packages/ibcmail/src/client/state.rs @@ -1,7 +1,11 @@ use cw_storage_plus::Map; -use crate::{IbcMailMessage, MessageHash}; +use crate::{DeliveryStatus, Header, MailMessage, MessageHash, ReceivedMessage}; // TODO: use an indexed map in the future -pub const RECEIVED: Map = Map::new("received"); -pub const SENT: Map = Map::new("sent"); +pub const RECEIVED: Map = Map::new("received"); +pub const SENT: Map = Map::new("sent"); +pub const SENT_STATUS: Map = Map::new("status"); + +/// Set of features supported by the client +pub const FEATURES: Map = Map::new("features"); diff --git a/packages/ibcmail/src/features.rs b/packages/ibcmail/src/features.rs new file mode 100644 index 0000000..98c9cbe --- /dev/null +++ b/packages/ibcmail/src/features.rs @@ -0,0 +1 @@ +pub const DELIVERY_STATUS_FEATURE: &str = "delivery"; diff --git a/packages/ibcmail/src/lib.rs b/packages/ibcmail/src/lib.rs index 261d7d2..cdc402c 100644 --- a/packages/ibcmail/src/lib.rs +++ b/packages/ibcmail/src/lib.rs @@ -1,11 +1,19 @@ pub mod client; +pub mod features; pub mod server; +use crate::server::error::ServerError; +use abstract_adapter::sdk::ModuleRegistryInterface; +use abstract_adapter::std::version_control::NamespaceResponse; use abstract_app::objects::TruncatedChainId; +use abstract_app::sdk::ModuleRegistry; use abstract_app::std::objects::AccountId; use abstract_app::std::objects::{account::AccountTrace, namespace::Namespace}; use const_format::concatcp; -use cosmwasm_std::Timestamp; +use cosmwasm_std::{StdError, StdResult, Timestamp}; +use std::fmt; +use std::fmt::{Display, Formatter}; +use thiserror::Error; pub const IBCMAIL_NAMESPACE: &str = "ibcmail"; pub const IBCMAIL_CLIENT_ID: &str = concatcp!(IBCMAIL_NAMESPACE, ":", "client"); @@ -18,39 +26,89 @@ pub type MessageHash = String; /// Struct representing new message to send to another client // # ANCHOR: message #[cosmwasm_schema::cw_serde] -pub struct Message { - pub recipient: Recipient, +pub struct MailMessage { pub subject: String, pub body: String, } // # ANCHOR_END: message -impl Message { - pub fn new(recipient: Recipient, subject: impl Into, body: impl Into) -> Self { +#[cosmwasm_schema::cw_serde] +pub struct SentMessage { + pub message: MailMessage, + pub header: Header, + pub status: DeliveryStatus, +} + +#[cosmwasm_schema::cw_serde] +pub struct ReceivedMessage { + pub message: MailMessage, + pub header: Header, + pub metadata: ServerMetadata, +} + +impl MailMessage { + pub fn new(subject: impl Into, body: impl Into) -> Self { Self { - recipient, subject: subject.into(), body: body.into(), } } } - #[cosmwasm_schema::cw_serde] -pub struct IbcMailMessage { - pub id: MessageHash, +pub struct Header { pub sender: Sender, + pub recipient: Recipient, + pub id: MessageHash, pub version: String, pub timestamp: Timestamp, - pub message: Message, + pub reply_to: Option, } +pub type Route = AccountTrace; + +/// Metadata that can be set optionally by the client when they send a message +#[derive(Default)] #[cosmwasm_schema::cw_serde] -pub struct Header { - pub current_hop: u32, +pub struct ClientMetadata { + pub route: Option, +} + +impl ClientMetadata { + pub fn new_with_route(route: Route) -> Self { + Self { route: Some(route) } + } +} + +/// Metadata used by the server for routing or other means +#[cosmwasm_schema::cw_serde] +pub struct ServerMetadata { pub route: Route, } -pub type Route = AccountTrace; +impl ServerMetadata { + pub fn reverse_route(&self) -> StdResult { + match self.route.clone() { + Route::Remote(mut route) => { + route.reverse(); + Ok(Route::Remote(route)) + } + Route::Local => Ok(Route::Local), + } + } + + pub fn current_hop(&self, current_chain: &TruncatedChainId) -> StdResult { + match self.route { + Route::Local => Ok(0), + Route::Remote(ref route) => { + let position = route.iter().position(|chain| chain == current_chain); + match position { + Some(position) => Ok(position as u32), + None => Err(StdError::generic_err("Current chain not in route")), + } + } + } + } +} #[non_exhaustive] #[cosmwasm_schema::cw_serde] @@ -63,6 +121,10 @@ pub enum Recipient { namespace: Namespace, chain: Option, }, + Server { + chain: TruncatedChainId, + address: String, + }, } impl From for Recipient { @@ -84,6 +146,29 @@ impl Recipient { pub fn namespace(namespace: Namespace, chain: Option) -> Self { Recipient::Namespace { namespace, chain } } + + pub fn resolve_account_id( + &self, + module_registry: ModuleRegistry, + ) -> Result { + match self { + Recipient::Account { id: account_id, .. } => Ok(account_id.clone()), + Recipient::Namespace { namespace, .. } => { + // TODO: this only allows for addressing recipients via namespace of their email account directly. + // If they have the email application installed on a sub-account, this will not be able to identify the sub-account. + let namespace_status = module_registry.query_namespace(namespace.clone())?; + match namespace_status { + NamespaceResponse::Claimed(info) => Ok(info.account_id), + NamespaceResponse::Unclaimed {} => { + Err(ServerError::UnclaimedNamespace(namespace.clone())) + } + } + } + _ => Err(ServerError::NotImplemented( + "Non-account recipients not supported".to_string(), + )), + } + } } #[non_exhaustive] @@ -93,6 +178,11 @@ pub enum Sender { id: AccountId, chain: Option, }, + Server { + chain: TruncatedChainId, + // String because it's a different chain + address: String, + }, } impl Sender { @@ -104,9 +194,63 @@ impl Sender { } } -#[non_exhaustive] +impl TryFrom for Recipient { + type Error = StdError; + + fn try_from(sender: Sender) -> Result { + match sender { + Sender::Account { id, chain } => Ok(Recipient::Account { id, chain }), + Sender::Server { chain, address } => Ok(Recipient::Server { chain, address }), + _ => Err(StdError::generic_err("Cannot convert Sender to Recipient")), + } + } +} + #[cosmwasm_schema::cw_serde] -pub enum MessageStatus { +pub enum MessageKind { Sent, Received, } + +impl Display for MessageKind { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + MessageKind::Sent => write!(f, "Sent"), + MessageKind::Received => write!(f, "Received"), + } + } +} + +#[derive(Error)] +#[non_exhaustive] +#[cosmwasm_schema::cw_serde] +pub enum DeliveryFailure { + #[error("Recipient not found")] + RecipientNotFound, + #[error("Unknown failure: {0}")] + Unknown(String), +} + +#[non_exhaustive] +#[cosmwasm_schema::cw_serde] +pub enum DeliveryStatus { + Sent, + Delivered, + Failure(DeliveryFailure), +} + +impl From for DeliveryStatus { + fn from(failure: DeliveryFailure) -> Self { + DeliveryStatus::Failure(failure) + } +} + +impl Display for DeliveryStatus { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + DeliveryStatus::Sent => write!(f, "Sent"), + DeliveryStatus::Delivered => write!(f, "Received"), + DeliveryStatus::Failure(failure) => write!(f, "Failed: {}", failure), + } + } +} diff --git a/packages/ibcmail/src/server/api.rs b/packages/ibcmail/src/server/api.rs index 7342698..02c0906 100644 --- a/packages/ibcmail/src/server/api.rs +++ b/packages/ibcmail/src/server/api.rs @@ -10,7 +10,7 @@ use cosmwasm_std::{CosmosMsg, Deps, Uint128}; use crate::{ server::msg::{ServerExecuteMsg, ServerQueryMsg}, - IbcMailMessage, Route, IBCMAIL_SERVER_ID, + ClientMetadata, Header, MailMessage, IBCMAIL_SERVER_ID, }; // API for Abstract SDK users @@ -49,10 +49,15 @@ impl<'a, T: ServerInterface> MailServer<'a, T> { pub fn process_msg( &self, - msg: IbcMailMessage, - route: Option, + message: MailMessage, + header: Header, + metadata: Option, ) -> AbstractSdkResult { - self.request(ServerExecuteMsg::ProcessMessage { msg, route }) + self.request(ServerExecuteMsg::ProcessMessage { + message, + header, + metadata, + }) } } diff --git a/packages/ibcmail/src/server/error.rs b/packages/ibcmail/src/server/error.rs index f188b92..e25273d 100644 --- a/packages/ibcmail/src/server/error.rs +++ b/packages/ibcmail/src/server/error.rs @@ -1,3 +1,5 @@ +use crate::{DeliveryFailure, MessageHash}; +use abstract_adapter::objects::AccountId; use abstract_adapter::std::ibc::ModuleIbcInfo; use abstract_adapter::{ sdk::AbstractSdkError, std::AbstractError, AdapterError as AbstractAdapterError, @@ -25,6 +27,9 @@ pub enum ServerError { #[error("{0}")] Admin(#[from] AdminError), + #[error("{0}")] + DeliveryFailure(#[from] DeliveryFailure), + #[error("{0}")] AdapterError(#[from] AbstractAdapterError), @@ -42,4 +47,16 @@ pub enum ServerError { #[error("Unclaimed namespace: {0}")] UnclaimedNamespace(Namespace), + + #[error("Awaited message not found: {0}")] + AwaitedMsgNotFound(MessageHash), + + #[error("No sending account")] + NoSenderAccount, + + #[error("Mismatched sender. Expected: {expected:?}, Actual: {actual:?}")] + MismatchedSender { + expected: AccountId, + actual: AccountId, + }, } diff --git a/packages/ibcmail/src/server/mod.rs b/packages/ibcmail/src/server/mod.rs index 63ea5f0..294af68 100644 --- a/packages/ibcmail/src/server/mod.rs +++ b/packages/ibcmail/src/server/mod.rs @@ -8,6 +8,7 @@ use crate::server::{ pub mod api; pub mod error; pub mod msg; +pub mod state; /// The type of the client that is used to build your client and access the Abstract SDK features. pub type ServerAdapter = diff --git a/packages/ibcmail/src/server/msg.rs b/packages/ibcmail/src/server/msg.rs index ff63277..1cb7dc7 100644 --- a/packages/ibcmail/src/server/msg.rs +++ b/packages/ibcmail/src/server/msg.rs @@ -1,6 +1,9 @@ use cosmwasm_schema::QueryResponses; -use crate::{server::ServerAdapter, Header, IbcMailMessage, Route}; +use crate::{ + server::ServerAdapter, ClientMetadata, DeliveryStatus, Header, MailMessage, MessageHash, + ServerMetadata, +}; // This is used for type safety and re-exporting the contract endpoint structs. abstract_adapter::adapter_msg_types!(ServerAdapter, ServerExecuteMsg, ServerQueryMsg); @@ -12,19 +15,58 @@ pub struct ServerInstantiateMsg {} /// App execute messages #[cosmwasm_schema::cw_serde] pub enum ServerExecuteMsg { - /// Route a message + /// Process a message sent by the client ProcessMessage { - msg: IbcMailMessage, - route: Option, + message: MailMessage, + header: Header, + metadata: Option, }, } +#[non_exhaustive] +#[cosmwasm_schema::cw_serde] +pub enum ServerMessage { + Mail { + message: MailMessage, + }, + DeliveryStatus { + id: MessageHash, + status: DeliveryStatus, + }, +} + +impl ServerMessage { + pub fn mail(message: MailMessage) -> Self { + ServerMessage::Mail { message } + } + + pub fn delivery_status(id: MessageHash, status: DeliveryStatus) -> Self { + ServerMessage::DeliveryStatus { id, status } + } +} + /// App execute messages #[non_exhaustive] #[cosmwasm_schema::cw_serde] pub enum ServerIbcMessage { /// Route a message - RouteMessage { msg: IbcMailMessage, header: Header }, + RouteMessage { + msg: ServerMessage, + header: Header, + metadata: ServerMetadata, + }, +} + +/// App execute messages +#[non_exhaustive] +#[cosmwasm_schema::cw_serde] +pub enum ServerCallbackMessage { + /// Update a message + UpdateMessage { + id: MessageHash, + header: Header, + status: DeliveryStatus, + }, } /// App query messages diff --git a/packages/ibcmail/src/server/state.rs b/packages/ibcmail/src/server/state.rs new file mode 100644 index 0000000..d51b166 --- /dev/null +++ b/packages/ibcmail/src/server/state.rs @@ -0,0 +1,6 @@ +use crate::{Header, MessageHash, ServerMetadata}; +use abstract_app::objects::TruncatedChainId; +use cw_storage_plus::{Item, Map}; + +pub const AWAITING: Map<&MessageHash, TruncatedChainId> = Map::new("awaiting"); +pub const AWAITING_DELIVERY: Item> = Item::new("awaiting_delivery"); diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 2bb32c9..7a967ab 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -15,7 +15,7 @@ default = [] abstract-app = { workspace = true, features = ["test-utils"] } abstract-cw-orch-polytone = "4.0.1" abstract-interface = { workspace = true } -abstract-interchain-tests = { git = "https://github.com/AbstractSDK/abstract", version = "0.23.0", tag = "v0.23.0" } +#abstract-interchain-tests = { git = "https://github.com/AbstractSDK/abstract", version = "=0.23.0", tag = "v0.23.0" } #abstract-interchain-tests = { git = "https://github.com/AbstractSDK/abstract", version = "0.22.1", branch = "removemm" } #cw-orch = { workspace = true } cw-orch-interchain = { version = "0.3.1", features = ["daemon"] } @@ -30,7 +30,8 @@ client = { workspace = true, features = ["interface"] } speculoos = { workspace = true } server = { workspace = true, features = ["interface"] } abstract-client = { workspace = true, features = ["interchain"] } -clap.workspace = true -env_logger = "0.11.3" +clap = { workspace = true, features = ["derive"] } dotenv = { workspace = true } + +env_logger = { workspace = true } anyhow = "1.0.86" diff --git a/tests/src/bin/approve.rs b/tests/src/bin/approve.rs index 1977c60..c667c8d 100644 --- a/tests/src/bin/approve.rs +++ b/tests/src/bin/approve.rs @@ -13,8 +13,7 @@ use abstract_interface::Abstract; use clap::Parser; use cw_orch::{ anyhow, - environment::TxHandler, - prelude::{*, DaemonBuilder, networks::parse_network}, + prelude::{networks::parse_network, DaemonBuilder, *}, tokio::runtime::Runtime, }; @@ -32,7 +31,8 @@ fn publish(networks: Vec) -> anyhow::Result<()> { // Create an [`AbstractClient`] let abs = Abstract::new(chain.clone()); - abs.version_control.approve_all_modules_for_namespace(app_namespace)?; + abs.version_control + .approve_all_modules_for_namespace(app_namespace)?; } Ok(()) } diff --git a/tests/src/bin/demo.rs b/tests/src/bin/demo.rs index f2e6712..e4b9c3c 100644 --- a/tests/src/bin/demo.rs +++ b/tests/src/bin/demo.rs @@ -9,7 +9,7 @@ use cw_orch_interchain::{ChannelCreationValidator, DaemonInterchainEnv, Intercha use networks::{HARPOON_4, PION_1}; use client::ClientInterface; -use ibcmail::{client::msg::ClientExecuteMsgFns, Message}; +use ibcmail::{client::msg::ClientExecuteMsgFns, ClientMetadata, MailMessage}; use tests::TEST_NAMESPACE; const SRC: ChainInfo = PION_1; @@ -39,13 +39,14 @@ fn test() -> anyhow::Result<()> { .namespace(Namespace::new(TEST_NAMESPACE)?) .build()?; - let mail_msg = Message::new(dst_acc.id()?.into(), "test-subject", "test-body"); + let mail_msg = MailMessage::new("test-subject", "test-body"); let send = src_client.send_message( mail_msg, - Some(AccountTrace::Remote(vec![TruncatedChainId::from_chain_id( - DST.chain_id, - )])), + dst_acc.id()?.into(), + Some(ClientMetadata::new_with_route(AccountTrace::Remote(vec![ + TruncatedChainId::from_chain_id(DST.chain_id), + ]))), )?; interchain.await_and_check_packets(SRC.chain_id, send)?; diff --git a/tests/src/bin/full_demo.rs b/tests/src/bin/full_demo.rs index 141b06f..2bd40be 100644 --- a/tests/src/bin/full_demo.rs +++ b/tests/src/bin/full_demo.rs @@ -8,8 +8,8 @@ use abstract_app::{ }, std::{ ibc_client::QueryMsgFns as IbcQueryFns, - IBC_HOST, version_control::{ExecuteMsgFns, ModuleFilter, QueryMsgFns}, + IBC_HOST, }, }; use abstract_client::AbstractClient; @@ -21,7 +21,7 @@ use cw_orch_interchain::{ChannelCreationValidator, DaemonInterchainEnv, Intercha use networks::{HARPOON_4, PION_1}; use client::ClientInterface; -use ibcmail::{client::msg::ClientExecuteMsgFns, IBCMAIL_NAMESPACE, Message}; +use ibcmail::{client::msg::ClientExecuteMsgFns, ClientMetadata, MailMessage, IBCMAIL_NAMESPACE}; use tests::TEST_NAMESPACE; const SRC: ChainInfo = HARPOON_4; @@ -100,10 +100,11 @@ fn test() -> anyhow::Result<()> { // )?; let send = dst_client.send_message( - Message::new(src_acc.id()?.into(), "test-subject", "test-body"), - Some(AccountTrace::Remote(vec![TruncatedChainId::from_chain_id( - SRC.chain_id, - )])), + MailMessage::new("test-subject", "test-body"), + src_acc.id()?.into(), + Some(ClientMetadata::new_with_route(AccountTrace::Remote(vec![ + TruncatedChainId::from_chain_id(SRC.chain_id), + ]))), )?; interchain.await_and_check_packets(DST.chain_id, send)?; diff --git a/tests/src/client.rs b/tests/src/client.rs index a8a501f..e5446cd 100644 --- a/tests/src/client.rs +++ b/tests/src/client.rs @@ -1,13 +1,18 @@ -use abstract_app::objects::{AccountId, namespace::Namespace}; -use abstract_client::{AbstractClient, Application, Environment}; +use abstract_app::objects::TruncatedChainId; +use abstract_app::objects::{namespace::Namespace, AccountId}; +use abstract_client::{AbstractClient, Application}; +use abstract_cw_orch_polytone::PolytoneConnection; +use abstract_interface::Abstract; use cw_orch::{anyhow, prelude::*}; +use cw_orch_interchain::{InterchainEnv, MockBech32InterchainEnv}; use speculoos::prelude::*; // Use prelude to get all the necessary imports -use client::{*, contract::interface::ClientInterface, msg::ClientInstantiateMsg}; +use client::{contract::interface::ClientInterface, msg::ClientInstantiateMsg, *}; +use ibcmail::ClientMetadata; use ibcmail::{ - IBCMAIL_NAMESPACE, IBCMAIL_SERVER_ID, IbcMailMessage, Message, Recipient, - Sender, server::msg::ServerInstantiateMsg, + server::msg::ServerInstantiateMsg, Header, MailMessage, ReceivedMessage, Recipient, Route, + Sender, ServerMetadata, IBCMAIL_NAMESPACE, IBCMAIL_SERVER_ID, }; use server::ServerInterface; @@ -69,26 +74,57 @@ impl TestEnv { client2: app2, }) } + + pub fn assert_no_received_messages(&self) -> anyhow::Result<()> { + assert_that!( + self.client1 + .list_received_messages(None, None, None)? + .messages + ) + .is_empty(); + assert_that!( + self.client2 + .list_received_messages(None, None, None)? + .messages + ) + .is_empty(); + Ok(()) + } +} + +impl TestEnv { + pub fn connect_to( + &self, + other: &TestEnv, + interchain: &impl cw_orch_interchain::InterchainEnv, + ) -> anyhow::Result<()> { + self.abs.connect_to(&other.abs, interchain)?; + Ok(()) + } } -fn create_test_message(from: AccountId, to: AccountId) -> IbcMailMessage { - IbcMailMessage { - id: "test-id".to_string(), - sender: Sender::account(from.clone(), None), - message: Message { +fn create_received_message(from: AccountId, to: AccountId, route: Route) -> ReceivedMessage { + ReceivedMessage { + header: Header { + sender: Sender::account(from.clone(), None), recipient: Recipient::account(to.clone(), None), + id: "test-id".to_string(), + version: "0.0.1".to_string(), + timestamp: Default::default(), + reply_to: None, + }, + message: MailMessage { subject: "test-subject".to_string(), body: "test-body".to_string(), }, - timestamp: Default::default(), - version: "0.0.1".to_string(), + metadata: ServerMetadata { route }, } } mod receive_msg { use speculoos::assert_that; - use ibcmail::{IBCMAIL_SERVER_ID, MessageStatus}; + use ibcmail::IBCMAIL_SERVER_ID; use super::*; @@ -109,7 +145,11 @@ mod receive_msg { server_account_id, app_account_id ); - let msg = create_test_message(server_account_id.clone(), app_account_id.clone()); + let received = create_received_message( + server_account_id.clone(), + app_account_id.clone(), + Route::Local, + ); let server_addr = app .account() .module_addresses(vec![IBCMAIL_SERVER_ID.into()])? @@ -118,11 +158,11 @@ mod receive_msg { .clone(); println!("app_account_id: {:?}", app.account().id()); - let res = app.call_as(&server_addr).receive_message(msg); + let res = app.call_as(&server_addr).receive_message(received.clone()); assert_that!(res).is_ok(); - let messages = app.list_messages(MessageStatus::Received, None, None, None)?; + let messages = app.list_received_messages(None, None, None)?; assert_that!(messages.messages).has_length(1); Ok(()) @@ -137,7 +177,8 @@ mod receive_msg { let app_account_id = app.account().id().unwrap(); - let msg = create_test_message(app_account_id.clone(), app_account_id.clone()); + let msg = + create_received_message(app_account_id.clone(), app_account_id.clone(), Route::Local); let res = app.receive_message(msg); assert_that!(res) @@ -151,11 +192,12 @@ mod receive_msg { mod send_msg { use std::str::FromStr; - use abstract_app::{objects::account::AccountTrace, std::version_control::ExecuteMsgFns}; use abstract_app::objects::TruncatedChainId; + use abstract_app::{objects::account::AccountTrace, std::version_control::ExecuteMsgFns}; + use cw_orch_interchain::{InterchainEnv, MockBech32InterchainEnv}; - use ibcmail::{IBCMAIL_CLIENT_ID, Message, MessageStatus, server::error::ServerError}; + use ibcmail::{server::error::ServerError, ClientMetadata, MailMessage, IBCMAIL_CLIENT_ID}; use super::*; @@ -167,16 +209,33 @@ mod send_msg { let client1 = env.client1; let client2 = env.client2; - let msg = Message::new( - Recipient::account(client2.account().id()?, None), - "test-subject", - "test-body", - ); + let recipient = Recipient::account(client2.account().id()?, None); + let msg = MailMessage::new("test-subject", "test-body"); + + let res = client1.send_message(msg, recipient, None); + + assert_that!(res).is_ok(); + + Ok(()) + } + + #[test] + fn local_message_gets_delivery_result() -> anyhow::Result<()> { + // Create a sender and mock env + let mock = MockBech32::new("mock"); + let env = TestEnv::setup(mock)?; + let client1 = env.client1; + let client2 = env.client2; - let res = client1.send_message(msg, None); + let recipient = Recipient::account(client2.account().id()?, None); + let msg = MailMessage::new("test-subject", "test-body"); + let res = client1.send_message(msg, recipient, None); assert_that!(res).is_ok(); + let received_messages = client2.list_received_messages(None, None, None)?.messages; + assert_that!(received_messages).has_length(1); + Ok(()) } @@ -194,13 +253,10 @@ mod send_msg { .version_control() .claim_namespace(client2.account().id()?, namespace.to_string())?; - let msg = Message::new( - Recipient::namespace(namespace.try_into()?, None), - "test-subject", - "test-body", - ); + let msg = MailMessage::new("test-subject", "test-body"); - let res = client1.send_message(msg, None); + let res = + client1.send_message(msg, Recipient::namespace(namespace.try_into()?, None), None); assert_that!(res).is_ok(); Ok(()) @@ -215,13 +271,10 @@ mod send_msg { let bad_namespace: Namespace = "nope".try_into()?; - let msg = Message::new( - Recipient::namespace(bad_namespace.clone(), None), - "test-subject", - "test-body", - ); + let msg = MailMessage::new("test-subject", "test-body"); - let res = client1.send_message(msg, None); + let res = + client1.send_message(msg, Recipient::namespace(bad_namespace.clone(), None), None); assert_that!(res).is_err().matches(|e| { e.root() @@ -235,41 +288,34 @@ mod send_msg { #[test] fn can_send_remote_message() -> anyhow::Result<()> { // Create a sender and mock env - let interchain = MockBech32InterchainEnv::new(vec![ - ("juno-1", "juno18k2uq7srsr8lwrae6zr0qahpn29rsp7tw83nyx"), - ( - "archway-1", - "archway18k2uq7srsr8lwrae6zr0qahpn29rsp7td7wvfd", - ), - ]); + let interchain = + MockBech32InterchainEnv::new(vec![("juno-1", "juno"), ("archway-1", "archway")]); - // /Users/adair/.cargo/registry/src/index.crates.io-6f17d22bba15001f/cw-orch-mock-0.22.0/src/queriers/env.rs:12:70: - // index out of bounds: the len is 1 but the index is 1 (when initializing with "juno") let arch_env = TestEnv::setup(interchain.get_chain("archway-1")?)?; let juno_env = TestEnv::setup(interchain.get_chain("juno-1")?)?; - arch_env.abs.connect_to(&juno_env.abs, &interchain)?; + arch_env.connect_to(&juno_env, &interchain)?; let arch_client = arch_env.client1; let juno_client = juno_env.client1; // the trait `From<&str>` is not implemented for `abstract_app::objects::chain_name::TruncatedChainId` - let arch_to_juno_msg = Message::new( + let arch_to_juno_msg = MailMessage::new("test-subject", "test-body"); + + let res = arch_client.send_message( + arch_to_juno_msg, Recipient::account( juno_client.account().id()?, Some(TruncatedChainId::from_string("juno".into())?), ), - "test-subject", - "test-body", + None, ); - let res = arch_client.send_message(arch_to_juno_msg, None); - assert_that!(res).is_ok(); interchain.await_and_check_packets("archway-1", res?)?; - let arch_messages = arch_client.list_messages(MessageStatus::Received, None, None, None)?; + let arch_messages = arch_client.list_received_messages(None, None, None)?; assert_that!(arch_messages.messages).is_empty(); let juno_client_1_module_addresses = juno_client @@ -298,15 +344,15 @@ mod send_msg { let juno_mail_client = ClientInterface::new(IBCMAIL_CLIENT_ID, juno_env.env.clone()); juno_mail_client.set_address(&juno_client_1_module_addresses.modules[0].1.clone()); let juno_mail_client_messages = - juno_mail_client.list_messages(MessageStatus::Received, None, None, None)?; + juno_mail_client.list_received_messages(None, None, None)?; assert_that!(juno_mail_client_messages.messages).has_length(1); - let juno_messages = juno_client.list_messages(MessageStatus::Received, None, None, None)?; + let juno_messages = juno_client.list_received_messages(None, None, None)?; assert_that!(juno_messages.messages).has_length(1); // Sanity check messages method - let juno_message_id = juno_messages.messages.first().cloned().unwrap().id; - let juno_message = juno_client.messages(vec![juno_message_id], MessageStatus::Received)?; + let juno_message_id = juno_messages.messages.first().cloned().unwrap().header.id; + let juno_message = juno_client.received_messages(vec![juno_message_id])?; assert_that!(juno_message.messages).has_length(1); Ok(()) @@ -316,15 +362,9 @@ mod send_msg { fn can_send_remote_message_2_hop() -> anyhow::Result<()> { // Create a sender and mock env let interchain = MockBech32InterchainEnv::new(vec![ - ("juno-1", "juno18k2uq7srsr8lwrae6zr0qahpn29rsp7tw83nyx"), - ( - "archway-1", - "archway18k2uq7srsr8lwrae6zr0qahpn29rsp7td7wvfd", - ), - ( - "neutron-1", - "neutron18k2uq7srsr8lwrae6zr0qahpn29rsp7tu2m2ea", - ), + ("juno-1", "juno"), + ("archway-1", "archway"), + ("neutron-1", "neutron"), ]); // /Users/adair/.cargo/registry/src/index.crates.io-6f17d22bba15001f/cw-orch-mock-0.22.0/src/queriers/env.rs:12:70: @@ -333,8 +373,8 @@ mod send_msg { let juno_env = TestEnv::setup(interchain.get_chain("juno-1")?)?; let neutron_env = TestEnv::setup(interchain.get_chain("neutron-1")?)?; - arch_env.abs.connect_to(&juno_env.abs, &interchain)?; - juno_env.abs.connect_to(&neutron_env.abs, &interchain)?; + arch_env.connect_to(&juno_env, &interchain)?; + juno_env.connect_to(&neutron_env, &interchain)?; // ibc_abstract_setup(&interchain, "archway-1", "juno-1")?; // ibc_abstract_setup(&interchain, "juno-1", "neutron-1")?; @@ -344,26 +384,23 @@ mod send_msg { let neutron_client = neutron_env.client1; // the trait `From<&str>` is not implemented for `abstract_app::objects::chain_name::TruncatedChainId` - let arch_to_neutron_msg = Message::new( + let arch_to_neutron_msg = MailMessage::new("test-subject", "test-body"); + + let res = arch_client.send_message( + arch_to_neutron_msg, Recipient::account( neutron_client.account().id()?, Some(TruncatedChainId::from_string("neutron".into())?), ), - "test-subject", - "test-body", - ); - - let res = arch_client.send_message( - arch_to_neutron_msg, - Some(AccountTrace::Remote(vec![ + Some(ClientMetadata::new_with_route(AccountTrace::Remote(vec![ "juno".parse()?, TruncatedChainId::from_str("neutron")?, - ])), + ]))), )?; interchain.await_and_check_packets("archway-1", res.clone())?; - let arch_messages = arch_client.list_messages(MessageStatus::Received, None, None, None)?; + let arch_messages = arch_client.list_received_messages(None, None, None)?; assert_that!(arch_messages.messages).is_empty(); let neutron_client_1_module_addresses = neutron_client @@ -383,7 +420,7 @@ mod send_msg { let neutron_mail_client = ClientInterface::new(IBCMAIL_CLIENT_ID, neutron_env.env.clone()); neutron_mail_client.set_address(&neutron_client_1_module_addresses.modules[0].1.clone()); let neutron_mail_client_messages = - neutron_mail_client.list_messages(MessageStatus::Received, None, None, None)?; + neutron_mail_client.list_received_messages(None, None, None)?; assert_that!(neutron_mail_client_messages.messages).has_length(1); // let juno_messages = neutron_client.list_messages(None, None, None)?; @@ -392,3 +429,135 @@ mod send_msg { Ok(()) } } + +mod update_status { + use super::*; + use ibcmail::DeliveryFailure; + use ibcmail::DeliveryStatus; + use speculoos::assert_that; + + #[test] + fn send_remote_message_1_hop_account_dne_updates_status_to_failed() -> anyhow::Result<()> { + // Create a sender and mock env + let interchain = + MockBech32InterchainEnv::new(vec![("juno-1", "juno"), ("archway-1", "archway")]); + + let arch_env = TestEnv::setup(interchain.get_chain("archway-1")?)?; + let juno_env = TestEnv::setup(interchain.get_chain("juno-1")?)?; + + arch_env.connect_to(&juno_env, &interchain)?; + + let arch_client = arch_env.client1; + let juno_client = juno_env.client1; + + // the trait `From<&str>` is not implemented for `abstract_app::objects::chain_name::TruncatedChainId` + let arch_to_juno_msg = MailMessage::new("test-subject", "test-body"); + + let res = arch_client.send_message( + arch_to_juno_msg, + Recipient::account( + AccountId::local(420), + Some(TruncatedChainId::from_string("juno".into())?), + ), + Some(ClientMetadata::new_with_route(Route::Remote(vec![ + TruncatedChainId::from_string("juno".into())?, + ]))), + ); + + assert_that!(res).is_ok(); + + let server = ServerInterface::new(IBCMAIL_SERVER_ID, arch_env.env.clone()); + println!("server: {:?}", server.address()?); + let abstr = Abstract::new(arch_env.env.clone()); + println!("ibc_host: {:?}", abstr.ibc.host.address()?); + let poly = PolytoneConnection::load_from(arch_env.env.clone(), juno_env.env.clone()); + println!("poly_note: {:?}", poly.note.address()?); + + let packets = interchain.await_packets("archway-1", res?)?; + + assert_that!( + arch_client + .list_received_messages(None, None, None)? + .messages + ) + .is_empty(); + assert_that!( + juno_client + .list_received_messages(None, None, None)? + .messages + ) + .is_empty(); + + // interchain.await_packets("archway-1", res?)?; + // println!("packets: {:?}", packets); + + Ok(()) + } + + #[test] + fn send_remote_message_2_hop_account_dne_updates_status_to_failed() -> anyhow::Result<()> { + // Create a sender and mock env + let interchain = MockBech32InterchainEnv::new(vec![ + ("juno-1", "juno"), + ("archway-1", "archway"), + ("neutron-1", "neutron"), + ]); + + let arch_env = TestEnv::setup(interchain.get_chain("archway-1")?)?; + let juno_env = TestEnv::setup(interchain.get_chain("juno-1")?)?; + let neutron_env = TestEnv::setup(interchain.get_chain("neutron-1")?)?; + + arch_env.connect_to(&juno_env, &interchain)?; + juno_env.connect_to(&neutron_env, &interchain)?; + + let arch_client = arch_env.client1.clone(); + let juno_client = juno_env.client1.clone(); + let neutron_client = neutron_env.client1.clone(); + + // the trait `From<&str>` is not implemented for `abstract_app::objects::chain_name::TruncatedChainId` + let mail_message = MailMessage::new("test-subject", "test-body"); + + let res = arch_client.send_message( + mail_message, + Recipient::account( + AccountId::local(420), + Some(TruncatedChainId::from_string("neutron".into())?), + ), + Some(ClientMetadata::new_with_route(Route::Remote(vec![ + TruncatedChainId::from_string("juno".into())?, + TruncatedChainId::from_string("neutron".into())?, + ]))), + ); + + assert_that!(res).is_ok(); + + let server = ServerInterface::new(IBCMAIL_SERVER_ID, arch_env.env.clone()); + println!("server: {:?}", server.address()?); + let abstr = Abstract::new(arch_env.env.clone()); + println!("ibc_host: {:?}", abstr.ibc.host.address()?); + let poly = PolytoneConnection::load_from(arch_env.env.clone(), juno_env.env.clone()); + println!("poly_note: {:?}", poly.note.address()?); + + let packets = interchain.await_packets("archway-1", res?)?; + + arch_env.assert_no_received_messages()?; + juno_env.assert_no_received_messages()?; + neutron_env.assert_no_received_messages()?; + + assert_that!(arch_client.list_sent_messages(None, None, None)?.messages).has_length(1); + + let sent_message = arch_client + .list_sent_messages(None, None, None)? + .messages + .first() + .cloned() + .unwrap(); + assert_that!(sent_message.status) + .is_equal_to(DeliveryStatus::from(DeliveryFailure::RecipientNotFound)); + + // interchain.await_packets("archway-1", res?)?; + // println!("packets: {:?}", packets); + + Ok(()) + } +} diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 3ebb49e..8fd0181 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -1,4 +1,4 @@ #[cfg(test)] mod client; -pub const TEST_NAMESPACE: &str = "ibcmail-demo"; \ No newline at end of file +pub const TEST_NAMESPACE: &str = "ibcmail-demo";