diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..04a9402 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,74 @@ +name: Test Localic-utils + +on: [push] + +env: + GO_VERSION: 1.21 + +jobs: + neutron-int-test: + name: Neutron integration test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Install local-ic + run: cd examples && git clone https://github.com/strangelove-ventures/interchaintest.git && cd interchaintest/local-interchain && make install + + - name: Get cargo + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Run neutron example + run: cd examples && local-ic start neutron_gaia --api-port 42069 & curl --head -X GET --retry 200 --retry-connrefused --retry-delay 1 http://localhost:42069 && cd examples && cargo run --example neutron + neutron-osmosis-int-test: + name: Neutron-Osmosis integration test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Install local-ic + run: cd examples && git clone https://github.com/strangelove-ventures/interchaintest.git && cd interchaintest/local-interchain && make install + + - name: Get cargo + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Run neutron-osmosis example + run: cd examples && local-ic start neutron_gaia --api-port 42069 & curl --head -X GET --retry 200 --retry-connrefused --retry-delay 1 http://localhost:42069 && cd examples && cargo run --example neutron_osmosis + osmosis-int-test: + name: Osmosis integration test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Install local-ic + run: cd examples && git clone https://github.com/strangelove-ventures/interchaintest.git && cd interchaintest/local-interchain && make install + + - name: Get cargo + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Run osmosis example + run: cd examples && local-ic start neutron_gaia --api-port 42069 & curl --head -X GET --retry 200 --retry-connrefused --retry-delay 1 http://localhost:42069 && cd examples && cargo run --example osmosis diff --git a/.gitignore b/.gitignore index 6985cf1..71a5c33 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,13 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb + + +# Added by cargo + +/target + +# Generated by emacs +*~ + +examples/configs/logs.json \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f1bf31c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "localic-utils" +version = "0.1.0" +edition = "2021" + +[dependencies] +localic-std = { git = "https://github.com/strangelove-ventures/interchaintest", rev = "f326371" } +cosmwasm-std = "1.5.4" +serde = { version = "1.0.203", features = ["derive"] } +serde_json = "1.0.117" +thiserror = "1.0" +derive_builder = "0.20.0" +log = "0.4.21" +astroport = "5.1.0" +reqwest = { version = "0.11.20", features = ["rustls-tls"] } + +[dev-dependencies] +env_logger = "0.11.3" \ No newline at end of file diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..4f51ee7 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,15 @@ +# localic-utils examples + +To run the examples: + +## Start local-interchain + +```bash +local-ic start --api-port 42069 +``` + +## Run the eample + +```bash +cargo run --example +``` diff --git a/examples/chains/neutron_gaia.json b/examples/chains/neutron_gaia.json new file mode 100644 index 0000000..71b2907 --- /dev/null +++ b/examples/chains/neutron_gaia.json @@ -0,0 +1,206 @@ +{ + "chains": [ + { + "name": "gaia", + "chain_id": "localcosmos-1", + "denom": "uatom", + "binary": "gaiad", + "bech32_prefix": "cosmos", + "docker_image": { + "version": "v15.0.0-rc2" + }, + "gas_prices": "0%DENOM%", + "chain_type": "cosmos", + "coin_type": 118, + "trusting_period": "336h", + "gas_adjustment": 2.0, + "number_vals": 1, + "number_node": 0, + "ibc_paths": [], + "debugging": true, + "block_time": "500ms", + "host_port_override": { + "26657": "26658", + "1317": "1318", + "9090": "9091" + }, + "genesis": { + "modify": [ + { + "key": "app_state.gov.params.voting_period", + "value": "3s" + }, + { + "key": "app_state.interchainaccounts.host_genesis_state.params.allow_messages", + "value": [ + "/cosmos.bank.v1beta1.MsgSend", + "/cosmos.bank.v1beta1.MsgMultiSend", + "/cosmos.staking.v1beta1.MsgDelegate", + "/cosmos.staking.v1beta1.MsgUndelegate", + "/cosmos.staking.v1beta1.MsgBeginRedelegate", + "/cosmos.staking.v1beta1.MsgRedeemTokensforShares", + "/cosmos.staking.v1beta1.MsgTokenizeShares", + "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", + "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", + "/ibc.applications.transfer.v1.MsgTransfer" + ] + } + ], + "accounts": [ + { + "name": "acc0", + "address": "cosmos1hj5fveer5cjtn4wd6wstzugjfdxzl0xpxvjjvr", + "amount": "20000000000%DENOM%", + "mnemonic": "decorate bright ozone fork gallery riot bus exhaust worth way bone indoor calm squirrel merry zero scheme cotton until shop any excess stage laundry" + } + ] + } + }, + { + "name": "neutron", + "chain_id": "localneutron-1", + "denom": "untrn", + "binary": "neutrond", + "bech32_prefix": "neutron", + "docker_image": { + "version": "v3.0.4", + "repository": "ghcr.io/strangelove-ventures/heighliner/neutron" + }, + "gas_prices": "0.0untrn,0.0uatom", + "chain_type": "cosmos", + "coin_type": 118, + "trusting_period": "336h", + "gas_adjustment": 1.3, + "number_vals": 1, + "number_node": 0, + "ics_consumer_link": "localcosmos-1", + "ibc_paths": ["neutron-osmosis"], + "debugging": true, + "block_time": "500ms", + "host_port_override": { + "26657": "26657", + "1317": "1317", + "9090": "9090" + }, + "genesis": { + "modify": [ + { + "key": "consensus_params.block.max_gas", + "value": "100000000" + }, + { + "key": "app_state.ccvconsumer.params.soft_opt_out_threshold", + "value": "0.05" + }, + { + "key": "app_state.ccvconsumer.params.reward_denoms", + "value": ["untrn"] + }, + { + "key": "app_state.ccvconsumer.params.provider_reward_denoms", + "value": ["uatom"] + }, + { + "key": "consensus_params.block.max_gas", + "value": "1000000000" + }, + { + "key": "app_state.globalfee.params.minimum_gas_prices", + "value": [ + { + "denom": "untrn", + "amount": "0" + } + ] + }, + { + "key": "app_state.feeburner.params.treasury_address", + "value": "neutron1hj5fveer5cjtn4wd6wstzugjfdxzl0xpznmsky" + }, + { + "key": "app_state.tokenfactory.params.fee_collector_address", + "value": "neutron1hj5fveer5cjtn4wd6wstzugjfdxzl0xpznmsky" + }, + { + "key": "app_state.interchainaccounts.host_genesis_state.params.allow_messages", + "value": [ + "/cosmos.bank.v1beta1.MsgSend", + "/cosmos.bank.v1beta1.MsgMultiSend", + "/cosmos.staking.v1beta1.MsgDelegate", + "/cosmos.staking.v1beta1.MsgUndelegate", + "/cosmos.staking.v1beta1.MsgBeginRedelegate", + "/cosmos.staking.v1beta1.MsgRedeemTokensforShares", + "/cosmos.staking.v1beta1.MsgTokenizeShares", + "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", + "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", + "/ibc.applications.transfer.v1.MsgTransfer", + "/ibc.lightclients.localhost.v2.ClientState", + "/ibc.core.client.v1.MsgCreateClient", + "/ibc.core.client.v1.Query/ClientState", + "/ibc.core.client.v1.Query/ConsensusState", + "/ibc.core.connection.v1.Query/Connection" + ] + } + ], + "accounts": [ + { + "name": "acc0", + "address": "neutron1hj5fveer5cjtn4wd6wstzugjfdxzl0xpznmsky", + "amount": "100000000000000000%DENOM%", + "mnemonic": "decorate bright ozone fork gallery riot bus exhaust worth way bone indoor calm squirrel merry zero scheme cotton until shop any excess stage laundry" + } + ] + } + }, + { + "name": "osmosis", + "chain_id": "localosmosis-1", + "denom": "uosmo", + "binary": "osmosisd", + "bech32_prefix": "osmo", + "docker_image": { + "version": "v25.0.4", + "repository": "ghcr.io/strangelove-ventures/heighliner/osmosis" + }, + "gas_prices": "0.0025%DENOM%", + "chain_type": "cosmos", + "coin_type": 118, + "trusting_period": "336h", + "gas_adjustment": 2, + "number_vals": 1, + "number_node": 0, + "ibc_paths": ["neutron-osmosis"], + "debugging": true, + "block_time": "500ms", + "host_port_override": { + "26657": "26659", + "1317": "1319", + "9090": "9092" + }, + "genesis": { + "modify": [ + { + "key": "app_state.gov.params.voting_period", + "value": "3s" + }, + { + "key": "app_state.gov.params.max_deposit_period", + "value": "15s" + }, + { + "key": "app_state.gov.params.min_deposit.0.denom", + "value": "uosmo" + } + ], + "accounts": [ + { + "name": "acc0", + "address": "osmo1hj5fveer5cjtn4wd6wstzugjfdxzl0xpwhpz63", + "amount": "10000000000%DENOM%", + "mnemonic": "decorate bright ozone fork gallery riot bus exhaust worth way bone indoor calm squirrel merry zero scheme cotton until shop any excess stage laundry" + } + ] + } + } + ] +} diff --git a/examples/configs/logs.json b/examples/configs/logs.json new file mode 100644 index 0000000..9c926f5 --- /dev/null +++ b/examples/configs/logs.json @@ -0,0 +1,72 @@ +{ + "start_time": 1719858423, + "chains": [ + { + "chain_id": "localcosmos-1", + "chain_name": "localcosmos-1", + "rpc_address": "http://0.0.0.0:26658", + "rest_address": "http://0.0.0.0:1318", + "grpc_address": "0.0.0.0:9091", + "p2p_address": "0.0.0.0:43999", + "ibc_paths": [] + }, + { + "chain_id": "localneutron-1", + "chain_name": "localneutron-1", + "rpc_address": "http://0.0.0.0:26657", + "rest_address": "http://0.0.0.0:1317", + "grpc_address": "0.0.0.0:9090", + "p2p_address": "0.0.0.0:33117", + "ibc_paths": [ + "neutron-osmosis" + ] + }, + { + "chain_id": "localosmosis-1", + "chain_name": "localosmosis-1", + "rpc_address": "http://0.0.0.0:26659", + "rest_address": "http://0.0.0.0:1319", + "grpc_address": "0.0.0.0:9092", + "p2p_address": "0.0.0.0:37211", + "ibc_paths": [ + "neutron-osmosis" + ] + } + ], + "ibc_channels": [ + { + "chain_id": "localneutron-1", + "channel": { + "state": "STATE_OPEN", + "ordering": "ORDER_UNORDERED", + "counterparty": { + "port_id": "transfer", + "channel_id": "channel-0" + }, + "connection_hops": [ + "connection-0" + ], + "version": "ics20-1", + "port_id": "transfer", + "channel_id": "channel-0" + } + }, + { + "chain_id": "localosmosis-1", + "channel": { + "state": "STATE_OPEN", + "ordering": "ORDER_UNORDERED", + "counterparty": { + "port_id": "transfer", + "channel_id": "channel-0" + }, + "connection_hops": [ + "connection-0" + ], + "version": "ics20-1", + "port_id": "transfer", + "channel_id": "channel-0" + } + } + ] +} \ No newline at end of file diff --git a/examples/contracts/astroport_factory.wasm b/examples/contracts/astroport_factory.wasm new file mode 100644 index 0000000..c318fd5 Binary files /dev/null and b/examples/contracts/astroport_factory.wasm differ diff --git a/examples/contracts/astroport_native_coin_registry.wasm b/examples/contracts/astroport_native_coin_registry.wasm new file mode 100644 index 0000000..7b7e8d9 Binary files /dev/null and b/examples/contracts/astroport_native_coin_registry.wasm differ diff --git a/examples/contracts/astroport_pair.wasm b/examples/contracts/astroport_pair.wasm new file mode 100644 index 0000000..dc57a44 Binary files /dev/null and b/examples/contracts/astroport_pair.wasm differ diff --git a/examples/contracts/astroport_pair_stable.wasm b/examples/contracts/astroport_pair_stable.wasm new file mode 100644 index 0000000..83099b1 Binary files /dev/null and b/examples/contracts/astroport_pair_stable.wasm differ diff --git a/examples/contracts/astroport_whitelist.wasm b/examples/contracts/astroport_whitelist.wasm new file mode 100644 index 0000000..01d4a57 Binary files /dev/null and b/examples/contracts/astroport_whitelist.wasm differ diff --git a/examples/contracts/auction.wasm b/examples/contracts/auction.wasm new file mode 100644 index 0000000..c6b59bf Binary files /dev/null and b/examples/contracts/auction.wasm differ diff --git a/examples/contracts/auctions_manager.wasm b/examples/contracts/auctions_manager.wasm new file mode 100644 index 0000000..2837aa7 Binary files /dev/null and b/examples/contracts/auctions_manager.wasm differ diff --git a/examples/contracts/cw20_base.wasm b/examples/contracts/cw20_base.wasm new file mode 100644 index 0000000..95d9f2a Binary files /dev/null and b/examples/contracts/cw20_base.wasm differ diff --git a/examples/contracts/price_oracle.wasm b/examples/contracts/price_oracle.wasm new file mode 100644 index 0000000..fe14ead Binary files /dev/null and b/examples/contracts/price_oracle.wasm differ diff --git a/examples/neutron.rs b/examples/neutron.rs new file mode 100644 index 0000000..a494104 --- /dev/null +++ b/examples/neutron.rs @@ -0,0 +1,128 @@ +use cosmwasm_std::Decimal; +use localic_utils::{types::contract::MinAmount, ConfigChainBuilder, TestContextBuilder}; +use std::error::Error; + +const ACC_0_ADDR: &str = "neutron1hj5fveer5cjtn4wd6wstzugjfdxzl0xpznmsky"; + +/// Demonstrates using localic-utils for neutron. +fn main() -> Result<(), Box> { + env_logger::init(); + + // Create a testcontext + let mut ctx = TestContextBuilder::default() + .with_unwrap_raw_logs(true) + .with_api_url("http://localhost:42069/") + .with_artifacts_dir("contracts") + .with_chain(ConfigChainBuilder::default_neutron().build()?) + .build()?; + + // Upload contracts + ctx.build_tx_upload_contracts().send()?; + + // Create a token in the tokenfactory + ctx.build_tx_create_tokenfactory_token() + .with_subdenom("bruhtoken") + .send()?; + ctx.build_tx_create_tokenfactory_token() + .with_subdenom("amoguscoin") + .send()?; + + let bruhtoken = ctx.get_tokenfactory_denom(ACC_0_ADDR, "bruhtoken"); + let amoguscoin = ctx.get_tokenfactory_denom(ACC_0_ADDR, "amoguscoin"); + + // Deploy valence auctions + ctx.build_tx_create_auctions_manager() + .with_min_auction_amount(&[( + &String::from("untrn"), + MinAmount { + send: "0".into(), + start_auction: "0".into(), + }, + )]) + .with_server_addr(ACC_0_ADDR) + .send()?; + ctx.build_tx_create_price_oracle().send()?; + ctx.build_tx_manual_oracle_price_update() + .with_offer_asset("untrn") + .with_ask_asset(amoguscoin.as_str()) + .with_price(Decimal::percent(10)) + .send()?; + ctx.build_tx_update_auction_oracle().send()?; + + ctx.build_tx_mint_tokenfactory_token() + .with_denom(bruhtoken.as_str()) + .with_amount(10000000000) + .send()?; + ctx.build_tx_mint_tokenfactory_token() + .with_denom(amoguscoin.as_str()) + .with_amount(10000000000) + .send()?; + + ctx.build_tx_create_auction() + .with_offer_asset("untrn") + .with_ask_asset(bruhtoken.as_str()) + .with_amount_offer_asset(10000) + .send()?; + ctx.build_tx_create_auction() + .with_offer_asset("untrn") + .with_ask_asset(amoguscoin.as_str()) + .with_amount_offer_asset(10000) + .send()?; + + ctx.get_auction(("untrn", ctx.get_tokenfactory_denom(ACC_0_ADDR, "bruhtoken")))?; + ctx.get_auction(( + "untrn", + ctx.get_tokenfactory_denom(ACC_0_ADDR, "amoguscoin"), + ))?; + + ctx.build_tx_create_token_registry() + .with_owner(ACC_0_ADDR) + .send()?; + ctx.build_tx_create_factory() + .with_owner(ACC_0_ADDR) + .send()?; + ctx.build_tx_create_pool() + .with_denom_a("untrn") + .with_denom_b(amoguscoin.clone()) + .send()?; + ctx.build_tx_create_pool() + .with_denom_a("untrn") + .with_denom_b(bruhtoken) + .send()?; + + let pool = ctx.get_astroport_pool( + "untrn", + ctx.get_tokenfactory_denom(ACC_0_ADDR, "amoguscoin"), + )?; + + assert!(pool + .query_value(&serde_json::json!({ + "pair": {} + })) + .get("data") + .and_then(|data| data.get("asset_infos")) + .is_some()); + + ctx.build_tx_fund_auction() + .with_offer_asset("untrn") + .with_ask_asset(amoguscoin.as_str()) + .with_amount_offer_asset(10000) + .send()?; + + ctx.build_tx_start_auction() + .with_offer_asset("untrn") + .with_ask_asset(amoguscoin.as_str()) + .with_end_block_delta(1000000) + .send()?; + + ctx.build_tx_fund_pool() + .with_denom_a("untrn") + .with_denom_b(amoguscoin) + .with_amount_denom_a(10000) + .with_amount_denom_b(10000) + .with_slippage_tolerance(Decimal::percent(50)) + .with_liq_token_receiver(ACC_0_ADDR) + .send()?; + + Ok(()) +} diff --git a/examples/neutron_osmosis.rs b/examples/neutron_osmosis.rs new file mode 100644 index 0000000..81855cf --- /dev/null +++ b/examples/neutron_osmosis.rs @@ -0,0 +1,74 @@ +use localic_utils::{error::Error as LocalIcUtilsError, ConfigChainBuilder, TestContextBuilder}; +use std::error::Error; + +const ACC_0_ADDR: &str = "osmo1hj5fveer5cjtn4wd6wstzugjfdxzl0xpwhpz63"; +const NEUTRON_ACC_0_ADDR: &str = "neutron1hj5fveer5cjtn4wd6wstzugjfdxzl0xpznmsky"; + +/// Demonstrates using localic-utils for neutron + osmosis. +fn main() -> Result<(), Box> { + env_logger::init(); + + // Create a testcontext + let mut ctx = TestContextBuilder::default() + .with_unwrap_raw_logs(true) + .with_api_url("http://localhost:42069/") + .with_artifacts_dir("contracts") + .with_chain(ConfigChainBuilder::default_neutron().build()?) + .with_chain(ConfigChainBuilder::default_osmosis().build()?) + .with_transfer_channel("osmosis", "neutron") + .with_transfer_channel("neutron", "osmosis") + .build()?; + + ctx.build_tx_create_tokenfactory_token() + .with_chain_name("neutron") + .with_subdenom("bruhtoken") + .send()?; + let bruhtoken = ctx.get_tokenfactory_denom(NEUTRON_ACC_0_ADDR, "bruhtoken"); + ctx.build_tx_mint_tokenfactory_token() + .with_chain_name("neutron") + .with_amount(10000000000000000000) + .with_denom(&bruhtoken) + .send()?; + + // Transfer from osmosis to neutron and neutron to osmosis + ctx.build_tx_transfer() + .with_chain_name("neutron") + .with_recipient(ACC_0_ADDR) + .with_denom("untrn") + .with_amount(1000000) + .send()?; + ctx.build_tx_transfer() + .with_chain_name("neutron") + .with_recipient(ACC_0_ADDR) + .with_denom(&bruhtoken) + .with_amount(1000000) + .send()?; + + let ibc_bruhtoken = ctx.get_ibc_denom(&bruhtoken, "neutron", "osmosis").ok_or( + LocalIcUtilsError::MissingContextVariable(format!("ibc_denom::{}", &bruhtoken)), + )?; + let ibc_neutron = ctx.get_ibc_denom("untrn", "neutron", "osmosis").ok_or( + LocalIcUtilsError::MissingContextVariable(format!("ibc_denom::{}", "untrn")), + )?; + + // Create an osmosis pool + ctx.build_tx_create_osmo_pool() + .with_weight(&ibc_neutron, 1) + .with_weight(&ibc_bruhtoken, 1) + .with_initial_deposit(&ibc_neutron, 1) + .with_initial_deposit(&ibc_bruhtoken, 1) + .send()?; + + // Get its id + let pool_id = ctx.get_osmo_pool(&ibc_neutron, &ibc_bruhtoken)?; + + // Fund the pool + ctx.build_tx_fund_osmo_pool() + .with_pool_id(pool_id) + .with_max_amount_in(&ibc_neutron, 10000) + .with_max_amount_in(&ibc_bruhtoken, 10000) + .with_share_amount_out(1000000000000) + .send()?; + + Ok(()) +} diff --git a/examples/osmosis.rs b/examples/osmosis.rs new file mode 100644 index 0000000..edfe8cd --- /dev/null +++ b/examples/osmosis.rs @@ -0,0 +1,52 @@ +use localic_utils::{ConfigChainBuilder, TestContextBuilder, OSMOSIS_CHAIN_NAME}; +use std::error::Error; + +const ACC_0_ADDR: &str = "osmo1hj5fveer5cjtn4wd6wstzugjfdxzl0xpwhpz63"; + +/// Demonstrates using localic-utils for neutron. +fn main() -> Result<(), Box> { + env_logger::init(); + + // Create a testcontext + let mut ctx = TestContextBuilder::default() + .with_unwrap_raw_logs(true) + .with_api_url("http://localhost:42069/") + .with_artifacts_dir("contracts") + .with_chain(ConfigChainBuilder::default_neutron().build()?) + .with_chain(ConfigChainBuilder::default_osmosis().build()?) + .build()?; + + // Create some tokens on osmosis + ctx.build_tx_create_tokenfactory_token() + .with_chain_name(OSMOSIS_CHAIN_NAME) + .with_subdenom("bruhtoken") + .send()?; + let bruhtoken = ctx.get_tokenfactory_denom(ACC_0_ADDR, "bruhtoken"); + ctx.build_tx_mint_tokenfactory_token() + .with_chain_name(OSMOSIS_CHAIN_NAME) + .with_amount(10000000000000000000) + .with_denom(&bruhtoken) + .with_recipient_addr(ACC_0_ADDR) + .send()?; + + // Create an osmosis pool + ctx.build_tx_create_osmo_pool() + .with_weight("uosmo", 1) + .with_weight(&bruhtoken, 1) + .with_initial_deposit("uosmo", 1) + .with_initial_deposit(&bruhtoken, 1) + .send()?; + + // Get its id + let pool_id = ctx.get_osmo_pool("uosmo", &bruhtoken)?; + + // Fund the pool + ctx.build_tx_fund_osmo_pool() + .with_pool_id(pool_id) + .with_max_amount_in("uosmo", 10000) + .with_max_amount_in(&bruhtoken, 10000) + .with_share_amount_out(1000000000000) + .send()?; + + Ok(()) +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..54bdd54 --- /dev/null +++ b/flake.lock @@ -0,0 +1,82 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1719506693, + "narHash": "sha256-C8e9S7RzshSdHB7L+v9I51af1gDM5unhJ2xO1ywxNH8=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "b2852eb9365c6de48ffb0dc2c9562591f652242a", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1719627476, + "narHash": "sha256-LBfULF+2sCaWmkjmj1LkkGrAS/E9ZdXU1A5wWKjt9p0=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "5be53be9e5c766fc72fc5d65ba8a566cc0c3217f", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..185a1a4 --- /dev/null +++ b/flake.nix @@ -0,0 +1,26 @@ +{ + description = "A flake providing a reproducible environment for localic-utils"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, flake-utils, rust-overlay }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; overlays = [ rust-overlay.overlays.default ]; }; + in { + devShells.default = pkgs.mkShell { + nativeBuildInputs = with pkgs.buildPackages; [ + rust-bin.stable.latest.default + ]; + + buildInputs = with pkgs; [ openssl libiconv pkg-config ]; + }; + }); +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..c088766 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,30 @@ +use localic_std::errors::LocalError; +use reqwest::Error as ReqwestError; +use serde_json::Error as SerdeJsonError; +use std::io::Error as IoError; +use thiserror::Error; + +/// General error during testing. +#[derive(Error, Debug)] +pub enum Error { + #[error("local interchain failure: `{0}`")] + LocalInterchain(#[from] LocalError), + #[error("IO failure: `{0}`")] + Io(#[from] IoError), + #[error("serialization failed: `{0}`")] + Serialization(#[from] SerdeJsonError), + #[error("failed to query container with cmd `{0}`")] + ContainerCmd(String), + #[error("an unknown error occurred: `{0}`")] + Misc(String), + #[error("test context missing variable `{0}`")] + MissingContextVariable(String), + #[error("the builder is missing a parameter `{0}`")] + MissingBuilderParam(String), + #[error("the transaction {hash:?} failed: {error:?}")] + TxFailed { hash: String, error: String }, + #[error("the transaction has no logs")] + TxMissingLogs, + #[error("the HTTP client encountered an error: `{0}`")] + HttpError(#[from] ReqwestError), +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..45e6af7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,59 @@ +pub mod error; +pub mod types; +pub mod utils; + +/// A builder for the testing environment harness. +pub use utils::test_context::TestContextBuilder; + +/// A builder for localic chain configs. +pub use types::config::ConfigChainBuilder; + +/// The IBC port name for ibc transfers. +pub const TRANSFER_PORT: &str = "transfer"; + +/// File extension for WASM files +pub const WASM_EXTENSION: &str = "wasm"; + +/// Neutron chain info +pub const NEUTRON_CHAIN_NAME: &str = "neutron"; +pub const NEUTRON_CHAIN_PREFIX: &str = "neutron"; +pub const NEUTRON_CHAIN_DENOM: &str = "untrn"; +pub const NEUTRON_CHAIN_ADMIN_ADDR: &str = "neutron1hj5fveer5cjtn4wd6wstzugjfdxzl0xpznmsky"; + +/// Osmosis chain info +pub const OSMOSIS_CHAIN_ID: &str = "localosmosis-1"; +pub const OSMOSIS_CHAIN_DENOM: &str = "uosmo"; +pub const OSMOSIS_CHAIN_PREFIX: &str = "osmo"; +pub const OSMOSIS_CHAIN_ADMIN_ADDR: &str = "osmo1hj5fveer5cjtn4wd6wstzugjfdxzl0xpwhpz63"; +pub const OSMOSIS_CHAIN_NAME: &str = "osmosis"; +pub const OSMOSIS_POOLFILE_PATH: &str = "/tmp/pool_file.json"; + +/// Stride chain info +pub const STRIDE_CHAIN_ID: &str = "localstride-1"; +pub const STRIDE_CHAIN_DENOM: &str = "ustrd"; +pub const STRIDE_CHAIN_PREFIX: &str = "stride"; +pub const STRIDE_CHAIN_ADMIN_ADDR: &str = "stride1u20df3trc2c2zdhm8qvh2hdjx9ewh00sv6eyy8"; +pub const STRIDE_CHAIN_NAME: &str = "stride"; + +/// File names +pub const AUCTION_CONTRACT_NAME: &str = "auction"; +pub const AUCTIONS_MANAGER_CONTRACT_NAME: &str = "auctions_manager"; +pub const TOKEN_REGISTRY_NAME: &str = "astroport_native_coin_registry"; +pub const FACTORY_NAME: &str = "astroport_factory"; +pub const PAIR_NAME: &str = "astroport_pair"; +pub const STABLE_PAIR_NAME: &str = "astroport_pair_stable"; +pub const TOKEN_NAME: &str = "cw20_base"; +pub const WHITELIST_NAME: &str = "astroport_whitelist"; +pub const PRICE_ORACLE_NAME: &str = "price_oracle"; + +/// Local ic info +pub const LOCAL_IC_API_URL: &str = "http://localhost:42069/"; + +/// Builder defautls +pub const DEFAULT_KEY: &str = "acc0"; +pub const DEFAULT_TRANSFER_PORT: &str = "transfer"; +pub const DEFAULT_AUCTION_LABEL: &str = "auction"; +pub const DEFAULT_NEUTRON_CHAIN_ID: &str = "localneutron-1"; + +pub const TX_HASH_QUERY_RETRIES: u16 = 5; +pub const TX_HASH_QUERY_PAUSE_SEC: u64 = 2; diff --git a/src/types/config.rs b/src/types/config.rs new file mode 100644 index 0000000..2e14e1f --- /dev/null +++ b/src/types/config.rs @@ -0,0 +1,66 @@ +use super::super::{ + DEFAULT_NEUTRON_CHAIN_ID, NEUTRON_CHAIN_ADMIN_ADDR, NEUTRON_CHAIN_DENOM, NEUTRON_CHAIN_NAME, + NEUTRON_CHAIN_PREFIX, OSMOSIS_CHAIN_ADMIN_ADDR, OSMOSIS_CHAIN_DENOM, OSMOSIS_CHAIN_ID, + OSMOSIS_CHAIN_NAME, OSMOSIS_CHAIN_PREFIX, STRIDE_CHAIN_ADMIN_ADDR, STRIDE_CHAIN_DENOM, + STRIDE_CHAIN_ID, STRIDE_CHAIN_NAME, STRIDE_CHAIN_PREFIX, +}; +use derive_builder::Builder; +use serde::Deserialize; + +#[derive(Deserialize, Default, Builder, Debug)] +#[builder(setter(into, prefix = "with"))] +pub struct ChainsVec { + pub chains: Vec, +} + +impl Into> for ChainsVec { + fn into(self) -> Vec { + self.chains + } +} + +#[derive(Clone, Deserialize, Default, Builder, Debug)] +#[builder(setter(into, prefix = "with"))] +pub struct ConfigChain { + pub denom: String, + pub debugging: bool, + pub chain_id: String, + pub chain_name: String, + pub chain_prefix: String, + pub admin_addr: String, +} + +impl ConfigChainBuilder { + pub fn default_neutron() -> Self { + Self { + denom: Some(String::from(NEUTRON_CHAIN_DENOM)), + debugging: Some(true), + chain_id: Some(String::from(DEFAULT_NEUTRON_CHAIN_ID)), + chain_name: Some(String::from(NEUTRON_CHAIN_NAME)), + chain_prefix: Some(String::from(NEUTRON_CHAIN_PREFIX)), + admin_addr: Some(String::from(NEUTRON_CHAIN_ADMIN_ADDR)), + } + } + + pub fn default_osmosis() -> Self { + Self { + denom: Some(String::from(OSMOSIS_CHAIN_DENOM)), + debugging: Some(true), + chain_id: Some(String::from(OSMOSIS_CHAIN_ID)), + chain_name: Some(String::from(OSMOSIS_CHAIN_NAME)), + chain_prefix: Some(String::from(OSMOSIS_CHAIN_PREFIX)), + admin_addr: Some(String::from(OSMOSIS_CHAIN_ADMIN_ADDR)), + } + } + + pub fn default_stride() -> Self { + Self { + denom: Some(String::from(STRIDE_CHAIN_DENOM)), + debugging: Some(true), + chain_id: Some(String::from(STRIDE_CHAIN_ID)), + chain_name: Some(String::from(STRIDE_CHAIN_NAME)), + chain_prefix: Some(String::from(STRIDE_CHAIN_PREFIX)), + admin_addr: Some(String::from(STRIDE_CHAIN_ADMIN_ADDR)), + } + } +} diff --git a/src/types/contract.rs b/src/types/contract.rs new file mode 100644 index 0000000..6a07237 --- /dev/null +++ b/src/types/contract.rs @@ -0,0 +1,38 @@ +use serde::Serialize; +use std::path::PathBuf; + +/// A deployed CosmWasm contract with a code id, address, and artifact path. +#[derive(Debug, Clone)] +pub struct DeployedContractInfo { + pub code_id: u64, + pub address: String, + pub artifact_path: PathBuf, +} + +/* + Valence contract bindings +*/ + +#[derive(Serialize, Clone)] +pub struct AuctionStrategy { + pub start_price_perc: u64, + pub end_price_perc: u64, +} + +#[derive(Serialize, Clone)] +pub struct PriceFreshnessStrategy { + pub limit: String, + pub multipliers: Vec<(String, String)>, +} + +#[derive(Serialize, Clone)] +pub struct ChainHaltConfig { + pub cap: String, + pub block_avg: String, +} + +#[derive(Serialize, Clone)] +pub struct MinAmount { + pub send: String, + pub start_auction: String, +} diff --git a/src/types/ibc.rs b/src/types/ibc.rs new file mode 100644 index 0000000..dca0c25 --- /dev/null +++ b/src/types/ibc.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +pub struct Trace { + pub channel_id: String, + pub port_id: String, + pub base_denom: String, + pub dest_denom: String, +} + +#[derive(Deserialize)] +pub struct Channel { + pub channel_id: String, + pub connection_hops: Vec, + pub counterparty: Counterparty, + pub ordering: String, + pub port_id: String, + pub state: String, + pub version: String, +} + +#[derive(Deserialize, Debug)] +pub struct Counterparty { + pub channel_id: String, + pub port_id: String, +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..8ef094a --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,7 @@ +/// Docker configuration for local-interchain. +pub mod config; + +/// Types for interfacing with cosmwasm contracts. +pub mod contract; + +pub mod ibc; diff --git a/src/utils/fixtures.rs b/src/utils/fixtures.rs new file mode 100644 index 0000000..ce10294 --- /dev/null +++ b/src/utils/fixtures.rs @@ -0,0 +1,354 @@ +use super::{ + super::{ + error::Error, types::ibc::Trace, AUCTION_CONTRACT_NAME, FACTORY_NAME, NEUTRON_CHAIN_NAME, + OSMOSIS_CHAIN_NAME, PAIR_NAME, PRICE_ORACLE_NAME, STABLE_PAIR_NAME, + TX_HASH_QUERY_PAUSE_SEC, TX_HASH_QUERY_RETRIES, + }, + test_context::TestContext, +}; +use localic_std::modules::cosmwasm::CosmWasm; +use serde_json::Value; +use std::{path::PathBuf, thread, time::Duration}; + +impl TestContext { + /// Gets the event log of a transaction as a JSON object, + /// or returns an error if it does not exist. + pub fn guard_tx_errors(&self, chain_name: &str, hash: &str) -> Result<(), Error> { + if !self.unwrap_logs { + return Ok(()); + } + + let chain = self.get_chain(chain_name); + let mut logs = None; + + for _ in 0..TX_HASH_QUERY_RETRIES { + thread::sleep(Duration::from_secs(TX_HASH_QUERY_PAUSE_SEC)); + + let mut tx_res = chain.rb.query_tx_hash(hash); + + if tx_res.get("raw_log").is_none() { + continue; + } + + logs = Some(tx_res["raw_log"].take()); + + break; + } + + let raw_log = logs.as_ref().and_then(|raw_log| raw_log.as_str()).unwrap(); + + if &raw_log == &"" { + return Ok(()); + } + + let logs = serde_json::from_str::(raw_log).map_err(|_| Error::TxFailed { + hash: hash.to_owned(), + error: raw_log.to_owned(), + })?; + + if let Some(err) = logs.as_str() { + return Err(Error::TxFailed { + hash: hash.to_owned(), + error: err.to_owned(), + }); + } + + Ok(()) + } + + /// Get a new CosmWasm instance for a contract identified by a name. + pub fn get_contract(&self, name: &str) -> Result { + let chain = self.get_chain(NEUTRON_CHAIN_NAME); + + let code_id = chain + .contract_codes + .get(name) + .ok_or(Error::Misc(format!("contract '{name}' is missing")))?; + + let artifacts_path = &self.artifacts_dir; + + Ok(CosmWasm::new_from_existing( + &chain.rb, + Some(PathBuf::from(format!("{artifacts_path}/{name}.wasm"))), + Some(*code_id), + None, + )) + } + + /// Get a new CosmWasm instance for the existing deployed auctions manager. + pub fn get_auctions_manager(&self) -> Result { + let neutron = self.get_chain(NEUTRON_CHAIN_NAME); + + let contract_info = self + .auctions_manager + .as_ref() + .ok_or(Error::MissingContextVariable(String::from( + "auctions_manager", + )))?; + + Ok(CosmWasm::new_from_existing( + &neutron.rb, + Some(contract_info.artifact_path.clone()), + Some(contract_info.code_id.clone()), + Some(contract_info.address.clone()), + )) + } + + /// Get a new CosmWasm instance for the existing deployed auctions manager. + pub fn get_price_oracle(&self) -> Result { + let neutron = self.get_chain(NEUTRON_CHAIN_NAME); + + let mut contract = self.get_contract(PRICE_ORACLE_NAME)?; + let contract_addr = neutron + .contract_addrs + .get(PRICE_ORACLE_NAME) + .and_then(|addrs| addrs.get(0)) + .cloned() + .ok_or(Error::MissingContextVariable(String::from( + "contract_addrs::price_oracle", + )))?; + contract.contract_addr = Some(contract_addr); + + Ok(contract) + } + + /// Gets a CosmWasm instance for an auction with a given pair. + pub fn get_auction, TDenomB: AsRef>( + &self, + denoms: (TDenomA, TDenomB), + ) -> Result { + let mut auction_contract = self.get_contract(AUCTION_CONTRACT_NAME)?; + + // The auctions manager for this deployment + let contract_a = self.get_auctions_manager()?; + + // Get the address of the auction specified + let resp = contract_a.query_value(&serde_json::json!({ + "get_pair_addr": { + "pair": (denoms.0.as_ref(), denoms.1.as_ref()) + } + })); + + auction_contract.contract_addr = Some( + resp.get("data") + .and_then(|json| json.as_str()) + .ok_or(Error::Misc(format!("tx failed with resp: {:?}", resp)))? + .to_owned(), + ); + + Ok(auction_contract) + } + + pub fn get_tokenfactory_denom(&self, creator_addr: &str, subdenom: &str) -> String { + format!("factory/{creator_addr}/{subdenom}") + } + + /// Gets the deployed atroport factory for Neutron. + pub fn get_astroport_factory(&self) -> Result, Error> { + let neutron = self.get_chain(NEUTRON_CHAIN_NAME); + + let code_id = + neutron + .contract_codes + .get(FACTORY_NAME) + .ok_or(Error::MissingContextVariable(format!( + "contract_codes::{FACTORY_NAME}", + )))?; + let contract_addrs = + neutron + .contract_addrs + .get(FACTORY_NAME) + .ok_or(Error::MissingContextVariable(format!( + "contract_addrs::{FACTORY_NAME}", + )))?; + + let artifacts_path = self.artifacts_dir.as_str(); + + Ok(contract_addrs + .into_iter() + .map(|addr| { + CosmWasm::new_from_existing( + &neutron.rb, + Some(PathBuf::from(format!( + "{artifacts_path}/{FACTORY_NAME}.wasm" + ))), + Some(*code_id), + Some(addr.clone()), + ) + }) + .collect::>()) + } + + /// Gets a previously deployed astroport pair. + pub fn get_astroport_pool( + &self, + denom_a: impl AsRef, + denom_b: impl AsRef, + ) -> Result { + let factories = self.get_astroport_factory()?; + let factory = factories + .get(0) + .ok_or(Error::MissingContextVariable(String::from(FACTORY_NAME)))?; + + let pair_info = factory.query_value(&serde_json::json!( + { + "pair": { + "asset_infos": [ + { + "native_token": { + "denom": denom_a.as_ref(), + } + }, + { + "native_token": { + "denom": denom_b.as_ref(), + } + } + ] + } + } + )); + + let addr = pair_info + .get("data") + .and_then(|data| data.get("contract_addr")) + .and_then(|addr| addr.as_str()) + .unwrap(); + let kind = pair_info + .get("data") + .and_then(|data| data.get("pair_type")) + .unwrap(); + + let neutron = self.get_chain(NEUTRON_CHAIN_NAME); + + if kind.get("xyk").is_some() { + let contract = self.get_contract(PAIR_NAME)?; + + return Ok(CosmWasm::new_from_existing( + &neutron.rb, + contract.file_path, + contract.code_id, + Some(addr.to_owned()), + )); + } + + let contract = self.get_contract(STABLE_PAIR_NAME)?; + + Ok(CosmWasm::new_from_existing( + &neutron.rb, + contract.file_path, + contract.code_id, + Some(addr.to_owned()), + )) + } + + /// Gets the id of the pool with the specifieed denoms. + pub fn get_osmo_pool( + &self, + denom_a: impl AsRef, + denom_b: impl AsRef, + ) -> Result { + let osmosis = self.get_chain(OSMOSIS_CHAIN_NAME); + let denom_a_str = denom_a.as_ref(); + + let res = osmosis.rb.query( + &format!("q poolmanager list-pools-by-denom {denom_a_str} --output=json"), + true, + ); + + let res_text = res.get("text").and_then(|v| v.as_str()).unwrap(); + let res_value: Value = serde_json::from_str(res_text)?; + + let pools_value = res_value.get("pools").unwrap(); + let pool = pools_value + .as_array() + .and_then(|pools| { + pools.iter().find(|pool_value| { + pool_value + .get("pool_assets") + .and_then(|assets_val| { + assets_val.as_array().and_then(|arr| { + arr.iter().find(|asset| { + asset["token"]["denom"].as_str() == Some(denom_b.as_ref()) + }) + }) + }) + .is_some() + }) + }) + .and_then(|pool| pool.get("id")) + .and_then(|id_str| id_str.as_str()) + .unwrap(); + + Ok(pool.parse().unwrap()) + } + + /// Gets the IBC denom for a base denom given a src and dest chain. + pub fn get_ibc_denom( + &mut self, + base_denom: impl AsRef, + src_chain: impl Into, + dest_chain: impl Into, + ) -> Option { + let src_chain_string = src_chain.into(); + let dest_chain_string = dest_chain.into(); + let base_denom_str = base_denom.as_ref(); + + if let Some(denom) = self + .ibc_denoms + .get(&(base_denom_str.into(), dest_chain_string.clone())) + { + return Some(denom.clone()); + } + + let dest_chain = self.get_chain(&dest_chain_string); + + let channel = self + .transfer_channel_ids + .get(&(dest_chain_string.clone(), src_chain_string.clone()))?; + let trace = format!("transfer/{}/{}", channel, base_denom_str); + + let resp = dest_chain + .rb + .q(&format!("q ibc-transfer denom-hash {trace}"), true); + + let ibc_denom = format!( + "ibc/{}", + serde_json::from_str::(&resp.get("text")?.as_str()?) + .ok()? + .get("hash")? + .as_str()? + ); + + self.ibc_denoms.insert( + (base_denom_str.into(), dest_chain_string.clone()), + ibc_denom.clone(), + ); + self.ibc_denoms + .insert((ibc_denom.clone(), src_chain_string), base_denom_str.into()); + + Some(ibc_denom) + } + + /// Gets the IBC channel and port for a given denom. + pub fn get_ibc_trace( + &mut self, + base_denom: impl Into + AsRef, + src_chain: impl Into + AsRef, + dest_chain: impl Into + AsRef, + ) -> Option { + let dest_denom = + self.get_ibc_denom(base_denom.as_ref(), src_chain.as_ref(), dest_chain.as_ref())?; + + let channel = self + .transfer_channel_ids + .get(&(src_chain.into(), dest_chain.into()))?; + + Some(Trace { + port_id: "transfer".to_owned(), + channel_id: channel.to_owned(), + base_denom: base_denom.into(), + dest_denom, + }) + } +} diff --git a/src/utils/fs.rs b/src/utils/fs.rs new file mode 100644 index 0000000..9d5d162 --- /dev/null +++ b/src/utils/fs.rs @@ -0,0 +1,65 @@ +use super::{ + super::{error::Error, DEFAULT_KEY, NEUTRON_CHAIN_NAME, WASM_EXTENSION}, + test_context::TestContext, +}; +use localic_std::modules::cosmwasm::CosmWasm; +use std::{ffi::OsStr, fs}; + +/// A tx uploading contract artifacts. +pub struct UploadContractsTxBuilder<'a> { + key: Option<&'a str>, + test_ctx: &'a mut TestContext, +} + +impl<'a> UploadContractsTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = Some(key); + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_upload_contracts( + self.key + .ok_or(Error::MissingBuilderParam(String::from("key")))?, + ) + } +} + +impl TestContext { + pub fn build_tx_upload_contracts(&mut self) -> UploadContractsTxBuilder { + UploadContractsTxBuilder { + key: Some(DEFAULT_KEY), + test_ctx: self, + } + } + + fn tx_upload_contracts(&mut self, key: &str) -> Result<(), Error> { + fs::read_dir(&self.artifacts_dir)? + .filter_map(|dir_ent| dir_ent.ok()) + .filter(|dir_ent| { + dir_ent.path().extension().and_then(OsStr::to_str) == Some(WASM_EXTENSION) + }) + .map(|ent| ent.path()) + .map(fs::canonicalize) + .try_for_each(|maybe_abs_path| { + let path = maybe_abs_path?; + let neutron_local_chain = self.get_mut_chain(NEUTRON_CHAIN_NAME); + + let mut cw = CosmWasm::new(&neutron_local_chain.rb); + + let code_id = cw.store(key, &path)?; + + let id = path + .file_stem() + .and_then(|stem| stem.to_str()) + .ok_or(Error::Misc(String::from("failed to format file path")))?; + neutron_local_chain + .contract_codes + .insert(id.to_string(), code_id); + + Ok(()) + }) + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..02786a1 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,4 @@ +mod fixtures; +pub mod fs; +pub mod setup; +pub mod test_context; diff --git a/src/utils/setup/astroport.rs b/src/utils/setup/astroport.rs new file mode 100644 index 0000000..1de8b89 --- /dev/null +++ b/src/utils/setup/astroport.rs @@ -0,0 +1,485 @@ +use super::super::{ + super::{ + error::Error, types::contract::DeployedContractInfo, DEFAULT_KEY, FACTORY_NAME, + NEUTRON_CHAIN_ADMIN_ADDR, NEUTRON_CHAIN_NAME, PAIR_NAME, STABLE_PAIR_NAME, TOKEN_NAME, + TOKEN_REGISTRY_NAME, WHITELIST_NAME, + }, + test_context::TestContext, +}; +use astroport::{ + asset::{Asset, AssetInfo}, + factory::{self, PairConfig, PairType}, + native_coin_registry, pair, +}; +use cosmwasm_std::Decimal; + +/// A tx creating a token registry. +pub struct CreateTokenRegistryTxBuilder<'a> { + key: Option<&'a str>, + owner: Option, + test_ctx: &'a mut TestContext, +} + +impl<'a> CreateTokenRegistryTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = Some(key); + + self + } + + pub fn with_owner(&mut self, owner: impl Into) -> &mut Self { + self.owner = Some(owner.into()); + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_create_token_registry( + self.key + .ok_or(Error::MissingBuilderParam(String::from("key")))?, + self.owner + .clone() + .ok_or(Error::MissingBuilderParam(String::from("owner")))?, + ) + } +} + +/// A tx creating a token registry. +pub struct CreatePoolTxBuilder<'a> { + key: &'a str, + pair_type: PairType, + denom_a: Option, + denom_b: Option, + test_ctx: &'a mut TestContext, +} + +impl<'a> CreatePoolTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + pub fn with_pairtype(&mut self, pairtype: PairType) -> &mut Self { + self.pair_type = pairtype; + + self + } + + pub fn with_denom_a(&mut self, denom_a: impl Into) -> &mut Self { + self.denom_a = Some(denom_a.into()); + + self + } + + pub fn with_denom_b(&mut self, denom_b: impl Into) -> &mut Self { + self.denom_b = Some(denom_b.into()); + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_create_pool( + self.key, + self.pair_type.clone(), + self.denom_a + .clone() + .ok_or(Error::MissingBuilderParam(String::from("denom_a")))?, + self.denom_b + .clone() + .ok_or(Error::MissingBuilderParam(String::from("denom_b")))?, + ) + } +} + +/// A tx creating an astroport factory. +pub struct CreateFactoryTxBuilder<'a> { + key: &'a str, + owner: String, + test_ctx: &'a mut TestContext, +} + +impl<'a> CreateFactoryTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + pub fn with_owner(&mut self, owner: impl Into) -> &mut Self { + self.owner = owner.into(); + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx + .tx_create_factory(self.key, self.owner.clone()) + } +} + +/// A tx funding an astroport pool. +pub struct FundPoolTxBuilder<'a> { + key: &'a str, + denom_a: Option, + denom_b: Option, + amt_denom_a: Option, + amt_denom_b: Option, + slippage_tolerance: Option, + liq_token_receiver: Option<&'a str>, + test_ctx: &'a mut TestContext, +} + +impl<'a> FundPoolTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + pub fn with_denom_a(&mut self, denom_a: impl Into) -> &mut Self { + self.denom_a = Some(denom_a.into()); + + self + } + + pub fn with_denom_b(&mut self, denom_b: impl Into) -> &mut Self { + self.denom_b = Some(denom_b.into()); + + self + } + + pub fn with_amount_denom_a(&mut self, amt_denom_a: u128) -> &mut Self { + self.amt_denom_a = Some(amt_denom_a); + + self + } + + pub fn with_amount_denom_b(&mut self, amt_denom_b: u128) -> &mut Self { + self.amt_denom_b = Some(amt_denom_b); + + self + } + + pub fn with_slippage_tolerance(&mut self, slippage_tolerance: Decimal) -> &mut Self { + self.slippage_tolerance = Some(slippage_tolerance); + + self + } + + pub fn with_liq_token_receiver(&mut self, receiver_addr: &'a str) -> &mut Self { + self.liq_token_receiver = Some(receiver_addr); + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_fund_pool( + self.key, + self.denom_a + .clone() + .ok_or(Error::MissingBuilderParam(String::from("denom_a")))?, + self.denom_b + .clone() + .ok_or(Error::MissingBuilderParam(String::from("denom_b")))?, + self.amt_denom_a + .ok_or(Error::MissingBuilderParam(String::from("amt_denom_a")))?, + self.amt_denom_b + .ok_or(Error::MissingBuilderParam(String::from("amt_denom_b")))?, + self.slippage_tolerance + .ok_or(Error::MissingBuilderParam(String::from( + "slippage_tolerance", + )))?, + self.liq_token_receiver + .ok_or(Error::MissingBuilderParam(String::from( + "liq_token_receiver", + )))?, + ) + } +} + +impl TestContext { + pub fn build_tx_create_token_registry(&mut self) -> CreateTokenRegistryTxBuilder { + CreateTokenRegistryTxBuilder { + key: Some(DEFAULT_KEY), + owner: Some(NEUTRON_CHAIN_ADMIN_ADDR.to_owned()), + test_ctx: self, + } + } + + /// Instantiates the token registry. + fn tx_create_token_registry( + &mut self, + key: &str, + owner_addr: impl Into, + ) -> Result<(), Error> { + let mut contract_a = self.get_contract(TOKEN_REGISTRY_NAME)?; + let code_id = contract_a + .code_id + .ok_or(Error::MissingContextVariable(String::from( + "astroport_token_registry::code_id", + )))?; + + let contract = contract_a.instantiate( + key, + serde_json::to_string(&native_coin_registry::InstantiateMsg { + owner: owner_addr.into(), + })? + .as_str(), + TOKEN_REGISTRY_NAME, + None, + "--gas 1000000", + )?; + let addr = contract.address; + let artifact_path = + contract_a + .file_path + .ok_or(Error::MissingContextVariable(String::from( + "astroport_token_registry::artifact_path", + )))?; + + let neutron = self.get_mut_chain(NEUTRON_CHAIN_NAME); + + neutron + .contract_addrs + .entry(TOKEN_REGISTRY_NAME.to_owned()) + .or_default() + .push(addr.clone()); + + self.astroport_token_registry = Some(DeployedContractInfo { + code_id, + address: addr, + artifact_path, + }); + + Ok(()) + } + + /// Instantiates the astroport factory. + pub fn build_tx_create_factory(&mut self) -> CreateFactoryTxBuilder { + CreateFactoryTxBuilder { + key: DEFAULT_KEY, + owner: NEUTRON_CHAIN_ADMIN_ADDR.to_owned(), + test_ctx: self, + } + } + + /// Instantiates the astroport factory. + fn tx_create_factory( + &mut self, + key: &str, + factory_owner: impl Into, + ) -> Result<(), Error> { + let neutron = self.get_chain(NEUTRON_CHAIN_NAME); + + let pair_xyk_code_id = + neutron + .contract_codes + .get(PAIR_NAME) + .ok_or(Error::MissingContextVariable(String::from( + "contract_codes::astroport_pair", + )))?; + let pair_stable_code_id = + neutron + .contract_codes + .get(STABLE_PAIR_NAME) + .ok_or(Error::MissingContextVariable(String::from( + "contract_codes::astroport_pair_stable", + )))?; + let token_code_id = + neutron + .contract_codes + .get(TOKEN_NAME) + .ok_or(Error::MissingContextVariable(String::from( + "contract_codes::cw20_base", + )))?; + let whitelist_code_id = + neutron + .contract_codes + .get(WHITELIST_NAME) + .ok_or(Error::MissingContextVariable(String::from( + "contract_codes::astroport_whitelist", + )))?; + + let native_registry_addr = neutron + .contract_addrs + .get(TOKEN_REGISTRY_NAME) + .and_then(|maybe_addr| maybe_addr.get(0)) + .ok_or(Error::MissingContextVariable(String::from( + "contract_ddrs::astroport_native_coin_registry", + )))?; + + let mut contract_a = self.get_contract(FACTORY_NAME)?; + + let contract = contract_a.instantiate( + key, + serde_json::to_string(&factory::InstantiateMsg { + pair_configs: vec![ + PairConfig { + code_id: *pair_xyk_code_id, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }, + PairConfig { + code_id: *pair_stable_code_id, + pair_type: PairType::Stable {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + permissioned: false, + }, + ], + token_code_id: *token_code_id, + owner: factory_owner.into(), + whitelist_code_id: *whitelist_code_id, + coin_registry_address: native_registry_addr.clone(), + fee_address: None, + generator_address: None, + tracker_config: None, + })? + .as_str(), + FACTORY_NAME, + None, + "", + )?; + + let neutron = self.get_mut_chain(NEUTRON_CHAIN_NAME); + + neutron + .contract_addrs + .entry(FACTORY_NAME.to_owned()) + .or_default() + .push(contract.address); + + Ok(()) + } + + /// Creates a pool with the specififed denoms. + pub fn build_tx_create_pool(&mut self) -> CreatePoolTxBuilder { + CreatePoolTxBuilder { + key: DEFAULT_KEY, + pair_type: PairType::Xyk {}, + denom_a: Default::default(), + denom_b: Default::default(), + test_ctx: self, + } + } + + /// Creates a pool with the specififed denoms. + fn tx_create_pool( + &self, + key: &str, + pair_type: PairType, + denom_a: impl Into, + denom_b: impl Into, + ) -> Result<(), Error> { + // Factory contract instance + let contracts = self.get_astroport_factory()?; + let contract_a = contracts + .get(0) + .ok_or(Error::MissingContextVariable(String::from(FACTORY_NAME)))?; + + // Create the pair + let tx = contract_a.execute( + key, + serde_json::to_string(&factory::ExecuteMsg::CreatePair { + pair_type, + asset_infos: vec![ + AssetInfo::NativeToken { + denom: denom_a.into(), + }, + AssetInfo::NativeToken { + denom: denom_b.into(), + }, + ], + init_params: None, + })? + .as_str(), + "--gas 1000000", + )?; + + // Get the address of the createed contract via logs + let tx_hash = tx.tx_hash.ok_or(Error::Misc(String::from( + "transaction did not produce a tx hash", + )))?; + + let _ = self.guard_tx_errors(NEUTRON_CHAIN_NAME, tx_hash.as_str())?; + + Ok(()) + } + + /// Provides liquidity for a specific astroport pool. + pub fn build_tx_fund_pool(&mut self) -> FundPoolTxBuilder { + FundPoolTxBuilder { + key: DEFAULT_KEY, + denom_a: Default::default(), + denom_b: Default::default(), + amt_denom_a: Default::default(), + amt_denom_b: Default::default(), + slippage_tolerance: Default::default(), + liq_token_receiver: Default::default(), + test_ctx: self, + } + } + + /// Provides liquidity for a specific astroport pool. + fn tx_fund_pool( + &mut self, + key: &str, + denom_a: impl Into + AsRef, + denom_b: impl Into + AsRef, + amt_denom_a: u128, + amt_denom_b: u128, + slippage_tolerance: Decimal, + liq_token_receiver: impl Into, + ) -> Result<(), Error> { + // Get the instance from the address + let pool = self.get_astroport_pool(denom_a.as_ref(), denom_b.as_ref())?; + + let denom_a = denom_a.into(); + let denom_b = denom_b.into(); + + // Provide liquidity + let tx = pool + .execute( + key, + serde_json::to_string(&pair::ExecuteMsg::ProvideLiquidity { + assets: vec![ + Asset { + info: AssetInfo::NativeToken { + denom: denom_a.clone(), + }, + amount: amt_denom_a.into(), + }, + Asset { + info: AssetInfo::NativeToken { + denom: denom_b.clone(), + }, + amount: amt_denom_b.into(), + }, + ], + slippage_tolerance: Some(slippage_tolerance), + auto_stake: None, + receiver: Some(liq_token_receiver.into()), + min_lp_to_receive: None, + })? + .as_str(), + &format!("--amount {amt_denom_a}{denom_a},{amt_denom_b}{denom_b} --gas 1000000"), + )? + .tx_hash + .ok_or(Error::TxMissingLogs)?; + + let _ = self.guard_tx_errors(NEUTRON_CHAIN_NAME, tx.as_str())?; + + Ok(()) + } +} diff --git a/src/utils/setup/ibc.rs b/src/utils/setup/ibc.rs new file mode 100644 index 0000000..d887543 --- /dev/null +++ b/src/utils/setup/ibc.rs @@ -0,0 +1,121 @@ +use super::super::{ + super::{error::Error, DEFAULT_KEY, DEFAULT_TRANSFER_PORT, NEUTRON_CHAIN_NAME}, + test_context::{LocalChain, TestContext}, +}; + +pub struct TransferTxBuilder<'a> { + key: &'a str, + src_chain_name: &'a str, + recipient: Option<&'a str>, + denom: Option<&'a str>, + amount: Option, + port: &'a str, + test_ctx: &'a mut TestContext, +} + +impl<'a> TransferTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + pub fn with_chain_name(&mut self, chain_name: &'a str) -> &mut Self { + self.src_chain_name = chain_name; + + self + } + + pub fn with_recipient(&mut self, recipient: &'a str) -> &mut Self { + self.recipient = Some(recipient); + + self + } + + pub fn with_denom(&mut self, denom: &'a str) -> &mut Self { + self.denom = Some(denom); + + self + } + + pub fn with_amount(&mut self, amount: u128) -> &mut Self { + self.amount = Some(amount); + + self + } + + pub fn with_port(&mut self, port: &'a str) -> &mut Self { + self.port = port; + + self + } + + /// Sends the built IBC transfer tx. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_transfer( + self.key, + self.src_chain_name, + self.recipient + .ok_or(Error::MissingBuilderParam(String::from("recipient")))?, + self.denom + .ok_or(Error::MissingBuilderParam(String::from("denom")))?, + self.amount + .ok_or(Error::MissingBuilderParam(String::from("amount")))?, + self.port, + ) + } +} + +impl TestContext { + /// Creates a builder building a transaction transfering funds over IBC. + pub fn build_tx_transfer(&mut self) -> TransferTxBuilder { + TransferTxBuilder { + key: DEFAULT_KEY, + src_chain_name: NEUTRON_CHAIN_NAME, + recipient: Default::default(), + denom: Default::default(), + amount: Default::default(), + port: DEFAULT_TRANSFER_PORT, + test_ctx: self, + } + } + + fn tx_transfer( + &mut self, + key: &str, + src_chain_name: &str, + recipient: &str, + denom: &str, + amount: u128, + port: &str, + ) -> Result<(), Error> { + let dest_chain: &LocalChain = self + .chains + .values() + .find(|chain| recipient.starts_with(&chain.chain_prefix)) + .ok_or(Error::MissingContextVariable(format!("chain::{recipient}")))?; + + let chain = self.get_chain(src_chain_name); + let fee_denom = &chain.native_denom; + + let channel = self + .transfer_channel_ids + .get(&(chain.chain_name.clone(), dest_chain.chain_name.clone())) + .ok_or(Error::MissingContextVariable(format!( + "channel_id::{}-{}", + chain.chain_name, dest_chain.chain_name + )))?; + + let receipt = chain.rb.tx(&format!("tx ibc-transfer transfer {port} {channel} {recipient} {amount}{denom} --fees=100000{fee_denom} --from={key}"), true)?; + + let _ = self.guard_tx_errors( + src_chain_name, + receipt + .get("txhash") + .and_then(|receipt| receipt.as_str()) + .ok_or(Error::TxMissingLogs)?, + )?; + + Ok(()) + } +} diff --git a/src/utils/setup/mod.rs b/src/utils/setup/mod.rs new file mode 100644 index 0000000..4f7ba16 --- /dev/null +++ b/src/utils/setup/mod.rs @@ -0,0 +1,5 @@ +pub mod astroport; +pub mod ibc; +pub mod osmosis; +pub mod tokens; +pub mod valence; diff --git a/src/utils/setup/osmosis.rs b/src/utils/setup/osmosis.rs new file mode 100644 index 0000000..0bd49d7 --- /dev/null +++ b/src/utils/setup/osmosis.rs @@ -0,0 +1,222 @@ +use super::super::{ + super::{error::Error, DEFAULT_KEY, OSMOSIS_CHAIN_NAME, OSMOSIS_POOLFILE_PATH}, + test_context::TestContext, +}; +use cosmwasm_std::Decimal; +use std::{fs::OpenOptions, io::Write, path::Path}; + +pub struct CreateOsmoPoolTxBuilder<'a> { + key: &'a str, + weights: Vec<(u64, &'a str)>, + initial_deposit: Vec<(u64, &'a str)>, + swap_fee: Decimal, + exit_fee: Decimal, + future_governor: &'a str, + test_ctx: &'a mut TestContext, +} + +impl<'a> CreateOsmoPoolTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + pub fn with_weight(&mut self, denom: &'a str, weight: u64) -> &mut Self { + self.weights.push((weight, denom)); + + self + } + + pub fn with_initial_deposit(&mut self, denom: &'a str, deposit: u64) -> &mut Self { + self.initial_deposit.push((deposit, denom)); + + self + } + + pub fn with_swap_fee(&mut self, swap_fee: Decimal) -> &mut Self { + self.swap_fee = swap_fee; + + self + } + + pub fn with_exit_fee(&mut self, exit_fee: Decimal) -> &mut Self { + self.exit_fee = exit_fee; + + self + } + + pub fn with_future_governor(&mut self, future_governor: &'a str) -> &mut Self { + self.future_governor = future_governor; + + self + } + + /// Sends the transaction, returning the pool ID if it was created successfully. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_create_osmo_pool( + self.key, + self.weights.iter().cloned(), + self.initial_deposit.iter().cloned(), + self.swap_fee, + self.exit_fee, + self.future_governor, + ) + } +} + +pub struct FundOsmoPoolTxBuilder<'a> { + key: &'a str, + pool_id: Option, + max_amounts_in: Vec<(u64, &'a str)>, + share_amount_out: Option, + test_ctx: &'a mut TestContext, +} + +impl<'a> FundOsmoPoolTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + pub fn with_pool_id(&mut self, pool_id: u64) -> &mut Self { + self.pool_id = Some(pool_id); + + self + } + + pub fn with_max_amount_in(&mut self, denom: &'a str, amount: u64) -> &mut Self { + self.max_amounts_in.push((amount, denom)); + + self + } + + pub fn with_share_amount_out(&mut self, share_amount_out: u64) -> &mut Self { + self.share_amount_out = Some(share_amount_out); + + self + } + + /// Sends the transaction, returning the pool ID if it was created successfully. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_fund_osmo_pool( + self.key, + self.pool_id + .ok_or(Error::MissingBuilderParam(String::from("pool_id")))?, + self.max_amounts_in.iter().cloned(), + self.share_amount_out + .ok_or(Error::MissingBuilderParam(String::from("share_amount_out")))?, + ) + } +} + +impl TestContext { + pub fn build_tx_create_osmo_pool(&mut self) -> CreateOsmoPoolTxBuilder { + CreateOsmoPoolTxBuilder { + key: DEFAULT_KEY, + weights: Default::default(), + initial_deposit: Default::default(), + swap_fee: Decimal::percent(0), + exit_fee: Decimal::percent(0), + future_governor: "128h", + test_ctx: self, + } + } + + /// Creates an osmosis pool with the given denoms. + fn tx_create_osmo_pool<'a>( + &mut self, + key: &str, + weights: impl Iterator, + initial_deposit: impl Iterator, + swap_fee: Decimal, + exit_fee: Decimal, + future_governor: &'a str, + ) -> Result<(), Error> { + let osmosis = self.get_chain(OSMOSIS_CHAIN_NAME); + + // Osmosisd requires a JSON file to specify the + // configuration of the pool being created + let poolfile_str = serde_json::json!({ + "weights": weights.map(|(weight, denom)| format!("{weight}{denom}")).collect::>().join(","), + "initial-deposit": initial_deposit.map(|(deposit, denom)| format!("{deposit}{denom}")).collect::>().join(","), + "swap-fee": swap_fee, + "exit-fee": exit_fee, + "future-governor": future_governor, + }) + .to_string(); + + // Copy poolfile to localosmo + let mut f = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(OSMOSIS_POOLFILE_PATH)?; + f.write_all(poolfile_str.as_bytes())?; + + let _ = osmosis + .rb + .upload_file(&Path::new(OSMOSIS_POOLFILE_PATH), true)? + .send()? + .text()?; + + let chain_id = &osmosis.rb.chain_id; + let remote_poolfile_path = format!("/var/cosmos-chain/{chain_id}/pool_file.json"); + + // Create pool + let receipt = osmosis.rb.tx( + format!("tx poolmanager create-pool --pool-file {remote_poolfile_path} --from {key} --fees 2500uosmo --gas 1000000") + .as_str(), + true, + )?; + + let _ = self.guard_tx_errors( + OSMOSIS_CHAIN_NAME, + receipt + .get("txhash") + .and_then(|receipt| receipt.as_str()) + .ok_or(Error::TxMissingLogs)?, + )?; + + Ok(()) + } + + pub fn build_tx_fund_osmo_pool(&mut self) -> FundOsmoPoolTxBuilder { + FundOsmoPoolTxBuilder { + key: DEFAULT_KEY, + pool_id: Default::default(), + max_amounts_in: Default::default(), + share_amount_out: Default::default(), + test_ctx: self, + } + } + + /// Creates an osmosis pool with the given denoms. + fn tx_fund_osmo_pool<'a>( + &mut self, + key: &str, + pool_id: u64, + max_amounts_in: impl Iterator, + share_amount_out: u64, + ) -> Result<(), Error> { + let osmosis = self.get_chain(OSMOSIS_CHAIN_NAME); + + // Enter LP + let receipt = osmosis.rb.tx( + format!("tx gamm join-pool --pool-id {pool_id} --max-amounts-in {} --share-amount-out {share_amount_out} --from {key} --fees 2500uosmo --gas 1000000", max_amounts_in.map(|(weight, denom)| format!("{weight}{denom}")).collect::>().join(",")) + .as_str(), + true, + )?; + + let _ = self.guard_tx_errors( + OSMOSIS_CHAIN_NAME, + receipt + .get("txhash") + .and_then(|receipt| receipt.as_str()) + .ok_or(Error::TxMissingLogs)?, + )?; + + Ok(()) + } +} diff --git a/src/utils/setup/tokens.rs b/src/utils/setup/tokens.rs new file mode 100644 index 0000000..1ba06c3 --- /dev/null +++ b/src/utils/setup/tokens.rs @@ -0,0 +1,199 @@ +use super::super::{ + super::{error::Error, DEFAULT_KEY, NEUTRON_CHAIN_NAME}, + test_context::TestContext, +}; + +/// A tx creating a tokenfactory token. +pub struct CreateTokenFactoryTokenTxBuilder<'a> { + key: Option<&'a str>, + chain_name: Option, + subdenom: Option<&'a str>, + test_ctx: &'a mut TestContext, +} + +impl<'a> CreateTokenFactoryTokenTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = Some(key); + + self + } + + pub fn with_chain_name(&mut self, chain_name: impl Into) -> &mut Self { + self.chain_name = Some(chain_name.into()); + + self + } + + pub fn with_subdenom(&mut self, subdenom: &'a str) -> &mut Self { + self.subdenom = Some(subdenom); + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_create_tokenfactory_token( + self.chain_name + .as_ref() + .ok_or(Error::MissingBuilderParam(String::from("chain_name")))?, + self.key + .ok_or(Error::MissingBuilderParam(String::from("key")))?, + self.subdenom + .ok_or(Error::MissingBuilderParam(String::from("subdenom")))?, + ) + } +} + +/// A tx minting a tokens from the token factory. +pub struct MintTokenFactoryTokenTxBuilder<'a> { + key: Option<&'a str>, + chain_name: Option, + denom: Option<&'a str>, + amount: Option, + recipient_addr: Option<&'a str>, + test_ctx: &'a mut TestContext, +} + +impl<'a> MintTokenFactoryTokenTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = Some(key); + + self + } + + pub fn with_chain_name(&mut self, chain_name: impl Into) -> &mut Self { + self.chain_name = Some(chain_name.into()); + + self + } + + pub fn with_denom(&mut self, denom: &'a str) -> &mut Self { + self.denom = Some(denom); + + self + } + + pub fn with_amount(&mut self, amount: u128) -> &mut Self { + self.amount = Some(amount); + + self + } + + pub fn with_recipient_addr(&mut self, addr: &'a str) -> &mut Self { + self.recipient_addr = Some(addr); + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_mint_tokenfactory_token( + self.chain_name + .as_ref() + .ok_or(Error::MissingBuilderParam(String::from("chain_name")))?, + self.key + .ok_or(Error::MissingBuilderParam(String::from("key")))?, + self.denom + .ok_or(Error::MissingBuilderParam(String::from("denom")))?, + self.amount + .ok_or(Error::MissingBuilderParam(String::from("amount")))?, + self.recipient_addr, + ) + } +} + +impl TestContext { + pub fn build_tx_create_tokenfactory_token(&mut self) -> CreateTokenFactoryTokenTxBuilder { + CreateTokenFactoryTokenTxBuilder { + key: Some(DEFAULT_KEY), + chain_name: Some(NEUTRON_CHAIN_NAME.to_owned()), + subdenom: Default::default(), + test_ctx: self, + } + } + + /// Creates a tokenfactory token with the given subdenom on the given chain. + pub fn tx_create_tokenfactory_token( + &mut self, + chain_name: &str, + key: &str, + subdenom: &str, + ) -> Result<(), Error> { + let chain = self.get_chain(chain_name); + let fee_denom = chain.native_denom.as_str(); + + let receipt = chain.rb.tx( + format!("tx tokenfactory create-denom {subdenom} --from {key} --fees 25000{fee_denom} --gas 10000000") + .as_str(), + true, + )?; + + let _ = self.guard_tx_errors( + chain_name, + receipt + .get("txhash") + .and_then(|receipt| receipt.as_str()) + .ok_or(Error::TxMissingLogs)?, + )?; + + Ok(()) + } + + /// Creates a builder for a tx minting a quantity of a tokenfactory token on the specified chain. + pub fn build_tx_mint_tokenfactory_token(&mut self) -> MintTokenFactoryTokenTxBuilder { + MintTokenFactoryTokenTxBuilder { + key: Some(DEFAULT_KEY), + chain_name: Some(NEUTRON_CHAIN_NAME.to_owned()), + denom: Default::default(), + amount: Default::default(), + recipient_addr: Default::default(), + test_ctx: self, + } + } + + fn tx_mint_tokenfactory_token( + &mut self, + chain_name: &str, + key: &str, + denom: &str, + amount: u128, + recipient: Option<&str>, + ) -> Result<(), Error> { + let chain = self.get_chain(chain_name); + let fee_denom = chain.native_denom.as_str(); + + if let Some(recipient) = recipient { + let receipt = chain.rb.tx( + format!("tx tokenfactory mint {amount}{denom} {recipient} --from {key} --fees 500{fee_denom}") + .as_str(), + true, + )?; + + let _ = self.guard_tx_errors( + chain_name, + receipt + .get("txhash") + .and_then(|receipt| receipt.as_str()) + .ok_or(Error::TxMissingLogs)?, + )?; + + return Ok(()); + } + + let receipt = chain.rb.tx( + format!("tx tokenfactory mint {amount}{denom} --from {key} --fees 500{fee_denom}") + .as_str(), + true, + )?; + + let _ = self.guard_tx_errors( + chain_name, + receipt + .get("txhash") + .and_then(|receipt| receipt.as_str()) + .ok_or(Error::TxMissingLogs)?, + )?; + + Ok(()) + } +} diff --git a/src/utils/setup/valence.rs b/src/utils/setup/valence.rs new file mode 100644 index 0000000..9534037 --- /dev/null +++ b/src/utils/setup/valence.rs @@ -0,0 +1,818 @@ +use super::super::{ + super::{ + error::Error, + types::contract::{ + AuctionStrategy, ChainHaltConfig, DeployedContractInfo, MinAmount, + PriceFreshnessStrategy, + }, + AUCTIONS_MANAGER_CONTRACT_NAME, AUCTION_CONTRACT_NAME, DEFAULT_AUCTION_LABEL, DEFAULT_KEY, + NEUTRON_CHAIN_ADMIN_ADDR, NEUTRON_CHAIN_NAME, PRICE_ORACLE_NAME, + }, + test_context::TestContext, +}; +use cosmwasm_std::Decimal; +use localic_std::modules::cosmwasm::CosmWasm; +use serde_json::Value; + +/// A tx creating an auctions manager. +pub struct CreateAuctionsManagerTxBuilder<'a> { + key: &'a str, + min_auction_amount: &'a [(&'a str, MinAmount)], + server_addr: &'a str, + test_ctx: &'a mut TestContext, +} + +impl<'a> CreateAuctionsManagerTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + pub fn with_min_auction_amount( + &mut self, + min_auction_amount: &'a [(&'a str, MinAmount)], + ) -> &mut Self { + self.min_auction_amount = min_auction_amount; + + self + } + + pub fn with_server_addr(&mut self, addr: &'a str) -> &mut Self { + self.server_addr = addr; + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_create_auctions_manager( + self.key, + self.min_auction_amount, + self.server_addr, + ) + } +} + +pub struct CreateAuctionTxBuilder<'a> { + key: &'a str, + offer_asset: Option<&'a str>, + ask_asset: Option<&'a str>, + auction_strategy: AuctionStrategy, + chain_halt_config: ChainHaltConfig, + price_freshness_strategy: PriceFreshnessStrategy, + label: &'a str, + amount_offer_asset: Option, + test_ctx: &'a mut TestContext, +} + +impl<'a> CreateAuctionTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + pub fn with_offer_asset(&mut self, asset: &'a str) -> &mut Self { + self.offer_asset = Some(asset); + + self + } + + pub fn with_ask_asset(&mut self, asset: &'a str) -> &mut Self { + self.ask_asset = Some(asset); + + self + } + + pub fn with_auction_strategy(&mut self, auction_strategy: AuctionStrategy) -> &mut Self { + self.auction_strategy = auction_strategy; + + self + } + + pub fn with_chain_halt_config(&mut self, chain_halt_config: ChainHaltConfig) -> &mut Self { + self.chain_halt_config = chain_halt_config; + + self + } + + pub fn with_price_freshness_strategy( + &mut self, + price_freshness_strategy: PriceFreshnessStrategy, + ) -> &mut Self { + self.price_freshness_strategy = price_freshness_strategy; + + self + } + + pub fn with_label(&mut self, label: &'a str) -> &mut Self { + self.label = label; + + self + } + + pub fn with_amount_offer_asset(&mut self, amount_offer_asset: u128) -> &mut Self { + self.amount_offer_asset = Some(amount_offer_asset); + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_create_auction( + self.key, + ( + self.offer_asset + .ok_or(Error::MissingBuilderParam(String::from("pair")))?, + self.ask_asset + .ok_or(Error::MissingBuilderParam(String::from("pair")))?, + ), + self.auction_strategy.clone(), + self.chain_halt_config.clone(), + self.price_freshness_strategy.clone(), + self.label, + self.amount_offer_asset + .ok_or(Error::MissingBuilderParam(String::from( + "amount_offer_asset", + )))?, + ) + } +} + +pub struct FundAuctionTxBuilder<'a> { + key: &'a str, + offer_asset: Option<&'a str>, + ask_asset: Option<&'a str>, + amt_offer_asset: Option, + test_ctx: &'a mut TestContext, +} + +impl<'a> FundAuctionTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + pub fn with_offer_asset(&mut self, asset: &'a str) -> &mut Self { + self.offer_asset = Some(asset); + + self + } + + pub fn with_ask_asset(&mut self, asset: &'a str) -> &mut Self { + self.ask_asset = Some(asset); + + self + } + + pub fn with_amount_offer_asset(&mut self, amount_offer_asset: u128) -> &mut Self { + self.amt_offer_asset = Some(amount_offer_asset); + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_fund_auction( + self.key, + ( + self.offer_asset + .ok_or(Error::MissingBuilderParam(String::from("pair")))?, + self.ask_asset + .ok_or(Error::MissingBuilderParam(String::from("pair")))?, + ), + self.amt_offer_asset + .ok_or(Error::MissingBuilderParam(String::from( + "amount_offer_asset", + )))?, + ) + } +} + +pub struct StartAuctionTxBuilder<'a> { + key: &'a str, + offer_asset: Option<&'a str>, + ask_asset: Option<&'a str>, + end_block_delta: Option, + test_ctx: &'a mut TestContext, +} + +impl<'a> StartAuctionTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + pub fn with_offer_asset(&mut self, asset: &'a str) -> &mut Self { + self.offer_asset = Some(asset); + + self + } + + pub fn with_ask_asset(&mut self, asset: &'a str) -> &mut Self { + self.ask_asset = Some(asset); + + self + } + + pub fn with_end_block_delta(&mut self, delta_blocks: u128) -> &mut Self { + self.end_block_delta = Some(delta_blocks); + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_start_auction( + self.key, + self.end_block_delta + .ok_or(Error::MissingBuilderParam(String::from("end_block_delta")))?, + ( + self.offer_asset + .ok_or(Error::MissingBuilderParam(String::from("pair")))?, + self.ask_asset + .ok_or(Error::MissingBuilderParam(String::from("pair")))?, + ), + ) + } +} + +pub struct MigrateAuctionTxBuilder<'a> { + key: &'a str, + offer_asset: Option<&'a str>, + ask_asset: Option<&'a str>, + test_ctx: &'a mut TestContext, +} + +impl<'a> MigrateAuctionTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + pub fn with_offer_asset(&mut self, asset: &'a str) -> &mut Self { + self.offer_asset = Some(asset); + + self + } + + pub fn with_ask_asset(&mut self, asset: &'a str) -> &mut Self { + self.ask_asset = Some(asset); + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_migrate_auction( + self.key, + ( + self.offer_asset + .ok_or(Error::MissingBuilderParam(String::from("pair")))?, + self.ask_asset + .ok_or(Error::MissingBuilderParam(String::from("pair")))?, + ), + ) + } +} + +pub struct CreatePriceOracleTxBuilder<'a> { + key: &'a str, + seconds_allow_manual_change: u64, + seconds_auction_prices_fresh: u64, + test_ctx: &'a mut TestContext, +} + +impl<'a> CreatePriceOracleTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + pub fn with_seconds_allow_manual_change(&mut self, sec: u64) -> &mut Self { + self.seconds_allow_manual_change = sec; + + self + } + + pub fn with_seconds_auction_prices_fresh(&mut self, sec: u64) -> &mut Self { + self.seconds_auction_prices_fresh = sec; + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_create_price_oracle( + self.key, + self.seconds_allow_manual_change, + self.seconds_auction_prices_fresh, + ) + } +} + +pub struct UpdateAuctionOracleTxBuilder<'a> { + key: &'a str, + test_ctx: &'a mut TestContext, +} + +impl<'a> UpdateAuctionOracleTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_update_auction_oracle(self.key) + } +} + +pub struct ManualOraclePriceUpdateTxBuilder<'a> { + key: &'a str, + offer_asset: Option<&'a str>, + ask_asset: Option<&'a str>, + price: Option, + test_ctx: &'a mut TestContext, +} + +impl<'a> ManualOraclePriceUpdateTxBuilder<'a> { + pub fn with_key(&mut self, key: &'a str) -> &mut Self { + self.key = key; + + self + } + + pub fn with_offer_asset(&mut self, asset: &'a str) -> &mut Self { + self.offer_asset = Some(asset); + + self + } + + pub fn with_ask_asset(&mut self, asset: &'a str) -> &mut Self { + self.ask_asset = Some(asset); + + self + } + + pub fn with_price(&mut self, price: Decimal) -> &mut Self { + self.price = Some(price); + + self + } + + /// Sends the transaction. + pub fn send(&mut self) -> Result<(), Error> { + self.test_ctx.tx_manual_oracle_price_update( + self.key, + self.offer_asset + .ok_or(Error::MissingBuilderParam(String::from("offer_asset")))?, + self.ask_asset + .ok_or(Error::MissingBuilderParam(String::from("ask_asset")))?, + self.price + .ok_or(Error::MissingBuilderParam(String::from("price")))?, + ) + } +} + +impl TestContext { + pub fn build_tx_create_auctions_manager(&mut self) -> CreateAuctionsManagerTxBuilder { + CreateAuctionsManagerTxBuilder { + key: DEFAULT_KEY, + min_auction_amount: &[], + server_addr: NEUTRON_CHAIN_ADMIN_ADDR, + test_ctx: self, + } + } + + /// Creates an auction manager on Neutron, updating the autions manager + /// code id and address in the TestContext. + fn tx_create_auctions_manager<'a>( + &mut self, + sender_key: &str, + min_auction_amount: impl AsRef<[(&'a str, MinAmount)]>, + server_addr: impl AsRef, + ) -> Result<(), Error> { + let mut contract_a: CosmWasm = self.get_contract(AUCTIONS_MANAGER_CONTRACT_NAME)?; + let neutron = self.get_chain(NEUTRON_CHAIN_NAME); + + let auction_code_id = + neutron + .contract_codes + .get(AUCTION_CONTRACT_NAME) + .ok_or(Error::Misc(format!( + "contract '{AUCTION_CONTRACT_NAME}' is missing" + )))?; + + let contract = contract_a.instantiate( + sender_key, + serde_json::json!({ + "auction_code_id": auction_code_id, + "min_auction_amount": min_auction_amount.as_ref(), + "server_addr": server_addr.as_ref(), + }) + .to_string() + .as_str(), + AUCTIONS_MANAGER_CONTRACT_NAME, + None, + "", + )?; + + self.auctions_manager = Some(DeployedContractInfo { + code_id: contract_a.code_id.ok_or(Error::Misc(format!( + "contract '{AUCTIONS_MANAGER_CONTRACT_NAME}' has no code ID" + )))?, + address: contract.address.clone(), + artifact_path: contract_a.file_path.ok_or(Error::Misc(format!( + "contract '{AUCTIONS_MANAGER_CONTRACT_NAME}' has no file path" + )))?, + }); + + let chain = self.get_mut_chain(NEUTRON_CHAIN_NAME); + + chain + .contract_addrs + .entry(AUCTIONS_MANAGER_CONTRACT_NAME.to_owned()) + .or_default() + .push(contract.address); + + Ok(()) + } + + pub fn build_tx_create_price_oracle(&mut self) -> CreatePriceOracleTxBuilder { + CreatePriceOracleTxBuilder { + key: DEFAULT_KEY, + seconds_allow_manual_change: 0, + seconds_auction_prices_fresh: 100000000000, + test_ctx: self, + } + } + + /// Creates an auction manager on Neutron, updating the autions manager + /// code id and address in the TestContext. + fn tx_create_price_oracle<'a>( + &mut self, + sender_key: &str, + seconds_allow_manual_change: u64, + seconds_auction_prices_fresh: u64, + ) -> Result<(), Error> { + let auctions_manager: CosmWasm = self.get_auctions_manager()?; + let auctions_manager_addr = + auctions_manager + .contract_addr + .ok_or(Error::MissingContextVariable(String::from( + "contract_addresses::auctions_manager", + )))?; + + let mut contract_a = self.get_contract(PRICE_ORACLE_NAME)?; + let contract = contract_a.instantiate( + sender_key, + serde_json::json!({ + "auctions_manager_addr": auctions_manager_addr, + "seconds_allow_manual_change": seconds_allow_manual_change, + "seconds_auction_prices_fresh": seconds_auction_prices_fresh, + }) + .to_string() + .as_str(), + PRICE_ORACLE_NAME, + None, + "", + )?; + + let chain = self.get_mut_chain(NEUTRON_CHAIN_NAME); + + chain + .contract_addrs + .entry(PRICE_ORACLE_NAME.to_owned()) + .or_default() + .push(contract.address); + + Ok(()) + } + + /// Creates an auction on Neutron. Requires that an auction manager has already been deployed. + pub fn build_tx_create_auction<'a>(&mut self) -> CreateAuctionTxBuilder { + CreateAuctionTxBuilder { + key: DEFAULT_KEY, + offer_asset: Default::default(), + ask_asset: Default::default(), + auction_strategy: AuctionStrategy { + start_price_perc: 5000, + end_price_perc: 5000, + }, + chain_halt_config: ChainHaltConfig { + cap: "14400".into(), + block_avg: "3".into(), + }, + price_freshness_strategy: PriceFreshnessStrategy { + limit: "3".into(), + multipliers: vec![("2".into(), "2".into()), ("1".into(), "1.5".into())], + }, + label: DEFAULT_AUCTION_LABEL, + amount_offer_asset: Default::default(), + test_ctx: self, + } + } + + /// Creates an auction on Neutron. Requires that an auction manager has already been deployed. + fn tx_create_auction<'a, TDenomA: AsRef, TDenomB: AsRef>( + &mut self, + sender_key: &str, + pair: (TDenomA, TDenomB), + auction_strategy: AuctionStrategy, + chain_halt_config: ChainHaltConfig, + price_freshness_strategy: PriceFreshnessStrategy, + label: impl AsRef, + amount_denom_a: u128, + ) -> Result<(), Error> { + // The auctions manager for this deployment + let contract_a = self.get_auctions_manager()?; + let denom_a = pair.0.as_ref(); + + let receipt = contract_a.execute( + sender_key, + serde_json::json!( + { + "admin": { + "new_auction": { + "msg": { + "pair": (pair.0.as_ref(), pair.1.as_ref()), + "auction_strategy": auction_strategy, + "chain_halt_config": chain_halt_config, + "price_freshness_strategy": price_freshness_strategy + }, + "label": label.as_ref(), + }, + }}) + .to_string() + .as_str(), + format!("--amount {amount_denom_a}{denom_a} --gas 2000000").as_str(), + )?; + + log::debug!( + "submitted tx creating auction ({}, {}) {:?}", + pair.0.as_ref(), + pair.1.as_ref(), + receipt + ); + + let _ = self.guard_tx_errors( + NEUTRON_CHAIN_NAME, + receipt.tx_hash.ok_or(Error::TxMissingLogs)?.as_str(), + )?; + + Ok(()) + } + + /// Creates an auction on Neutron. Requires that an auction manager has already been deployed. + pub fn build_tx_migrate_auction<'a>(&mut self) -> MigrateAuctionTxBuilder { + MigrateAuctionTxBuilder { + key: DEFAULT_KEY, + offer_asset: Default::default(), + ask_asset: Default::default(), + test_ctx: self, + } + } + + /// Creates an auction on Neutron. Requires that an auction manager has already been deployed. + fn tx_migrate_auction<'a, TDenomA: AsRef, TDenomB: AsRef>( + &mut self, + sender_key: &str, + pair: (TDenomA, TDenomB), + ) -> Result<(), Error> { + // The auctions manager for this deployment + let contract_a = self.get_auctions_manager()?; + let code_id = self.get_contract(AUCTION_CONTRACT_NAME)?.code_id.ok_or( + Error::MissingContextVariable(String::from("code_ids::auction")), + )?; + + let receipt = contract_a.execute( + sender_key, + serde_json::json!( + { + "admin": { + "migrate_auction": { + "pair": (pair.0.as_ref(), pair.1.as_ref()), + "code_id": code_id, + "msg": { + "no_state_change": {} + }, + }, + }}) + .to_string() + .as_str(), + format!("--gas 2000000").as_str(), + )?; + + log::debug!( + "submitted tx migrating auction ({}, {}) {:?}", + pair.0.as_ref(), + pair.1.as_ref(), + receipt + ); + + let _ = self.guard_tx_errors( + NEUTRON_CHAIN_NAME, + receipt.tx_hash.ok_or(Error::TxMissingLogs)?.as_str(), + )?; + + Ok(()) + } + + /// Creates a builder setting the oracle address on the auctions manager on neutron. + pub fn build_tx_update_auction_oracle(&mut self) -> UpdateAuctionOracleTxBuilder { + UpdateAuctionOracleTxBuilder { + key: DEFAULT_KEY, + test_ctx: self, + } + } + + fn tx_update_auction_oracle(&mut self, sender_key: &str) -> Result<(), Error> { + // The auctions manager for this deployment + let contract_a = self.get_auctions_manager()?; + let neutron = self.get_chain(NEUTRON_CHAIN_NAME); + let oracle = neutron + .contract_addrs + .get(PRICE_ORACLE_NAME) + .and_then(|addrs| addrs.get(0)) + .ok_or(Error::MissingContextVariable(String::from( + "contract_addrs::price_oracle", + )))?; + + let receipt = contract_a.execute( + sender_key, + serde_json::json!( + { + "admin": { + "update_oracle": { + "oracle_addr": oracle, + }, + }}) + .to_string() + .as_str(), + format!("--gas 2000000").as_str(), + )?; + + let _ = self.guard_tx_errors( + NEUTRON_CHAIN_NAME, + receipt.tx_hash.ok_or(Error::TxMissingLogs)?.as_str(), + )?; + + Ok(()) + } + + /// Creates a builder setting the oracle address on the auctions manager on neutron. + pub fn build_tx_manual_oracle_price_update(&mut self) -> ManualOraclePriceUpdateTxBuilder { + ManualOraclePriceUpdateTxBuilder { + key: DEFAULT_KEY, + offer_asset: Default::default(), + ask_asset: Default::default(), + price: Default::default(), + test_ctx: self, + } + } + + fn tx_manual_oracle_price_update( + &mut self, + sender_key: &str, + offer_asset: &str, + ask_asset: &str, + price: Decimal, + ) -> Result<(), Error> { + // The auctions manager for this deployment + let oracle = self.get_price_oracle()?; + + let receipt = oracle.execute( + sender_key, + serde_json::json!( + { + "manual_price_update": { + "pair": (offer_asset, ask_asset), + "price": price, + } + }) + .to_string() + .as_str(), + format!("--gas 2000000").as_str(), + )?; + + let _ = self.guard_tx_errors( + NEUTRON_CHAIN_NAME, + receipt.tx_hash.ok_or(Error::TxMissingLogs)?.as_str(), + )?; + + Ok(()) + } + + /// Sends the specified amount of funds to an auction. + pub fn build_tx_fund_auction(&mut self) -> FundAuctionTxBuilder { + FundAuctionTxBuilder { + key: DEFAULT_KEY, + offer_asset: Default::default(), + ask_asset: Default::default(), + amt_offer_asset: Default::default(), + test_ctx: self, + } + } + + /// Sends the specified amount of funds to an auction. + fn tx_fund_auction, TDenomB: AsRef>( + &mut self, + sender_key: &str, + pair: (TDenomA, TDenomB), + amt_offer_asset: u128, + ) -> Result<(), Error> { + let manager = self.get_auctions_manager()?; + + let denom_a = pair.0.as_ref(); + + let receipt = manager.execute( + sender_key, + serde_json::json!({ + "auction_funds": { + "pair": (pair.0.as_ref(), pair.1.as_ref()), + }, + }) + .to_string() + .as_str(), + format!("--amount {amt_offer_asset}{denom_a} --gas 1000000").as_str(), + )?; + + let _ = self.guard_tx_errors( + NEUTRON_CHAIN_NAME, + receipt.tx_hash.ok_or(Error::TxMissingLogs)?.as_str(), + )?; + + Ok(()) + } + + /// Builds a transaction to start the auction. + pub fn build_tx_start_auction(&mut self) -> StartAuctionTxBuilder { + StartAuctionTxBuilder { + key: DEFAULT_KEY, + offer_asset: Default::default(), + ask_asset: Default::default(), + end_block_delta: Default::default(), + test_ctx: self, + } + } + + /// Starts the specified auction. + fn tx_start_auction, TDenomB: AsRef>( + &mut self, + sender_key: &str, + end_blocks: u128, + pair: (TDenomA, TDenomB), + ) -> Result<(), Error> { + let manager = self.get_auctions_manager()?; + let neutron = self.get_chain(NEUTRON_CHAIN_NAME); + + let start_block_resp = neutron + .rb + .bin("q block --node=%RPC% --chain-id=%CHAIN_ID%", true); + let maybe_start_block_data: Value = start_block_resp + .get("text") + .and_then(|maybe_text| maybe_text.as_str()) + .and_then(|text| serde_json::from_str(text).ok()) + .ok_or(Error::ContainerCmd(String::from("query block")))?; + + let maybe_start_block = maybe_start_block_data + .get("block") + .and_then(|block| block.get("header")) + .and_then(|header| header.get("height")) + .ok_or(Error::ContainerCmd(String::from("query block")))?; + let start_block = maybe_start_block + .as_str() + .and_then(|s| s.parse::().ok()) + .ok_or(Error::ContainerCmd(String::from("query block")))?; + + let receipt = manager.execute( + sender_key, + serde_json::json!({ + "server": { + "open_auction": { + "pair": (pair.0.as_ref(), pair.1.as_ref()), + "params": { + "end_block": start_block + end_blocks, + "start_block": start_block, + } + }}, + }) + .to_string() + .as_str(), + "--gas 1000000", + )?; + + let _ = self.guard_tx_errors( + NEUTRON_CHAIN_NAME, + receipt.tx_hash.ok_or(Error::TxMissingLogs)?.as_str(), + )?; + + Ok(()) + } +} diff --git a/src/utils/test_context.rs b/src/utils/test_context.rs new file mode 100644 index 0000000..d2e29a3 --- /dev/null +++ b/src/utils/test_context.rs @@ -0,0 +1,638 @@ +use super::super::{ + error::Error, + types::{config::ConfigChain, contract::DeployedContractInfo, ibc::Channel as QueryChannel}, + LOCAL_IC_API_URL, TRANSFER_PORT, +}; +use cosmwasm_std::{StdError, StdResult}; +use localic_std::{ + modules::cosmwasm::CosmWasm, relayer::Channel, relayer::Relayer, + transactions::ChainRequestBuilder, +}; +use std::{collections::HashMap, path::PathBuf}; + +/// A configurable builder that can be used to create a TestContext. +pub struct TestContextBuilder { + chains: Vec, + api_url: Option, + transfer_channel_ids: HashMap<(String, String), String>, + ccv_channel_ids: HashMap<(String, String), String>, + connection_ids: HashMap<(String, String), String>, + ibc_denoms: HashMap<(String, String), String>, + artifacts_dir: Option, + unwrap_raw_logs: bool, + transfer_channels: Vec<(String, String)>, +} + +impl Default for TestContextBuilder { + fn default() -> Self { + Self { + chains: Default::default(), + api_url: Some(LOCAL_IC_API_URL.to_owned()), + transfer_channel_ids: Default::default(), + ccv_channel_ids: Default::default(), + connection_ids: Default::default(), + ibc_denoms: Default::default(), + artifacts_dir: Default::default(), + unwrap_raw_logs: Default::default(), + transfer_channels: Default::default(), + } + } +} + +impl TestContextBuilder { + /// Resets the chains that this builder will create a context for to the specified value. + pub fn with_chains(&mut self, chains: impl Into>) -> &mut Self { + self.chains = chains.into(); + + self + } + + /// Adds the specified chain to the context built by the builder. + pub fn with_chain(&mut self, chain: ConfigChain) -> &mut Self { + self.chains.push(chain); + + self + } + + /// Sets the local-ic endpoint that the built context will interact with. + pub fn with_api_url(&mut self, api_url: impl Into) -> &mut Self { + self.api_url = Some(api_url.into()); + + self + } + + /// Sets the transfer channel ID map to the specified map. + pub fn with_transfer_channel_ids( + &mut self, + ids: impl Into>, + ) -> &mut Self { + self.transfer_channel_ids = ids.into(); + + self + } + + /// Inserts a given channel ID for a chain pair into the builder. + pub fn with_transfer_channel_id( + &mut self, + chain_a: impl Into, + chain_b: impl Into, + channel_id: impl Into, + ) -> &mut Self { + self.transfer_channel_ids + .insert((chain_a.into(), chain_b.into()), channel_id.into()); + + self + } + + /// Inserts a channel for transfer between the specified chains. + pub fn with_transfer_channel( + &mut self, + chain_a: impl Into, + chain_b: impl Into, + ) -> &mut Self { + self.transfer_channels + .push((chain_a.into(), chain_b.into())); + + self + } + + /// Sets the transfer channel ID map to the specified map. + pub fn with_ccv_channel_ids( + &mut self, + ids: impl Into>, + ) -> &mut Self { + self.ccv_channel_ids = ids.into(); + + self + } + + /// Inserts a given channel ID for a chain pair into the builder. + pub fn with_ccv_channel_id( + &mut self, + chain_a: impl Into, + chain_b: impl Into, + channel_id: impl Into, + ) -> &mut Self { + self.ccv_channel_ids + .insert((chain_a.into(), chain_b.into()), channel_id.into()); + + self + } + + /// Sets the connection ID map to the specified map. + pub fn with_connection_ids( + &mut self, + ids: impl Into>, + ) -> &mut Self { + self.connection_ids = ids.into(); + + self + } + + /// Inserts a given connection ID for a chain pair into the builder. + pub fn with_connection_id( + &mut self, + chain_a: impl Into, + chain_b: impl Into, + channel_id: impl Into, + ) -> &mut Self { + self.connection_ids + .insert((chain_a.into(), chain_b.into()), channel_id.into()); + + self + } + + /// Sets the IBC denom map to the specified map. + pub fn with_ibc_denoms( + &mut self, + denoms: impl Into>, + ) -> &mut Self { + self.ibc_denoms = denoms.into(); + + self + } + + /// Inserts a given connection ID for a chain pair into the builder. + pub fn with_ibc_denom( + &mut self, + chain_a: impl Into, + chain_b: impl Into, + denom: impl Into, + ) -> &mut Self { + self.ibc_denoms + .insert((chain_a.into(), chain_b.into()), denom.into()); + + self + } + + /// Sets the artifacts dir to the specified directory. + pub fn with_artifacts_dir(&mut self, dir: impl Into) -> &mut Self { + self.artifacts_dir = Some(dir.into()); + + self + } + + /// Sets the artifacts dir to the specified directory. + pub fn with_unwrap_raw_logs(&mut self, unwrap_logs: bool) -> &mut Self { + self.unwrap_raw_logs = unwrap_logs; + + self + } + + /// Builds a TestContext from the specified options. + pub fn build(&self) -> Result { + let TestContextBuilder { + chains, + transfer_channel_ids, + ccv_channel_ids, + connection_ids, + ibc_denoms, + api_url, + artifacts_dir, + unwrap_raw_logs, + transfer_channels, + } = self; + + // Upload contract artifacts + + /// Deploys all neutron contracts to the test context. + fn config_chain_to_local_chain( + c: ConfigChain, + api_url: String, + ) -> Result { + let rb = ChainRequestBuilder::new(api_url.to_owned(), c.chain_id.clone(), c.debugging)?; + + let relayer = Relayer::new(&rb); + let channels = relayer.get_channels(&rb.chain_id)?; + + Ok(LocalChain::new( + rb, + c.admin_addr, + c.denom, + channels, + c.chain_name, + c.chain_prefix, + )) + } + + let chains_res: Result, Error> = chains + .clone() + .into_iter() + .map(|builder| { + config_chain_to_local_chain( + builder, + api_url + .clone() + .ok_or(Error::MissingBuilderParam(String::from("api_url")))?, + ) + }) + .fold(Ok(HashMap::new()), |acc, x| { + let x = x?; + let mut acc = acc?; + + acc.insert(x.chain_name.clone(), x); + + Ok(acc) + }); + let chains = chains_res?; + + let mut transfer_channel_ids = transfer_channel_ids.clone(); + + for (chain_a, chain_b) in transfer_channels { + let chain_a_chain = chains + .get(chain_a) + .ok_or(Error::MissingBuilderParam(String::from("chain")))?; + let chain_b_chain = chains + .get(chain_b) + .ok_or(Error::MissingBuilderParam(String::from("chain")))?; + + let conns = find_pairwise_transfer_channel_ids( + &chain_a_chain.rb, + &chain_a_chain.rb.chain_id, + &chain_b_chain.rb.chain_id, + )?; + + transfer_channel_ids.insert((chain_a.clone(), chain_b.clone()), conns.0.channel_id); + transfer_channel_ids.insert((chain_b.clone(), chain_a.clone()), conns.1.channel_id); + } + + Ok(TestContext { + chains, + transfer_channel_ids, + ccv_channel_ids: ccv_channel_ids.clone(), + connection_ids: connection_ids.clone(), + ibc_denoms: ibc_denoms.clone(), + artifacts_dir: artifacts_dir + .clone() + .ok_or(Error::MissingBuilderParam(String::from("artifacts_dir")))?, + auctions_manager: None, + astroport_token_registry: None, + astroport_factory: None, + unwrap_logs: *unwrap_raw_logs, + }) + } +} + +pub struct TestContext { + pub chains: HashMap, + // maps (src_chain_id, dest_chain_id) to transfer channel id + pub transfer_channel_ids: HashMap<(String, String), String>, + // maps (src_chain_id, dest_chain_id) to ccv channel id + pub ccv_channel_ids: HashMap<(String, String), String>, + // maps (src_chain_id, dest_chain_id) to connection id + pub connection_ids: HashMap<(String, String), String>, + // maps (src_chain_id, dest_chain_id) to src chain native + // denom -> ibc denom on dest chain + pub ibc_denoms: HashMap<(String, String), String>, + /// The path to .wasm contract artifacts + pub artifacts_dir: String, + + /// Valence deployment info + pub auctions_manager: Option, + + /// Astroport deployment info + pub astroport_token_registry: Option, + pub astroport_factory: Option, + + /// Whether or not logs should be expected and guarded for each tx + pub unwrap_logs: bool, +} + +pub struct LocalChain { + /// ChainRequestBuilder + pub rb: ChainRequestBuilder, + /// contract codes stored on this chain (filename -> code_id) + pub contract_codes: HashMap, + /// outgoing channel ids + pub channels: Vec, + /// outgoing connection ids available (dest_chain_id -> connection_id) + pub connection_ids: HashMap, + pub admin_addr: String, + pub native_denom: String, + /// contract addresses for deployed instances of contracts + pub contract_addrs: HashMap>, + /// The name of the chain + pub chain_name: String, + pub chain_prefix: String, +} + +impl LocalChain { + pub fn new( + rb: ChainRequestBuilder, + admin_addr: String, + native_denom: String, + channels: Vec, + chain_name: String, + chain_prefix: String, + ) -> Self { + Self { + rb, + contract_codes: Default::default(), + channels, + connection_ids: Default::default(), + admin_addr, + native_denom, + contract_addrs: Default::default(), + chain_name, + chain_prefix, + } + } + + pub fn get_cw(&mut self) -> CosmWasm { + CosmWasm::new(&self.rb) + } + + pub fn save_code(&mut self, abs_path: PathBuf, code: u64) { + let id = abs_path.file_stem().unwrap().to_str().unwrap(); + self.contract_codes.insert(id.to_string(), code); + } +} + +impl TestContext { + pub fn get_transfer_channels(&self) -> TestContextQuery { + TestContextQuery::new(self, QueryType::TransferChannel) + } + + pub fn get_connections(&self) -> TestContextQuery { + TestContextQuery::new(self, QueryType::Connection) + } + + pub fn get_ccv_channels(&self) -> TestContextQuery { + TestContextQuery::new(self, QueryType::CCVChannel) + } + + pub fn get_ibc_denoms(&self) -> TestContextQuery { + TestContextQuery::new(self, QueryType::IBCDenom) + } + + pub fn get_admin_addr(&self) -> TestContextQuery { + TestContextQuery::new(self, QueryType::AdminAddr) + } + + pub fn get_native_denom(&self) -> TestContextQuery { + TestContextQuery::new(self, QueryType::NativeDenom) + } + + pub fn get_request_builder(&self) -> TestContextQuery { + TestContextQuery::new(self, QueryType::RequestBuilder) + } + + pub fn get_chain(&self, chain_name: &str) -> &LocalChain { + self.chains.get(chain_name).unwrap() + } + + pub fn get_mut_chain(&mut self, chain_name: &str) -> &mut LocalChain { + self.chains.get_mut(chain_name).unwrap() + } +} + +pub enum QueryType { + TransferChannel, + Connection, + CCVChannel, + IBCDenom, + AdminAddr, + NativeDenom, + RequestBuilder, +} + +pub struct TestContextQuery<'a> { + context: &'a TestContext, + query_type: QueryType, + src_chain: Option, + dest_chain: Option, + contract_name: Option, +} + +impl<'a> TestContextQuery<'a> { + pub fn new(context: &'a TestContext, query_type: QueryType) -> Self { + Self { + context, + query_type, + src_chain: None, + dest_chain: None, + contract_name: None, + } + } + + pub fn src(mut self, src_chain: &str) -> Self { + self.src_chain = Some(src_chain.to_string()); + self + } + + pub fn dest(mut self, dest_chain: &str) -> Self { + self.dest_chain = Some(dest_chain.to_string()); + self + } + + pub fn contract(mut self, contract_name: &str) -> Self { + self.contract_name = Some(contract_name.to_string()); + self + } + + pub fn get(self) -> String { + let query_response = match self.query_type { + QueryType::TransferChannel => self.get_transfer_channel(), + QueryType::Connection => self.get_connection_id(), + QueryType::CCVChannel => self.get_ccv_channel(), + QueryType::IBCDenom => self.get_ibc_denom(), + QueryType::AdminAddr => self.get_admin_addr(), + QueryType::NativeDenom => self.get_native_denom(), + _ => None, + }; + query_response.unwrap() + } + + pub fn get_all(self) -> Vec { + match self.query_type { + QueryType::TransferChannel => self.get_all_transfer_channels(), + QueryType::Connection => self.get_all_connections(), + _ => vec![], + } + } + + pub fn get_request_builder(mut self, chain: &str) -> &'a ChainRequestBuilder { + self.src_chain = Some(chain.to_string()); + let rb = match self.query_type { + QueryType::RequestBuilder => self.get_rb(), + _ => None, + }; + rb.unwrap() + } + + fn get_transfer_channel(self) -> Option { + if let (Some(ref src), Some(ref dest)) = (self.src_chain, self.dest_chain) { + self.context + .transfer_channel_ids + .get(&(src.clone(), dest.clone())) + .cloned() + } else { + None + } + } + + fn get_all_transfer_channels(self) -> Vec { + if let Some(ref src) = self.src_chain { + self.context + .transfer_channel_ids + .iter() + .filter(|((s, _), _)| s == src) + .map(|(_, v)| v.clone()) + .collect::>() + } else { + vec![] + } + } + + fn get_connection_id(self) -> Option { + if let (Some(ref src), Some(ref dest)) = (self.src_chain, self.dest_chain) { + self.context + .connection_ids + .get(&(src.clone(), dest.clone())) + .cloned() + } else { + None + } + } + + fn get_all_connections(self) -> Vec { + if let Some(ref src) = self.src_chain { + self.context + .connection_ids + .iter() + .filter(|((s, _), _)| s == src) + .map(|(_, v)| v.clone()) + .collect::>() + } else { + vec![] + } + } + + fn get_ccv_channel(self) -> Option { + if let (Some(ref src), Some(ref dest)) = (self.src_chain, self.dest_chain) { + self.context + .ccv_channel_ids + .get(&(src.clone(), dest.clone())) + .cloned() + } else { + None + } + } + + fn get_ibc_denom(self) -> Option { + if let (Some(ref src), Some(ref dest)) = (self.src_chain, self.dest_chain) { + self.context + .ibc_denoms + .get(&(src.clone(), dest.clone())) + .cloned() + } else { + None + } + } + + fn get_admin_addr(self) -> Option { + if let Some(ref src) = self.src_chain { + self.context + .chains + .get(src) + .map(|chain| chain.admin_addr.clone()) + } else { + None + } + } + + fn get_native_denom(self) -> Option { + if let Some(ref src) = self.src_chain { + self.context + .chains + .get(src) + .map(|chain| chain.native_denom.clone()) + } else { + None + } + } + + fn get_rb(self) -> Option<&'a ChainRequestBuilder> { + if let Some(ref src) = self.src_chain { + self.context.chains.get(src).map(|chain| &chain.rb) + } else { + None + } + } +} + +pub fn find_pairwise_transfer_channel_ids( + rb: &ChainRequestBuilder, + src_chain_id: &str, + dest_chain_id: &str, +) -> Result<(PairwiseChannelResult, PairwiseChannelResult), Error> { + let relayer = Relayer::new(rb); + let cmd = format!("rly q channels {src_chain_id} {dest_chain_id}"); + let result = relayer.execute(cmd.as_str(), true).unwrap(); + let json_string = result["text"].as_str().unwrap(); + let channels = json_string + .split('\n') + .filter(|s| !s.is_empty()) + .map(|s| serde_json::from_str(s)); + + for maybe_channel in channels { + let channel: QueryChannel = maybe_channel?; + + if channel.port_id == TRANSFER_PORT { + let party_channel = PairwiseChannelResult { + index: 0, + channel_id: channel.channel_id.to_owned(), + connection_id: channel.connection_hops[0].to_owned(), + }; + let counterparty_channel = PairwiseChannelResult { + index: 0, + channel_id: channel.counterparty.channel_id.to_owned(), + connection_id: channel.connection_hops[0].to_owned(), + }; + + return Ok((party_channel, counterparty_channel)); + } + } + + Err(Error::MissingContextVariable(String::from(format!( + "channel_ids::{src_chain_id}-{dest_chain_id}" + )))) +} + +pub fn find_pairwise_ccv_channel_ids( + provider_channels: &[Channel], + consumer_channels: &[Channel], +) -> StdResult<(PairwiseChannelResult, PairwiseChannelResult)> { + for (a_i, a_chan) in provider_channels.iter().enumerate() { + for (b_i, b_chan) in consumer_channels.iter().enumerate() { + if a_chan.channel_id == b_chan.counterparty.channel_id + && b_chan.channel_id == a_chan.counterparty.channel_id + && a_chan.port_id == "provider" + && b_chan.port_id == "consumer" + && a_chan.ordering == "ORDER_ORDERED" + && b_chan.ordering == "ORDER_ORDERED" + { + let provider_channel_result = PairwiseChannelResult { + index: a_i, + channel_id: a_chan.channel_id.to_string(), + connection_id: a_chan.connection_hops[0].to_string(), + }; + let consumer_channel_result = PairwiseChannelResult { + index: b_i, + channel_id: b_chan.channel_id.to_string(), + connection_id: b_chan.connection_hops[0].to_string(), + }; + return Ok((provider_channel_result, consumer_channel_result)); + } + } + } + Err(StdError::generic_err( + "failed to match pairwise ccv channels", + )) +} + +pub struct PairwiseChannelResult { + pub index: usize, + pub channel_id: String, + pub connection_id: String, +}