diff --git a/Cargo.toml b/Cargo.toml index 69fdeea..fb24c7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,12 +22,14 @@ thiserror = { version = "1.0.64" } schemars = "0.8" cw-asset = { version = "4.0.0" } -abstract-app = { version = "0.24.1-beta.2" } -abstract-adapter = { version = "0.24.1-beta.2" } -abstract-standalone = { version = "0.24.1-beta.2" } -abstract-interface = { version = "0.24.1-beta.2" } -abstract-client = { version = "0.24.1-beta.2" } -cw-orch = { version = "0.25.1" } +abstract-app = { version = "0.24.1" } +abstract-adapter = { version = "0.24.1" } +abstract-standalone = { version = "0.24.1" } +abstract-client = { version = "0.24.1" } +cw-orch = { version = "0.27.0" } +{% if include_ibc_app %} +cw-orch-interchain = { version = "0.8.0" } +{% endif %} lazy_static = "1.4.0" const_format = "0.2.33" @@ -45,3 +47,8 @@ clap = { version = "4.5.19" } {% if include_standalone %} {{standalone_name | kebab_case}} = { path = "contracts/{{standalone_name}}" } {% endif %} + +[patch.crates-io] +abstract-client = { git = "https://github.com/AbstractSDK/abstract.git", branch = "buckram/fix-abstract-client-fetch_or_build_account" } +abstract-std = { git = "https://github.com/AbstractSDK/abstract.git", branch = "buckram/fix-abstract-client-fetch_or_build_account" } +abstract-interface = { git = "https://github.com/AbstractSDK/abstract.git", branch = "buckram/fix-abstract-client-fetch_or_build_account" } diff --git a/cargo-generate.toml b/cargo-generate.toml index d85149a..d549a89 100644 --- a/cargo-generate.toml +++ b/cargo-generate.toml @@ -9,10 +9,9 @@ post = ["post-script.rhai"] include_app = { prompt = "Include App Module", default = true, type = "bool" } include_adapter = { prompt = "Include Adapter Module", default = true, type = "bool" } include_standalone = { prompt = "Include Standalone Module", default = true, type = "bool" } +include_ibc_app = { prompt = "Include IBC App Module", default = true, type = "bool" } [conditional.'include_app'.placeholders] -# App can have IBC -with_ibc = { prompt = "Would you like to include IBC template in the app?", default = false, type = "bool" } app_name = { prompt = "Enter App Name", default = "my_app", type = "string" } [conditional.'include_adapter'.placeholders] @@ -21,6 +20,9 @@ adapter_name = { prompt = "Enter Adapter Name", default = "my_adapter", type = " [conditional.'include_standalone'.placeholders] standalone_name = { prompt = "Enter Standalone Name", default = "my_standalone", type = "string" } +[conditional.'include_ibc_app'.placeholders] +ibc_app_name = { prompt = "Enter IBC App Name", default = "my_ibc_app", type = "string" } + # Ignore files if not included [conditional.'!include_app'] ignore = ["contracts/{{app_name}}"] @@ -28,6 +30,5 @@ ignore = ["contracts/{{app_name}}"] ignore = ["contracts/{{adapter_name}}"] [conditional.'!include_standalone'] ignore = ["contracts/{{standalone_name}}"] - -[conditional.'!with_ibc'] -ignore = ["contracts/{{app_name}}/src/ibc"] +[conditional.'!include_ibc_app'] +ignore = ["contracts/{{ibc_app_name}}"] diff --git a/contracts/{{adapter_name}}/Cargo.toml b/contracts/{{adapter_name}}/Cargo.toml index cc336f2..6379def 100644 --- a/contracts/{{adapter_name}}/Cargo.toml +++ b/contracts/{{adapter_name}}/Cargo.toml @@ -48,7 +48,6 @@ const_format = { workspace = true } # Dependencies for interface cw-orch = { workspace = true } -abstract-interface = { workspace = true } # Dependencies for bins clap = { workspace = true, optional = true, features = ["derive"] } diff --git a/contracts/{{adapter_name}}/src/bin/publish.rs b/contracts/{{adapter_name}}/src/bin/publish.rs index a83351a..22aa14d 100644 --- a/contracts/{{adapter_name}}/src/bin/publish.rs +++ b/contracts/{{adapter_name}}/src/bin/publish.rs @@ -32,8 +32,10 @@ fn publish(networks: Vec) -> anyhow::Result<()> { // Get the [`Publisher`] that owns the namespace, otherwise create a new one and claim the namespace let publisher: Publisher<_> = abstract_client - .publisher_builder(adapter_namespace) - .build()?; + .account_builder() + .namespace(adapter_namespace) + .build()? + .publisher()?; if publisher.account().owner()? != chain.sender_addr() { panic!("The current sender can not publish to this namespace. Please use the wallet that owns the Account that owns the Namespace.") diff --git a/contracts/{{adapter_name}}/tests/integration.rs b/contracts/{{adapter_name}}/tests/integration.rs index ea926cd..061299b 100644 --- a/contracts/{{adapter_name}}/tests/integration.rs +++ b/contracts/{{adapter_name}}/tests/integration.rs @@ -31,7 +31,11 @@ impl TestEnv { abs_client.set_balance(&sender, &coins(123, "ucosm"))?; // Publish the adapter - let publisher = abs_client.publisher_builder(namespace).build()?; + let publisher = abs_client + .account_builder() + .namespace(namespace) + .build()? + .publisher()?; publisher.publish_adapter::<{{adapter_name | upper_camel_case}}InstantiateMsg, {{adapter_name | upper_camel_case}}Interface<_>>( {{adapter_name | upper_camel_case}}InstantiateMsg {}, )?; @@ -67,8 +71,8 @@ fn update_config() -> anyhow::Result<()> { // Note that it's not a requirement to have it installed in this case let publisher_account = env .abs - .publisher_builder(Namespace::new({{project-name | shouty_snake_case}}_NAMESPACE).unwrap()) - .build()?; + .fetch_account(Namespace::new({{project-name | shouty_snake_case}}_NAMESPACE).unwrap())? + .publisher()?; adapter.execute( &AdapterRequestMsg { @@ -109,7 +113,7 @@ fn set_status() -> anyhow::Result<()> { let new_account = env .abs .account_builder() - .install_adapter::<{{adapter_name | upper_camel_case}}Interface>()? + .install_adapter::<{{adapter_name | upper_camel_case}}Interface>() .build()?; new_account.as_ref().execute_on_module( diff --git a/contracts/{{app_name}}/Cargo.toml b/contracts/{{app_name}}/Cargo.toml index bc3c774..106f4c4 100644 --- a/contracts/{{app_name}}/Cargo.toml +++ b/contracts/{{app_name}}/Cargo.toml @@ -48,7 +48,6 @@ const_format = { workspace = true } # Dependencies for interface cw-orch = { workspace = true } -abstract-interface = { workspace = true } # Dependencies for bins clap = { workspace = true, optional = true, features = ["derive"] } diff --git a/contracts/{{app_name}}/src/bin/publish.rs b/contracts/{{app_name}}/src/bin/publish.rs index 2c8f361..f6b77cf 100644 --- a/contracts/{{app_name}}/src/bin/publish.rs +++ b/contracts/{{app_name}}/src/bin/publish.rs @@ -30,7 +30,11 @@ fn publish(networks: Vec) -> anyhow::Result<()> { let abstract_client: AbstractClient = AbstractClient::new(chain.clone())?; // Get the [`Publisher`] that owns the namespace, otherwise create a new one and claim the namespace - let publisher: Publisher<_> = abstract_client.publisher_builder(app_namespace).build()?; + let publisher: Publisher<_> = abstract_client + .account_builder() + .namespace(app_namespace) + .build()? + .publisher()?; if publisher.account().owner()? != chain.sender_addr() { panic!("The current sender can not publish to this namespace. Please use the wallet that owns the Account that owns the Namespace.") diff --git a/contracts/{{app_name}}/src/contract.rs b/contracts/{{app_name}}/src/contract.rs index d552ef1..88c825f 100644 --- a/contracts/{{app_name}}/src/contract.rs +++ b/contracts/{{app_name}}/src/contract.rs @@ -23,9 +23,7 @@ const APP: {{app_name | upper_camel_case}} = {{app_name | upper_camel_case}}::ne .with_execute(handlers::execute_handler) .with_query(handlers::query_handler) .with_migrate(handlers::migrate_handler) - .with_replies(&[(INSTANTIATE_REPLY_ID, replies::instantiate_reply)]){% if with_ibc %} - .with_ibc_callback(crate::ibc::ibc_callback) - .with_module_ibc(crate::ibc::receive_module_ibc){% endif %} + .with_replies(&[(INSTANTIATE_REPLY_ID, replies::instantiate_reply)]) .with_dependencies(&[]); // Export handlers @@ -37,7 +35,7 @@ abstract_app::cw_orch_interface!(APP, {{app_name | upper_camel_case}}, {{app_nam // TODO: add to docmuentation // https://linear.app/abstract-sdk/issue/ABS-414/add-documentation-on-dependencycreation-trait #[cfg(not(target_arch = "wasm32"))] -impl abstract_interface::DependencyCreation +impl abstract_app::abstract_interface::DependencyCreation for crate::{{app_name | upper_camel_case}}Interface { type DependenciesConfig = cosmwasm_std::Empty; diff --git a/contracts/{{app_name}}/src/ibc/callback.rs b/contracts/{{app_name}}/src/ibc/callback.rs deleted file mode 100644 index c2fd002..0000000 --- a/contracts/{{app_name}}/src/ibc/callback.rs +++ /dev/null @@ -1,19 +0,0 @@ -use abstract_app::{ - sdk::AbstractResponse, - std::ibc::{Callback, IbcResult}, -}; -use cosmwasm_std::{DepsMut, Env}; - -use crate::contract::{ - {{app_name | upper_camel_case}}, {{app_name | upper_camel_case}}Result -}; - -pub fn ibc_callback( - _deps: DepsMut, - _env: Env, - module: {{app_name | upper_camel_case}}, - _callback: Callback, - _result: IbcResult, -) -> {{app_name | upper_camel_case}}Result { - Ok(module.response("callback")) -} \ No newline at end of file diff --git a/contracts/{{app_name}}/src/ibc/module.rs b/contracts/{{app_name}}/src/ibc/module.rs deleted file mode 100644 index 7817711..0000000 --- a/contracts/{{app_name}}/src/ibc/module.rs +++ /dev/null @@ -1,21 +0,0 @@ -use abstract_app::std::ibc::ModuleIbcInfo; -use cosmwasm_std::{Binary, DepsMut, Env, Response, from_json}; - -use crate::{ - contract::{ - {{app_name | upper_camel_case}}, {{app_name | upper_camel_case}}Result - }, - msg::IbcMsg, -}; - -pub fn receive_module_ibc( - _deps: DepsMut, - _env: Env, - _module: {{app_name | upper_camel_case}}, - _source_module: ModuleIbcInfo, - msg: Binary, -) -> {{app_name | upper_camel_case}}Result { - let _msg: IbcMsg = from_json(&msg)?; - // do something with received _msg - Ok(Response::new()) -} diff --git a/contracts/{{app_name}}/src/lib.rs b/contracts/{{app_name}}/src/lib.rs index 54edede..70e15d2 100644 --- a/contracts/{{app_name}}/src/lib.rs +++ b/contracts/{{app_name}}/src/lib.rs @@ -5,8 +5,6 @@ pub mod msg; mod replies; pub mod state; -{% if with_ibc %}pub mod ibc;{% endif %} - pub use error::{{app_name | upper_camel_case}}Error; /// The version of your app diff --git a/contracts/{{app_name}}/src/msg.rs b/contracts/{{app_name}}/src/msg.rs index 804f569..9d3d756 100644 --- a/contracts/{{app_name}}/src/msg.rs +++ b/contracts/{{app_name}}/src/msg.rs @@ -45,14 +45,3 @@ pub struct ConfigResponse {} pub struct CountResponse { pub count: i32, } - -{% if with_ibc %} -#[cosmwasm_schema::cw_serde] -pub enum IbcCallbackMsg { - Empty {}, -} - -#[cosmwasm_schema::cw_serde] -pub struct IbcMsg { -} -{% endif %} \ No newline at end of file diff --git a/contracts/{{app_name}}/tests/integration.rs b/contracts/{{app_name}}/tests/integration.rs index 22ec346..e25a77b 100644 --- a/contracts/{{app_name}}/tests/integration.rs +++ b/contracts/{{app_name}}/tests/integration.rs @@ -32,7 +32,11 @@ impl TestEnv { abs_client.set_balance(&sender, &coins(123, "ucosm"))?; // Publish the app - let publisher = abs_client.publisher_builder(namespace).build()?; + let publisher = abs_client + .account_builder() + .namespace(namespace) + .build()? + .publisher()?; publisher.publish_app::<{{app_name | upper_camel_case}}Interface<_>>()?; let app = publisher diff --git a/contracts/{{ibc_app_name}}/Cargo.toml b/contracts/{{ibc_app_name}}/Cargo.toml new file mode 100644 index 0000000..182be37 --- /dev/null +++ b/contracts/{{ibc_app_name}}/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "ping-pong" +version = "0.1.0" +edition = "2021" + +[features] +default = ["export"] +export = [] +daemon-bin = [ + "cw-orch/daemon", + "dep:clap", + "dep:abstract-client", + "dep:dotenv", + "dep:env_logger", +] +schema = ["abstract-app/schema"] + +[lib] +crate-type = ["cdylib", "rlib"] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-controllers = { workspace = true } +cw-storage-plus = { workspace = true } +thiserror = { workspace = true } +schemars = { workspace = true } +cw-asset = { workspace = true } +abstract-app = { workspace = true } +abstract-ibc-client = { version = "0.24.1", default-features = false } +const_format = { workspace = true } + +# Dependencies for interface +cw-orch = { workspace = true } +abstract-interface = { workspace = true } + +# Dependencies for bins +clap = { workspace = true, optional = true, features = ["derive"] } +abstract-client = { workspace = true, features = [ + "interchain", +], optional = true } +dotenv = { workspace = true, optional = true } +env_logger = { workspace = true, optional = true } + +[dev-dependencies] +cw-orch-interchain = { workspace = true, features = ["daemon"] } +cw-orch = { workspace = true, features = ["daemon"] } +abstract-client = { workspace = true, features = ["interchain"] } +env_logger = "0.11" diff --git a/contracts/{{ibc_app_name}}/metadata.json b/contracts/{{ibc_app_name}}/metadata.json new file mode 100644 index 0000000..544344a --- /dev/null +++ b/contracts/{{ibc_app_name}}/metadata.json @@ -0,0 +1,9 @@ +{ + "name": "MyIbcApp", + "description": "IBC app description.", + "website": "", + "docs": "", + "type": "app", + "icon": "GiTrade", + "enabled": true +} diff --git a/contracts/{{ibc_app_name}}/src/bin/publish.rs b/contracts/{{ibc_app_name}}/src/bin/publish.rs new file mode 100644 index 0000000..b67ff50 --- /dev/null +++ b/contracts/{{ibc_app_name}}/src/bin/publish.rs @@ -0,0 +1,65 @@ +//! Publishes the module to the Abstract platform by uploading it and registering it on the app store. +//! +//! Info: The mnemonic used to register the module must be the same as the owner of the account that claimed the namespace. +//! +//! ## Example +//! +//! ```bash +//! $ just publish test-app uni-6 osmo-test-5 +//! ``` +use ping_pong::PING_PONG_ID; + +use abstract_app::objects::namespace::Namespace; +use abstract_client::{AbstractClient, Publisher}; +use clap::Parser; +use cw_orch::{anyhow, daemon::networks::parse_network, prelude::*, tokio::runtime::Runtime}; +use ping_pong::PingPongInterface; + +fn publish(networks: Vec) -> anyhow::Result<()> { + // run for each requested network + for network in networks { + // Setup + let rt = Runtime::new()?; + let chain = DaemonBuilder::new(network).handle(rt.handle()).build()?; + + let app_namespace = Namespace::from_id(PING_PONG_ID)?; + + // Create an [`AbstractClient`] + let abstract_client: AbstractClient = AbstractClient::new(chain.clone())?; + + // Get the [`Publisher`] that owns the namespace, otherwise create a new one and claim the namespace + let publisher: Publisher<_> = abstract_client + .account_builder() + .namespace(app_namespace) + .build()? + .publisher()?; + + if publisher.account().owner()? != chain.sender_addr() { + panic!("The current sender can not publish to this namespace. Please use the wallet that owns the Account that owns the Namespace.") + } + + // Publish the App to the Abstract Platform + publisher.publish_app::>()?; + } + Ok(()) +} + +#[derive(Parser, Default, Debug)] +#[command(author, version, about, long_about = None)] +struct Arguments { + /// Network Id to publish on + #[arg(short, long, value_delimiter = ' ', num_args = 1..)] + network_ids: Vec, +} + +fn main() { + dotenv::dotenv().ok(); + env_logger::init(); + let args = Arguments::parse(); + let networks = args + .network_ids + .iter() + .map(|n| parse_network(n).unwrap()) + .collect(); + publish(networks).unwrap(); +} diff --git a/contracts/{{ibc_app_name}}/src/bin/schema.rs b/contracts/{{ibc_app_name}}/src/bin/schema.rs new file mode 100644 index 0000000..3ab47ac --- /dev/null +++ b/contracts/{{ibc_app_name}}/src/bin/schema.rs @@ -0,0 +1,13 @@ +use cosmwasm_schema::remove_schemas; +use ping_pong::contract::PingPong; +use std::env::current_dir; +use std::fs::create_dir_all; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + PingPong::export_schema(&out_dir); +} diff --git a/contracts/{{ibc_app_name}}/src/contract.rs b/contracts/{{ibc_app_name}}/src/contract.rs new file mode 100644 index 0000000..89baed0 --- /dev/null +++ b/contracts/{{ibc_app_name}}/src/contract.rs @@ -0,0 +1,60 @@ +use abstract_app::{objects::dependency::StaticDependency, std::IBC_CLIENT, AppContract}; +use cosmwasm_std::Response; + +use crate::{ + error::PingPongError, + handlers, ibc, + msg::{AppMigrateMsg, PingPongExecuteMsg, PingPongInstantiateMsg, PingPongQueryMsg}, + APP_VERSION, PING_PONG_ID, +}; + +/// The type of the result returned by your app's entry points. +pub type PingPongResult = Result; + +/// The type of the app that is used to build your app and access the Abstract SDK features. +pub type PingPong = AppContract< + PingPongError, + PingPongInstantiateMsg, + PingPongExecuteMsg, + PingPongQueryMsg, + AppMigrateMsg, +>; + +const APP: PingPong = PingPong::new(PING_PONG_ID, APP_VERSION, None) + .with_instantiate(handlers::instantiate_handler) + .with_execute(handlers::execute_handler) + .with_query(handlers::query_handler) + .with_dependencies(&[StaticDependency::new( + IBC_CLIENT, + &[abstract_ibc_client::contract::CONTRACT_VERSION], + )]) + .with_module_ibc(ibc::receive_module_ibc) + .with_ibc_callback(ibc::ibc_callback); + +// Export handlers +#[cfg(feature = "export")] +abstract_app::export_endpoints!(APP, PingPong); + +abstract_app::cw_orch_interface!(APP, PingPong, PingPongInterface); + +#[cfg(not(target_arch = "wasm32"))] +use abstract_app::std::account::ModuleInstallConfig; +#[cfg(not(target_arch = "wasm32"))] +impl abstract_app::abstract_interface::DependencyCreation + for crate::PingPongInterface +{ + type DependenciesConfig = cosmwasm_std::Empty; + + fn dependency_install_configs( + _configuration: Self::DependenciesConfig, + ) -> Result, abstract_app::abstract_interface::AbstractInterfaceError> + { + Ok(vec![ModuleInstallConfig::new( + abstract_app::objects::module::ModuleInfo::from_id( + IBC_CLIENT, + abstract_ibc_client::contract::CONTRACT_VERSION.into(), + )?, + None, + )]) + } +} diff --git a/contracts/{{ibc_app_name}}/src/error.rs b/contracts/{{ibc_app_name}}/src/error.rs new file mode 100644 index 0000000..e5b069c --- /dev/null +++ b/contracts/{{ibc_app_name}}/src/error.rs @@ -0,0 +1,32 @@ +use abstract_app::{ + objects::module::ModuleInfo, sdk::AbstractSdkError, std::AbstractError, + AppError as AbstractAppError, +}; +use cosmwasm_std::StdError; +use cw_asset::AssetError; +use cw_controllers::AdminError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum PingPongError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Abstract(#[from] AbstractError), + + #[error(transparent)] + AbstractSdk(#[from] AbstractSdkError), + + #[error(transparent)] + Asset(#[from] AssetError), + + #[error(transparent)] + Admin(#[from] AdminError), + + #[error(transparent)] + DappError(#[from] AbstractAppError), + + #[error("Caller module is not a PingPong: {source_module}")] + UnauthorizedIbc { source_module: ModuleInfo }, +} diff --git a/contracts/{{ibc_app_name}}/src/handlers/execute.rs b/contracts/{{ibc_app_name}}/src/handlers/execute.rs new file mode 100644 index 0000000..e84309d --- /dev/null +++ b/contracts/{{ibc_app_name}}/src/handlers/execute.rs @@ -0,0 +1,49 @@ +use abstract_app::{ + objects::TruncatedChainId, + sdk::{IbcClient, IbcInterface}, + std::ibc::Callback, + traits::AbstractResponse, +}; +use cosmwasm_std::{CosmosMsg, DepsMut, Env, MessageInfo}; + +use crate::{ + contract::{PingPong, PingPongResult}, + msg::{PingPongCallbackMsg, PingPongExecuteMsg, PingPongIbcMsg}, +}; + +pub fn execute_handler( + deps: DepsMut, + env: Env, + _info: MessageInfo, + module: PingPong, + msg: PingPongExecuteMsg, +) -> PingPongResult { + match msg { + PingPongExecuteMsg::Ping { opponent_chain } => { + ping_pong(deps, &env, opponent_chain, module) + } + } +} + +pub(crate) fn ping_pong( + deps: DepsMut, + env: &Env, + opponent_chain: TruncatedChainId, + module: PingPong, +) -> PingPongResult { + // # ANCHOR: ibc_client + let self_module_info = module.module_info()?; + let ibc_client: IbcClient<_> = module.ibc_client(deps.as_ref(), env); + let ibc_action: CosmosMsg = ibc_client.module_ibc_action( + opponent_chain.clone(), + self_module_info, + // Start by playing a Ping + &PingPongIbcMsg::Ping {}, + Some(Callback::new(&PingPongCallbackMsg::Pinged { + opponent_chain, + })?), + )?; + // # ANCHOR_END: ibc_client + + Ok(module.response("ping").add_message(ibc_action)) +} diff --git a/contracts/{{ibc_app_name}}/src/handlers/instantiate.rs b/contracts/{{ibc_app_name}}/src/handlers/instantiate.rs new file mode 100644 index 0000000..9bad31e --- /dev/null +++ b/contracts/{{ibc_app_name}}/src/handlers/instantiate.rs @@ -0,0 +1,16 @@ +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; + +use crate::{ + contract::{PingPong, PingPongResult}, + msg::PingPongInstantiateMsg, +}; + +pub fn instantiate_handler( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _module: PingPong, + _msg: PingPongInstantiateMsg, +) -> PingPongResult { + Ok(Response::new()) +} diff --git a/contracts/{{ibc_app_name}}/src/handlers/mod.rs b/contracts/{{ibc_app_name}}/src/handlers/mod.rs new file mode 100644 index 0000000..d27bf0a --- /dev/null +++ b/contracts/{{ibc_app_name}}/src/handlers/mod.rs @@ -0,0 +1,7 @@ +pub mod execute; +pub mod instantiate; +pub mod query; + +pub use crate::handlers::{ + execute::execute_handler, instantiate::instantiate_handler, query::query_handler, +}; diff --git a/contracts/{{ibc_app_name}}/src/handlers/query.rs b/contracts/{{ibc_app_name}}/src/handlers/query.rs new file mode 100644 index 0000000..f7e8dbf --- /dev/null +++ b/contracts/{{ibc_app_name}}/src/handlers/query.rs @@ -0,0 +1,34 @@ +use cosmwasm_std::{to_json_binary, Binary, Deps, Env, StdResult}; + +use crate::{ + contract::{PingPong, PingPongResult}, + msg::{PingPongQueryMsg, StatusResponse}, + state::{PINGS, PONGS}, +}; + +pub fn query_handler( + deps: Deps, + _env: Env, + _module: &PingPong, + msg: PingPongQueryMsg, +) -> PingPongResult { + match msg { + PingPongQueryMsg::Status {} => to_json_binary(&query_status(deps)?), + } + .map_err(Into::into) +} + +fn query_status(deps: Deps) -> StdResult { + // Sum pings + let pings = PINGS + .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .map(|result| result.map(|(_k, v)| v).unwrap_or_default()) + .sum(); + // Sum pongs + let pongs = PONGS + .range(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .map(|result| result.map(|(_k, v)| v).unwrap_or_default()) + .sum(); + + Ok(StatusResponse { pings, pongs }) +} diff --git a/contracts/{{ibc_app_name}}/src/ibc/callback.rs b/contracts/{{ibc_app_name}}/src/ibc/callback.rs new file mode 100644 index 0000000..aa4703b --- /dev/null +++ b/contracts/{{ibc_app_name}}/src/ibc/callback.rs @@ -0,0 +1,41 @@ +use abstract_app::{ + sdk::AbstractResponse, + std::{ + ibc::{Callback, IbcResult}, + ABSTRACT_EVENT_TYPE, + }, +}; +use cosmwasm_std::{from_json, DepsMut, Env}; + +use crate::{ + contract::{PingPong, PingPongResult}, + msg::PingPongCallbackMsg, + state::PINGS, +}; + +pub fn ibc_callback( + deps: DepsMut, + _env: Env, + module: PingPong, + callback: Callback, + result: IbcResult, +) -> PingPongResult { + match from_json(callback.msg)? { + PingPongCallbackMsg::Pinged { opponent_chain } => { + let exec_events = result.get_execute_events()?; + + let pong = exec_events.into_iter().any(|e| { + e.ty == ABSTRACT_EVENT_TYPE + && e.attributes + .iter() + .any(|a| a.key == "action" && a.value == "pong") + }); + if pong { + PINGS.update(deps.storage, &opponent_chain, |l| { + PingPongResult::Ok(l.unwrap_or_default() + 1) + })?; + } + Ok(module.response("pong")) + } + } +} diff --git a/contracts/{{app_name}}/src/ibc/mod.rs b/contracts/{{ibc_app_name}}/src/ibc/mod.rs similarity index 100% rename from contracts/{{app_name}}/src/ibc/mod.rs rename to contracts/{{ibc_app_name}}/src/ibc/mod.rs diff --git a/contracts/{{ibc_app_name}}/src/ibc/module.rs b/contracts/{{ibc_app_name}}/src/ibc/module.rs new file mode 100644 index 0000000..6c74358 --- /dev/null +++ b/contracts/{{ibc_app_name}}/src/ibc/module.rs @@ -0,0 +1,34 @@ +use abstract_app::{sdk::AbstractResponse, std::ibc::ModuleIbcInfo}; +use cosmwasm_std::{ensure_eq, from_json, Binary, DepsMut, Env, Response}; + +use crate::{ + contract::{PingPong, PingPongResult}, + error::PingPongError, + msg::PingPongIbcMsg, + state::PONGS, +}; + +pub fn receive_module_ibc( + deps: DepsMut, + _env: Env, + module: PingPong, + source_module: ModuleIbcInfo, + msg: Binary, +) -> PingPongResult { + let this_module_info = module.module_info()?; + ensure_eq!( + source_module.module, + this_module_info, + PingPongError::UnauthorizedIbc { + source_module: source_module.module.clone() + } + ); + let ibc_msg: PingPongIbcMsg = from_json(msg)?; + match ibc_msg { + PingPongIbcMsg::Ping {} => PONGS.update(deps.storage, &source_module.chain, |pongs| { + PingPongResult::Ok(pongs.unwrap_or_default() + 1) + })?, + }; + + Ok(module.response("pong")) +} diff --git a/contracts/{{ibc_app_name}}/src/lib.rs b/contracts/{{ibc_app_name}}/src/lib.rs new file mode 100644 index 0000000..633b89a --- /dev/null +++ b/contracts/{{ibc_app_name}}/src/lib.rs @@ -0,0 +1,17 @@ +pub mod contract; +pub mod error; +mod handlers; +mod ibc; +pub mod msg; +pub mod state; + +pub use contract::interface::PingPongInterface; +pub use msg::{PingPongExecuteMsgFns, PingPongQueryMsgFns}; + +/// The version of your app +pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub const TESTGEN_LOCAL_NAMESPACE: &str = "testgen-local"; +pub const PING_PONG_NAME: &str = "ping-pong"; +pub const PING_PONG_ID: &str = + const_format::concatcp!(TESTGEN_LOCAL_NAMESPACE, ":", PING_PONG_NAME); diff --git a/contracts/{{ibc_app_name}}/src/msg.rs b/contracts/{{ibc_app_name}}/src/msg.rs new file mode 100644 index 0000000..be5b0a0 --- /dev/null +++ b/contracts/{{ibc_app_name}}/src/msg.rs @@ -0,0 +1,46 @@ +use abstract_app::objects::TruncatedChainId; +use cosmwasm_schema::QueryResponses; + +use crate::contract::PingPong; + +// This is used for type safety and re-exporting the contract endpoint structs. +abstract_app::app_msg_types!(PingPong, PingPongExecuteMsg, PingPongQueryMsg); + +/// App instantiate message +#[cosmwasm_schema::cw_serde] +pub struct PingPongInstantiateMsg {} + +/// App execute messages +#[cosmwasm_schema::cw_serde] +#[derive(cw_orch::ExecuteFns)] +pub enum PingPongExecuteMsg { + /// Increment ping in this module and pong on its counterpart on another chain. + Ping { opponent_chain: TruncatedChainId }, +} + +/// App query messages +#[cosmwasm_schema::cw_serde] +#[derive(QueryResponses, cw_orch::QueryFns)] +pub enum PingPongQueryMsg { + #[returns(StatusResponse)] + Status {}, +} + +#[cosmwasm_schema::cw_serde] +pub enum PingPongIbcMsg { + Ping {}, +} + +#[cosmwasm_schema::cw_serde] +pub enum PingPongCallbackMsg { + Pinged { opponent_chain: TruncatedChainId }, +} + +#[cosmwasm_schema::cw_serde] +pub struct AppMigrateMsg {} + +#[cosmwasm_schema::cw_serde] +pub struct StatusResponse { + pub pings: u32, + pub pongs: u32, +} diff --git a/contracts/{{ibc_app_name}}/src/state.rs b/contracts/{{ibc_app_name}}/src/state.rs new file mode 100644 index 0000000..722c9b1 --- /dev/null +++ b/contracts/{{ibc_app_name}}/src/state.rs @@ -0,0 +1,5 @@ +use abstract_app::objects::TruncatedChainId; +use cw_storage_plus::Map; + +pub const PINGS: Map<&TruncatedChainId, u32> = Map::new("pings"); +pub const PONGS: Map<&TruncatedChainId, u32> = Map::new("pongs"); diff --git a/contracts/{{ibc_app_name}}/tests/ping_pong_tests.rs b/contracts/{{ibc_app_name}}/tests/ping_pong_tests.rs new file mode 100644 index 0000000..c34aad6 --- /dev/null +++ b/contracts/{{ibc_app_name}}/tests/ping_pong_tests.rs @@ -0,0 +1,165 @@ +use abstract_app::objects::namespace::Namespace; + +use abstract_client::{AbstractClient, AbstractInterchainClient, Application, RemoteAccount}; + +use abstract_app::std::objects::TruncatedChainId; + +use cw_orch::{anyhow, prelude::*}; +use cw_orch_interchain::prelude::*; + +use ping_pong::msg::{PingPongInstantiateMsg, StatusResponse}; +use ping_pong::PING_PONG_ID; +use ping_pong::{PingPongExecuteMsgFns, PingPongInterface, PingPongQueryMsgFns}; + +const JUNO: &str = "juno-1"; +const STARGAZE: &str = "stargaze-1"; + +#[allow(unused)] +struct PingPong> { + abs_juno: AbstractClient, + abs_stargaze: AbstractClient, + ping_pong: Application>, + remote_account: RemoteAccount, + mock_interchain: IbcEnv, +} + +impl PingPong { + /// Set up the test environment with two Accounts that has the App installed + fn setup() -> anyhow::Result> { + // Logger + let _ = env_logger::builder().is_test(true).try_init(); + + // Create a sender and mock env + let mock_interchain = + MockBech32InterchainEnv::new(vec![(JUNO, "juno"), (STARGAZE, "stargaze")]); + + let interchain_abstract = AbstractInterchainClient::deploy_mock(&mock_interchain)?; + + let abs_juno = interchain_abstract.client(JUNO)?; + let abs_stargaze = interchain_abstract.client(STARGAZE)?; + + let namespace = Namespace::from_id(PING_PONG_ID)?; + // Publish and install on both chains + let publisher_juno = abs_juno + .account_builder() + .namespace(namespace.clone()) + .build()? + .publisher()?; + publisher_juno.publish_app::>()?; + let app = publisher_juno + .account() + .install_app_with_dependencies::>( + &PingPongInstantiateMsg {}, + Empty {}, + &[], + )?; + + let publisher_stargaze = abs_stargaze + .account_builder() + .namespace(namespace) + .build()? + .publisher()?; + publisher_stargaze.publish_app::>()?; + + let remote_account = app + .account() + .remote_account_builder(mock_interchain.clone(), &abs_stargaze) + .install_app_with_dependencies::>( + &PingPongInstantiateMsg {}, + Empty {}, + )? + .build()?; + Ok(PingPong { + abs_juno, + abs_stargaze, + ping_pong: app, + remote_account, + mock_interchain, + }) + } +} + +#[test] +fn successful_install() -> anyhow::Result<()> { + let env = PingPong::setup()?; + let app1 = env.ping_pong; + + let status = app1.status()?; + assert_eq!(status, StatusResponse { pings: 0, pongs: 0 }); + + let app2 = env.remote_account.application::>()?; + + let status: StatusResponse = app2.status()?; + + assert_eq!(status, StatusResponse { pings: 0, pongs: 0 }); + Ok(()) +} + +#[test] +fn successful_ping() -> anyhow::Result<()> { + // Create a sender and mock env + let env = PingPong::setup()?; + let app = env.ping_pong; + let remote_app = env.remote_account.application::>()?; + + let status = app.status()?; + assert_eq!(status, StatusResponse { pings: 0, pongs: 0 }); + let status = remote_app.status()?; + assert_eq!(status, StatusResponse { pings: 0, pongs: 0 }); + + // juno pings stargaze + let pp = app.ping(TruncatedChainId::from_chain_id(STARGAZE))?; + env.mock_interchain.await_and_check_packets(JUNO, pp)?; + + // juno pinged, stargaze ponged. + let status = app.status()?; + assert_eq!(status, StatusResponse { pings: 1, pongs: 0 }); + let status = remote_app.status()?; + assert_eq!(status, StatusResponse { pings: 0, pongs: 1 }); + + // repeat + let pp = app.ping(TruncatedChainId::from_chain_id(STARGAZE))?; + env.mock_interchain.await_and_check_packets(JUNO, pp)?; + + let status = app.status()?; + assert_eq!(status, StatusResponse { pings: 2, pongs: 0 }); + let status = remote_app.status()?; + assert_eq!(status, StatusResponse { pings: 0, pongs: 2 }); + + Ok(()) +} + +#[test] +fn successful_ping_to_home_chain() -> anyhow::Result<()> { + // Create a sender and mock env + let env = PingPong::setup()?; + let app = env.ping_pong; + let remote_app = env.remote_account.application::>()?; + + // stargaze pings juno + // Note that `RemoteApplication` takes care of waiting for ibc + remote_app.execute( + &ping_pong::msg::PingPongExecuteMsg::Ping { + opponent_chain: TruncatedChainId::from_chain_id(JUNO), + } + .into(), + vec![], + )?; + + // stargaze pinged, juno ponged + let status = app.status()?; + assert_eq!(status, StatusResponse { pings: 0, pongs: 1 }); + let status = remote_app.status()?; + assert_eq!(status, StatusResponse { pings: 1, pongs: 0 }); + + // juno ping, stargaze pong + let pp = app.ping(TruncatedChainId::from_chain_id(STARGAZE))?; + env.mock_interchain.await_and_check_packets(JUNO, pp)?; + + let status = app.status()?; + assert_eq!(status, StatusResponse { pings: 1, pongs: 1 }); + let status = remote_app.status()?; + assert_eq!(status, StatusResponse { pings: 1, pongs: 1 }); + + Ok(()) +} diff --git a/contracts/{{standalone_name}}/Cargo.toml b/contracts/{{standalone_name}}/Cargo.toml index 7bcab67..1964af7 100644 --- a/contracts/{{standalone_name}}/Cargo.toml +++ b/contracts/{{standalone_name}}/Cargo.toml @@ -48,7 +48,6 @@ const_format = { version = "0.2.32" } # Dependencies for interface cw-orch = { workspace = true } -abstract-interface = { workspace = true } # Dependencies for bins clap = { workspace = true, optional = true, features = ["derive"] } diff --git a/contracts/{{standalone_name}}/src/bin/publish.rs b/contracts/{{standalone_name}}/src/bin/publish.rs index 414d5a0..5a0deb2 100644 --- a/contracts/{{standalone_name}}/src/bin/publish.rs +++ b/contracts/{{standalone_name}}/src/bin/publish.rs @@ -31,8 +31,10 @@ fn publish(networks: Vec) -> anyhow::Result<()> { // Get the [`Publisher`] that owns the namespace, otherwise create a new one and claim the namespace let publisher: Publisher<_> = abstract_client - .publisher_builder(standalone_namespace) - .build()?; + .account_builder() + .namespace(standalone_namespace) + .build()? + .publisher()?; if publisher.account().owner()? != chain.sender_addr() { panic!("The current sender can not publish to this namespace. Please use the wallet that owns the Account that owns the Namespace.") diff --git a/contracts/{{standalone_name}}/src/interface.rs b/contracts/{{standalone_name}}/src/interface.rs index 0f5f8bc..f7b9e10 100644 --- a/contracts/{{standalone_name}}/src/interface.rs +++ b/contracts/{{standalone_name}}/src/interface.rs @@ -15,13 +15,13 @@ use crate::{ )] pub struct {{standalone_name | upper_camel_case}}Interface; -impl abstract_interface::DependencyCreation +impl abstract_standalone::abstract_interface::DependencyCreation for {{standalone_name | upper_camel_case}}Interface { type DependenciesConfig = cosmwasm_std::Empty; } -impl abstract_interface::RegisteredModule +impl abstract_standalone::abstract_interface::RegisteredModule for {{standalone_name | upper_camel_case}}Interface { type InitMsg = <{{standalone_name | upper_camel_case}}Interface as InstantiableContract>::InstantiateMsg; diff --git a/contracts/{{standalone_name}}/tests/integration.rs b/contracts/{{standalone_name}}/tests/integration.rs index e86ec09..82a63f5 100644 --- a/contracts/{{standalone_name}}/tests/integration.rs +++ b/contracts/{{standalone_name}}/tests/integration.rs @@ -32,7 +32,11 @@ impl TestEnv { abs_client.set_balance(&sender, &coins(123, "ucosm"))?; // Publish the standalone - let publisher = abs_client.publisher_builder(namespace).build()?; + let publisher = abs_client + .account_builder() + .namespace(namespace) + .build()? + .publisher()?; publisher.publish_standalone::<{{standalone_name | upper_camel_case}}Interface<_>>()?; let standalone = publisher