diff --git a/CHANGELOG.md b/CHANGELOG.md index 463a59daf..9e3670d5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - feat: Osmosis Token Factory ADO [(#929)](https://github.com/andromedaprotocol/andromeda-core/pull/929) - feat: Wildcard in Permissions [(#949)](https://github.com/andromedaprotocol/andromeda-core/pull/949) - feat: Additional Permission Queries [(#960)](https://github.com/andromedaprotocol/andromeda-core/pull/960) +- feat: Proxy ADO [(#961)](https://github.com/andromedaprotocol/andromeda-core/pull/961) - feat: Standardized events in ADOS [(#970)](https://github.com/andromedaprotocol/andromeda-core/pull/970) ### Changed diff --git a/Cargo.lock b/Cargo.lock index 575ca517d..e2527acbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -802,6 +802,28 @@ dependencies = [ "cw20 2.0.0", ] +[[package]] +name = "andromeda-proxy" +version = "0.1.0" +dependencies = [ + "andromeda-socket", + "andromeda-std", + "andromeda-testing", + "cosmwasm-schema 2.2.2", + "cosmwasm-std 2.2.2", + "cw-multi-test", + "cw-orch", + "cw-orch-daemon 0.24.6", + "cw-storage-plus 2.0.0", + "cw-utils 2.0.0", + "cw2 2.0.0", + "cw20 2.0.0", + "osmosis-std", + "schemars 0.8.22", + "serde", + "thiserror 2.0.15", +] + [[package]] name = "andromeda-rate-limiting-withdrawals" version = "2.1.1-b.5" @@ -4296,6 +4318,8 @@ dependencies = [ "andromeda-kernel", "andromeda-math", "andromeda-non-fungible-tokens", + "andromeda-proxy", + "andromeda-socket", "andromeda-splitter", "andromeda-std", "andromeda-testing", diff --git a/contracts/os/andromeda-kernel/src/execute.rs b/contracts/os/andromeda-kernel/src/execute.rs index 23ac309eb..622d293d7 100644 --- a/contracts/os/andromeda-kernel/src/execute.rs +++ b/contracts/os/andromeda-kernel/src/execute.rs @@ -117,14 +117,25 @@ pub fn handle_local( let sub_msg = if config.direct || ado_type.is_none() { amp_message.generate_sub_msg_direct(recipient_addr, ReplyId::AMPMsg.repr()) } else { - let origin = ctx.map_or(info.sender.to_string(), |ctx| ctx.get_origin()); + let origin = ctx + .clone() + .map_or(info.sender.to_string(), |ctx| ctx.get_origin()); let previous_sender = info.sender.to_string(); - - AMPPkt::new(origin, previous_sender, vec![amp_message.clone()]).to_sub_msg( - recipient_addr, - Some(funds.clone()), - ReplyId::AMPMsg.repr(), - )? + println!("origin: {origin}"); + println!("prev sender: {previous_sender}"); + if let Some(ctx) = ctx { + AMPPkt::new_with_ctx(ctx, vec![amp_message.clone()]).to_sub_msg( + recipient_addr, + Some(funds.clone()), + ReplyId::AMPMsg.repr(), + )? + } else { + AMPPkt::new(origin, previous_sender, vec![amp_message.clone()]).to_sub_msg( + recipient_addr, + Some(funds.clone()), + ReplyId::AMPMsg.repr(), + )? + } }; Ok(Response::default() diff --git a/contracts/os/andromeda-kernel/src/ibc.rs b/contracts/os/andromeda-kernel/src/ibc.rs index 1f510cc5e..152710ca4 100644 --- a/contracts/os/andromeda-kernel/src/ibc.rs +++ b/contracts/os/andromeda-kernel/src/ibc.rs @@ -18,10 +18,10 @@ use cosmwasm_schema::cw_serde; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - ensure, from_json, to_json_binary, Addr, Binary, Deps, DepsMut, Empty, Env, - Ibc3ChannelOpenResponse, IbcBasicResponse, IbcChannel, IbcChannelCloseMsg, - IbcChannelConnectMsg, IbcChannelOpenMsg, IbcOrder, IbcPacketAckMsg, IbcPacketReceiveMsg, - IbcPacketTimeoutMsg, IbcReceiveResponse, MessageInfo, SubMsg, WasmMsg, + ensure, from_json, to_json_binary, Binary, Deps, DepsMut, Empty, Env, Ibc3ChannelOpenResponse, + IbcBasicResponse, IbcChannel, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, + IbcOrder, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, + MessageInfo, SubMsg, WasmMsg, }; pub const PACKET_LIFETIME: u64 = 604_800u64; @@ -138,14 +138,13 @@ pub fn do_ibc_packet_receive( deps.branch(), MessageInfo { funds: vec![], - sender: Addr::unchecked( - "cosmwasm122xa328nvn93rsemr980psc9m9qwh8xj8rdje4qtp68m5tyt7yusajjrpz", - ), + sender: env.clone().contract.address, }, env.clone(), ); match packet_msg { IbcExecuteMsg::SendMessage { amp_packet } => { + let previous_hops = amp_packet.ctx.get_previous_hops(); // Try to pull the username's address from the packet match amp_packet.ctx.get_origin_username() { Some(username) => { @@ -163,12 +162,14 @@ pub fn do_ibc_packet_receive( } } None => { - // Added this to keep track of original sender, even if the address is invalid on the receiving chain - let new_amp_packet = AMPPkt::new( - amp_packet.ctx.get_origin(), + let ctx = AMPCtx::new_with_hops( + env.clone().contract.address, env.contract.address, - amp_packet.messages.clone(), + None, + previous_hops, ); + + let new_amp_packet = AMPPkt::new_with_ctx(ctx, amp_packet.messages.clone()); execute_env.amp_ctx = Some(new_amp_packet); } } @@ -209,11 +210,12 @@ pub fn do_ibc_packet_receive( match username_addr { Some(addr) => { // Add potential username to the context - let mut ctx = AMPCtx::new(addr, env.contract.address, original_sender_username); - // Add previous hops to the context - for hop in previous_hops { - ctx.add_hop(hop); - } + let ctx = AMPCtx::new_with_hops( + addr, + env.contract.address, + original_sender_username, + previous_hops, + ); let amp_packet = AMPPkt::new_with_ctx(ctx, vec![msg.clone()]); execute_env.amp_ctx = Some(amp_packet.clone()); diff --git a/contracts/os/andromeda-kernel/src/tests/test_handler.rs b/contracts/os/andromeda-kernel/src/tests/test_handler.rs index 5424781ed..0e5072993 100644 --- a/contracts/os/andromeda-kernel/src/tests/test_handler.rs +++ b/contracts/os/andromeda-kernel/src/tests/test_handler.rs @@ -75,7 +75,7 @@ fn test_handle_local() { ctx: Some(AMPCtx::new("origin", MOCK_APP_CONTRACT, None)), expected_submessage: AMPPkt::new( "origin", - SENDER, + "cosmwasm1t3hmmkn74pqs5htv9swgzpjf7np9z8zc98hgj8hx879cp5juk4eqmnwcm4", vec![AMPMsg::new( MOCK_APP_CONTRACT, to_json_binary(&true).unwrap(), @@ -101,7 +101,7 @@ fn test_handle_local() { ctx: Some(AMPCtx::new("origin", MOCK_APP_CONTRACT, None)), expected_submessage: AMPPkt::new( "origin", - SENDER, + "cosmwasm1t3hmmkn74pqs5htv9swgzpjf7np9z8zc98hgj8hx879cp5juk4eqmnwcm4", vec![AMPMsg::new( MOCK_APP_CONTRACT, to_json_binary(&true).unwrap(), diff --git a/contracts/socket/andromeda-proxy/.cargo/config b/contracts/socket/andromeda-proxy/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/socket/andromeda-proxy/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/socket/andromeda-proxy/Cargo.toml b/contracts/socket/andromeda-proxy/Cargo.toml new file mode 100644 index 000000000..602637ae1 --- /dev/null +++ b/contracts/socket/andromeda-proxy/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "andromeda-proxy" +version = "0.1.0" +edition = "2021" +rust-version = "1.86.0" + + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +library = [] +testing = ["cw-multi-test", "andromeda-testing"] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +schemars = "0.8.16" +serde = { workspace = true } +thiserror = { workspace = true } +cw-utils = { workspace = true } +andromeda-std = { workspace = true } +cw20 = { workspace = true } +andromeda-socket = { workspace = true } +osmosis-std = "0.27.0" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +cw-multi-test = { workspace = true, optional = true } +cw-orch = { workspace = true } +andromeda-testing = { workspace = true, optional = true } +cw-orch-daemon = "0.24.2" diff --git a/contracts/socket/andromeda-proxy/examples/schema.rs b/contracts/socket/andromeda-proxy/examples/schema.rs new file mode 100644 index 000000000..27e5e4010 --- /dev/null +++ b/contracts/socket/andromeda-proxy/examples/schema.rs @@ -0,0 +1,13 @@ +use andromeda_socket::proxy::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use cosmwasm_schema::write_api; +use std::env::current_dir; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + }; +} diff --git a/contracts/socket/andromeda-proxy/src/contract.rs b/contracts/socket/andromeda-proxy/src/contract.rs new file mode 100644 index 000000000..8210fa2b0 --- /dev/null +++ b/contracts/socket/andromeda-proxy/src/contract.rs @@ -0,0 +1,269 @@ +use crate::state::{ + authorize, get_reply_id, ADMINS, BATCH_REPLY_ID_FAIL_ON_ERROR, BATCH_REPLY_ID_IGNORE_ERROR, + REPLY_ID, +}; +use andromeda_socket::proxy::{ + ExecuteMsg, ExecutionType, InitParams, InstantiateMsg, Operation, QueryMsg, +}; +use andromeda_std::{ + ado_base::{InstantiateMsg as BaseInstantiateMsg, MigrateMsg}, + ado_contract::ADOContract, + amp::AndrAddr, + andr_execute_fn, + common::context::ExecuteContext, + error::ContractError, + os::aos_querier::AOSQuerier, +}; +use cosmwasm_std::{ + attr, entry_point, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply, Response, + StdError, SubMsg, WasmMsg, +}; +use cw2::set_contract_version; + +const CONTRACT_NAME: &str = "crates.io:andromeda-proxy"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + let inst_resp = ADOContract::default().instantiate( + deps.storage, + env, + deps.api, + &deps.querier, + info.clone(), + BaseInstantiateMsg { + ado_type: CONTRACT_NAME.to_string(), + ado_version: CONTRACT_VERSION.to_string(), + kernel_address: msg.kernel_address.clone(), + owner: msg.owner, + }, + )?; + + ADMINS.save(deps.storage, &msg.admins)?; + + Ok(inst_resp + .add_attribute("method", "instantiate") + .add_attribute("owner", info.sender)) +} + +#[andr_execute_fn] +pub fn execute(ctx: ExecuteContext, msg: ExecuteMsg) -> Result { + match msg { + ExecuteMsg::Instantiate { + init_params, + message, + admin, + label, + } => send_instantiate(&ctx, init_params, message, admin, label, None), + ExecuteMsg::Execute { + contract_addr, + message, + } => send_execute(&ctx, contract_addr, message, None), + ExecuteMsg::BatchExecute { operations } => send_batch_execute(ctx, operations), + ExecuteMsg::ModifyAdmins { admins } => execute_modify_admins(ctx, admins), + _ => ADOContract::default().execute(ctx, msg), + } +} + +// Used for cross-chain creation of denom +fn send_execute( + ctx: &ExecuteContext, + contract_addr: AndrAddr, + message: Binary, + fail_on_error: Option, +) -> Result { + authorize(ctx)?; + + let reply_id = get_reply_id(fail_on_error); + // Forward the message + let sub_msg = SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: contract_addr + .get_raw_address(&ctx.deps.as_ref())? + .into_string(), + msg: message, + funds: ctx.info.funds.clone(), + }), + reply_id, + ); + + Ok(Response::default() + .add_submessage(sub_msg) + .add_attribute("action", "send_execute")) +} + +fn send_batch_execute( + ctx: ExecuteContext, + operations: Vec, +) -> Result { + authorize(&ctx)?; + + let mut resp = Response::default(); + + for op in operations { + let result = match op.execution { + ExecutionType::Instantiate { + init_params, + message, + admin, + label, + } => send_instantiate( + &ctx, + init_params, + message, + admin, + label, + Some(op.fail_on_error), + ), + ExecutionType::Execute { + contract_addr, + message, + } => send_execute(&ctx, contract_addr, message, Some(op.fail_on_error)), + ExecutionType::Migrate { + contract_addr, + new_code_id, + migrate_msg, + } => send_migrate( + &ctx, + contract_addr, + new_code_id, + migrate_msg, + Some(op.fail_on_error), + ), + }; + + match result { + Ok(new_res) => { + resp = resp + .add_submessages(new_res.messages) + .add_attributes(new_res.attributes); + } + Err(e) if op.fail_on_error => return Err(e), + Err(_) => {} // ignore error, continue loop + } + } + + Ok(resp) +} + +fn send_instantiate( + ctx: &ExecuteContext, + init_params: InitParams, + message: Binary, + admin: Option, + label: Option, + fail_on_error: Option, +) -> Result { + authorize(ctx)?; + + let code_id = match init_params { + InitParams::CodeId(code_id) => code_id, + InitParams::AdoVersion(ado_version) => { + let adodb_addr = AOSQuerier::adodb_address_getter( + &ctx.deps.querier, + &ctx.contract.get_kernel_address(ctx.deps.storage)?, + )?; + AOSQuerier::code_id_getter(&ctx.deps.querier, &adodb_addr, ado_version.as_str())? + } + }; + + let admin = match admin { + Some(admin) => Some(admin.get_raw_address(&ctx.deps.as_ref())?.into_string()), + None => None, + }; + let reply_id = get_reply_id(fail_on_error); + // Forward the message + let sub_msg = SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Instantiate { + admin, + code_id, + msg: message, + funds: ctx.info.funds.clone(), + label: label.unwrap_or("default".to_string()), + }), + reply_id, + ); + + Ok(Response::default() + .add_attribute("action", "send_instantiate") + .add_submessage(sub_msg)) +} + +fn execute_modify_admins( + ctx: ExecuteContext, + admins: Vec, +) -> Result { + authorize(&ctx)?; + + ADMINS.save(ctx.deps.storage, &admins)?; + + Ok(Response::default().add_attribute("action", "modify_admins")) +} + +fn send_migrate( + ctx: &ExecuteContext, + contract_addr: AndrAddr, + new_code_id: u64, + migrate_msg: Binary, + fail_on_error: Option, +) -> Result { + authorize(ctx)?; + let reply_id = get_reply_id(fail_on_error); + let sub_msg = SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Migrate { + contract_addr: contract_addr + .get_raw_address(&ctx.deps.as_ref())? + .into_string(), + new_code_id, + msg: migrate_msg, + }), + reply_id, + ); + + Ok(Response::default() + .add_submessage(sub_msg) + .add_attribute("action", "send_migrate")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + ADOContract::default().query(deps, env, msg) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, env: Env, _msg: MigrateMsg) -> Result { + ADOContract::default().migrate(deps, env, CONTRACT_NAME, CONTRACT_VERSION) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(_deps: DepsMut, _env: Env, msg: Reply) -> Result { + match msg.id { + REPLY_ID => { + if msg.result.is_err() { + Err(ContractError::Std(StdError::generic_err(format!( + "Contract error: {:?}", + msg.result.unwrap_err() + )))) + } else { + Ok( + Response::default() + .add_attributes(vec![attr("action", "message_sent_success")]), + ) + } + } + BATCH_REPLY_ID_FAIL_ON_ERROR => Err(ContractError::Std(StdError::generic_err(format!( + "Contract error: {:?}", + msg.result.unwrap_err() + )))), + BATCH_REPLY_ID_IGNORE_ERROR => Ok(Response::default()), + _ => Err(ContractError::Std(StdError::generic_err( + "Invalid Reply ID".to_string(), + ))), + } +} diff --git a/contracts/socket/andromeda-proxy/src/interface.rs b/contracts/socket/andromeda-proxy/src/interface.rs new file mode 100644 index 000000000..86d367e55 --- /dev/null +++ b/contracts/socket/andromeda-proxy/src/interface.rs @@ -0,0 +1,6 @@ +use andromeda_socket::proxy::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_std::{ado_base::MigrateMsg, contract_interface, deploy::ADOMetadata}; + +pub const CONTRACT_ID: &str = "osmosis-proxy"; + +contract_interface!(ProxyContract, CONTRACT_ID, "andromeda_proxy.wasm"); diff --git a/contracts/socket/andromeda-proxy/src/lib.rs b/contracts/socket/andromeda-proxy/src/lib.rs new file mode 100644 index 000000000..694ba68c0 --- /dev/null +++ b/contracts/socket/andromeda-proxy/src/lib.rs @@ -0,0 +1,11 @@ +pub mod contract; +pub mod state; + +#[cfg(all(not(target_arch = "wasm32"), feature = "testing"))] +pub mod mock; + +#[cfg(not(target_arch = "wasm32"))] +mod interface; + +#[cfg(not(target_arch = "wasm32"))] +pub use crate::interface::ProxyContract; diff --git a/contracts/socket/andromeda-proxy/src/mock.rs b/contracts/socket/andromeda-proxy/src/mock.rs new file mode 100644 index 000000000..1955de949 --- /dev/null +++ b/contracts/socket/andromeda-proxy/src/mock.rs @@ -0,0 +1,102 @@ +#![cfg(all(not(target_arch = "wasm32"), feature = "testing"))] + +use crate::contract::{execute, instantiate, query}; +use andromeda_socket::proxy::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_std::ado_base::permissioning::{Permission, PermissioningMessage}; +use andromeda_std::ado_base::rates::{Rate, RatesMessage}; +use andromeda_std::amp::messages::AMPPkt; +use andromeda_std::amp::AndrAddr; + +use andromeda_testing::mock::MockApp; +use andromeda_testing::{ + mock_ado, + mock_contract::{ExecuteResult, MockADO, MockContract}, +}; +use cosmwasm_std::{Addr, Empty}; +use cw_multi_test::{Contract, ContractWrapper, Executor}; + +pub struct MockProxy(Addr); +mock_ado!(MockProxy, ExecuteMsg, QueryMsg); + +impl MockProxy { + pub fn instantiate( + code_id: u64, + sender: Addr, + app: &mut MockApp, + admins: Vec, + kernel_address: impl Into, + owner: Option, + ) -> MockProxy { + let msg = mock_osmosis_token_factory_instantiate_msg(admins, kernel_address, owner); + let addr = app + .instantiate_contract( + code_id, + sender.clone(), + &msg, + &[], + "Osmosis Token Factory", + Some(sender.to_string()), + ) + .unwrap(); + MockProxy(Addr::unchecked(addr)) + } + + pub fn execute_add_rate( + &self, + app: &mut MockApp, + sender: Addr, + action: String, + rate: Rate, + ) -> ExecuteResult { + self.execute(app, &mock_set_rate_msg(action, rate), sender, &[]) + } + + pub fn execute_set_permission( + &self, + app: &mut MockApp, + sender: Addr, + actors: Vec, + action: String, + permission: Permission, + ) -> ExecuteResult { + let msg = mock_set_permission(actors, action, permission); + self.execute(app, &msg, sender, &[]) + } +} + +pub fn mock_andromeda_osmosis_token_factory() -> Box> { + let contract = ContractWrapper::new_with_empty(execute, instantiate, query); + Box::new(contract) +} + +pub fn mock_osmosis_token_factory_instantiate_msg( + admins: Vec, + kernel_address: impl Into, + owner: Option, +) -> InstantiateMsg { + InstantiateMsg { + admins, + kernel_address: kernel_address.into(), + owner, + } +} + +pub fn mock_set_rate_msg(action: String, rate: Rate) -> ExecuteMsg { + ExecuteMsg::Rates(RatesMessage::SetRate { action, rate }) +} + +pub fn mock_set_permission( + actors: Vec, + action: String, + permission: Permission, +) -> ExecuteMsg { + ExecuteMsg::Permissioning(PermissioningMessage::SetPermission { + actors, + action, + permission, + }) +} + +pub fn mock_receive_packet(packet: AMPPkt) -> ExecuteMsg { + ExecuteMsg::AMPReceive(packet) +} diff --git a/contracts/socket/andromeda-proxy/src/state.rs b/contracts/socket/andromeda-proxy/src/state.rs new file mode 100644 index 000000000..265a7a819 --- /dev/null +++ b/contracts/socket/andromeda-proxy/src/state.rs @@ -0,0 +1,35 @@ +use andromeda_std::{common::context::ExecuteContext, error::ContractError}; +use cosmwasm_std::ensure; +use cw_storage_plus::Item; + +/// List of addresses that are allowed to operate this ADO +pub const ADMINS: Item> = Item::new("admins"); + +/// If message is sent through AMP, it checks if the orignial sender is authorized. +/// If it's a direct message, it checks if the latest sender of the message is authorized. +pub(crate) fn authorize(ctx: &ExecuteContext) -> Result<(), ContractError> { + // Fetch original sender of the amp packet (if available) + let sender = ctx + .amp_ctx + .clone() + .and_then(|pkt| pkt.ctx.get_previous_hops().first().cloned()) + .map(|hop| hop.address.to_string()) + .unwrap_or(ctx.info.sender.to_string()); + + let admins = ADMINS.load(ctx.deps.storage)?; + + // Check authority + ensure!(admins.contains(&sender), ContractError::Unauthorized {}); + Ok(()) +} + +pub(crate) const REPLY_ID: u64 = 1; +pub(crate) const BATCH_REPLY_ID_FAIL_ON_ERROR: u64 = 101; +pub(crate) const BATCH_REPLY_ID_IGNORE_ERROR: u64 = 201; +pub(crate) fn get_reply_id(fail_on_error: Option) -> u64 { + match fail_on_error { + Some(true) => BATCH_REPLY_ID_FAIL_ON_ERROR, + Some(false) => BATCH_REPLY_ID_IGNORE_ERROR, + None => REPLY_ID, + } +} diff --git a/packages/andromeda-socket/src/lib.rs b/packages/andromeda-socket/src/lib.rs index 2649781ec..1b1ceff11 100644 --- a/packages/andromeda-socket/src/lib.rs +++ b/packages/andromeda-socket/src/lib.rs @@ -1,3 +1,4 @@ pub mod astroport; pub mod osmosis; pub mod osmosis_token_factory; +pub mod proxy; diff --git a/packages/andromeda-socket/src/proxy.rs b/packages/andromeda-socket/src/proxy.rs new file mode 100644 index 000000000..79960a4b0 --- /dev/null +++ b/packages/andromeda-socket/src/proxy.rs @@ -0,0 +1,76 @@ +use andromeda_std::amp::AndrAddr; +use andromeda_std::os::adodb::ADOVersion; +use andromeda_std::{andr_exec, andr_instantiate, andr_query}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Binary; + +#[andr_instantiate] +#[cw_serde] +pub struct InstantiateMsg { + pub admins: Vec, +} + +#[cw_serde] +pub enum InitParams { + CodeId(u64), + AdoVersion(ADOVersion), +} + +#[cw_serde] +pub struct Operation { + pub execution: ExecutionType, + pub fail_on_error: bool, +} + +#[cw_serde] +pub enum ExecutionType { + Instantiate { + init_params: InitParams, + message: Binary, + admin: Option, + label: Option, + }, + Execute { + contract_addr: AndrAddr, + message: Binary, + }, + Migrate { + contract_addr: AndrAddr, + new_code_id: u64, + migrate_msg: Binary, + }, +} + +#[andr_exec] +#[cw_serde] +#[cfg_attr(not(target_arch = "wasm32"), derive(cw_orch::ExecuteFns))] +pub enum ExecuteMsg { + Instantiate { + init_params: InitParams, + message: Binary, + admin: Option, + label: Option, + }, + + #[cfg_attr(not(target_arch = "wasm32"), cw_orch(payable))] + Execute { + contract_addr: AndrAddr, + message: Binary, + // Funds will be native + }, + + #[cfg_attr(not(target_arch = "wasm32"), cw_orch(payable))] + BatchExecute { + operations: Vec, + }, + + ModifyAdmins { + admins: Vec, + }, +} + +#[cfg_attr(not(target_arch = "wasm32"), derive(cw_orch::QueryFns))] +#[andr_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg {} diff --git a/packages/std/src/amp/messages.rs b/packages/std/src/amp/messages.rs index f84065349..54342d05d 100644 --- a/packages/std/src/amp/messages.rs +++ b/packages/std/src/amp/messages.rs @@ -240,6 +240,21 @@ impl AMPCtx { } } + pub fn new_with_hops( + origin: impl Into, + previous_sender: impl Into, + origin_username: Option, + previous_hops: Vec, + ) -> AMPCtx { + AMPCtx { + origin: origin.into(), + origin_username, + previous_sender: previous_sender.into(), + id: None, + previous_hops, + } + } + /// Gets the original sender of a message pub fn get_origin(&self) -> String { self.origin.clone() @@ -255,6 +270,10 @@ impl AMPCtx { self.previous_sender.clone() } + pub fn get_previous_hops(&self) -> Vec { + self.previous_hops.clone() + } + /// Gets the previous sender of a message pub fn get_id(&self) -> Option { self.id.clone() diff --git a/tests/ibc-tests/Cargo.toml b/tests/ibc-tests/Cargo.toml index 5d78cd067..8d89e2d42 100644 --- a/tests/ibc-tests/Cargo.toml +++ b/tests/ibc-tests/Cargo.toml @@ -18,6 +18,10 @@ path = "interchain.rs" name = "splitter-ibc" path = "splitter_ibc.rs" +[[test]] +name = "proxy" +path = "proxy.rs" + [dev-dependencies] andromeda-kernel = {path = "../../contracts/os/andromeda-kernel", features = [ @@ -26,11 +30,14 @@ andromeda-kernel = {path = "../../contracts/os/andromeda-kernel", features = [ andromeda-fixed-amount-splitter = { path = "../../contracts/finance/andromeda-fixed-amount-splitter", features = [ "testing", ] } + andromeda-app = { workspace = true } andromeda-std = { workspace = true } andromeda-testing = { workspace = true } andromeda-finance = { workspace = true } andromeda-math = { workspace = true } +andromeda-socket = { workspace = true } + andromeda-counter = { path = "../../contracts/math/andromeda-counter", features = [ "testing", ] } @@ -46,6 +53,9 @@ andromeda-auction = { path = "../../contracts/non-fungible-tokens/andromeda-auct andromeda-cw721 = { path = "../../contracts/non-fungible-tokens/andromeda-cw721", features = [ "testing", ] } +andromeda-proxy = { path = "../../contracts/socket/andromeda-proxy", features = [ + "testing", +] } andromeda-non-fungible-tokens = { workspace = true } cosmwasm-std = { workspace = true, features = ["staking"] } cw-multi-test = { workspace = true, features = ["cosmwasm_1_2", "staking"]} diff --git a/tests/ibc-tests/mod.rs b/tests/ibc-tests/mod.rs index 1fdff58a4..0b412c901 100644 --- a/tests/ibc-tests/mod.rs +++ b/tests/ibc-tests/mod.rs @@ -8,4 +8,7 @@ mod interchain; mod splitter_ibc; #[cfg(test)] -mod test_ibc; \ No newline at end of file +mod test_ibc; + +#[cfg(test)] +mod proxy; diff --git a/tests/ibc-tests/proxy.rs b/tests/ibc-tests/proxy.rs new file mode 100644 index 000000000..76712eb29 --- /dev/null +++ b/tests/ibc-tests/proxy.rs @@ -0,0 +1,118 @@ +#![cfg(not(target_arch = "wasm32"))] + +use andromeda_fixed_amount_splitter::FixedAmountSplitterContract; +use andromeda_proxy::ProxyContract; +use andromeda_socket::proxy::InitParams; +use andromeda_std::{ + amp::{messages::AMPMsg, AndrAddr, Recipient}, + os, +}; +use andromeda_testing::{interchain::ensure_packet_success, InterchainTestEnv}; +use cosmwasm_std::{to_json_binary, Coin, Uint128}; +use cw_orch::prelude::*; +use cw_orch_interchain::prelude::*; + +#[test] +fn test_proxy_ibc() { + let InterchainTestEnv { + mut juno, + osmosis, + interchain, + .. + } = InterchainTestEnv::new(); + + let owner_on_osmosis = osmosis.chain.addr_make("ownerosmo"); + let owner_on_juno = juno.chain.addr_make("ownerjuno"); + + // Deploy on Osmosis + let proxy_osmosis = ProxyContract::new(osmosis.chain.clone()); + proxy_osmosis.upload().unwrap(); + + // This contract will eventually be instantiated by the proxy contract + let splitter_osmosis = FixedAmountSplitterContract::new(osmosis.chain.clone()); + splitter_osmosis.upload().unwrap(); + + let admins = vec![owner_on_juno.to_string(), owner_on_osmosis.to_string()]; + // Owner on osmosis will init the proxy on osmo, and set his juno address as admin + proxy_osmosis + .instantiate( + &andromeda_socket::proxy::InstantiateMsg { + admins, + kernel_address: osmosis.aos.kernel.address().unwrap().into_string(), + owner: None, + }, + None, + &[], + ) + .unwrap(); + + // Register contract + osmosis + .aos + .adodb + .execute( + &os::adodb::ExecuteMsg::Publish { + code_id: proxy_osmosis.code_id().unwrap(), + ado_type: "proxy".to_string(), + action_fees: None, + version: "0.1.0".to_string(), + publisher: None, + }, + &[], + ) + .unwrap(); + + // Create IBC message + let proxy_osmosis_recipient = AndrAddr::from_string(format!( + "ibc://{}/{}", + osmosis.chain_name, + proxy_osmosis.address().unwrap() + )); + + let splitter_init_msg = andromeda_finance::fixed_amount_splitter::InstantiateMsg { + recipients: vec![andromeda_finance::fixed_amount_splitter::AddressAmount { + recipient: Recipient { + address: AndrAddr::from_string(owner_on_osmosis.to_string()), + msg: None, + ibc_recovery_address: None, + }, + coins: vec![Coin { + denom: "osmo".to_string(), + amount: Uint128::new(100), + }], + }] + .into(), + default_recipient: None, + lock_time: None, + kernel_address: osmosis.aos.kernel.address().unwrap().into_string(), + owner: None, + }; + + let message = AMPMsg::new( + proxy_osmosis_recipient, + to_json_binary(&andromeda_socket::proxy::ExecuteMsg::Instantiate { + init_params: InitParams::CodeId(splitter_osmosis.code_id().unwrap()), + message: to_json_binary(&splitter_init_msg).unwrap(), + admin: None, + label: None, + }) + .unwrap(), + None, + ); + + // Execute IBC msg from Juno + juno.aos.kernel.set_sender(&owner_on_juno); + let kernel_juno_send_request = juno + .aos + .kernel + .execute(&os::kernel::ExecuteMsg::Send { message }, &[]) + .unwrap(); + + splitter_osmosis.addr_str().unwrap_err(); + + // Wait for packet processing + let packet_lifetime = interchain + .await_packets(&juno.chain_id, kernel_juno_send_request) + .unwrap(); + ensure_packet_success(packet_lifetime); +}