diff --git a/.changelog/unreleased/features/4133-osmosis-swaps.md b/.changelog/unreleased/features/4133-osmosis-swaps.md new file mode 100644 index 0000000000..a6cd190100 --- /dev/null +++ b/.changelog/unreleased/features/4133-osmosis-swaps.md @@ -0,0 +1,4 @@ +- Integrate Namada and Osmosis, to allow swapping assets privately. Osmosis + is leveraged for its liquidity and DEX capabilities, while Namada is + leveraged for its shielded pool (i.e. MASP) and privacy guarantees. + ([\#4133](https://github.com/anoma/namada/pull/4133)) \ No newline at end of file diff --git a/.github/workflows/scripts/e2e.json b/.github/workflows/scripts/e2e.json index c63422bf67..30046d7243 100644 --- a/.github/workflows/scripts/e2e.json +++ b/.github/workflows/scripts/e2e.json @@ -9,6 +9,8 @@ "e2e::ibc_tests::ibc_pfm_happy_flows": 485, "e2e::ibc_tests::ibc_pfm_unhappy_flows": 485, "e2e::ibc_tests::ibc_upgrade_client": 280, + "e2e::ibc_tests::ibc_shielded_recv_middleware_happy_flow": 280, + "e2e::ibc_tests::ibc_shielded_recv_middleware_unhappy_flow": 280, "e2e::eth_bridge_tests::test_add_to_bridge_pool": 10, "e2e::ledger_tests::double_signing_gets_slashed": 12, "e2e::ledger_tests::ledger_many_txs_in_a_block": 55, diff --git a/Cargo.lock b/Cargo.lock index df2c54c921..77940026d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3788,10 +3788,90 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "ibc-middleware-module" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=module/v0.1.0#3d3b436f7c58000c7498d68e88c15a955433a619" +dependencies = [ + "ibc-core-channel-types", + "ibc-core-host-types", + "ibc-core-router", + "ibc-core-router-types", + "ibc-primitives", +] + +[[package]] +name = "ibc-middleware-module" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=orm/v0.4.0#8d341de14ff5e2a637699796cffbf0fbbaee001f" +dependencies = [ + "ibc-core-channel-types", + "ibc-core-host-types", + "ibc-core-router", + "ibc-core-router-types", + "ibc-primitives", +] + +[[package]] +name = "ibc-middleware-module" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.9.0#3d3b436f7c58000c7498d68e88c15a955433a619" +dependencies = [ + "ibc-core-channel-types", + "ibc-core-host-types", + "ibc-core-router", + "ibc-core-router-types", + "ibc-primitives", +] + +[[package]] +name = "ibc-middleware-module-macros" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=module-macros/v0.1.0#3d3b436f7c58000c7498d68e88c15a955433a619" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "ibc-middleware-module-macros" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=orm/v0.4.0#8d341de14ff5e2a637699796cffbf0fbbaee001f" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "ibc-middleware-module-macros" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.9.0#3d3b436f7c58000c7498d68e88c15a955433a619" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "ibc-middleware-overflow-receive" +version = "0.4.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=orm/v0.4.0#8d341de14ff5e2a637699796cffbf0fbbaee001f" +dependencies = [ + "ibc-app-transfer-types", + "ibc-core-channel-types", + "ibc-core-host-types", + "ibc-core-router", + "ibc-core-router-types", + "ibc-middleware-module 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=orm/v0.4.0)", + "ibc-middleware-module-macros 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=orm/v0.4.0)", + "ibc-primitives", + "serde", + "serde_json", +] + [[package]] name = "ibc-middleware-packet-forward" -version = "0.8.0" -source = "git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.8.0#9c4a410063df8562c726c76009ff08b4e5a1894a" +version = "0.9.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.9.0#3d3b436f7c58000c7498d68e88c15a955433a619" dependencies = [ "borsh", "dur", @@ -3802,6 +3882,8 @@ dependencies = [ "ibc-core-host-types", "ibc-core-router", "ibc-core-router-types", + "ibc-middleware-module 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.9.0)", + "ibc-middleware-module-macros 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.9.0)", "ibc-primitives", "serde", "serde_json", @@ -5194,6 +5276,9 @@ dependencies = [ "dur", "ibc", "ibc-derive", + "ibc-middleware-module 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=module/v0.1.0)", + "ibc-middleware-module-macros 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=module-macros/v0.1.0)", + "ibc-middleware-overflow-receive", "ibc-middleware-packet-forward", "ibc-testkit", "ics23", @@ -5421,6 +5506,7 @@ dependencies = [ "arbitrary", "assert_matches", "async-trait", + "bech32 0.8.1", "bimap", "borsh", "circular-queue", @@ -6950,9 +7036,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.35" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] diff --git a/Cargo.toml b/Cargo.toml index 38a15f0d32..0592ba7ab6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -121,7 +121,10 @@ git2 = { version = "0.18.1", default-features = false } # branch tiago/optional-ack ibc = { git = "https://github.com/heliaxdev/cosmos-ibc-rs", rev = "38489943c4e75206eaffeeeec6153c039c2499d1", features = ["serde"] } ibc-derive = { git = "https://github.com/heliaxdev/cosmos-ibc-rs", rev = "38489943c4e75206eaffeeeec6153c039c2499d1" } -ibc-middleware-packet-forward = { git = "https://github.com/heliaxdev/ibc-middleware", tag = "pfm/v0.8.0", features = ["borsh"] } +ibc-middleware-module = { git = "https://github.com/heliaxdev/ibc-middleware", tag = "module/v0.1.0" } +ibc-middleware-module-macros = { git = "https://github.com/heliaxdev/ibc-middleware", tag = "module-macros/v0.1.0" } +ibc-middleware-overflow-receive = { git = "https://github.com/heliaxdev/ibc-middleware", tag = "orm/v0.4.0" } +ibc-middleware-packet-forward = { git = "https://github.com/heliaxdev/ibc-middleware", tag = "pfm/v0.9.0", features = ["borsh"] } ibc-testkit = { git = "https://github.com/heliaxdev/cosmos-ibc-rs", rev = "38489943c4e75206eaffeeeec6153c039c2499d1", default-features = false } ics23 = "0.12.0" usize-set = { version = "0.10.3", features = ["serialize-borsh", "serialize-serde"] } diff --git a/crates/apps/src/bin/namada/cli.rs b/crates/apps/src/bin/namada/cli.rs index f3165a4a41..bbe37d5959 100644 --- a/crates/apps/src/bin/namada/cli.rs +++ b/crates/apps/src/bin/namada/cli.rs @@ -50,6 +50,7 @@ fn handle_command(cmd: cli::cmds::Namada, raw_sub_cmd: String) -> Result<()> { | cli::cmds::Namada::TxShieldingTransfer(_) | cli::cmds::Namada::TxUnshieldingTransfer(_) | cli::cmds::Namada::TxIbcTransfer(_) + | cli::cmds::Namada::TxOsmosisSwap(_) | cli::cmds::Namada::TxUpdateAccount(_) | cli::cmds::Namada::TxRevealPk(_) | cli::cmds::Namada::TxInitProposal(_) diff --git a/crates/apps_lib/src/cli.rs b/crates/apps_lib/src/cli.rs index ed74f4cf08..6e715b86d5 100644 --- a/crates/apps_lib/src/cli.rs +++ b/crates/apps_lib/src/cli.rs @@ -63,6 +63,7 @@ pub mod cmds { TxShieldingTransfer(TxShieldingTransfer), TxUnshieldingTransfer(TxUnshieldingTransfer), TxIbcTransfer(TxIbcTransfer), + TxOsmosisSwap(TxOsmosisSwap), TxUpdateAccount(TxUpdateAccount), TxInitProposal(TxInitProposal), TxVoteProposal(TxVoteProposal), @@ -84,6 +85,7 @@ pub mod cmds { .subcommand(TxShieldingTransfer::def().display_order(2)) .subcommand(TxUnshieldingTransfer::def().display_order(2)) .subcommand(TxIbcTransfer::def().display_order(2)) + .subcommand(TxOsmosisSwap::def().display_order(2)) .subcommand(TxUpdateAccount::def().display_order(2)) .subcommand(TxInitProposal::def().display_order(2)) .subcommand(TxVoteProposal::def().display_order(2)) @@ -107,6 +109,8 @@ pub mod cmds { SubCmd::parse(matches).map(Self::TxUnshieldingTransfer); let tx_ibc_transfer = SubCmd::parse(matches).map(Self::TxIbcTransfer); + let tx_osmosis_swap = + SubCmd::parse(matches).map(Self::TxOsmosisSwap); let tx_update_account = SubCmd::parse(matches).map(Self::TxUpdateAccount); let tx_init_proposal = @@ -124,6 +128,7 @@ pub mod cmds { .or(tx_shielding_transfer) .or(tx_unshielding_transfer) .or(tx_ibc_transfer) + .or(tx_osmosis_swap) .or(tx_update_account) .or(tx_init_proposal) .or(tx_vote_proposal) @@ -239,6 +244,7 @@ pub mod cmds { .subcommand(TxShieldingTransfer::def().display_order(1)) .subcommand(TxUnshieldingTransfer::def().display_order(1)) .subcommand(TxIbcTransfer::def().display_order(1)) + .subcommand(TxOsmosisSwap::def().display_order(1)) .subcommand(TxUpdateAccount::def().display_order(1)) .subcommand(TxInitAccount::def().display_order(1)) .subcommand(TxRevealPk::def().display_order(1)) @@ -314,6 +320,7 @@ pub mod cmds { let tx_unshielding_transfer = Self::parse_with_ctx(matches, TxUnshieldingTransfer); let tx_ibc_transfer = Self::parse_with_ctx(matches, TxIbcTransfer); + let tx_osmosis_swap = Self::parse_with_ctx(matches, TxOsmosisSwap); let tx_update_account = Self::parse_with_ctx(matches, TxUpdateAccount); let tx_init_account = Self::parse_with_ctx(matches, TxInitAccount); @@ -402,6 +409,7 @@ pub mod cmds { .or(tx_shielding_transfer) .or(tx_unshielding_transfer) .or(tx_ibc_transfer) + .or(tx_osmosis_swap) .or(tx_update_account) .or(tx_init_account) .or(tx_reveal_pk) @@ -496,6 +504,7 @@ pub mod cmds { TxShieldingTransfer(TxShieldingTransfer), TxUnshieldingTransfer(TxUnshieldingTransfer), TxIbcTransfer(TxIbcTransfer), + TxOsmosisSwap(TxOsmosisSwap), QueryResult(QueryResult), TxUpdateAccount(TxUpdateAccount), TxInitAccount(TxInitAccount), @@ -1407,6 +1416,25 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct TxOsmosisSwap(pub args::TxOsmosisSwap); + + impl SubCmd for TxOsmosisSwap { + const CMD: &'static str = "osmosis-swap"; + + fn parse(matches: &ArgMatches) -> Option { + matches.subcommand_matches(Self::CMD).map(|matches| { + TxOsmosisSwap(args::TxOsmosisSwap::parse(matches)) + }) + } + + fn def() -> App { + App::new(Self::CMD) + .about(wrap!("Swap two asset kinds using Osmosis.")) + .add_args::>() + } + } + #[derive(Clone, Debug)] pub struct TxUpdateAccount(pub args::TxUpdateAccount); @@ -3356,6 +3384,7 @@ pub mod args { use crate::wrap; pub const ADDRESS: Arg = arg("address"); + pub const ADDRESS_OPT: ArgOpt = arg_opt("address"); pub const ADD_PERSISTENT_PEERS: ArgFlag = flag("add-persistent-peers"); pub const ALIAS_OPT: ArgOpt = ALIAS.opt(); pub const ALIAS: Arg = arg("alias"); @@ -3508,6 +3537,7 @@ pub mod args { pub const LIST_FIND_ADDRESSES_ONLY: ArgFlag = flag("addr"); pub const LIST_FIND_KEYS_ONLY: ArgFlag = flag("keys"); pub const LOCALHOST: ArgFlag = flag("localhost"); + pub const LOCAL_RECOVERY_ADDR: Arg = arg("local-recovery-addr"); pub const MASP_EPOCH: ArgOpt = arg_opt("masp-epoch"); pub const MAX_COMMISSION_RATE_CHANGE: Arg = arg("max-commission-rate-change"); @@ -3516,21 +3546,30 @@ pub mod args { pub const MAX_ETH_GAS: ArgOpt = arg_opt("max_eth-gas"); pub const MEMO_OPT: ArgOpt = arg_opt("memo"); pub const MIGRATION_PATH: ArgOpt = arg_opt("migration-path"); + pub const MINIMUM_AMOUNT: ArgOpt = + arg_opt("minimum-amount"); pub const MODE: ArgOpt = arg_opt("mode"); pub const NET_ADDRESS: Arg = arg("net-address"); pub const NAMADA_START_TIME: ArgOpt = arg_opt("time"); pub const NO_CONVERSIONS: ArgFlag = flag("no-conversions"); pub const NO_EXPIRATION: ArgFlag = flag("no-expiration"); pub const NUT: ArgFlag = flag("nut"); + pub const OSMOSIS_REST_RPC: Arg = arg("osmosis-rest-rpc"); pub const OUT_FILE_PATH_OPT: ArgOpt = arg_opt("out-file-path"); pub const OUTPUT: ArgOpt = arg_opt("output"); + pub const OUTPUT_DENOM: Arg = arg("output-denom"); pub const OUTPUT_FOLDER_PATH: ArgOpt = arg_opt("output-folder-path"); + pub const OSMOSIS_POOL_HOP: ArgMulti = + arg_multi("pool-hop"); + pub const OVERFLOW_OPT: ArgOpt = arg_opt("overflow-addr"); pub const OWNER: Arg = arg("owner"); pub const OWNER_OPT: ArgOpt = OWNER.opt(); pub const PATH: Arg = arg("path"); pub const PATH_OPT: ArgOpt = arg_opt("path"); pub const PAYMENT_ADDRESS_TARGET: Arg = arg("target"); + pub const PAYMENT_ADDRESS_TARGET_OPT: ArgOpt = + arg_opt("target-pa"); pub const PORT_ID: ArgDefault = arg_default( "port-id", DefaultFn(|| PortId::from_str("transfer").unwrap()), @@ -3578,6 +3617,7 @@ pub mod args { pub const SHIELDED: ArgFlag = flag("shielded"); pub const SHOW_IBC_TOKENS: ArgFlag = flag("show-ibc-tokens"); pub const SIGNER: ArgOpt = arg_opt("signer"); + pub const SLIPPAGE: ArgOpt = arg_opt("slippage-percentage"); pub const SIGNING_KEYS: ArgMulti = arg_multi("signing-keys"); pub const SIGNATURES: ArgMulti = arg_multi("signatures"); @@ -3591,6 +3631,7 @@ pub mod args { pub const STORAGE_KEY: Arg = arg("storage-key"); pub const SUSPEND_ACTION: ArgFlag = flag("suspend"); pub const TARGET: Arg = arg("target"); + pub const TARGET_OPT: ArgOpt = arg_opt("target"); pub const TEMPLATES_PATH: Arg = arg("templates-path"); pub const TIMEOUT_HEIGHT: ArgOpt = arg_opt("timeout-height"); pub const TIMEOUT_SEC_OFFSET: ArgOpt = arg_opt("timeout-sec-offset"); @@ -3634,6 +3675,7 @@ pub mod args { pub const WASM_CHECKSUMS_PATH: Arg = arg("wasm-checksums-path"); pub const WASM_DIR: ArgOpt = arg_opt("wasm-dir"); pub const WEBSITE_OPT: ArgOpt = arg_opt("website"); + pub const WINDOW_SECONDS: ArgOpt = arg_opt("window-seconds"); pub const WITH_INDEXER: ArgOpt = arg_opt("with-indexer"); pub const WRAPPER_SIGNATURE_OPT: ArgOpt = arg_opt("gas-signature"); pub const TX_PATH: Arg = arg("tx-path"); @@ -5028,6 +5070,184 @@ pub mod args { } } + impl CliToSdk> for TxOsmosisSwap { + type Error = std::io::Error; + + fn to_sdk( + self, + ctx: &mut Context, + ) -> Result, Self::Error> { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + let recipient = match self.recipient { + Either::Left(r) => Either::Left(chain_ctx.get(&r)), + Either::Right(r) => Either::Right(chain_ctx.get(&r)), + }; + let overflow = self.overflow.map(|r| chain_ctx.get(&r)); + Ok(TxOsmosisSwap { + transfer: self.transfer.to_sdk(ctx)?, + output_denom: self.output_denom, + recipient, + overflow, + slippage: self.slippage, + local_recovery_addr: self.local_recovery_addr, + route: self.route, + osmosis_rest_rpc: self.osmosis_rest_rpc, + }) + } + } + + impl Args for TxOsmosisSwap { + fn parse(matches: &ArgMatches) -> Self { + let transfer = TxIbcTransfer::parse(matches); + let osmosis_rest_rpc = OSMOSIS_REST_RPC.parse(matches); + let output_denom = OUTPUT_DENOM.parse(matches); + let maybe_trans_recipient = TARGET_OPT.parse(matches); + let maybe_shielded_recipient = + PAYMENT_ADDRESS_TARGET_OPT.parse(matches); + let maybe_overflow = OVERFLOW_OPT.parse(matches); + let slippage_percent = SLIPPAGE.parse(matches); + if slippage_percent + .is_some_and(|percent| !(0.0..=100.0).contains(&percent)) + { + panic!( + "The slippage percent must be a number between 0 and 100." + ) + } + let window_seconds = WINDOW_SECONDS.parse(matches); + let minimum_amount = MINIMUM_AMOUNT.parse(matches); + let slippage = minimum_amount + .map(|d| Slippage::MinOutputAmount(d.redenominate(0).amount())) + .or_else(|| { + Some(Slippage::Twap { + slippage_percentage: slippage_percent + .expect( + "If a minimum amount was not provided, \ + slippage-percentage and window-seconds must \ + be specified.", + ) + .to_string(), + window_seconds: window_seconds.expect( + "If a minimum amount was not provided, \ + slippage-percentage and window-seconds must be \ + specified.", + ), + }) + }) + .unwrap(); + let local_recovery_addr = LOCAL_RECOVERY_ADDR.parse(matches); + let route = match OSMOSIS_POOL_HOP.parse(matches) { + r if r.is_empty() => None, + r => Some(r), + }; + Self { + transfer, + output_denom, + recipient: if let Some(target) = maybe_trans_recipient { + Either::Left(target) + } else { + Either::Right(maybe_shielded_recipient.unwrap()) + }, + overflow: maybe_overflow, + slippage, + local_recovery_addr, + route, + osmosis_rest_rpc, + } + } + + fn def(app: App) -> App { + app.add_args::>() + .arg( + OSMOSIS_REST_RPC + .def() + .help(wrap!("A url pointing to an Osmosis REST rpc.")), + ) + .arg(OSMOSIS_POOL_HOP.def().help(wrap!( + "Individual hop of the route to take through Osmosis \ + pools. This value takes the form \ + :. When unspecified, \ + the optimal route is queried on the fly." + ))) + .arg(OUTPUT_DENOM.def().help(wrap!( + "IBC trace path (on Namada) of the desired asset. This is \ + a string of the form \ + `transfer//`, where `` \ + is the channel that connects Namada to some counterparty \ + chain." + ))) + .arg( + TARGET_OPT + .def() + .conflicts_with(OVERFLOW_OPT.name) + .conflicts_with(PAYMENT_ADDRESS_TARGET_OPT.name) + .help(wrap!( + "Transparent Namada address that shall receive \ + the swapped tokens." + )), + ) + .arg( + PAYMENT_ADDRESS_TARGET_OPT + .def() + .conflicts_with(TARGET_OPT.name) + .help(wrap!( + "Namada payment address that shall receive the \ + minimum amount of tokens swapped on Osmosis." + )), + ) + .arg(OVERFLOW_OPT.def().help(wrap!( + "Transparent address that receives the amount of target \ + asset exceeding the minimum trade amount. Only \ + applicable when shielding assets that have been swapped \ + on Osmosis. This address should not be linkable to any \ + of the user's personal accounts, to maximize the privacy \ + of the trade. If unspecified, a disposable address is \ + generated." + ))) + .arg(SLIPPAGE.def().requires(WINDOW_SECONDS.name).help(wrap!( + "Slippage percentage, as a number between 0 and 100. \ + Represents the maximum acceptable deviation from the \ + expected price during a trade." + ))) + .arg(WINDOW_SECONDS.def().requires(SLIPPAGE.name).help(wrap!( + "Time period (in seconds) over which the average price is \ + calculated." + ))) + .arg( + MINIMUM_AMOUNT + .def() + .conflicts_with(SLIPPAGE.name) + .conflicts_with(WINDOW_SECONDS.name) + .help(wrap!( + "Minimum amount of target asset that the trade \ + should produce." + )), + ) + .arg(LOCAL_RECOVERY_ADDR.def().help(wrap!( + "Address on Osmosis from which to recover funds in case \ + of failure." + ))) + .group( + ArgGroup::new("slippage") + .args([SLIPPAGE.name, MINIMUM_AMOUNT.name]) + .required(true), + ) + .group( + ArgGroup::new("transfer-target") + .args([ + TARGET_OPT.name, + PAYMENT_ADDRESS_TARGET_OPT.name, + ]) + .required(true), + ) + .mut_arg(RECEIVER.name, |arg| { + arg.long("swap-contract").help(wrap!( + "Address of the Osmosis contract performing the swap. \ + It will be the receiver of the IBC transfer." + )) + }) + } + } + impl CliToSdk> for TxInitAccount { type Error = std::io::Error; @@ -6857,11 +7077,22 @@ pub mod args { query, output_folder: self.output_folder, target: chain_ctx.get(&self.target), - token: self.token, amount: self.amount, expiration: self.expiration, - port_id: self.port_id, - channel_id: self.channel_id, + asset: match self.asset { + IbcShieldingTransferAsset::LookupNamadaAddress { + port_id, + channel_id, + token, + } => IbcShieldingTransferAsset::LookupNamadaAddress { + port_id, + channel_id, + token, + }, + IbcShieldingTransferAsset::Address(addr) => { + IbcShieldingTransferAsset::Address(chain_ctx.get(&addr)) + } + }, }) } } @@ -6890,11 +7121,13 @@ pub mod args { query, output_folder, target, - token, amount, expiration, - port_id, - channel_id, + asset: IbcShieldingTransferAsset::LookupNamadaAddress { + port_id, + channel_id, + token, + }, } } diff --git a/crates/apps_lib/src/cli/client.rs b/crates/apps_lib/src/cli/client.rs index d926671e17..7899bdcc5d 100644 --- a/crates/apps_lib/src/cli/client.rs +++ b/crates/apps_lib/src/cli/client.rs @@ -113,6 +113,21 @@ impl CliApi { let namada = ctx.to_sdk(client, io); tx::submit_ibc_transfer(&namada, args).await?; } + Sub::TxOsmosisSwap(TxOsmosisSwap(args)) => { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + let ledger_address = + chain_ctx.get(&args.transfer.tx.ledger_address); + let client = client.unwrap_or_else(|| { + C::from_tendermint_address(&ledger_address) + }); + client.wait_until_node_is_synced(&io).await?; + + let args = args.to_sdk(&mut ctx)?; + let namada = ctx.to_sdk(client, io); + let args = args.into_ibc_transfer(&namada).await?; + + tx::submit_ibc_transfer(&namada, args).await?; + } Sub::TxUpdateAccount(TxUpdateAccount(args)) => { let chain_ctx = ctx.borrow_mut_chain_or_exit(); let ledger_address = diff --git a/crates/apps_lib/src/client/tx.rs b/crates/apps_lib/src/client/tx.rs index f954e43dc4..a2f8b03037 100644 --- a/crates/apps_lib/src/client/tx.rs +++ b/crates/apps_lib/src/client/tx.rs @@ -1909,12 +1909,13 @@ pub async fn gen_ibc_shielding_transfer( context: &impl Namada, args: args::GenIbcShieldingTransfer, ) -> Result<(), error::Error> { - if let Some(masp_tx) = - tx::gen_ibc_shielding_transfer(context, args.clone()).await? + let output_folder = args.output_folder.clone(); + + if let Some(masp_tx) = tx::gen_ibc_shielding_transfer(context, args).await? { let tx_id = masp_tx.txid().to_string(); let filename = format!("ibc_masp_tx_{}.memo", tx_id); - let output_path = match &args.output_folder { + let output_path = match output_folder { Some(path) => path.join(filename), None => filename.into(), }; diff --git a/crates/core/src/address.rs b/crates/core/src/address.rs index 0590c695eb..962b40d66c 100644 --- a/crates/core/src/address.rs +++ b/crates/core/src/address.rs @@ -393,6 +393,18 @@ impl Debug for Address { } } +impl From<&Address> for Signer { + fn from(address: &Address) -> Signer { + address.to_string().into() + } +} + +impl From
for Signer { + fn from(address: Address) -> Signer { + (&address).into() + } +} + impl TryFrom<&Signer> for Address { type Error = DecodeError; diff --git a/crates/core/src/token.rs b/crates/core/src/token.rs index 7b1b4b8d62..09028217f9 100644 --- a/crates/core/src/token.rs +++ b/crates/core/src/token.rs @@ -534,6 +534,15 @@ impl DenominatedAmount { .ok_or(AmountParseError::PrecisionOverflow) } + /// Create a new [`DenominatedAmount`] with the same underlying + /// amout but a new denomination. + pub fn redenominate(self, new_denom: u8) -> Self { + Self { + amount: self.amount, + denom: new_denom.into(), + } + } + /// Multiply this number by 10^denom and return the computed integer if /// possible. Otherwise error out. pub fn scale( diff --git a/crates/ibc/Cargo.toml b/crates/ibc/Cargo.toml index f520e810c3..c6f2be9004 100644 --- a/crates/ibc/Cargo.toml +++ b/crates/ibc/Cargo.toml @@ -45,6 +45,9 @@ konst.workspace = true linkme = {workspace = true, optional = true} ibc.workspace = true ibc-derive.workspace = true +ibc-middleware-module.workspace = true +ibc-middleware-module-macros.workspace = true +ibc-middleware-overflow-receive.workspace = true ibc-middleware-packet-forward.workspace = true ibc-testkit = {workspace = true, optional = true} ics23.workspace = true diff --git a/crates/ibc/src/context/middlewares.rs b/crates/ibc/src/context/middlewares.rs index 8d65666e63..d563071bde 100644 --- a/crates/ibc/src/context/middlewares.rs +++ b/crates/ibc/src/context/middlewares.rs @@ -1,7 +1,7 @@ //! Middleware entry points on Namada. pub mod pfm_mod; -// mod crossroads_mod; +pub mod shielded_recv; use std::cell::RefCell; use std::collections::BTreeSet; @@ -12,16 +12,18 @@ use std::rc::Rc; use ibc::core::host::types::identifiers::PortId; use ibc::core::router::module::Module; use ibc::core::router::types::module::ModuleId; -use ibc_middleware_packet_forward::{PacketForwardMiddleware, PfmContext}; +use ibc_middleware_overflow_receive::OverflowReceiveMiddleware; +use ibc_middleware_packet_forward::PacketForwardMiddleware; use namada_core::address::Address; use self::pfm_mod::PfmTransferModule; +use self::shielded_recv::ShieldedRecvModule; use crate::context::transfer_mod::TransferModule; use crate::{IbcCommonContext, IbcStorageContext}; /// The stack of middlewares of the transfer module. pub type TransferMiddlewares = - PacketForwardMiddleware>; + OverflowReceiveMiddleware>; /// Create a new instance of [`TransferMiddlewares`] pub fn create_transfer_middlewares( @@ -32,16 +34,18 @@ where C: IbcCommonContext + Debug, Params: namada_systems::parameters::Read<::Storage>, { - PacketForwardMiddleware::wrap(PfmTransferModule { - transfer_module: TransferModule::new(ctx, verifiers), - _phantom: PhantomData, + OverflowReceiveMiddleware::wrap(ShieldedRecvModule { + next: PacketForwardMiddleware::wrap(PfmTransferModule { + transfer_module: TransferModule::new(ctx, verifiers), + _phantom: PhantomData, + }), }) } impl crate::ModuleWrapper for TransferMiddlewares where C: IbcCommonContext + Debug, - PfmTransferModule: PfmContext, + Params: namada_systems::parameters::Read<::Storage>, { fn as_module(&self) -> &dyn Module { self diff --git a/crates/ibc/src/context/middlewares/pfm_mod.rs b/crates/ibc/src/context/middlewares/pfm_mod.rs index 70454fbf4a..f414695a5f 100644 --- a/crates/ibc/src/context/middlewares/pfm_mod.rs +++ b/crates/ibc/src/context/middlewares/pfm_mod.rs @@ -26,6 +26,8 @@ use ibc::core::host::types::identifiers::{ use ibc::core::router::module::Module; use ibc::core::router::types::module::ModuleExtras; use ibc::primitives::Signer; +use ibc_middleware_module::MiddlewareModule; +use ibc_middleware_module_macros::from_middleware; use ibc_middleware_packet_forward::{ InFlightPacket, InFlightPacketKey, PfmContext, }; @@ -60,167 +62,27 @@ impl Debug } } -impl Module for PfmTransferModule +from_middleware! { + impl Module for PfmTransferModule + where + C: IbcCommonContext + Debug, +} + +impl MiddlewareModule for PfmTransferModule where C: IbcCommonContext + Debug, { - fn on_chan_open_init_validate( - &self, - order: Order, - connection_hops: &[ConnectionId], - port_id: &PortId, - channel_id: &ChannelId, - counterparty: &Counterparty, - version: &Version, - ) -> Result { - self.transfer_module.on_chan_open_init_validate( - order, - connection_hops, - port_id, - channel_id, - counterparty, - version, - ) - } - - fn on_chan_open_init_execute( - &mut self, - order: Order, - connection_hops: &[ConnectionId], - port_id: &PortId, - channel_id: &ChannelId, - counterparty: &Counterparty, - version: &Version, - ) -> Result<(ModuleExtras, Version), ChannelError> { - self.transfer_module.on_chan_open_init_execute( - order, - connection_hops, - port_id, - channel_id, - counterparty, - version, - ) - } - - fn on_chan_open_try_validate( - &self, - order: Order, - connection_hops: &[ConnectionId], - port_id: &PortId, - channel_id: &ChannelId, - counterparty: &Counterparty, - counterparty_version: &Version, - ) -> Result { - self.transfer_module.on_chan_open_try_validate( - order, - connection_hops, - port_id, - channel_id, - counterparty, - counterparty_version, - ) - } - - fn on_chan_open_try_execute( - &mut self, - order: Order, - connection_hops: &[ConnectionId], - port_id: &PortId, - channel_id: &ChannelId, - counterparty: &Counterparty, - counterparty_version: &Version, - ) -> Result<(ModuleExtras, Version), ChannelError> { - self.transfer_module.on_chan_open_try_execute( - order, - connection_hops, - port_id, - channel_id, - counterparty, - counterparty_version, - ) - } - - fn on_chan_open_ack_validate( - &self, - port_id: &PortId, - channel_id: &ChannelId, - counterparty_version: &Version, - ) -> Result<(), ChannelError> { - self.transfer_module.on_chan_open_ack_validate( - port_id, - channel_id, - counterparty_version, - ) - } - - fn on_chan_open_ack_execute( - &mut self, - port_id: &PortId, - channel_id: &ChannelId, - counterparty_version: &Version, - ) -> Result { - self.transfer_module.on_chan_open_ack_execute( - port_id, - channel_id, - counterparty_version, - ) - } - - fn on_chan_open_confirm_validate( - &self, - port_id: &PortId, - channel_id: &ChannelId, - ) -> Result<(), ChannelError> { - self.transfer_module - .on_chan_open_confirm_validate(port_id, channel_id) - } + type NextMiddleware = TransferModule; - fn on_chan_open_confirm_execute( - &mut self, - port_id: &PortId, - channel_id: &ChannelId, - ) -> Result { - self.transfer_module - .on_chan_open_confirm_execute(port_id, channel_id) + fn next_middleware(&self) -> &Self::NextMiddleware { + &self.transfer_module } - fn on_chan_close_init_validate( - &self, - port_id: &PortId, - channel_id: &ChannelId, - ) -> Result<(), ChannelError> { - self.transfer_module - .on_chan_close_init_validate(port_id, channel_id) + fn next_middleware_mut(&mut self) -> &mut Self::NextMiddleware { + &mut self.transfer_module } - fn on_chan_close_init_execute( - &mut self, - port_id: &PortId, - channel_id: &ChannelId, - ) -> Result { - self.transfer_module - .on_chan_close_init_execute(port_id, channel_id) - } - - fn on_chan_close_confirm_validate( - &self, - port_id: &PortId, - channel_id: &ChannelId, - ) -> Result<(), ChannelError> { - self.transfer_module - .on_chan_close_confirm_validate(port_id, channel_id) - } - - fn on_chan_close_confirm_execute( - &mut self, - port_id: &PortId, - channel_id: &ChannelId, - ) -> Result { - self.transfer_module - .on_chan_close_confirm_execute(port_id, channel_id) - } - - fn on_recv_packet_execute( + fn middleware_on_recv_packet_execute( &mut self, packet: &Packet, relayer: &Signer, @@ -243,50 +105,6 @@ where self.transfer_module.on_recv_packet_execute(packet, relayer) } } - - fn on_acknowledgement_packet_validate( - &self, - packet: &Packet, - acknowledgement: &Acknowledgement, - relayer: &Signer, - ) -> Result<(), PacketError> { - self.transfer_module.on_acknowledgement_packet_validate( - packet, - acknowledgement, - relayer, - ) - } - - fn on_acknowledgement_packet_execute( - &mut self, - packet: &Packet, - acknowledgement: &Acknowledgement, - relayer: &Signer, - ) -> (ModuleExtras, Result<(), PacketError>) { - self.transfer_module.on_acknowledgement_packet_execute( - packet, - acknowledgement, - relayer, - ) - } - - fn on_timeout_packet_validate( - &self, - packet: &Packet, - relayer: &Signer, - ) -> Result<(), PacketError> { - self.transfer_module - .on_timeout_packet_validate(packet, relayer) - } - - fn on_timeout_packet_execute( - &mut self, - packet: &Packet, - relayer: &Signer, - ) -> (ModuleExtras, Result<(), PacketError>) { - self.transfer_module - .on_timeout_packet_execute(packet, relayer) - } } impl PfmContext for PfmTransferModule diff --git a/crates/ibc/src/context/middlewares/shielded_recv.rs b/crates/ibc/src/context/middlewares/shielded_recv.rs new file mode 100644 index 0000000000..56aa5d42d0 --- /dev/null +++ b/crates/ibc/src/context/middlewares/shielded_recv.rs @@ -0,0 +1,213 @@ +//! This middleware is to handle automatically shielding the results of a +//! shielded swap. +//! +//! Since we do not know the resulting amount of assets from the swap ahead of +//! time, we cannot create a MASP note at the onset. We instead, create a note +//! for the minimum amount, which will be shielded. All assets exceeding the +//! minimum amount will be transferred to an overflow address specified by +//! the user. + +use std::cell::RefCell; +use std::collections::BTreeSet; +use std::fmt::{Debug, Formatter}; +use std::rc::Rc; + +use ibc::apps::transfer::context::TokenTransferExecutionContext; +use ibc::apps::transfer::types::packet::PacketData; +use ibc::apps::transfer::types::{Coin, PrefixedDenom}; +use ibc::core::channel::types::acknowledgement::{ + Acknowledgement, AcknowledgementStatus, StatusValue as AckStatusValue, +}; +use ibc::core::channel::types::channel::{Counterparty, Order}; +use ibc::core::channel::types::error::{ChannelError, PacketError}; +use ibc::core::channel::types::packet::Packet; +use ibc::core::channel::types::Version; +use ibc::core::host::types::identifiers::{ChannelId, ConnectionId, PortId}; +use ibc::core::router::module::Module; +use ibc::core::router::types::module::ModuleExtras; +use ibc::primitives::Signer; +use ibc_middleware_module::MiddlewareModule; +use ibc_middleware_module_macros::from_middleware; +use ibc_middleware_overflow_receive::OverflowRecvContext; +use ibc_middleware_packet_forward::PacketForwardMiddleware; +use namada_core::address::{Address, MASP, MULTITOKEN}; +use namada_core::token; +use serde_json::{Map, Value}; + +use crate::context::middlewares::pfm_mod::PfmTransferModule; +use crate::msg::{NamadaMemo, OsmosisSwapMemoData}; +use crate::{Error, IbcCommonContext, IbcStorageContext, TokenTransferContext}; + +/// A middleware for handling IBC pockets received +/// after a shielded swap. The minimum amount will +/// be shielded and the rest placed in an overflow +/// account. +pub struct ShieldedRecvModule +where + C: IbcCommonContext + Debug, + Params: namada_systems::parameters::Read<::Storage>, +{ + /// The next middleware module + pub next: PacketForwardMiddleware>, +} + +impl ShieldedRecvModule +where + C: IbcCommonContext + Debug, + Params: namada_systems::parameters::Read<::Storage>, +{ + fn insert_verifier(&self, address: Address) { + self.next + .next() + .transfer_module + .ctx + .verifiers + .borrow_mut() + .insert(address); + } + + fn get_ctx(&self) -> Rc> { + self.next.next().transfer_module.ctx.inner.clone() + } + + fn get_verifiers(&self) -> Rc>> { + self.next.next().transfer_module.ctx.verifiers.clone() + } +} + +impl Debug for ShieldedRecvModule +where + C: IbcCommonContext + Debug, + Params: namada_systems::parameters::Read<::Storage>, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct(stringify!(ShieldedRecvModule)) + .field("next", &self.next) + .finish() + } +} + +from_middleware! { + impl Module for ShieldedRecvModule + where + C: IbcCommonContext + Debug, + Params: namada_systems::parameters::Read<::Storage>, +} + +impl MiddlewareModule for ShieldedRecvModule +where + C: IbcCommonContext + Debug, + Params: namada_systems::parameters::Read<::Storage>, +{ + type NextMiddleware = PacketForwardMiddleware>; + + fn next_middleware(&self) -> &Self::NextMiddleware { + &self.next + } + + fn next_middleware_mut(&mut self) -> &mut Self::NextMiddleware { + &mut self.next + } + + fn middleware_on_recv_packet_execute( + &mut self, + packet: &Packet, + relayer: &Signer, + ) -> (ModuleExtras, Option) { + let Ok(data) = serde_json::from_slice::(&packet.data) + else { + // NB: this isn't an ICS-20 packet + return self.next.on_recv_packet_execute(packet, relayer); + }; + let Ok(memo) = serde_json::from_str::>( + data.memo.as_ref(), + ) else { + // NB: this isn't a shielded recv packet + return self.next.on_recv_packet_execute(packet, relayer); + }; + + if data.receiver.as_ref() != MASP.to_string() { + let ack = AcknowledgementStatus::error( + AckStatusValue::new(format!( + "Shielded receive error: Address {:?} is not the MASP", + data.receiver.as_ref() + )) + .expect("Ack is not empty"), + ); + return (ModuleExtras::empty(), Some(ack.into())); + } + + self.insert_verifier(memo.namada.osmosis_swap.overflow_receiver); + self.insert_verifier(MULTITOKEN); + + self.next.on_recv_packet_execute(packet, relayer) + } +} + +impl ibc_middleware_overflow_receive::PacketMetadata + for NamadaMemo +{ + type AccountId = Address; + type Amount = token::Amount; + + fn is_overflow_receive_msg(msg: &Map) -> bool { + msg.get("namada").map_or(false, |maybe_namada_obj| { + maybe_namada_obj + .as_object() + .map_or(false, |namada| namada.contains_key("osmosis_swap")) + }) + } + + fn strip_middleware_msg( + json_obj_memo: Map, + ) -> Map { + json_obj_memo + } + + fn overflow_receiver(&self) -> &Address { + &self.namada.osmosis_swap.overflow_receiver + } + + fn target_amount(&self) -> &token::Amount { + &self.namada.osmosis_swap.shielded_amount + } +} + +impl OverflowRecvContext for ShieldedRecvModule +where + C: IbcCommonContext + Debug, + Params: namada_systems::parameters::Read<::Storage>, +{ + type Error = Error; + type PacketMetadata = NamadaMemo; + + fn mint_coins_execute( + &mut self, + receiver: &Address, + coin: &Coin, + ) -> Result<(), Self::Error> { + let ctx = self.get_ctx(); + let verifiers = self.get_verifiers(); + let mut token_transfer_context = + TokenTransferContext::new(ctx, verifiers); + token_transfer_context + .mint_coins_execute(receiver, coin) + .map_err(Error::TokenTransfer) + } + + fn unescrow_coins_execute( + &mut self, + receiver: &Address, + port: &PortId, + channel: &ChannelId, + coin: &Coin, + ) -> Result<(), Self::Error> { + let ctx = self.get_ctx(); + let verifiers = self.get_verifiers(); + let mut token_transfer_context = + TokenTransferContext::new(ctx, verifiers); + token_transfer_context + .unescrow_coins_execute(receiver, port, channel, coin) + .map_err(Error::TokenTransfer) + } +} diff --git a/crates/ibc/src/lib.rs b/crates/ibc/src/lib.rs index 69a064dcb6..ea03869024 100644 --- a/crates/ibc/src/lib.rs +++ b/crates/ibc/src/lib.rs @@ -617,7 +617,7 @@ where tx_data: &[u8], ) -> Result<(Option, Option), Error> { let message = decode_message::(tx_data)?; - match message { + let result = match message { IbcMessage::Transfer(msg) => { let mut token_transfer_ctx = TokenTransferContext::new( self.ctx.inner.clone(), @@ -635,7 +635,6 @@ where )) })?, ); - self.insert_verifiers()?; if msg.transfer.is_some() { token_transfer_ctx.enable_shielded_transfer(); } @@ -665,7 +664,6 @@ where )) })?, ); - self.insert_verifiers()?; send_nft_transfer_execute( &mut self.ctx, &mut nft_transfer_ctx, @@ -685,7 +683,6 @@ where )) })?, ); - self.insert_verifiers()?; } execute(&mut self.ctx, &mut self.router, *envelope.clone()) .map_err(|e| Error::Context(Box::new(e)))?; @@ -710,7 +707,9 @@ where }; Ok((None, masp_tx)) } - } + }; + self.insert_verifiers()?; + result } /// Check the result of receiving the packet by checking the packet @@ -748,13 +747,12 @@ where let verifiers = Rc::new(RefCell::new(BTreeSet::
::new())); let message = decode_message::(tx_data)?; - match message { + let result = match message { IbcMessage::Transfer(msg) => { let mut token_transfer_ctx = TokenTransferContext::new( self.ctx.inner.clone(), verifiers.clone(), ); - self.insert_verifiers()?; if msg.transfer.is_some() { token_transfer_ctx.enable_shielded_transfer(); } @@ -782,7 +780,9 @@ where validate(&self.ctx, &self.router, *envelope) .map_err(|e| Error::Context(Box::new(e))) } - } + }; + self.insert_verifiers()?; + result } fn insert_verifiers(&self) -> Result<(), Error> { diff --git a/crates/ibc/src/msg.rs b/crates/ibc/src/msg.rs index 8a15817c00..c5d75407df 100644 --- a/crates/ibc/src/msg.rs +++ b/crates/ibc/src/msg.rs @@ -1,4 +1,6 @@ use std::collections::BTreeMap; +use std::fmt; +use std::str::FromStr; use borsh::schema::{Declaration, Definition, Fields}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; @@ -16,6 +18,106 @@ use ibc::core::host::types::identifiers::PortId; use ibc::primitives::proto::Protobuf; use masp_primitives::transaction::Transaction as MaspTransaction; use namada_core::borsh::BorshSerializeExt; +use namada_core::string_encoding::StringEncoded; +use serde::{Deserialize, Serialize}; + +trait Sealed {} + +/// Marker trait that denotes whether an IBC memo is valid +/// in Namada. +#[allow(private_bounds)] +pub trait ValidNamadaMemo: Sealed {} + +impl Sealed for NamadaMemo {} +impl ValidNamadaMemo for NamadaMemo {} + +impl Sealed for NamadaMemo {} +impl ValidNamadaMemo for NamadaMemo {} + +/// Osmosis swap memo data. +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct OsmosisSwapMemoData { + /// The inner memo data. + pub osmosis_swap: OsmosisSwapMemoDataInner, +} + +/// Osmosis swap inner memo data. +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct OsmosisSwapMemoDataInner { + /// Shielding transfer data. Hex encodes the borsh serialized MASP + /// transfer. + pub shielding_data: StringEncoded, + /// The amount that is shielded onto the MASP. Corresponds to the + /// minimum output amount from the swap. + pub shielded_amount: namada_core::token::Amount, + /// The receiver of the difference between the transferred tokens and + /// the minimum output amount. + pub overflow_receiver: namada_core::address::Address, +} + +impl From> for NamadaMemo { + fn from(memo: NamadaMemo) -> Self { + memo.namada.into() + } +} + +impl From for NamadaMemo { + fn from( + OsmosisSwapMemoData { + osmosis_swap: + OsmosisSwapMemoDataInner { + shielding_data, + shielded_amount, + overflow_receiver, + }, + }: OsmosisSwapMemoData, + ) -> Self { + Self { + namada: NamadaMemoData::OsmosisSwap { + overflow_receiver, + shielded_amount, + shielding_data, + }, + } + } +} + +impl From for NamadaMemo { + fn from(data: OsmosisSwapMemoData) -> Self { + Self { namada: data } + } +} + +/// Memo data serialized as a JSON object included +/// in IBC packets. +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct NamadaMemo { + /// The inner memo data. + pub namada: Data, +} + +/// Data included in a Namada memo. +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum NamadaMemoData { + /// Generic message sent over IBC. + Memo(String), + /// Osmosis swap message. + OsmosisSwap { + /// Shielding transfer data. Hex encodes the borsh serialized MASP + /// transfer. + shielding_data: StringEncoded, + /// The amount that is shielded onto the MASP. Corresponds to the + /// minimum output amount from the swap. + shielded_amount: namada_core::token::Amount, + /// The receiver of the difference between the transferred tokens and + /// the minimum output amount. + overflow_receiver: namada_core::address::Address, + }, +} /// The different variants of an Ibc message #[derive(Debug, Clone)] @@ -132,9 +234,32 @@ impl BorshSchema for MsgNftTransfer { #[derive(Debug, Clone, BorshDeserialize, BorshSerialize)] pub struct IbcShieldingData(pub MaspTransaction); +impl From<&IbcShieldingData> for String { + fn from(data: &IbcShieldingData) -> Self { + HEXUPPER.encode(&data.serialize_to_vec()) + } +} + impl From for String { fn from(data: IbcShieldingData) -> Self { - HEXUPPER.encode(&data.serialize_to_vec()) + (&data).into() + } +} + +impl fmt::Display for IbcShieldingData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", String::from(self)) + } +} + +impl FromStr for IbcShieldingData { + type Err = String; + + fn from_str(s: &str) -> Result { + let bytes = HEXUPPER + .decode(s.as_bytes()) + .map_err(|err| err.to_string())?; + IbcShieldingData::try_from_slice(&bytes).map_err(|err| err.to_string()) } } @@ -154,8 +279,17 @@ pub fn extract_masp_tx_from_envelope( pub fn decode_ibc_shielding_data( s: impl AsRef, ) -> Option { - let bytes = HEXUPPER.decode(s.as_ref().as_bytes()).ok()?; - IbcShieldingData::try_from_slice(&bytes).ok() + let sref = s.as_ref(); + + serde_json::from_str(sref).map_or_else( + |_| sref.parse().ok(), + |NamadaMemo { namada: memo_data }| match memo_data { + NamadaMemoData::Memo(memo) => memo.parse().ok(), + NamadaMemoData::OsmosisSwap { shielding_data, .. } => { + Some(shielding_data.raw) + } + }, + ) } /// Extract MASP transaction from IBC packet memo diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 517a0baaa1..f81e052373 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -91,6 +91,7 @@ namada_wallet = {path = "../wallet" } arbitrary = { workspace = true, optional = true } async-trait.workspace = true +bech32.workspace = true bimap.workspace = true borsh.workspace = true circular-queue.workspace = true diff --git a/crates/sdk/src/args.rs b/crates/sdk/src/args.rs index 84274231ff..24168c7b78 100644 --- a/crates/sdk/src/args.rs +++ b/crates/sdk/src/args.rs @@ -8,7 +8,7 @@ use std::time::Duration as StdDuration; use either::Either; use masp_primitives::transaction::components::sapling::builder::BuildParams; use masp_primitives::zip32::PseudoExtendedKey; -use namada_core::address::Address; +use namada_core::address::{Address, MASP}; use namada_core::chain::{BlockHeight, ChainId, Epoch}; use namada_core::collections::HashMap; use namada_core::dec::Dec; @@ -16,21 +16,30 @@ use namada_core::ethereum_events::EthAddress; use namada_core::keccak::KeccakHash; use namada_core::key::{common, SchemeType}; use namada_core::masp::{MaspEpoch, PaymentAddress}; +use namada_core::string_encoding::StringEncoded; use namada_core::time::DateTimeUtc; +use namada_core::token::Amount; use namada_core::{storage, token}; use namada_governance::cli::onchain::{ DefaultProposal, PgfFundingProposal, PgfStewardProposal, }; use namada_ibc::IbcShieldingData; +use namada_io::{display_line, Io}; use namada_token::masp::utils::RetryStrategy; use namada_tx::data::GasLimit; use namada_tx::Memo; use serde::{Deserialize, Serialize}; use zeroize::Zeroizing; +use crate::error::Error; use crate::eth_bridge::bridge_pool; use crate::ibc::core::host::types::identifiers::{ChannelId, PortId}; -use crate::signing::SigningTxData; +use crate::ibc::{NamadaMemo, NamadaMemoData}; +use crate::rpc::{ + get_registry_from_xcs_osmosis_contract, osmosis_denom_from_namada_denom, + query_osmosis_pool_routes, +}; +use crate::signing::{gen_disposable_signing_key, SigningTxData}; use crate::wallet::{DatedSpendingKey, DatedViewingKey}; use crate::{rpc, tx, Namada}; @@ -477,6 +486,321 @@ impl TxUnshieldingTransfer { } } +/// Individual hop of some route to take through Osmosis pools. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct OsmosisPoolHop { + /// The id of the pool to use on Osmosis. + pub pool_id: String, + /// The output denomination expected from the + /// pool on Osmosis. + pub token_out_denom: String, +} + +impl FromStr for OsmosisPoolHop { + type Err = String; + + fn from_str(s: &str) -> Result { + s.split_once(':').map_or_else( + || { + Err(format!( + "Expected : string, but found \ + {s:?} instead" + )) + }, + |(pool_id, token_out_denom)| { + Ok(OsmosisPoolHop { + pool_id: pool_id.to_owned(), + token_out_denom: token_out_denom.to_owned(), + }) + }, + ) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +/// Constraints on the osmosis swap +pub enum Slippage { + /// Specifies the minimum amount to be received + MinOutputAmount(Amount), + /// A time-weighted average price + Twap { + /// The maximum percentage difference allowed between the estimated and + /// actual trade price. This must be a decimal number in the range + /// `[0, 100]`. + slippage_percentage: String, + /// The time period (in seconds) over which the average price is + /// calculated + window_seconds: u64, + }, +} + +/// An token swap on Osmosis +#[derive(Debug, Clone)] +pub struct TxOsmosisSwap { + /// The IBC transfer data + pub transfer: TxIbcTransfer, + /// The token we wish to receive (on Namada) + pub output_denom: String, + /// Address of the recipient on Namada + pub recipient: Either, + /// Address to receive funds exceeding the minimum amount, + /// in case of IBC shieldings + /// + /// If unspecified, a disposable address is generated to + /// receive funds with + pub overflow: Option, + /// Constraints on the osmosis swap + pub slippage: Slippage, + /// Recovery address (on Osmosis) in case of failure + pub local_recovery_addr: String, + /// The route to take through Osmosis pools + pub route: Option>, + /// A REST rpc endpoint to Osmosis + pub osmosis_rest_rpc: String, +} + +impl TxOsmosisSwap { + /// Create an IBC transfer from the input arguments + pub async fn into_ibc_transfer( + self, + ctx: &impl Namada, + ) -> crate::error::Result> { + #[derive(Serialize)] + struct Memo { + wasm: Wasm, + } + + #[derive(Serialize)] + struct Wasm { + contract: String, + msg: Message, + } + + #[derive(Serialize)] + struct Message { + osmosis_swap: OsmosisSwap, + } + + #[derive(Serialize)] + struct OsmosisSwap { + receiver: String, + output_denom: String, + slippage: Slippage, + on_failed_delivery: LocalRecoveryAddr, + route: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + final_memo: Option>, + } + + #[derive(Serialize)] + struct LocalRecoveryAddr { + local_recovery_addr: String, + } + + #[inline] + fn assert_json_obj( + value: serde_json::Value, + ) -> serde_json::Map { + match value { + serde_json::Value::Object(x) => x, + _ => unreachable!(), + } + } + + const OSMOSIS_SQS_SERVER: &str = "https://sqsprod.osmosis.zone"; + + let Self { + mut transfer, + recipient, + slippage, + local_recovery_addr, + route, + overflow, + osmosis_rest_rpc, + output_denom: namada_output_denom, + } = self; + + let recipient = recipient.map_either( + |addr| addr, + |payment_addr| async move { + let overflow_receiver = if let Some(overflow) = overflow { + overflow + } else { + let addr = (&gen_disposable_signing_key(ctx).await).into(); + display_line!( + ctx.io(), + "Sending unshielded funds to disposable address {addr}", + ); + addr + }; + (payment_addr, overflow_receiver) + }, + ); + + // validate `local_recovery_addr` and the contract addr + if !bech32::decode(&local_recovery_addr) + .is_ok_and(|(hrp, _, _)| hrp == "osmo") + { + // TODO: validate that addr has 20 bytes? + return Err(Error::Other(format!( + "Invalid Osmosis recovery address {local_recovery_addr:?}" + ))); + } + if !bech32::decode(&transfer.receiver) + .is_ok_and(|(hrp, _, _)| hrp == "osmo") + { + // TODO: validate that addr has 32 bytes? + return Err(Error::Other(format!( + "Invalid Osmosis contract address {local_recovery_addr:?}" + ))); + } + + let registry_xcs_addr = get_registry_from_xcs_osmosis_contract( + &osmosis_rest_rpc, + &transfer.receiver, + ) + .await?; + + let (osmosis_output_denom, namada_output_addr) = + osmosis_denom_from_namada_denom( + &osmosis_rest_rpc, + ®istry_xcs_addr, + &namada_output_denom, + ) + .await?; + + let route = if let Some(route) = route { + route + } else { + query_osmosis_pool_routes( + ctx, + &transfer.token, + transfer.amount, + transfer.channel_id.clone(), + &osmosis_output_denom, + OSMOSIS_SQS_SERVER, + ) + .await? + .pop() + .ok_or_else(|| { + Error::Other(format!( + "No route found to swap {:?} of {} with {}", + transfer.amount, transfer.token, namada_output_addr, + )) + })? + }; + + let (receiver, slippage, final_memo) = match recipient { + Either::Left(transparent_recipient) => { + (transparent_recipient.to_string(), slippage, None) + } + Either::Right(fut) => { + let (payment_addr, overflow_receiver) = fut.await; + + let amount_to_shield = match slippage { + Slippage::MinOutputAmount(amount_to_shield) => { + amount_to_shield + } + Slippage::Twap { .. } => todo!( + "Cannot compute min output amount from slippage TWAP \ + yet" + ), + }; + + let shielding_tx = tx::gen_ibc_shielding_transfer( + ctx, + GenIbcShieldingTransfer { + query: Query { + ledger_address: transfer.tx.ledger_address.clone(), + }, + output_folder: None, + target: + namada_core::masp::TransferTarget::PaymentAddress( + payment_addr, + ), + asset: IbcShieldingTransferAsset::Address( + namada_output_addr, + ), + amount: InputAmount::Validated( + token::DenominatedAmount::new( + amount_to_shield, + 0u8.into(), + ), + ), + expiration: transfer.tx.expiration.clone(), + }, + ) + .await? + .ok_or_else(|| { + Error::Other( + "Failed to generate IBC shielding transfer".to_owned(), + ) + })?; + + let memo = assert_json_obj( + serde_json::to_value(&NamadaMemo { + namada: NamadaMemoData::OsmosisSwap { + shielding_data: StringEncoded::new( + IbcShieldingData(shielding_tx), + ), + shielded_amount: amount_to_shield, + overflow_receiver, + }, + }) + .unwrap(), + ); + + ( + MASP.to_string(), + Slippage::MinOutputAmount(amount_to_shield), + Some(memo), + ) + } + }; + + let cosmwasm_memo = Memo { + wasm: Wasm { + contract: transfer.receiver.clone(), + msg: Message { + osmosis_swap: OsmosisSwap { + output_denom: osmosis_output_denom, + slippage, + final_memo, + receiver, + on_failed_delivery: LocalRecoveryAddr { + local_recovery_addr, + }, + route, + }, + }, + }, + }; + let namada_memo = transfer.ibc_memo.take().map(|memo| { + assert_json_obj( + serde_json::to_value(&NamadaMemo { + namada: NamadaMemoData::Memo(memo), + }) + .unwrap(), + ) + }); + + let memo = { + let mut m = serde_json::to_value(&cosmwasm_memo).unwrap(); + let m_obj = m.as_object_mut().unwrap(); + + if let Some(mut namada_memo) = namada_memo { + m_obj.append(&mut namada_memo); + } + + m + }; + + transfer.ibc_memo = Some(serde_json::to_string(&memo).unwrap()); + Ok(transfer) + } +} + /// IBC transfer transaction arguments #[derive(Clone, Debug)] pub struct TxIbcTransfer { @@ -2918,14 +3242,26 @@ pub struct GenIbcShieldingTransfer { pub output_folder: Option, /// The target address pub target: C::TransferTarget, - /// The token address which could be a non-namada address - pub token: String, /// Transferred token amount pub amount: InputAmount, /// The optional expiration of the masp shielding transaction pub expiration: TxExpiration, - /// Port ID via which the token is received - pub port_id: PortId, - /// Channel ID via which the token is received - pub channel_id: ChannelId, + /// Asset to shield over IBC to Namada + pub asset: IbcShieldingTransferAsset, +} + +/// IBC shielding transfer asset, to be used by [`GenIbcShieldingTransfer`] +#[derive(Clone, Debug)] +pub enum IbcShieldingTransferAsset { + /// Attempt to look-up the address of the asset to shield on Namada + LookupNamadaAddress { + /// The token address which could be a non-namada address + token: String, + /// Port ID via which the token is received + port_id: PortId, + /// Channel ID via which the token is received + channel_id: ChannelId, + }, + /// Namada address of the token that will be received. + Address(C::Address), } diff --git a/crates/sdk/src/rpc.rs b/crates/sdk/src/rpc.rs index 2e9792b61c..eac8e2159e 100644 --- a/crates/sdk/src/rpc.rs +++ b/crates/sdk/src/rpc.rs @@ -2,6 +2,7 @@ #![allow(clippy::result_large_err)] +use core::str::FromStr; use std::cell::Cell; use std::collections::{BTreeMap, BTreeSet}; use std::ops::ControlFlow; @@ -16,6 +17,9 @@ use namada_core::arith::checked; use namada_core::chain::{BlockHeight, Epoch}; use namada_core::collections::{HashMap, HashSet}; use namada_core::hash::Hash; +use namada_core::ibc::apps::nft_transfer::types::TracePrefix; +use namada_core::ibc::apps::transfer::types::PrefixedDenom; +use namada_core::ibc::core::host::types::identifiers::ChannelId; use namada_core::ibc::IbcTokenHash; use namada_core::key::common; use namada_core::masp::MaspEpoch; @@ -36,9 +40,11 @@ use namada_governance::storage::proposal::{ use namada_governance::utils::{ compute_proposal_result, ProposalResult, ProposalVotes, Vote, }; +use namada_ibc::core::host::types::identifiers::PortId; use namada_ibc::storage::{ ibc_trace_key, ibc_trace_key_prefix, is_ibc_trace_key, }; +use namada_ibc::trace::calc_ibc_token_hash; use namada_io::{display_line, edisplay_line, Client, Io}; use namada_parameters::{storage as params_storage, EpochDuration}; use namada_proof_of_stake::parameters::PosParams; @@ -51,9 +57,9 @@ use namada_state::{BlockHeader, LastBlock}; use namada_token::masp::MaspTokenRewardData; use namada_tx::data::{BatchedTxResult, DryRunResult, ResultCode, TxResult}; use namada_tx::event::{Batch as BatchAttr, Code as CodeAttr}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -use crate::args::InputAmount; +use crate::args::{InputAmount, OsmosisPoolHop}; use crate::control_flow::time; use crate::error::{EncodingError, Error, QueryError, TxSubmitError}; use crate::events::{extend, Event}; @@ -65,6 +71,7 @@ use crate::queries::RPC; use crate::tendermint::block::Height; use crate::tendermint::merkle::proof::ProofOps; use crate::tendermint_rpc::query::Query; +use crate::tx::get_ibc_src_port_channel; use crate::{error, Namada, Tx}; /// Query an estimate of the maximum block time. @@ -1518,3 +1525,376 @@ pub async fn query_ibc_denom( token.as_ref().to_string() } + +/// Query the registry contract embedded in the state of +/// an input Crosschain Swaps Osmosis contract. +pub async fn get_registry_from_xcs_osmosis_contract( + rest_rpc_addr: &str, + xcs_contract_addr: &str, +) -> Result { + #[derive(Deserialize)] + struct RespData { + models: Vec, + } + + #[derive(Deserialize)] + struct Model { + key: String, + value: String, + } + + #[derive(Deserialize)] + struct XcsConfig { + registry_contract: String, + } + + let request_url = format!( + "{rest_rpc_addr}/cosmwasm/wasm/v1/contract/{xcs_contract_addr}/state" + ); + let RespData { models } = reqwest::get(&request_url) + .await + .map_err(|e| { + Error::Other(format!( + "Failed to fetch headers of request {request_url:?}: {e}" + )) + })? + .json() + .await + .map_err(|e| { + Error::Other(format!( + "Failed to fetch JSON body of request {request_url:?}: {e}" + )) + })?; + + let Some(Model { + value: base64_encoded_config, + .. + }) = models.into_iter().find(|Model { key, .. }| { + // NB: this value corresponds to the hex encoding of the + // string "config". the crosschain swaps contract of the set + // of xcs contracts stores, in its internal state, the params + // it was initialized with, namely the address of the registry + // contract. the point behind querying the initialization + // params is to ultimately query the address of the registry + // contract. + const HEX_ENCODED_CONFIG_KEY: &str = "636F6E666967"; + key == HEX_ENCODED_CONFIG_KEY + }) + else { + return Err(Error::Other(format!( + "Could not find config of XCS contract {xcs_contract_addr}" + ))); + }; + + let xcs_cfg_json = data_encoding::BASE64 + .decode(base64_encoded_config.as_bytes()) + .map_err(|e| Error::Other(e.to_string()))?; + + let XcsConfig { registry_contract } = serde_json::from_slice(&xcs_cfg_json) + .map_err(|e| Error::Other(e.to_string()))?; + + Ok(registry_contract) +} + +/// Given a Namada asset returned from an Osmosis swap, +/// find the corresponding asset denom on Osmosis. +/// +/// This is done by querying the XCS registry contract. The Namada asset +/// is also returned, parsed as an [`Address`]. +pub async fn osmosis_denom_from_namada_denom( + rest_rpc_addr: &str, + registry_contract_addr: &str, + namada_denom: &str, +) -> Result<(String, Address), Error> { + async fn fetch_contract_data( + contract_addr: &str, + rest_rpc_addr: &str, + json_query: &str, + ) -> Result { + #[derive(Deserialize)] + struct RespData { + data: String, + } + + let encoded_query = data_encoding::BASE64.encode(json_query.as_bytes()); + let request_url = format!( + "{rest_rpc_addr}/cosmwasm/wasm/v1/contract/{contract_addr}/smart/\ + {encoded_query}" + ); + + let RespData { data } = reqwest::get(&request_url) + .await + .map_err(|e| { + Error::Other(format!( + "Failed to fetch headers of request {request_url:?}: {e}" + )) + })? + .json() + .await + .map_err(|e| { + Error::Other(format!( + "Failed to fetch JSON body of request {request_url:?}: {e}" + )) + })?; + + Ok(data) + } + + let chain_name_req = |prefix| { + format!( + r#"{{"get_chain_name_from_bech32_prefix": {{"prefix": "{prefix}" }} }}"# + ) + }; + let channel_pair_req = |src, dest| { + format!( + r#"{{"get_channel_from_chain_pair": {{"source_chain": "{src}", "destination_chain": "{dest}" }} }}"# + ) + }; + let dest_chain_req = |on_chain, via_channel| { + format!( + r#"{{"get_destination_chain_from_source_chain_via_channel": {{"on_chain": "{on_chain}", "via_channel": "{via_channel}" }} }}"# + ) + }; + + //////////////////////////////////////////////////////////////////////////// + + let nam_denom = PrefixedDenom::from_str(namada_denom).map_err(|e| { + Error::Other(format!( + "Could not parse {namada_denom} as a trace path {e}" + )) + })?; + + let namada_chain_name = fetch_contract_data( + registry_contract_addr, + rest_rpc_addr, + &chain_name_req("tnam"), + ) + .await?; + let osmosis_chain_name = fetch_contract_data( + registry_contract_addr, + rest_rpc_addr, + &chain_name_req("osmo"), + ) + .await?; + + if nam_denom.trace_path.is_empty() { + // Namada native asset + + let address = nam_denom + .base_denom + .as_str() + .parse::
() + .map_err(|err| { + Error::Encode(EncodingError::Decoding(format!( + "Failed to parse base denom {} as Namada address: {err}", + nam_denom.base_denom + ))) + })?; + + // validate that the base denom is not another ibc token + if matches!(&address, Address::Internal(InternalAddress::IbcToken(_))) { + return Err(Error::Encode(EncodingError::Decoding(format!( + "Base denom {} cannot be an IBC token hash", + nam_denom.base_denom + )))); + } + + let channel_from_osmosis_to_namada = fetch_contract_data( + registry_contract_addr, + rest_rpc_addr, + &channel_pair_req(&osmosis_chain_name, &namada_chain_name), + ) + .await?; + + Ok(( + format!( + "transfer/{channel_from_osmosis_to_namada}/{}", + nam_denom.base_denom + ), + address, + )) + } else { + let channel_from_namada_to_src: ChannelId = nam_denom + .trace_path + .to_string() + .strip_prefix("transfer/") + .ok_or_else(|| { + Error::Other( + "Expected the output denom to originate from the transfer \ + port" + .to_string(), + ) + })? + .parse() + .map_err(|_| { + Error::Other(format!( + "Expected a single hop of the form `transfer/channel` in \ + {namada_denom}" + )) + })?; + + // we get chain name from which the base denom originated + let src_chain_name = fetch_contract_data( + registry_contract_addr, + rest_rpc_addr, + &dest_chain_req( + &namada_chain_name, + channel_from_namada_to_src.as_str(), + ), + ) + .await?; + + if src_chain_name == osmosis_chain_name { + // this is an osmosis native token + Ok(( + nam_denom.base_denom.to_string(), + namada_ibc::trace::ibc_token(namada_denom), + )) + } else { + // this asset is not native to osmosis + let channel_from_osmosis_to_src = fetch_contract_data( + registry_contract_addr, + rest_rpc_addr, + &channel_pair_req(&osmosis_chain_name, &src_chain_name), + ) + .await?; + + Ok(( + format!( + "transfer/{channel_from_osmosis_to_src}/{}", + nam_denom.base_denom + ), + namada_ibc::trace::ibc_token(namada_denom), + )) + } + } +} + +/// Query a route of Osmosis liquidity pools +/// for swapping betwixt token and output_denom +/// assets. +pub async fn query_osmosis_pool_routes( + ctx: &impl Namada, + token: &Address, + amount: InputAmount, + channel_id: ChannelId, + output_denom: &str, + osmosis_sqs_server_url: &str, +) -> Result>, Error> { + #[derive(Deserialize)] + struct PoolHop { + id: u64, + token_out_denom: String, + } + + impl From for OsmosisPoolHop { + fn from(value: PoolHop) -> Self { + Self { + pool_id: value.id.to_string(), + token_out_denom: value.token_out_denom, + } + } + } + + #[derive(Deserialize)] + struct Route { + pools: Vec, + } + + #[derive(Deserialize)] + struct ResponseOk { + route: Vec, + } + + #[derive(Deserialize)] + struct ResponseErr { + message: String, + } + + let coin = { + let denom = query_ibc_denom(ctx, token.to_string(), None).await; + let amount = validate_amount(ctx, amount, token, false).await?; + + let PrefixedDenom { + mut trace_path, + base_denom, + } = PrefixedDenom::from_str(&denom).map_err(|_| { + Error::Other(format!( + "Could not decode {token} as an IBC token address" + )) + })?; + + let prefix_on_namada = + TracePrefix::new(PortId::transfer(), channel_id.clone()); + + if trace_path.starts_with(&prefix_on_namada) { + // we received an asset from osmosis, so the asset we + // send back won't have our `transfer/channel` prefix + trace_path.remove_prefix(&prefix_on_namada); + } else { + // in this case, osmosis will prefix the asset it receives + // with the channel to namada + let channel = + get_ibc_src_port_channel(ctx, &PortId::transfer(), &channel_id) + .await? + .1; + trace_path + .add_prefix(TracePrefix::new(PortId::transfer(), channel)); + } + + let amount = amount.redenominate(0); + + let token_denom = if trace_path.is_empty() { + base_denom.to_string() + } else { + format!( + "ibc/{}", + calc_ibc_token_hash( + PrefixedDenom { + trace_path, + base_denom + } + .to_string() + ) + ) + }; + + format!("{amount}{token_denom}") + }; + + let client = reqwest::Client::new(); + let response = client + .get(format!("{osmosis_sqs_server_url}/router/quote")) + .query(&[ + ("tokenIn", coin.as_str()), + ("tokenOutDenom", output_denom), + ("humanDenoms", "false"), + ]) + .send() + .await + .map_err(|err| { + Error::Other(format!("Failed to query Osmosis SQS: {err}",)) + })?; + + if !response.status().is_success() { + let ResponseErr { message } = response.json().await.map_err(|err| { + Error::Other(format!( + "Failed to read failure response from HTTP request body: {err}" + )) + })?; + return Err(Error::Other(format!( + "Invalid Osmosis SQS query: {message}" + ))); + } + + let ResponseOk { route } = response.json().await.map_err(|err| { + Error::Other(format!( + "Failed to read success response from HTTP request body: {err}" + )) + })?; + + Ok(route + .into_iter() + .map(|r| r.pools.into_iter().map(OsmosisPoolHop::from).collect()) + .collect()) +} diff --git a/crates/sdk/src/signing.rs b/crates/sdk/src/signing.rs index a2e92b5cb7..1252373faa 100644 --- a/crates/sdk/src/signing.rs +++ b/crates/sdk/src/signing.rs @@ -410,11 +410,7 @@ pub async fn aux_signing_data( }; let fee_payer = if disposable_signing_key { - context - .wallet_mut() - .await - .gen_disposable_signing_key(&mut OsRng) - .to_public() + gen_disposable_signing_key(context).await } else { match &args.wrapper_fee_payer { Some(keypair) => keypair.clone(), @@ -497,6 +493,17 @@ pub async fn generate_tx_signatures( }) } +/// Generate a disposable signing key. +pub async fn gen_disposable_signing_key( + context: &impl Namada, +) -> common::PublicKey { + context + .wallet_mut() + .await + .gen_disposable_signing_key(&mut OsRng) + .to_public() +} + /// Information about the post-fee balance of the tx's source. Used to correctly /// handle balance validation in the inner tx #[derive(Debug)] diff --git a/crates/sdk/src/tx.rs b/crates/sdk/src/tx.rs index 19a6a5ba17..9c8c3834e5 100644 --- a/crates/sdk/src/tx.rs +++ b/crates/sdk/src/tx.rs @@ -3884,22 +3884,33 @@ pub async fn gen_ibc_shielding_transfer( args: args::GenIbcShieldingTransfer, ) -> Result> { let source = IBC; - let (src_port_id, src_channel_id) = - get_ibc_src_port_channel(context, &args.port_id, &args.channel_id) - .await?; - let ibc_denom = - rpc::query_ibc_denom(context, &args.token, Some(&source)).await; - // Need to check the prefix - let token = namada_ibc::received_ibc_token( - &ibc_denom, - &src_port_id, - &src_channel_id, - &args.port_id, - &args.channel_id, - ) - .map_err(|e| { - Error::Other(format!("Getting IBC Token failed: error {e}")) - })?; + + let token = match args.asset { + args::IbcShieldingTransferAsset::Address(addr) => addr, + args::IbcShieldingTransferAsset::LookupNamadaAddress { + token, + port_id, + channel_id, + } => { + let (src_port_id, src_channel_id) = + get_ibc_src_port_channel(context, &port_id, &channel_id) + .await?; + let ibc_denom = + rpc::query_ibc_denom(context, &token, Some(&source)).await; + + namada_ibc::received_ibc_token( + &ibc_denom, + &src_port_id, + &src_channel_id, + &port_id, + &channel_id, + ) + .map_err(|e| { + Error::Other(format!("Getting IBC Token failed: error {e}")) + })? + } + }; + let validated_amount = validate_amount(context, args.amount, &token, false).await?; @@ -3936,7 +3947,7 @@ pub async fn gen_ibc_shielding_transfer( Ok(shielded_transfer.map(|st| st.masp_tx)) } -async fn get_ibc_src_port_channel( +pub(crate) async fn get_ibc_src_port_channel( context: &impl Namada, dest_port_id: &PortId, dest_channel_id: &ChannelId, diff --git a/crates/tests/fixtures/osmosis_data/wasm_bytecode/crosschain_registry.wasm b/crates/tests/fixtures/osmosis_data/wasm_bytecode/crosschain_registry.wasm new file mode 100644 index 0000000000..ebd2324e65 Binary files /dev/null and b/crates/tests/fixtures/osmosis_data/wasm_bytecode/crosschain_registry.wasm differ diff --git a/crates/tests/fixtures/osmosis_data/wasm_bytecode/crosschain_swaps.wasm b/crates/tests/fixtures/osmosis_data/wasm_bytecode/crosschain_swaps.wasm new file mode 100644 index 0000000000..c79e7121df Binary files /dev/null and b/crates/tests/fixtures/osmosis_data/wasm_bytecode/crosschain_swaps.wasm differ diff --git a/crates/tests/fixtures/osmosis_data/wasm_bytecode/swaprouter.wasm b/crates/tests/fixtures/osmosis_data/wasm_bytecode/swaprouter.wasm new file mode 100644 index 0000000000..e43efcb25c Binary files /dev/null and b/crates/tests/fixtures/osmosis_data/wasm_bytecode/swaprouter.wasm differ diff --git a/crates/tests/src/e2e/helpers.rs b/crates/tests/src/e2e/helpers.rs index 3df93fddc7..1b911ce53b 100644 --- a/crates/tests/src/e2e/helpers.rs +++ b/crates/tests/src/e2e/helpers.rs @@ -497,6 +497,7 @@ pub fn make_hermes_config( hermes_dir: &TestDir, test_a: &Test, test_b: &Test, + relayer: Option<&str>, ) -> Result<()> { let mut config = toml::map::Map::new(); @@ -535,12 +536,17 @@ pub fn make_hermes_config( config.insert("telemetry".to_owned(), Value::Table(telemetry)); let chains = vec![ - make_hermes_chain_config(test_a), + match CosmosChainType::chain_type(test_a.net.chain_id.as_str()) { + Ok(chain_type) => make_hermes_chain_config_for_cosmos( + hermes_dir, chain_type, test_a, relayer, + ), + Err(_) => make_hermes_chain_config(hermes_dir, test_a), + }, match CosmosChainType::chain_type(test_b.net.chain_id.as_str()) { Ok(chain_type) => make_hermes_chain_config_for_cosmos( - hermes_dir, chain_type, test_b, + hermes_dir, chain_type, test_b, relayer, ), - Err(_) => make_hermes_chain_config(test_b), + Err(_) => make_hermes_chain_config(hermes_dir, test_b), }, ]; @@ -558,7 +564,7 @@ pub fn make_hermes_config( Ok(()) } -fn make_hermes_chain_config(test: &Test) -> Value { +fn make_hermes_chain_config(hermes_dir: &TestDir, test: &Test) -> Value { let chain_id = test.net.chain_id.as_str(); let rpc_addr = get_actor_rpc(test, Who::Validator(0)); let rpc_addr = rpc_addr.strip_prefix("http://").unwrap(); @@ -598,6 +604,13 @@ fn make_hermes_chain_config(test: &Test) -> Value { chain.insert("max_block_time".to_owned(), Value::String("60s".to_owned())); + let hermes_dir: &Path = hermes_dir.as_ref(); + let key_dir = hermes_dir.join("hermes/keys"); + chain.insert( + "key_store_folder".to_owned(), + Value::String(key_dir.to_string_lossy().to_string()), + ); + Value::Table(chain) } @@ -605,11 +618,14 @@ fn make_hermes_chain_config_for_cosmos( hermes_dir: &TestDir, chain_type: CosmosChainType, test: &Test, + relayer: Option<&str>, ) -> Value { let mut table = toml::map::Map::new(); table.insert("mode".to_owned(), Value::String("push".to_owned())); - let offset = chain_type.get_offset(); - let url = format!("ws://127.0.0.1:6416{}/websocket", offset); + let url = format!( + "ws://127.0.0.1:{}/websocket", + chain_type.get_rpc_port_number() + ); table.insert("url".to_owned(), Value::String(url)); table.insert("batch_delay".to_owned(), Value::String("500ms".to_owned())); let event_source = Value::Table(table); @@ -623,11 +639,17 @@ fn make_hermes_chain_config_for_cosmos( chain.insert( "rpc_addr".to_owned(), - Value::String(format!("http://127.0.0.1:6416{}", offset)), + Value::String(format!( + "http://127.0.0.1:{}", + chain_type.get_rpc_port_number() + )), ); chain.insert( "grpc_addr".to_owned(), - Value::String(format!("http://127.0.0.1:{}", offset + 9090)), + Value::String(format!( + "http://127.0.0.1:{}", + chain_type.get_grpc_port_number() + )), ); chain.insert("event_source".to_owned(), event_source); @@ -637,7 +659,7 @@ fn make_hermes_chain_config_for_cosmos( ); chain.insert( "key_name".to_owned(), - Value::String(setup::constants::COSMOS_RELAYER.to_string()), + Value::String(relayer.unwrap_or(constants::COSMOS_RELAYER).to_string()), ); let hermes_dir: &Path = hermes_dir.as_ref(); let key_dir = hermes_dir.join("hermes/keys"); @@ -646,10 +668,14 @@ fn make_hermes_chain_config_for_cosmos( Value::String(key_dir.to_string_lossy().to_string()), ); chain.insert("store_prefix".to_owned(), Value::String("ibc".to_owned())); - chain.insert("max_gas".to_owned(), Value::Integer(500_000)); - chain.insert("gas_multiplier".to_owned(), Value::Float(1.3)); + chain.insert("max_gas".to_owned(), Value::Integer(500_000_000)); + chain.insert("gas_multiplier".to_owned(), Value::Float(2.3)); let mut table = toml::map::Map::new(); - table.insert("price".to_owned(), Value::Float(0.001)); + if let CosmosChainType::Osmosis = chain_type { + table.insert("price".to_owned(), Value::Float(0.01)); + } else { + table.insert("price".to_owned(), Value::Float(0.001)); + } table.insert("denom".to_owned(), Value::String("stake".to_string())); chain.insert("gas_price".to_owned(), Value::Table(table)); @@ -672,9 +698,8 @@ pub fn update_cosmos_config(test: &Test) -> Result<()> { *timeout_propose = "1s".into(); } } - let offset = CosmosChainType::chain_type(test.net.chain_id.as_str()) - .unwrap() - .get_offset(); + let chain_type = + CosmosChainType::chain_type(test.net.chain_id.as_str()).unwrap(); let p2p = values .get_mut("p2p") .expect("Test failed") @@ -683,7 +708,8 @@ pub fn update_cosmos_config(test: &Test) -> Result<()> { let Some(laddr) = p2p.get_mut("laddr") else { panic!("Test failed") }; - *laddr = format!("tcp://0.0.0.0:266{}{}", offset, offset).into(); + *laddr = + format!("tcp://0.0.0.0:{}", chain_type.get_p2p_port_number()).into(); let rpc = values .get_mut("rpc") .expect("Test failed") @@ -692,7 +718,8 @@ pub fn update_cosmos_config(test: &Test) -> Result<()> { let Some(laddr) = rpc.get_mut("laddr") else { panic!("Test failed") }; - *laddr = format!("tcp://0.0.0.0:6416{offset}").into(); + *laddr = + format!("tcp://0.0.0.0:{}", chain_type.get_rpc_port_number()).into(); let mut file = OpenOptions::new() .write(true) @@ -757,14 +784,37 @@ pub fn update_cosmos_config(test: &Test) -> Result<()> { serde_json::to_writer_pretty(writer, &genesis) .expect("Writing Cosmos genesis.toml failed"); + if matches!(chain_type, CosmosChainType::Osmosis) { + let client_path = cosmos_dir.join("config/client.toml"); + let s = std::fs::read_to_string(&client_path) + .expect("Reading Osmosis client config failed"); + let mut values = s + .parse::() + .expect("Parsing Osmosis client config failed"); + let Some(laddr) = values.get_mut("node") else { + panic!("Test failed") + }; + *laddr = format!("tcp://0.0.0.0:{}", chain_type.get_rpc_port_number()) + .into(); + let mut file = OpenOptions::new() + .write(true) + .truncate(true) + .open(&client_path)?; + file.write_all(values.to_string().as_bytes()).map_err(|e| { + eyre!(format!( + "Writing a Osmosis client config file failed: {}", + e + )) + })?; + } + Ok(()) } pub fn get_cosmos_rpc_address(test: &Test) -> String { - let offset = CosmosChainType::chain_type(test.net.chain_id.as_str()) - .unwrap() - .get_offset(); - format!("127.0.0.1:6416{offset}") + let chain_type = + CosmosChainType::chain_type(test.net.chain_id.as_str()).unwrap(); + format!("127.0.0.1:{}", chain_type.get_rpc_port_number()) } pub fn find_cosmos_address( diff --git a/crates/tests/src/e2e/ibc_tests.rs b/crates/tests/src/e2e/ibc_tests.rs index 2e63db39f5..e2698db993 100644 --- a/crates/tests/src/e2e/ibc_tests.rs +++ b/crates/tests/src/e2e/ibc_tests.rs @@ -11,6 +11,7 @@ use core::str::FromStr; use core::time::Duration; +use std::fs::File; use std::path::{Path, PathBuf}; use color_eyre::eyre::Result; @@ -41,9 +42,11 @@ use namada_sdk::ibc::core::host::types::identifiers::{ use namada_sdk::ibc::primitives::proto::Any; use namada_sdk::ibc::storage::*; use namada_sdk::ibc::trace::ibc_token; +use namada_sdk::ibc::IbcShieldingData; use namada_sdk::token::Amount; use namada_test_utils::TestWasms; use prost::Message; +use serde_json::json; use setup::constants::*; use sha2::{Digest, Sha256}; @@ -56,10 +59,10 @@ use crate::e2e::ledger_tests::{ start_namada_ledger_node_wait_wasm, write_json_file, }; use crate::e2e::setup::{ - self, apply_use_device, run_cosmos_cmd, run_hermes_cmd, - set_ethereum_bridge_mode, setup_cosmos, setup_hermes, sleep, working_dir, - Bin, CosmosChainType, NamadaCmd, Test, TestDir, Who, - ENV_VAR_COSMWASM_CONTRACT_DIR, + self, apply_use_device, osmosis_fixtures_dir, run_cosmos_cmd, + run_cosmos_cmd_homeless, run_hermes_cmd, set_ethereum_bridge_mode, + setup_cosmos, setup_hermes, sleep, working_dir, Bin, CosmosChainType, + NamadaCmd, Test, TestDir, Who, ENV_VAR_COSMWASM_CONTRACT_DIR, }; use crate::ibc::primitives::Signer; use crate::strings::TX_APPLIED_SUCCESS; @@ -140,6 +143,7 @@ fn ibc_transfers() -> Result<()> { None, None, None, + None, false, )?; wait_for_packet_relay( @@ -217,6 +221,7 @@ fn ibc_transfers() -> Result<()> { None, None, None, + None, false, )?; wait_for_packet_relay( @@ -299,6 +304,7 @@ fn ibc_transfers() -> Result<()> { None, None, None, + None, true, )?; wait_for_packet_relay( @@ -353,6 +359,7 @@ fn ibc_transfers() -> Result<()> { None, None, None, + None, false, )?; wait_for_packet_relay( @@ -381,6 +388,7 @@ fn ibc_transfers() -> Result<()> { Some(Duration::new(10, 0)), None, None, + None, false, )?; // wait for the timeout @@ -413,6 +421,7 @@ fn ibc_transfers() -> Result<()> { None, None, None, + None, true, )?; wait_for_packet_relay( @@ -443,6 +452,7 @@ fn ibc_transfers() -> Result<()> { Some(Duration::new(10, 0)), None, None, + None, true, )?; // wait for the timeout @@ -605,6 +615,7 @@ fn ibc_nft_transfers() -> Result<()> { None, None, None, + None, false, )?; clear_packet(&hermes_dir, &port_id_namada, &channel_id_namada, &test)?; @@ -668,6 +679,7 @@ fn ibc_nft_transfers() -> Result<()> { None, None, None, + None, true, )?; clear_packet(&hermes_dir, &port_id_namada, &channel_id_namada, &test)?; @@ -1110,6 +1122,7 @@ fn ibc_rate_limit() -> Result<()> { None, None, None, + None, false, )?; @@ -1129,6 +1142,7 @@ fn ibc_rate_limit() -> Result<()> { Some( "Transfer exceeding the per-epoch throughput limit is not allowed", ), + None, false, )?; @@ -1152,6 +1166,7 @@ fn ibc_rate_limit() -> Result<()> { None, None, None, + None, false, )?; @@ -1190,6 +1205,80 @@ fn ibc_rate_limit() -> Result<()> { Ok(()) } +/// Create a packet forward memo and serialize it +fn packet_forward_memo( + receiver: Signer, + port_id: &PortId, + channel_id: &ChannelId, + timeout: Option, + next: Option>, +) -> String { + serde_json::to_string(&serde_json::Value::Object( + packet_forward_memo_value(receiver, port_id, channel_id, timeout, next), + )) + .expect("Test failed") +} + +fn packet_forward_memo_value( + receiver: Signer, + port_id: &PortId, + channel_id: &ChannelId, + timeout: Option, + next: Option>, +) -> serde_json::Map { + let value = + serde_json::to_value(&ibc_middleware_packet_forward::PacketMetadata { + forward: ForwardMetadata { + receiver, + port: port_id.clone(), + channel: channel_id.clone(), + timeout: timeout.map(|t| { + ibc_middleware_packet_forward::Duration::from_dur( + dur::Duration::from_std(t), + ) + }), + retries: Some(0), + next, + }, + }) + .expect("Test failed"); + + if let serde_json::Value::Object(memo) = value { + memo + } else { + unreachable!() + } +} + +fn shielded_recv_memo_value( + masp_transfer_path: &Path, + shielded_amount: Amount, + overflow_receiver: namada_core::address::Address, +) -> serde_json::Map { + use namada_core::string_encoding::StringEncoded; + use namada_sdk::ibc::{NamadaMemo, NamadaMemoData}; + + let transfer = + std::fs::read_to_string(masp_transfer_path).expect("Test failed"); + let tx = StringEncoded::new( + IbcShieldingData::from_str(&transfer).expect("Test failed"), + ); + let data = NamadaMemoData::OsmosisSwap { + shielding_data: tx, + shielded_amount, + overflow_receiver, + }; + + let value = serde_json::to_value(&NamadaMemo { namada: data }) + .expect("Test failed"); + + if let serde_json::Value::Object(memo) = value { + memo + } else { + unreachable!() + } +} + /// Test the happy flows of ibc pfm /// /// Setup: Two instances of Gaia and one @@ -1266,6 +1355,7 @@ fn ibc_pfm_happy_flows() -> Result<()> { &port_id_namada, &channel_id_namada_2, None, + None, ); transfer_from_cosmos( &test_gaia_1, @@ -1313,6 +1403,7 @@ fn ibc_pfm_happy_flows() -> Result<()> { &port_id_namada, &channel_id_namada_1, None, + None, ); transfer_from_cosmos( &test_gaia_2, @@ -1361,6 +1452,7 @@ fn ibc_pfm_happy_flows() -> Result<()> { None, None, None, + None, false, )?; @@ -1386,6 +1478,7 @@ fn ibc_pfm_happy_flows() -> Result<()> { &port_id_namada, &channel_id_namada_2, None, + None, ); transfer_from_cosmos( @@ -1436,6 +1529,7 @@ fn ibc_pfm_happy_flows() -> Result<()> { &port_id_namada, &channel_id_namada_1, None, + None, ); transfer_from_cosmos( &test_gaia_2, @@ -1472,6 +1566,7 @@ fn ibc_pfm_happy_flows() -> Result<()> { Ok(()) } + /// Test the flows of ibc pfm where the packet cannot be /// completed and refunds must be issued. /// @@ -1560,6 +1655,7 @@ fn ibc_pfm_unhappy_flows() -> Result<()> { &port_id_namada, &channel_id_namada_2, None, + None, ); transfer_from_cosmos( &test_gaia_1, @@ -1595,6 +1691,7 @@ fn ibc_pfm_unhappy_flows() -> Result<()> { &port_id_namada, &channel_id_namada_2, Some(Duration::new(1, 0)), + None, ); // Stop Hermes for timeout test let mut hermes_2 = bg_hermes_2.foreground(); @@ -1652,6 +1749,7 @@ fn ibc_pfm_unhappy_flows() -> Result<()> { None, None, None, + None, false, )?; @@ -1677,6 +1775,7 @@ fn ibc_pfm_unhappy_flows() -> Result<()> { &port_id_namada, &channel_id_namada_2, None, + None, ); transfer_from_cosmos( &test_gaia_1, @@ -1717,6 +1816,7 @@ fn ibc_pfm_unhappy_flows() -> Result<()> { &port_id_namada, &channel_id_namada_2, Some(Duration::new(1, 0)), + None, ); // Stop Hermes for timeout test let mut hermes_2 = bg_hermes_2.foreground(); @@ -1770,6 +1870,7 @@ fn ibc_pfm_unhappy_flows() -> Result<()> { &port_id_namada, &channel_id_namada_1, None, + None, ); transfer_from_cosmos( &test_gaia_2, @@ -1808,6 +1909,7 @@ fn ibc_pfm_unhappy_flows() -> Result<()> { &port_id_namada, &channel_id_namada_2, None, + None, ); transfer_from_cosmos( &test_gaia_1, @@ -1853,6 +1955,7 @@ fn ibc_pfm_unhappy_flows() -> Result<()> { &port_id_namada, &channel_id_namada_2, Some(Duration::new(1, 0)), + None, ); // Stop Hermes for timeout test let mut hermes_2 = bg_hermes_2.foreground(); @@ -1906,6 +2009,193 @@ fn ibc_pfm_unhappy_flows() -> Result<()> { Ok(()) } +/// Test that we are able to use the shielded-receive +/// middleware to shield funds specified in the memo +/// message. +#[test] +fn ibc_shielded_recv_middleware_happy_flow() -> Result<()> { + let update_genesis = + |mut genesis: templates::All, base_dir: &_| { + genesis.parameters.parameters.epochs_per_year = + epochs_per_year_from_min_duration(1800); + genesis.parameters.ibc_params.default_mint_limit = + Amount::max_signed(); + genesis + .parameters + .ibc_params + .default_per_epoch_throughput_limit = Amount::max_signed(); + setup::set_validators(1, genesis, base_dir, |_| 0, vec![]) + }; + let (ledger, gaia, test, test_gaia) = + run_namada_cosmos(CosmosChainType::Gaia(None), update_genesis)?; + let _bg_ledger = ledger.background(); + let _bg_gaia = gaia.background(); + sleep(5); + + let hermes_dir = setup_hermes(&test, &test_gaia)?; + let port_id_namada = FT_PORT_ID.parse().unwrap(); + let port_id_gaia = FT_PORT_ID.parse().unwrap(); + let (channel_id_namada, channel_id_gaia) = create_channel_with_hermes( + &hermes_dir, + &test, + &test_gaia, + &port_id_namada, + &port_id_gaia, + )?; + + // Start relaying + let hermes = run_hermes(&hermes_dir)?; + let _bg_hermes = hermes.background(); + + // 1. Shield 10 NAM to AA_PAYMENT_ADDRESS + transfer_on_chain( + &test, + "shield", + ALBERT, + AA_PAYMENT_ADDRESS, + NAM, + 10, + ALBERT_KEY, + &[], + )?; + check_shielded_balance(&test, AA_VIEWING_KEY, NAM, 10)?; + + // 2. Unshield from A_SPENDING_KEY to B_SPENDING_KEY, + // using the packet forward and shielded receive + // middlewares + let nam_addr = find_address(&test, NAM)?; + let overflow_addr = "tnam1qrqzqa0l0rzzrlr20n487l6n865t8ndv6uhseulq"; + let ibc_denom_on_gaia = format!("transfer/{channel_id_gaia}/{nam_addr}"); + let memo_path = gen_ibc_shielding_data( + &test, + AB_PAYMENT_ADDRESS, + &ibc_denom_on_gaia, + 8, + &port_id_namada, + &channel_id_namada, + )?; + let memo = packet_forward_memo( + MASP.to_string().into(), + &PortId::transfer(), + &channel_id_namada, + None, + Some(shielded_recv_memo_value( + &memo_path, + Amount::native_whole(8), + overflow_addr.parse().unwrap(), + )), + ); + transfer( + &test, + A_SPENDING_KEY, + "PacketForwardMiddleware", + NAM, + 10, + Some(ALBERT_KEY), + &PortId::transfer(), + &channel_id_namada, + None, + None, + None, + Some(&memo), + true, + )?; + wait_for_packet_relay(&hermes_dir, &port_id_gaia, &channel_id_gaia, &test)?; + + // Check the token on Namada + check_shielded_balance(&test, AA_VIEWING_KEY, NAM, 0)?; + check_shielded_balance(&test, AB_VIEWING_KEY, NAM, 8)?; + check_balance(&test, overflow_addr, NAM, 2)?; + + Ok(()) +} + +/// Test that if the received amount underflows the minimum +/// amount, we error out and refund assets. +#[test] +fn ibc_shielded_recv_middleware_unhappy_flow() -> Result<()> { + let update_genesis = + |mut genesis: templates::All, base_dir: &_| { + genesis.parameters.parameters.epochs_per_year = + epochs_per_year_from_min_duration(1800); + genesis.parameters.ibc_params.default_mint_limit = + Amount::max_signed(); + genesis + .parameters + .ibc_params + .default_per_epoch_throughput_limit = Amount::max_signed(); + setup::set_validators(1, genesis, base_dir, |_| 0, vec![]) + }; + let (ledger, gaia, test, test_gaia) = + run_namada_cosmos(CosmosChainType::Gaia(None), update_genesis)?; + let _bg_ledger = ledger.background(); + let _bg_gaia = gaia.background(); + sleep(5); + + let hermes_dir = setup_hermes(&test, &test_gaia)?; + let port_id_namada = FT_PORT_ID.parse().unwrap(); + let port_id_gaia = FT_PORT_ID.parse().unwrap(); + let (channel_id_namada, channel_id_gaia) = create_channel_with_hermes( + &hermes_dir, + &test, + &test_gaia, + &port_id_namada, + &port_id_gaia, + )?; + + // Start relaying + let hermes = run_hermes(&hermes_dir)?; + let _bg_hermes = hermes.background(); + + let nam_addr = find_address(&test, NAM)?; + let overflow_addr = "tnam1qrqzqa0l0rzzrlr20n487l6n865t8ndv6uhseulq"; + let ibc_denom_on_gaia = format!("transfer/{channel_id_gaia}/{nam_addr}"); + check_balance(&test, ALBERT, NAM, 2_000_000)?; + + let memo_path = gen_ibc_shielding_data( + &test, + AB_PAYMENT_ADDRESS, + &ibc_denom_on_gaia, + 8, + &port_id_namada, + &channel_id_namada, + )?; + let memo = packet_forward_memo( + MASP.to_string().into(), + &PortId::transfer(), + &channel_id_namada, + None, + Some(shielded_recv_memo_value( + &memo_path, + Amount::native_whole(8), + overflow_addr.parse().unwrap(), + )), + ); + transfer( + &test, + ALBERT, + "PacketForwardMiddleware", + NAM, + 7, + Some(ALBERT_KEY), + &PortId::transfer(), + &channel_id_namada, + None, + None, + None, + Some(&memo), + false, + )?; + wait_for_packet_relay(&hermes_dir, &port_id_gaia, &channel_id_gaia, &test)?; + + // Check the token on Namada + check_balance(&test, ALBERT, NAM, 2_000_000)?; + check_shielded_balance(&test, AB_VIEWING_KEY, NAM, 0)?; + check_balance(&test, overflow_addr, NAM, 0)?; + + Ok(()) +} + fn run_namada_cosmos( chain_type: CosmosChainType, mut update_genesis: impl FnMut( @@ -1925,14 +2215,19 @@ fn run_namada_cosmos( let ledger = start_namada_ledger_node_wait_wasm(&test, Some(0), Some(40))?; - // Cosmos - let test_cosmos = setup_cosmos(chain_type)?; - let cosmos = run_cosmos(&test_cosmos, true)?; - sleep(5); + let (cosmos, test_cosmos) = setup_and_boot_cosmos(chain_type)?; Ok((ledger, cosmos, test, test_cosmos)) } +fn setup_and_boot_cosmos( + chain_type: CosmosChainType, +) -> Result<(NamadaCmd, Test)> { + let test_cosmos = setup_cosmos(chain_type)?; + let cosmos = run_cosmos(&test_cosmos, true)?; + Ok((cosmos, test_cosmos)) +} + fn create_channel_with_hermes( hermes_dir: &TestDir, test_a: &Test, @@ -1947,6 +2242,7 @@ fn create_channel_with_hermes( } else { FT_CHANNEL_VERSION }; + let args = [ "create", "channel", @@ -2004,7 +2300,7 @@ fn run_cosmos(test: &Test, kill: bool) -> Result { .output() .unwrap(); } - let port_arg = format!("0.0.0.0:{}", 9090 + chain_type.get_offset()); + let port_arg = format!("0.0.0.0:{}", chain_type.get_grpc_port_number()); let args = ["start", "--pruning", "nothing", "--grpc.address", &port_arg]; let cosmos = run_cosmos_cmd(test, args, Some(40))?; Ok(cosmos) @@ -2144,6 +2440,7 @@ fn try_invalid_transfers( None, // the IBC denom can't be parsed when using an invalid port Some(&format!("Invalid IBC denom: {nam_addr}")), + None, false, )?; @@ -2160,6 +2457,7 @@ fn try_invalid_transfers( None, None, Some("IBC token transfer error: context error: `ICS04 Channel error"), + None, false, )?; @@ -2215,6 +2513,7 @@ fn transfer( timeout_sec: Option, shielding_data_path: Option, expected_err: Option<&str>, + ibc_memo: Option<&str>, gen_refund_target: bool, ) -> Result { let rpc = get_actor_rpc(test, Who::Validator(0)); @@ -2242,6 +2541,10 @@ fn transfer( &rpc, ]); + if let Some(ibc_memo) = ibc_memo { + tx_args.extend_from_slice(&["--ibc-memo", ibc_memo]); + } + if let Some(signer) = signer { tx_args.extend_from_slice(&["--signing-keys", signer]); } else { @@ -2632,10 +2935,9 @@ fn transfer_from_cosmos( let port_id = port_id.to_string(); let channel_id = channel_id.to_string(); let amount = format!("{}{}", amount, token.as_ref()); - let offset = CosmosChainType::chain_type(test.net.chain_id.as_str()) - .unwrap() - .get_offset(); - let rpc = format!("tcp://127.0.0.1:6416{offset}"); + let chain_type = + CosmosChainType::chain_type(test.net.chain_id.as_str()).unwrap(); + let rpc = format!("tcp://127.0.0.1:{}", chain_type.get_rpc_port_number()); // If the receiver is a pyament address we want to mask it to the more // general MASP internal address to improve on privacy let receiver = match PaymentAddress::from_str(receiver.as_ref()) { @@ -2794,10 +3096,9 @@ fn check_cosmos_balance( expected_amount: u64, ) -> Result<()> { let addr = find_cosmos_address(test, owner)?; - let offset = CosmosChainType::chain_type(test.net.chain_id.as_str()) - .unwrap() - .get_offset(); - let rpc = format!("tcp://127.0.0.1:6416{offset}"); + let chain_type = + CosmosChainType::chain_type(test.net.chain_id.as_str()).unwrap(); + let rpc = format!("tcp://127.0.0.1:{}", chain_type.get_rpc_port_number()); let args = ["query", "bank", "balances", &addr, "--node", &rpc]; let mut cosmos = run_cosmos_cmd(test, args, Some(40))?; cosmos.exp_string(&format!("amount: \"{expected_amount}\""))?; @@ -3195,26 +3496,756 @@ fn nft_transfer_from_cosmos( Ok(()) } -/// Create a packet forward memo and serialize it -fn packet_forward_memo( - receiver: Signer, - port_id: &PortId, - channel_id: &ChannelId, - timeout: Option, -) -> String { - serde_json::to_string(&ibc_middleware_packet_forward::PacketMetadata { - forward: ForwardMetadata { - receiver, - port: port_id.clone(), - channel: channel_id.clone(), - timeout: timeout.map(|t| { - ibc_middleware_packet_forward::Duration::from_dur( - dur::Duration::from_std(t), - ) - }), - retries: Some(0), - next: None, - }, - }) - .expect("Test failed") +/// Basic Osmosis test that checks if the chain has been set up correctly. +#[test] +fn osmosis_basic() -> Result<()> { + let (osmosis, test_osmosis) = + setup_and_boot_cosmos(CosmosChainType::Osmosis)?; + + let _bg_osmosis = osmosis.background(); + sleep(5); + + check_cosmos_balance(&test_osmosis, COSMOS_USER, COSMOS_COIN, 1_000)?; + + Ok(()) } + +#[test] +fn osmosis_xcs() -> Result<()> { + // ========================================================== + // This test requires quite a long setup. Jump to the next + // occurrence of `SETUP DONE` in order to skip all of this + // nonsense. + // ========================================================== + + // Set up a big Cosmos party + let update_genesis = + |mut genesis: templates::All, base_dir: &_| { + genesis.parameters.parameters.epochs_per_year = + epochs_per_year_from_min_duration(1800); + genesis.parameters.ibc_params.default_mint_limit = + Amount::max_signed(); + genesis + .parameters + .ibc_params + .default_per_epoch_throughput_limit = Amount::max_signed(); + setup::set_validators(1, genesis, base_dir, |_| 0, vec![]) + }; + let (namada, gaia, test_namada, test_gaia) = + run_namada_cosmos(CosmosChainType::Gaia(Some(1)), update_genesis)?; + + let (osmosis, test_osmosis) = + setup_and_boot_cosmos(CosmosChainType::Osmosis)?; + + let _bg_osmosis = osmosis.background(); + let _bg_ledger = namada.background(); + let _bg_gaia = gaia.background(); + + // The MC of the party + let osmosis_jones = find_cosmos_address(&test_osmosis, COSMOS_USER)?; + + // Everyone shall take a 5 second nap, partied too hard + // (big up MC Osmosis Jones) + sleep(5); + + // Create hermes relayers with the following config: + // ================================================= + // + // namada -- osmosis -- gaia + // \_____________________/ + + // Set up initial hermes configs + let hermes_gaia_namada = setup_hermes(&test_gaia, &test_namada) + .map_err(|_| eyre!("failed to join thread hermes_gaia_namada"))?; + let hermes_gaia_osmosis = setup_hermes(&test_gaia, &test_osmosis) + .map_err(|_| eyre!("failed to join thread hermes_gaia_osmosis"))?; + let hermes_namada_osmosis = setup_hermes(&test_namada, &test_osmosis) + .map_err(|_| eyre!("failed to join thread hermes_namada_osmosis"))?; + std::thread::sleep(Duration::from_secs(5)); + // Set up channels + let (channel_from_gaia_to_namada, channel_from_namada_to_gaia) = + create_channel_with_hermes( + &hermes_gaia_namada, + &test_gaia, + &test_namada, + &PortId::transfer(), + &PortId::transfer(), + )?; + + // Osmosis currently uses an older version of the Cosmos SDK + // that will error if txs are sent too close together. See + // https://github.com/cosmos/cosmos-sdk/issues/13621 + std::thread::sleep(Duration::from_secs(5)); + let (channel_from_gaia_to_osmosis, channel_from_osmosis_to_gaia) = + create_channel_with_hermes( + &hermes_gaia_osmosis, + &test_gaia, + &test_osmosis, + &PortId::transfer(), + &PortId::transfer(), + )?; + + let (channel_from_namada_to_osmosis, channel_from_osmosis_to_namada) = + create_channel_with_hermes( + &hermes_namada_osmosis, + &test_namada, + &test_osmosis, + &PortId::transfer(), + &PortId::transfer(), + )?; + + // Start relaying + let hermes_1 = run_hermes(&hermes_gaia_namada)?; + let hermes_2 = run_hermes(&hermes_gaia_osmosis)?; + let hermes_3 = run_hermes(&hermes_namada_osmosis)?; + let _bg_hermes_1 = hermes_1.background(); + let _bg_hermes_2 = hermes_2.background(); + let _bg_hermes_3 = hermes_3.background(); + + // Transfer assets to Osmosis, in order to create pools + + // Transfer NAM from Namada + transfer( + &test_namada, + ALBERT, + &osmosis_jones, + NAM, + 500, + Some(ALBERT_KEY), + &PortId::transfer(), + &channel_from_namada_to_osmosis, + None, + None, + None, + None, + false, + )?; + // Transfer Samoleans from Gaia + transfer_from_cosmos( + &test_gaia, + COSMOS_USER, + &osmosis_jones, + COSMOS_COIN, + 500, + &PortId::transfer(), + &channel_from_gaia_to_osmosis, + None, + None, + )?; + + // Related to the issue above. In general, it takes osmosis some time + // to update state. Thus calls to it should be spaced out accordingly. + std::thread::sleep(Duration::from_secs(5)); + + // Check balance of transferred assets on Osmosis + let nam_token_addr = find_address(&test_namada, NAM)?; + check_cosmos_balance( + &test_osmosis, + COSMOS_USER, + format!("transfer/{channel_from_osmosis_to_namada}/{nam_token_addr}"), + 500_000_000, + )?; + + check_cosmos_balance( + &test_osmosis, + COSMOS_USER, + format!("transfer/{channel_from_osmosis_to_gaia}/{COSMOS_COIN}"), + 500, + )?; + + // Set up contracts on Osmosis + let rpc_osmosis = + format!("tcp://{}", get_cosmos_rpc_address(&test_osmosis)); + + const CROSSCHAIN_REGISTRY_CODE_ID: &str = "1"; + const SWAPROUTER_CODE_ID: &str = "2"; + const CROSSCHAIN_SWAPS_CODE_ID: &str = "3"; + + const CROSSCHAIN_REGISTRY_SHA256_HASH: &str = + "3a90b1dc50ba2c63c40298b1645f3a56431d895857cab75f97c5b8266f64e4fa"; + const SWAPROUTER_CODE_SHA256_HASH: &str = + "bd579ce619c16d50118f4a14f98fa1b2724ce11c7de2f8c532a9b2587bd98bbd"; + const CROSSCHAIN_SWAPS_SHA256_HASH: &str = + "87c3c3422e876f117efc5cda6ae30b4c897054148d424815daeb2b3038ec6cfd"; + + // Deploy each contract's wasm bytecode + for wasm in [ + "crosschain_registry.wasm", + "swaprouter.wasm", + "crosschain_swaps.wasm", + ] { + let wasm_path = osmosis_fixtures_dir().join("wasm_bytecode").join(wasm); + let wasm_path_str = wasm_path.to_string_lossy(); + std::thread::sleep(Duration::from_secs(5)); + let args = cosmos_common_args( + "5000000", + None, + test_osmosis.net.chain_id.as_str(), + &rpc_osmosis, + vec!["tx", "wasm", "store", &wasm_path_str], + ); + let mut osmosis_cmd = run_cosmos_cmd(&test_osmosis, args, Some(40))?; + osmosis_cmd.assert_success(); + } + + // Instantiate `crosschain_registry.wasm` + let json = format!(r#"{{"owner":"{osmosis_jones}"}}"#); + std::thread::sleep(Duration::from_secs(5)); + let crosschain_registry_addr = build_contract_addr( + &test_osmosis, + CROSSCHAIN_REGISTRY_SHA256_HASH, + &osmosis_jones, + )?; + std::thread::sleep(Duration::from_secs(5)); + let args = cosmos_common_args( + "2500000", + Some("0.01stake"), + test_osmosis.net.chain_id.as_str(), + &rpc_osmosis, + vec![ + "tx", + "wasm", + "instantiate2", + CROSSCHAIN_REGISTRY_CODE_ID, + &json, + CONTRACT_SALT_HEX, + "--label", + "CrosschainRegistry", + "--no-admin", + "--hex", + ], + ); + let mut osmosis_cmd = run_cosmos_cmd(&test_osmosis, args, Some(40))?; + osmosis_cmd.assert_success(); + + // Instantiate `swaprouter.wasm` + let json = format!(r#"{{"owner":"{osmosis_jones}"}}"#); + std::thread::sleep(Duration::from_secs(10)); + let swaprouter_addr = build_contract_addr( + &test_osmosis, + SWAPROUTER_CODE_SHA256_HASH, + &osmosis_jones, + )?; + std::thread::sleep(Duration::from_secs(10)); + let args = cosmos_common_args( + "250000", + Some("0.01stake"), + test_osmosis.net.chain_id.as_str(), + &rpc_osmosis, + vec![ + "tx", + "wasm", + "instantiate2", + SWAPROUTER_CODE_ID, + &json, + CONTRACT_SALT_HEX, + "--label", + "SwapRouter", + "--no-admin", + "--hex", + ], + ); + let mut osmosis_cmd = run_cosmos_cmd(&test_osmosis, args, Some(40))?; + osmosis_cmd.assert_success(); + + // Instantiate `swaprouter.wasm` + std::thread::sleep(Duration::from_secs(10)); + let json = format!( + r#"{{"governor":"{osmosis_jones}","swap_contract":"{swaprouter_addr}","registry_contract":"{crosschain_registry_addr}"}}"# + ); + let crosschain_swaps_addr = build_contract_addr( + &test_osmosis, + CROSSCHAIN_SWAPS_SHA256_HASH, + &osmosis_jones, + )?; + std::thread::sleep(Duration::from_secs(10)); + let args = cosmos_common_args( + "500000", + Some("0.01stake"), + test_osmosis.net.chain_id.as_str(), + &rpc_osmosis, + vec![ + "tx", + "wasm", + "instantiate2", + CROSSCHAIN_SWAPS_CODE_ID, + &json, + CONTRACT_SALT_HEX, + "--label", + "CrosschainSwaps", + "--no-admin", + "--hex", + ], + ); + + let mut osmosis_cmd = run_cosmos_cmd(&test_osmosis, args, Some(40))?; + osmosis_cmd.assert_success(); + std::thread::sleep(Duration::from_secs(5)); + + // Modify the bech32 prefixes + let msg = serde_json::to_string(&json!({ + "modify_bech32_prefixes": { + "operations": [ + { + "operation": "set", + "chain_name": "namada", + "prefix": "tnam" + }, + { + "operation": "set", + "chain_name": "osmosis", + "prefix": "osmo" + }, + { + "operation": "set", + "chain_name": "gaia", + "prefix": "cosmo" + } + ] + }})) + .unwrap(); + + let args = cosmos_common_args( + "5000000", + Some("0.01stake"), + test_osmosis.net.chain_id.as_str(), + &rpc_osmosis, + vec!["tx", "wasm", "execute", &crosschain_registry_addr, &msg], + ); + let mut osmosis_cmd = run_cosmos_cmd(&test_osmosis, args, Some(40))?; + osmosis_cmd.assert_success(); + std::thread::sleep(Duration::from_secs(5)); + + // Modify the channel chain links + let msg = serde_json::to_string(&json!({ + "modify_chain_channel_links": { + "operations": [ + { + "operation": "set", + "source_chain": "namada", + "destination_chain": "osmosis", + "channel_id": channel_from_namada_to_osmosis + }, + { + "operation": "set", + "source_chain": "osmosis", + "destination_chain": "namada", + "channel_id": channel_from_osmosis_to_namada + }, + { + "operation": "set", + "source_chain": "namada", + "destination_chain": "gaia", + "channel_id": channel_from_namada_to_gaia + }, + { + "operation": "set", + "source_chain": "gaia", + "destination_chain": "namada", + "channel_id": channel_from_gaia_to_namada + }, + { + "operation": "set", + "source_chain": "gaia", + "destination_chain": "osmosis", + "channel_id": channel_from_gaia_to_osmosis + }, + { + "operation": "set", + "source_chain": "osmosis", + "destination_chain": "gaia", + "channel_id": channel_from_osmosis_to_gaia + } + ] + }})) + .unwrap(); + let args = cosmos_common_args( + "5000000", + Some("0.01stake"), + test_osmosis.net.chain_id.as_str(), + &rpc_osmosis, + vec!["tx", "wasm", "execute", &crosschain_registry_addr, &msg], + ); + let mut osmosis_cmd = run_cosmos_cmd(&test_osmosis, args, Some(40))?; + osmosis_cmd.assert_success(); + std::thread::sleep(Duration::from_secs(5)); + + // Enable PFM on gaia and namada + for (chain, token) in [ + ( + "namada", + get_gaia_denom_hash(format!( + "transfer/{channel_from_osmosis_to_namada}/{nam_token_addr}" + )), + ), + ( + "gaia", + get_gaia_denom_hash(format!( + "transfer/{channel_from_osmosis_to_gaia}/{COSMOS_COIN}" + )), + ), + ] { + let msg = format!(r#"{{"propose_pfm": {{"chain": "{chain}"}}}}"#); + let amount = format!("1{token}"); + let args = cosmos_common_args( + "5000000", + Some("0.01stake"), + test_osmosis.net.chain_id.as_str(), + &rpc_osmosis, + vec![ + "tx", + "wasm", + "execute", + &crosschain_registry_addr, + &msg, + "--amount", + &amount, + ], + ); + let mut osmosis_cmd = run_cosmos_cmd(&test_osmosis, args, Some(40))?; + osmosis_cmd.assert_success(); + std::thread::sleep(Duration::from_secs(5)); + } + + wait_for_packet_relay( + &hermes_namada_osmosis, + &PortId::transfer(), + &channel_from_osmosis_to_namada, + &test_osmosis, + )?; + wait_for_packet_relay( + &hermes_gaia_osmosis, + &PortId::transfer(), + &channel_from_osmosis_to_gaia, + &test_osmosis, + )?; + + // Check that the PFM was successfully enabled + // on the contract + for chain in ["namada", "gaia"] { + let msg = + format!(r#"{{"has_packet_forwarding": {{"chain": "{chain}"}}}}"#); + let args = vec![ + "query", + "wasm", + "contract-state", + "smart", + &crosschain_registry_addr, + &msg, + ]; + let mut osmosis_cmd = run_cosmos_cmd(&test_osmosis, args, Some(40))?; + osmosis_cmd.exp_string("data: true")?; + } + + // Create a LP on Osmosis with Samoleans and Nam + create_pool( + &test_osmosis, + &rpc_osmosis, + &[ + ( + &get_gaia_denom_hash(format!( + "transfer/{channel_from_osmosis_to_namada}/\ + {nam_token_addr}" + )), + 100, + ), + ( + &get_gaia_denom_hash(format!( + "transfer/{channel_from_osmosis_to_gaia}/{COSMOS_COIN}" + )), + 100, + ), + ], + )?; + run_cosmos_cmd( + &test_osmosis, + ["query", "poolmanager", "pool", "1"], + Some(40), + )? + .assert_success(); + + // Shield some nam + let rpc_namada = get_actor_rpc(&test_namada, Who::Validator(0)); + + run!( + &test_namada, + Bin::Client, + [ + "shield", + "--source", + BERTHA, + "--target", + AA_PAYMENT_ADDRESS, + "--amount", + "0.000056", + "--token", + NAM, + "--node", + &rpc_namada, + ], + Some(40) + )? + .assert_success(); + + shielded_sync(&test_namada, AA_VIEWING_KEY)?; + + let query_args = vec![ + "balance", + "--owner", + AA_VIEWING_KEY, + "--token", + NAM, + "--node", + &rpc_namada, + ]; + let mut client = run!(&test_namada, Bin::Client, query_args, Some(40))?; + client.exp_string("nam: 0.000056")?; + client.assert_success(); + + // ========================================================== + // SETUP DONE + // ========================================================== + // At this point, we have IBC channels between Osmosis, Gaia + // and Namada. There is a LP with nam and samoleans, that we + // can use to swap between the two assets. Moreover, we have + // shielded some nam tokens. + // ========================================================== + + // We wish to receive samoleans on namada + let output_denom_on_namada = + format!("transfer/{channel_from_namada_to_gaia}/{COSMOS_COIN}"); + + // But on osmosis, we will end up with this token + let output_denom_on_osmosis = get_gaia_denom_hash(format!( + "transfer/{channel_from_osmosis_to_gaia}/{COSMOS_COIN}" + )); + + // Transparently swap samoleans with nam + run!( + &test_namada, + Bin::Client, + [ + "osmosis-swap", + "--osmosis-rest-rpc", + "http://localhost:1317", + "--source", + BERTHA, + "--token", + NAM, + "--amount", + "0.000064", + "--channel-id", + channel_from_namada_to_osmosis.as_ref(), + "--output-denom", + &output_denom_on_namada, + "--local-recovery-addr", + &osmosis_jones, + "--swap-contract", + &crosschain_swaps_addr, + "--minimum-amount", + "1", + "--target", + BERTHA, + "--pool-hop", + &format!("1:{output_denom_on_osmosis}"), + "--node", + &rpc_namada, + ], + Some(40), + )? + .assert_success(); + + wait_for_packet_relay( + &hermes_namada_osmosis, + &PortId::transfer(), + &channel_from_osmosis_to_namada, + &test_osmosis, + )?; + wait_for_packet_relay( + &hermes_gaia_namada, + &PortId::transfer(), + &channel_from_gaia_to_namada, + &test_namada, + )?; + + // Check that the swap worked + // 39 is derived from the uniswap formula: + // floor( 100 - (100*100/(100 + 64)) ) + check_balance( + &test_namada, + BERTHA, + format!("transfer/{channel_from_namada_to_gaia}/{COSMOS_COIN}"), + 39, + )?; + + // Perform a shielded swap of samoleans and nam + run!( + &test_namada, + Bin::Client, + [ + "osmosis-swap", + "--osmosis-rest-rpc", + "http://localhost:1317", + "--source", + AA_VIEWING_KEY, + "--token", + NAM, + "--amount", + "0.000056", + "--channel-id", + channel_from_namada_to_osmosis.as_ref(), + "--output-denom", + &output_denom_on_namada, + "--local-recovery-addr", + &osmosis_jones, + "--swap-contract", + &crosschain_swaps_addr, + "--minimum-amount", + "10", + "--target-pa", + AA_PAYMENT_ADDRESS, + "--overflow-addr", + ALBERT, + "--pool-hop", + &format!("1:{output_denom_on_osmosis}"), + "--gas-payer", + ALBERT_KEY, + "--node", + &rpc_namada, + "--gas-limit", + "500000", + ], + Some(40), + )? + .assert_success(); + + wait_for_packet_relay( + &hermes_namada_osmosis, + &PortId::transfer(), + &channel_from_osmosis_to_namada, + &test_osmosis, + )?; + wait_for_packet_relay( + &hermes_gaia_namada, + &PortId::transfer(), + &channel_from_gaia_to_namada, + &test_namada, + )?; + + // Check that the minimum amount got shielded + check_shielded_balance( + &test_namada, + AA_VIEWING_KEY, + &output_denom_on_namada, + 10, + )?; + // 5 is derived from the uniswap formula: + // floor( 61 - ( 164 * 61 / (164 + 56) ) ) minus + // the minimum amount (10) which was shielded + check_balance(&test_namada, ALBERT, &output_denom_on_namada, 5)?; + + Ok(()) +} + +fn cosmos_common_args<'a>( + gas: &'a str, + gas_price: Option<&'a str>, + chain_id: &'a str, + rpc: &'a str, + mut args: Vec<&'a str>, +) -> Vec<&'a str> { + args.extend_from_slice(&[ + "--from", + COSMOS_USER, + "--gas", + gas, + "--gas-prices", + gas_price.unwrap_or("0.01stake"), + "--node", + rpc, + "--keyring-backend", + "test", + "--chain-id", + chain_id, + "--yes", + ]); + args +} + +fn build_contract_addr( + test: &Test, + wasm_bytecode_hash: &str, + creator_addr: &str, +) -> Result { + let osmosis_home = test.test_dir.as_ref().join("osmosis"); + let args = [ + "--home", + osmosis_home.to_str().unwrap(), + "query", + "wasm", + "build-address", + wasm_bytecode_hash, + creator_addr, + CONTRACT_SALT_HEX, + ]; + let mut cosmos = run_cosmos_cmd_homeless(test, args, Some(40))?; + let (_, matched) = cosmos.exp_regex("address: .*\n")?; + let mut parts = matched.split(':'); + parts.next().unwrap(); + Ok(parts.next().unwrap().trim().to_string()) +} + +/// Create a liquidity pool on osmosis. +/// All tokens will be 1:1 swappable in the provided +/// denoms. +fn create_pool( + test: &Test, + rpc: &str, + denoms_and_deposits: &[(&str, u64)], +) -> Result<()> { + let json_path = test.test_dir.path().join("pool.json"); + let mut weights = + denoms_and_deposits + .iter() + .fold(String::new(), |mut acc, (d, _)| { + acc.push_str(&format!("1{d},")); + acc + }); + weights.pop(); + let mut init_deposits = + denoms_and_deposits + .iter() + .fold(String::new(), |mut acc, (d, a)| { + acc.push_str(&format!("{a}{d},")); + acc + }); + init_deposits.pop(); + let pool_json = json!({ + "weights": weights, + "initial-deposit": init_deposits, + "swap-fee": "0.001", + "exit-fee": "0.000" + }); + let json_file = File::create(&json_path)?; + serde_json::to_writer(json_file, &pool_json)?; + let json_path = json_path.to_string_lossy(); + let args = cosmos_common_args( + "2500000", + Some("0.01stake"), + "osmosis", + rpc, + vec![ + "tx", + "poolmanager", + "create-pool", + "--pool-file", + &json_path, + ], + ); + let mut osmosis_cmd = run_cosmos_cmd(test, args, Some(40))?; + osmosis_cmd.assert_success(); + std::thread::sleep(Duration::from_secs(5)); + Ok(()) +} + +const CONTRACT_SALT_HEX: &str = "01020304"; diff --git a/crates/tests/src/e2e/setup.rs b/crates/tests/src/e2e/setup.rs index d0bdb14f80..4eec9b69be 100644 --- a/crates/tests/src/e2e/setup.rs +++ b/crates/tests/src/e2e/setup.rs @@ -42,8 +42,8 @@ use rand::Rng; use tempfile::{tempdir, tempdir_in, TempDir}; use crate::e2e::helpers::{ - find_cosmos_address, generate_bin_command, make_hermes_config, - update_cosmos_config, + find_cosmos_address, generate_bin_command, get_cosmos_rpc_address, + make_hermes_config, update_cosmos_config, }; /// For `color_eyre::install`, which fails if called more than once in the same @@ -844,6 +844,22 @@ pub fn working_dir() -> PathBuf { working_dir } +/// Return the path to all test fixture. +pub fn fixtures_dir() -> PathBuf { + let mut dir = working_dir(); + dir.push("crates"); + dir.push("tests"); + dir.push("fixtures"); + dir +} + +/// Return the path to all osmosis fixture. +pub fn osmosis_fixtures_dir() -> PathBuf { + let mut dir = fixtures_dir(); + dir.push("osmosis_data"); + dir +} + /// A command under test pub struct NamadaCmd { pub session: Session>, @@ -1243,31 +1259,126 @@ pub fn setup_hermes(test_a: &Test, test_b: &Test) -> Result { let hermes_dir = TestDir::new(); println!("\n{}", "Setting up Hermes".underline().green(),); - - make_hermes_config(&hermes_dir, test_a, test_b)?; + let chain_name_a = + CosmosChainType::chain_type(test_a.net.chain_id.as_str()) + .map(|c| c.chain_id()) + .ok(); + let chain_name_b = + CosmosChainType::chain_type(test_b.net.chain_id.as_str()) + .map(|c| c.chain_id()) + .ok(); + let relayer = chain_name_a + .zip(chain_name_b) + .map(|(a, b)| format!("{a}_{b}_relayer")); + make_hermes_config( + &hermes_dir, + test_a, + test_b, + relayer.as_ref().map(|s| s.as_ref()), + )?; for test in [test_a, test_b] { let chain_id = test.net.chain_id.as_str(); let chain_dir = test.test_dir.as_ref().join(chain_id); - let key_file_path = match CosmosChainType::chain_type(chain_id) { - Ok(_) => chain_dir - .join(format!("{}_seed.json", constants::COSMOS_RELAYER)), - Err(_) => wallet::wallet_file(chain_dir), + match CosmosChainType::chain_type(chain_id) { + Ok(_) => { + if let Some(relayer) = relayer.as_ref() { + // we create a new relayer for each ibc connection between + // to non-Namada chains + let key_file = + chain_dir.join(format!("{relayer}_seed.json")); + let args = [ + "keys", + "add", + relayer, + "--keyring-backend", + "test", + "--output", + "json", + ]; + let mut cosmos = run_cosmos_cmd(test, args, Some(10))?; + let result = cosmos.exp_string("\n")?; + let mut file = File::create(&key_file).unwrap(); + file.write_all(result.as_bytes()).map_err(|e| { + eyre!(format!( + "Writing a Cosmos key file failed: {}", + e + )) + })?; + + let account = find_cosmos_address(test, relayer)?; + // Add tokens to the new relayer account + let args = [ + "tx", + "bank", + "send", + constants::COSMOS_RELAYER, + &account, + "500000000stake", + "--from", + constants::COSMOS_RELAYER, + "--gas", + "250000", + "--gas-prices", + "0.01stake", + "--node", + &format!("http://{}", get_cosmos_rpc_address(test)), + "--keyring-backend", + "test", + "--chain-id", + chain_id, + "--yes", + ]; + + let mut cosmos = run_cosmos_cmd(test, args, Some(10))?; + cosmos.assert_success(); + + // add to hermes + let args = [ + "keys", + "add", + "--chain", + chain_id, + "--key-file", + &key_file.to_string_lossy(), + "--key-name", + relayer, + ]; + let mut hermes = + run_hermes_cmd(&hermes_dir, args, Some(20))?; + hermes.assert_success(); + } else { + let key_file_path = chain_dir.join(format!( + "{}_seed.json", + constants::COSMOS_RELAYER + )); + let args = [ + "keys", + "add", + "--chain", + chain_id, + "--key-file", + &key_file_path.to_string_lossy(), + ]; + let mut hermes = + run_hermes_cmd(&hermes_dir, args, Some(20))?; + hermes.assert_success(); + } + } + Err(_) => { + let key_file_path = wallet::wallet_file(&chain_dir); + let args = [ + "keys", + "add", + "--chain", + chain_id, + "--key-file", + &key_file_path.to_string_lossy(), + ]; + let mut hermes = run_hermes_cmd(&hermes_dir, args, Some(20))?; + hermes.assert_success(); + } }; - let args = [ - "keys", - "add", - // TODO: this overwrite is required because hermes keys are pulled - // from the namada chain dir... however, ideally we would store the - // `wallet.toml` file under hermes' own dir - "--overwrite", - "--chain", - chain_id, - "--key-file", - &key_file_path.to_string_lossy(), - ]; - let mut hermes = run_hermes_cmd(&hermes_dir, args, Some(20))?; - hermes.assert_success(); } Ok(hermes_dir) @@ -1341,9 +1452,46 @@ where pub enum CosmosChainType { Gaia(Option), CosmWasm, + Osmosis, } impl CosmosChainType { + fn genesis_cmd_args<'a>(&self, mut args: Vec<&'a str>) -> Vec<&'a str> { + if !matches!(self, CosmosChainType::Osmosis) { + args.insert(0, "genesis"); + } + args + } + + fn add_genesis_account_args<'a>( + &self, + account: &'a str, + coins: &'a str, + ) -> Vec<&'a str> { + self.genesis_cmd_args(vec!["add-genesis-account", account, coins]) + } + + fn gentx_args<'a>( + &self, + account: &'a str, + coins: &'a str, + chain_id: &'a str, + ) -> Vec<&'a str> { + self.genesis_cmd_args(vec![ + "gentx", + account, + coins, + "--keyring-backend", + "test", + "--chain-id", + chain_id, + ]) + } + + fn collect_gentxs_args<'a>(&self) -> Vec<&'a str> { + self.genesis_cmd_args(vec!["collect-gentxs"]) + } + pub fn chain_id(&self) -> String { match self { Self::Gaia(Some(suffix)) => { @@ -1351,6 +1499,7 @@ impl CosmosChainType { } Self::Gaia(_) => constants::GAIA_CHAIN_ID.to_string(), Self::CosmWasm => constants::COSMWASM_CHAIN_ID.to_string(), + Self::Osmosis => constants::OSMOSIS_CHAIN_ID.to_string(), } } @@ -1358,6 +1507,7 @@ impl CosmosChainType { match self { Self::Gaia(_) => "gaiad", Self::CosmWasm => "wasmd", + Self::Osmosis => "osmosisd", } } @@ -1365,6 +1515,9 @@ impl CosmosChainType { if chain_id == constants::COSMWASM_CHAIN_ID { return Ok(Self::CosmWasm); } + if chain_id == constants::OSMOSIS_CHAIN_ID { + return Ok(Self::Osmosis); + } match chain_id.strip_prefix(constants::GAIA_CHAIN_ID) { Some("") => Ok(Self::Gaia(None)), Some(suffix) => { @@ -1380,13 +1533,29 @@ impl CosmosChainType { match self { Self::Gaia(_) => "cosmos", Self::CosmWasm => "wasm", + Self::Osmosis => "osmo", } } - pub fn get_offset(&self) -> u64 { + pub fn get_p2p_port_number(&self) -> u64 { + 10_000 + self.get_offset() + } + + pub fn get_rpc_port_number(&self) -> u64 { + 20_000 + self.get_offset() + } + + pub fn get_grpc_port_number(&self) -> u64 { + 30_000 + self.get_offset() + } + + fn get_offset(&self) -> u64 { + // NB: ensure none of these ever conflict match self { - Self::Gaia(Some(off)) => *off, - _ => 0, + Self::CosmWasm => 0, + Self::Osmosis => 1, + Self::Gaia(None) => 2, + Self::Gaia(Some(off)) => 3 + *off, } } } @@ -1437,47 +1606,42 @@ pub fn setup_cosmos(chain_type: CosmosChainType) -> Result { // Add tokens to a user account let account = find_cosmos_address(&test, constants::COSMOS_USER)?; - let args = [ - "genesis", - "add-genesis-account", - &account, - "100000000stake,1000samoleans", - ]; + let args = if let CosmosChainType::Osmosis = chain_type { + chain_type.add_genesis_account_args( + &account, + "100000000stake,1000samoleans, 10000000000uosmo", + ) + } else { + chain_type + .add_genesis_account_args(&account, "100000000stake,1000samoleans") + }; let mut cosmos = run_cosmos_cmd(&test, args, Some(10))?; cosmos.assert_success(); // Add the stake token to the relayer let account = find_cosmos_address(&test, constants::COSMOS_RELAYER)?; - let args = ["genesis", "add-genesis-account", &account, "10000stake"]; + let args = + chain_type.add_genesis_account_args(&account, "10000000000stake"); let mut cosmos = run_cosmos_cmd(&test, args, Some(10))?; cosmos.assert_success(); // Add the stake token to the validator let validator = find_cosmos_address(&test, constants::COSMOS_VALIDATOR)?; - let args = [ - "genesis", - "add-genesis-account", - &validator, - "200000000000stake", - ]; + let args = + chain_type.add_genesis_account_args(&validator, "200000000000stake"); let mut cosmos = run_cosmos_cmd(&test, args, Some(10))?; cosmos.assert_success(); // stake - let args = [ - "genesis", - "gentx", + let args = chain_type.gentx_args( constants::COSMOS_VALIDATOR, "100000000000stake", - "--keyring-backend", - "test", - "--chain-id", &chain_id, - ]; + ); let mut cosmos = run_cosmos_cmd(&test, args, Some(10))?; cosmos.assert_success(); - let args = ["genesis", "collect-gentxs"]; + let args = chain_type.collect_gentxs_args(); let mut cosmos = run_cosmos_cmd(&test, args, Some(10))?; cosmos.assert_success(); @@ -1486,6 +1650,67 @@ pub fn setup_cosmos(chain_type: CosmosChainType) -> Result { Ok(test) } +pub fn run_cosmos_cmd_homeless( + test: &Test, + args: I, + timeout_sec: Option, +) -> Result +where + I: IntoIterator, + S: AsRef, +{ + let chain_id = test.net.chain_id.as_str(); + let chain_type = CosmosChainType::chain_type(chain_id)?; + let mut run_cmd = Command::new(chain_type.command_path()); + run_cmd.args(args); + + let args: String = + run_cmd.get_args().map(|s| s.to_string_lossy()).join(" "); + let cmd_str = + format!("{} {}", run_cmd.get_program().to_string_lossy(), args); + + let session = Session::spawn(run_cmd).map_err(|e| { + eyre!( + "\n\n{}: {}\n{}: {}", + "Failed to run Cosmos command".underline().red(), + cmd_str, + "Error".underline().red(), + e + ) + })?; + + let log_path = { + let mut rng = rand::thread_rng(); + let log_dir = test.get_base_dir(Who::NonValidator).join("logs"); + std::fs::create_dir_all(&log_dir)?; + log_dir.join(format!( + "{}-cosmos-{}.log", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_micros(), + rng.gen::() + )) + }; + let logger = OpenOptions::new() + .write(true) + .create_new(true) + .open(&log_path)?; + let mut session = expectrl::session::log(session, logger).unwrap(); + + session.set_expect_timeout(timeout_sec.map(std::time::Duration::from_secs)); + + let cmd_process = NamadaCmd { + session, + cmd_str, + log_path, + }; + + println!("{}:\n{}", "> Running".underline().green(), &cmd_process); + + Ok(cmd_process) +} + pub fn run_cosmos_cmd( test: &Test, args: I, @@ -1601,10 +1826,10 @@ pub mod constants { pub const APFEL: &str = "Apfel"; pub const KARTOFFEL: &str = "Kartoffel"; - // Gaia or CosmWasm + // Gaia or CosmWasm or Osmosis pub const GAIA_CHAIN_ID: &str = "gaia"; + pub const OSMOSIS_CHAIN_ID: &str = "osmosis"; pub const COSMWASM_CHAIN_ID: &str = "cosmwasm"; - pub const COSMOS_RPC: &str = "127.0.0.1:64160"; pub const COSMOS_USER: &str = "user"; pub const COSMOS_RELAYER: &str = "relayer"; pub const COSMOS_VALIDATOR: &str = "validator"; diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 6400887db7..1e6a4389fb 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -3076,10 +3076,90 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "ibc-middleware-module" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=module/v0.1.0#3d3b436f7c58000c7498d68e88c15a955433a619" +dependencies = [ + "ibc-core-channel-types", + "ibc-core-host-types", + "ibc-core-router", + "ibc-core-router-types", + "ibc-primitives", +] + +[[package]] +name = "ibc-middleware-module" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=orm/v0.4.0#8d341de14ff5e2a637699796cffbf0fbbaee001f" +dependencies = [ + "ibc-core-channel-types", + "ibc-core-host-types", + "ibc-core-router", + "ibc-core-router-types", + "ibc-primitives", +] + +[[package]] +name = "ibc-middleware-module" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.9.0#3d3b436f7c58000c7498d68e88c15a955433a619" +dependencies = [ + "ibc-core-channel-types", + "ibc-core-host-types", + "ibc-core-router", + "ibc-core-router-types", + "ibc-primitives", +] + +[[package]] +name = "ibc-middleware-module-macros" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=module-macros/v0.1.0#3d3b436f7c58000c7498d68e88c15a955433a619" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "ibc-middleware-module-macros" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=orm/v0.4.0#8d341de14ff5e2a637699796cffbf0fbbaee001f" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "ibc-middleware-module-macros" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.9.0#3d3b436f7c58000c7498d68e88c15a955433a619" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "ibc-middleware-overflow-receive" +version = "0.4.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=orm/v0.4.0#8d341de14ff5e2a637699796cffbf0fbbaee001f" +dependencies = [ + "ibc-app-transfer-types", + "ibc-core-channel-types", + "ibc-core-host-types", + "ibc-core-router", + "ibc-core-router-types", + "ibc-middleware-module 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=orm/v0.4.0)", + "ibc-middleware-module-macros 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=orm/v0.4.0)", + "ibc-primitives", + "serde", + "serde_json", +] + [[package]] name = "ibc-middleware-packet-forward" -version = "0.8.0" -source = "git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.8.0#9c4a410063df8562c726c76009ff08b4e5a1894a" +version = "0.9.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.9.0#3d3b436f7c58000c7498d68e88c15a955433a619" dependencies = [ "borsh", "dur", @@ -3090,6 +3170,8 @@ dependencies = [ "ibc-core-host-types", "ibc-core-router", "ibc-core-router-types", + "ibc-middleware-module 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.9.0)", + "ibc-middleware-module-macros 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.9.0)", "ibc-primitives", "serde", "serde_json", @@ -3991,6 +4073,9 @@ dependencies = [ "dur", "ibc", "ibc-derive", + "ibc-middleware-module 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=module/v0.1.0)", + "ibc-middleware-module-macros 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=module-macros/v0.1.0)", + "ibc-middleware-overflow-receive", "ibc-middleware-packet-forward", "ibc-testkit", "ics23", @@ -4102,6 +4187,7 @@ name = "namada_sdk" version = "0.46.1" dependencies = [ "async-trait", + "bech32 0.8.1", "bimap", "borsh", "circular-queue", @@ -5318,9 +5404,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.35" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] diff --git a/wasm_for_tests/Cargo.lock b/wasm_for_tests/Cargo.lock index f993f3e33b..f6e7a4aa12 100644 --- a/wasm_for_tests/Cargo.lock +++ b/wasm_for_tests/Cargo.lock @@ -1710,10 +1710,90 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "ibc-middleware-module" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=module/v0.1.0#3d3b436f7c58000c7498d68e88c15a955433a619" +dependencies = [ + "ibc-core-channel-types", + "ibc-core-host-types", + "ibc-core-router", + "ibc-core-router-types", + "ibc-primitives", +] + +[[package]] +name = "ibc-middleware-module" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=orm/v0.4.0#8d341de14ff5e2a637699796cffbf0fbbaee001f" +dependencies = [ + "ibc-core-channel-types", + "ibc-core-host-types", + "ibc-core-router", + "ibc-core-router-types", + "ibc-primitives", +] + +[[package]] +name = "ibc-middleware-module" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.9.0#3d3b436f7c58000c7498d68e88c15a955433a619" +dependencies = [ + "ibc-core-channel-types", + "ibc-core-host-types", + "ibc-core-router", + "ibc-core-router-types", + "ibc-primitives", +] + +[[package]] +name = "ibc-middleware-module-macros" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=module-macros/v0.1.0#3d3b436f7c58000c7498d68e88c15a955433a619" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "ibc-middleware-module-macros" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=orm/v0.4.0#8d341de14ff5e2a637699796cffbf0fbbaee001f" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "ibc-middleware-module-macros" +version = "0.1.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.9.0#3d3b436f7c58000c7498d68e88c15a955433a619" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "ibc-middleware-overflow-receive" +version = "0.4.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=orm/v0.4.0#8d341de14ff5e2a637699796cffbf0fbbaee001f" +dependencies = [ + "ibc-app-transfer-types", + "ibc-core-channel-types", + "ibc-core-host-types", + "ibc-core-router", + "ibc-core-router-types", + "ibc-middleware-module 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=orm/v0.4.0)", + "ibc-middleware-module-macros 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=orm/v0.4.0)", + "ibc-primitives", + "serde", + "serde_json", +] + [[package]] name = "ibc-middleware-packet-forward" -version = "0.8.0" -source = "git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.8.0#9c4a410063df8562c726c76009ff08b4e5a1894a" +version = "0.9.0" +source = "git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.9.0#3d3b436f7c58000c7498d68e88c15a955433a619" dependencies = [ "borsh", "dur", @@ -1724,6 +1804,8 @@ dependencies = [ "ibc-core-host-types", "ibc-core-router", "ibc-core-router-types", + "ibc-middleware-module 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.9.0)", + "ibc-middleware-module-macros 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=pfm/v0.9.0)", "ibc-primitives", "serde", "serde_json", @@ -2363,6 +2445,9 @@ dependencies = [ "dur", "ibc", "ibc-derive", + "ibc-middleware-module 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=module/v0.1.0)", + "ibc-middleware-module-macros 0.1.0 (git+https://github.com/heliaxdev/ibc-middleware?tag=module-macros/v0.1.0)", + "ibc-middleware-overflow-receive", "ibc-middleware-packet-forward", "ics23", "konst", @@ -3222,9 +3307,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ]