diff --git a/Cargo.lock b/Cargo.lock index 15db2d07..af89f1f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1914,6 +1914,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "skip-go-swap-adapter-swap-source" +version = "0.3.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "skip", + "thiserror", +] + [[package]] name = "skip-go-swap-adapter-white-whale" version = "0.3.0" diff --git a/contracts/adapters/swap/source/Cargo.toml b/contracts/adapters/swap/source/Cargo.toml new file mode 100644 index 00000000..c01c33d1 --- /dev/null +++ b/contracts/adapters/swap/source/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "skip-go-swap-adapter-swap-source" +version = { workspace = true } +rust-version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +documentation = { workspace = true } +keywords = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw2 = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +skip = { workspace = true } +thiserror = { workspace = true } \ No newline at end of file diff --git a/contracts/adapters/swap/source/src/contract.rs b/contracts/adapters/swap/source/src/contract.rs new file mode 100644 index 00000000..db701bd8 --- /dev/null +++ b/contracts/adapters/swap/source/src/contract.rs @@ -0,0 +1,383 @@ +use crate::error::ContractError; +use crate::msg::InstantiateMsg; +use crate::swap_source; +use crate::swap_source::Hop; +use cosmwasm_std::{ + to_json_binary, to_json_string, Addr, Binary, Coin, Decimal, Deps, DepsMut, Empty, Env, + MessageInfo, Response, StdError, Uint128, +}; + +use cw2::set_contract_version; +use cw_storage_plus::Item; +use cw_utils::one_coin; +use skip::asset::Asset; +use skip::swap::{ + get_ask_denom_for_routes, ExecuteMsg, QueryMsg, Route, SimulateSmartSwapExactAssetInResponse, + SimulateSwapExactAssetInResponse, SimulateSwapExactAssetOutResponse, SwapOperation, +}; + +///////////// +/// STATE /// +///////////// +const ENTRY_POINT_CONTRACT_ADDRESS: Item = Item::new("e"); +const ROUTER_CONTRACT_ADDRESS: Item = Item::new("r"); + +/////////////// +/// MIGRATE /// +/////////////// + +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: Empty) -> Result { + Ok(Response::default()) +} + +/////////////////// +/// INSTANTIATE /// +/////////////////// + +// Contract name and version used for migration. +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // Validate entry point contract address and router address + let checked_entry_point_contract_address = + deps.api.addr_validate(&msg.entry_point_contract_address)?; + + let checked_router_address = deps.api.addr_validate(&msg.router_contract_address)?; + + // Store the entry point and router contract address + ENTRY_POINT_CONTRACT_ADDRESS.save(deps.storage, &checked_entry_point_contract_address)?; + ROUTER_CONTRACT_ADDRESS.save(deps.storage, &checked_router_address)?; + + Ok(Response::new() + .add_attribute("action", "instantiate") + .add_attribute( + "entry_point_contract_address", + checked_entry_point_contract_address.to_string(), + )) +} + +///////////////// +/// EXECUTE //// +//////////////// + +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Swap { operations } => execute_swap(deps, env, info, operations), + _ => unimplemented!("not implemented"), + } +} + +// Executes a swap with the given swap operations and then transfers the funds back to the caller +fn execute_swap( + deps: DepsMut, + _env: Env, + info: MessageInfo, + operations: Vec, +) -> Result { + // Get entry point contract address from storage + let entry_point_contract_address = ENTRY_POINT_CONTRACT_ADDRESS.load(deps.storage)?; + + // Enforce the caller is the entry point contract + if info.sender != entry_point_contract_address { + return Err(ContractError::Unauthorized); + } + + // Get coin in from the message info, error if there is not exactly one coin sent + // this will fail in case more coins are sent. + let coin_in = one_coin(&info)?; + + let hops = operations.into_iter().map(|e| swap_source::Hop { + pool: e.pool, + denom: e.denom_out, + }); + + let router_contract_address = ROUTER_CONTRACT_ADDRESS.load(deps.storage)?; + + // Make the swap msg + let swap_msg = swap_source::ExecuteMsg::SwapExactAmountInWithHops { + receiver: Some(info.sender.into()), + min_out: Uint128::zero(), + hops: hops.collect(), + } + .into_cosmos_msg(router_contract_address, vec![coin_in])?; + + Ok(Response::new() + .add_message(swap_msg) + .add_attribute("action", "dispatch_swap_and_transfer_back")) +} + +///////////// +/// QUERY /// +///////////// + +#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::SimulateSwapExactAssetOut { + asset_out, + swap_operations, + } => Ok(to_json_binary(&query_simulate_swap_exact_asset_out( + deps, + asset_out, + swap_operations, + )?)?), + QueryMsg::SimulateSwapExactAssetIn { + asset_in, + swap_operations, + } => Ok(to_json_binary(&query_simulate_swap_exact_asset_in( + deps, + asset_in, + swap_operations, + )?)?), + QueryMsg::SimulateSwapExactAssetOutWithMetadata { + asset_out, + swap_operations, + include_spot_price, + } => Ok(to_json_binary( + &query_simulate_swap_exact_asset_out_with_metadata( + deps, + asset_out, + swap_operations, + include_spot_price, + )?, + )?), + QueryMsg::SimulateSwapExactAssetInWithMetadata { + asset_in, + swap_operations, + include_spot_price, + } => Ok(to_json_binary( + &query_simulate_swap_exact_asset_in_with_metadata( + deps, + asset_in, + swap_operations, + include_spot_price, + )?, + )?), + QueryMsg::SimulateSmartSwapExactAssetIn { asset_in, routes } => { + let ask_denom = get_ask_denom_for_routes(&routes)?; + Ok(to_json_binary(&query_simulate_smart_swap_exact_asset_in( + deps, asset_in, ask_denom, &routes, + )?)?) + } + QueryMsg::SimulateSmartSwapExactAssetInWithMetadata { + asset_in, + routes, + include_spot_price, + } => { + let ask_denom = get_ask_denom_for_routes(&routes)?; + + Ok(to_json_binary( + &query_simulate_smart_swap_exact_asset_in_with_metadata( + deps, + asset_in, + ask_denom, + routes, + include_spot_price, + )?, + )?) + } + } +} + +fn query_simulate_swap_exact_asset_out( + deps: Deps, + asset_out: Asset, + operations: Vec, +) -> Result { + query_simulate_swap_exact_asset_out_with_metadata(deps, asset_out, operations, false) + .map(|r| r.asset_in) +} + +fn query_simulate_swap_exact_asset_out_with_metadata( + deps: Deps, + asset_out: Asset, + mut operations: Vec, + include_price: bool, +) -> Result { + let coin_out = get_coin_from_asset(asset_out)?; + let (first_op, last_op) = ( + operations + .first() + .ok_or(ContractError::SwapOperationsEmpty)?, + operations + .last() + .ok_or(ContractError::SwapOperationsEmpty)?, + ); + + // check that coin out matches the last swap operations out + if coin_out.denom != last_op.denom_out { + return Err(ContractError::CoinOutDenomMismatch); + } + + let denom_in = first_op.denom_in.clone(); + + let router = ROUTER_CONTRACT_ADDRESS.load(deps.storage)?; + + let first_hop = Hop { + pool: first_op.pool.clone(), + denom: first_op.denom_in.clone(), + }; + + let mut hops = vec![first_hop]; + // we omit the last swap operation + // since that is already the coin_out + operations.pop(); + // if it is not empty, we fill the other left operations. + if !operations.is_empty() { + hops.extend(operations.into_iter().map(|op| Hop { + pool: op.pool, + denom: op.denom_out, + })); + } + + let query_msg = swap_source::QueryMsg::SimulateSwapExactAmountOutWithHops { + want_out: coin_out, + hops, + }; + + let router_resp = deps + .querier + .query_wasm_smart::( + router, &query_msg, + ) + .map_err(|_| { + let ctx = to_json_string(&query_msg).unwrap(); + StdError::generic_err(ctx) + })?; + + Ok(SimulateSwapExactAssetOutResponse { + asset_in: Coin { + denom: denom_in, + amount: router_resp.need_input, + } + .into(), + spot_price: include_price.then_some(router_resp.spot_price), + }) +} + +fn query_simulate_swap_exact_asset_in( + deps: Deps, + asset: Asset, + operations: Vec, +) -> Result { + query_simulate_swap_exact_asset_in_with_metadata(deps, asset, operations, false) + .map(|v| v.asset_out) +} + +fn query_simulate_swap_exact_asset_in_with_metadata( + deps: Deps, + asset: Asset, + operations: Vec, + price: bool, +) -> Result { + let expected_denom_out = operations.last().unwrap().denom_out.clone(); + + let coin = get_coin_from_asset(asset)?; + + let router = ROUTER_CONTRACT_ADDRESS.load(deps.storage)?; + + let query_msg = swap_source::QueryMsg::SimulateSwapExactAmountInWithHops { + input: coin, + hops: operations + .into_iter() + .map(|op| Hop { + pool: op.pool, + denom: op.denom_out, + }) + .collect(), + }; + + let router_resp = deps + .querier + .query_wasm_smart::(router, &query_msg)?; + + let coin_out = Coin { + denom: expected_denom_out, + amount: router_resp.coin_out, + }; + + Ok(SimulateSwapExactAssetInResponse { + asset_out: coin_out.into(), + spot_price: price.then_some(router_resp.spot_price), + }) +} + +fn query_simulate_smart_swap_exact_asset_in_with_metadata( + deps: Deps, + asset_in: Asset, + ask_denom: String, + routes: Vec, + include_price: bool, +) -> Result { + let (asset_out, spot_price) = + simulate_smart_swap_exact_asset_in(deps, asset_in, ask_denom, &routes, include_price)?; + + Ok(SimulateSmartSwapExactAssetInResponse { + asset_out, + spot_price, + }) +} + +fn query_simulate_smart_swap_exact_asset_in( + deps: Deps, + asset_in: Asset, + ask_denom: String, + routes: &[Route], +) -> Result { + simulate_smart_swap_exact_asset_in(deps, asset_in, ask_denom, routes, false).map(|r| r.0) +} + +fn simulate_smart_swap_exact_asset_in( + deps: Deps, + asset_in: Asset, + ask_denom: String, + routes: &[Route], + include_price: bool, +) -> Result<(Asset, Option), ContractError> { + let mut asset_out = Asset::new(deps.api, &ask_denom, Uint128::zero()); + + let mut weighted_price = Decimal::zero(); + + for route in routes { + let swap_in_with_meta = query_simulate_swap_exact_asset_in_with_metadata( + deps, + route.offer_asset.clone(), + route.operations.clone(), + include_price, + )?; + + asset_out.add(swap_in_with_meta.asset_out.amount())?; + + if include_price { + let weight = Decimal::from_ratio(route.offer_asset.amount(), asset_in.amount()); + // the spot price returned by the swap baby router is the + // price of the cumulative swap operations. + weighted_price += swap_in_with_meta.spot_price.unwrap() * weight; + } + } + + Ok((asset_out, include_price.then_some(weighted_price))) +} + +fn get_coin_from_asset(asset: Asset) -> Result { + match asset { + Asset::Native(coin) => Ok(coin), + Asset::Cw20(_) => Err(ContractError::AssetNotNative), + } +} diff --git a/contracts/adapters/swap/source/src/error.rs b/contracts/adapters/swap/source/src/error.rs new file mode 100644 index 00000000..a3e1e417 --- /dev/null +++ b/contracts/adapters/swap/source/src/error.rs @@ -0,0 +1,31 @@ +use cosmwasm_std::StdError; +use cw_utils::PaymentError; +use skip::error::SkipError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Skip(#[from] SkipError), + + #[error(transparent)] + Payment(#[from] PaymentError), + + #[error(transparent)] + Overflow(#[from] cosmwasm_std::OverflowError), + + #[error("Unauthorized")] + Unauthorized, + + #[error("swap_operations cannot be empty")] + SwapOperationsEmpty, + + #[error("Asset Must Be Native, there is no Support for CW20 Tokens")] + AssetNotNative, + + #[error("coin_out denom must match the last swap operation's denom out")] + CoinOutDenomMismatch, +} diff --git a/contracts/adapters/swap/source/src/lib.rs b/contracts/adapters/swap/source/src/lib.rs new file mode 100644 index 00000000..de87ab36 --- /dev/null +++ b/contracts/adapters/swap/source/src/lib.rs @@ -0,0 +1,5 @@ +pub mod contract; +pub mod error; +pub mod msg; +pub mod state; +pub mod swap_source; diff --git a/contracts/adapters/swap/source/src/msg.rs b/contracts/adapters/swap/source/src/msg.rs new file mode 100644 index 00000000..5bd444ef --- /dev/null +++ b/contracts/adapters/swap/source/src/msg.rs @@ -0,0 +1,7 @@ +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct InstantiateMsg { + pub entry_point_contract_address: String, + pub router_contract_address: String, +} diff --git a/contracts/adapters/swap/source/src/state.rs b/contracts/adapters/swap/source/src/state.rs new file mode 100644 index 00000000..069f4ff2 --- /dev/null +++ b/contracts/adapters/swap/source/src/state.rs @@ -0,0 +1,8 @@ +use cosmwasm_std::Addr; +use cw_storage_plus::Item; + +///////////// +/// STATE /// +///////////// +pub const ENTRY_POINT_CONTRACT_ADDRESS: Item = Item::new("e"); +pub const ROUTER_CONTRACT_ADDRESS: Item = Item::new("r"); diff --git a/contracts/adapters/swap/source/src/swap_source.rs b/contracts/adapters/swap/source/src/swap_source.rs new file mode 100644 index 00000000..48d85ff2 --- /dev/null +++ b/contracts/adapters/swap/source/src/swap_source.rs @@ -0,0 +1,62 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{to_json_binary, Coin, CosmosMsg, Decimal, StdResult, Uint128, WasmMsg}; + +#[cw_serde] +pub enum ExecuteMsg { + /// Swaps the provided funds for the specified output + /// going through the provided steps. Fails if the + /// swap does not meet the minimum amount out. + SwapExactAmountInWithHops { + receiver: Option, + min_out: Uint128, + hops: Vec, + }, +} + +impl ExecuteMsg { + pub fn into_cosmos_msg( + self, + addr: impl Into, + funds: Vec, + ) -> StdResult { + Ok(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: addr.into(), + msg: to_json_binary(&self)?, + funds, + })) + } +} + +#[cw_serde] +pub struct SwapExactAmountInWithHopsResponse { + pub coin_out: Uint128, + pub spot_price: Decimal, + pub fees: Vec, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(QuerySimulateSwapExactAmountInWithHopsResponse)] + SimulateSwapExactAmountInWithHops { input: Coin, hops: Vec }, + #[returns(QuerySimulateSwapExactAmountOutWithHopsResponse)] + SimulateSwapExactAmountOutWithHops { want_out: Coin, hops: Vec }, +} + +#[cw_serde] +pub struct QuerySimulateSwapExactAmountInWithHopsResponse { + pub coin_out: Uint128, + pub fees: Vec, + pub spot_price: Decimal, +} +#[cw_serde] +pub struct QuerySimulateSwapExactAmountOutWithHopsResponse { + pub need_input: Uint128, + pub fees: Vec, + pub spot_price: Decimal, +} +#[cw_serde] +pub struct Hop { + pub pool: String, + pub denom: String, +} diff --git a/contracts/adapters/swap/source/tests/execute_test.rs b/contracts/adapters/swap/source/tests/execute_test.rs new file mode 100644 index 00000000..8dcf0c1f --- /dev/null +++ b/contracts/adapters/swap/source/tests/execute_test.rs @@ -0,0 +1,136 @@ +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{to_json_binary, Addr, Coin, CosmosMsg, WasmMsg}; +use skip::swap::{ExecuteMsg, SwapOperation}; +use skip_go_swap_adapter_swap_source::contract::{execute, instantiate}; +use skip_go_swap_adapter_swap_source::error::ContractError; +use skip_go_swap_adapter_swap_source::msg::InstantiateMsg; +use skip_go_swap_adapter_swap_source::state::{ + ENTRY_POINT_CONTRACT_ADDRESS, ROUTER_CONTRACT_ADDRESS, +}; +use skip_go_swap_adapter_swap_source::swap_source::ExecuteMsg as SwapBabyExecuteMsg; +use skip_go_swap_adapter_swap_source::swap_source::Hop as SwapBabyHop; + +#[test] +fn test_instantiate() { + // Setup + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("addr", &[]); + let entry_point_addr = Addr::unchecked("entry_point"); + let router_addr = Addr::unchecked("router"); + + // Instantiate the contract + let msg = InstantiateMsg { + entry_point_contract_address: entry_point_addr.to_string(), + router_contract_address: router_addr.to_string(), + }; + let result = instantiate(deps.as_mut(), env, info, msg); + assert!(result.is_ok(), "Instantiation should succeed"); + + // Verify state + let stored_entry_point = ENTRY_POINT_CONTRACT_ADDRESS + .load(deps.as_ref().storage) + .unwrap(); + assert_eq!( + entry_point_addr, stored_entry_point, + "Incorrect entry point address stored" + ); + + let stored_router = ROUTER_CONTRACT_ADDRESS.load(deps.as_ref().storage).unwrap(); + assert_eq!( + router_addr, stored_router, + "Incorrect router address stored" + ); +} + +#[test] +fn test_execute_unauthorized() { + // Setup + let mut deps = mock_dependencies(); + let env = mock_env(); + let instantiate_info = mock_info("addr", &[]); + let entry_point_addr = Addr::unchecked("entry_point"); + let router_addr = Addr::unchecked("router"); + + // Instantiate the contract + let instantiate_msg = InstantiateMsg { + entry_point_contract_address: entry_point_addr.to_string(), + router_contract_address: router_addr.to_string(), + }; + instantiate( + deps.as_mut(), + env.clone(), + instantiate_info, + instantiate_msg, + ) + .unwrap(); + + // Attempt unauthorized execution + let unauthorized_info = mock_info("unauthorized", &[]); + let execute_msg = ExecuteMsg::Swap { operations: vec![] }; + + let result = execute(deps.as_mut(), env, unauthorized_info, execute_msg); + + // Verify the error + match result { + Err(ContractError::Unauthorized {}) => {} + _ => panic!("Expected Unauthorized error, but got: {:?}", result), + } +} + +#[test] +fn test_execute_swap_success() { + // Setup + let mut deps = mock_dependencies(); + let env = mock_env(); + let entry_point_addr = Addr::unchecked("entry_point"); + let router_addr = Addr::unchecked("router"); + + // Instantiate the contract + let instantiate_msg = InstantiateMsg { + entry_point_contract_address: entry_point_addr.to_string(), + router_contract_address: router_addr.to_string(), + }; + let info = mock_info("addr", &[]); + instantiate(deps.as_mut(), env.clone(), info, instantiate_msg).unwrap(); + + // Prepare swap operations + let operations = vec![SwapOperation { + pool: "cw-BTC-USDT".to_string(), + denom_in: "BTC".to_string(), + denom_out: "USDT".to_string(), + interface: None, + }]; + + // Execute the swap + let execute_msg = ExecuteMsg::Swap { + operations: operations.clone(), + }; + let funds = vec![Coin::new(1000, "BTC")]; + let info = mock_info(entry_point_addr.as_str(), &funds); + + let result = execute(deps.as_mut(), env, info, execute_msg).unwrap(); + + // Assertions + assert_eq!(result.messages.len(), 1, "Expected one response message"); + + let actual_msg = &result.messages[0].msg; + let expected_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: router_addr.to_string(), + msg: to_json_binary(&SwapBabyExecuteMsg::SwapExactAmountInWithHops { + receiver: Some(entry_point_addr.to_string()), + min_out: Default::default(), + hops: operations + .iter() + .map(|op| SwapBabyHop { + pool: op.pool.clone(), + denom: op.denom_out.clone(), + }) + .collect(), + }) + .unwrap(), + funds, + }); + + assert_eq!(actual_msg, &expected_msg, "Unexpected message content"); +} diff --git a/contracts/adapters/swap/source/tests/queries_test.rs b/contracts/adapters/swap/source/tests/queries_test.rs new file mode 100644 index 00000000..b95c4950 --- /dev/null +++ b/contracts/adapters/swap/source/tests/queries_test.rs @@ -0,0 +1,448 @@ +use cosmwasm_schema::serde::Serialize; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{ + from_json, to_json_binary, Binary, Coin, ContractResult, Decimal, QuerierResult, Uint128, + WasmQuery, +}; +use skip::swap::{Route, SwapOperation}; +use skip_go_swap_adapter_swap_source::contract::{instantiate, query}; +use skip_go_swap_adapter_swap_source::error::ContractError; +use skip_go_swap_adapter_swap_source::msg::InstantiateMsg; +use skip_go_swap_adapter_swap_source::swap_source; +use std::str::FromStr; + +struct TestCase { + // name defines the test case name + name: &'static str, + // the skip query msg + input: skip::swap::QueryMsg, + // what the contract should forward to the router + expected_router_input: swap_source::QueryMsg, + // what the router will reply back + swap_source_router_resp: Binary, + // what the skip adapter contract should respond back + expected_output: Result, +} + +impl TestCase { + fn new( + name: &'static str, + input: skip::swap::QueryMsg, + expected_router_input: swap_source::QueryMsg, + router_response: impl Serialize, + expected_output: Result, + ) -> Self { + Self { + name, + input, + expected_router_input, + swap_source_router_resp: to_json_binary(&router_response).unwrap(), + expected_output: expected_output.map(|v| to_json_binary(&v).unwrap()), + } + } + + fn run_test(self) { + let mut deps = mock_dependencies(); + let env = mock_env(); + + let msg = InstantiateMsg { + entry_point_contract_address: "entry_point".to_string(), + router_contract_address: "router".to_string(), + }; + instantiate(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg).unwrap(); + + let querier = move |msg: &WasmQuery| -> QuerierResult { + match msg { + WasmQuery::Smart { contract_addr, msg } => { + assert_eq!(contract_addr, "router"); + let msg = from_json::(msg) + .expect("unable to decode router message"); + assert_eq!( + self.expected_router_input, msg, + "Test '{}': expected router request does not match the one from the adapter", + self.name, + ); + QuerierResult::Ok(ContractResult::Ok(self.swap_source_router_resp.clone())) + } + _ => panic!("unexpected query"), + } + }; + deps.querier.update_wasm(querier); + let query_resp = query(deps.as_ref(), env, self.input); + match self.expected_output { + Ok(r) => { + assert_eq!(r, query_resp.expect("no error wanted")); + } + Err(_err) => { + query_resp.expect_err("error wanted"); + } + } + } +} + +#[test] +fn tests() { + let tests = vec![ + TestCase::new( + "simulate swap exact asset in", + skip::swap::QueryMsg::SimulateSwapExactAssetIn { + asset_in: Coin::new(100, "btc").into(), + swap_operations: vec![ + SwapOperation { + pool: "cw-btc-usdt".to_string(), + denom_in: "BTC".to_string(), + denom_out: "USDT".to_string(), + interface: None, + }, + SwapOperation { + pool: "cw-eth-usdt".to_string(), + denom_in: "USDT".to_string(), + denom_out: "ETH".to_string(), + interface: None, + }, + ], + }, + swap_source::QueryMsg::SimulateSwapExactAmountInWithHops { + input: Coin::new(100, "btc"), + hops: vec![ + swap_source::Hop { + pool: "cw-btc-usdt".to_string(), + denom: "USDT".to_string(), + }, + swap_source::Hop { + pool: "cw-eth-usdt".to_string(), + denom: "ETH".to_string(), + }, + ], + }, + swap_source::QuerySimulateSwapExactAmountInWithHopsResponse { + coin_out: Uint128::new(10), + fees: vec![], + spot_price: Decimal::from_str("0.10").unwrap(), + }, + Ok(skip::asset::Asset::Native(Coin::new(10, "ETH"))), + ), + // simulate swap exact asset in with metadata and price + TestCase::new( + "simulate swap exact asset in with metadata and price", + skip::swap::QueryMsg::SimulateSwapExactAssetInWithMetadata { + asset_in: Coin::new(1, "BTC").into(), + swap_operations: vec![SwapOperation { + pool: "cw-btc-usdt".to_string(), + denom_in: "BTC".to_string(), + denom_out: "USDT".to_string(), + interface: None, + }], + include_spot_price: true, + }, + swap_source::QueryMsg::SimulateSwapExactAmountInWithHops { + input: Coin::new(1, "BTC"), + hops: vec![swap_source::Hop { + pool: "cw-btc-usdt".to_string(), + denom: "USDT".to_string(), + }], + }, + swap_source::QuerySimulateSwapExactAmountInWithHopsResponse { + coin_out: Uint128::new(100_000), + fees: vec![], + spot_price: Decimal::from_str("100000").unwrap(), + }, + Ok(skip::swap::SimulateSwapExactAssetInResponse { + asset_out: Coin::new(100_000, "USDT").into(), + spot_price: Some(Decimal::from_str("100000").unwrap()), + }), + ), + TestCase::new( + "simulate swap exact amount in with metadata and without price", + skip::swap::QueryMsg::SimulateSwapExactAssetInWithMetadata { + asset_in: Coin::new(1, "BTC").into(), + swap_operations: vec![SwapOperation { + pool: "cw-btc-usdt".to_string(), + denom_in: "BTC".to_string(), + denom_out: "USDT".to_string(), + interface: None, + }], + include_spot_price: false, + }, + swap_source::QueryMsg::SimulateSwapExactAmountInWithHops { + input: Coin::new(1, "BTC"), + hops: vec![swap_source::Hop { + pool: "cw-btc-usdt".to_string(), + denom: "USDT".to_string(), + }], + }, + swap_source::QuerySimulateSwapExactAmountInWithHopsResponse { + coin_out: Uint128::new(100_000), + fees: vec![], + spot_price: Decimal::from_str("100000").unwrap(), + }, + Ok(skip::swap::SimulateSwapExactAssetInResponse { + asset_out: Coin::new(100_000, "USDT").into(), + spot_price: None, + }), + ), + TestCase::new( + "simulate swap exact amount out without metadata", + skip::swap::QueryMsg::SimulateSwapExactAssetOut { + asset_out: Coin::new(10, "ETH").into(), + swap_operations: vec![ + SwapOperation { + pool: "cw-btc-usdt".to_string(), + denom_in: "BTC".to_string(), + denom_out: "USDT".to_string(), + interface: None, + }, + SwapOperation { + pool: "cw-eth-usdt".to_string(), + denom_in: "USDT".to_string(), + denom_out: "ETH".to_string(), + interface: None, + }, + ], + }, + swap_source::QueryMsg::SimulateSwapExactAmountOutWithHops { + want_out: Coin::new(10, "ETH"), + hops: vec![ + swap_source::Hop { + pool: "cw-btc-usdt".to_string(), + denom: "BTC".to_string(), + }, + swap_source::Hop { + pool: "cw-btc-usdt".to_string(), + denom: "USDT".to_string(), + }, + ], + }, + swap_source::QuerySimulateSwapExactAmountOutWithHopsResponse { + need_input: Uint128::new(1), + fees: vec![], + spot_price: Decimal::from_str("0.1").unwrap(), + }, + Ok(skip::asset::Asset::Native(Coin::new(1, "BTC"))), + ), + TestCase::new( + "simulate swap out with metadata and price", + skip::swap::QueryMsg::SimulateSwapExactAssetOutWithMetadata { + asset_out: Coin::new(10, "ETH").into(), + swap_operations: vec![ + SwapOperation { + pool: "cw-btc-usdt".to_string(), + denom_in: "BTC".to_string(), + denom_out: "USDT".to_string(), + interface: None, + }, + SwapOperation { + pool: "cw-eth-usdt".to_string(), + denom_in: "USDT".to_string(), + denom_out: "ETH".to_string(), + interface: None, + }, + ], + include_spot_price: true, + }, + swap_source::QueryMsg::SimulateSwapExactAmountOutWithHops { + want_out: Coin::new(10, "ETH"), + hops: vec![ + swap_source::Hop { + pool: "cw-btc-usdt".to_string(), + denom: "BTC".to_string(), + }, + swap_source::Hop { + pool: "cw-btc-usdt".to_string(), + denom: "USDT".to_string(), + }, + ], + }, + swap_source::QuerySimulateSwapExactAmountOutWithHopsResponse { + need_input: Uint128::new(1), + fees: vec![], + spot_price: Decimal::from_str("0.1").unwrap(), + }, + Ok(skip::swap::SimulateSwapExactAssetOutResponse { + asset_in: Coin::new(1, "BTC").into(), + spot_price: Some(Decimal::from_str("0.1").unwrap()), + }), + ), + TestCase::new( + "simulate swap out with metadata and without price", + skip::swap::QueryMsg::SimulateSwapExactAssetOutWithMetadata { + asset_out: Coin::new(10, "ETH").into(), + swap_operations: vec![ + SwapOperation { + pool: "cw-btc-usdt".to_string(), + denom_in: "BTC".to_string(), + denom_out: "USDT".to_string(), + interface: None, + }, + SwapOperation { + pool: "cw-eth-usdt".to_string(), + denom_in: "USDT".to_string(), + denom_out: "ETH".to_string(), + interface: None, + }, + ], + include_spot_price: false, + }, + swap_source::QueryMsg::SimulateSwapExactAmountOutWithHops { + want_out: Coin::new(10, "ETH"), + hops: vec![ + swap_source::Hop { + pool: "cw-btc-usdt".to_string(), + denom: "BTC".to_string(), + }, + swap_source::Hop { + pool: "cw-btc-usdt".to_string(), + denom: "USDT".to_string(), + }, + ], + }, + swap_source::QuerySimulateSwapExactAmountOutWithHopsResponse { + need_input: Uint128::new(1), + fees: vec![], + spot_price: Decimal::from_str("0.1").unwrap(), + }, + Ok(skip::swap::SimulateSwapExactAssetOutResponse { + asset_in: Coin::new(1, "BTC").into(), + spot_price: None, + }), + ), + TestCase::new( + "simulate smart swap exact asset in", + skip::swap::QueryMsg::SimulateSmartSwapExactAssetIn { + asset_in: Coin::new(1, "BTC").into(), + routes: vec![Route { + offer_asset: Coin::new(1, "BTC").into(), + operations: vec![ + SwapOperation { + pool: "cw-eth-btc".to_string(), + denom_in: "BTC".to_string(), + denom_out: "ETH".to_string(), + interface: None, + }, + SwapOperation { + pool: "cw-eth-usdt".to_string(), + denom_in: "ETH".to_string(), + denom_out: "USDT".to_string(), + interface: None, + }, + ], + }], + }, + swap_source::QueryMsg::SimulateSwapExactAmountInWithHops { + input: Coin::new(1, "BTC"), + hops: vec![ + swap_source::Hop { + pool: "cw-eth-btc".to_string(), + denom: "ETH".to_string(), + }, + swap_source::Hop { + pool: "cw-eth-usdt".to_string(), + denom: "USDT".to_string(), + }, + ], + }, + swap_source::QuerySimulateSwapExactAmountInWithHopsResponse { + coin_out: Uint128::new(100_000), + fees: vec![], + spot_price: Decimal::from_str("100000").unwrap(), + }, + Ok(skip::asset::Asset::Native(Coin::new(100_000, "USDT"))), + ), + TestCase::new( + "simulate smart swap exact asset in with metadata and price", + skip::swap::QueryMsg::SimulateSmartSwapExactAssetInWithMetadata { + asset_in: Coin::new(1, "BTC").into(), + routes: vec![Route { + offer_asset: Coin::new(1, "BTC").into(), + operations: vec![ + SwapOperation { + pool: "cw-eth-btc".to_string(), + denom_in: "BTC".to_string(), + denom_out: "ETH".to_string(), + interface: None, + }, + SwapOperation { + pool: "cw-eth-usdt".to_string(), + denom_in: "ETH".to_string(), + denom_out: "USDT".to_string(), + interface: None, + }, + ], + }], + include_spot_price: true, + }, + swap_source::QueryMsg::SimulateSwapExactAmountInWithHops { + input: Coin::new(1, "BTC"), + hops: vec![ + swap_source::Hop { + pool: "cw-eth-btc".to_string(), + denom: "ETH".to_string(), + }, + swap_source::Hop { + pool: "cw-eth-usdt".to_string(), + denom: "USDT".to_string(), + }, + ], + }, + swap_source::QuerySimulateSwapExactAmountInWithHopsResponse { + coin_out: Uint128::new(100_000), + fees: vec![], + spot_price: Decimal::from_str("100000").unwrap(), + }, + Ok(skip::swap::SimulateSmartSwapExactAssetInResponse { + asset_out: Coin::new(100_000, "USDT").into(), + spot_price: Some(Decimal::from_str("100000").unwrap()), + }), + ), + TestCase::new( + "simulate smart swap exact asset in with metadata without price", + skip::swap::QueryMsg::SimulateSmartSwapExactAssetInWithMetadata { + asset_in: Coin::new(1, "BTC").into(), + routes: vec![Route { + offer_asset: Coin::new(1, "BTC").into(), + operations: vec![ + SwapOperation { + pool: "cw-eth-btc".to_string(), + denom_in: "BTC".to_string(), + denom_out: "ETH".to_string(), + interface: None, + }, + SwapOperation { + pool: "cw-eth-usdt".to_string(), + denom_in: "ETH".to_string(), + denom_out: "USDT".to_string(), + interface: None, + }, + ], + }], + include_spot_price: false, + }, + swap_source::QueryMsg::SimulateSwapExactAmountInWithHops { + input: Coin::new(1, "BTC"), + hops: vec![ + swap_source::Hop { + pool: "cw-eth-btc".to_string(), + denom: "ETH".to_string(), + }, + swap_source::Hop { + pool: "cw-eth-usdt".to_string(), + denom: "USDT".to_string(), + }, + ], + }, + swap_source::QuerySimulateSwapExactAmountInWithHopsResponse { + coin_out: Uint128::new(100_000), + fees: vec![], + spot_price: Decimal::from_str("100000").unwrap(), + }, + Ok(skip::swap::SimulateSmartSwapExactAssetInResponse { + asset_out: Coin::new(100_000, "USDT").into(), + spot_price: None, + }), + ), + ]; + + for t in tests { + t.run_test(); + } +}