diff --git a/Cargo.toml b/Cargo.toml index b2555e275..f47cf40c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ members = [ "ibc-apps/ics20-transfer", "ibc-apps/ics721-nft-transfer/types", "ibc-apps/ics721-nft-transfer", + "ibc-apps/ics27-interchain-accounts", "ibc-apps", "ibc-core/ics24-host/cosmos", "ibc-data-types", @@ -66,6 +67,7 @@ sha2 = { version = "0.10.8", default-features = false } serde = { version = "1.0", default-features = false } serde_json = { package = "serde-json-wasm", version = "1.0.1", default-features = false } subtle-encoding = { version = "0.5", default-features = false } +cosmrs = { version = "0.16.0" } # ibc dependencies ibc = { version = "0.53.0", path = "./ibc", default-features = false } @@ -89,8 +91,9 @@ ibc-client-cw = { version = "0.53.0", path = "./ibc-clients/cw-contex ibc-client-tendermint = { version = "0.53.0", path = "./ibc-clients/ics07-tendermint", default-features = false } ibc-client-tendermint-cw = { version = "0.53.0", path = "./ibc-clients/ics07-tendermint/cw-contract", default-features = false } -ibc-app-transfer = { version = "0.53.0", path = "./ibc-apps/ics20-transfer", default-features = false } -ibc-app-nft-transfer = { version = "0.53.0", path = "./ibc-apps/ics721-nft-transfer", default-features = false } +ibc-app-transfer = { version = "0.53.0", path = "./ibc-apps/ics20-transfer", default-features = false } +ibc-app-nft-transfer = { version = "0.53.0", path = "./ibc-apps/ics721-nft-transfer", default-features = false } +ibc-app-interchain-accounts = { version = "0.53.0", path = "./ibc-apps/ics27-interchain-accounts", default-features = false } ibc-core-client-context = { version = "0.53.0", path = "./ibc-core/ics02-client/context", default-features = false } ibc-core-client-types = { version = "0.53.0", path = "./ibc-core/ics02-client/types", default-features = false } diff --git a/ibc-apps/Cargo.toml b/ibc-apps/Cargo.toml index 6ef3c6ac6..ce98e7f16 100644 --- a/ibc-apps/Cargo.toml +++ b/ibc-apps/Cargo.toml @@ -18,8 +18,9 @@ description = """ all-features = true [dependencies] -ibc-app-transfer = { workspace = true } -ibc-app-nft-transfer = { workspace = true, optional = true, features = [ "std", "serde", "schema", "borsh", "parity-scale-codec" ] } +ibc-app-transfer = { workspace = true } +ibc-app-nft-transfer = { workspace = true, optional = true, features = [ "std", "serde", "schema", "borsh", "parity-scale-codec" ] } +ibc-app-interchain-accounts = { workspace = true } [features] default = [ "std" ] diff --git a/ibc-apps/ics27-interchain-accounts/Cargo.toml b/ibc-apps/ics27-interchain-accounts/Cargo.toml new file mode 100644 index 000000000..68d932635 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "ibc-app-interchain-accounts" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +keywords = [ "blockchain", "cosmos", "ibc", "transfer", "ics20" ] +readme = "./../README.md" + +description = """ + Maintained by `ibc-rs`, contains the implementation of the ICS-20 Fungible Token Transfer + application logic and re-exports essential data structures and domain types from + `ibc-app-transfer-types` crate. +""" + +[package.metadata.docs.rs] +all-features = true + +[dependencies] +# external dependencies +serde = { workspace = true } +serde_json = { workspace = true, optional = true } +cosmrs = { workspace = true } +prost = { workspace = true } +sha2 = { workspace = true } + +# ibc dependencies +ibc-core = { workspace = true } +ibc-app-transfer-types = { workspace = true } +ibc-proto = { workspace = true } + +[dev-dependencies] +subtle-encoding = { workspace = true } + +[features] +default = [ "std" ] +std = [ + "ibc-app-transfer-types/std", + "ibc-core/std", + "serde_json/std", +] +serde = [ + "ibc-app-transfer-types/serde", + "ibc-core/serde", + "serde_json", +] +schema = [ + "ibc-app-transfer-types/schema", + "ibc-core/schema", + "serde", + "std", +] +borsh = [ + "ibc-app-transfer-types/borsh", + "ibc-core/borsh", +] +parity-scale-codec = [ + "ibc-app-transfer-types/parity-scale-codec", + "ibc-core/parity-scale-codec", +] diff --git a/ibc-apps/ics27-interchain-accounts/src/account.rs b/ibc-apps/ics27-interchain-accounts/src/account.rs new file mode 100644 index 000000000..628ea6e8a --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/account.rs @@ -0,0 +1,166 @@ +use alloc::string::{String, ToString}; +use alloc::vec; + +use cosmrs::AccountId; +use ibc_core::host::types::identifiers::PortId; +use ibc_core::primitives::proto::Protobuf; +use ibc_core::primitives::Signer; +use ibc_proto::cosmos::auth::v1beta1::BaseAccount as RawBaseAccount; +use ibc_proto::google::protobuf::Any; +use ibc_proto::ibc::apps::interchain_accounts::v1::InterchainAccount as RawInterchainAccount; +use prost::Message; +use sha2::{Digest, Sha256}; + +use super::error::InterchainAccountError; +use super::MODULE_ID_STR; + +/// Defines an interchain account type with a generic base account. +/// +/// TODO: to put a note that we currently only support Cosmos-SDK driven chains. +#[derive(Clone, Debug)] +pub struct InterchainAccount { + /// The base account. + base_account: A, + /// The account owner. + owner: PortId, +} + +impl InterchainAccount { + /// Constructs a new interchain account instance. + pub fn new(base_account: A, owner: PortId) -> Self { + Self { + base_account, + owner, + } + } + + /// Constructs a new interchain account with a Cosmos-SDK base account. + pub fn new_with_sdk_base_account( + address: AccountId, + owner: PortId, + ) -> InterchainAccount { + let acc = SdkBaseAccount { + address, + pub_key: Any { + type_url: String::new(), + value: vec![], + }, + account_number: 0, + sequence: 0, + }; + InterchainAccount::new(acc, owner) + } + + pub fn address(&self) -> Signer { + self.base_account.address() + } +} + +impl BaseAccount for SdkBaseAccount { + fn address(&self) -> Signer { + Signer::from(self.address.to_string()) + } +} + +impl Protobuf for InterchainAccount {} + +impl TryFrom for InterchainAccount { + type Error = InterchainAccountError; + + fn try_from(raw: RawInterchainAccount) -> Result { + Ok(InterchainAccount { + base_account: match raw.base_account { + Some(base_account) => SdkBaseAccount::try_from(base_account)?, + None => return Err(InterchainAccountError::not_found("base account")), + }, + owner: raw.account_owner.parse().unwrap(), + }) + } +} + +impl From> for RawInterchainAccount { + fn from(domain: InterchainAccount) -> Self { + RawInterchainAccount { + base_account: Some(domain.base_account.into()), + account_owner: domain.owner.to_string(), + } + } +} + +/// Defines the base account for Cosmos-SDK driven chains. +#[derive(Clone, Debug)] +pub struct SdkBaseAccount { + /// The address of the account. + pub address: AccountId, + /// The public key of the account. + pub pub_key: Any, + /// The account number. + pub account_number: u64, + /// The sequence number. + pub sequence: u64, +} + +impl Protobuf for SdkBaseAccount {} + +impl TryFrom for SdkBaseAccount { + type Error = InterchainAccountError; + + fn try_from(raw: RawBaseAccount) -> Result { + // TODO: should we check anything here? regarding number and sequence? + Ok(SdkBaseAccount { + address: raw + .address + .parse() + .map_err(InterchainAccountError::source)?, + pub_key: match raw.pub_key { + Some(pub_key) => pub_key, + None => return Err(InterchainAccountError::not_found("missing base account")), + }, + account_number: raw.account_number, + sequence: raw.sequence, + }) + } +} + +impl From for RawBaseAccount { + fn from(domain: SdkBaseAccount) -> Self { + RawBaseAccount { + address: domain.address.to_string(), + pub_key: Some(domain.pub_key), + account_number: domain.account_number, + sequence: domain.sequence, + } + } +} + +const TYPE_URL: &str = "/cosmos.auth.v1beta1.BaseAccount"; + +impl From for Any { + fn from(account: SdkBaseAccount) -> Self { + let account = RawBaseAccount::from(account); + Any { + type_url: TYPE_URL.to_string(), + value: account.encode_to_vec(), + } + } +} + +/// Enforces minimum definition requirement for a base account. +pub trait BaseAccount { + fn address(&self) -> Signer; +} + +pub fn get_sdk_controller_account() -> Result { + let mut hasher = Sha256::new(); + + hasher.update(MODULE_ID_STR.as_bytes()); + + let mut hash = hasher.finalize().to_vec(); + + hash.truncate(20); + + let controller_account = + AccountId::new(MODULE_ID_STR, &hash).map_err(InterchainAccountError::source)?; + + Ok(controller_account) +} diff --git a/ibc-apps/ics27-interchain-accounts/src/context.rs b/ibc-apps/ics27-interchain-accounts/src/context.rs new file mode 100644 index 000000000..c5dea76a8 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/context.rs @@ -0,0 +1,85 @@ +use ibc_core::host::types::identifiers::{ChannelId, ConnectionId, PortId}; +use ibc_core::host::{ExecutionContext, ValidationContext}; +use ibc_core::primitives::Signer; + +use super::account::{BaseAccount, InterchainAccount}; +use super::error::InterchainAccountError; +use super::host::params::Params; + +pub trait InterchainAccountValidationContext: ValidationContext { + type AccountId: TryFrom; + + /// Returns true if the controller functionality is enabled on the chain + fn is_controller_enabled(&self) -> bool; + + /// Returns the active `ChannelId` from the store by the provided + /// `ConnectionId` and `PortId` + fn get_active_channel_id( + &self, + connection_id: &ConnectionId, + port_id: &PortId, + ) -> Result; + + /// Returns the parameters needed for functioning as a host chain + fn get_params(&self) -> Result; + + /// Returns the `AccountId` for the given address + fn get_interchain_account( + &self, + address: &Signer, + ) -> Result; + + /// Returns the InterchainAccount address from the store associated with + /// the provided ConnectionId and PortId + fn get_ica_address( + &self, + connection_id: &ConnectionId, + port_id: &PortId, + ) -> Result; +} + +pub trait InterchainAccountExecutionContext: + ExecutionContext + InterchainAccountValidationContext +{ + /// Stores the active `ChannelId` to the store by the provided + /// `ConnectionId` and `PortId` + fn store_active_channel_id( + &mut self, + connection_id: ConnectionId, + port_id: PortId, + channel_id: ChannelId, + ) -> Result<(), InterchainAccountError>; + + /// Stores the parameters for functioning as a host chain + fn store_params(&mut self, params: Params) -> Result<(), InterchainAccountError>; + + /// Generates a new interchain account address. + /// + /// It uses the host `ConnectionId`, the controller `PortId`, and may also + /// (in case of Cosmos SDK chains) incorporate block dependent information. + fn generate_ica_address( + &self, + connection_id: ConnectionId, + port_id: PortId, + ) -> Result; + + /// Stores the interchain account address + fn store_ica_address( + &mut self, + connection_id: ConnectionId, + port_id: PortId, + interchain_account_address: Signer, + ) -> Result<(), InterchainAccountError>; + + /// Creates a new interchain account with the provided account information + fn new_interchain_account( + &mut self, + account: InterchainAccount, + ) -> Result; + + /// Stores the created interchain account to the store + fn store_interchain_account( + &mut self, + account: Self::AccountId, + ) -> Result<(), InterchainAccountError>; +} diff --git a/ibc-apps/ics27-interchain-accounts/src/controller/callback.rs b/ibc-apps/ics27-interchain-accounts/src/controller/callback.rs new file mode 100644 index 000000000..bd92ba3db --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/controller/callback.rs @@ -0,0 +1,307 @@ +//! Provides a set of default on-module callback functions for the controller chain. +use ibc_core::channel::types::acknowledgement::{Acknowledgement, AcknowledgementStatus}; +use ibc_core::channel::types::channel::{Counterparty, Order, State}; +use ibc_core::channel::types::packet::Packet; +use ibc_core::channel::types::Version; +use ibc_core::host::types::identifiers::{ChannelId, ConnectionId, PortId}; +use ibc_core::host::types::path::ChannelEndPath; +use ibc_core::primitives::Signer; +use ibc_core::router::types::module::ModuleExtras; + +use crate::context::{InterchainAccountExecutionContext, InterchainAccountValidationContext}; +use crate::error::InterchainAccountError; +use crate::metadata::Metadata; +use crate::port::{default_host_port_id, verify_controller_port_id_prefix}; + +/// Default validation callback function on the chan_open_init request for the +/// controller chain. +/// +/// It performs basic validation for channel initialization when receiving a +/// request to register an interchain account. +pub fn on_chan_open_init_validate( + ctx_a: &impl InterchainAccountValidationContext, + order: Order, + conn_hops_on_a: &[ConnectionId], + port_id_on_a: &PortId, + _chan_id_on_a: &ChannelId, + counterparty: &Counterparty, + version_on_a: &Version, +) -> Result<(), InterchainAccountError> { + if !ctx_a.is_controller_enabled() { + return Err(InterchainAccountError::not_supported( + "controller chain is not enabled", + )); + } + + if order != Order::Ordered { + return Err(InterchainAccountError::not_supported( + "only ordered channels are supported", + )); + } + + verify_controller_port_id_prefix(port_id_on_a)?; + + let port_id_on_b = default_host_port_id()?; + + if counterparty.port_id != port_id_on_b { + return Err(InterchainAccountError::mismatch("counterparty port id") + .expected(&port_id_on_b) + .given(&counterparty.port_id)); + } + + // Validates the provided version ending up with a correct metadata + let metadata = if version_on_a.is_empty() { + let conn_end_on_a = ctx_a.connection_end(&conn_hops_on_a[0])?; + let conn_id_on_b = if let Some(id) = conn_end_on_a.counterparty().connection_id.clone() { + id + } else { + return Err(InterchainAccountError::not_found( + "counterparty connection id", + )); + }; + + Metadata::new_default(conn_hops_on_a[0].clone(), conn_id_on_b) + } else { + serde_json::from_str::(version_on_a.as_str()) + .map_err(InterchainAccountError::source)? + }; + + metadata.validate(ctx_a, conn_hops_on_a)?; + + if let Ok(active_chan_id_on_a) = ctx_a.get_active_channel_id(&conn_hops_on_a[0], port_id_on_a) { + let chan_end_on_a = + ctx_a.channel_end(&ChannelEndPath::new(port_id_on_a, &active_chan_id_on_a))?; + + chan_end_on_a.verify_state_matches(&State::Open)?; + + metadata.verify_prev_metadata_matches(chan_end_on_a.version())?; + } + + Ok(()) +} + +/// Default execution callback function on the chan_open_init request for the controller chain. +pub fn on_chan_open_init_execute( + _ctx_a: &mut impl InterchainAccountExecutionContext, + _order: Order, + _conn_hops_on_a: &[ConnectionId], + _port_id_on_a: PortId, + _chan_id_on_a: ChannelId, + _counterparty: Counterparty, + version_on_a: Version, +) -> Result<(ModuleExtras, Version), InterchainAccountError> { + Ok((ModuleExtras::empty(), version_on_a)) +} + +/// Default validation callback function on the chan_open_try request for the controller chain +/// +/// Note: if your chain serves as both the controller and the host chain, you +/// may utilize the default +/// [host](crate::host::callback::on_chan_open_try_validate) +/// callback implementation instead. +pub fn on_chan_open_try_validate( + _ctx_a: &impl InterchainAccountValidationContext, + _order: Order, + _conn_hops_on_a: &[ConnectionId], + _port_id_on_a: &PortId, + _chan_id_on_a: &ChannelId, + _counterparty: &Counterparty, + _version_on_b: &Version, +) -> Result<(), InterchainAccountError> { + Err(InterchainAccountError::not_allowed( + "channel handshake must be initiated by the controller chain", + )) +} + +/// Default execution callback function on the chan_open_try request for the controller chain +/// +/// Note: if your chain serves as both the controller and the host chain, you +/// may utilize the default +/// [host](crate::host::callback::on_chan_open_try_execute) +/// callback implementation instead. +pub fn on_chan_open_try_execute( + _ctx_a: &mut impl InterchainAccountExecutionContext, + _order: Order, + _conn_hops_on_a: &[ConnectionId], + _port_id_on_a: &PortId, + _chan_id_on_a: &ChannelId, + _counterparty: &Counterparty, + _version_on_b: &Version, +) -> Result<(ModuleExtras, Version), InterchainAccountError> { + Err(InterchainAccountError::not_allowed( + "channel handshake must be initiated by the controller chain", + )) +} + +/// Default validation callback function on the chan_open_ack request for the controller chain +pub fn on_chan_open_ack_validate( + ctx_a: &impl InterchainAccountValidationContext, + port_id_on_a: &PortId, + chan_id_on_a: &ChannelId, + version_on_b: &Version, +) -> Result<(), InterchainAccountError> { + if !ctx_a.is_controller_enabled() { + return Err(InterchainAccountError::not_supported( + "controller chain is not enabled.", + )); + } + + let port_id_on_b = default_host_port_id()?; + + if port_id_on_a == &port_id_on_b { + return Err(InterchainAccountError::invalid( + "port id cannot be the same as host chain port id.", + )); + } + + verify_controller_port_id_prefix(port_id_on_a)?; + + let metadata = serde_json::from_str::(version_on_b.as_str()) + .map_err(InterchainAccountError::source)?; + + // Checks that no active channel exists for the given controller port identifier + if ctx_a + .get_active_channel_id(&metadata.conn_id_on_a, port_id_on_a) + .is_ok() + { + return Err(InterchainAccountError::already_exists("active channel").given(&port_id_on_a)); + } + + let chan_end_on_a = ctx_a.channel_end(&ChannelEndPath::new(port_id_on_a, chan_id_on_a))?; + + chan_end_on_a.verify_state_matches(&State::Init)?; + + metadata.validate(ctx_a, chan_end_on_a.connection_hops())?; + + Ok(()) +} + +/// Default execution callback function on the chan_open_ack request for the controller chain +/// +/// It sets the active channel for the interchain account/owner pair and stores +/// the associated interchain account address by it's corresponding port identifier. +pub fn on_chan_open_ack_execute( + ctx_a: &mut impl InterchainAccountExecutionContext, + port_id_on_a: PortId, + chan_id_on_a: ChannelId, + version_on_b: Version, +) -> Result { + let metadata = serde_json::from_str::(version_on_b.as_str()) + .map_err(InterchainAccountError::source)?; + + ctx_a.store_active_channel_id( + metadata.conn_id_on_a.clone(), + port_id_on_a.clone(), + chan_id_on_a, + )?; + ctx_a.store_ica_address(metadata.conn_id_on_a, port_id_on_a, metadata.address)?; + + Ok(ModuleExtras::empty()) +} + +/// Default validation callback function on the chan_open_confirm request for the controller chain +/// +/// Note: if your chain serves as both the controller and the host chain, you +/// may utilize the default +/// [host](crate::host::callback::on_chan_open_confirm_validate) +/// callback implementation instead. +pub fn on_chan_open_confirm_validate( + _ctx_a: &impl InterchainAccountValidationContext, + _port_id_on_a: &PortId, + _chan_id_on_a: &ChannelId, +) -> Result<(), InterchainAccountError> { + Err(InterchainAccountError::not_allowed( + "channel handshake must be initiated by the controller chain", + )) +} + +/// Default execution callback function on the chan_open_confirm request for the controller chain +/// +/// Note: if your chain serves as both the controller and the host chain, you +/// may utilize the default +/// [host](crate::host::callback::on_chan_open_confirm_execute) +/// callback implementation instead. +pub fn on_chan_open_confirm_execute( + _ctx_a: &mut impl InterchainAccountExecutionContext, + _port_id_on_a: PortId, + _chan_id_on_a: ChannelId, +) -> Result { + Err(InterchainAccountError::not_allowed( + "channel handshake must be initiated by the controller chain", + )) +} + +/// Default validation callback function on the chan_close_init request for the controller chain +pub fn on_chan_close_init_validate( + _ctx_a: &impl InterchainAccountValidationContext, + _port_id_on_a: &PortId, + _chan_id_on_a: &ChannelId, +) -> Result<(), InterchainAccountError> { + Err(InterchainAccountError::invalid("channel cannot be closed.")) +} + +/// Default execution callback function on the chan_close_init request for the controller chain +pub fn on_chan_close_init_execute( + _ctx_a: &mut impl InterchainAccountExecutionContext, + _port_id_on_a: PortId, + _chan_id_on_a: ChannelId, +) -> Result { + Err(InterchainAccountError::invalid("channel cannot be closed.")) +} + +/// Default validation callback function on the recv_packet request for the controller chain +/// +/// Note: if your chain serves as both the controller and the host chain, you +/// may utilize the default +/// [host](crate::host::callback::on_recv_packet_execute) +/// callback implementation instead. +pub fn on_recv_packet_execute( + _ctx_a: &mut impl InterchainAccountExecutionContext, + _packet: Packet, +) -> (ModuleExtras, Acknowledgement) { + ( + ModuleExtras::empty(), + AcknowledgementStatus::error( + InterchainAccountError::not_allowed("packet cannot be received").into(), + ) + .into(), + ) +} + +/// Default validation callback function on the acknowledgment_packet request for the controller chain +pub fn on_acknowledgement_packet_validate( + _ctx_a: &impl InterchainAccountValidationContext, + _packet: &Packet, + _ack_on_a: &Acknowledgement, + _relayer: &Signer, +) -> Result<(), InterchainAccountError> { + Ok(()) +} + +/// Default execution callback function on the acknowledgment_packet request for the controller chain +pub fn on_acknowledgement_packet_execute( + _ctx_a: &mut impl InterchainAccountExecutionContext, + _packet: Packet, + _ack_on_a: Acknowledgement, + _relayer: Signer, +) -> (ModuleExtras, Result<(), InterchainAccountError>) { + (ModuleExtras::empty(), Ok(())) +} + +/// Default validation callback function on the timeout_packet request for the controller chain +pub fn on_timeout_packet_validate( + _ctx_a: &impl InterchainAccountValidationContext, + _packet: &Packet, + _relayer: &Signer, +) -> Result<(), InterchainAccountError> { + Ok(()) +} + +/// Default execution callback function on the timeout_packet request for the controller chain +pub fn on_timeout_packet_execute( + _ctx_a: &mut impl InterchainAccountExecutionContext, + _packet: Packet, + _relayer: Signer, +) -> (ModuleExtras, Result<(), InterchainAccountError>) { + (ModuleExtras::empty(), Ok(())) +} diff --git a/ibc-apps/ics27-interchain-accounts/src/controller/handler/mod.rs b/ibc-apps/ics27-interchain-accounts/src/controller/handler/mod.rs new file mode 100644 index 000000000..4dfd2bdda --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/controller/handler/mod.rs @@ -0,0 +1,2 @@ +pub mod register_interchain_account; +pub mod send_tx; diff --git a/ibc-apps/ics27-interchain-accounts/src/controller/handler/register_interchain_account.rs b/ibc-apps/ics27-interchain-accounts/src/controller/handler/register_interchain_account.rs new file mode 100644 index 000000000..65c46d3e1 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/controller/handler/register_interchain_account.rs @@ -0,0 +1,126 @@ +use ibc_core::channel::handler::{chan_open_init_execute, chan_open_init_validate}; +use ibc_core::channel::types::channel::Order; +use ibc_core::channel::types::msgs::MsgChannelOpenInit; +use ibc_core::host::types::identifiers::ChannelId; +use ibc_core::host::types::path::ChannelEndPath; + +use crate::context::{InterchainAccountExecutionContext, InterchainAccountValidationContext}; +use crate::controller::msgs::MsgRegisterInterchainAccount; +use crate::error::InterchainAccountError; +use crate::port::{default_host_port_id, new_controller_port_id}; + +/// Entry point for registering an interchain account. +/// +/// - Generates a new `PortId` using the provided owner and returns an error if +/// the port is already in use. +/// - Callers are expected to provide the appropriate application version. For +/// example, this could be an ICS27 encoded metadata type with a nested +/// application version. +/// - Gaining access to interchain accounts whose channels have closed cannot be +/// done with this function. A regular MsgChannelOpenInit must be used. +pub fn register_interchain_account( + ctx_a: &mut Ctx, + msg: MsgRegisterInterchainAccount, +) -> Result<(), InterchainAccountError> +where + Ctx: InterchainAccountExecutionContext, +{ + register_interchain_account_validate(ctx_a, &msg)?; + register_interchain_account_execute(ctx_a, msg) +} + +/// Validate interchain account registration message. +pub fn register_interchain_account_validate( + ctx_a: &ValCtx, + msg: &MsgRegisterInterchainAccount, +) -> Result<(), InterchainAccountError> +where + ValCtx: InterchainAccountValidationContext, +{ + ctx_a.validate_message_signer(&msg.owner)?; + + let port_id_on_a = new_controller_port_id(&msg.owner)?; + + if let Ok(active_chan_id_on_a) = ctx_a.get_active_channel_id(&msg.conn_id_on_a, &port_id_on_a) { + let chan_end_path_on_a = ChannelEndPath::new(&port_id_on_a, &active_chan_id_on_a); + + let chan_end_on_a = ctx_a.channel_end(&chan_end_path_on_a)?; + + if !chan_end_on_a.is_closed() { + return Err(InterchainAccountError::already_exists( + "channel is already active or a handshake is in flight", + )); + } + } + + let module_id = ctx_a + .lookup_module_by_port(&port_id_on_a) + .ok_or(InterchainAccountError::not_found("no module found").given(&port_id_on_a))?; + + let port_id_on_b = default_host_port_id()?; + + let msg_chan_open_init = MsgChannelOpenInit::new( + port_id_on_a, + vec![msg.conn_id_on_a.clone()], + port_id_on_b, + Order::Ordered, + msg.owner.clone(), + msg.version.clone(), + ); + + if let Err(e) = chan_open_init_validate(ctx_a, module_id, msg_chan_open_init) { + // TODO: uncomment the below line after refactoring the logger to get + // accessible from the context. + // + // ctx_a.log_message(format!( "error registering interchain account. + // Error: {}", e )); + return Err(InterchainAccountError::source(e)); + } + + Ok(()) +} + +/// Execute interchain account registration message. +pub fn register_interchain_account_execute( + ctx_a: &mut ExecCtx, + msg: MsgRegisterInterchainAccount, +) -> Result<(), InterchainAccountError> +where + ExecCtx: InterchainAccountExecutionContext, +{ + let port_id_on_a = new_controller_port_id(&msg.owner)?; + + let module_id = ctx_a + .lookup_module_by_port(&port_id_on_a) + .ok_or(InterchainAccountError::not_found("no module found").given(&port_id_on_a))?; + + let port_id_on_b = default_host_port_id()?; + + let msg_chan_open_init = MsgChannelOpenInit::new( + port_id_on_a, + vec![msg.conn_id_on_a.clone()], + port_id_on_b, + Order::Ordered, + msg.owner.clone(), + msg.version, + ); + + if let Err(e) = chan_open_init_execute(ctx_a, module_id, msg_chan_open_init) { + ctx_a.log_message(format!( + "error registering interchain account. Error: {}", + e + )); + return Err(InterchainAccountError::source(e)); + } + + let chan_counter_on_a = ctx_a.channel_counter()?; + + let chan_id_on_a = ChannelId::new(chan_counter_on_a); + + ctx_a.log_message(format!( + "successfully registered interchain account with channel id: {}", + chan_id_on_a + )); + + Ok(()) +} diff --git a/ibc-apps/ics27-interchain-accounts/src/controller/handler/send_tx.rs b/ibc-apps/ics27-interchain-accounts/src/controller/handler/send_tx.rs new file mode 100644 index 000000000..2dc764769 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/controller/handler/send_tx.rs @@ -0,0 +1,119 @@ +use alloc::string::ToString; + +use ibc_core::channel::handler::send_packet; +use ibc_core::channel::types::packet::Packet; +use ibc_core::channel::types::timeout::TimeoutHeight; +use ibc_core::handler::types::events::MessageEvent; +use ibc_core::host::types::path::{ChannelEndPath, SeqSendPath}; +use ibc_core::primitives::Timestamp; + +use crate::context::InterchainAccountExecutionContext; +use crate::controller::msgs::MsgSendTx; +use crate::error::InterchainAccountError; +use crate::port::new_controller_port_id; +use crate::MODULE_ID_STR; + +/// Processes a pre-built packet data containing messages to be executed on the +/// host chain +/// +/// Note: if the packet is timed out, the channel will be closed. In the case of +/// channel closure, a new channel may be reopened to reconnect to the host chain. +pub fn send_tx(ctx_a: &mut Ctx, msg: MsgSendTx) -> Result<(), InterchainAccountError> +where + Ctx: InterchainAccountExecutionContext, +{ + send_tx_validate(ctx_a, msg.clone())?; + send_tx_execute(ctx_a, msg) +} + +/// Validate interchain account send tx messages. +fn send_tx_validate(ctx_a: &Ctx, msg: MsgSendTx) -> Result<(), InterchainAccountError> +where + Ctx: InterchainAccountExecutionContext, +{ + ctx_a.validate_message_signer(&msg.owner)?; + + let port_id = new_controller_port_id(&msg.owner)?; + + let host_timestamp = ctx_a.host_timestamp()?; + + let absolute_timestamp = calc_absolute_timeout(&host_timestamp, &msg.relative_timeout)?; + + // TODO: Why need this? + // Verifies that the packet is not expired + // This assumes time synchrony to a certain degree between the controller and counterparty host chain. + if absolute_timestamp > host_timestamp { + return Err(InterchainAccountError::invalid("timeout is in the past")); + } + + ctx_a.get_active_channel_id(&msg.conn_id_on_a, &port_id)?; + + Ok(()) +} + +/// Execute interchain account send tx messages. +fn send_tx_execute(ctx_a: &mut Ctx, msg: MsgSendTx) -> Result<(), InterchainAccountError> +where + Ctx: InterchainAccountExecutionContext, +{ + let port_id_on_a = new_controller_port_id(&msg.owner)?; + + let active_channel_id = ctx_a.get_active_channel_id(&msg.conn_id_on_a, &port_id_on_a)?; + + let chan_end_path_on_a = ChannelEndPath::new(&port_id_on_a, &active_channel_id); + + let chan_end_on_a = ctx_a.channel_end(&chan_end_path_on_a)?; + + let port_id_on_b = chan_end_on_a.counterparty().port_id(); + + let chan_id_on_b = + chan_end_on_a + .counterparty() + .channel_id() + .ok_or(InterchainAccountError::empty( + "channel id on counterparty is not set", + ))?; + + let seq_send_path_on_a = SeqSendPath::new(&port_id_on_a, &active_channel_id); + + let seq_on_a = ctx_a.get_next_sequence_send(&seq_send_path_on_a)?; + + let host_timestamp = ctx_a.host_timestamp()?; + + let absolute_timestamp = calc_absolute_timeout(&host_timestamp, &msg.relative_timeout)?; + + let packet = Packet { + seq_on_a, + port_id_on_a, + chan_id_on_a: active_channel_id, + port_id_on_b: port_id_on_b.clone(), + chan_id_on_b: chan_id_on_b.clone(), + data: msg.packet_data.data, + timeout_height_on_b: TimeoutHeight::Never, + timeout_timestamp_on_b: absolute_timestamp, + }; + + send_packet(ctx_a, packet)?; + + ctx_a.emit_ibc_event(MessageEvent::Module(MODULE_ID_STR.to_string()).into()); + + Ok(()) +} + +fn calc_absolute_timeout( + host_timestamp: &Timestamp, + msg_relative_timestamp: &Timestamp, +) -> Result { + let host_timestamp = host_timestamp.nanoseconds(); + + let absolute_nanos = host_timestamp + .checked_add(msg_relative_timestamp.nanoseconds()) + .ok_or(InterchainAccountError::invalid( + "timeout is too large and overflows", + ))?; + + let absolute_timestamp = + Timestamp::from_nanoseconds(absolute_nanos).map_err(InterchainAccountError::source)?; + + Ok(absolute_timestamp) +} diff --git a/ibc-apps/ics27-interchain-accounts/src/controller/mod.rs b/ibc-apps/ics27-interchain-accounts/src/controller/mod.rs new file mode 100644 index 000000000..0ee555136 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/controller/mod.rs @@ -0,0 +1,3 @@ +pub mod callback; +pub mod handler; +pub mod msgs; diff --git a/ibc-apps/ics27-interchain-accounts/src/controller/msgs/mod.rs b/ibc-apps/ics27-interchain-accounts/src/controller/msgs/mod.rs new file mode 100644 index 000000000..dad8910e4 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/controller/msgs/mod.rs @@ -0,0 +1,5 @@ +mod register_interchain_account; +mod send_tx; + +pub use register_interchain_account::MsgRegisterInterchainAccount; +pub use send_tx::MsgSendTx; diff --git a/ibc-apps/ics27-interchain-accounts/src/controller/msgs/register_interchain_account.rs b/ibc-apps/ics27-interchain-accounts/src/controller/msgs/register_interchain_account.rs new file mode 100644 index 000000000..8c0834131 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/controller/msgs/register_interchain_account.rs @@ -0,0 +1,73 @@ +use alloc::string::{String, ToString}; + +use ibc_core::channel::types::Version; +use ibc_core::host::types::identifiers::ConnectionId; +use ibc_core::primitives::proto::Protobuf; +use ibc_core::primitives::Signer; +use ibc_proto::ibc::apps::interchain_accounts::controller::v1::MsgRegisterInterchainAccount as RawMsgRegisterInterchainAccount; + +use crate::error::InterchainAccountError; + +pub(crate) const TYPE_URL: &str = + "/ibc.applications.interchain_account.controller.v1.MsgRegisterInterchainAccount"; + +// Defines the domain type for the interchain account registration message. +#[derive(Clone, Debug)] +pub struct MsgRegisterInterchainAccount { + /// The owner of the interchain account. + pub owner: Signer, + /// The connection identifier on the controller chain. + /// Note: to learn about our naming convention, see [here](crate). + pub conn_id_on_a: ConnectionId, + /// The version of the interchain account. + pub version: Version, +} + +impl MsgRegisterInterchainAccount { + pub fn new( + owner: Signer, + conn_id_on_a: ConnectionId, + version: Version, + ) -> MsgRegisterInterchainAccount { + MsgRegisterInterchainAccount { + owner, + conn_id_on_a, + version, + } + } +} + +impl Protobuf for MsgRegisterInterchainAccount {} + +impl TryFrom for MsgRegisterInterchainAccount { + type Error = InterchainAccountError; + + fn try_from(raw: RawMsgRegisterInterchainAccount) -> Result { + if raw.owner.is_empty() { + return Err(InterchainAccountError::empty("controller owner address")); + } + + Ok(MsgRegisterInterchainAccount { + owner: raw.owner.into(), + conn_id_on_a: raw + .connection_id + .parse() + .map_err(InterchainAccountError::source)?, + version: raw + .version + .parse() + .map_err(InterchainAccountError::source)?, + }) + } +} + +impl From for RawMsgRegisterInterchainAccount { + fn from(domain: MsgRegisterInterchainAccount) -> Self { + RawMsgRegisterInterchainAccount { + owner: domain.owner.to_string(), + connection_id: domain.conn_id_on_a.to_string(), + version: domain.version.to_string(), + ordering: todo!(), + } + } +} diff --git a/ibc-apps/ics27-interchain-accounts/src/controller/msgs/send_tx.rs b/ibc-apps/ics27-interchain-accounts/src/controller/msgs/send_tx.rs new file mode 100644 index 000000000..bd9388513 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/controller/msgs/send_tx.rs @@ -0,0 +1,63 @@ +use alloc::string::{String, ToString}; + +use ibc_core::host::types::identifiers::ConnectionId; +use ibc_core::primitives::proto::Protobuf; +use ibc_core::primitives::{Signer, Timestamp}; +use ibc_proto::ibc::apps::interchain_accounts::controller::v1::MsgSendTx as RawMsgSendTx; + +use crate::error::InterchainAccountError; +use crate::packet::InterchainAccountPacketData; + +pub(crate) const TYPE_URL: &str = "/ibc.applications.interchain_account.controller.v1.MsgSendTx"; + +/// Defines the domain type for the `MsgSendTx` message. +#[derive(Clone, Debug)] +pub struct MsgSendTx { + /// The controller owner address + pub owner: Signer, + /// The connection id on the controller chain + pub conn_id_on_a: ConnectionId, + /// The packet data + pub packet_data: InterchainAccountPacketData, + /// The relative timeout + pub relative_timeout: Timestamp, +} + +impl Protobuf for MsgSendTx {} + +impl TryFrom for MsgSendTx { + type Error = InterchainAccountError; + + fn try_from(raw: RawMsgSendTx) -> Result { + let relative_timeout = Timestamp::from_nanoseconds(raw.relative_timeout) + .map_err(InterchainAccountError::source)?; + + if !relative_timeout.is_set() { + return Err(InterchainAccountError::empty("relative timeout is not set")); + } + + Ok(MsgSendTx { + owner: raw.owner.into(), + conn_id_on_a: raw + .connection_id + .parse() + .map_err(InterchainAccountError::source)?, + packet_data: match raw.packet_data { + Some(packet_data) => packet_data.try_into()?, + None => Err(InterchainAccountError::empty("packet data"))?, + }, + relative_timeout, + }) + } +} + +impl From for RawMsgSendTx { + fn from(domain: MsgSendTx) -> Self { + RawMsgSendTx { + owner: domain.owner.to_string(), + connection_id: domain.conn_id_on_a.to_string(), + packet_data: Some(domain.packet_data.into()), + relative_timeout: domain.relative_timeout.nanoseconds(), + } + } +} diff --git a/ibc-apps/ics27-interchain-accounts/src/error.rs b/ibc-apps/ics27-interchain-accounts/src/error.rs new file mode 100644 index 000000000..3aa458a69 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/error.rs @@ -0,0 +1,172 @@ +use ibc_core::channel::types::acknowledgement::StatusValue; +use ibc_core::channel::types::error::ChannelError; +use ibc_core::handler::types::error::ContextError; + +/// Defines the interchain account error type +#[derive(Clone, Debug)] +pub struct InterchainAccountError { + /// Error code. + pub code: Code, + /// Refers to the type, value or place that error originated from or pertains to. + pub origin: String, + /// The given value that caused the error. + pub given: Option, + /// The optional expected value. + pub expected: Option, +} + +impl InterchainAccountError { + /// Constructs a new interchain account error instance with the given code and message. + pub fn new(code: Code, origin: impl Into) -> Self { + Self { + code, + origin: origin.into(), + given: None, + expected: None, + } + } + + /// Constructs an `Empty` error with the given message. + pub fn empty(origin: impl Into) -> Self { + Self::new(Code::Empty, origin) + } + + /// Constructs a `NotFound` error with the given message. + pub fn not_found(origin: impl Into) -> Self { + Self::new(Code::NotFound, origin) + } + + /// Constructs an `AlreadyExists` error with the given message. + pub fn already_exists(origin: impl Into) -> Self { + Self::new(Code::AlreadyExists, origin) + } + + /// Constructs an `Invalid` error with the given message. + pub fn invalid(origin: impl Into) -> Self { + Self::new(Code::Invalid, origin) + } + + /// Constructs a `MisMatch` error with the given message. + pub fn mismatch(origin: impl Into) -> Self { + Self::new(Code::Mismatch, origin) + } + + /// Constructs a `NotAllowed` error with the given message. + pub fn not_allowed(origin: impl Into) -> Self { + Self::new(Code::NotAllowed, origin) + } + + /// Constructs a `NotSupported` error with the given message. + pub fn not_supported(origin: impl Into) -> Self { + Self::new(Code::NotSupported, origin) + } + + /// Constructs a `Source` error with the given message. + pub fn source(origin: impl ToString) -> Self { + Self { + code: Code::Source, + origin: origin.to_string(), + expected: None, + given: None, + } + } + + /// Adds an expected value to the error message. + pub fn expected(&self, expected: &impl ToString) -> Self { + Self { + expected: Some(expected.to_string()), + ..self.clone() + } + } + + /// Adds a given value to the error message. + pub fn given(&self, given: &impl ToString) -> Self { + Self { + given: Some(given.to_string()), + ..self.clone() + } + } +} + +impl core::fmt::Display for InterchainAccountError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Code: {} Origin: {}", self.code, self.origin)?; + + if let Some(given) = &self.given { + write!(f, ", Given: {}", given)?; + } + + if let Some(expected) = &self.expected { + write!(f, ", Expected: {}", expected)?; + } + + Ok(()) + } +} + +impl From for InterchainAccountError { + fn from(err: ContextError) -> InterchainAccountError { + Self::source(err) + } +} + +impl From for InterchainAccountError { + fn from(err: ChannelError) -> InterchainAccountError { + Self::source(err) + } +} + +impl From for StatusValue { + fn from(err: InterchainAccountError) -> Self { + StatusValue::new(err.to_string()).expect("error message cannot be empty") + } +} + +#[derive(Clone, Debug)] +pub enum Code { + /// cannot be empty! + Empty = 0, + + /// not found! + NotFound = 1, + + /// already exists! + AlreadyExists = 2, + + /// has invalid state! + Invalid = 3, + + /// state mismatch! + Mismatch = 4, + + /// not allowed! + NotAllowed = 5, + + /// not supported! + NotSupported = 6, + + /// from other source! + Source = 7, +} + +impl Code { + /// Returns a string description of the error code. + pub fn description(&self) -> &'static str { + match self { + Code::Empty => "cannot be empty!", + Code::NotFound => "not found!", + Code::AlreadyExists => "already exists!", + Code::Invalid => "invalid state!", + Code::Mismatch => "state mismatch!", + Code::NotAllowed => "not allowed!", + Code::NotSupported => "not supported!", + Code::Source => "from other source!", + } + } +} + +impl core::fmt::Display for Code { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(self.description(), f) + } +} diff --git a/ibc-apps/ics27-interchain-accounts/src/events.rs b/ibc-apps/ics27-interchain-accounts/src/events.rs new file mode 100644 index 000000000..4b461a4c3 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/events.rs @@ -0,0 +1,5 @@ +//! Defines all interchain accounts' event types +const _EVENT_TYPE_INTERCHAIN_ACCOUNT: &str = "ibc_interchainaccounts"; + +/// Contains all events variants that can be emitted from the interchain account application +pub enum Event {} diff --git a/ibc-apps/ics27-interchain-accounts/src/host/callback.rs b/ibc-apps/ics27-interchain-accounts/src/host/callback.rs new file mode 100644 index 000000000..ef2422a29 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/host/callback.rs @@ -0,0 +1,375 @@ +//! Provides a set of default on-module callback functions for the host chain. + +use alloc::string::ToString; + +use ibc_core::channel::types::acknowledgement::{Acknowledgement, AcknowledgementStatus}; +use ibc_core::channel::types::channel::{Counterparty, Order}; +use ibc_core::channel::types::packet::Packet; +use ibc_core::channel::types::Version; +use ibc_core::host::types::identifiers::{ChannelId, ConnectionId, PortId}; +use ibc_core::host::types::path::ChannelEndPath; +use ibc_core::primitives::Signer; +use ibc_core::router::types::module::ModuleExtras; + +use super::handler::create_interchain_account::create_interchain_account; +use super::handler::on_recv_packet::on_recv_packet; +use crate::ack_success; +use crate::context::{InterchainAccountExecutionContext, InterchainAccountValidationContext}; +use crate::error::InterchainAccountError; +use crate::metadata::Metadata; +use crate::port::default_host_port_id; + +/// Default validation callback function on the chan_open_init request for the host chain +/// +/// Note: if your chain serves as both the controller and the host chain, you +/// may utilize the default +/// [controller](crate::controller::callback::on_chan_open_init_validate) +/// callback implementation instead. +pub fn on_chan_open_init_validate( + _ctx_b: &impl InterchainAccountValidationContext, + _order: Order, + _conn_hops_on_b: &[ConnectionId], + _port_id_on_b: &PortId, + _chan_id_on_b: &ChannelId, + _counterparty: &Counterparty, + _version_on_b: &Version, +) -> Result<(), InterchainAccountError> { + Err(InterchainAccountError::not_allowed( + "channel handshake must be initiated by the controller chain", + )) +} + +/// Default execution callback function on the chan_open_init request for the host chain +/// +/// Note: if your chain serves as both the controller and the host chain, you +/// may utilize the default +/// [controller](crate::controller::callback::on_chan_open_init_execute) +/// callback implementation instead. +pub fn on_chan_open_init_execute( + _ctx_b: &mut impl InterchainAccountExecutionContext, + _order: Order, + _conn_hops_on_b: &[ConnectionId], + _port_id_on_b: PortId, + _chan_id_on_b: ChannelId, + _counterparty: Counterparty, + _version_on_b: &Version, +) -> Result<(ModuleExtras, Version), InterchainAccountError> { + Err(InterchainAccountError::not_allowed( + "channel handshake must be initiated by the controller chain", + )) +} +/// Default validation callback function on the chan_open_try request for the host chain +pub fn on_chan_open_try_validate( + ctx_b: &impl InterchainAccountValidationContext, + order: Order, + conn_hops_on_b: &[ConnectionId], + port_id_on_b: &PortId, + chan_id_on_b: &ChannelId, + counterparty: &Counterparty, + version_on_a: &Version, +) -> Result<(), InterchainAccountError> { + let params = ctx_b.get_params()?; + + if !params.host_enabled { + return Err(InterchainAccountError::not_supported( + "host chain is not enabled.", + )); + } + + if order != Order::Ordered { + return Err(InterchainAccountError::not_supported( + "only ordered channels are supported.", + )); + } + let host_port_id = default_host_port_id()?; + + if port_id_on_b != &host_port_id { + return Err( + InterchainAccountError::mismatch("port id must be the default host port id.") + .given(port_id_on_b) + .expected(&host_port_id), + ); + } + + let metadata = serde_json::from_str::(version_on_a.as_str()) + .map_err(InterchainAccountError::source)?; + + metadata.validate(ctx_b, conn_hops_on_b)?; + + if let Ok(active_channel_id) = + ctx_b.get_active_channel_id(&conn_hops_on_b[0], counterparty.port_id()) + { + if &active_channel_id != chan_id_on_b { + return Err( + InterchainAccountError::mismatch("active channel id mismatch") + .given(chan_id_on_b) + .expected(&active_channel_id), + ); + } + + let chan_end_path = ChannelEndPath::new(port_id_on_b, &active_channel_id); + + let chan_end_on_b = ctx_b.channel_end(&chan_end_path)?; + + if chan_end_on_b.state().is_open() { + return Err(InterchainAccountError::invalid("channel is already open.")); + } + + metadata.verify_prev_metadata_matches(chan_end_on_b.version())?; + } + + Ok(()) +} + +/// Default execution callback function on the chan_open_try request for the host chain +pub fn on_chan_open_try_execute( + ctx_b: &mut impl InterchainAccountExecutionContext, + _order: Order, + conn_hops_on_b: &[ConnectionId], + port_id_on_b: &PortId, + _chan_id_on_b: &ChannelId, + counterparty: &Counterparty, + version_on_a: &Version, +) -> Result<(ModuleExtras, Version), InterchainAccountError> { + let ica_address = if let Ok(ica_account) = + ctx_b.get_ica_address(&conn_hops_on_b[0], counterparty.port_id()) + { + ctx_b.validate_message_signer(&ica_account)?; + + ctx_b.log_message("reopening existing interchain account".to_string()); + + ctx_b.get_interchain_account(&ica_account)?; + + ica_account + } else { + create_interchain_account(ctx_b, conn_hops_on_b[0].clone(), port_id_on_b.clone())? + }; + + ctx_b.log_message("interchain account created".to_string()); + + let mut metadata = serde_json::from_str::(version_on_a.as_str()) + .map_err(InterchainAccountError::source)?; + + metadata.address = ica_address; + + let metadata_str = serde_json::to_string(&metadata).map_err(InterchainAccountError::source)?; + + Ok((ModuleExtras::empty(), Version::new(metadata_str))) +} + +/// Default validation callback function on the chan_open_ack request for the host chain +/// +/// Note: if your chain serves as both the controller and the host chain, you +/// may utilize the default +/// [controller](crate::controller::callback::on_chan_open_ack_validate) +/// callback implementation instead. +pub fn on_chan_open_ack_validate( + _ctx_b: &impl InterchainAccountValidationContext, + _port_id_on_b: &PortId, + _chan_id_on_b: &ChannelId, + _version_on_a: &Version, +) -> Result<(), InterchainAccountError> { + Err(InterchainAccountError::not_allowed( + "channel handshake must be initiated by the controller chain", + )) +} + +/// Default execution callback function on the chan_open_ack request for the host chain +/// +/// Note: if your chain serves as both the controller and the host chain, you +/// may utilize the default +/// [controller](crate::controller::callback::on_chan_open_ack_execute) +/// callback implementation instead. +pub fn on_chan_open_ack_execute( + _ctx_b: &mut impl InterchainAccountExecutionContext, + _port_id_on_b: PortId, + _chan_id_on_b: ChannelId, + _version_on_a: Version, +) -> Result { + Err(InterchainAccountError::not_allowed( + "channel handshake must be initiated by the controller chain", + )) +} + +/// Default validation callback function on the chan_open_confirm request for the host chain +pub fn on_chan_open_confirm_validate( + ctx_b: &impl InterchainAccountValidationContext, + port_id_on_b: &PortId, + chan_id_on_b: &ChannelId, +) -> Result<(), InterchainAccountError> { + let params = ctx_b.get_params()?; + + if !params.host_enabled { + return Err(InterchainAccountError::not_supported( + "host chain is not enabled.", + )); + } + + // It is assumed the controller chain will not allow multiple active channels to be created for the same ConnectionId/PortId + // If the controller chain does allow multiple active channels to be created for the same ConnectionId/PortId, + // disallowing overwriting the current active channel guarantees the channel can no longer be used as the controller + // and host will disagree on what the currently active channel is + ctx_b.channel_end(&ChannelEndPath::new(port_id_on_b, chan_id_on_b))?; + Ok(()) +} + +/// Default execution callback function on the chan_open_confirm request for the host chain +pub fn on_chan_open_confirm_execute( + ctx_b: &mut impl InterchainAccountExecutionContext, + port_id_on_b: PortId, + chan_id_on_b: ChannelId, +) -> Result { + let chan_end = ctx_b.channel_end(&ChannelEndPath::new(&port_id_on_b, &chan_id_on_b))?; + + ctx_b.store_active_channel_id( + chan_end.connection_hops[0].clone(), + port_id_on_b, + chan_id_on_b, + )?; + + Ok(ModuleExtras::empty()) +} + +/// Default validation callback function on the chan_close_init request for the host chain +pub fn on_chan_close_init_validate( + _ctx_b: &impl InterchainAccountValidationContext, + _port_id_on_b: &PortId, + _chan_id_on_b: &ChannelId, +) -> Result<(), InterchainAccountError> { + Err(InterchainAccountError::invalid("channel cannot be closed")) +} + +/// Default execution callback function on the chan_close_init request for the host chain +pub fn on_chan_close_init_execute( + _ctx_b: &mut impl InterchainAccountExecutionContext, + _port_id_on_b: PortId, + _chan_id_on_b: ChannelId, +) -> Result { + Err(InterchainAccountError::invalid("channel cannot be closed")) +} + +/// Default validation callback function on the chan_close_confirm request for the host chain +pub fn on_chan_close_confirm_validate( + _ctx_b: &impl InterchainAccountValidationContext, + _port_id_on_b: &PortId, + _chan_ib_on_b: &ChannelId, +) -> Result<(), InterchainAccountError> { + Ok(()) +} + +/// Default execution callback function on the chan_close_confirm request for the host chain +pub fn on_chan_close_confirm_execute( + _ctx_b: &mut impl InterchainAccountExecutionContext, + _port_id_on_b: PortId, + _chan_id_on_b: ChannelId, +) -> Result { + Ok(ModuleExtras::empty()) +} + +/// Default execution callback function on the recv_packet request for the host chain +pub fn on_recv_packet_execute( + ctx_b: &mut impl InterchainAccountExecutionContext, + packet: &Packet, +) -> (ModuleExtras, Acknowledgement) { + let params = match ctx_b.get_params() { + Ok(params) => params, + Err(e) => { + return ( + ModuleExtras::empty(), + AcknowledgementStatus::error(e.into()).into(), + ) + } + }; + + if !params.host_enabled { + return ( + ModuleExtras::empty(), + AcknowledgementStatus::error( + InterchainAccountError::not_supported("host chain is not enabled.").into(), + ) + .into(), + ); + } + + if let Err(e) = on_recv_packet(ctx_b, packet) { + return ( + ModuleExtras::empty(), + AcknowledgementStatus::error(e.into()).into(), + ); + } + + ( + ModuleExtras::empty(), + AcknowledgementStatus::success(ack_success()).into(), + ) +} + +/// Default validation callback function on the acknowledgement_packet request for the host chain +/// +/// Note: if your chain serves as both the controller and the host chain, you +/// may utilize the default +/// [controller](crate::controller::callback::on_acknowledgement_packet_validate) +/// callback implementation instead. +pub fn on_acknowledgement_packet_validate( + _ctx_b: &impl InterchainAccountValidationContext, + _packet: &Packet, + _ack_on_b: &Acknowledgement, + _relayer: &Signer, +) -> Result<(), InterchainAccountError> { + Err(InterchainAccountError::not_allowed( + "cannot receive acknowledgement on a host channel end, a host chain does not send a packet over the channel", + )) +} + +/// Default execution callback function on the acknowledgement_packet request for the host chain +/// +/// Note: if your chain serves as both the controller and the host chain, you +/// may utilize the default +/// [controller](crate::controller::callback::on_acknowledgement_packet_execute) +/// callback implementation instead. +pub fn on_acknowledgement_packet_execute( + _ctx_b: &mut impl InterchainAccountExecutionContext, + _packet: Packet, + _ack_on_b: Acknowledgement, + _relayer: Signer, +) -> (ModuleExtras, Result<(), InterchainAccountError>) { + (ModuleExtras::empty(), Err( + InterchainAccountError::not_allowed( + "cannot receive acknowledgement on a host channel end, a host chain does not send a packet over the channel", + ) + )) +} + +/// Default validation callback function on the timeout_packet request for the host chain +/// +/// Note: if your chain serves as both the controller and the host chain, you +/// may utilize the default +/// [controller](crate::controller::callback::on_timeout_packet_validate) +/// callback implementation instead. +pub fn on_timeout_packet_validate( + _ctx_b: &impl InterchainAccountValidationContext, + _packet: &Packet, + _relayer: &Signer, +) -> Result<(), InterchainAccountError> { + Err(InterchainAccountError::not_allowed( + "cannot cause a packet timeout on a host channel end, a host chain does not send a packet over the channel", + )) +} + +/// Default execution callback function on the timeout_packet request for the host chain +/// +/// Note: if your chain serves as both the controller and the host chain, you +/// may utilize the default +/// [controller](crate::controller::callback::on_timeout_packet_execute) +/// callback implementation instead. +pub fn on_timeout_packet_execute( + _ctx_b: &mut impl InterchainAccountExecutionContext, + _packet: Packet, + _relayer: Signer, +) -> (ModuleExtras, Result<(), InterchainAccountError>) { + (ModuleExtras::empty(), Err( + InterchainAccountError::not_allowed( + "cannot cause a packet timeout on a host channel end, a host chain does not send a packet over the channel", + ) + )) +} diff --git a/ibc-apps/ics27-interchain-accounts/src/host/handler/create_interchain_account.rs b/ibc-apps/ics27-interchain-accounts/src/host/handler/create_interchain_account.rs new file mode 100644 index 000000000..aa38a8985 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/host/handler/create_interchain_account.rs @@ -0,0 +1,47 @@ +use core::str::FromStr; + +use cosmrs::AccountId; +use ibc_core::host::types::identifiers::{ConnectionId, PortId}; +use ibc_core::primitives::Signer; + +use crate::account::{InterchainAccount, SdkBaseAccount}; +use crate::context::InterchainAccountExecutionContext; +use crate::error::InterchainAccountError; + +/// Creates a new interchain account. +/// +/// Generates an address using the host `ConnectionId`, the controller `PortID`, +/// and block dependent information. +/// Returns an error if an account already exists for the generated account. +/// Sets an interchain account type and updates the interchain account address mapping +pub fn create_interchain_account( + ctx_b: &mut Ctx, + conn_id_on_b: ConnectionId, + port_id_on_a: PortId, +) -> Result +where + Ctx: InterchainAccountExecutionContext, +{ + let address = ctx_b.generate_ica_address(conn_id_on_b.clone(), port_id_on_a.clone())?; + + // TODO: This is a sdk specific code. Needed a generic way to create an account + let account = AccountId::from_str(address.as_ref()).map_err(InterchainAccountError::source)?; + + // TODO: it should be renamed to smt like `validate_account` (later PR) + ctx_b.validate_message_signer(&address)?; + + ctx_b.get_interchain_account(&address)?; + + let interchain_account = InterchainAccount::::new_with_sdk_base_account( + account, + port_id_on_a.clone(), + ); + + let account = ctx_b.new_interchain_account(interchain_account.clone())?; + + ctx_b.store_interchain_account(account)?; + + ctx_b.store_ica_address(conn_id_on_b, port_id_on_a, interchain_account.address())?; + + Ok(interchain_account.address()) +} diff --git a/ibc-apps/ics27-interchain-accounts/src/host/handler/mod.rs b/ibc-apps/ics27-interchain-accounts/src/host/handler/mod.rs new file mode 100644 index 000000000..7c4e71129 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/host/handler/mod.rs @@ -0,0 +1,2 @@ +pub mod create_interchain_account; +pub mod on_recv_packet; diff --git a/ibc-apps/ics27-interchain-accounts/src/host/handler/on_recv_packet.rs b/ibc-apps/ics27-interchain-accounts/src/host/handler/on_recv_packet.rs new file mode 100644 index 000000000..218052e73 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/host/handler/on_recv_packet.rs @@ -0,0 +1,96 @@ +use alloc::vec::Vec; + +use ibc_core::channel::types::packet::Packet; +use ibc_core::entrypoint::{execute, validate}; +use ibc_core::handler::types::msgs::MsgEnvelope; +use ibc_core::host::types::identifiers::{ConnectionId, PortId}; +use ibc_core::host::types::path::ChannelEndPath; +use ibc_core::primitives::proto::Protobuf; +use ibc_core::router::router::Router; + +use crate::context::InterchainAccountExecutionContext; +use crate::error::InterchainAccountError; +use crate::host::msgs::cosmos_tx::CosmosTx; +use crate::packet::InterchainAccountPacketData; + +/// Handles a given interchain accounts packet on a destination host chain. +/// If the transaction is successfully executed, the transaction response bytes will be returned. +pub fn on_recv_packet(ctx_b: &mut Ctx, packet: &Packet) -> Result<(), InterchainAccountError> +where + Ctx: InterchainAccountExecutionContext, +{ + let ica_packet_date = InterchainAccountPacketData::try_from(packet.data.clone())?; + + let cosmos_tx = + CosmosTx::decode_vec(&ica_packet_date.data).map_err(InterchainAccountError::source)?; + + let chan_end_path_on_b = ChannelEndPath::new(&packet.port_id_on_b, &packet.chan_id_on_b); + + let chan_end_on_b = ctx_b.channel_end(&chan_end_path_on_b)?; + + let mut envelope_msgs: Vec = Vec::new(); + + for msg in cosmos_tx.messages { + let envelope = msg + .try_into() + .map_err(|_| InterchainAccountError::invalid("msg is not of the MsgEnvelope type"))?; + envelope_msgs.push(envelope); + } + + validate_msgs( + ctx_b, + &envelope_msgs, + &chan_end_on_b.connection_hops, + &packet.port_id_on_a, + )?; + + execute_msgs(ctx_b, envelope_msgs)?; + + Ok(()) +} + +/// Validates the provided msgs contain the correct interchain account signer +/// address retrieved from state using the provided controller port identifier +pub fn validate_msgs( + ctx_b: &Ctx, + msgs: &Vec, + conn_hops_on_b: &[ConnectionId], + port_id_on_a: &PortId, +) -> Result<(), InterchainAccountError> +where + Ctx: InterchainAccountExecutionContext, +{ + ctx_b.get_ica_address(&conn_hops_on_b[0], port_id_on_a)?; + + let params_on_b = ctx_b.get_params()?; + + for msg in msgs { + if !params_on_b.contains_msg_type(msg) { + Err(InterchainAccountError::not_allowed( + "msg type is not allowed", + ))?; + } + + ctx_b.validate_message_signer(&msg.signer())?; + } + + Ok(()) +} + +/// Handles a given interchain accounts packet on a destination host chain. +/// If the transaction is successfully executed, the transaction response bytes is returned. +pub fn execute_msgs( + ctx_b: &mut Ctx, + msgs: Vec, +) -> Result<(), InterchainAccountError> +where + Ctx: InterchainAccountExecutionContext, +{ + for msg in msgs { + // TODO(rano): ctx should have router already + validate(ctx_b, msg.clone()).map_err(InterchainAccountError::source)?; + execute(ctx_b, msg).map_err(InterchainAccountError::source)?; + } + + Ok(()) +} diff --git a/ibc-apps/ics27-interchain-accounts/src/host/mod.rs b/ibc-apps/ics27-interchain-accounts/src/host/mod.rs new file mode 100644 index 000000000..74bd50a19 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/host/mod.rs @@ -0,0 +1,7 @@ +pub mod callback; +pub mod handler; +pub mod msgs; +pub mod params; + +/// The key used when generating a module address for the host chain. +pub const HOST_ACCOUNT_KEY: &str = "icahost-accounts"; diff --git a/ibc-apps/ics27-interchain-accounts/src/host/msgs/cosmos_tx.rs b/ibc-apps/ics27-interchain-accounts/src/host/msgs/cosmos_tx.rs new file mode 100644 index 000000000..1da0c578d --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/host/msgs/cosmos_tx.rs @@ -0,0 +1,51 @@ +//! Defines the `CosmosTx` message type, which sends a list of messages to hosts + +use alloc::string::ToString; +use alloc::vec::Vec; + +use ibc_core::primitives::proto::Protobuf; +use ibc_proto::google::protobuf::Any; +use ibc_proto::ibc::apps::interchain_accounts::v1::CosmosTx as RawCosmosTx; + +use crate::error::InterchainAccountError; + +const TYPE_URL: &str = "/ibc.applications.interchain_accounts.v1.CosmosTx"; + +#[derive(Clone, Debug)] +pub struct CosmosTx { + /// The list of messages to be executed on the host chain. + pub messages: Vec, +} + +impl Protobuf for CosmosTx {} + +impl TryFrom for CosmosTx { + type Error = InterchainAccountError; + + fn try_from(raw: RawCosmosTx) -> Result { + if raw.messages.is_empty() { + return Err(InterchainAccountError::empty("msgs of CosmosTx")); + } + + Ok(CosmosTx { + messages: raw.messages, + }) + } +} + +impl From for RawCosmosTx { + fn from(value: CosmosTx) -> Self { + RawCosmosTx { + messages: value.messages, + } + } +} + +impl From for Any { + fn from(value: CosmosTx) -> Self { + Any { + type_url: TYPE_URL.to_string(), + value: value.encode_vec(), + } + } +} diff --git a/ibc-apps/ics27-interchain-accounts/src/host/msgs/mod.rs b/ibc-apps/ics27-interchain-accounts/src/host/msgs/mod.rs new file mode 100644 index 000000000..a425cef98 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/host/msgs/mod.rs @@ -0,0 +1,2 @@ +pub mod cosmos_tx; +pub mod update_params; diff --git a/ibc-apps/ics27-interchain-accounts/src/host/msgs/update_params.rs b/ibc-apps/ics27-interchain-accounts/src/host/msgs/update_params.rs new file mode 100644 index 000000000..a7292f523 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/host/msgs/update_params.rs @@ -0,0 +1,4 @@ +//! Defines the `MsgUpdateParams` allowing to update the parameters of the host. +//! +//! TODO: This is a very new feature that would be available only after we upgrade +//! ibc-proto-rs with the latest version of ibc-go proto types (v7.0.1) diff --git a/ibc-apps/ics27-interchain-accounts/src/host/params.rs b/ibc-apps/ics27-interchain-accounts/src/host/params.rs new file mode 100644 index 000000000..f950ab44a --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/host/params.rs @@ -0,0 +1,68 @@ +use alloc::string::String; +use alloc::vec::Vec; + +use ibc_core::handler::types::msgs::MsgEnvelope; +use ibc_core::primitives::proto::Protobuf; +use ibc_proto::ibc::apps::interchain_accounts::host::v1::Params as RawParams; + +use crate::error::InterchainAccountError; + +pub const ALLOW_ALL_HOST_MSGS: &str = "*"; + +/// Defines the interchain account host parameters. +#[derive(Clone, Debug)] +pub struct Params { + /// Enables or disables the host submodule. + pub host_enabled: bool, + /// Defines a list of message typeURLs allowed to be executed on a host chain. + pub allow_messages: Vec, +} + +impl Params { + pub fn new(host_enabled: bool, allow_messages: Vec) -> Self { + Params { + host_enabled, + allow_messages, + } + } + + pub fn contains_msg_type(&self, msg: &MsgEnvelope) -> bool { + if self.allow_messages.len() == 1 && self.allow_messages[0] == ALLOW_ALL_HOST_MSGS { + true + } else { + self.allow_messages.contains(&msg.type_url()) + } + } +} + +impl Protobuf for Params {} + +impl TryFrom for Params { + type Error = InterchainAccountError; + + fn try_from(raw: RawParams) -> Result { + if raw.allow_messages.is_empty() { + return Err(InterchainAccountError::empty("allow_messages")); + } + + if raw.allow_messages.iter().any(|m| m.trim().is_empty()) { + return Err(InterchainAccountError::empty( + "allow_messages cannot contain empty strings", + )); + } + + Ok(Params { + host_enabled: raw.host_enabled, + allow_messages: raw.allow_messages, + }) + } +} + +impl From for RawParams { + fn from(value: Params) -> Self { + RawParams { + host_enabled: value.host_enabled, + allow_messages: value.allow_messages, + } + } +} diff --git a/ibc-apps/ics27-interchain-accounts/src/lib.rs b/ibc-apps/ics27-interchain-accounts/src/lib.rs new file mode 100644 index 000000000..a517de7a5 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/lib.rs @@ -0,0 +1,40 @@ +//! Implementation of Interchain Accounts (ICS27) application logic. +//! +//! Note: to be consistent with our naming convention defined in the +//! [`Core`](crate::core) module, we use the following terminology: +//! + We call "chain A" the chain that runs as the controller chain for the +//! interchain account application +//! + We call "chain B" the chain that runs as the host chain for the interchain +//! account application +//! In variable names: +//! + `_a` implies "belongs to chain A" +//! + `on_a` implies "stored on chain A" + +pub mod account; +pub mod context; +pub mod controller; +pub mod error; +pub mod events; +pub mod host; +pub mod metadata; +pub mod packet; +pub mod port; + +extern crate alloc; + +/// Module identifier for the ICS27 application. +pub const MODULE_ID_STR: &str = "interchainaccounts"; + +/// ICS27 application current version. +pub const VERSION: &str = "ics27-1"; + +/// The successful string used for creating an acknowledgement status, +/// equivalent to `base64::encode(0x01)`. +pub const ACK_SUCCESS: &str = "AQ=="; //TODO: what's the result string? + +use ibc_core::channel::types::acknowledgement::StatusValue; + +/// Returns a successful acknowledgement status for the interchain accounts application. +pub fn ack_success() -> StatusValue { + StatusValue::new(ACK_SUCCESS).expect("ack status value is never supposed to be empty") +} diff --git a/ibc-apps/ics27-interchain-accounts/src/metadata.rs b/ibc-apps/ics27-interchain-accounts/src/metadata.rs new file mode 100644 index 000000000..61552e71a --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/metadata.rs @@ -0,0 +1,243 @@ +use alloc::string::{String, ToString}; +use core::str::FromStr; + +use ibc_core::channel::types::Version; +use ibc_core::host::types::identifiers::ConnectionId; +use ibc_core::primitives::proto::Protobuf; +use ibc_core::primitives::Signer; +use ibc_proto::ibc::apps::interchain_accounts::v1::Metadata as RawMetadata; +use serde::{Deserialize, Serialize}; + +use super::context::InterchainAccountValidationContext; +use super::error::InterchainAccountError; +use super::VERSION; + +/// Defines a set of protocol specific data encoded into the ICS27 channel version bytestring +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Metadata { + /// Defines the ICS27 protocol version + pub version: Version, + /// Defines the connection identifier associated with the controller chain + pub conn_id_on_a: ConnectionId, + /// Defines the connection identifier associated with the host chain + pub conn_id_on_b: ConnectionId, + /// Defines the interchain account address to be fulfilled upon the OnChanOpenTry handshake step + /// NOTE: the address field is empty on the `OnChanOpenInit` handshake step + pub address: Signer, + /// Defines the supported codec format + pub encoding: SupportedEncoding, + /// Defines the type of transactions the interchain account can execute + pub tx_type: SupportedTxType, +} + +impl Metadata { + /// Constructs a new Metadata instance + pub fn new( + version: Version, + conn_id_on_a: ConnectionId, + conn_id_on_b: ConnectionId, + address: Signer, + encoding: SupportedEncoding, + tx_type: SupportedTxType, + ) -> Self { + Metadata { + version, + conn_id_on_a, + conn_id_on_b, + address, + encoding, + tx_type, + } + } + + /// Constructs a new Metadata instance with default values + pub fn new_default(conn_id_on_a: ConnectionId, conn_id_on_b: ConnectionId) -> Self { + Self::new( + Version::from(VERSION.to_string()), + conn_id_on_a, + conn_id_on_b, + Signer::new_empty(), + SupportedEncoding::Proto3, + SupportedTxType::SDKMultiMsg, + ) + } + + /// Validate the metadata using the provided validation context and connection hops + pub fn validate( + &self, + ctx: &impl InterchainAccountValidationContext, + connection_hops: &[ConnectionId], + ) -> Result<(), InterchainAccountError> { + let expected_metadata_version = Version::from(VERSION.to_string()); + + if self.version != expected_metadata_version { + return Err( + InterchainAccountError::mismatch("channel version mismatch.") + .given(&self.version) + .expected(&expected_metadata_version), + ); + } + + if self.encoding != SupportedEncoding::Proto3 { + return Err(InterchainAccountError::not_supported("encoding type")); + } + + if self.tx_type != SupportedTxType::SDKMultiMsg { + return Err(InterchainAccountError::not_supported("tx type")); + } + + if self.conn_id_on_a != connection_hops[0] { + return Err(InterchainAccountError::mismatch("connection id mismatch.") + .given(&self.conn_id_on_a) + .expected(&connection_hops[0])); + } + + let conn_end_on_a = ctx.connection_end(&connection_hops[0])?; + + let conn_id_on_b = conn_end_on_a.counterparty().clone().connection_id.ok_or( + InterchainAccountError::not_found("connection id on counterparty"), + )?; + + if self.conn_id_on_b != conn_id_on_b { + return Err(InterchainAccountError::mismatch("connection id mismatch.") + .given(&self.conn_id_on_b) + .expected(&conn_id_on_b)); + } + + ctx.validate_message_signer(&self.address)?; + + Ok(()) + } + + // Compares a metadata to a previous version string set in a channel struct. + // It ensures all fields are equal except the Address string + pub fn verify_prev_metadata_matches( + &self, + previous_version: &Version, + ) -> Result<(), InterchainAccountError> { + let previous_metadata = serde_json::from_str::(previous_version.as_str()) + .map_err(InterchainAccountError::source)?; + + if self.version != previous_metadata.version { + return Err(InterchainAccountError::mismatch("channel version") + .given(&previous_metadata.version) + .expected(&self.version)); + } + + if self.encoding != previous_metadata.encoding { + return Err(InterchainAccountError::mismatch("encoding type") + .given(&previous_metadata.encoding) + .expected(&self.encoding)); + } + + if self.tx_type != previous_metadata.tx_type { + return Err(InterchainAccountError::mismatch("tx type") + .given(&previous_metadata.tx_type) + .expected(&self.tx_type)); + } + + if self.conn_id_on_a != previous_metadata.conn_id_on_a { + return Err( + InterchainAccountError::mismatch("connection id on the controller chain") + .given(&previous_metadata.conn_id_on_a) + .expected(&self.conn_id_on_a), + ); + } + + if self.conn_id_on_b != previous_metadata.conn_id_on_b { + return Err( + InterchainAccountError::mismatch("connection id on the host chain") + .given(&previous_metadata.conn_id_on_b) + .expected(&self.conn_id_on_b), + ); + } + + Ok(()) + } +} + +impl Protobuf for Metadata {} + +impl From for RawMetadata { + fn from(domain: Metadata) -> Self { + RawMetadata { + version: domain.version.to_string(), + controller_connection_id: domain.conn_id_on_a.to_string(), + host_connection_id: domain.conn_id_on_b.to_string(), + address: domain.address.to_string(), + encoding: domain.encoding.to_string(), + tx_type: domain.tx_type.to_string(), + } + } +} + +impl TryFrom for Metadata { + type Error = InterchainAccountError; + + fn try_from(raw: RawMetadata) -> Result { + Ok(Metadata { + version: raw.version.parse().unwrap(), + conn_id_on_a: raw + .controller_connection_id + .parse() + .map_err(InterchainAccountError::source)?, + conn_id_on_b: raw + .host_connection_id + .parse() + .map_err(InterchainAccountError::source)?, + address: Signer::new(raw.address), + encoding: raw.encoding.parse()?, + tx_type: raw.tx_type.parse()?, + }) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum SupportedEncoding { + Proto3, +} + +impl FromStr for SupportedEncoding { + type Err = InterchainAccountError; + + fn from_str(s: &str) -> Result { + match s { + "proto3" => Ok(SupportedEncoding::Proto3), + _ => Err(InterchainAccountError::not_supported( + "supported encoding type", + )), + } + } +} + +impl ToString for SupportedEncoding { + fn to_string(&self) -> String { + match self { + SupportedEncoding::Proto3 => "proto3".to_string(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum SupportedTxType { + SDKMultiMsg, +} + +impl FromStr for SupportedTxType { + type Err = InterchainAccountError; + + fn from_str(s: &str) -> Result { + match s { + "sdk_multi_msg" => Ok(SupportedTxType::SDKMultiMsg), + _ => Err(InterchainAccountError::not_supported("supported tx type")), + } + } +} + +impl ToString for SupportedTxType { + fn to_string(&self) -> String { + match self { + SupportedTxType::SDKMultiMsg => "sdk_multi_msg".to_string(), + } + } +} diff --git a/ibc-apps/ics27-interchain-accounts/src/packet.rs b/ibc-apps/ics27-interchain-accounts/src/packet.rs new file mode 100644 index 000000000..b1f96dd62 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/packet.rs @@ -0,0 +1,86 @@ +use alloc::string::String; +use alloc::vec::Vec; + +use ibc_core::primitives::proto::Protobuf; +pub use ibc_proto::ibc::apps::interchain_accounts::v1::InterchainAccountPacketData as RawInterchainAccountPacketData; + +use crate::error::InterchainAccountError; + +const MAX_MEMO_CHAR_LENGTH: usize = 256; + +/// Defines the domain type for the interchain account packet data. +#[derive(Clone, Debug)] +pub struct InterchainAccountPacketData { + /// The type of the packet data. + pub packet_type: ICAPacketType, + /// The data to be sent to the interchain accounts host chain. + pub data: Vec, + /// The memo to be included in the transaction. + pub memo: String, +} + +impl TryFrom> for InterchainAccountPacketData { + type Error = InterchainAccountError; + + fn try_from(data: Vec) -> Result { + if data.is_empty() { + return Err(InterchainAccountError::empty("packet data")); + } + Ok(Self { + packet_type: ICAPacketType::ExecuteTx, + data, + memo: String::new(), + }) + } +} + +impl Protobuf for InterchainAccountPacketData {} + +impl TryFrom for InterchainAccountPacketData { + type Error = InterchainAccountError; + + fn try_from(raw: RawInterchainAccountPacketData) -> Result { + let packet_type = match raw.r#type { + 0 => ICAPacketType::ExecuteTx, + _ => { + return Err(InterchainAccountError::invalid( + "packet data type must be of type ExecuteTx", + )) + } + }; + + if raw.data.is_empty() { + return Err(InterchainAccountError::empty("packet data")); + } + + if raw.memo.len() > MAX_MEMO_CHAR_LENGTH { + return Err(InterchainAccountError::invalid( + "packet memo cannot be greater than 256 characters", + )); + } + + Ok(InterchainAccountPacketData { + packet_type, + data: raw.data, + memo: raw.memo, + }) + } +} + +impl From for RawInterchainAccountPacketData { + fn from(domain: InterchainAccountPacketData) -> Self { + RawInterchainAccountPacketData { + r#type: domain.packet_type as i32, + data: domain.data, + memo: domain.memo, + } + } +} + +/// Defines a classification of message issued from a controller chain to its +/// associated interchain accounts host +#[derive(Clone, Debug)] +pub enum ICAPacketType { + /// Execute a transaction on an interchain accounts host chain + ExecuteTx = 0, +} diff --git a/ibc-apps/ics27-interchain-accounts/src/port.rs b/ibc-apps/ics27-interchain-accounts/src/port.rs new file mode 100644 index 000000000..98c63db67 --- /dev/null +++ b/ibc-apps/ics27-interchain-accounts/src/port.rs @@ -0,0 +1,39 @@ +use alloc::format; +use core::str::FromStr; + +use ibc_core::host::types::identifiers::PortId; +use ibc_core::primitives::Signer; + +use crate::error::InterchainAccountError; + +/// The default prefix for the controller port identifiers. +pub const CONTROLLER_PORT_PREFIX: &str = "interchain-account"; + +/// The default port identifier that the host chain typically bind with. +pub const HOST_PORT_ID: &str = "icahost"; + +/// Returns a new prefixed controller port identifier using the given owner string +pub fn new_controller_port_id(owner: &Signer) -> Result { + if owner.as_ref().is_empty() { + return Err(InterchainAccountError::empty("controller owner address")); + } + + let port_id_str = format!("{}-{}", CONTROLLER_PORT_PREFIX, owner.as_ref()); + + PortId::from_str(&port_id_str).map_err(InterchainAccountError::source) +} + +pub fn verify_controller_port_id_prefix(port_id: &PortId) -> Result<(), InterchainAccountError> { + if !port_id.as_str().starts_with(CONTROLLER_PORT_PREFIX) { + return Err(InterchainAccountError::invalid("controller port id prefix") + .expected(&format!("{CONTROLLER_PORT_PREFIX} as prefix")) + .given(port_id)); + } + + Ok(()) +} + +/// Returns an instance of the default host port identifier. +pub fn default_host_port_id() -> Result { + PortId::from_str(HOST_PORT_ID).map_err(InterchainAccountError::source) +} diff --git a/ibc-core/ics04-channel/types/src/channel.rs b/ibc-core/ics04-channel/types/src/channel.rs index d551c441f..d7d0319fd 100644 --- a/ibc-core/ics04-channel/types/src/channel.rs +++ b/ibc-core/ics04-channel/types/src/channel.rs @@ -228,6 +228,10 @@ impl ChannelEnd { self.state == State::Open } + pub fn is_closed(&self) -> bool { + self.state == State::Closed + } + pub fn state(&self) -> &State { &self.state } @@ -279,7 +283,7 @@ impl ChannelEnd { /// Checks if the state of this channel end is not closed. pub fn verify_not_closed(&self) -> Result<(), ChannelError> { - if self.state.eq(&State::Closed) { + if self.is_closed() { return Err(ChannelError::InvalidState { expected: "Channel state cannot be Closed".to_string(), actual: self.state.to_string(), diff --git a/ibc-core/ics04-channel/types/src/msgs/chan_open_init.rs b/ibc-core/ics04-channel/types/src/msgs/chan_open_init.rs index 7ab3f1598..00344ac79 100644 --- a/ibc-core/ics04-channel/types/src/msgs/chan_open_init.rs +++ b/ibc-core/ics04-channel/types/src/msgs/chan_open_init.rs @@ -31,6 +31,24 @@ pub struct MsgChannelOpenInit { } impl MsgChannelOpenInit { + pub fn new( + port_id_on_a: PortId, + connection_hops_on_a: Vec, + port_id_on_b: PortId, + ordering: Order, + signer: Signer, + version_proposal: Version, + ) -> Self { + Self { + port_id_on_a, + connection_hops_on_a, + port_id_on_b, + ordering, + signer, + version_proposal, + } + } + /// Checks if the `connection_hops` has a length of `expected`. /// /// Note: Current IBC version only supports one connection hop. diff --git a/ibc-core/ics04-channel/types/src/version.rs b/ibc-core/ics04-channel/types/src/version.rs index 2cfc1e46c..6d42b24df 100644 --- a/ibc-core/ics04-channel/types/src/version.rs +++ b/ibc-core/ics04-channel/types/src/version.rs @@ -42,7 +42,7 @@ impl Version { } pub fn is_empty(&self) -> bool { - self.0.is_empty() + self.0.trim().is_empty() } pub fn as_str(&self) -> &str { diff --git a/ibc-core/ics25-handler/types/src/msgs.rs b/ibc-core/ics25-handler/types/src/msgs.rs index 1272c0aff..1699b3b89 100644 --- a/ibc-core/ics25-handler/types/src/msgs.rs +++ b/ibc-core/ics25-handler/types/src/msgs.rs @@ -6,6 +6,7 @@ use ibc_core_channel_types::msgs::{ CHAN_OPEN_INIT_TYPE_URL, CHAN_OPEN_TRY_TYPE_URL, RECV_PACKET_TYPE_URL, TIMEOUT_ON_CLOSE_TYPE_URL, TIMEOUT_TYPE_URL, }; +use ibc_core_client_types::msgs::RECOVER_CLIENT_TYPE_URL; #[allow(deprecated)] use ibc_core_client_types::msgs::{ ClientMsg, MsgCreateClient, MsgSubmitMisbehaviour, MsgUpdateClient, MsgUpgradeClient, @@ -19,6 +20,7 @@ use ibc_core_connection_types::msgs::{ }; use ibc_core_router_types::error::RouterError; use ibc_primitives::prelude::*; +use ibc_primitives::Signer; use ibc_proto::google::protobuf::Any; use ibc_proto::Protobuf; @@ -36,6 +38,73 @@ pub enum MsgEnvelope { Packet(PacketMsg), } +impl MsgEnvelope { + pub fn signer(&self) -> Signer { + match self.clone() { + MsgEnvelope::Client(msg) => match msg { + ClientMsg::CreateClient(msg) => msg.signer, + ClientMsg::UpdateClient(msg) => msg.signer, + ClientMsg::UpgradeClient(msg) => msg.signer, + ClientMsg::RecoverClient(msg) => msg.signer, + #[allow(deprecated)] + ClientMsg::Misbehaviour(msg) => msg.signer, + }, + MsgEnvelope::Connection(msg) => match msg { + ConnectionMsg::OpenInit(msg) => msg.signer, + ConnectionMsg::OpenTry(msg) => msg.signer, + ConnectionMsg::OpenAck(msg) => msg.signer, + ConnectionMsg::OpenConfirm(msg) => msg.signer, + }, + MsgEnvelope::Channel(msg) => match msg { + ChannelMsg::OpenInit(msg) => msg.signer, + ChannelMsg::OpenTry(msg) => msg.signer, + ChannelMsg::OpenAck(msg) => msg.signer, + ChannelMsg::OpenConfirm(msg) => msg.signer, + ChannelMsg::CloseInit(msg) => msg.signer, + ChannelMsg::CloseConfirm(msg) => msg.signer, + }, + MsgEnvelope::Packet(msg) => match msg { + PacketMsg::Recv(msg) => msg.signer, + PacketMsg::Ack(msg) => msg.signer, + PacketMsg::Timeout(msg) => msg.signer, + PacketMsg::TimeoutOnClose(msg) => msg.signer, + }, + } + } + + pub fn type_url(&self) -> String { + match self { + MsgEnvelope::Client(msg) => match msg { + ClientMsg::CreateClient(_msg) => CREATE_CLIENT_TYPE_URL.into(), + ClientMsg::UpdateClient(_msg) => UPDATE_CLIENT_TYPE_URL.into(), + ClientMsg::UpgradeClient(_msg) => UPGRADE_CLIENT_TYPE_URL.into(), + ClientMsg::Misbehaviour(_msg) => SUBMIT_MISBEHAVIOUR_TYPE_URL.into(), + ClientMsg::RecoverClient(_msg) => RECOVER_CLIENT_TYPE_URL.into(), + }, + MsgEnvelope::Connection(msg) => match msg { + ConnectionMsg::OpenInit(_msg) => CONN_OPEN_INIT_TYPE_URL.into(), + ConnectionMsg::OpenTry(_msg) => CONN_OPEN_TRY_TYPE_URL.into(), + ConnectionMsg::OpenAck(_msg) => CONN_OPEN_ACK_TYPE_URL.into(), + ConnectionMsg::OpenConfirm(_msg) => CONN_OPEN_CONFIRM_TYPE_URL.into(), + }, + MsgEnvelope::Channel(msg) => match msg { + ChannelMsg::OpenInit(_msg) => CHAN_OPEN_INIT_TYPE_URL.into(), + ChannelMsg::OpenTry(_msg) => CHAN_OPEN_TRY_TYPE_URL.into(), + ChannelMsg::OpenAck(_msg) => CHAN_OPEN_ACK_TYPE_URL.into(), + ChannelMsg::OpenConfirm(_msg) => CHAN_OPEN_CONFIRM_TYPE_URL.into(), + ChannelMsg::CloseInit(_msg) => CHAN_CLOSE_INIT_TYPE_URL.into(), + ChannelMsg::CloseConfirm(_msg) => CHAN_CLOSE_CONFIRM_TYPE_URL.into(), + }, + MsgEnvelope::Packet(msg) => match msg { + PacketMsg::Recv(_msg) => RECV_PACKET_TYPE_URL.into(), + PacketMsg::Ack(_msg) => ACKNOWLEDGEMENT_TYPE_URL.into(), + PacketMsg::Timeout(_msg) => TIMEOUT_TYPE_URL.into(), + PacketMsg::TimeoutOnClose(_msg) => TIMEOUT_ON_CLOSE_TYPE_URL.into(), + }, + } + } +} + #[allow(deprecated)] impl TryFrom for MsgEnvelope { type Error = RouterError; diff --git a/ibc-primitives/Cargo.toml b/ibc-primitives/Cargo.toml index 0afd83407..94547e4aa 100644 --- a/ibc-primitives/Cargo.toml +++ b/ibc-primitives/Cargo.toml @@ -27,6 +27,7 @@ prost = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, optional = true } time = { version = ">=0.3.0, <0.3.37", default-features = false } +cosmrs = { workspace = true } # ibc dependencies ibc-proto = { workspace = true } diff --git a/ibc-primitives/src/types/signer.rs b/ibc-primitives/src/types/signer.rs index a00cd498e..35cefb734 100644 --- a/ibc-primitives/src/types/signer.rs +++ b/ibc-primitives/src/types/signer.rs @@ -1,3 +1,4 @@ +use cosmrs::AccountId; use derive_more::Display; use crate::prelude::*; @@ -20,6 +21,20 @@ use crate::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display)] pub struct Signer(String); +impl Signer { + pub fn new(s: impl Into) -> Self { + Self(s.into()) + } + + pub fn new_empty() -> Self { + Self::new(String::new()) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + impl From for Signer { fn from(s: String) -> Self { Self(s) @@ -31,3 +46,9 @@ impl AsRef for Signer { self.0.as_str() } } + +impl From for Signer { + fn from(account_id: AccountId) -> Self { + Self(account_id.to_string()) + } +}