diff --git a/.gitignore b/.gitignore index 07c84838..8ba95355 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ ci/configs/cosm-orc/local.yaml contracts/**/Cargo.lock packages/**/Cargo.lock +*.lock diff --git a/Cargo.lock b/Cargo.lock index 812354ee..91fb5689 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,9 +139,9 @@ dependencies = [ [[package]] name = "cosmwasm-crypto" -version = "1.2.6" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c0e41be7e6c7d7ab3c61cdc32fcfaa14f948491a401cbc1c74bb33b6f4b851" +checksum = "bb64554a91d6a9231127f4355d351130a0b94e663d5d9dc8b3a54ca17d83de49" dependencies = [ "digest 0.10.7", "ed25519-zebra", @@ -152,18 +152,18 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.2.6" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a7ee2798c92c00dd17bebb4210f81d5f647e5e92d847959b7977e0fd29a3500" +checksum = "a0fb2ce09f41a3dae1a234d56a9988f9aff4c76441cd50ef1ee9a4f20415b028" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.2.6" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "407aca6f1671a08b60db8167f03bb7cb6b2378f0ddd9a030367b66ba33c2fd41" +checksum = "230e5d1cefae5331db8934763c81b9c871db6a2cd899056a5694fa71d292c815" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -174,9 +174,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.2.6" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d1e00b8fd27ff923c10303023626358e23a6f9079f8ebec23a8b4b0bfcd4b3" +checksum = "43dadf7c23406cb28079d69e6cb922c9c29b9157b0fe887e3b79c783b7d4bcb8" dependencies = [ "proc-macro2", "quote", @@ -185,9 +185,9 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.2.6" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d5fdfd112b070055f068fad079d490117c8e905a588b92a5a7c9276d029930" +checksum = "4337eef8dfaf8572fe6b6b415d6ec25f9308c7bb09f2da63789209fb131363be" dependencies = [ "base64 0.13.1", "cosmwasm-crypto", @@ -215,7 +215,7 @@ dependencies = [ "cw-fifo", "cw-multi-test", "cw-storage-plus 1.1.0", - "cw2 1.0.1", + "cw2 1.1.0", "serde", "thiserror", ] @@ -236,7 +236,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", - "cw2 1.0.1", + "cw2 1.1.0", "thiserror", ] @@ -253,7 +253,7 @@ dependencies = [ "cw-multi-test", "cw-storage-plus 1.1.0", "cw-utils 1.0.1", - "cw2 1.0.1", + "cw2 1.1.0", "neutron-sdk", "prost 0.11.9", "prost-types", @@ -278,7 +278,7 @@ dependencies = [ "cw-multi-test", "cw-storage-plus 1.1.0", "cw-utils 1.0.1", - "cw2 1.0.1", + "cw2 1.1.0", "cw20", "neutron-sdk", "prost 0.11.9", @@ -291,6 +291,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "covenant-ls" +version = "1.0.0" +dependencies = [ + "cosmos-sdk-proto 0.14.0", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.1.0", + "cw2 1.1.0", + "neutron-sdk", + "protobuf", + "schemars", + "serde", + "serde-json-wasm 0.4.1", + "thiserror", +] + [[package]] name = "cpufeatures" version = "0.2.8" @@ -415,7 +432,7 @@ checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw2 1.0.1", + "cw2 1.1.0", "schemars", "semver", "serde", @@ -437,15 +454,16 @@ dependencies = [ [[package]] name = "cw2" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb70cee2cf0b4a8ff7253e6bc6647107905e8eb37208f87d54f67810faa62f8" +checksum = "29ac2dc7a55ad64173ca1e0a46697c31b7a5c51342f55a1e84a724da4eb99908" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", "schemars", "serde", + "thiserror", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 31c80fed..7a2f3fd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,10 +30,9 @@ covenant-clock = { path = "contracts/clock" } covenant-clock-tester = { path = "contracts/clock-tester" } covenant-lp = { path = "contracts/lper" } covenant-clock-derive = { path = "packages/clock-derive" } +covenant-ls = { path = "contracts/ls" } cw-fifo = { path = "packages/cw-fifo" } - - # the sha2 version here is the same as the one used by # cosmwasm-std. when bumping cosmwasm-std, this should also be # updated. to find cosmwasm_std's sha function: diff --git a/contracts/depositor/src/contract.rs b/contracts/depositor/src/contract.rs index 1afec7e4..810d95ee 100644 --- a/contracts/depositor/src/contract.rs +++ b/contracts/depositor/src/contract.rs @@ -33,7 +33,7 @@ use neutron_sdk::{ use crate::state::{ add_error_to_queue, read_errors_from_queue, read_reply_payload, read_sudo_payload, save_sudo_payload, AcknowledgementResult, - ACKNOWLEDGEMENT_RESULTS, INTERCHAIN_ACCOUNTS, SUDO_PAYLOAD_REPLY_ID, CLOCK_ADDRESS, STRIDE_ATOM_RECEIVER, NATIVE_ATOM_RECEIVER, IBC_PORT_ID, ICA_ADDRESS, SudoPayload, save_reply_payload, CONTRACT_STATE, ContractState, GAIA_NEUTRON_IBC_TRANSFER_CHANNEL_ID, + ACKNOWLEDGEMENT_RESULTS, INTERCHAIN_ACCOUNTS, SUDO_PAYLOAD_REPLY_ID, CLOCK_ADDRESS, STRIDE_ATOM_RECEIVER, NATIVE_ATOM_RECEIVER, IBC_PORT_ID, ICA_ADDRESS, SudoPayload, save_reply_payload, CONTRACT_STATE, ContractState, GAIA_NEUTRON_IBC_TRANSFER_CHANNEL_ID, NEUTRON_GAIA_CONNECTION_ID, GAIA_STRIDE_IBC_TRANSFER_CHANNEL_ID, LP_ADDRESS, }; // Default timeout for SubmitTX is two weeks @@ -41,8 +41,6 @@ const DEFAULT_TIMEOUT_SECONDS: u64 = 60 * 60 * 24 * 7 * 2; // const DEFAULT_TIMEOUT_HEIGHT: u64 = 10000000; const NEUTRON_DENOM: &str = "untrn"; const ATOM_DENOM: &str = "uatom"; -const IBC_CONNECTION: &str = "connection-0"; -const ICS_CONNECTION_ID: &str = "connection-1"; const INTERCHAIN_ACCOUNT_ID: &str = "test"; const TRANSFER_PORT: &str = "transfer"; @@ -78,6 +76,8 @@ pub fn instantiate( CLOCK_ADDRESS.save(deps.storage, &Addr::unchecked(msg.clock_address))?; CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; GAIA_NEUTRON_IBC_TRANSFER_CHANNEL_ID.save(deps.storage, &msg.gaia_neutron_ibc_transfer_channel_id)?; + NEUTRON_GAIA_CONNECTION_ID.save(deps.storage, &msg.neutron_gaia_connection_id)?; + GAIA_STRIDE_IBC_TRANSFER_CHANNEL_ID.save(deps.storage, &msg.gaia_stride_ibc_transfer_channel_id)?; Ok(Response::default()) } @@ -109,12 +109,93 @@ fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> NeutronResult try_register_gaia_ica(deps, env), - ContractState::ICACreated => try_receive_atom_from_ica(deps, env, info, gaia_account_address), - ContractState::ReceivedFunds => try_execute_transfers(deps, env, info, gaia_account_address), + ContractState::ICACreated => try_liquid_stake(deps, env, info, gaia_account_address), + ContractState::LiquidStaked => try_receive_atom_from_ica(deps, env, info, gaia_account_address), ContractState::Complete => Ok(Response::default()), } } +fn try_liquid_stake( + deps: DepsMut, + env: Env, + _info: MessageInfo, + _gaia_account_address: String +) -> NeutronResult> { + let fee = IbcFee { + recv_fee: vec![], // must be empty + ack_fee: vec![cosmwasm_std::Coin { + denom: NEUTRON_DENOM.to_string(), + amount: Uint128::new(1000u128) + }], + timeout_fee: vec![cosmwasm_std::Coin { + denom: NEUTRON_DENOM.to_string(), + amount: Uint128::new(1000u128) + }], + }; + let port_id = IBC_PORT_ID.load(deps.storage)?; + + let interchain_account = INTERCHAIN_ACCOUNTS.load(deps.storage, port_id.clone())?; + + match interchain_account { + Some((address, controller_conn_id)) => { + let stride_receiver = STRIDE_ATOM_RECEIVER.load(deps.storage)?; + let gaia_stride_channel: String = GAIA_STRIDE_IBC_TRANSFER_CHANNEL_ID.load(deps.storage)?; + + let amount = String::from(stride_receiver.amount.to_string()); + let st_ica = String::from(stride_receiver.address.to_string()); + + let coin = Coin { + denom: ATOM_DENOM.to_string(), + amount, + }; + + let autopilot_receiver = format!("{{\"autopilot\": {{\"receiver\": {st_ica},\"stakeibc\": {{\"stride_address\": {st_ica},\"action\": \"LiquidStake\"}}}}}}"); + + let stride_msg = MsgTransfer { + source_port: "transfer".to_string(), + source_channel: gaia_stride_channel, + token: Some(coin), + sender: address.clone(), + receiver: autopilot_receiver.to_string(), + timeout_height: Some(Height { + revision_number: 3, + revision_height: 800, + }), + timeout_timestamp: 0, + }; + + // Serialize the Transfer message + let mut buf = Vec::new(); + buf.reserve(stride_msg.encoded_len()); + if let Err(e) = stride_msg.encode(&mut buf) { + return Err(StdError::generic_err(format!("Encode error: {}", e)).into()); + } + + let protobuf = ProtobufAny { + type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), + value: Binary::from(buf), + }; + + let stride_submit_msg = NeutronMsg::submit_tx( + controller_conn_id, + INTERCHAIN_ACCOUNT_ID.to_string(), + vec![protobuf], + "".to_string(), + 100000, + fee + ); + + CONTRACT_STATE.save(deps.storage, &ContractState::LiquidStaked)?; + + Ok(Response::default() + .add_submessage(SubMsg::new(stride_submit_msg)) + ) + }, + None => return Err(NeutronError::Std(StdError::NotFound { kind: "no ica found".to_string() })), + } +} + + fn try_receive_atom_from_ica( deps: DepsMut, env: Env, @@ -141,7 +222,6 @@ fn try_receive_atom_from_ica( let source_channel = GAIA_NEUTRON_IBC_TRANSFER_CHANNEL_ID.load(deps.storage)?; let lp_receiver = NATIVE_ATOM_RECEIVER.load(deps.storage)?; let amount = String::from(lp_receiver.amount.to_string()); - // let receiver = String::from(lp_receiver.address.to_string()); let coin = Coin { denom: ATOM_DENOM.to_string(), @@ -151,11 +231,9 @@ fn try_receive_atom_from_ica( let msg = MsgTransfer { source_port: "transfer".to_string(), source_channel: source_channel, - token: Some(coin), + token: Some(coin.clone()), sender: address.clone(), - // receiver: String::from(lp_receiver.address.to_string()), receiver: env.contract.address.to_string(), - // TODO: look into what the timeout_height should be timeout_height: Some(Height { revision_number: 2, revision_height: 500, @@ -176,14 +254,16 @@ fn try_receive_atom_from_ica( }; let submit_msg = NeutronMsg::submit_tx( - controller_conn_id, + controller_conn_id.clone(), INTERCHAIN_ACCOUNT_ID.to_string(), vec![protobuf], - address, + address.clone(), 100000, - fee + fee.clone(), ); + CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; + Ok(Response::default() .add_submessage(SubMsg::new(submit_msg)) ) @@ -197,10 +277,9 @@ fn try_register_gaia_ica( env: Env, ) -> NeutronResult> { let gaia_acc_id = INTERCHAIN_ACCOUNT_ID.to_string(); - // let ibc_connection_id = String::from("connection-0"); - let ics_connection_id = ICS_CONNECTION_ID.to_string(); + let connection_id = NEUTRON_GAIA_CONNECTION_ID.load(deps.storage)?; let register = NeutronMsg::register_interchain_account( - ics_connection_id, + connection_id, gaia_acc_id.clone() ); let key = get_port_id(env.contract.address.as_str(), &gaia_acc_id); @@ -212,139 +291,6 @@ fn try_register_gaia_ica( Ok(Response::new().add_message(register)) } -fn try_execute_transfers( - mut deps: DepsMut, - env: Env, - _info: MessageInfo, - gaia_account_address: String -) -> NeutronResult> { - // validate that tick was triggered by the authorized clock - // validate whether ICA has enough atom? - - let stride_atom_receiver = STRIDE_ATOM_RECEIVER.load(deps.branch().storage)?; - let native_atom_receiver = NATIVE_ATOM_RECEIVER.load(deps.branch().storage)?; - - // match bal { - // Ok(coin) => { - // validate depositor ICA has enough atoms to perform both transfers? - - // 1. transfer 1/2 of atoms to liquid-staker module - // 2. transfer 1/2 of atoms from ICA to liquidity-pooler module - - // let fee = min_ntrn_ibc_fee(query_min_ibc_fee(deps.as_ref())?.min_fee); - let _neutron_coin = Coin { - denom: NEUTRON_DENOM.to_string(), - amount: 1000u128.to_string(), - }; - let fee = IbcFee { - recv_fee: vec![], // must be empty - ack_fee: vec![cosmwasm_std::Coin { denom: NEUTRON_DENOM.to_string(), amount: Uint128::new(1000u128) }], - timeout_fee: vec![cosmwasm_std::Coin { denom: NEUTRON_DENOM.to_string(), amount: Uint128::new(1000u128) }], - }; - - let ls_coin = Coin { - denom: ATOM_DENOM.to_string(), - amount: stride_atom_receiver.amount.to_string(), - }; - let lp_coin = Coin { - denom: ATOM_DENOM.to_string(), - amount: native_atom_receiver.amount.to_string(), - }; - - let ls_msg = MsgTransfer { - source_port: TRANSFER_PORT.to_string(), - source_channel: "channel-0".to_string(), - token: Some(ls_coin), - sender: gaia_account_address.clone(), - receiver: stride_atom_receiver.address, - timeout_height: None, - timeout_timestamp: 0, - }; - - let lp_msg = MsgTransfer { - source_port: TRANSFER_PORT.to_string(), - source_channel: "channel-0".to_string(), - token: Some(lp_coin), - sender: gaia_account_address.clone(), - receiver: native_atom_receiver.address, - timeout_height: None, - timeout_timestamp: 0, - }; - - // Serialize the Transfer messages - let mut ls_buf = Vec::new(); - ls_buf.reserve(ls_msg.encoded_len()); - - if let Err(e) = ls_msg.encode(&mut ls_buf) { - return Err(StdError::generic_err(format!("Encode error: {}", e)).into()); - } - - let mut lp_buf = Vec::new(); - lp_buf.reserve(lp_msg.encoded_len()); - - if let Err(e) = lp_msg.encode(&mut lp_buf) { - return Err(StdError::generic_err(format!("Encode error: {}", e)).into()); - } - - let ls_protobuf = ProtobufAny { - type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), - value: Binary::from(ls_buf), - }; - let lp_protobuf = ProtobufAny { - type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), - value: Binary::from(lp_buf), - }; - - let ls_cosmos_msg = NeutronMsg::submit_tx( - IBC_CONNECTION.to_string(), - INTERCHAIN_ACCOUNT_ID.to_string(), - vec![ls_protobuf], - "".to_string(), - DEFAULT_TIMEOUT_SECONDS, - fee.clone() - ); - let lp_cosmos_msg = NeutronMsg::submit_tx( - IBC_CONNECTION.to_string(), - INTERCHAIN_ACCOUNT_ID.to_string(), - vec![lp_protobuf], - "".to_string(), - DEFAULT_TIMEOUT_SECONDS, - fee - ); - - let ls_submsg = msg_with_sudo_callback( - deps.branch(), - ls_cosmos_msg, - SudoPayload { - port_id: get_port_id( - env.contract.address.to_string(), - INTERCHAIN_ACCOUNT_ID.to_string(), - ), - // Here you can store some information about the transaction to help you parse - // the acknowledgement later. - message: "ls transfer".to_string(), - }, - )?; - - let lp_submsg = msg_with_sudo_callback( - deps, - lp_cosmos_msg, - SudoPayload { - port_id: get_port_id( - env.contract.address.to_string(), - INTERCHAIN_ACCOUNT_ID.to_string() - ), - // Here you can store some information about the transaction to help you parse - // the acknowledgement later. - message: "lp transfer".to_string(), - }, - )?; - - Ok(Response::default() - .add_submessages(vec![ls_submsg, lp_submsg]) - ) -} - fn msg_with_sudo_callback>, T>( deps: DepsMut, msg: C, diff --git a/contracts/depositor/src/msg.rs b/contracts/depositor/src/msg.rs index 9d659d03..a1deab65 100644 --- a/contracts/depositor/src/msg.rs +++ b/contracts/depositor/src/msg.rs @@ -8,6 +8,8 @@ pub struct InstantiateMsg { pub atom_receiver: WeightedReceiver, pub clock_address: String, pub gaia_neutron_ibc_transfer_channel_id: String, + pub neutron_gaia_connection_id: String, + pub gaia_stride_ibc_transfer_channel_id: String, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] diff --git a/contracts/depositor/src/state.rs b/contracts/depositor/src/state.rs index 74bfdff0..01422b04 100644 --- a/contracts/depositor/src/state.rs +++ b/contracts/depositor/src/state.rs @@ -17,6 +17,9 @@ pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); pub const LP_ADDRESS: Item = Item::new("lp_address"); // the ibc transfer channel pub const GAIA_NEUTRON_IBC_TRANSFER_CHANNEL_ID: Item = Item::new("gn_ibc_chann_id"); +pub const GAIA_STRIDE_IBC_TRANSFER_CHANNEL_ID: Item = Item::new("gs_ibc_chan_id"); + +pub const NEUTRON_GAIA_CONNECTION_ID: Item = Item::new("ng_conn_id"); pub const ICA_ADDRESS: Item = Item::new("ica_address"); // ICA @@ -29,7 +32,7 @@ pub const IBC_PORT_ID: Item = Item::new("ibc_port_id"); pub enum ContractState { Instantiated, ICACreated, - ReceivedFunds, + LiquidStaked, Complete, } diff --git a/contracts/depositor/src/tests/suite.rs b/contracts/depositor/src/tests/suite.rs index 75c80dcf..67c46042 100644 --- a/contracts/depositor/src/tests/suite.rs +++ b/contracts/depositor/src/tests/suite.rs @@ -1,27 +1,39 @@ -use cosmwasm_std::{Addr, Empty, Uint128}; -use cw_multi_test::{App, Contract, ContractWrapper, Executor}; +use cosmwasm_std::{Addr, Uint128, Empty}; +use cw_multi_test::{App, Executor, Contract, ContractWrapper}; +use neutron_sdk::{bindings::query::NeutronQuery, NeutronError, NeutronResult}; -use crate::msg::{InstantiateMsg, QueryMsg, WeightedReceiver}; +use crate::{msg::{InstantiateMsg, QueryMsg, WeightedReceiver}, contract::{query, execute, instantiate}}; pub const CREATOR_ADDR: &str = "creator"; pub const ST_ATOM_DENOM: &str = "stride-atom"; pub const NATIVE_ATOM_DENOM: &str = "native-atom"; -pub const DEFAULT_RECEIVER_AMOUNT: Uint128 = Uint128::new(10); -pub const DEFAULT_CLOCK_ADDRESS: &str = "clock-address"; +pub const _DEFAULT_RECEIVER_AMOUNT: Uint128 = Uint128::new(10); +pub const _DEFAULT_CLOCK_ADDRESS: &str = "clock-address"; + +// pub fn mock_dependencies() -> OwnedDeps { +// OwnedDeps { +// storage: MockStorage::default(), +// api: MockApi::default(), +// querier: MockQuerier::default(), +// custom_query_type: PhantomData, +// } +// } -// fn depositor_contract() -> Box> { -// let contract = ContractWrapper::new( -// crate::contract::execute, -// crate::contract::instantiate, -// crate::contract::query, -// ); -// // todo -// Box::new(contract) +// fn depositor_contract() -> Box>> { +// // let contract = ContractWrapper::new( +// // crate::contract::execute, +// // crate::contract::instantiate, +// // query, +// // ); +// // // todo +// // Box::new(contract) +// let execute_func = +// Box::new(ContractWrapper::new(execute_fn, instantiate_fn, query_fn)) // } pub(crate) struct Suite { - app: App, - pub _admin: Addr, + pub app: App, + pub admin: Addr, pub depositor_address: Addr, pub depositor_code: u64, } @@ -30,8 +42,6 @@ pub(crate) struct SuiteBuilder { pub instantiate: InstantiateMsg, } - - impl Default for SuiteBuilder { fn default() -> Self { Self { @@ -46,6 +56,8 @@ impl Default for SuiteBuilder { }, clock_address: "default-clock".to_string(), gaia_neutron_ibc_transfer_channel_id: "channel-3".to_string(), + neutron_gaia_connection_id: "connection-0".to_string(), + gaia_stride_ibc_transfer_channel_id: "channel-3".to_string(), }, } } @@ -54,7 +66,7 @@ impl Default for SuiteBuilder { impl SuiteBuilder { pub fn build(self) -> Suite { let mut app = App::default(); - + // app.store_code() // let depositor_code = app.store_code(depositor_contract()); let depositor_code = 1; let depositor_address = app @@ -70,7 +82,7 @@ impl SuiteBuilder { Suite { app, - _admin: Addr::unchecked(CREATOR_ADDR), + admin: Addr::unchecked(CREATOR_ADDR), depositor_address, depositor_code, } diff --git a/contracts/depositor/src/tests/tests.rs b/contracts/depositor/src/tests/tests.rs index e9addb31..c3d05068 100644 --- a/contracts/depositor/src/tests/tests.rs +++ b/contracts/depositor/src/tests/tests.rs @@ -1,11 +1,9 @@ -use cosmwasm_std::Addr; - -use super::suite::{SuiteBuilder, DEFAULT_CLOCK_ADDRESS, DEFAULT_RECEIVER_AMOUNT, NATIVE_ATOM_DENOM, ST_ATOM_DENOM}; +use super::suite::{SuiteBuilder}; #[test] fn test_instantiate_happy() { - let suite = SuiteBuilder::default() + let _suite = SuiteBuilder::default() .build(); // suite.assert_clock_address(Addr::unchecked(DEFAULT_CLOCK_ADDRESS)); diff --git a/contracts/lper/src/contract.rs b/contracts/lper/src/contract.rs index 8acae8c6..fa62a7dc 100644 --- a/contracts/lper/src/contract.rs +++ b/contracts/lper/src/contract.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{MessageInfo, Response, - StdResult, Addr, DepsMut, Env, Binary, Deps, to_binary, SubMsg, WasmMsg, CosmosMsg, Coin, Uint128, Reply, + StdResult, Addr, DepsMut, Env, Binary, Deps, to_binary, WasmMsg, CosmosMsg, Coin, Uint128, }; use cw2::set_contract_version; @@ -47,7 +47,7 @@ pub fn instantiate( #[entry_point] pub fn execute( - mut deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, @@ -79,7 +79,7 @@ fn no_op() -> NeutronResult> { fn try_enter_lp_position( deps: DepsMut, env: Env, - info: MessageInfo, + _info: MessageInfo, ) -> NeutronResult> { let pool_address = LP_POSITION.load(deps.storage)?; @@ -119,7 +119,7 @@ fn try_enter_lp_position( fn try_withdraw( deps: DepsMut, env: Env, - info: MessageInfo, + _info: MessageInfo, ) -> NeutronResult> { let pool_address = LP_POSITION.load(deps.storage)?; // todo @@ -148,7 +148,7 @@ fn try_withdraw( } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult { +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> NeutronResult { match msg { QueryMsg::ClockAddress {} => Ok( to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)? diff --git a/contracts/ls/.cargo/config b/contracts/ls/.cargo/config new file mode 100644 index 00000000..5f6aa466 --- /dev/null +++ b/contracts/ls/.cargo/config @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +schema = "run --bin schema" diff --git a/contracts/ls/Cargo.toml b/contracts/ls/Cargo.toml new file mode 100644 index 00000000..3aadaed6 --- /dev/null +++ b/contracts/ls/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "covenant-ls" +authors = ["benskey bekauz@protonmail.com"] +description = "Liquid Staker module for stride covenant" +edition = { workspace = true } +license = { workspace = true } +rust-version = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# disables #[entry_point] (i.e. instantiate/execute/query) export +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +thiserror = { workspace = true } +schemars = { workspace = true } +serde-json-wasm = { workspace = true } +serde = { workspace = true } +neutron-sdk = { workspace = true } +cosmos-sdk-proto = { workspace = true } +protobuf = { workspace = true } \ No newline at end of file diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs new file mode 100644 index 00000000..072d9d0f --- /dev/null +++ b/contracts/ls/src/contract.rs @@ -0,0 +1,616 @@ +use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; +use cosmos_sdk_proto::ibc::applications::transfer::v1::MsgTransfer; +use cosmos_sdk_proto::ibc::core::client::v1::Height; +use cosmos_sdk_proto::traits::Message; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_binary, Binary, CosmosMsg, CustomQuery, Deps, DepsMut, Env, MessageInfo, Reply, Response, + StdError, StdResult, SubMsg, Addr, Uint128, +}; +use cw2::set_contract_version; +use neutron_sdk::bindings::msg::IbcFee; +use neutron_sdk::bindings::types::ProtobufAny; +use neutron_sdk::interchain_queries::v045::new_register_transfers_query_msg; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, MigrateMsg}; +use crate::state::{CLOCK_ADDRESS, CONTRACT_STATE, STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID, LP_ADDRESS, ContractState, ICA_ADDRESS, INTERCHAIN_ACCOUNTS, IBC_PORT_ID, add_error_to_queue, AcknowledgementResult, ACKNOWLEDGEMENT_RESULTS, read_sudo_payload, save_sudo_payload, read_reply_payload, read_errors_from_queue, SUDO_PAYLOAD_REPLY_ID, save_reply_payload, SudoPayload, NEUTRON_STRIDE_IBC_CONNECTION_ID, LS_DENOM}; +use neutron_sdk::{ + bindings::{ + msg::{MsgSubmitTxResponse, NeutronMsg}, + query::{NeutronQuery, QueryInterchainAccountAddressResponse}, + }, + interchain_txs::helpers::{ + decode_acknowledgement_response, get_port_id, + }, + sudo::msg::{RequestPacket, SudoMsg}, + NeutronError, NeutronResult, +}; + +// Default timeout for SubmitTX is two weeks +const DEFAULT_TIMEOUT_SECONDS: u64 = 60 * 60 * 24 * 7 * 2; +// const DEFAULT_TIMEOUT_HEIGHT: u64 = 10000000; +const NEUTRON_DENOM: &str = "untrn"; +const ATOM_DENOM: &str = "uatom"; +const STATOM_DENOM: &str = "stuatom"; + +const INTERCHAIN_ACCOUNT_ID: &str = "stride-ica"; +const TRANSFER_PORT: &str = "transfer"; + +const CONTRACT_NAME: &str = "crates.io:covenant-ls"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +struct OpenAckVersion { + version: String, + controller_connection_id: String, + host_connection_id: String, + address: String, + encoding: String, + tx_type: String, +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> NeutronResult> { + deps.api.debug("WASMDEBUG: instantiate"); + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // TODO: validations + + CLOCK_ADDRESS.save(deps.storage, &Addr::unchecked(msg.clock_address))?; + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID.save(deps.storage, &msg.stride_neutron_ibc_transfer_channel_id)?; + LP_ADDRESS.save(deps.storage, &msg.lp_address)?; + NEUTRON_STRIDE_IBC_CONNECTION_ID.save(deps.storage, &msg.neutron_stride_ibc_connection_id)?; + LS_DENOM.save(deps.storage, &msg.ls_denom)?; + + Ok(Response::default()) +} + +#[entry_point] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> NeutronResult> { + deps.api + .debug(format!("WASMDEBUG: execute: received msg: {:?}", msg).as_str()); + match msg { + ExecuteMsg::Tick { } => try_tick(deps, env, info), + ExecuteMsg::Received { } => try_handle_received(), + } +} + + +fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> NeutronResult> { + let current_state = CONTRACT_STATE.load(deps.storage)?; + let ica_address: Result = ICA_ADDRESS.load(deps.storage); + let gaia_account_address = match ica_address { + Ok(addr) => addr, + Err(_) => "todo".to_string(), + }; + // TODO: validate caller is clock + + match current_state { + ContractState::Instantiated => try_register_stride_ica(deps, env), + ContractState::ICACreated => try_execute_transfer(deps, env, info), + ContractState::Complete => Ok(Response::default()), + } +} + +fn try_register_stride_ica( + deps: DepsMut, + env: Env, +) -> NeutronResult> { + let stride_acc_id = INTERCHAIN_ACCOUNT_ID.to_string(); + let connection_id = NEUTRON_STRIDE_IBC_CONNECTION_ID.load(deps.storage)?; + let register = NeutronMsg::register_interchain_account( + connection_id, + stride_acc_id.clone() + ); + let key = get_port_id(env.contract.address.as_str(), &stride_acc_id); + IBC_PORT_ID.save(deps.storage, &key)?; + + // we are saving empty data here because we handle response of registering ICA in sudo_open_ack method + INTERCHAIN_ACCOUNTS.save(deps.storage, key, &None)?; + + Ok(Response::new().add_message(register)) +} + +fn try_execute_transfer( + mut deps: DepsMut, + env: Env, + _info: MessageInfo, +) -> NeutronResult> { + + let fee = IbcFee { + recv_fee: vec![], // must be empty + ack_fee: vec![cosmwasm_std::Coin { + denom: NEUTRON_DENOM.to_string(), + amount: Uint128::new(1000u128) + }], + timeout_fee: vec![cosmwasm_std::Coin { + denom: NEUTRON_DENOM.to_string(), + amount: Uint128::new(1000u128) + }], + }; + + let port_id = IBC_PORT_ID.load(deps.storage)?; + let interchain_account = INTERCHAIN_ACCOUNTS.load(deps.storage, port_id.clone())?; + + match interchain_account { + Some((address, controller_conn_id)) => { + + let source_channel = STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID.load(deps.storage)?; + let lp_receiver = LP_ADDRESS.load(deps.storage)?; + + let coin = Coin { + denom: STATOM_DENOM.to_string(), + amount: "10".to_string(), + }; + + let msg = MsgTransfer { + source_port: "transfer".to_string(), + source_channel, + token: Some(coin.clone()), + sender: address.clone(), + receiver: lp_receiver.clone(), + timeout_height: Some(Height { + revision_number: 2, + revision_height: 800, + }), + timeout_timestamp: 0, + }; + + // Serialize the Transfer message + let mut buf = Vec::new(); + buf.reserve(msg.encoded_len()); + if let Err(e) = msg.encode(&mut buf) { + return Err(StdError::generic_err(format!("Encode error: {}", e)).into()); + } + + let protobuf = ProtobufAny { + type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), + value: Binary::from(buf), + }; + + let submit_msg = NeutronMsg::submit_tx( + controller_conn_id, + INTERCHAIN_ACCOUNT_ID.to_string(), + vec![protobuf], + lp_receiver.to_string(), + 100000, + fee, + ); + + CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; + + Ok(Response::default() + .add_submessage(SubMsg::new(submit_msg)) + ) + }, + None => return Err(NeutronError::Std(StdError::NotFound { kind: "no ica found".to_string() })), + } +} + +fn msg_with_sudo_callback>, T>( + deps: DepsMut, + msg: C, + payload: SudoPayload, +) -> StdResult> { + save_reply_payload(deps.storage, payload)?; + Ok(SubMsg::reply_on_success(msg, SUDO_PAYLOAD_REPLY_ID)) +} + +pub fn register_transfers_query( + connection_id: String, + recipient: String, + update_period: u64, + min_height: Option, +) -> NeutronResult> { + let msg = + new_register_transfers_query_msg(connection_id, recipient, update_period, min_height)?; + + Ok(Response::new().add_message(msg)) +} + +fn try_handle_received() -> NeutronResult> { + + Ok(Response::default().add_attribute("try_handle_received", "received msg`")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult { + match msg { + QueryMsg::LpAddress {} => Ok( + to_binary(&LP_ADDRESS.may_load(deps.storage)?)? + ), + QueryMsg::ClockAddress {} => Ok( + to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)? + ), + QueryMsg::InterchainAccountAddress { + interchain_account_id, + connection_id, + } => query_interchain_address(deps, env, interchain_account_id, connection_id), + } +} + +// returns ICA address from Neutron ICA SDK module +pub fn query_interchain_address( + deps: Deps, + env: Env, + interchain_account_id: String, + connection_id: String, +) -> NeutronResult { + let query = NeutronQuery::InterchainAccountAddress { + owner_address: env.contract.address.to_string(), + interchain_account_id, + connection_id, + }; + + let res: QueryInterchainAccountAddressResponse = deps.querier.query(&query.into())?; + Ok(to_binary(&res)?) +} + + +pub fn query_ls_interchain_address( + deps: Deps, + _env: Env, +) -> NeutronResult { + let addr = ICA_ADDRESS.load(deps.storage); + + match addr { + Ok(val) => { + let address_response = QueryInterchainAccountAddressResponse { + interchain_account_address: val, + }; + Ok(to_binary(&address_response)?) + }, + Err(_) => Err(NeutronError::Std(StdError::not_found("no ica stored"))), + } +} + +// returns ICA address from the contract storage. The address was saved in sudo_open_ack method +pub fn query_interchain_address_contract( + deps: Deps, + env: Env, + interchain_account_id: String, +) -> NeutronResult { + Ok(to_binary(&get_ica(deps, &env, &interchain_account_id)?)?) +} + +// returns the result +pub fn query_acknowledgement_result( + deps: Deps, + env: Env, + interchain_account_id: String, + sequence_id: u64, +) -> NeutronResult { + let port_id = get_port_id(env.contract.address.as_str(), &interchain_account_id); + let res = ACKNOWLEDGEMENT_RESULTS.may_load(deps.storage, (port_id, sequence_id))?; + Ok(to_binary(&res)?) +} + +pub fn query_errors_queue(deps: Deps) -> NeutronResult { + let res = read_errors_from_queue(deps.storage)?; + Ok(to_binary(&res)?) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo: received sudo msg: {:?}", msg).as_str()); + + match msg { + // For handling successful (non-error) acknowledgements. + SudoMsg::Response { request, data } => sudo_response(deps, request, data), + + // For handling error acknowledgements. + SudoMsg::Error { request, details } => sudo_error(deps, request, details), + + // For handling error timeouts. + SudoMsg::Timeout { request } => sudo_timeout(deps, env, request), + + // For handling successful registering of ICA + SudoMsg::OpenAck { + port_id, + channel_id, + counterparty_channel_id, + counterparty_version, + } => sudo_open_ack( + deps, + env, + port_id, + channel_id, + counterparty_channel_id, + counterparty_version, + ), + _ => Ok(Response::default()), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult { + deps.api.debug("WASMDEBUG: migrate"); + Ok(Response::default()) +} + +// handler +fn sudo_open_ack( + deps: DepsMut, + _env: Env, + port_id: String, + channel_id: String, + counterparty_channel_id: String, + counterparty_version: String, +) -> StdResult { + // The version variable contains a JSON value with multiple fields, + // including the generated account address. + let parsed_version: Result = + serde_json_wasm::from_str(counterparty_version.as_str()); + + // Update the storage record associated with the interchain account. + if let Ok(parsed_version) = parsed_version { + INTERCHAIN_ACCOUNTS.save( + deps.storage, + port_id, + &Some(( + parsed_version.clone().address, + parsed_version.clone().controller_connection_id, + )), + )?; + ICA_ADDRESS.save(deps.storage, &parsed_version.address)?; + CONTRACT_STATE.save(deps.storage, &ContractState::ICACreated)?; + return Ok(Response::default()); + } + Err(StdError::generic_err("Can't parse counterparty_version")) +} + +fn sudo_response(deps: DepsMut, request: RequestPacket, data: Binary) -> StdResult { + deps.api.debug( + format!( + "WASMDEBUG: sudo_response: sudo received: {:?} {:?}", + request, data + ) + .as_str(), + ); + + // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. + // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE + // FOR LATER INSPECTION. + // In this particular case, we return an error because not having the sequence id + // in the request value implies that a fatal error occurred on Neutron side. + let seq_id = request + .sequence + .ok_or_else(|| StdError::generic_err("sequence not found"))?; + + // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. + // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE + // FOR LATER INSPECTION. + // In this particular case, we return an error because not having the sequence id + // in the request value implies that a fatal error occurred on Neutron side. + let channel_id = request + .source_channel + .ok_or_else(|| StdError::generic_err("channel_id not found"))?; + + // NOTE: NO ERROR IS RETURNED HERE. THE CHANNEL LIVES ON. + // In this particular example, this is a matter of developer's choice. Not being able to read + // the payload here means that there was a problem with the contract while submitting an + // interchain transaction. You can decide that this is not worth killing the channel, + // write an error log and / or save the acknowledgement to an errors queue for later manual + // processing. The decision is based purely on your application logic. + let payload = read_sudo_payload(deps.storage, channel_id, seq_id).ok(); + if payload.is_none() { + let error_msg = "WASMDEBUG: Error: Unable to read sudo payload"; + deps.api.debug(error_msg); + add_error_to_queue(deps.storage, error_msg.to_string()); + return Ok(Response::default()); + } + + deps.api + .debug(format!("WASMDEBUG: sudo_response: sudo payload: {:?}", payload).as_str()); + + // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. + // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE + // FOR LATER INSPECTION. + // In this particular case, we return an error because not being able to parse this data + // that a fatal error occurred on Neutron side, or that the remote chain sent us unexpected data. + // Both cases require immediate attention. + let parsed_data = decode_acknowledgement_response(data)?; + + let mut item_types = vec![]; + for item in parsed_data { + let item_type = item.msg_type.as_str(); + item_types.push(item_type.to_string()); + match item_type { + "/ibc.applications.transfer.v1.MsgTransfer" => { + deps.api.debug( + format!("MsgTransfer response: {:?}", item.data).as_str() + ); + }, + _ => { + deps.api.debug( + format!( + "This type of acknowledgement is not implemented: {:?}", + payload + ) + .as_str(), + ); + } + } + } + + if let Some(payload) = payload { + // update but also check that we don't update same seq_id twice + ACKNOWLEDGEMENT_RESULTS.update( + deps.storage, + (payload.port_id, seq_id), + |maybe_ack| -> StdResult { + match maybe_ack { + Some(_ack) => Err(StdError::generic_err("trying to update same seq_id")), + None => Ok(AcknowledgementResult::Success(item_types)), + } + }, + )?; + } + + Ok(Response::default()) +} + +fn sudo_timeout(deps: DepsMut, _env: Env, request: RequestPacket) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo timeout request: {:?}", request).as_str()); + + // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. + // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE + // FOR LATER INSPECTION. + // In this particular case, we return an error because not having the sequence id + // in the request value implies that a fatal error occurred on Neutron side. + let seq_id = request + .sequence + .ok_or_else(|| StdError::generic_err("sequence not found"))?; + + // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. + // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE + // FOR LATER INSPECTION. + // In this particular case, we return an error because not having the sequence id + // in the request value implies that a fatal error occurred on Neutron side. + let channel_id = request + .source_channel + .ok_or_else(|| StdError::generic_err("channel_id not found"))?; + + // update but also check that we don't update same seq_id twice + // NOTE: NO ERROR IS RETURNED HERE. THE CHANNEL LIVES ON. + // In this particular example, this is a matter of developer's choice. Not being able to read + // the payload here means that there was a problem with the contract while submitting an + // interchain transaction. You can decide that this is not worth killing the channel, + // write an error log and / or save the acknowledgement to an errors queue for later manual + // processing. The decision is based purely on your application logic. + // Please be careful because it may lead to an unexpected state changes because state might + // has been changed before this call and will not be reverted because of supressed error. + let payload = read_sudo_payload(deps.storage, channel_id, seq_id).ok(); + if let Some(payload) = payload { + // update but also check that we don't update same seq_id twice + ACKNOWLEDGEMENT_RESULTS.update( + deps.storage, + (payload.port_id, seq_id), + |maybe_ack| -> StdResult { + match maybe_ack { + Some(_ack) => Err(StdError::generic_err("trying to update same seq_id")), + None => Ok(AcknowledgementResult::Timeout(payload.message)), + } + }, + )?; + } else { + let error_msg = "WASMDEBUG: Error: Unable to read sudo payload"; + deps.api.debug(error_msg); + add_error_to_queue(deps.storage, error_msg.to_string()); + } + + Ok(Response::default()) +} + +fn sudo_error(deps: DepsMut, request: RequestPacket, details: String) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo error: {}", details).as_str()); + deps.api + .debug(format!("WASMDEBUG: request packet: {:?}", request).as_str()); + + // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. + // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE + // FOR LATER INSPECTION. + // In this particular case, we return an error because not having the sequence id + // in the request value implies that a fatal error occurred on Neutron side. + let seq_id = request + .sequence + .ok_or_else(|| StdError::generic_err("sequence not found"))?; + + // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. + // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE + // FOR LATER INSPECTION. + // In this particular case, we return an error because not having the sequence id + // in the request value implies that a fatal error occurred on Neutron side. + let channel_id = request + .source_channel + .ok_or_else(|| StdError::generic_err("channel_id not found"))?; + let payload = read_sudo_payload(deps.storage, channel_id, seq_id).ok(); + + if let Some(payload) = payload { + // update but also check that we don't update same seq_id twice + ACKNOWLEDGEMENT_RESULTS.update( + deps.storage, + (payload.port_id, seq_id), + |maybe_ack| -> StdResult { + match maybe_ack { + Some(_ack) => Err(StdError::generic_err("trying to update same seq_id")), + None => Ok(AcknowledgementResult::Error((payload.message, details))), + } + }, + )?; + } else { + let error_msg = "WASMDEBUG: Error: Unable to read sudo payload"; + deps.api.debug(error_msg); + add_error_to_queue(deps.storage, error_msg.to_string()); + } + + Ok(Response::default()) +} + +// prepare_sudo_payload is called from reply handler +// The method is used to extract sequence id and channel from SubmitTxResponse to process sudo payload defined in msg_with_sudo_callback later in Sudo handler. +// Such flow msg_with_sudo_callback() -> reply() -> prepare_sudo_payload() -> sudo() allows you "attach" some payload to your SubmitTx message +// and process this payload when an acknowledgement for the SubmitTx message is received in Sudo handler +fn prepare_sudo_payload(mut deps: DepsMut, _env: Env, msg: Reply) -> StdResult { + let payload = read_reply_payload(deps.storage)?; + let resp: MsgSubmitTxResponse = serde_json_wasm::from_slice( + msg.result + .into_result() + .map_err(StdError::generic_err)? + .data + .ok_or_else(|| StdError::generic_err("no result"))? + .as_slice(), + ) + .map_err(|e| StdError::generic_err(format!("failed to parse response: {:?}", e)))?; + deps.api + .debug(format!("WASMDEBUG: reply msg: {:?}", resp).as_str()); + let seq_id = resp.sequence_id; + let channel_id = resp.channel; + save_sudo_payload(deps.branch().storage, channel_id, seq_id, payload)?; + Ok(Response::new()) +} + +fn get_ica( + deps: Deps, + env: &Env, + interchain_account_id: &str, +) -> Result<(String, String), StdError> { + let key = get_port_id(env.contract.address.as_str(), interchain_account_id); + + INTERCHAIN_ACCOUNTS + .load(deps.storage, key)? + .ok_or_else(|| StdError::generic_err("Interchain account is not created yet")) +} + +#[entry_point] +pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: reply msg: {:?}", msg).as_str()); + match msg.id { + SUDO_PAYLOAD_REPLY_ID => prepare_sudo_payload(deps, env, msg), + _ => Err(StdError::generic_err(format!( + "unsupported reply message id {}", + msg.id + ))), + } +} + \ No newline at end of file diff --git a/contracts/ls/src/error.rs b/contracts/ls/src/error.rs new file mode 100644 index 00000000..e69de29b diff --git a/contracts/ls/src/lib.rs b/contracts/ls/src/lib.rs new file mode 100644 index 00000000..878ef0ed --- /dev/null +++ b/contracts/ls/src/lib.rs @@ -0,0 +1,12 @@ +#![warn(clippy::unwrap_used, clippy::expect_used)] + +extern crate core; + +pub mod contract; +pub mod error; +pub mod msg; +pub mod state; + +#[allow(clippy::unwrap_used)] +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/contracts/ls/src/msg.rs b/contracts/ls/src/msg.rs new file mode 100644 index 00000000..75cb89d2 --- /dev/null +++ b/contracts/ls/src/msg.rs @@ -0,0 +1,35 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct InstantiateMsg { + pub autopilot_position: String, + pub clock_address: String, + pub stride_neutron_ibc_transfer_channel_id: String, + pub neutron_stride_ibc_connection_id: String, + pub lp_address: String, + pub ls_denom: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + Tick {}, + Received {}, +} + + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + ClockAddress {}, + InterchainAccountAddress { + interchain_account_id: String, + connection_id: String, + }, + LpAddress {}, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct MigrateMsg {} \ No newline at end of file diff --git a/contracts/ls/src/state.rs b/contracts/ls/src/state.rs new file mode 100644 index 00000000..103365c1 --- /dev/null +++ b/contracts/ls/src/state.rs @@ -0,0 +1,103 @@ +use cosmwasm_std::{Addr, Storage, StdResult, from_binary, Binary, Order, to_vec}; +use cw_storage_plus::{Item, Map}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +// store the clock address to verify calls +pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); + +// the ibc transfer channel +pub const STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID: Item = Item::new("sn_ibc_chann_id"); +pub const NEUTRON_STRIDE_IBC_CONNECTION_ID: Item = Item::new("ns_ibc_conn_id"); +pub const LP_ADDRESS: Item = Item::new("lp_address"); +pub const ICA_ADDRESS: Item = Item::new("ica_address"); +pub const LS_DENOM: Item = Item::new("ls_denom"); + +// ICA +pub const INTERCHAIN_ACCOUNTS: Map> = + Map::new("interchain_accounts"); +pub const IBC_PORT_ID: Item = Item::new("ibc_port_id"); + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ContractState { + Instantiated, + ICACreated, + Complete, +} + +pub const CONTRACT_STATE: Item = Item::new("contract_state"); + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct SudoPayload { + pub message: String, + pub port_id: String, +} + +pub const SUDO_PAYLOAD_REPLY_ID: u64 = 1; + +pub const REPLY_ID_STORAGE: Item> = Item::new("reply_queue_id"); +pub const SUDO_PAYLOAD: Map<(String, u64), Vec> = Map::new("sudo_payload"); + +// interchain transaction responses - ack/err/timeout state to query later +pub const ACKNOWLEDGEMENT_RESULTS: Map<(String, u64), AcknowledgementResult> = + Map::new("acknowledgement_results"); + +pub const ERRORS_QUEUE: Map = Map::new("errors_queue"); + +/// Serves for storing acknowledgement calls for interchain transactions +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum AcknowledgementResult { + /// Success - Got success acknowledgement in sudo with array of message item types in it + Success(Vec), + /// Error - Got error acknowledgement in sudo with payload message in it and error details + Error((String, String)), + /// Timeout - Got timeout acknowledgement in sudo with payload message in it + Timeout(String), +} + +pub fn save_reply_payload(store: &mut dyn Storage, payload: SudoPayload) -> StdResult<()> { + REPLY_ID_STORAGE.save(store, &to_vec(&payload)?) +} + +pub fn read_reply_payload(store: &mut dyn Storage) -> StdResult { + let data = REPLY_ID_STORAGE.load(store)?; + from_binary(&Binary(data)) +} + +pub fn add_error_to_queue(store: &mut dyn Storage, error_msg: String) -> Option<()> { + let result = ERRORS_QUEUE + .keys(store, None, None, Order::Descending) + .next() + .and_then(|data| data.ok()) + .map(|c| c + 1) + .or(Some(0)); + + result.and_then(|idx| ERRORS_QUEUE.save(store, idx, &error_msg).ok()) +} + +pub fn read_errors_from_queue(store: &dyn Storage) -> StdResult, String)>> { + ERRORS_QUEUE + .range_raw(store, None, None, Order::Ascending) + .collect() +} + +pub fn read_sudo_payload( + store: &mut dyn Storage, + channel_id: String, + seq_id: u64, +) -> StdResult { + let data = SUDO_PAYLOAD.load(store, (channel_id, seq_id))?; + from_binary(&Binary(data)) +} + +pub fn save_sudo_payload( + store: &mut dyn Storage, + channel_id: String, + seq_id: u64, + payload: SudoPayload, +) -> StdResult<()> { + SUDO_PAYLOAD.save(store, (channel_id, seq_id), &to_vec(&payload)?) +} \ No newline at end of file diff --git a/contracts/ls/src/tests/mod.rs b/contracts/ls/src/tests/mod.rs new file mode 100644 index 00000000..b3e04424 --- /dev/null +++ b/contracts/ls/src/tests/mod.rs @@ -0,0 +1,2 @@ +mod tests; +mod suite; \ No newline at end of file diff --git a/contracts/ls/src/tests/suite.rs b/contracts/ls/src/tests/suite.rs new file mode 100644 index 00000000..e69de29b diff --git a/contracts/ls/src/tests/tests.rs b/contracts/ls/src/tests/tests.rs new file mode 100644 index 00000000..e69de29b diff --git a/stride-covenant/justfile b/stride-covenant/justfile index dde0c9a1..d29582a2 100644 --- a/stride-covenant/justfile +++ b/stride-covenant/justfile @@ -23,7 +23,8 @@ optimize: simtest: optimize if [[ $(uname -m) =~ "arm64" ]]; then \ mv ./../artifacts/covenant_depositor-aarch64.wasm ./../artifacts/covenant_depositor.wasm && \ - mv ./../artifacts/covenant_lper-aarch64.wasm ./../artifacts/covenant_lper.wasm \ + mv ./../artifacts/covenant_lper-aarch64.wasm ./../artifacts/covenant_lper.wasm && \ + mv ./../artifacts/covenant_ls-aarch64.wasm ./../artifacts/covenant_ls.wasm \ ;fi cp ./astroport/*.wasm ./../artifacts @@ -32,4 +33,7 @@ simtest: optimize cp -R ./../artifacts/*.wasm tests/interchaintest/wasms go clean -testcache - cd tests/interchaintest/ && go test -v ./... \ No newline at end of file + cd tests/interchaintest/ && go test -timeout 20m -v ./... + +ictest: + cd tests/interchaintest/ && go test -timeout 20m -v ./... \ No newline at end of file diff --git a/stride-covenant/tests/interchaintest/genesis_helpers.go b/stride-covenant/tests/interchaintest/genesis_helpers.go index c29afb1c..a347b42b 100644 --- a/stride-covenant/tests/interchaintest/genesis_helpers.go +++ b/stride-covenant/tests/interchaintest/genesis_helpers.go @@ -1,12 +1,15 @@ package ibc_test import ( + "context" "encoding/json" "fmt" "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/icza/dyno" + "github.com/strangelove-ventures/interchaintest/v3/chain/cosmos" "github.com/strangelove-ventures/interchaintest/v3/ibc" + "github.com/strangelove-ventures/interchaintest/v3/testreporter" ) // Sets custom fields for the Neutron genesis file that interchaintest isn't aware of by default. @@ -76,6 +79,30 @@ func setupGaiaGenesis(allowed_messages []string) func(ibc.ChainConfig, []byte) ( } } +func setupStrideGenesis(allowed_messages []string) func(ibc.ChainConfig, []byte) ([]byte, error) { + return func(chainConfig ibc.ChainConfig, genbz []byte) ([]byte, error) { + g := make(map[string]interface{}) + if err := json.Unmarshal(genbz, &g); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis file: %w", err) + } + + if err := dyno.Set(g, true, "app_state", "autopilot", "params", "stakeibc_active"); err != nil { + return nil, fmt.Errorf("failed to set autopilot stakeibc in genesis json: %w", err) + } + + if err := dyno.Set(g, allowed_messages, "app_state", "interchainaccounts", "host_genesis_state", "params", "allow_messages"); err != nil { + return nil, fmt.Errorf("failed to set allow_messages for interchainaccount host in genesis json: %w", err) + } + + out, err := json.Marshal(g) + if err != nil { + return nil, fmt.Errorf("failed to marshal genesis bytes to json: %w", err) + } + + return out, nil + } +} + func getCreateValidatorCmd(chain ibc.Chain) []string { // Before receiving a validator set change (VSC) packet, // consumer chains disallow bank transfers. To trigger a VSC @@ -102,3 +129,12 @@ func getCreateValidatorCmd(chain ibc.Chain) []string { return cmd } + +func getChannelMap(r ibc.Relayer, ctx context.Context, eRep *testreporter.RelayerExecReporter, + cosmosStride *cosmos.CosmosChain, cosmosNeutron *cosmos.CosmosChain, cosmosAtom *cosmos.CosmosChain) map[string]string { + channelMap := map[string]string{ + "hi": "Dog", + } + + return channelMap +} diff --git a/stride-covenant/tests/interchaintest/ics_test.go b/stride-covenant/tests/interchaintest/ics_test.go index 7e1af7ed..ef71f77f 100644 --- a/stride-covenant/tests/interchaintest/ics_test.go +++ b/stride-covenant/tests/interchaintest/ics_test.go @@ -4,7 +4,9 @@ import ( "context" "encoding/json" "fmt" + "path/filepath" "strconv" + "strings" "testing" "time" @@ -13,6 +15,7 @@ import ( ibctest "github.com/strangelove-ventures/interchaintest/v3" "github.com/strangelove-ventures/interchaintest/v3/chain/cosmos" "github.com/strangelove-ventures/interchaintest/v3/ibc" + "github.com/strangelove-ventures/interchaintest/v3/relayer" "github.com/strangelove-ventures/interchaintest/v3/relayer/rly" "github.com/strangelove-ventures/interchaintest/v3/testreporter" @@ -28,8 +31,6 @@ func TestICS(t *testing.T) { t.Skip("skipping in short mode") } - t.Parallel() - ctx := context.Background() // Chain Factory @@ -79,8 +80,10 @@ func TestICS(t *testing.T) { ChainID: "stride-3", Images: []ibc.DockerImage{ { - Repository: "ghcr.io/strangelove-ventures/heighliner/stride", - Version: "v9.2.1", + // Repository: "ghcr.io/strangelove-ventures/heighliner/stride", + // Version: "v9.2.1", + Repository: "stride", + Version: "local", UidGid: "1025:1025", }, }, @@ -91,6 +94,18 @@ func TestICS(t *testing.T) { GasAdjustment: 1.3, TrustingPeriod: "1197504s", NoHostMount: false, + ModifyGenesis: setupStrideGenesis([]string{ + "/cosmos.bank.v1beta1.MsgSend", + "/cosmos.bank.v1beta1.MsgMultiSend", + "/cosmos.staking.v1beta1.MsgDelegate", + "/cosmos.staking.v1beta1.MsgUndelegate", + "/cosmos.staking.v1beta1.MsgBeginRedelegate", + "/cosmos.staking.v1beta1.MsgRedeemTokensforShares", + "/cosmos.staking.v1beta1.MsgTokenizeShares", + "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", + "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", + "/ibc.applications.transfer.v1.MsgTransfer", + }), }, }, }) @@ -101,7 +116,7 @@ func TestICS(t *testing.T) { // interchaintest has one interface for a chain with IBC // support, and another for a Cosmos blockchain. atom, neutron, stride := chains[0], chains[1], chains[2] - _, cosmosNeutron := atom.(*cosmos.CosmosChain), neutron.(*cosmos.CosmosChain) + cosmosAtom, cosmosNeutron, cosmosStride := atom.(*cosmos.CosmosChain), neutron.(*cosmos.CosmosChain), stride.(*cosmos.CosmosChain) // Relayer Factory client, network := ibctest.DockerSetup(t) @@ -118,29 +133,36 @@ func TestICS(t *testing.T) { const gaiaNeutronICSPath = "gn-ics-path" const gaiaNeutronIBCPath = "gn-ibc-path" const gaiaStrideIBCPath = "gs-ibc-path" + const neutronStrideIBCPath = "ns-ibc-path" ic := ibctest.NewInterchain(). - AddChain(atom). - AddChain(neutron). - AddChain(stride). + AddChain(cosmosAtom). + AddChain(cosmosNeutron). + AddChain(cosmosStride). AddRelayer(r, "relayer"). AddProviderConsumerLink(ibctest.ProviderConsumerLink{ - Provider: atom, - Consumer: neutron, + Provider: cosmosAtom, + Consumer: cosmosNeutron, Relayer: r, Path: gaiaNeutronICSPath, }). AddLink(ibctest.InterchainLink{ - Chain1: atom, - Chain2: neutron, + Chain1: cosmosAtom, + Chain2: cosmosNeutron, Relayer: r, Path: gaiaNeutronIBCPath, }). AddLink(ibctest.InterchainLink{ - Chain1: atom, - Chain2: stride, + Chain1: cosmosAtom, + Chain2: cosmosStride, Relayer: r, Path: gaiaStrideIBCPath, + }). + AddLink(ibctest.InterchainLink{ + Chain1: cosmosNeutron, + Chain2: cosmosStride, + Relayer: r, + Path: neutronStrideIBCPath, }) // Log location @@ -165,7 +187,7 @@ func TestICS(t *testing.T) { require.NoError(t, err, "failed to wait for blocks") // Start the relayer and clean it up when the test ends. - err = r.StartRelayer(ctx, eRep, gaiaNeutronICSPath, gaiaNeutronIBCPath, gaiaStrideIBCPath) + err = r.StartRelayer(ctx, eRep, gaiaNeutronICSPath, gaiaNeutronIBCPath, gaiaStrideIBCPath, neutronStrideIBCPath) require.NoError(t, err, "failed to start relayer with given paths") t.Cleanup(func() { err = r.StopRelayer(ctx, eRep) @@ -177,18 +199,6 @@ func TestICS(t *testing.T) { err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) require.NoError(t, err, "failed to wait for blocks") - connections, err := r.GetConnections(ctx, eRep, "neutron-2") - require.NoError(t, err, "failed to get neutron-2 IBC connections from relayer") - var neutronIcsConnectionId string - for _, connection := range connections { - for _, version := range connection.Versions { - if version.String() != "transfer" { - neutronIcsConnectionId = connection.ID - break - } - } - } - cmd := getCreateValidatorCmd(atom) _, _, err = atom.Exec(ctx, cmd, nil) require.NoError(t, err) @@ -203,7 +213,18 @@ func TestICS(t *testing.T) { // by interchaintest in the genesis file. users := ibctest.GetAndFundTestUsers(t, ctx, "default", int64(100_000_000), atom, neutron, stride) gaiaUser, neutronUser, strideUser := users[0], users[1], users[2] - _, _ = gaiaUser, strideUser + + strideAdminMnemonic := "tone cause tribe this switch near host damage idle fragile antique tail soda alien depth write wool they rapid unfold body scan pledge soft" + strideAdmin, _ := ibctest.GetAndFundTestUserWithMnemonic(ctx, "default", strideAdminMnemonic, (100_000_000), cosmosStride) + + cosmosStride.SendFunds(ctx, strideUser.KeyName, ibc.WalletAmount{ + Address: strideAdmin.Bech32Address(stride.Config().Bech32Prefix), + Denom: "ustride", + Amount: 10000000, + }) + + err = testutil.WaitForBlocks(ctx, 10, atom, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") neutronUserBal, err := neutron.GetBalance( ctx, @@ -212,52 +233,205 @@ func TestICS(t *testing.T) { require.NoError(t, err, "failed to fund neutron user") require.EqualValues(t, int64(100_000_000), neutronUserBal) - neutronChannelInfo, _ := r.GetChannels(ctx, eRep, neutron.Config().ChainID) - var neutronGaiaIBCChannel ibc.ChannelOutput - var neutronGaiaICSChannel ibc.ChannelOutput - // find the ics channel - for _, s := range neutronChannelInfo { - if s.Ordering == "ORDER_ORDERED" { - neutronGaiaICSChannel = s + neutronChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosNeutron.Config().ChainID) + gaiaChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosAtom.Config().ChainID) + strideChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosStride.Config().ChainID) + strideConnectionInfo, _ := r.GetConnections(ctx, eRep, cosmosStride.Config().ChainID) + neutronConnectionInfo, _ := r.GetConnections(ctx, eRep, cosmosNeutron.Config().ChainID) + gaiaConnectionInfo, _ := r.GetConnections(ctx, eRep, cosmosAtom.Config().ChainID) + + /// Find all the pairwise connections + var strideGaiaConnectionId, gaiaStrideConnectionId string + var strideNeutronConnectionId, neutronStrideConnectionId string + + // There should be two sets of connections for gaia <> neutron. Tranfer and CCV + neutronGaiaConnectionIds := make([]string, 2) + gaiaNeutronConnectionIds := make([]string, 2) + + var neutronGaiaTransferConnectionId, neutronGaiaICSConnectionId string + var gaiaNeutronTransferConnectionId, gaiaNeutronICSConnectionId string + + // We iterate over stride connections + for _, strideConn := range strideConnectionInfo { + for _, neutronConn := range neutronConnectionInfo { + if neutronConn.ClientID == strideConn.Counterparty.ClientId && + strideConn.ClientID == neutronConn.Counterparty.ClientId && + neutronConn.ID == strideConn.Counterparty.ConnectionId && + strideConn.ID == neutronConn.Counterparty.ConnectionId { + strideNeutronConnectionId = strideConn.ID + neutronStrideConnectionId = neutronConn.ID + } + } + for _, gaiaConn := range gaiaConnectionInfo { + if strideConn.ClientID == gaiaConn.Counterparty.ClientId && + gaiaConn.ClientID == strideConn.Counterparty.ClientId && + gaiaConn.ID == strideConn.Counterparty.ConnectionId && + strideConn.ID == gaiaConn.Counterparty.ConnectionId { + strideGaiaConnectionId = strideConn.ID + gaiaStrideConnectionId = gaiaConn.ID + } + } + } + for _, neutronConn := range neutronConnectionInfo { + for _, gaiaConn := range gaiaConnectionInfo { + if neutronConn.ClientID == gaiaConn.Counterparty.ClientId && + gaiaConn.ClientID == neutronConn.Counterparty.ClientId && + neutronConn.ID == gaiaConn.Counterparty.ConnectionId && + gaiaConn.ID == neutronConn.Counterparty.ConnectionId { + neutronGaiaConnectionIds = append(neutronGaiaConnectionIds, neutronConn.ID) + gaiaNeutronConnectionIds = append(gaiaNeutronConnectionIds, neutronConn.ID) + break + } + } + } + + var strideNeutronChannelId, neutronStrideChannelId string + var strideGaiaChannelId, gaiaStrideChannelId string + var neutronGaiaICSChannelId, gaiaNeutronICSChannelId string + var neutronGaiaTransferChannelId, gaiaNeutronTransferChannelId string + + for _, s := range strideChannelInfo { + found := false + if !found { + for _, n := range neutronChannelInfo { + if s.ChannelID == n.Counterparty.ChannelID && + n.ChannelID == s.Counterparty.ChannelID && + s.PortID == n.Counterparty.PortID && + n.Ordering == "ORDER_UNORDERED" && + s.ConnectionHops[0] == strideNeutronConnectionId && + n.ConnectionHops[0] == neutronStrideConnectionId { + strideNeutronChannelId = s.ChannelID + neutronStrideChannelId = n.ChannelID + found = true + break + } + } + } + if found { break } } - // find the ibc transfer channel to gaia (same connection hops) - print("\n neutron channels:\n") - for _, s := range neutronChannelInfo { - channelJson, _ := json.Marshal(s) - print("\n", string(channelJson), "\n") - if s.State == "STATE_OPEN" && s.Ordering == "ORDER_UNORDERED" && s.PortID == "transfer" { - if len(s.Counterparty.ChannelID) > 5 && s.Counterparty.PortID == "transfer" && s.ConnectionHops[0] == neutronGaiaICSChannel.ConnectionHops[0] { - neutronGaiaIBCChannel = s + + for _, s := range strideChannelInfo { + found := false + if !found { + for _, g := range gaiaChannelInfo { + if s.ChannelID == g.Counterparty.ChannelID && + s.Counterparty.ChannelID == g.ChannelID && + s.PortID == g.Counterparty.PortID && + g.Ordering == "ORDER_UNORDERED" && + s.ConnectionHops[0] == strideGaiaConnectionId && + g.ConnectionHops[0] == gaiaStrideConnectionId { + strideGaiaChannelId = s.ChannelID + gaiaStrideChannelId = g.ChannelID + found = true + break + } } } + if found { + break + } + } + + for _, n := range neutronChannelInfo { + for _, g := range gaiaChannelInfo { + if n.PortID == "consumer" && + g.PortID == "provider" && + n.ChannelID == g.Counterparty.ChannelID && + g.ChannelID == n.Counterparty.ChannelID { + neutronGaiaICSChannelId = n.ChannelID + gaiaNeutronICSChannelId = g.ChannelID + neutronGaiaICSConnectionId = n.ConnectionHops[0] + gaiaNeutronICSConnectionId = g.ConnectionHops[0] + + } + } + } + + for _, ngci := range neutronGaiaConnectionIds { + if neutronGaiaICSConnectionId != ngci { + neutronGaiaTransferConnectionId = ngci + } } - gaiaNeutronIBCChannel := neutronGaiaIBCChannel.Counterparty + for _, gnci := range gaiaNeutronConnectionIds { + if gaiaNeutronICSConnectionId != gnci { + gaiaNeutronTransferConnectionId = gnci + } + } - print("\n gaia channels:\n") - gaiaChannelInfo, _ := r.GetChannels(ctx, eRep, atom.Config().ChainID) - for _, s := range gaiaChannelInfo { - channelJson, _ := json.Marshal(s) - print("\n", string(channelJson), "\n") + for _, n := range neutronChannelInfo { + for _, g := range gaiaChannelInfo { + if n.PortID == "transfer" && + g.PortID == "transfer" && + n.ChannelID == g.Counterparty.ChannelID && + g.ChannelID == n.Counterparty.ChannelID && + n.ConnectionHops[0] == neutronGaiaTransferConnectionId && + g.ConnectionHops[0] == gaiaNeutronTransferConnectionId { + neutronGaiaTransferChannelId = n.ChannelID + gaiaNeutronTransferChannelId = g.ChannelID + } + } } + print("\n strideGaiaConnectionId: ", strideGaiaConnectionId) + print("\n gaiaStrideConnectionId: ", gaiaStrideConnectionId) + print("\n strideNeutronConnectionId: ", strideNeutronConnectionId) + print("\n neutronStrideConnectionId: ", neutronStrideConnectionId) + print("\n neutronGaiaTransferConnectionId: ", neutronGaiaTransferConnectionId) + print("\n neutronGaiaICSConnectionId: ", neutronGaiaICSConnectionId) + print("\n gaiaNeutronTransferConnectionId: ", gaiaNeutronTransferConnectionId) + print("\n gaiaNeutronICSConnectionId: ", gaiaNeutronICSConnectionId) + + print("\n gaiaStrideChannelId: ", gaiaStrideChannelId) + print("\n strideGaiaChannelId: ", strideGaiaChannelId) + print("\n strideNeutronChannelId: ", strideNeutronChannelId) + print("\n neutronStrideChannelId: ", neutronStrideChannelId) + print("\n neutronGaiaTransferChannelId: ", neutronGaiaTransferChannelId) + print("\n gaiaNeutronTransferChannelId: ", gaiaNeutronTransferChannelId) + print("\n neutronGaiaICSChannelId: ", neutronGaiaICSChannelId) + print("\n gaiaNeutronICSChannelId: ", gaiaNeutronICSChannelId) + + _, _, _, _, _ = neutronGaiaTransferChannelId, gaiaNeutronTransferChannelId, neutronGaiaICSChannelId, gaiaNeutronICSChannelId, neutronStrideChannelId + _, _, _ = gaiaStrideConnectionId, strideGaiaConnectionId, strideNeutronConnectionId + t.Run("stride covenant tests", func(t *testing.T) { const clockContractAddress = "clock_contract_address" const holderContractAddress = "holder_contract_address" var lperContractAddress string var depositorContractAddress string + var lsContractAddress string var stAtomWeightedReceiver WeightedReceiver var atomWeightedReceiver WeightedReceiver + var strideICAAddress string neutronSrcDenomTrace := transfertypes.ParseDenomTrace( transfertypes.GetPrefixedDenom("transfer", - neutronGaiaIBCChannel.ChannelID, + neutronGaiaTransferChannelId, atom.Config().Denom)) neutronDstIbcDenom := neutronSrcDenomTrace.IBCDenom() + // gaiaAddr := gaiaUser.Bech32Address(atom.Config().Bech32Prefix) + atomSrcDenomTrace := transfertypes.ParseDenomTrace( + transfertypes.GetPrefixedDenom("transfer", + strideGaiaChannelId, + atom.Config().Denom)) + strideAtomIbcDenom := atomSrcDenomTrace.IBCDenom() + + neutronStatomDenomTrace := transfertypes.ParseDenomTrace( + transfertypes.GetPrefixedDenom("transfer", + neutronStrideChannelId, + "stuatom")) + neutronStatomDenom := neutronStatomDenomTrace.IBCDenom() + print("\nneutronDstIbcDenom: ", neutronDstIbcDenom) + print("\nstrideAtomIbcDenom: ", strideAtomIbcDenom) + print("\nneutronStatomDenom: ", neutronStatomDenom) + + print("\n") + + _ = strideAtomIbcDenom var coinRegistryAddress string var factoryAddress string var stableswapAddress string @@ -265,7 +439,142 @@ func TestICS(t *testing.T) { var whitelistAddress string _, _ = tokenAddress, whitelistAddress + t.Run("register stride host zone", func(t *testing.T) { + + cmd := []string{"strided", "tx", "stakeibc", "register-host-zone", + strideGaiaConnectionId, + cosmosAtom.Config().Denom, + cosmosAtom.Config().Bech32Prefix, + strideAtomIbcDenom, + strideGaiaChannelId, + "1", + "--from", strideAdmin.KeyName, + "--gas", "auto", + "--gas-adjustment", `1.3`, + "--output", "json", + "--chain-id", cosmosStride.Config().ChainID, + "--node", cosmosStride.GetRPCAddress(), + "--home", cosmosStride.HomeDir(), + "--keyring-backend", keyring.BackendTest, + "-y", + } + + _, _, err = cosmosStride.Exec(ctx, cmd, nil) + require.NoError(t, err, "failed to register host zone on stride") + + err = testutil.WaitForBlocks(ctx, 5, stride) + require.NoError(t, err, "failed to wait for blocks") + }) + + t.Run("register gaia validators on stride", func(t *testing.T) { + + type Validator struct { + Name string `json:"name"` + Address string `json:"address"` + Weight int `json:"weight"` + } + + type Data struct { + BlockHeight string `json:"block_height"` + Total string `json:"total"` + Validators []Validator `json:"validators"` + } + + valcmd := []string{"gaiad", "query", "tendermint-validator-set", + "50", + "--chain-id", cosmosAtom.Config().ChainID, + "--node", cosmosAtom.GetRPCAddress(), + "--home", cosmosAtom.HomeDir(), + } + resp, _, err := cosmosAtom.Exec(ctx, valcmd, nil) + require.NoError(t, err, "Failed to query valset") + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") + + var addresses []string + var votingPowers []string + + lines := strings.Split(string(resp), "\n") + + for _, line := range lines { + if strings.HasPrefix(line, "- address: ") { + address := strings.TrimPrefix(line, "- address: ") + addresses = append(addresses, address) + } else if strings.HasPrefix(line, " voting_power: ") { + votingPower := strings.TrimPrefix(line, " voting_power: ") + votingPowers = append(votingPowers, votingPower) + } + } + + // Create validators slice + var validators []Validator + + for i := 1; i <= len(addresses); i++ { + votingPowStr := strings.ReplaceAll(votingPowers[i-1], "\"", "") + valWeight, err := strconv.Atoi(votingPowStr) + require.NoError(t, err, "failed to parse voting power") + + validator := Validator{ + Name: fmt.Sprintf("val%d", i), + Address: addresses[i-1], + Weight: valWeight, + } + validators = append(validators, validator) + } + + // Create JSON object + data := map[string][]Validator{ + "validators": validators, + } + + // Convert to JSON + jsonData, err := json.Marshal(data) + require.NoError(t, err, "failed to marshall data") + + fullPath := filepath.Join(cosmosStride.HomeDir(), "vals.json") + bashCommand := "echo '" + string(jsonData) + "' > " + fullPath + fullPathCmd := []string{"/bin/sh", "-c", bashCommand} + + _, _, err = cosmosStride.Exec(ctx, fullPathCmd, nil) + require.NoError(t, err, "failed to create json with gaia LS validator set on stride") + + err = testutil.WaitForBlocks(ctx, 5, stride) + require.NoError(t, err, "failed to wait for blocks") + + cmd := []string{"strided", "tx", "stakeibc", "add-validators", + cosmosAtom.Config().ChainID, + fullPath, + "--from", strideAdmin.KeyName, + "--gas", "auto", + "--gas-adjustment", `1.3`, + "--output", "json", + "--chain-id", cosmosStride.Config().ChainID, + "--node", cosmosStride.GetRPCAddress(), + "--home", cosmosStride.HomeDir(), + "--keyring-backend", keyring.BackendTest, + "-y", + } + + _, _, err = cosmosStride.Exec(ctx, cmd, nil) + require.NoError(t, err, "failed to register host zone on stride") + + err = testutil.WaitForBlocks(ctx, 5, stride) + require.NoError(t, err, "failed to wait for blocks") + + queryCmd := []string{"strided", "query", "stakeibc", + "show-validators", + cosmosAtom.Config().ChainID, + "--chain-id", cosmosStride.Config().ChainID, + "--node", cosmosStride.GetRPCAddress(), + "--home", cosmosStride.HomeDir(), + } + + _, _, err = cosmosStride.Exec(ctx, queryCmd, nil) + require.NoError(t, err, "failed to query host validators") + }) + t.Run("deploy astroport contracts", func(t *testing.T) { + stablePairCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, "wasms/astroport_pair_stable.wasm") require.NoError(t, err, "failed to store astroport stableswap contract") stablePairCodeId, err := strconv.ParseUint(stablePairCodeIdStr, 10, 64) @@ -273,8 +582,6 @@ func TestICS(t *testing.T) { factoryCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, "wasms/astroport_factory.wasm") require.NoError(t, err, "failed to store astroport factory contract") - // factoryCodeId, err := strconv.ParseUint(factoryCodeIdStr, 10, 64) - // require.NoError(t, err, "failed to parse codeId into uint64") whitelistCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, "wasms/astroport_whitelist.wasm") require.NoError(t, err, "failed to store astroport whitelist contract") @@ -288,7 +595,6 @@ func TestICS(t *testing.T) { t.Run("astroport token", func(t *testing.T) { - // cap := uint64(1) msg := NativeTokenInstantiateMsg{ Name: "nativetoken", Symbol: "ntk", @@ -312,7 +618,7 @@ func TestICS(t *testing.T) { tokenAddress, err = cosmosNeutron.InstantiateContract(ctx, neutronUser.KeyName, tokenCodeIdStr, string(str), true) require.NoError(t, err, "Failed to instantiate Native Token") - err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron) require.NoError(t, err, "failed to wait for blocks") }) @@ -331,8 +637,7 @@ func TestICS(t *testing.T) { whitelistAddress, err = cosmosNeutron.InstantiateContract( ctx, neutronUser.KeyName, whitelistCodeIdStr, string(str), true) require.NoError(t, err, "Failed to instantiate Whitelist") - - err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron) require.NoError(t, err, "failed to wait for blocks") }) @@ -351,7 +656,7 @@ func TestICS(t *testing.T) { ctx, neutronUser.KeyName, coinRegistryCodeId, string(str), true) require.NoError(t, err, "Failed to instantiate NativeCoinRegistry") coinRegistryAddress = nativeCoinRegistryAddress - err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron) require.NoError(t, err, "failed to wait for blocks") }) @@ -377,8 +682,7 @@ func TestICS(t *testing.T) { } _, _, err = cosmosNeutron.Exec(ctx, addCmd, nil) require.NoError(t, err, err) - - err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron) require.NoError(t, err, "failed to wait for blocks") }) @@ -413,8 +717,7 @@ func TestICS(t *testing.T) { ctx, neutronUser.KeyName, factoryCodeIdStr, string(str), true) require.NoError(t, err, "Failed to instantiate Factory") factoryAddress = factoryAddr - - err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron) require.NoError(t, err, "failed to wait for blocks") }) @@ -467,15 +770,14 @@ func TestICS(t *testing.T) { ) require.NoError(t, err, "Failed to instantiate stableswap") stableswapAddress = stableswapAddr - - err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron) require.NoError(t, err, "failed to wait for blocks") }) }) t.Run("instantiate lper contract", func(t *testing.T) { - codeId, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, "wasms/stride_lper.wasm") + codeId, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, "wasms/covenant_lper.wasm") require.NoError(t, err, "failed to store neutron ICA contract") lpInfo := LpInfo{ Addr: stableswapAddress, @@ -492,6 +794,7 @@ func TestICS(t *testing.T) { lperContractAddress, err = cosmosNeutron.InstantiateContract(ctx, neutronUser.KeyName, codeId, string(str), true) require.NoError(t, err, "failed to instantiate lper contract: ", err) + print("\n lper: ", lperContractAddress) t.Run("query instantiated clock", func(t *testing.T) { var response ClockQueryResponse @@ -513,13 +816,72 @@ func TestICS(t *testing.T) { }) }) + t.Run("instantiate LS contract", func(t *testing.T) { + codeId, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, "wasms/covenant_ls.wasm") + require.NoError(t, err, "failed to store neutron ICA contract") + + msg := LsInstantiateMsg{ + AutopilotPosition: "todo", + ClockAddress: clockContractAddress, + StrideNeutronIBCTransferChannelId: strideNeutronChannelId, + LpAddress: lperContractAddress, + NeutronStrideIBCConnectionId: neutronStrideConnectionId, + LsDenom: "stuatom", + } + + str, err := json.Marshal(msg) + require.NoError(t, err, "Failed to marshall LsInstantiateMsg") + + lsContractAddress, err = cosmosNeutron.InstantiateContract(ctx, neutronUser.KeyName, codeId, string(str), true) + require.NoError(t, err, "failed to instantiate ls contract: ", err) + print("\nls contract:", lsContractAddress, "\n") + }) + + t.Run("create stride ICA", func(t *testing.T) { + // should remain constant + cmd = []string{"neutrond", "tx", "wasm", "execute", lsContractAddress, + `{"tick":{}}`, + "--from", neutronUser.KeyName, + "--gas-prices", "0.0untrn", + "--gas-adjustment", `1.5`, + "--output", "json", + "--home", "/var/cosmos-chain/neutron-2", + "--node", neutron.GetRPCAddress(), + "--home", neutron.HomeDir(), + "--chain-id", neutron.Config().ChainID, + "--from", neutronUser.KeyName, + "--gas", "auto", + "--keyring-backend", keyring.BackendTest, + "-y", + } + + _, _, err = cosmosNeutron.Exec(ctx, cmd, nil) + require.NoError(t, err) + + err = testutil.WaitForBlocks(ctx, 5, atom, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") + + var response QueryResponse + err = cosmosNeutron.QueryContract(ctx, lsContractAddress, IcaExampleContractQuery{ + InterchainAccountAddress: InterchainAccountAddressQuery{ + InterchainAccountId: "stride-ica", + ConnectionId: neutronStrideConnectionId, + }, + }, &response) + require.NoError(t, err, "failed to query ICA account address") + require.NotEmpty(t, response.Data.InterchainAccountAddress) + strideICAAddress = response.Data.InterchainAccountAddress + + print("\nstride ICA instantiated with address ", strideICAAddress, "\n") + }) + t.Run("instantiate depositor contract", func(t *testing.T) { - codeId, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, "wasms/stride_depositor.wasm") + codeId, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, "wasms/covenant_depositor.wasm") require.NoError(t, err, "failed to store neutron ICA contract") stAtomWeightedReceiver = WeightedReceiver{ Amount: int64(10), - Address: lperContractAddress, + Address: strideICAAddress, } atomWeightedReceiver = WeightedReceiver{ @@ -531,7 +893,9 @@ func TestICS(t *testing.T) { StAtomReceiver: stAtomWeightedReceiver, AtomReceiver: atomWeightedReceiver, ClockAddress: clockContractAddress, - GaiaNeutronIBCTransferChannelId: gaiaNeutronIBCChannel.ChannelID, + GaiaNeutronIBCTransferChannelId: gaiaNeutronTransferChannelId, + GaiaStrideIBCTransferChannelId: gaiaStrideChannelId, + NeutronGaiaConnectionId: neutronGaiaTransferConnectionId, } str, err := json.Marshal(msg) @@ -569,7 +933,7 @@ func TestICS(t *testing.T) { }) var addrResponse QueryResponse - t.Run("first tick instantiates ICA", func(t *testing.T) { + t.Run("tick depositor to instantiate ICA", func(t *testing.T) { // should remain constant cmd = []string{"neutrond", "tx", "wasm", "execute", depositorContractAddress, `{"tick":{}}`, @@ -597,7 +961,7 @@ func TestICS(t *testing.T) { err = cosmosNeutron.QueryContract(ctx, depositorContractAddress, IcaExampleContractQuery{ InterchainAccountAddress: InterchainAccountAddressQuery{ InterchainAccountId: icaAccountId, - ConnectionId: neutronIcsConnectionId, + ConnectionId: neutronGaiaTransferConnectionId, }, }, &response) require.NoError(t, err, "failed to query ICA account address") @@ -621,14 +985,14 @@ func TestICS(t *testing.T) { t.Run("multisig transfers atom to ICA account", func(t *testing.T) { // transfer funds from gaiaUser to the newly generated ICA account - err := atom.SendFunds(ctx, gaiaUser.KeyName, ibc.WalletAmount{ + err := cosmosAtom.SendFunds(ctx, gaiaUser.KeyName, ibc.WalletAmount{ Address: icaAccountAddress, Amount: 20, Denom: atom.Config().Denom, }) require.NoError(t, err, "failed to send funds from gaia to neutron ICA") - err = testutil.WaitForBlocks(ctx, 10, atom, neutron) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron) require.NoError(t, err, "failed to wait for blocks") atomBal, err := atom.GetBalance(ctx, icaAccountAddress, atom.Config().Denom) @@ -636,7 +1000,7 @@ func TestICS(t *testing.T) { require.EqualValues(t, 20, atomBal) }) - t.Run("fund depositor contract with some neutron", func(t *testing.T) { + t.Run("fund contracts with neutron", func(t *testing.T) { err := neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ Address: depositorContractAddress, Amount: 500001, @@ -644,78 +1008,139 @@ func TestICS(t *testing.T) { }) require.NoError(t, err, "failed to send funds from neutron user to depositor contract") - err = testutil.WaitForBlocks(ctx, 10, atom, neutron) + + err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: lsContractAddress, + Amount: 500001, + Denom: neutron.Config().Denom, + }) + + require.NoError(t, err, "failed to send funds from neutron user to ls contract") + + err = testutil.WaitForBlocks(ctx, 2, atom, neutron) require.NoError(t, err, "failed to wait for blocks") - neutronBal, err := neutron.GetBalance(ctx, depositorContractAddress, neutron.Config().Denom) + depositorNeutronBal, err := neutron.GetBalance(ctx, depositorContractAddress, neutron.Config().Denom) + require.NoError(t, err, "failed to get depositor neutron balance") + require.EqualValues(t, 500001, depositorNeutronBal) + + lsNeutronBal, err := neutron.GetBalance(ctx, lsContractAddress, neutron.Config().Denom) require.NoError(t, err, "failed to get depositor neutron balance") - require.EqualValues(t, 500001, neutronBal) + require.EqualValues(t, 500001, lsNeutronBal) }) - t.Run("second tick ibc transfers atom from ICA account to neutron", func(t *testing.T) { + t.Run("tick depositor to liquid stake on stride", func(t *testing.T) { atomBal, err := atom.GetBalance(ctx, icaAccountAddress, atom.Config().Denom) require.NoError(t, err, "failed to get ICA balance") require.EqualValues(t, 20, atomBal) + require.NoError(t, err, "failed to wait for blocks") cmd = []string{"neutrond", "tx", "wasm", "execute", depositorContractAddress, `{"tick":{}}`, "--from", neutronUser.KeyName, "--gas-adjustment", `1.3`, + "--gas-prices", "0.0untrn", "--output", "json", - "--home", "/var/cosmos-chain/neutron-2", - "--node", neutron.GetRPCAddress(), - "--home", neutron.HomeDir(), - "--chain-id", neutron.Config().ChainID, + "--node", cosmosNeutron.GetRPCAddress(), + "--home", cosmosNeutron.HomeDir(), + "--chain-id", cosmosNeutron.Config().ChainID, "--gas", "auto", - "--fees", "500000untrn", + "--fees", "5000untrn", "--keyring-backend", keyring.BackendTest, "-y", } - _, _, err = neutron.Exec(ctx, cmd, nil) + _, _, err = cosmosNeutron.Exec(ctx, cmd, nil) require.NoError(t, err) - err = testutil.WaitForBlocks(ctx, 20, atom, neutron) + err = testutil.WaitForBlocks(ctx, 20, atom, neutron, stride) require.NoError(t, err, "failed to wait for blocks") atomICABal, err := atom.GetBalance(ctx, icaAccountAddress, atom.Config().Denom) require.NoError(t, err, "failed to query ICA balance") require.Equal(t, int64(10), atomICABal) - neutronUserBalNew, err := neutron.GetBalance( + strideICABal, err := stride.GetBalance(ctx, strideICAAddress, "stuatom") + require.NoError(t, err, "failed to query ICA balance") + require.Equal(t, int64(10), strideICABal) + print("\n stride ica bal: ", strideICABal, "\n") + }) + + t.Run("tick ls to transfer stuatom to LPer", func(t *testing.T) { + + cmd = []string{"neutrond", "tx", "wasm", "execute", lsContractAddress, + `{"tick":{}}`, + "--from", neutronUser.KeyName, + "--gas-adjustment", `1.3`, + "--output", "json", + "--node", cosmosNeutron.GetRPCAddress(), + "--home", cosmosNeutron.HomeDir(), + "--chain-id", cosmosNeutron.Config().ChainID, + "--gas", "auto", + "--fees", "15000untrn", + "--keyring-backend", keyring.BackendTest, + "-y", + } + + _, _, err := cosmosNeutron.Exec(ctx, cmd, nil) + require.NoError(t, err) + + err = testutil.WaitForBlocks(ctx, 10, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") + + strideICABal, err := stride.GetBalance(ctx, strideICAAddress, "stuatom") + require.NoError(t, err, "failed to query ICA balance") + require.Equal(t, int64(0), strideICABal, "failed to withdraw stuatom to LPer") + + lperStatomBal, err := neutron.GetBalance( ctx, - depositorContractAddress, - neutronDstIbcDenom) - require.NoError(t, err, "failed to query depositor contract atom balance") - require.Equal(t, int64(10), neutronUserBalNew) + lperContractAddress, + neutronStatomDenom, + ) + require.NoError(t, err, "failed to query lper balance") + require.Equal(t, int64(10), lperStatomBal, "LPer did not receive stuatoms") }) - // to keep docker containers alive for debugging - // err = testutil.WaitForBlocks(ctx, 200, atom, neutron) - t.Run("subsequent ticks do nothing", func(t *testing.T) { + t.Run("tick depositor to ibc transfer atom from ICA account to neutron", func(t *testing.T) { + atomBal, err := atom.GetBalance(ctx, icaAccountAddress, atom.Config().Denom) + require.NoError(t, err, "failed to get ICA balance") + require.EqualValues(t, 10, atomBal) + cmd = []string{"neutrond", "tx", "wasm", "execute", depositorContractAddress, `{"tick":{}}`, "--from", neutronUser.KeyName, - "--gas-prices", "0.0untrn", - "--gas-adjustment", `1.5`, + "--gas-adjustment", `1.3`, "--output", "json", - "--home", "/var/cosmos-chain/neutron-2", - "--node", neutron.GetRPCAddress(), - "--home", neutron.HomeDir(), - "--chain-id", neutron.Config().ChainID, - "--from", "faucet", - "--gas", "50000.0untrn", + "--node", cosmosNeutron.GetRPCAddress(), + "--home", cosmosNeutron.HomeDir(), + "--chain-id", cosmosNeutron.Config().ChainID, + "--gas", "auto", + "--fees", "15000untrn", "--keyring-backend", keyring.BackendTest, "-y", } - _, _, err = neutron.Exec(ctx, cmd, nil) + _, _, err = cosmosNeutron.Exec(ctx, cmd, nil) require.NoError(t, err) err = testutil.WaitForBlocks(ctx, 10, atom, neutron) require.NoError(t, err, "failed to wait for blocks") + err = testutil.WaitForBlocks(ctx, 100, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") + + atomICABal, err := atom.GetBalance(ctx, icaAccountAddress, atom.Config().Denom) + require.NoError(t, err, "failed to query ICA balance") + require.Equal(t, int64(0), atomICABal) + + neutronUserBalNew, err := neutron.GetBalance( + ctx, + depositorContractAddress, + neutronDstIbcDenom) + require.NoError(t, err, "failed to query depositor contract atom balance") + require.Equal(t, int64(10), neutronUserBalNew) }) + require.NoError(t, err, "failed to wait for blocks") }) } diff --git a/stride-covenant/tests/interchaintest/types.go b/stride-covenant/tests/interchaintest/types.go index 09b999c1..3d4873c0 100644 --- a/stride-covenant/tests/interchaintest/types.go +++ b/stride-covenant/tests/interchaintest/types.go @@ -5,6 +5,8 @@ type DepositorInstantiateMsg struct { AtomReceiver WeightedReceiver `json:"atom_receiver"` ClockAddress string `json:"clock_address,string"` GaiaNeutronIBCTransferChannelId string `json:"gaia_neutron_ibc_transfer_channel_id"` + GaiaStrideIBCTransferChannelId string `json:"gaia_stride_ibc_transfer_channel_id"` + NeutronGaiaConnectionId string `json:"neutron_gaia_connection_id"` } type LPerInstantiateMsg struct { @@ -207,3 +209,13 @@ type WhitelistInstantiateMsg struct { Admins []string `json:"admins"` Mutable bool `json:"mutable"` } + +// ls +type LsInstantiateMsg struct { + AutopilotPosition string `json:"autopilot_position,string"` + ClockAddress string `json:"clock_address,string"` + StrideNeutronIBCTransferChannelId string `json:"stride_neutron_ibc_transfer_channel_id"` + LpAddress string `json:"lp_address"` + NeutronStrideIBCConnectionId string `json:"neutron_stride_ibc_connection_id"` + LsDenom string `json:"ls_denom"` +}