From 91edbce4936f9d8971ac221e51e1bc2d2bdf0e5e Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 3 Aug 2023 12:32:39 +0200 Subject: [PATCH 001/586] init v2 --- contracts/covenant/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/covenant/README.md b/contracts/covenant/README.md index 5d3f0bdf..6a4e420f 100644 --- a/contracts/covenant/README.md +++ b/contracts/covenant/README.md @@ -9,7 +9,6 @@ clock -> holder -> lp -> ls -> depositor 1. instantiate clock, holder 1. instantiate lp with clock and holder addresses 1. instantiate ls with clock and lper addresses -1. tick ls, instantiate ICA 1. instantiate depositor with stride ICA, lper, and clock addresses 1. tick depositor to instantiate gaia ICA 1. tick depositor to LS on stride From 261cfc524ac0f4eb85ff447f2cae2f5ee37403cf Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 3 Aug 2023 13:02:16 +0200 Subject: [PATCH 002/586] init ibc-forwarder --- contracts/ibc-forwarder/.cargo/config | 3 ++ contracts/ibc-forwarder/Cargo.toml | 48 +++++++++++++++++++++++++++ contracts/ibc-forwarder/LICENSE | 29 ++++++++++++++++ contracts/ibc-forwarder/README.md | 26 +++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 contracts/ibc-forwarder/.cargo/config create mode 100644 contracts/ibc-forwarder/Cargo.toml create mode 100644 contracts/ibc-forwarder/LICENSE create mode 100644 contracts/ibc-forwarder/README.md diff --git a/contracts/ibc-forwarder/.cargo/config b/contracts/ibc-forwarder/.cargo/config new file mode 100644 index 00000000..5f6aa466 --- /dev/null +++ b/contracts/ibc-forwarder/.cargo/config @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +schema = "run --bin schema" diff --git a/contracts/ibc-forwarder/Cargo.toml b/contracts/ibc-forwarder/Cargo.toml new file mode 100644 index 00000000..c8fa381c --- /dev/null +++ b/contracts/ibc-forwarder/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "covenant-ibc-forwarder" +edition = { workspace = true } +authors = ["benskey bekauz@protonmail.com"] +description = "IBC Forwarder module for covenants" +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +exclude = [ + "contract.wasm", + "hash.txt", +] + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +covenant-clock-derive = { workspace = true} +covenant-ls = { workspace = true, features=["library"] } +covenant-clock = { workspace = true, features=["library"]} +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +cw2 = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +sha2 = { workspace = true } +neutron-sdk = { workspace = true } +cosmos-sdk-proto = { workspace = true } +protobuf = { workspace = true } +schemars = { workspace = true } +serde-json-wasm = { workspace = true } +base64 = { workspace = true } +prost = { workspace = true } +prost-types = { workspace = true } +bech32 = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +anyhow = { workspace = true } diff --git a/contracts/ibc-forwarder/LICENSE b/contracts/ibc-forwarder/LICENSE new file mode 100644 index 00000000..0bdef96b --- /dev/null +++ b/contracts/ibc-forwarder/LICENSE @@ -0,0 +1,29 @@ +Copyright 2023 Timewave + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/contracts/ibc-forwarder/README.md b/contracts/ibc-forwarder/README.md new file mode 100644 index 00000000..1371e16a --- /dev/null +++ b/contracts/ibc-forwarder/README.md @@ -0,0 +1,26 @@ +# IBC Forwarder + +IBC Forwarders are contracts instantiated on neutron with the sole responsibility of +receiving funds to an ICA on a remote chain and forwarding them to another module. + +In addition to being aware of all IBC related information such as ICA & IBC transfer +timeouts and channel/connection-ids, forwarder needs to have a destination contract +address. + +The destination contract is used to perform a `DepositAddress {}` query, which will return an +`Option`. This gives us two cases: + +1. `None`, in which case IBC Forwarder does nothing and keeps waiting +1. `Addr`, which is then used as a destination address to forward the funds to + +While IBC Forwarder should remain agnostic to any underlying details of what the +deposit address is, a few examples of it may be: + +- another ICA address or an autopilot receiver string in case of Liquid Staker +- contract address itself in case of Liquid Pooler + +IBC Forwarder needs to receive funds in order to be able to forward them. To enable +that, we expose a `DepositAddress {}` query method. After instantiating its ICA, +forwarder can return that ICA address as its deposit address. Prior to ICA +instantiation the query should be returning `None`, indicating that it is not yet +ready to receive funds. From 0eb9977b8000d508d0d0ff09bd89a58eab6d9535 Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 3 Aug 2023 19:39:17 +0200 Subject: [PATCH 003/586] wip: forwarding funds --- contracts/ibc-forwarder/src/contract.rs | 182 ++++++++++++++++++++++++ contracts/ibc-forwarder/src/error.rs | 14 ++ contracts/ibc-forwarder/src/lib.rs | 8 ++ contracts/ibc-forwarder/src/msg.rs | 88 ++++++++++++ contracts/ibc-forwarder/src/state.rs | 35 +++++ 5 files changed, 327 insertions(+) create mode 100644 contracts/ibc-forwarder/src/contract.rs create mode 100644 contracts/ibc-forwarder/src/error.rs create mode 100644 contracts/ibc-forwarder/src/lib.rs create mode 100644 contracts/ibc-forwarder/src/msg.rs create mode 100644 contracts/ibc-forwarder/src/state.rs diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs new file mode 100644 index 00000000..910ff330 --- /dev/null +++ b/contracts/ibc-forwarder/src/contract.rs @@ -0,0 +1,182 @@ +use cosmos_sdk_proto::{cosmos::base::v1beta1::Coin, ibc::applications::transfer::v1::MsgTransfer}; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; + +use cosmwasm_std::{Env, MessageInfo, Response, Deps, DepsMut, StdError, Binary, Addr}; +use covenant_clock::helpers::verify_clock; +use cw2::set_contract_version; +use neutron_sdk::{NeutronResult, bindings::{msg::NeutronMsg, query::NeutronQuery, types::ProtobufAny}, interchain_txs::helpers::get_port_id, NeutronError,}; +use prost::Message; + +use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState, RemoteChainInfo}, state::{CONTRACT_STATE, CLOCK_ADDRESS, INTERCHAIN_ACCOUNTS, IBC_FEE, ICA_TIMEOUT, IBC_TRANSFER_TIMEOUT, REMOTE_CHAIN_INFO, NEXT_CONTRACT}, error::ContractError}; + + +const CONTRACT_NAME: &str = "crates.io:covenant-ibc-forwarder"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +const INTERCHAIN_ACCOUNT_ID: &str = "ica"; + +type QueryDeps<'a> = Deps<'a, NeutronQuery>; +type ExecuteDeps<'a> = DepsMut<'a, NeutronQuery>; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: ExecuteDeps, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> NeutronResult> { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + + // ibc fees and timeouts + IBC_FEE.save(deps.storage, &msg.ibc_fee)?; + ICA_TIMEOUT.save(deps.storage, &msg.ica_timeout)?; + IBC_TRANSFER_TIMEOUT.save(deps.storage, &msg.ibc_transfer_timeout)?; + let next_contract = deps.api.addr_validate(&msg.next_contract)?; + NEXT_CONTRACT.save(deps.storage, &next_contract)?; + + REMOTE_CHAIN_INFO.save(deps.storage, &RemoteChainInfo { + connection_id: msg.remote_chain_connection_id, + channel_id: msg.remote_chain_channel_id, + denom: msg.denom, + amount: msg.amount, + })?; + + Ok(Response::default() + .add_attribute("method", "ibc_forwarder_instantiate") + + ) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: ExecuteDeps, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> NeutronResult> { + match msg { + ExecuteMsg::Tick {} => try_tick(deps, env, info), + } +} + + +/// attempts to advance the state machine. validates the caller to be the clock. +fn try_tick(deps: ExecuteDeps, env: Env, info: MessageInfo) -> NeutronResult> { + // Verify caller is the clock + verify_clock(&info.sender, &CLOCK_ADDRESS.load(deps.storage)?)?; + + let current_state = CONTRACT_STATE.load(deps.storage)?; + match current_state { + ContractState::Instantiated => try_register_ica(deps, env), + ContractState::ICACreated => try_forward_funds(env, deps), + ContractState::Complete => todo!(), + } +} + +/// tries to register an ICA on the remote chain +fn try_register_ica(deps: ExecuteDeps, env: Env) -> NeutronResult> { + + let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; + + let register_msg = NeutronMsg::register_interchain_account( + remote_chain_info.connection_id, + INTERCHAIN_ACCOUNT_ID.to_string() + ); + + let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); + + // we are saving empty data here because we handle response of registering ICA in sudo_open_ack method + INTERCHAIN_ACCOUNTS.save(deps.storage, key, &None)?; + + Ok(Response::new() + .add_attribute("method", "try_register_ica") + .add_message(register_msg) + ) +} + +fn try_forward_funds(env: Env, mut deps: ExecuteDeps) -> NeutronResult> { + + + // first we verify whether the next contract is ready for receiving the funds + let next_contract = NEXT_CONTRACT.load(deps.storage)?; + let deposit_address_query: Option = deps.querier.query_wasm_smart( + next_contract, + &crate::msg::QueryMsg::DepositAddress {}, + )?; + + // if query returns None, then we error and wait + let deposit_address = if let Some(addr) = deposit_address_query { + addr + } else { + return Err(NeutronError::Std( + StdError::not_found("Next contract is not ready for receiving the funds yet") + )) + }; + + let port_id = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); + let interchain_account = INTERCHAIN_ACCOUNTS.load( + deps.storage, + port_id.clone() + )?; + + match interchain_account { + Some((address, controller_conn_id)) => { + let ibc_transfer_timeout = IBC_TRANSFER_TIMEOUT.load(deps.storage)?; + let ica_timeout = ICA_TIMEOUT.load(deps.storage)?; + let fee = IBC_FEE.load(deps.storage)?; + let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; + + let coin = remote_chain_info.proto_coin(); + + let transfer_msg = MsgTransfer { + source_port: "transfer".to_string(), + source_channel: remote_chain_info.channel_id, + token: Some(coin), + sender: address, + receiver: deposit_address.to_string(), + timeout_height: None, + timeout_timestamp: env.block.time + .plus_seconds(ica_timeout.u64()) + .plus_seconds(ibc_transfer_timeout.u64()) + .nanos(), + }; + + let protobuf_msg = to_proto_msg_transfer(transfer_msg)?; + + // tx to our ICA that wraps the transfer message defined above + let submit_msg = NeutronMsg::submit_tx( + controller_conn_id, + INTERCHAIN_ACCOUNT_ID.to_string(), + vec![protobuf_msg], + "".to_string(), + ica_timeout.u64(), + fee, + ); + + // sudo callback msg + + Ok(Response::default()) + }, + None => Ok(Response::default() + .add_attribute("method", "try_forward_funds") + .add_attribute("error", "no_ica_found") + ), + } +} + +/// helper that serializes a MsgTransfer to protobuf +fn to_proto_msg_transfer(msg: impl Message) -> NeutronResult { + // Serialize the Transfer message + let mut buf = Vec::new(); + buf.reserve(msg.encoded_len()); + if let Err(e) = msg.encode(&mut buf) { + return Err(StdError::generic_err(format!("Encode error: {e}")).into()); + } + + Ok(ProtobufAny { + type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), + value: Binary::from(buf), + }) +} \ No newline at end of file diff --git a/contracts/ibc-forwarder/src/error.rs b/contracts/ibc-forwarder/src/error.rs new file mode 100644 index 00000000..3d44dc28 --- /dev/null +++ b/contracts/ibc-forwarder/src/error.rs @@ -0,0 +1,14 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Next contract is not ready for receiving the funds yet")] + DepositAddressNotAvailable {}, +} diff --git a/contracts/ibc-forwarder/src/lib.rs b/contracts/ibc-forwarder/src/lib.rs new file mode 100644 index 00000000..52fbe389 --- /dev/null +++ b/contracts/ibc-forwarder/src/lib.rs @@ -0,0 +1,8 @@ +#![warn(clippy::unwrap_used, clippy::expect_used)] + +extern crate core; + +pub mod contract; +pub mod error; +pub mod msg; +pub mod state; \ No newline at end of file diff --git a/contracts/ibc-forwarder/src/msg.rs b/contracts/ibc-forwarder/src/msg.rs new file mode 100644 index 00000000..0446c82a --- /dev/null +++ b/contracts/ibc-forwarder/src/msg.rs @@ -0,0 +1,88 @@ +use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Uint64; +use covenant_clock_derive::clocked; +use neutron_sdk::bindings::msg::IbcFee; + + +#[cw_serde] +pub struct InstantiateMsg { + /// address for the clock. this contract verifies + /// that only the clock can execute ticks + pub clock_address: String, + /// contract responsible for providing the address to forward the + /// funds to + pub next_contract: String, + + pub remote_chain_connection_id: String, + pub remote_chain_channel_id: String, + pub denom: String, + pub amount: String, + + // pub remote_chain_channel_id: String, + // pub remote_chain_connection_id: String, + /// neutron requires fees to be set to refund relayers for + /// submission of ack and timeout messages. + /// recv_fee and ack_fee paid in untrn from this contract + pub ibc_fee: IbcFee, + /// timeout in seconds. this is used to craft a timeout timestamp + /// that will be attached to the IBC transfer message from the ICA + /// on the host chain to its destination. typically this timeout + /// should be greater than the ICA timeout, otherwise if the ICA + /// times out, the destination chain receiving the funds will also + /// receive the IBC packet with an expired timestamp. + pub ibc_transfer_timeout: Uint64, + /// time in seconds for ICA SubmitTX messages from neutron + /// note that ICA uses ordered channels, a timeout implies + /// channel closed. We can reopen the channel by reregistering + /// the ICA with the same port id and connection id + pub ica_timeout: Uint64, +} + +#[cw_serde] +pub struct RemoteChainInfo { + /// connection id from neutron to the remote chain on which + /// we wish to open an ICA + pub connection_id: String, + pub channel_id: String, + pub denom: String, + pub amount: String, +} + +impl RemoteChainInfo { + pub fn proto_coin(&self) -> Coin { + Coin { + denom: self.denom.to_string(), + amount: self.amount.to_string(), + } + } +} + +#[clocked] +#[cw_serde] +pub enum ExecuteMsg {} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(Option)] + DepositAddress {}, +} + +#[cw_serde] +pub enum ContractState { + /// Contract was instantiated, ready create ica + Instantiated, + /// ICA was created, funds are ready to be forwarded + ICACreated, + /// forwarder is complete + Complete, +} + +/// SudoPayload is a type that stores information about a transaction that we try to execute +/// on the host chain. This is a type introduced for our convenience. +#[cw_serde] +pub struct SudoPayload { + pub message: String, + pub port_id: String, +} diff --git a/contracts/ibc-forwarder/src/state.rs b/contracts/ibc-forwarder/src/state.rs new file mode 100644 index 00000000..02979595 --- /dev/null +++ b/contracts/ibc-forwarder/src/state.rs @@ -0,0 +1,35 @@ +use cosmwasm_std::{Addr, Uint64}; +use cw_storage_plus::{Item, Map}; +use neutron_sdk::bindings::msg::IbcFee; + +use crate::msg::{ContractState, RemoteChainInfo}; + + + +/// tracks the current state of state machine +pub const CONTRACT_STATE: Item = Item::new("contract_state"); + +/// clock module address to verify the sender of incoming ticks +pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); + +pub const NEXT_CONTRACT: Item = Item::new("next_contract"); + +/// information needed for an ibc transfer to the remote chain +pub const REMOTE_CHAIN_INFO: Item = Item::new("r_c_info"); + +/// timeout in seconds for inner ibc MsgTransfer +pub const IBC_TRANSFER_TIMEOUT: Item = Item::new("ibc_transfer_timeout"); +/// time in seconds for ICA SubmitTX messages from neutron +pub const ICA_TIMEOUT: Item = Item::new("ica_timeout"); +/// neutron IbcFee for relayers +pub const IBC_FEE: Item = Item::new("ibc_fee"); + + +/// id of the connection between neutron and remote chain on which we +/// wish to open an ICA on +// pub const REMOTE_CHAIN_CONNECTION_ID: Item = Item::new("rc_conn_id"); +// pub const REMOTE_CHAIN_DENOM: Item = Item::new("rc_denom"); +// pub const TRANSFER_CHANNEL_ID: Item = Item::new("transfer_chann_id"); + +/// interchain accounts storage in form of (port_id) -> (address, controller_connection_id) +pub const INTERCHAIN_ACCOUNTS: Map> = Map::new("interchain_accounts"); From 3d4119fe0955f817a9733caf28ef1570bdf67894 Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 27 Jul 2023 14:52:25 +0200 Subject: [PATCH 004/586] init dev branch --- stride-covenant/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stride-covenant/README.md b/stride-covenant/README.md index 3547342b..7740cd67 100644 --- a/stride-covenant/README.md +++ b/stride-covenant/README.md @@ -1 +1 @@ -# Stride covenant \ No newline at end of file +# Stride covenant From 30224a37d4cdb69b0808a1b77480abbe929d1631 Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 28 Jul 2023 21:01:54 +0200 Subject: [PATCH 005/586] channel closure helper method --- .../tests/interchaintest/ics_test.go | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/stride-covenant/tests/interchaintest/ics_test.go b/stride-covenant/tests/interchaintest/ics_test.go index 4bebc915..17a8cf74 100644 --- a/stride-covenant/tests/interchaintest/ics_test.go +++ b/stride-covenant/tests/interchaintest/ics_test.go @@ -15,9 +15,9 @@ import ( ibctest "github.com/strangelove-ventures/interchaintest/v3" "github.com/strangelove-ventures/interchaintest/v3/chain/cosmos" "github.com/strangelove-ventures/interchaintest/v3/ibc" + "github.com/strangelove-ventures/interchaintest/v3/relayer/rly" "github.com/strangelove-ventures/interchaintest/v3/relayer" - "github.com/strangelove-ventures/interchaintest/v3/relayer/rly" "github.com/strangelove-ventures/interchaintest/v3/testreporter" "github.com/strangelove-ventures/interchaintest/v3/testutil" "github.com/stretchr/testify/require" @@ -186,8 +186,7 @@ func TestICS(t *testing.T) { Client: client, NetworkID: network, BlockDatabaseFile: ibctest.DefaultBlockDatabaseFilepath(), - - SkipPathCreation: false, + SkipPathCreation: false, }) require.NoError(t, err, "failed to build interchain") @@ -1231,6 +1230,25 @@ func TestICS(t *testing.T) { require.EqualValues(t, int64(atomFundsToDepositor), atomBal) }) + killChannel := func(path string, channelId string, portId string) ibc.RelayerExecResult { + channelClosureCmd := []string{ + "rly", "transact", "channel-close", path, channelId, portId, + } + relayerResult := r.Exec(ctx, eRep, channelClosureCmd, nil) + return relayerResult + } + + t.Run("kill gaia-neutron channel", func(t *testing.T) { + print("\nkilling gn channel\n") + + resp := killChannel(gaiaNeutronIBCPath, neutronGaiaTransferChannelId, "transfer") + // err = testutil.WaitForBlocks(ctx, 200, atom, neutron) + + print(string(resp.Stdout)) + err = testutil.WaitForBlocks(ctx, 200, atom, neutron) + + }) + // Tick the clock until the LSer has received stATOM // and Lper has received ATOM t.Run("tick clock until LSer receives funds", func(t *testing.T) { From 1406fbc3fc749be54c309f60cfa12a71cbfeb1c9 Mon Sep 17 00:00:00 2001 From: bekauz Date: Sun, 30 Jul 2023 00:24:39 +0200 Subject: [PATCH 006/586] advancing depositor state in sudo callback --- contracts/depositor/src/contract.rs | 30 +++++++------ .../tests/interchaintest/ics_test.go | 45 ++++++++++--------- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/contracts/depositor/src/contract.rs b/contracts/depositor/src/contract.rs index 931d47e1..02c84496 100644 --- a/contracts/depositor/src/contract.rs +++ b/contracts/depositor/src/contract.rs @@ -133,17 +133,19 @@ fn try_tick(deps: ExecuteDeps, env: Env, info: MessageInfo) -> NeutronResult { try_send_native_token(env, deps) }, - Err(_) => Ok(Response::default() - .add_attribute("method", "try_tick") - .add_attribute("ica_status", "not_created") - ), + Err(_) => { + Ok(Response::default() + .add_attribute("method", "try_tick") + .add_attribute("ica_status", "not_created") + ) + }, } } ContractState::VerifyNativeToken => try_verify_native_token(env, deps), ContractState::VerifyLp => try_verify_lp(env, deps), ContractState::Complete => { Ok(Response::default().add_attribute("status", "function_completed")) - } + }, } } @@ -227,10 +229,6 @@ fn try_send_native_token(env: Env, mut deps: ExecuteDeps) -> NeutronResult NeutronResult= receiver.amount { - // if funds have arrived on LP module, we advance the state and attempt to - // send the remaining funds to ICA on stride + // if funds have arrived on LP module, we advance the state CONTRACT_STATE.save(deps.storage, &ContractState::VerifyLp)?; - let ls_token_msg = try_send_ls_token(env, deps)?; return Ok(Response::default() - .add_submessage(ls_token_msg) .add_attribute("method", "try_verify_native_token") .add_attribute("receiver_balance", lper_native_token_balance.amount)); } else if env.block.time.nanos() >= pending_transfer_timeout.plus_minutes(5).nanos() { @@ -751,6 +746,11 @@ fn sudo_response(deps: ExecuteDeps, request: RequestPacket, data: Binary) -> Std // CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; // response = response.add_attribute("payload_message", "try_receive_atom_from_ica") // } + if payload.message == "try_send_native_token".to_string() { + // we advance the state machine to validation phase where we will query the balances of + // LP module to confirm that funds have arrived + CONTRACT_STATE.save(deps.storage, &ContractState::VerifyNativeToken)?; + } // update but also check that we don't update same seq_id twice ACKNOWLEDGEMENT_RESULTS.update( @@ -818,6 +818,10 @@ fn sudo_timeout(deps: ExecuteDeps, _env: Env, request: RequestPacket) -> StdResu add_error_to_queue(deps.storage, error_msg.to_string()); } + // timeout means that the ICA channel is closed + // we rollback the state to Instantiated to force reopen the channel + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + Ok(Response::default().add_attribute("method", "sudo_timeout")) } diff --git a/stride-covenant/tests/interchaintest/ics_test.go b/stride-covenant/tests/interchaintest/ics_test.go index 17a8cf74..c0ab5996 100644 --- a/stride-covenant/tests/interchaintest/ics_test.go +++ b/stride-covenant/tests/interchaintest/ics_test.go @@ -1026,8 +1026,8 @@ func TestICS(t *testing.T) { } timeouts := Timeouts{ - IcaTimeout: "30", // 30sec - IbcTransferTimeout: "45", // 45sec + IcaTimeout: "10", // sec + IbcTransferTimeout: "15", // sec } covenantMsg := CovenantInstantiateMsg{ @@ -1230,25 +1230,6 @@ func TestICS(t *testing.T) { require.EqualValues(t, int64(atomFundsToDepositor), atomBal) }) - killChannel := func(path string, channelId string, portId string) ibc.RelayerExecResult { - channelClosureCmd := []string{ - "rly", "transact", "channel-close", path, channelId, portId, - } - relayerResult := r.Exec(ctx, eRep, channelClosureCmd, nil) - return relayerResult - } - - t.Run("kill gaia-neutron channel", func(t *testing.T) { - print("\nkilling gn channel\n") - - resp := killChannel(gaiaNeutronIBCPath, neutronGaiaTransferChannelId, "transfer") - // err = testutil.WaitForBlocks(ctx, 200, atom, neutron) - - print(string(resp.Stdout)) - err = testutil.WaitForBlocks(ctx, 200, atom, neutron) - - }) - // Tick the clock until the LSer has received stATOM // and Lper has received ATOM t.Run("tick clock until LSer receives funds", func(t *testing.T) { @@ -1272,8 +1253,30 @@ func TestICS(t *testing.T) { require.NoError(t, err, "failed to query ICA balance") print("\n gaia ica atom bal: ", gaiaIcaBalance, "\n") + // switch off the relayer + err = r.StopRelayer(ctx, eRep) + require.NoError(t, err, "failed to stop relayer") + + err = testutil.WaitForBlocks(ctx, 5, atom, neutron) + require.NoError(t, err, "failed to wait for blocks") + const maxTicks = 20 tick := 1 + // do some ticks with relayer switched off + for tick <= maxTicks { + print("\n Ticking clock ", tick, " of ", maxTicks) + tickClock() + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") + + tick += 1 + } + + // now we restart the relayer and try again + err = r.StartRelayer(ctx, eRep, gaiaNeutronICSPath, gaiaNeutronIBCPath, gaiaStrideIBCPath, neutronStrideIBCPath) + require.NoError(t, err, "failed to start relayer with given paths") + + tick = 1 for tick <= maxTicks { print("\n Ticking clock ", tick, " of ", maxTicks) From ef257e26fe97d5479bff67c37f5b11de40463d58 Mon Sep 17 00:00:00 2001 From: bekauz Date: Sun, 30 Jul 2023 23:58:05 +0200 Subject: [PATCH 007/586] wip: ls timeout handling --- contracts/depositor/src/contract.rs | 52 +++-- contracts/ls/src/contract.rs | 203 ++++++++++-------- .../tests/interchaintest/ics_test.go | 115 +++++++--- 3 files changed, 223 insertions(+), 147 deletions(-) diff --git a/contracts/depositor/src/contract.rs b/contracts/depositor/src/contract.rs index 02c84496..6882ee77 100644 --- a/contracts/depositor/src/contract.rs +++ b/contracts/depositor/src/contract.rs @@ -284,6 +284,18 @@ fn try_send_ls_token(env: Env, mut deps: ExecuteDeps) -> NeutronResultneutron timeout to be equal to: + // current block + ICA timeout + ibc transfer timeout. + // this assumes the worst possible time of delivery for the ICA message + // which wraps the underlying MsgTransfer. + let msg_transfer_timeout = env + .block + .time + // we take the wrapping ICA tx timeout into account and assume the worst + .plus_seconds(ica_timeout.u64()) + // and then add the preset ibc transfer timeout + .plus_seconds(ibc_transfer_timeout.u64()); + // transfer message that will send funds from the ICA on gaia to our ICA on stride let stride_msg = MsgTransfer { source_port: "transfer".to_string(), @@ -292,11 +304,7 @@ fn try_send_ls_token(env: Env, mut deps: ExecuteDeps) -> NeutronResult NeutronResult NeutronResult> { let receiver = NATIVE_ATOM_RECEIVER.load(deps.storage)?; let lper_native_token_balance = query_lper_balance(deps.as_ref(), &receiver.address)?; - let pending_transfer_timeout = PENDING_NATIVE_TRANSFER_TIMEOUT.load(deps.storage)?; + let pending_transfer_timeout = PENDING_NATIVE_TRANSFER_TIMEOUT.may_load(deps.storage)?; if lper_native_token_balance.amount >= receiver.amount { // if funds have arrived on LP module, we advance the state CONTRACT_STATE.save(deps.storage, &ContractState::VerifyLp)?; + // nullifying any previous timeouts + PENDING_NATIVE_TRANSFER_TIMEOUT.remove(deps.storage); return Ok(Response::default() .add_attribute("method", "try_verify_native_token") - .add_attribute("receiver_balance", lper_native_token_balance.amount)); - } else if env.block.time.nanos() >= pending_transfer_timeout.plus_minutes(5).nanos() { - // funds are still not on the LP module and the msgTransfer timeout is due - // we can safely retry sending the funds again by reverting the state - // to ICACreated - CONTRACT_STATE.save(deps.storage, &ContractState::ICACreated)?; - return Ok(Response::default() - .add_attribute("method", "try_verify_native_token") - .add_attribute("status", "pending_transfer_timeout_due") - .add_attribute("contract_state", "ica_created") - ); + .add_attribute("contract_state", "verify_lp") + .add_attribute("receiver_balance", lper_native_token_balance.amount) + ) + } + + // if there is an active timeout set we validate it + if let Some(active_timeout) = pending_transfer_timeout { + if env.block.time.nanos() >= active_timeout.plus_minutes(5).nanos() { + // funds are still not on the LP module and the msgTransfer timeout is due + // we can safely retry sending the funds again by reverting the state + // to ICACreated + CONTRACT_STATE.save(deps.storage, &ContractState::ICACreated)?; + PENDING_NATIVE_TRANSFER_TIMEOUT.remove(deps.storage); + return Ok(Response::default() + .add_attribute("method", "try_verify_native_token") + .add_attribute("status", "pending_transfer_timeout_due") + .add_attribute("contract_state", "ica_created") + ) + } } // if tokens native tokens did not yet arrive to the LP module and the diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs index b76ad6d9..070f1fe9 100644 --- a/contracts/ls/src/contract.rs +++ b/contracts/ls/src/contract.rs @@ -40,7 +40,6 @@ const CONTRACT_NAME: &str = "crates.io:covenant-ls"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); const SUDO_PAYLOAD_REPLY_ID: u64 = 1u64; -const TRANSFER_REPLY_ID: u64 = 3u64; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -98,7 +97,16 @@ pub fn execute( .debug(format!("WASMDEBUG: execute: received msg: {msg:?}").as_str()); match msg { ExecuteMsg::Tick {} => try_tick(deps, env, info), - ExecuteMsg::Transfer { amount } => try_execute_transfer(deps, env, info, amount), + ExecuteMsg::Transfer { amount } => { + let state = CONTRACT_STATE.load(deps.storage)?; + match state { + ContractState::Instantiated => Ok(Response::default() + .add_attribute("method", "permisionless_transfer") + .add_attribute("status", "no_ica") + ), + ContractState::ICACreated => try_execute_transfer(deps, env, info, amount), + } + }, } } @@ -139,7 +147,7 @@ fn try_execute_transfer( amount: Uint128, ) -> NeutronResult> { let port_id = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); - let interchain_account = INTERCHAIN_ACCOUNTS.load(deps.storage, port_id)?; + let interchain_account = INTERCHAIN_ACCOUNTS.load(deps.storage, port_id.clone())?; match interchain_account { Some((address, controller_conn_id)) => { @@ -196,11 +204,20 @@ fn try_execute_transfer( fee, ); + let sudo_msg = msg_with_sudo_callback( + deps, + submit_msg, + SudoPayload { + port_id, + message: "permisionless_transfer".to_string(), + }, + )?; Ok(Response::default() + .add_submessage(sudo_msg) .add_attribute("method", "try_execute_transfer") - .add_submessage(SubMsg::reply_on_success(submit_msg, TRANSFER_REPLY_ID))) + ) } - None => Err(NeutronError::Fmt(Error)), + None => Err(NeutronError::Std(StdError::not_found("no ica found"))), } } @@ -326,85 +343,6 @@ pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> StdResult { } } -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { - deps.api.debug("WASMDEBUG: migrate"); - - match msg { - MigrateMsg::UpdateConfig { - clock_addr, - stride_neutron_ibc_transfer_channel_id, - lp_address, - neutron_stride_ibc_connection_id, - ls_denom, - ibc_fee, - ibc_transfer_timeout, - ica_timeout, - } => { - let mut resp = Response::default().add_attribute("method", "update_config"); - - if let Some(addr) = clock_addr { - let addr = deps.api.addr_validate(&addr)?; - CLOCK_ADDRESS.save(deps.storage, &addr)?; - resp = resp.add_attribute("clock_addr", addr.to_string()); - } - - if let Some(channel_id) = stride_neutron_ibc_transfer_channel_id { - STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID.save(deps.storage, &channel_id)?; - resp = resp.add_attribute("stride_neutron_ibc_transfer_channel_id", channel_id); - } - - if let Some(addr) = lp_address { - let addr = deps.api.addr_validate(&addr)?; - resp = resp.add_attribute("lp_address", addr.to_string()); - LP_ADDRESS.save(deps.storage, &addr)?; - } - - if let Some(connection_id) = neutron_stride_ibc_connection_id { - NEUTRON_STRIDE_IBC_CONNECTION_ID.save(deps.storage, &connection_id)?; - resp = resp.add_attribute("neutron_stride_ibc_connection_id", connection_id); - } - - if let Some(denom) = ls_denom { - LS_DENOM.save(deps.storage, &denom)?; - resp = resp.add_attribute("ls_denom", denom); - } - - if let Some(timeout) = ibc_transfer_timeout { - resp = resp.add_attribute("ibc_transfer_timeout", timeout); - IBC_TRANSFER_TIMEOUT.save(deps.storage, &timeout)?; - resp = resp.add_attribute("ibc_transfer_timeout", timeout); - } - - if let Some(timeout) = ica_timeout { - resp = resp.add_attribute("ica_timeout", timeout); - ICA_TIMEOUT.save(deps.storage, &timeout)?; - resp = resp.add_attribute("ica_timeout", timeout); - } - - if let Some(fee) = ibc_fee { - if fee.ack_fee.is_empty() || fee.timeout_fee.is_empty() || !fee.recv_fee.is_empty() - { - return Err(StdError::GenericErr { - msg: "invalid IbcFee".to_string(), - }); - } - IBC_FEE.save(deps.storage, &fee)?; - resp = resp.add_attribute("ibc_fee_ack", fee.ack_fee[0].to_string()); - resp = resp.add_attribute("ibc_fee_timeout", fee.timeout_fee[0].to_string()); - } - - Ok(resp) - } - MigrateMsg::UpdateCodeId { data: _ } => { - // This is a migrate message to update code id, - // Data is optional base64 that we can parse to any data we would like in the future - // let data: SomeStruct = from_binary(&data)?; - Ok(Response::default()) - } - } -} - // handler fn sudo_open_ack( deps: DepsMut, @@ -569,7 +507,14 @@ fn sudo_timeout(deps: DepsMut, _env: Env, request: RequestPacket) -> StdResult StdResult { @@ -659,7 +604,6 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> StdResult { .debug(format!("WASMDEBUG: reply msg: {msg:?}").as_str()); match msg.id { SUDO_PAYLOAD_REPLY_ID => prepare_sudo_payload(deps, env, msg), - TRANSFER_REPLY_ID => handle_transfer_reply(deps, env, msg), _ => Err(StdError::generic_err(format!( "unsupported reply message id {}", msg.id @@ -667,13 +611,82 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> StdResult { } } -pub fn handle_transfer_reply(deps: DepsMut, _env: Env, msg: Reply) -> StdResult { - deps.api.debug("WASMDEBUG: transfer reply"); - // if transfer errors, we roll back to instantiated state - // this will force an attempt to re-register the ICA - if msg.result.is_err() { - CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; - } - Ok(Response::default().add_attribute("method", "handle_transfer_reply")) -} +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { + deps.api.debug("WASMDEBUG: migrate"); + + match msg { + MigrateMsg::UpdateConfig { + clock_addr, + stride_neutron_ibc_transfer_channel_id, + lp_address, + neutron_stride_ibc_connection_id, + ls_denom, + ibc_fee, + ibc_transfer_timeout, + ica_timeout, + } => { + let mut resp = Response::default().add_attribute("method", "update_config"); + + if let Some(addr) = clock_addr { + let addr = deps.api.addr_validate(&addr)?; + CLOCK_ADDRESS.save(deps.storage, &addr)?; + resp = resp.add_attribute("clock_addr", addr.to_string()); + } + + if let Some(channel_id) = stride_neutron_ibc_transfer_channel_id { + STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID.save(deps.storage, &channel_id)?; + resp = resp.add_attribute("stride_neutron_ibc_transfer_channel_id", channel_id); + } + + if let Some(addr) = lp_address { + let addr = deps.api.addr_validate(&addr)?; + resp = resp.add_attribute("lp_address", addr.to_string()); + LP_ADDRESS.save(deps.storage, &addr)?; + } + + if let Some(connection_id) = neutron_stride_ibc_connection_id { + NEUTRON_STRIDE_IBC_CONNECTION_ID.save(deps.storage, &connection_id)?; + resp = resp.add_attribute("neutron_stride_ibc_connection_id", connection_id); + } + + if let Some(denom) = ls_denom { + LS_DENOM.save(deps.storage, &denom)?; + resp = resp.add_attribute("ls_denom", denom); + } + + if let Some(timeout) = ibc_transfer_timeout { + resp = resp.add_attribute("ibc_transfer_timeout", timeout); + IBC_TRANSFER_TIMEOUT.save(deps.storage, &timeout)?; + resp = resp.add_attribute("ibc_transfer_timeout", timeout); + } + + if let Some(timeout) = ica_timeout { + resp = resp.add_attribute("ica_timeout", timeout); + ICA_TIMEOUT.save(deps.storage, &timeout)?; + resp = resp.add_attribute("ica_timeout", timeout); + } + + if let Some(fee) = ibc_fee { + if fee.ack_fee.is_empty() || fee.timeout_fee.is_empty() || !fee.recv_fee.is_empty() + { + return Err(StdError::GenericErr { + msg: "invalid IbcFee".to_string(), + }); + } + IBC_FEE.save(deps.storage, &fee)?; + resp = resp.add_attribute("ibc_fee_ack", fee.ack_fee[0].to_string()); + resp = resp.add_attribute("ibc_fee_timeout", fee.timeout_fee[0].to_string()); + } + + Ok(resp) + } + MigrateMsg::UpdateCodeId { data: _ } => { + // This is a migrate message to update code id, + // Data is optional base64 that we can parse to any data we would like in the future + // let data: SomeStruct = from_binary(&data)?; + Ok(Response::default()) + } + } +} \ No newline at end of file diff --git a/stride-covenant/tests/interchaintest/ics_test.go b/stride-covenant/tests/interchaintest/ics_test.go index c0ab5996..c9507fbc 100644 --- a/stride-covenant/tests/interchaintest/ics_test.go +++ b/stride-covenant/tests/interchaintest/ics_test.go @@ -365,6 +365,28 @@ func TestICS(t *testing.T) { print("\n") _ = strideAtomIbcDenom + stopRelayer := func() { + print("\nstopping relayer...\n") + err = r.StopRelayer(ctx, eRep) + require.NoError(t, err, "failed to stop relayer") + + err = testutil.WaitForBlocks(ctx, 5, atom, neutron) + require.NoError(t, err, "failed to wait for blocks") + + print("\n") + } + + startRelayer := func() { + print("\nstarting relayer...\n") + err = r.StartRelayer(ctx, eRep, gaiaNeutronICSPath, gaiaNeutronIBCPath, gaiaStrideIBCPath, neutronStrideIBCPath) + require.NoError(t, err, "failed to start relayer with given paths") + + err = testutil.WaitForBlocks(ctx, 10, atom, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") + + print("\n") + } + t.Run("stride covenant tests", func(t *testing.T) { //----------------------------------------------// // Testing parameters @@ -1027,7 +1049,7 @@ func TestICS(t *testing.T) { timeouts := Timeouts{ IcaTimeout: "10", // sec - IbcTransferTimeout: "15", // sec + IbcTransferTimeout: "5", // sec } covenantMsg := CovenantInstantiateMsg{ @@ -1149,7 +1171,7 @@ func TestICS(t *testing.T) { } _, _, err := cosmosNeutron.Exec(ctx, cmd, nil) require.NoError(t, err) - + // print("\n clock response: ", string(resp), "\n") err = testutil.WaitForBlocks(ctx, 3, atom, neutron, stride) require.NoError(t, err, "failed to wait for blocks") @@ -1254,33 +1276,29 @@ func TestICS(t *testing.T) { print("\n gaia ica atom bal: ", gaiaIcaBalance, "\n") // switch off the relayer - err = r.StopRelayer(ctx, eRep) - require.NoError(t, err, "failed to stop relayer") + stopRelayer() - err = testutil.WaitForBlocks(ctx, 5, atom, neutron) - require.NoError(t, err, "failed to wait for blocks") - - const maxTicks = 20 - tick := 1 - // do some ticks with relayer switched off - for tick <= maxTicks { - print("\n Ticking clock ", tick, " of ", maxTicks) + maxTicks := 10 + // do some ticks with relayer switched off until + for i := 1; i < maxTicks; i++ { + print("\n Ticking clock ", i, " of ", maxTicks) tickClock() err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) require.NoError(t, err, "failed to wait for blocks") - - tick += 1 } // now we restart the relayer and try again - err = r.StartRelayer(ctx, eRep, gaiaNeutronICSPath, gaiaNeutronIBCPath, gaiaStrideIBCPath, neutronStrideIBCPath) - require.NoError(t, err, "failed to start relayer with given paths") + startRelayer() - tick = 1 - for tick <= maxTicks { + // assert depositor is back on instantiated state + depositorState, _, _ := tickClock() + require.EqualValues(t, "instantiated", depositorState, "depositor did not rollback the state") - print("\n Ticking clock ", tick, " of ", maxTicks) + maxTicks = 20 + for i := 1; i < maxTicks; i++ { + print("\n Ticking clock ", i, " of ", maxTicks) tickClock() + err = testutil.WaitForBlocks(ctx, 5, atom, neutron, stride) require.NoError(t, err, "failed to wait for blocks") @@ -1300,33 +1318,27 @@ func TestICS(t *testing.T) { lpAtomBalance == int64(atomFunds) { break } - tick += 1 } - // fail if we haven't transferred funds in under maxTicks - require.LessOrEqual(t, tick, maxTicks) atomICABal, err := atom.GetBalance(ctx, icaAccountAddress, atom.Config().Denom) require.NoError(t, err, "failed to query ICA balance") require.Equal(t, int64(0), atomICABal) }) t.Run("permissionlessly forward funds from Stride to LPer", func(t *testing.T) { - // Wait for a few blocks - err = testutil.WaitForBlocks(ctx, 5, atom, neutron, stride) - require.NoError(t, err, "failed to wait for blocks") + // Construct a transfer message msg := TransferExecutionMsg{ Transfer: TransferAmount{ Amount: strideRedemptionRate * atomToLiquidStake, }, } - str, err := json.Marshal(msg) + transferMsgJson, err := json.Marshal(msg) require.NoError(t, err) - // Anyone can call the tranfer function - print("\n attempting to move funds by executing message: ", string(str)) - cmd = []string{"neutrond", "tx", "wasm", "execute", lsContractAddress, - string(str), + // transfer command for permissionless transfer from stride ica to lper + transferCmd := []string{"neutrond", "tx", "wasm", "execute", lsContractAddress, + string(transferMsgJson), "--from", neutronUser.KeyName, "--gas-prices", "0.0untrn", "--gas-adjustment", `1.8`, @@ -1338,8 +1350,45 @@ func TestICS(t *testing.T) { "--keyring-backend", keyring.BackendTest, "-y", } - _, _, err = cosmosNeutron.Exec(ctx, cmd, nil) + + // switch off the relayer + stopRelayer() + // trigger sudo_timeout which rolls back the state + cosmosNeutron.Exec(ctx, transferCmd, nil) + + err = testutil.WaitForBlocks(ctx, 30, atom, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") + + maxTicks := 10 + // do some ticks with relayer switched off + for i := 1; i < maxTicks; i++ { + print("\n Ticking clock ", i, " of ", maxTicks) + tickClock() + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") + } + + // now we restart the relayer and go again + startRelayer() + + _, lsState, _ := tickClock() + require.EqualValues(t, "instantiated", lsState, "ls did not rollback the state") + + maxTicks = 20 + for i := 1; i < maxTicks; i++ { + _, lsState, _ = tickClock() + err = testutil.WaitForBlocks(ctx, 5, atom, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") + if lsState == "i_c_a_created" { + break + } + } + + // retry the transfer again + print("\n attempting permisionless transfer\n") + resp, _, err := cosmosNeutron.Exec(ctx, transferCmd, nil) require.NoError(t, err) + print("\ntransfer response: ", string(resp), "\n") err = testutil.WaitForBlocks(ctx, 10, atom, neutron, stride) require.NoError(t, err) @@ -1420,10 +1469,6 @@ func TestICS(t *testing.T) { }) - // TEST: Withdraw liquidity - // Check if LP tokens are burned - // Check if stATOM and ATOMs are returned - t.Run("holder can withdraw liquidity", func(t *testing.T) { lpTokenBal := queryLpTokenBalance(liquidityTokenAddress, holderContractAddress) print("\n holder lp token bal: ", lpTokenBal, "\n") From c6f0cdae4c1306bc503fce1348c2b57030e5ed76 Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 31 Jul 2023 00:45:28 +0200 Subject: [PATCH 008/586] wip: ls timeouts flushing packets --- stride-covenant/tests/interchaintest/ics_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stride-covenant/tests/interchaintest/ics_test.go b/stride-covenant/tests/interchaintest/ics_test.go index c9507fbc..56408437 100644 --- a/stride-covenant/tests/interchaintest/ics_test.go +++ b/stride-covenant/tests/interchaintest/ics_test.go @@ -1371,6 +1371,11 @@ func TestICS(t *testing.T) { // now we restart the relayer and go again startRelayer() + r.FlushPackets(ctx, eRep, neutronStrideIBCPath, strideNeutronChannelId) + r.FlushPackets(ctx, eRep, neutronStrideIBCPath, neutronStrideChannelId) + r.FlushAcknowledgements(ctx, eRep, neutronStrideIBCPath, strideNeutronChannelId) + r.FlushAcknowledgements(ctx, eRep, neutronStrideIBCPath, neutronStrideChannelId) + _, lsState, _ := tickClock() require.EqualValues(t, "instantiated", lsState, "ls did not rollback the state") From c011c6c85404c1586e941347c0eef7741ba041b7 Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 31 Jul 2023 23:18:11 +0200 Subject: [PATCH 009/586] wip: stride channel closures.. --- contracts/depositor/src/contract.rs | 56 +++----- contracts/depositor/src/state.rs | 8 ++ contracts/ls/src/contract.rs | 134 ++++++++++-------- contracts/ls/src/msg.rs | 9 ++ contracts/ls/src/state.rs | 8 ++ .../tests/interchaintest/ics_test.go | 96 +++++++------ 6 files changed, 177 insertions(+), 134 deletions(-) diff --git a/contracts/depositor/src/contract.rs b/contracts/depositor/src/contract.rs index 6882ee77..4398e3cd 100644 --- a/contracts/depositor/src/contract.rs +++ b/contracts/depositor/src/contract.rs @@ -10,14 +10,13 @@ use cosmwasm_std::{ use covenant_clock::helpers::verify_clock; use cw2::set_contract_version; use neutron_sdk::bindings::types::ProtobufAny; -use neutron_sdk::interchain_queries::v045::new_register_transfers_query_msg; use prost::Message; use crate::{ msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, OpenAckVersion, QueryMsg, ContractState, SudoPayload, AcknowledgementResult}, state::{ - IBC_TRANSFER_TIMEOUT, ICA_TIMEOUT, NEUTRON_ATOM_IBC_DENOM, PENDING_NATIVE_TRANSFER_TIMEOUT, + IBC_TRANSFER_TIMEOUT, ICA_TIMEOUT, NEUTRON_ATOM_IBC_DENOM, PENDING_NATIVE_TRANSFER_TIMEOUT, clear_sudo_payload, }, }; use neutron_sdk::{ @@ -43,7 +42,7 @@ type QueryDeps<'a> = Deps<'a, NeutronQuery>; type ExecuteDeps<'a> = DepsMut<'a, NeutronQuery>; const ATOM_DENOM: &str = "uatom"; -pub(crate) const INTERCHAIN_ACCOUNT_ID: &str = "ica"; +pub(crate) const INTERCHAIN_ACCOUNT_ID: &str = "gaia-ica"; pub const SUDO_PAYLOAD_REPLY_ID: u64 = 1; @@ -435,18 +434,6 @@ fn msg_with_sudo_callback>, T>( Ok(SubMsg::reply_on_success(msg, SUDO_PAYLOAD_REPLY_ID)) } -pub fn register_transfers_query( - connection_id: String, - recipient: String, - update_period: u64, - min_height: Option, -) -> NeutronResult> { - let msg = - new_register_transfers_query_msg(connection_id, recipient, update_period, min_height)?; - - Ok(Response::new().add_message(msg)) -} - #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: QueryDeps, env: Env, msg: QueryMsg) -> NeutronResult { match msg { @@ -676,6 +663,7 @@ fn sudo_open_ack( // Update the storage record associated with the interchain account. if let Ok(parsed_version) = parsed_version { + INTERCHAIN_ACCOUNTS.clear(deps.storage); INTERCHAIN_ACCOUNTS.save( deps.storage, port_id, @@ -817,28 +805,30 @@ fn sudo_timeout(deps: ExecuteDeps, _env: Env, request: RequestPacket) -> StdResu // processing. The decision is based purely on your application logic. // Please be careful because it may lead to an unexpected state changes because state might // has been changed before this call and will not be reverted because of supressed error. - let payload = read_sudo_payload(deps.storage, channel_id, seq_id).ok(); - if let Some(payload) = payload { - // update but also check that we don't update same seq_id twice - ACKNOWLEDGEMENT_RESULTS.update( - deps.storage, - (payload.port_id, seq_id), - |maybe_ack| -> StdResult { - match maybe_ack { - Some(_ack) => Err(StdError::generic_err("trying to update same seq_id")), - None => Ok(AcknowledgementResult::Timeout(payload.message)), - } - }, - )?; - } else { - let error_msg = "WASMDEBUG: Error: Unable to read sudo payload"; - deps.api.debug(error_msg); - add_error_to_queue(deps.storage, error_msg.to_string()); - } + // let payload = read_sudo_payload(deps.storage, channel_id, seq_id).ok(); + // if let Some(payload) = payload { + // // update but also check that we don't update same seq_id twice + // ACKNOWLEDGEMENT_RESULTS.update( + // deps.storage, + // (payload.port_id, seq_id), + // |maybe_ack| -> StdResult { + // // match maybe_ack { + // // Some(_ack) => Err(StdError::generic_err("trying to update same seq_id")), + // // None => Ok(AcknowledgementResult::Timeout(payload.message)), + // // } + // Ok(AcknowledgementResult::Timeout(payload.message)) + // }, + // )?; + // } else { + // let error_msg = "WASMDEBUG: Error: Unable to read sudo payload"; + // deps.api.debug(error_msg); + // add_error_to_queue(deps.storage, error_msg.to_string()); + // } // timeout means that the ICA channel is closed // we rollback the state to Instantiated to force reopen the channel CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + // clear_sudo_payload(deps.storage, channel_id, seq_id); Ok(Response::default().add_attribute("method", "sudo_timeout")) } diff --git a/contracts/depositor/src/state.rs b/contracts/depositor/src/state.rs index 350e98e6..55d287a8 100644 --- a/contracts/depositor/src/state.rs +++ b/contracts/depositor/src/state.rs @@ -101,3 +101,11 @@ pub fn save_sudo_payload( ) -> StdResult<()> { SUDO_PAYLOAD.save(store, (channel_id, seq_id), &to_vec(&payload)?) } + +pub fn clear_sudo_payload( + store: &mut dyn Storage, + channel_id: String, + seq_id: u64, +) { + SUDO_PAYLOAD.remove(store, (channel_id, seq_id)) +} \ No newline at end of file diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs index 070f1fe9..de02c736 100644 --- a/contracts/ls/src/contract.rs +++ b/contracts/ls/src/contract.rs @@ -1,5 +1,3 @@ -use std::fmt::Error; - use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; use cosmos_sdk_proto::ibc::applications::transfer::v1::MsgTransfer; use cosmos_sdk_proto::traits::Message; @@ -12,7 +10,6 @@ use cosmwasm_std::{ use covenant_clock::helpers::verify_clock; use cw2::set_contract_version; use neutron_sdk::bindings::types::ProtobufAny; -use neutron_sdk::interchain_queries::v045::new_register_transfers_query_msg; use crate::msg::{ AcknowledgementResult, ContractState, ExecuteMsg, InstantiateMsg, MigrateMsg, OpenAckVersion, @@ -34,7 +31,7 @@ use neutron_sdk::{ NeutronError, NeutronResult, }; -const INTERCHAIN_ACCOUNT_ID: &str = "ica"; +const INTERCHAIN_ACCOUNT_ID: &str = "stride-ica"; const CONTRACT_NAME: &str = "crates.io:covenant-ls"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -98,13 +95,25 @@ pub fn execute( match msg { ExecuteMsg::Tick {} => try_tick(deps, env, info), ExecuteMsg::Transfer { amount } => { - let state = CONTRACT_STATE.load(deps.storage)?; - match state { - ContractState::Instantiated => Ok(Response::default() - .add_attribute("method", "permisionless_transfer") - .add_attribute("status", "no_ica") - ), - ContractState::ICACreated => try_execute_transfer(deps, env, info, amount), + // let state = CONTRACT_STATE.load(deps.storage)?; + // match state { + // ContractState::Instantiated => Ok(Response::default() + // .add_attribute("method", "permisionless_transfer") + // .add_attribute("status", "no_ica") + // ), + // ContractState::ICACreated => try_execute_transfer(deps, env, info, amount), + // } + let ica_address = get_ica(deps.as_ref(), &env, INTERCHAIN_ACCOUNT_ID); + match ica_address { + Ok((_, _)) => { + try_execute_transfer(deps, env, info, amount) + }, + Err(_) => { + Ok(Response::default() + .add_attribute("method", "try_permisionless_transfer") + .add_attribute("ica_status", "not_created") + ) + }, } }, } @@ -231,18 +240,6 @@ fn msg_with_sudo_callback>, T>( Ok(SubMsg::reply_on_success(msg, SUDO_PAYLOAD_REPLY_ID)) } -pub fn register_transfers_query( - connection_id: String, - recipient: String, - update_period: u64, - min_height: Option, -) -> NeutronResult> { - let msg = - new_register_transfers_query_msg(connection_id, recipient, update_period, min_height)?; - - Ok(Response::new().add_message(msg)) -} - #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult { match msg { @@ -264,6 +261,11 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult Ok(to_binary(&IBC_TRANSFER_TIMEOUT.may_load(deps.storage)?)?) } QueryMsg::LsDenom {} => Ok(to_binary(&LS_DENOM.may_load(deps.storage)?)?), + QueryMsg::AcknowledgementResult { + interchain_account_id, + sequence_id, + } => query_acknowledgement_result(deps, env, interchain_account_id, sequence_id), + QueryMsg::ErrorsQueue {} => query_errors_queue(deps), } } @@ -359,6 +361,7 @@ fn sudo_open_ack( // Update the storage record associated with the interchain account. if let Ok(parsed_version) = parsed_version { + INTERCHAIN_ACCOUNTS.clear(deps.storage); INTERCHAIN_ACCOUNTS.save( deps.storage, port_id, @@ -466,19 +469,23 @@ fn sudo_timeout(deps: DepsMut, _env: Env, request: RequestPacket) -> StdResult StdResult StdResult { - match maybe_ack { - Some(_ack) => Err(StdError::generic_err("trying to update same seq_id")), - None => Ok(AcknowledgementResult::Timeout(payload.message)), - } - }, - )?; - } else { - let error_msg = "WASMDEBUG: Error: Unable to read sudo payload"; - deps.api.debug(error_msg); - add_error_to_queue(deps.storage, error_msg.to_string()); - } + // let payload = read_sudo_payload(deps.storage, channel_id, seq_id).ok(); + // if let Some(payload) = payload { + // // update but also check that we don't update same seq_id twice + // ACKNOWLEDGEMENT_RESULTS.update( + // deps.storage, + // (payload.port_id, seq_id), + // |maybe_ack| -> StdResult { + // match maybe_ack { + // Some(_ack) => Err(StdError::generic_err("trying to update same seq_id")), + // None => Ok(AcknowledgementResult::Timeout(payload.message)), + // } + // // Ok(AcknowledgementResult::Timeout(payload.message)) + // }, + // )?; + // } + // else { + // let error_msg = "WASMDEBUG: Error: Unable to read sudo payload"; + // deps.api.debug(error_msg); + // add_error_to_queue(deps.storage, error_msg.to_string()); + // } // timeout here means channel is closed. // we rollback the state to Instantiated to force reopen the channel. - CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; - - Ok(Response::default() - .add_attribute("method", "sudo_timeout") - .add_attribute("contract_state", "instantiated") - ) + // clear_sudo_payload(deps.storage, channel_id, seq_id); + // Ok(Response::default() + // .add_attribute("method", "sudo_timeout") + // .add_attribute("contract_state", "instantiated") + // ) + // Err(StdError::generic) } fn sudo_error(deps: DepsMut, request: RequestPacket, details: String) -> StdResult { @@ -564,9 +573,12 @@ fn sudo_error(deps: DepsMut, request: RequestPacket, details: String) -> StdResu } // prepare_sudo_payload is called from reply handler -// The method is used to extract sequence id and channel from SubmitTxResponse to process sudo payload defined in msg_with_sudo_callback later in Sudo handler. -// Such flow msg_with_sudo_callback() -> reply() -> prepare_sudo_payload() -> sudo() allows you "attach" some payload to your SubmitTx message -// and process this payload when an acknowledgement for the SubmitTx message is received in Sudo handler +// The method is used to extract sequence id and channel from SubmitTxResponse to +// process sudo payload defined in msg_with_sudo_callback later in Sudo handler. +// Such flow msg_with_sudo_callback() -> reply() -> prepare_sudo_payload() -> sudo() +// allows you "attach" some payload to your SubmitTx message +// and process this payload when an acknowledgement for the SubmitTx message +// is received in Sudo handler fn prepare_sudo_payload(mut deps: DepsMut, _env: Env, msg: Reply) -> StdResult { let payload = read_reply_payload(deps.storage)?; let resp: MsgSubmitTxResponse = serde_json_wasm::from_slice( diff --git a/contracts/ls/src/msg.rs b/contracts/ls/src/msg.rs index 8b476313..dc9f9752 100644 --- a/contracts/ls/src/msg.rs +++ b/contracts/ls/src/msg.rs @@ -103,6 +103,15 @@ pub enum QueryMsg { IbcTransferTimeout {}, #[returns(String)] LsDenom {}, + // this query returns acknowledgement result after interchain transaction + #[returns(Option)] + AcknowledgementResult { + interchain_account_id: String, + sequence_id: u64, + }, + // this query returns non-critical errors list + #[returns(Vec<(Vec, String)>)] + ErrorsQueue {}, } #[cw_serde] diff --git a/contracts/ls/src/state.rs b/contracts/ls/src/state.rs index b635bec8..2f3c0ea2 100644 --- a/contracts/ls/src/state.rs +++ b/contracts/ls/src/state.rs @@ -81,3 +81,11 @@ pub fn save_sudo_payload( ) -> StdResult<()> { SUDO_PAYLOAD.save(store, (channel_id, seq_id), &to_vec(&payload)?) } + +pub fn clear_sudo_payload( + store: &mut dyn Storage, + channel_id: String, + seq_id: u64, +) { + SUDO_PAYLOAD.remove(store, (channel_id, seq_id)) +} \ No newline at end of file diff --git a/stride-covenant/tests/interchaintest/ics_test.go b/stride-covenant/tests/interchaintest/ics_test.go index 56408437..263dca70 100644 --- a/stride-covenant/tests/interchaintest/ics_test.go +++ b/stride-covenant/tests/interchaintest/ics_test.go @@ -370,7 +370,7 @@ func TestICS(t *testing.T) { err = r.StopRelayer(ctx, eRep) require.NoError(t, err, "failed to stop relayer") - err = testutil.WaitForBlocks(ctx, 5, atom, neutron) + err = testutil.WaitForBlocks(ctx, 5, atom, neutron, stride) require.NoError(t, err, "failed to wait for blocks") print("\n") @@ -1172,7 +1172,7 @@ func TestICS(t *testing.T) { _, _, err := cosmosNeutron.Exec(ctx, cmd, nil) require.NoError(t, err) // print("\n clock response: ", string(resp), "\n") - err = testutil.WaitForBlocks(ctx, 3, atom, neutron, stride) + err = testutil.WaitForBlocks(ctx, 10, atom, neutron, stride) require.NoError(t, err, "failed to wait for blocks") var response ContractStateQueryResponse @@ -1196,6 +1196,33 @@ func TestICS(t *testing.T) { return currentDepositorState, currentLsState, currentLpState } + getLsPermisionlessTransferMsg := func(amount uint64) []string { + // Construct a transfer message + msg := TransferExecutionMsg{ + Transfer: TransferAmount{ + Amount: amount, + }, + } + transferMsgJson, err := json.Marshal(msg) + require.NoError(t, err) + + // transfer command for permissionless transfer from stride ica to lper + transferCmd := []string{"neutrond", "tx", "wasm", "execute", lsContractAddress, + string(transferMsgJson), + "--from", neutronUser.KeyName, + "--gas-prices", "0.0untrn", + "--gas-adjustment", `1.8`, + "--output", "json", + "--home", "/var/cosmos-chain/neutron-2", + "--node", neutron.GetRPCAddress(), + "--chain-id", neutron.Config().ChainID, + "--gas", "auto", + "--keyring-backend", keyring.BackendTest, + "-y", + } + return transferCmd + } + // Tick the clock until the depositor has created i_c_a t.Run("tick clock until depositor and Ls create ICA", func(t *testing.T) { const maxTicks = 20 @@ -1277,6 +1304,10 @@ func TestICS(t *testing.T) { // switch off the relayer stopRelayer() + transferCmd := getLsPermisionlessTransferMsg(strideRedemptionRate * atomToLiquidStake / 2) + cosmosNeutron.Exec(ctx, transferCmd, nil) + err = testutil.WaitForBlocks(ctx, 5, atom, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") maxTicks := 10 // do some ticks with relayer switched off until @@ -1326,60 +1357,45 @@ func TestICS(t *testing.T) { }) t.Run("permissionlessly forward funds from Stride to LPer", func(t *testing.T) { - - // Construct a transfer message - msg := TransferExecutionMsg{ - Transfer: TransferAmount{ - Amount: strideRedemptionRate * atomToLiquidStake, - }, - } - transferMsgJson, err := json.Marshal(msg) - require.NoError(t, err) - - // transfer command for permissionless transfer from stride ica to lper - transferCmd := []string{"neutrond", "tx", "wasm", "execute", lsContractAddress, - string(transferMsgJson), - "--from", neutronUser.KeyName, - "--gas-prices", "0.0untrn", - "--gas-adjustment", `1.8`, - "--output", "json", - "--home", "/var/cosmos-chain/neutron-2", - "--node", neutron.GetRPCAddress(), - "--chain-id", neutron.Config().ChainID, - "--gas", "auto", - "--keyring-backend", keyring.BackendTest, - "-y", - } + transferCmd := getLsPermisionlessTransferMsg(strideRedemptionRate * atomToLiquidStake / 2) + cosmosNeutron.Exec(ctx, transferCmd, nil) + err = testutil.WaitForBlocks(ctx, 5, atom, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") // switch off the relayer stopRelayer() // trigger sudo_timeout which rolls back the state cosmosNeutron.Exec(ctx, transferCmd, nil) - err = testutil.WaitForBlocks(ctx, 30, atom, neutron, stride) + err = testutil.WaitForBlocks(ctx, 40, atom, neutron, stride) require.NoError(t, err, "failed to wait for blocks") - maxTicks := 10 - // do some ticks with relayer switched off - for i := 1; i < maxTicks; i++ { - print("\n Ticking clock ", i, " of ", maxTicks) - tickClock() - err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) - require.NoError(t, err, "failed to wait for blocks") - } + // maxTicks := 10 + // // do some ticks with relayer switched off + // for i := 1; i < maxTicks; i++ { + // print("\n Ticking clock ", i, " of ", maxTicks) + // tickClock() + // err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) + // require.NoError(t, err, "failed to wait for blocks") + // } // now we restart the relayer and go again startRelayer() + err = testutil.WaitForBlocks(ctx, 30, atom, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") + r.FlushPackets(ctx, eRep, neutronStrideIBCPath, strideNeutronChannelId) r.FlushPackets(ctx, eRep, neutronStrideIBCPath, neutronStrideChannelId) r.FlushAcknowledgements(ctx, eRep, neutronStrideIBCPath, strideNeutronChannelId) r.FlushAcknowledgements(ctx, eRep, neutronStrideIBCPath, neutronStrideChannelId) + err = testutil.WaitForBlocks(ctx, 15, atom, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") _, lsState, _ := tickClock() require.EqualValues(t, "instantiated", lsState, "ls did not rollback the state") - maxTicks = 20 + maxTicks := 20 for i := 1; i < maxTicks; i++ { _, lsState, _ = tickClock() err = testutil.WaitForBlocks(ctx, 5, atom, neutron, stride) @@ -1390,10 +1406,10 @@ func TestICS(t *testing.T) { } // retry the transfer again - print("\n attempting permisionless transfer\n") - resp, _, err := cosmosNeutron.Exec(ctx, transferCmd, nil) - require.NoError(t, err) - print("\ntransfer response: ", string(resp), "\n") + // print("\n attempting permisionless transfer\n") + // resp, _, err := cosmosNeutron.Exec(ctx, transferCmd, nil) + // require.NoError(t, err) + // print("\ntransfer response: ", string(resp), "\n") err = testutil.WaitForBlocks(ctx, 10, atom, neutron, stride) require.NoError(t, err) From 2b974306498832f8ffe86b8a85ef324d628fb12d Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 4 Aug 2023 21:10:16 +0200 Subject: [PATCH 010/586] cleanup sudo_response handlers in ls & depositor --- contracts/depositor/src/contract.rs | 168 ++--------------- contracts/ls/src/contract.rs | 177 ++---------------- stride-covenant/justfile | 2 +- .../tests/interchaintest/ics_test.go | 61 +++--- 4 files changed, 57 insertions(+), 351 deletions(-) diff --git a/contracts/depositor/src/contract.rs b/contracts/depositor/src/contract.rs index 4398e3cd..bc62d2a0 100644 --- a/contracts/depositor/src/contract.rs +++ b/contracts/depositor/src/contract.rs @@ -663,7 +663,6 @@ fn sudo_open_ack( // Update the storage record associated with the interchain account. if let Ok(parsed_version) = parsed_version { - INTERCHAIN_ACCOUNTS.clear(deps.storage); INTERCHAIN_ACCOUNTS.save( deps.storage, port_id, @@ -679,202 +678,59 @@ fn sudo_open_ack( } fn sudo_response(deps: ExecuteDeps, request: RequestPacket, data: Binary) -> StdResult { - let response = Response::default().add_attribute("method", "sudo_response"); deps.api .debug(format!("WASMDEBUG: sudo_response: sudo received: {request:?} {data:?}").as_str()); - // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. - // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE - // FOR LATER INSPECTION. - // In this particular case, we return an error because not having the sequence id - // in the request value implies that a fatal error occurred on Neutron side. + // either of these errors will close the channel let seq_id = request .sequence .ok_or_else(|| StdError::generic_err("sequence not found"))?; - // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. - // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE - // FOR LATER INSPECTION. - // In this particular case, we return an error because not having the sequence id - // in the request value implies that a fatal error occurred on Neutron side. let channel_id = request .source_channel .ok_or_else(|| StdError::generic_err("channel_id not found"))?; - // NOTE: NO ERROR IS RETURNED HERE. THE CHANNEL LIVES ON. - // In this particular example, this is a matter of developer's choice. Not being able to read - // the payload here means that there was a problem with the contract while submitting an - // interchain transaction. You can decide that this is not worth killing the channel, - // write an error log and / or save the acknowledgement to an errors queue for later manual - // processing. The decision is based purely on your application logic. let payload = read_sudo_payload(deps.storage, channel_id, seq_id).ok(); - if payload.is_none() { - let error_msg = "WASMDEBUG: Error: Unable to read sudo payload"; - deps.api.debug(error_msg); - add_error_to_queue(deps.storage, error_msg.to_string()); - return Ok(Response::default()); - } - - deps.api - .debug(format!("WASMDEBUG: sudo_response: sudo payload: {payload:?}").as_str()); - - // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. - // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE - // FOR LATER INSPECTION. - // In this particular case, we return an error because not being able to parse this data - // that a fatal error occurred on Neutron side, or that the remote chain sent us unexpected data. - // Both cases require immediate attention. - let parsed_data = decode_acknowledgement_response(data)?; - - let mut item_types = vec![]; - for item in parsed_data { - let item_type = item.msg_type.as_str(); - item_types.push(item_type.to_string()); - match item_type { - "/ibc.applications.transfer.v1.MsgTransfer" => { - deps.api - .debug(format!("MsgTransfer response: {:?}", item.data).as_str()); - } - _ => { - deps.api.debug( - format!("This type of acknowledgement is not implemented: {payload:?}") - .as_str(), - ); - } - } - } if let Some(payload) = payload { - // if payload.message == "try_send_funds" { - // CONTRACT_STATE.save(deps.storage, &ContractState::FundsSent)?; - // response = response.add_attribute("payload_message", "try_send_funds") - // } else if payload.message == "try_receive_atom_from_ica" { - // CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; - // response = response.add_attribute("payload_message", "try_receive_atom_from_ica") - // } if payload.message == "try_send_native_token".to_string() { // we advance the state machine to validation phase where we will query the balances of // LP module to confirm that funds have arrived CONTRACT_STATE.save(deps.storage, &ContractState::VerifyNativeToken)?; } - - // update but also check that we don't update same seq_id twice - ACKNOWLEDGEMENT_RESULTS.update( - deps.storage, - (payload.port_id, seq_id), - |maybe_ack| -> StdResult { - match maybe_ack { - Some(_ack) => Err(StdError::generic_err("trying to update same seq_id")), - None => Ok(AcknowledgementResult::Success(item_types)), - } - }, - )?; } - Ok(response) + Ok(Response::default() + .add_attribute("method", "sudo_response") + ) } fn sudo_timeout(deps: ExecuteDeps, _env: Env, request: RequestPacket) -> StdResult { deps.api .debug(format!("WASMDEBUG: sudo timeout request: {request:?}").as_str()); - // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. - // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE - // FOR LATER INSPECTION. - // In this particular case, we return an error because not having the sequence id - // in the request value implies that a fatal error occurred on Neutron side. - let seq_id = request - .sequence - .ok_or_else(|| StdError::generic_err("sequence not found"))?; - - // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. - // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE - // FOR LATER INSPECTION. - // In this particular case, we return an error because not having the sequence id - // in the request value implies that a fatal error occurred on Neutron side. - let channel_id = request - .source_channel - .ok_or_else(|| StdError::generic_err("channel_id not found"))?; - - // update but also check that we don't update same seq_id twice - // NOTE: NO ERROR IS RETURNED HERE. THE CHANNEL LIVES ON. - // In this particular example, this is a matter of developer's choice. Not being able to read - // the payload here means that there was a problem with the contract while submitting an - // interchain transaction. You can decide that this is not worth killing the channel, - // write an error log and / or save the acknowledgement to an errors queue for later manual - // processing. The decision is based purely on your application logic. - // Please be careful because it may lead to an unexpected state changes because state might - // has been changed before this call and will not be reverted because of supressed error. - // let payload = read_sudo_payload(deps.storage, channel_id, seq_id).ok(); - // if let Some(payload) = payload { - // // update but also check that we don't update same seq_id twice - // ACKNOWLEDGEMENT_RESULTS.update( - // deps.storage, - // (payload.port_id, seq_id), - // |maybe_ack| -> StdResult { - // // match maybe_ack { - // // Some(_ack) => Err(StdError::generic_err("trying to update same seq_id")), - // // None => Ok(AcknowledgementResult::Timeout(payload.message)), - // // } - // Ok(AcknowledgementResult::Timeout(payload.message)) - // }, - // )?; - // } else { - // let error_msg = "WASMDEBUG: Error: Unable to read sudo payload"; - // deps.api.debug(error_msg); - // add_error_to_queue(deps.storage, error_msg.to_string()); - // } - - // timeout means that the ICA channel is closed - // we rollback the state to Instantiated to force reopen the channel + // revert the state to Instantiated to force re-creation of ICA CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; - // clear_sudo_payload(deps.storage, channel_id, seq_id); - Ok(Response::default().add_attribute("method", "sudo_timeout")) + // returning Ok as this is anticipated. channel is already closed. + Ok(Response::default()) } fn sudo_error(deps: ExecuteDeps, request: RequestPacket, details: String) -> StdResult { deps.api - .debug(format!("WASMDEBUG: sudo error: {details}").as_str()); + .debug(format!("WASMDEBUG: sudo error: {details}").as_str()); + deps.api .debug(format!("WASMDEBUG: request packet: {request:?}").as_str()); - // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. - // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE - // FOR LATER INSPECTION. - // In this particular case, we return an error because not having the sequence id - // in the request value implies that a fatal error occurred on Neutron side. - let seq_id = request + // either of these errors will close the channel + request .sequence .ok_or_else(|| StdError::generic_err("sequence not found"))?; - // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. - // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE - // FOR LATER INSPECTION. - // In this particular case, we return an error because not having the sequence id - // in the request value implies that a fatal error occurred on Neutron side. - let channel_id = request + request .source_channel .ok_or_else(|| StdError::generic_err("channel_id not found"))?; - let payload = read_sudo_payload(deps.storage, channel_id, seq_id).ok(); - - if let Some(payload) = payload { - // update but also check that we don't update same seq_id twice - ACKNOWLEDGEMENT_RESULTS.update( - deps.storage, - (payload.port_id, seq_id), - |maybe_ack| -> StdResult { - match maybe_ack { - Some(_ack) => Err(StdError::generic_err("trying to update same seq_id")), - None => Ok(AcknowledgementResult::Error((payload.message, details))), - } - }, - )?; - } else { - let error_msg = "WASMDEBUG: Error: Unable to read sudo payload"; - deps.api.debug(error_msg); - add_error_to_queue(deps.storage, error_msg.to_string()); - } Ok(Response::default().add_attribute("method", "sudo_error")) } diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs index de02c736..fb003445 100644 --- a/contracts/ls/src/contract.rs +++ b/contracts/ls/src/contract.rs @@ -361,7 +361,6 @@ fn sudo_open_ack( // Update the storage record associated with the interchain account. if let Ok(parsed_version) = parsed_version { - INTERCHAIN_ACCOUNTS.clear(deps.storage); INTERCHAIN_ACCOUNTS.save( deps.storage, port_id, @@ -370,8 +369,11 @@ fn sudo_open_ack( parsed_version.controller_connection_id, )), )?; + // we advance the state now that the channel is established CONTRACT_STATE.save(deps.storage, &ContractState::ICACreated)?; - return Ok(Response::default().add_attribute("method", "sudo_open_ack")); + return Ok(Response::default() + .add_attribute("method", "sudo_open_ack") + ) } Err(StdError::generic_err("Can't parse counterparty_version")) } @@ -380,83 +382,15 @@ fn sudo_response(deps: DepsMut, request: RequestPacket, data: Binary) -> StdResu deps.api .debug(format!("WASMDEBUG: sudo_response: sudo received: {request:?} {data:?}",).as_str()); - // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. - // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE - // FOR LATER INSPECTION. - // In this particular case, we return an error because not having the sequence id - // in the request value implies that a fatal error occurred on Neutron side. - let seq_id = request + // either of these errors will close the channel + request .sequence .ok_or_else(|| StdError::generic_err("sequence not found"))?; - // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. - // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE - // FOR LATER INSPECTION. - // In this particular case, we return an error because not having the sequence id - // in the request value implies that a fatal error occurred on Neutron side. - let channel_id = request + request .source_channel .ok_or_else(|| StdError::generic_err("channel_id not found"))?; - // NOTE: NO ERROR IS RETURNED HERE. THE CHANNEL LIVES ON. - // In this particular example, this is a matter of developer's choice. Not being able to read - // the payload here means that there was a problem with the contract while submitting an - // interchain transaction. You can decide that this is not worth killing the channel, - // write an error log and / or save the acknowledgement to an errors queue for later manual - // processing. The decision is based purely on your application logic. - let payload = read_sudo_payload(deps.storage, channel_id, seq_id).ok(); - if payload.is_none() { - let error_msg = "WASMDEBUG: Error: Unable to read sudo payload"; - deps.api.debug(error_msg); - add_error_to_queue(deps.storage, error_msg.to_string()); - return Ok(Response::default() - .add_attribute("method", "sudo_open_ack") - .add_attribute("error", "no_payload")); - } - - deps.api - .debug(format!("WASMDEBUG: sudo_response: sudo payload: {payload:?}").as_str()); - - // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. - // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE - // FOR LATER INSPECTION. - // In this particular case, we return an error because not being able to parse this data - // that a fatal error occurred on Neutron side, or that the remote chain sent us unexpected data. - // Both cases require immediate attention. - let parsed_data = decode_acknowledgement_response(data)?; - - let mut item_types = vec![]; - for item in parsed_data { - let item_type = item.msg_type.as_str(); - item_types.push(item_type.to_string()); - match item_type { - "/ibc.applications.transfer.v1.MsgTransfer" => { - deps.api - .debug(format!("MsgTransfer response: {:?}", item.data).as_str()); - } - _ => { - deps.api.debug( - format!("This type of acknowledgement is not implemented: {payload:?}") - .as_str(), - ); - } - } - } - - if let Some(payload) = payload { - // update but also check that we don't update same seq_id twice - ACKNOWLEDGEMENT_RESULTS.update( - deps.storage, - (payload.port_id, seq_id), - |maybe_ack| -> StdResult { - match maybe_ack { - Some(_ack) => Err(StdError::generic_err("trying to update same seq_id")), - None => Ok(AcknowledgementResult::Success(item_types)), - } - }, - )?; - } - Ok(Response::default().add_attribute("method", "sudo_response")) } @@ -464,66 +398,11 @@ fn sudo_timeout(deps: DepsMut, _env: Env, request: RequestPacket) -> StdResult StdResult { - // match maybe_ack { - // Some(_ack) => Err(StdError::generic_err("trying to update same seq_id")), - // None => Ok(AcknowledgementResult::Timeout(payload.message)), - // } - // // Ok(AcknowledgementResult::Timeout(payload.message)) - // }, - // )?; - // } - // else { - // let error_msg = "WASMDEBUG: Error: Unable to read sudo payload"; - // deps.api.debug(error_msg); - // add_error_to_queue(deps.storage, error_msg.to_string()); - // } - - // timeout here means channel is closed. - // we rollback the state to Instantiated to force reopen the channel. - // clear_sudo_payload(deps.storage, channel_id, seq_id); - // Ok(Response::default() - // .add_attribute("method", "sudo_timeout") - // .add_attribute("contract_state", "instantiated") - // ) - // Err(StdError::generic) + + // returning Ok as this is anticipated. channel is already closed. + Ok(Response::default()) } fn sudo_error(deps: DepsMut, request: RequestPacket, details: String) -> StdResult { @@ -532,42 +411,14 @@ fn sudo_error(deps: DepsMut, request: RequestPacket, details: String) -> StdResu deps.api .debug(format!("WASMDEBUG: request packet: {request:?}").as_str()); - // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. - // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE - // FOR LATER INSPECTION. - // In this particular case, we return an error because not having the sequence id - // in the request value implies that a fatal error occurred on Neutron side. - let seq_id = request + // either of these errors will close the channel + request .sequence .ok_or_else(|| StdError::generic_err("sequence not found"))?; - // WARNING: RETURNING THIS ERROR CLOSES THE CHANNEL. - // AN ALTERNATIVE IS TO MAINTAIN AN ERRORS QUEUE AND PUT THE FAILED REQUEST THERE - // FOR LATER INSPECTION. - // In this particular case, we return an error because not having the sequence id - // in the request value implies that a fatal error occurred on Neutron side. - let channel_id = request + request .source_channel .ok_or_else(|| StdError::generic_err("channel_id not found"))?; - let payload = read_sudo_payload(deps.storage, channel_id, seq_id).ok(); - - if let Some(payload) = payload { - // update but also check that we don't update same seq_id twice - ACKNOWLEDGEMENT_RESULTS.update( - deps.storage, - (payload.port_id, seq_id), - |maybe_ack| -> StdResult { - match maybe_ack { - Some(_ack) => Err(StdError::generic_err("trying to update same seq_id")), - None => Ok(AcknowledgementResult::Error((payload.message, details))), - } - }, - )?; - } else { - let error_msg = "WASMDEBUG: Error: Unable to read sudo payload"; - deps.api.debug(error_msg); - add_error_to_queue(deps.storage, error_msg.to_string()); - } Ok(Response::default().add_attribute("method", "sudo_error")) } diff --git a/stride-covenant/justfile b/stride-covenant/justfile index f4532d10..71603cfe 100644 --- a/stride-covenant/justfile +++ b/stride-covenant/justfile @@ -39,7 +39,7 @@ simtest: optimize cp -R ./../artifacts/*.wasm tests/interchaintest/wasms go clean -testcache - cd tests/interchaintest/ && go test -timeout 20m -v ./... + cd tests/interchaintest/ && go test -timeout 30m -v ./... ictest: cd tests/interchaintest/ && go test -timeout 20m -v ./... \ No newline at end of file diff --git a/stride-covenant/tests/interchaintest/ics_test.go b/stride-covenant/tests/interchaintest/ics_test.go index 263dca70..0d1b97fb 100644 --- a/stride-covenant/tests/interchaintest/ics_test.go +++ b/stride-covenant/tests/interchaintest/ics_test.go @@ -1304,10 +1304,6 @@ func TestICS(t *testing.T) { // switch off the relayer stopRelayer() - transferCmd := getLsPermisionlessTransferMsg(strideRedemptionRate * atomToLiquidStake / 2) - cosmosNeutron.Exec(ctx, transferCmd, nil) - err = testutil.WaitForBlocks(ctx, 5, atom, neutron, stride) - require.NoError(t, err, "failed to wait for blocks") maxTicks := 10 // do some ticks with relayer switched off until @@ -1354,21 +1350,21 @@ func TestICS(t *testing.T) { atomICABal, err := atom.GetBalance(ctx, icaAccountAddress, atom.Config().Denom) require.NoError(t, err, "failed to query ICA balance") require.Equal(t, int64(0), atomICABal) + err = testutil.WaitForBlocks(ctx, 5, atom, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") }) t.Run("permissionlessly forward funds from Stride to LPer", func(t *testing.T) { - transferCmd := getLsPermisionlessTransferMsg(strideRedemptionRate * atomToLiquidStake / 2) + transferCmd := getLsPermisionlessTransferMsg(strideRedemptionRate * atomToLiquidStake) cosmosNeutron.Exec(ctx, transferCmd, nil) - err = testutil.WaitForBlocks(ctx, 5, atom, neutron, stride) - require.NoError(t, err, "failed to wait for blocks") // switch off the relayer - stopRelayer() + // stopRelayer() // trigger sudo_timeout which rolls back the state - cosmosNeutron.Exec(ctx, transferCmd, nil) + // cosmosNeutron.Exec(ctx, transferCmd, nil) - err = testutil.WaitForBlocks(ctx, 40, atom, neutron, stride) - require.NoError(t, err, "failed to wait for blocks") + // err = testutil.WaitForBlocks(ctx, 40, atom, neutron, stride) + // require.NoError(t, err, "failed to wait for blocks") // maxTicks := 10 // // do some ticks with relayer switched off @@ -1380,30 +1376,30 @@ func TestICS(t *testing.T) { // } // now we restart the relayer and go again - startRelayer() + // startRelayer() - err = testutil.WaitForBlocks(ctx, 30, atom, neutron, stride) - require.NoError(t, err, "failed to wait for blocks") + // err = testutil.WaitForBlocks(ctx, 30, atom, neutron, stride) + // require.NoError(t, err, "failed to wait for blocks") - r.FlushPackets(ctx, eRep, neutronStrideIBCPath, strideNeutronChannelId) - r.FlushPackets(ctx, eRep, neutronStrideIBCPath, neutronStrideChannelId) - r.FlushAcknowledgements(ctx, eRep, neutronStrideIBCPath, strideNeutronChannelId) - r.FlushAcknowledgements(ctx, eRep, neutronStrideIBCPath, neutronStrideChannelId) - err = testutil.WaitForBlocks(ctx, 15, atom, neutron, stride) - require.NoError(t, err, "failed to wait for blocks") + // r.FlushPackets(ctx, eRep, neutronStrideIBCPath, strideNeutronChannelId) + // r.FlushPackets(ctx, eRep, neutronStrideIBCPath, neutronStrideChannelId) + // r.FlushAcknowledgements(ctx, eRep, neutronStrideIBCPath, strideNeutronChannelId) + // r.FlushAcknowledgements(ctx, eRep, neutronStrideIBCPath, neutronStrideChannelId) + // err = testutil.WaitForBlocks(ctx, 15, atom, neutron, stride) + // require.NoError(t, err, "failed to wait for blocks") - _, lsState, _ := tickClock() - require.EqualValues(t, "instantiated", lsState, "ls did not rollback the state") + // _, lsState, _ := tickClock() + // // require.EqualValues(t, "instantiated", lsState, "ls did not rollback the state") - maxTicks := 20 - for i := 1; i < maxTicks; i++ { - _, lsState, _ = tickClock() - err = testutil.WaitForBlocks(ctx, 5, atom, neutron, stride) - require.NoError(t, err, "failed to wait for blocks") - if lsState == "i_c_a_created" { - break - } - } + // maxTicks := 20 + // for i := 1; i < maxTicks; i++ { + // _, lsState, _ = tickClock() + // err = testutil.WaitForBlocks(ctx, 5, atom, neutron, stride) + // require.NoError(t, err, "failed to wait for blocks") + // if lsState == "i_c_a_created" { + // break + // } + // } // retry the transfer again // print("\n attempting permisionless transfer\n") @@ -1422,6 +1418,9 @@ func TestICS(t *testing.T) { require.NoError(t, err, "failed to query ICA balance") print("\n lp statom bal: ", lpStatomBalance, "\n") + // err = testutil.WaitForBlocks(ctx, 10, atom, neutron, stride) + // require.NoError(t, err) + require.Equal(t, int64(0), strideICABal) require.Equal(t, int64(strideRedemptionRate*atomToLiquidStake), lpStatomBalance) From ab1f9c0dbf128f76d86294d628a88a3486d00e2e Mon Sep 17 00:00:00 2001 From: bekauz Date: Sat, 5 Aug 2023 00:34:07 +0200 Subject: [PATCH 011/586] skipping automatic path creation on ictest --- .../interchaintest/connection_helpers.go | 19 +++++ .../tests/interchaintest/ics_test.go | 71 ++++++++++++++++++- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/stride-covenant/tests/interchaintest/connection_helpers.go b/stride-covenant/tests/interchaintest/connection_helpers.go index 8a3bbbf2..c769f563 100644 --- a/stride-covenant/tests/interchaintest/connection_helpers.go +++ b/stride-covenant/tests/interchaintest/connection_helpers.go @@ -1,11 +1,30 @@ package ibc_test import ( + "context" "errors" + "testing" "github.com/strangelove-ventures/interchaintest/v3/ibc" + "github.com/strangelove-ventures/interchaintest/v3/testreporter" + "github.com/stretchr/testify/require" ) +func generatePath(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, chainAId string, chainBId string, path string) { + err := r.GeneratePath(ctx, eRep, chainAId, chainBId, path) + require.NoError(t, err) +} + +func generateClient(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, path string) { + err := r.CreateClients(ctx, eRep, path, ibc.CreateClientOptions{TrustingPeriod: "330h"}) + require.NoError(t, err) +} + +func generateConnections(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, path string) { + err := r.CreateConnections(ctx, eRep, path) + require.NoError(t, err) +} + func getPairwiseConnectionIds(aconns ibc.ConnectionOutputs, bconns ibc.ConnectionOutputs) ([]string, []string, error) { abconnids := make([]string, 0) baconnids := make([]string, 0) diff --git a/stride-covenant/tests/interchaintest/ics_test.go b/stride-covenant/tests/interchaintest/ics_test.go index 0d1b97fb..71e0afe4 100644 --- a/stride-covenant/tests/interchaintest/ics_test.go +++ b/stride-covenant/tests/interchaintest/ics_test.go @@ -79,7 +79,7 @@ func TestICS(t *testing.T) { Denom: "untrn", GasPrices: "0.0untrn,0.0uatom", GasAdjustment: 1.3, - TrustingPeriod: "1197504s", + TrustingPeriod: "330h", NoHostMount: false, ModifyGenesis: setupNeutronGenesis("0.05", []string{"untrn"}, []string{"uatom"}), ConfigFileOverrides: configFileOverrides, @@ -102,7 +102,7 @@ func TestICS(t *testing.T) { Denom: "ustrd", GasPrices: "0.00ustrd", GasAdjustment: 1.3, - TrustingPeriod: "1197504s", + TrustingPeriod: "330h", NoHostMount: false, ModifyGenesis: setupStrideGenesis([]string{ "/cosmos.bank.v1beta1.MsgSend", @@ -186,13 +186,78 @@ func TestICS(t *testing.T) { Client: client, NetworkID: network, BlockDatabaseFile: ibctest.DefaultBlockDatabaseFilepath(), - SkipPathCreation: false, + SkipPathCreation: true, }) require.NoError(t, err, "failed to build interchain") err = testutil.WaitForBlocks(ctx, 10, atom, neutron, stride) require.NoError(t, err, "failed to wait for blocks") + // generate ibc paths + generatePath(t, ctx, r, eRep, cosmosAtom.Config().ChainID, cosmosNeutron.Config().ChainID, gaiaNeutronIBCPath) + generatePath(t, ctx, r, eRep, cosmosAtom.Config().ChainID, cosmosStride.Config().ChainID, gaiaStrideIBCPath) + generatePath(t, ctx, r, eRep, cosmosStride.Config().ChainID, cosmosNeutron.Config().ChainID, neutronStrideIBCPath) + // generate ics path + generatePath(t, ctx, r, eRep, cosmosNeutron.Config().ChainID, cosmosAtom.Config().ChainID, gaiaNeutronICSPath) + + // create clients + generateClient(t, ctx, r, eRep, gaiaNeutronICSPath) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron) + require.NoError(t, err, "failed to wait for blocks") + + neutronClients, _ := r.GetClients(ctx, eRep, cosmosNeutron.Config().ChainID) + neutronICSClientId := neutronClients[0].ClientID + atomClients, _ := r.GetClients(ctx, eRep, cosmosAtom.Config().ChainID) + atomICSClientId := atomClients[0].ClientID + + print("neutron ics client id ", neutronICSClientId) + print("atom ics client id ", atomICSClientId) + + err = r.UpdatePath(ctx, eRep, gaiaNeutronICSPath, ibc.PathUpdateOptions{ + SrcClientID: &neutronICSClientId, + DstClientID: &atomICSClientId, + }) + require.NoError(t, err) + err = r.CreateConnections(ctx, eRep, gaiaNeutronICSPath) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron) + require.NoError(t, err, "failed to wait for blocks") + + err = r.CreateChannel(ctx, eRep, gaiaNeutronICSPath, ibc.CreateChannelOptions{ + SourcePortName: "consumer", + DestPortName: "provider", + Order: ibc.Ordered, + Version: "1", + }) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron) + require.NoError(t, err, "failed to wait for blocks") + + generateClient(t, ctx, r, eRep, gaiaNeutronIBCPath) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron) + require.NoError(t, err, "failed to wait for blocks") + generateClient(t, ctx, r, eRep, gaiaStrideIBCPath) + err = testutil.WaitForBlocks(ctx, 2, atom, stride) + require.NoError(t, err, "failed to wait for blocks") + generateClient(t, ctx, r, eRep, neutronStrideIBCPath) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron) + require.NoError(t, err, "failed to wait for blocks") + + // create connections + generateConnections(t, ctx, r, eRep, gaiaNeutronIBCPath) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron) + require.NoError(t, err, "failed to wait for blocks") + generateConnections(t, ctx, r, eRep, neutronStrideIBCPath) + err = testutil.WaitForBlocks(ctx, 2, stride, neutron) + require.NoError(t, err, "failed to wait for blocks") + generateConnections(t, ctx, r, eRep, gaiaStrideIBCPath) + err = testutil.WaitForBlocks(ctx, 2, atom, stride) + require.NoError(t, err, "failed to wait for blocks") + + r.LinkPath(ctx, eRep, gaiaNeutronIBCPath, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) + r.LinkPath(ctx, eRep, neutronStrideIBCPath, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) + r.LinkPath(ctx, eRep, gaiaStrideIBCPath, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) + // Start the relayer and clean it up when the test ends. err = r.StartRelayer(ctx, eRep, gaiaNeutronICSPath, gaiaNeutronIBCPath, gaiaStrideIBCPath, neutronStrideIBCPath) require.NoError(t, err, "failed to start relayer with given paths") From 1c37a3184bbcd54198e6148d559787a9611fdf2f Mon Sep 17 00:00:00 2001 From: bekauz Date: Sat, 5 Aug 2023 22:26:43 +0200 Subject: [PATCH 012/586] connection debugging --- stride-covenant/justfile | 1 + .../interchaintest/connection_helpers.go | 71 ++++++++++++++++- .../tests/interchaintest/ics_test.go | 79 +++++++++---------- 3 files changed, 107 insertions(+), 44 deletions(-) diff --git a/stride-covenant/justfile b/stride-covenant/justfile index 71603cfe..7a33d842 100644 --- a/stride-covenant/justfile +++ b/stride-covenant/justfile @@ -42,4 +42,5 @@ simtest: optimize cd tests/interchaintest/ && go test -timeout 30m -v ./... ictest: + go clean -testcache cd tests/interchaintest/ && go test -timeout 20m -v ./... \ No newline at end of file diff --git a/stride-covenant/tests/interchaintest/connection_helpers.go b/stride-covenant/tests/interchaintest/connection_helpers.go index c769f563..930f7c03 100644 --- a/stride-covenant/tests/interchaintest/connection_helpers.go +++ b/stride-covenant/tests/interchaintest/connection_helpers.go @@ -3,10 +3,12 @@ package ibc_test import ( "context" "errors" + "strings" "testing" "github.com/strangelove-ventures/interchaintest/v3/ibc" "github.com/strangelove-ventures/interchaintest/v3/testreporter" + "github.com/strangelove-ventures/interchaintest/v3/testutil" "github.com/stretchr/testify/require" ) @@ -15,14 +17,79 @@ func generatePath(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *testre require.NoError(t, err) } -func generateClient(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, path string) { +func generateClient(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, path string, chainA ibc.Chain, chainB ibc.Chain) { err := r.CreateClients(ctx, eRep, path, ibc.CreateClientOptions{TrustingPeriod: "330h"}) require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 2, chainA, chainB) + require.NoError(t, err, "failed to wait for blocks") } -func generateConnections(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, path string) { +func generateConnections(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, path string, chainA ibc.Chain, chainB ibc.Chain) (string, string) { + chainAConns, _ := r.GetConnections(ctx, eRep, chainA.Config().ChainID) + chainBConns, _ := r.GetConnections(ctx, eRep, chainB.Config().ChainID) + err := r.CreateConnections(ctx, eRep, path) require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 2, chainA, chainB) + require.NoError(t, err, "failed to wait for blocks") + + newChainAConns, _ := r.GetConnections(ctx, eRep, chainA.Config().ChainID) + newChainBConns, _ := r.GetConnections(ctx, eRep, chainB.Config().ChainID) + + newChainAConnection := connectionDifference(chainAConns, newChainAConns) + newChainBConnection := connectionDifference(chainBConns, newChainBConns) + + require.NotEqual(t, 0, len(newChainAConnection), "more than one connection generated", strings.Join(newChainAConnection, " ")) + require.NotEqual(t, 0, len(newChainBConnection), "more than one connection generated", strings.Join(newChainBConnection, " ")) + + return newChainAConnection[0], newChainBConnection[0] +} + +func connectionDifference(a []*ibc.ConnectionOutput, b []*ibc.ConnectionOutput) (diff []string) { + + m := make(map[string]bool) + + // we first mark all existing connections + for _, item := range a { + m[item.ID] = true + } + + // and append all new ones + for _, item := range b { + if _, ok := m[item.ID]; !ok { + diff = append(diff, item.ID) + } + } + return +} + +func printChannels(channels []ibc.ChannelOutput, chain string) { + for _, channel := range channels { + print("\n\n", chain, " channels after create channel :", channel.ChannelID, " to ", channel.Counterparty.ChannelID, "\n") + } +} + +func printConnections(connections ibc.ConnectionOutputs) { + for _, connection := range connections { + print(connection.ID, "\n") + } +} + +func channelDifference(oldChannels, newChannels []ibc.ChannelOutput) (diff []string) { + m := make(map[string]bool) + // we first mark all existing channels + for _, channel := range newChannels { + m[channel.ChannelID] = true + } + + // then find the new ones + for _, channel := range oldChannels { + if _, ok := m[channel.ChannelID]; !ok { + diff = append(diff, channel.ChannelID) + } + } + + return } func getPairwiseConnectionIds(aconns ibc.ConnectionOutputs, bconns ibc.ConnectionOutputs) ([]string, []string, error) { diff --git a/stride-covenant/tests/interchaintest/ics_test.go b/stride-covenant/tests/interchaintest/ics_test.go index 71e0afe4..fde8ca16 100644 --- a/stride-covenant/tests/interchaintest/ics_test.go +++ b/stride-covenant/tests/interchaintest/ics_test.go @@ -127,6 +127,8 @@ func TestICS(t *testing.T) { // We have three chains atom, neutron, stride := chains[0], chains[1], chains[2] cosmosAtom, cosmosNeutron, cosmosStride := atom.(*cosmos.CosmosChain), neutron.(*cosmos.CosmosChain), stride.(*cosmos.CosmosChain) + // var atomChannels, neutronChannels, strideChannels []ibc.ChannelOutput + // var atomConnections, neutronConnections, strideConnections []ibc.ConnectionOutput // Relayer Factory client, network := ibctest.DockerSetup(t) @@ -193,35 +195,33 @@ func TestICS(t *testing.T) { err = testutil.WaitForBlocks(ctx, 10, atom, neutron, stride) require.NoError(t, err, "failed to wait for blocks") - // generate ibc paths + // generate paths generatePath(t, ctx, r, eRep, cosmosAtom.Config().ChainID, cosmosNeutron.Config().ChainID, gaiaNeutronIBCPath) generatePath(t, ctx, r, eRep, cosmosAtom.Config().ChainID, cosmosStride.Config().ChainID, gaiaStrideIBCPath) - generatePath(t, ctx, r, eRep, cosmosStride.Config().ChainID, cosmosNeutron.Config().ChainID, neutronStrideIBCPath) - // generate ics path + generatePath(t, ctx, r, eRep, cosmosNeutron.Config().ChainID, cosmosStride.Config().ChainID, neutronStrideIBCPath) generatePath(t, ctx, r, eRep, cosmosNeutron.Config().ChainID, cosmosAtom.Config().ChainID, gaiaNeutronICSPath) + // fetch the initial channels + // create clients - generateClient(t, ctx, r, eRep, gaiaNeutronICSPath) - err = testutil.WaitForBlocks(ctx, 2, atom, neutron) - require.NoError(t, err, "failed to wait for blocks") + generateClient(t, ctx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) neutronClients, _ := r.GetClients(ctx, eRep, cosmosNeutron.Config().ChainID) - neutronICSClientId := neutronClients[0].ClientID atomClients, _ := r.GetClients(ctx, eRep, cosmosAtom.Config().ChainID) - atomICSClientId := atomClients[0].ClientID + atomNeutronICSClient := atomClients[0] + neutronAtomICSClient := neutronClients[0] - print("neutron ics client id ", neutronICSClientId) - print("atom ics client id ", atomICSClientId) + print("\nneutron ics client id ", atomNeutronICSClient.ClientID) + print("\natom ics client id ", neutronAtomICSClient.ClientID) err = r.UpdatePath(ctx, eRep, gaiaNeutronICSPath, ibc.PathUpdateOptions{ - SrcClientID: &neutronICSClientId, - DstClientID: &atomICSClientId, + SrcClientID: &neutronAtomICSClient.ClientID, + DstClientID: &atomNeutronICSClient.ClientID, }) require.NoError(t, err) - err = r.CreateConnections(ctx, eRep, gaiaNeutronICSPath) - require.NoError(t, err) - err = testutil.WaitForBlocks(ctx, 2, atom, neutron) - require.NoError(t, err, "failed to wait for blocks") + + atomNeutronICSConnectionId, neutronAtomICSConnectionId := generateConnections(t, ctx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) + print("\n atomNeutronICSConnectionId: ", atomNeutronICSConnectionId, ", neutronAtomICSConnectionId: ", neutronAtomICSConnectionId, "\n") err = r.CreateChannel(ctx, eRep, gaiaNeutronICSPath, ibc.CreateChannelOptions{ SourcePortName: "consumer", @@ -233,29 +233,24 @@ func TestICS(t *testing.T) { err = testutil.WaitForBlocks(ctx, 2, atom, neutron) require.NoError(t, err, "failed to wait for blocks") - generateClient(t, ctx, r, eRep, gaiaNeutronIBCPath) - err = testutil.WaitForBlocks(ctx, 2, atom, neutron) - require.NoError(t, err, "failed to wait for blocks") - generateClient(t, ctx, r, eRep, gaiaStrideIBCPath) - err = testutil.WaitForBlocks(ctx, 2, atom, stride) - require.NoError(t, err, "failed to wait for blocks") - generateClient(t, ctx, r, eRep, neutronStrideIBCPath) - err = testutil.WaitForBlocks(ctx, 2, atom, neutron) - require.NoError(t, err, "failed to wait for blocks") + generateClient(t, ctx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) + generateClient(t, ctx, r, eRep, gaiaStrideIBCPath, cosmosAtom, cosmosStride) + generateClient(t, ctx, r, eRep, neutronStrideIBCPath, cosmosNeutron, cosmosStride) // create connections - generateConnections(t, ctx, r, eRep, gaiaNeutronIBCPath) - err = testutil.WaitForBlocks(ctx, 2, atom, neutron) - require.NoError(t, err, "failed to wait for blocks") - generateConnections(t, ctx, r, eRep, neutronStrideIBCPath) - err = testutil.WaitForBlocks(ctx, 2, stride, neutron) - require.NoError(t, err, "failed to wait for blocks") - generateConnections(t, ctx, r, eRep, gaiaStrideIBCPath) - err = testutil.WaitForBlocks(ctx, 2, atom, stride) - require.NoError(t, err, "failed to wait for blocks") + neutronStrideIBCConnId, strideNeutronIBCConnId := generateConnections(t, ctx, r, eRep, neutronStrideIBCPath, cosmosNeutron, cosmosStride) + print("\n neutronStrideIBCConnId: ", neutronStrideIBCConnId, ", strideNeutronIBCConnId: ", strideNeutronIBCConnId, "\n") + + gaiaStrideIBCConnId, strideGaiaIBCConnId := generateConnections(t, ctx, r, eRep, gaiaStrideIBCPath, cosmosAtom, cosmosStride) + print("\n gaiaStrideIBCConnId: ", gaiaStrideIBCConnId, ", strideGaiaIBCConnId: ", strideGaiaIBCConnId, "\n") + + atomNeutronIBCConnId, neutronAtomIBCConnId := generateConnections(t, ctx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) + print("\n atomNeutronIBCConnectionId: ", atomNeutronIBCConnId, ", neutronAtomIBCConnectionId: ", neutronAtomIBCConnId, "\n") r.LinkPath(ctx, eRep, gaiaNeutronIBCPath, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) + r.LinkPath(ctx, eRep, neutronStrideIBCPath, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) + r.LinkPath(ctx, eRep, gaiaStrideIBCPath, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) // Start the relayer and clean it up when the test ends. @@ -371,14 +366,14 @@ func TestICS(t *testing.T) { connectionChannelsOk = false } // Print out connections and channels for debugging - print("\n strideGaiaConnectionId: ", strideGaiaConnectionId) - print("\n strideNeutronConnectionId: ", strideNeutronConnectionId) - print("\n neutronStrideConnectionId: ", neutronStrideConnectionId) - print("\n neutronGaiaTransferConnectionId: ", neutronGaiaTransferConnectionId) - print("\n neutronGaiaICSConnectionId: ", neutronGaiaICSConnectionId) - print("\n gaiaStrideConnectionId: ", gaiaStrideConnectionId) - print("\n gaiaNeutronTransferConnectionId: ", gaiaNeutronTransferConnectionId) - print("\n gaiaNeutronICSConnectionId: ", gaiaNeutronICSConnectionId) + print("\n strideGaiaConnectionId: ", strideGaiaConnectionId, " vs. ", strideGaiaIBCConnId) + print("\n strideNeutronConnectionId: ", strideNeutronConnectionId, " vs. ", strideNeutronIBCConnId) + print("\n neutronStrideConnectionId: ", neutronStrideConnectionId, " vs. ", neutronStrideIBCConnId) + print("\n neutronGaiaTransferConnectionId: ", neutronGaiaTransferConnectionId, " vs. ", neutronAtomIBCConnId) + print("\n neutronGaiaICSConnectionId: ", neutronGaiaICSConnectionId, " vs. ", neutronGaiaICSConnectionId) + print("\n gaiaStrideConnectionId: ", gaiaStrideConnectionId, " vs. ", gaiaStrideIBCConnId) + print("\n gaiaNeutronTransferConnectionId: ", gaiaNeutronTransferConnectionId, " vs. ", atomNeutronIBCConnId) + print("\n gaiaNeutronICSConnectionId: ", gaiaNeutronICSConnectionId, " vs. ", atomNeutronICSConnectionId) print("\n strideGaiaChannelId: ", strideGaiaChannelId) print("\n strideNeutronChannelId: ", strideNeutronChannelId) print("\n neutronStrideChannelId: ", neutronStrideChannelId) From d133b9e36667d5e36f83c414df48160217030220 Mon Sep 17 00:00:00 2001 From: bekauz Date: Sat, 5 Aug 2023 22:41:15 +0200 Subject: [PATCH 013/586] connection ids without iterative matching --- stride-covenant/tests/interchaintest/ics_test.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/stride-covenant/tests/interchaintest/ics_test.go b/stride-covenant/tests/interchaintest/ics_test.go index fde8ca16..4c3d1a60 100644 --- a/stride-covenant/tests/interchaintest/ics_test.go +++ b/stride-covenant/tests/interchaintest/ics_test.go @@ -240,18 +240,21 @@ func TestICS(t *testing.T) { // create connections neutronStrideIBCConnId, strideNeutronIBCConnId := generateConnections(t, ctx, r, eRep, neutronStrideIBCPath, cosmosNeutron, cosmosStride) print("\n neutronStrideIBCConnId: ", neutronStrideIBCConnId, ", strideNeutronIBCConnId: ", strideNeutronIBCConnId, "\n") + r.LinkPath(ctx, eRep, neutronStrideIBCPath, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") gaiaStrideIBCConnId, strideGaiaIBCConnId := generateConnections(t, ctx, r, eRep, gaiaStrideIBCPath, cosmosAtom, cosmosStride) print("\n gaiaStrideIBCConnId: ", gaiaStrideIBCConnId, ", strideGaiaIBCConnId: ", strideGaiaIBCConnId, "\n") + r.LinkPath(ctx, eRep, gaiaStrideIBCPath, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") atomNeutronIBCConnId, neutronAtomIBCConnId := generateConnections(t, ctx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) print("\n atomNeutronIBCConnectionId: ", atomNeutronIBCConnId, ", neutronAtomIBCConnectionId: ", neutronAtomIBCConnId, "\n") - r.LinkPath(ctx, eRep, gaiaNeutronIBCPath, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) - - r.LinkPath(ctx, eRep, neutronStrideIBCPath, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) - - r.LinkPath(ctx, eRep, gaiaStrideIBCPath, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) + require.NoError(t, err, "failed to wait for blocks") // Start the relayer and clean it up when the test ends. err = r.StartRelayer(ctx, eRep, gaiaNeutronICSPath, gaiaNeutronIBCPath, gaiaStrideIBCPath, neutronStrideIBCPath) From 7aad69ab4eac310635e25301c5caafc7693e2f16 Mon Sep 17 00:00:00 2001 From: bekauz Date: Sun, 6 Aug 2023 19:21:26 +0200 Subject: [PATCH 014/586] manual path creation cleanup --- .../interchaintest/connection_helpers.go | 87 ++++++--------- .../tests/interchaintest/ics_test.go | 103 +++++------------- 2 files changed, 62 insertions(+), 128 deletions(-) diff --git a/stride-covenant/tests/interchaintest/connection_helpers.go b/stride-covenant/tests/interchaintest/connection_helpers.go index 930f7c03..06941475 100644 --- a/stride-covenant/tests/interchaintest/connection_helpers.go +++ b/stride-covenant/tests/interchaintest/connection_helpers.go @@ -17,6 +17,23 @@ func generatePath(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *testre require.NoError(t, err) } +func createValidator(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, chain ibc.Chain, counterparty ibc.Chain) { + cmd := getCreateValidatorCmd(chain) + _, _, err := chain.Exec(ctx, cmd, nil) + require.NoError(t, err) + + // Wait a bit for the VSC packet to get relayed. + err = testutil.WaitForBlocks(ctx, 2, chain, counterparty) + require.NoError(t, err, "failed to wait for blocks") +} + +func linkPath(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, chainA ibc.Chain, chainB ibc.Chain, path string) { + err := r.LinkPath(ctx, eRep, path, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 2, chainA, chainB) + require.NoError(t, err, "failed to wait for blocks") +} + func generateClient(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, path string, chainA ibc.Chain, chainB ibc.Chain) { err := r.CreateClients(ctx, eRep, path, ibc.CreateClientOptions{TrustingPeriod: "330h"}) require.NoError(t, err) @@ -111,18 +128,12 @@ func getPairwiseConnectionIds(aconns ibc.ConnectionOutputs, bconns ibc.Connectio if found { return abconnids, baconnids, nil } else { - return abconnids, baconnids, errors.New("No connection found") + return abconnids, baconnids, errors.New("no connection found") } } -// returns transfer channels and respective connections -func getPairwiseTransferChannelIds(achans []ibc.ChannelOutput, bchans []ibc.ChannelOutput, abconns []string, baconns []string) (string, string, string, string, error) { - var abchan string - var bachan string - var abconn string - var baconn string - - found := false +// returns transfer channel ids +func getPairwiseTransferChannelIds(achans []ibc.ChannelOutput, bchans []ibc.ChannelOutput, aToBConnId string, bToAConnId string) (string, string, error) { for _, a := range achans { for _, b := range bchans { @@ -131,37 +142,20 @@ func getPairwiseTransferChannelIds(achans []ibc.ChannelOutput, bchans []ibc.Chan a.PortID == "transfer" && b.PortID == "transfer" && a.Ordering == "ORDER_UNORDERED" && - b.Ordering == "ORDER_UNORDERED" { - for _, abcon := range abconns { - for _, bacon := range baconns { - if a.ConnectionHops[0] == abcon && - b.ConnectionHops[0] == bacon { - abchan = a.ChannelID - bachan = b.ChannelID - abconn = abcon - baconn = bacon - found = true - } - } - } + b.Ordering == "ORDER_UNORDERED" && + a.ConnectionHops[0] == aToBConnId && + b.ConnectionHops[0] == bToAConnId { + + return a.ChannelID, b.ChannelID, nil } } } - if found { - return abchan, bachan, abconn, baconn, nil - } else { - return abchan, bachan, abconn, baconn, errors.New("No transfer channel found") - } -} -// returns ccv channels and respective connections -func getPairwiseCCVChannelIds(achans []ibc.ChannelOutput, bchans []ibc.ChannelOutput, abconns []string, baconns []string) (string, string, string, string, error) { - var abchan string - var bachan string - var abconn string - var baconn string + return "", "", errors.New("no transfer channel found") +} - found := false +// returns ccv channel ids +func getPairwiseCCVChannelIds(achans []ibc.ChannelOutput, bchans []ibc.ChannelOutput, aToBConnId string, bToAConnId string) (string, string, error) { for _, a := range achans { for _, b := range bchans { if a.ChannelID == b.Counterparty.ChannelID && @@ -169,25 +163,12 @@ func getPairwiseCCVChannelIds(achans []ibc.ChannelOutput, bchans []ibc.ChannelOu a.PortID == "provider" && b.PortID == "consumer" && a.Ordering == "ORDER_ORDERED" && - b.Ordering == "ORDER_ORDERED" { - for _, abcon := range abconns { - for _, bacon := range baconns { - if a.ConnectionHops[0] == abcon && - b.ConnectionHops[0] == bacon { - abchan = a.ChannelID - bachan = b.ChannelID - abconn = abcon - baconn = bacon - found = true - } - } - } + b.Ordering == "ORDER_ORDERED" && + a.ConnectionHops[0] == aToBConnId && + b.ConnectionHops[0] == bToAConnId { + return a.ChannelID, b.ChannelID, nil } } } - if found { - return abchan, bachan, abconn, baconn, nil - } else { - return abchan, bachan, abconn, baconn, errors.New("No ccv channel found") - } + return "", "", errors.New("no ccv channel found") } diff --git a/stride-covenant/tests/interchaintest/ics_test.go b/stride-covenant/tests/interchaintest/ics_test.go index 4c3d1a60..e12e9842 100644 --- a/stride-covenant/tests/interchaintest/ics_test.go +++ b/stride-covenant/tests/interchaintest/ics_test.go @@ -201,8 +201,6 @@ func TestICS(t *testing.T) { generatePath(t, ctx, r, eRep, cosmosNeutron.Config().ChainID, cosmosStride.Config().ChainID, neutronStrideIBCPath) generatePath(t, ctx, r, eRep, cosmosNeutron.Config().ChainID, cosmosAtom.Config().ChainID, gaiaNeutronICSPath) - // fetch the initial channels - // create clients generateClient(t, ctx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) @@ -211,9 +209,6 @@ func TestICS(t *testing.T) { atomNeutronICSClient := atomClients[0] neutronAtomICSClient := neutronClients[0] - print("\nneutron ics client id ", atomNeutronICSClient.ClientID) - print("\natom ics client id ", neutronAtomICSClient.ClientID) - err = r.UpdatePath(ctx, eRep, gaiaNeutronICSPath, ibc.PathUpdateOptions{ SrcClientID: &neutronAtomICSClient.ClientID, DstClientID: &atomNeutronICSClient.ClientID, @@ -233,28 +228,18 @@ func TestICS(t *testing.T) { err = testutil.WaitForBlocks(ctx, 2, atom, neutron) require.NoError(t, err, "failed to wait for blocks") - generateClient(t, ctx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) - generateClient(t, ctx, r, eRep, gaiaStrideIBCPath, cosmosAtom, cosmosStride) + // create connections and link everything up generateClient(t, ctx, r, eRep, neutronStrideIBCPath, cosmosNeutron, cosmosStride) - - // create connections neutronStrideIBCConnId, strideNeutronIBCConnId := generateConnections(t, ctx, r, eRep, neutronStrideIBCPath, cosmosNeutron, cosmosStride) - print("\n neutronStrideIBCConnId: ", neutronStrideIBCConnId, ", strideNeutronIBCConnId: ", strideNeutronIBCConnId, "\n") - r.LinkPath(ctx, eRep, neutronStrideIBCPath, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) - err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) - require.NoError(t, err, "failed to wait for blocks") + linkPath(t, ctx, r, eRep, cosmosNeutron, cosmosStride, neutronStrideIBCPath) + generateClient(t, ctx, r, eRep, gaiaStrideIBCPath, cosmosAtom, cosmosStride) gaiaStrideIBCConnId, strideGaiaIBCConnId := generateConnections(t, ctx, r, eRep, gaiaStrideIBCPath, cosmosAtom, cosmosStride) - print("\n gaiaStrideIBCConnId: ", gaiaStrideIBCConnId, ", strideGaiaIBCConnId: ", strideGaiaIBCConnId, "\n") - r.LinkPath(ctx, eRep, gaiaStrideIBCPath, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) - err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) - require.NoError(t, err, "failed to wait for blocks") + linkPath(t, ctx, r, eRep, cosmosAtom, cosmosStride, gaiaStrideIBCPath) + generateClient(t, ctx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) atomNeutronIBCConnId, neutronAtomIBCConnId := generateConnections(t, ctx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) - print("\n atomNeutronIBCConnectionId: ", atomNeutronIBCConnId, ", neutronAtomIBCConnectionId: ", neutronAtomIBCConnId, "\n") - r.LinkPath(ctx, eRep, gaiaNeutronIBCPath, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) - err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) - require.NoError(t, err, "failed to wait for blocks") + linkPath(t, ctx, r, eRep, cosmosAtom, cosmosNeutron, gaiaNeutronIBCPath) // Start the relayer and clean it up when the test ends. err = r.StartRelayer(ctx, eRep, gaiaNeutronICSPath, gaiaNeutronIBCPath, gaiaStrideIBCPath, neutronStrideIBCPath) @@ -269,13 +254,7 @@ func TestICS(t *testing.T) { err = testutil.WaitForBlocks(ctx, 2, atom, neutron, stride) require.NoError(t, err, "failed to wait for blocks") - cmd := getCreateValidatorCmd(atom) - _, _, err = atom.Exec(ctx, cmd, nil) - require.NoError(t, err) - - // Wait a bit for the VSC packet to get relayed. - err = testutil.WaitForBlocks(ctx, 2, atom, neutron) - require.NoError(t, err, "failed to wait for blocks") + createValidator(t, ctx, r, eRep, atom, neutron) // Once the VSC packet has been relayed, x/bank transfers are // enabled on Neutron and we can fund its account. @@ -289,7 +268,7 @@ func TestICS(t *testing.T) { cosmosStride.SendFunds(ctx, strideUser.KeyName, ibc.WalletAmount{ Address: strideAdmin.Bech32Address(stride.Config().Bech32Prefix), - Denom: "ustride", + Denom: "ustrd", Amount: 10000000, }) @@ -303,19 +282,8 @@ func TestICS(t *testing.T) { require.NoError(t, err, "failed to fund neutron user") require.EqualValues(t, int64(500_000_000_000), neutronUserBal) - var strideGaiaConnectionId, gaiaStrideConnectionId string - var strideNeutronConnectionId, neutronStrideConnectionId string - var neutronGaiaTransferConnectionId, neutronGaiaICSConnectionId string - var gaiaNeutronTransferConnectionId, gaiaNeutronICSConnectionId string var liquidityTokenAddress string - neutronGaiaConnectionIds := make([]string, 0) - gaiaNeutronConnectionIds := make([]string, 0) - neutronStrideConnectionIds := make([]string, 0) - strideNeutronConnectionIds := make([]string, 0) - gaiaStrideConnectionIds := make([]string, 0) - strideGaiaConnectionIds := make([]string, 0) - var strideNeutronChannelId, neutronStrideChannelId string var strideGaiaChannelId, gaiaStrideChannelId string var neutronGaiaICSChannelId, gaiaNeutronICSChannelId string @@ -333,50 +301,35 @@ func TestICS(t *testing.T) { neutronChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosNeutron.Config().ChainID) gaiaChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosAtom.Config().ChainID) strideChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosStride.Config().ChainID) - strideConnectionInfo, _ := r.GetConnections(ctx, eRep, cosmosStride.Config().ChainID) - neutronConnectionInfo, _ := r.GetConnections(ctx, eRep, cosmosNeutron.Config().ChainID) - gaiaConnectionInfo, _ := r.GetConnections(ctx, eRep, cosmosAtom.Config().ChainID) connectionChannelsOk = true - /// Find all the pairwise connections - strideNeutronConnectionIds, neutronStrideConnectionIds, err = getPairwiseConnectionIds(strideConnectionInfo, neutronConnectionInfo) - if err != nil { - connectionChannelsOk = false - } - neutronGaiaConnectionIds, gaiaNeutronConnectionIds, err = getPairwiseConnectionIds(neutronConnectionInfo, gaiaConnectionInfo) - if err != nil { - connectionChannelsOk = false - } - strideGaiaConnectionIds, gaiaStrideConnectionIds, err = getPairwiseConnectionIds(strideConnectionInfo, gaiaConnectionInfo) - if err != nil { - connectionChannelsOk = false - } + // Find all pairwise channels - strideNeutronChannelId, neutronStrideChannelId, strideNeutronConnectionId, neutronStrideConnectionId, err = getPairwiseTransferChannelIds(strideChannelInfo, neutronChannelInfo, strideNeutronConnectionIds, neutronStrideConnectionIds) + strideNeutronChannelId, neutronStrideChannelId, err = getPairwiseTransferChannelIds(strideChannelInfo, neutronChannelInfo, strideNeutronIBCConnId, neutronStrideIBCConnId) if err != nil { connectionChannelsOk = false } - strideGaiaChannelId, gaiaStrideChannelId, strideGaiaConnectionId, gaiaStrideConnectionId, err = getPairwiseTransferChannelIds(strideChannelInfo, gaiaChannelInfo, strideGaiaConnectionIds, gaiaStrideConnectionIds) + strideGaiaChannelId, gaiaStrideChannelId, err = getPairwiseTransferChannelIds(strideChannelInfo, gaiaChannelInfo, strideGaiaIBCConnId, gaiaStrideIBCConnId) if err != nil { connectionChannelsOk = false } - gaiaNeutronTransferChannelId, neutronGaiaTransferChannelId, gaiaNeutronTransferConnectionId, neutronGaiaTransferConnectionId, err = getPairwiseTransferChannelIds(gaiaChannelInfo, neutronChannelInfo, gaiaNeutronConnectionIds, neutronGaiaConnectionIds) + gaiaNeutronTransferChannelId, neutronGaiaTransferChannelId, err = getPairwiseTransferChannelIds(gaiaChannelInfo, neutronChannelInfo, atomNeutronIBCConnId, neutronAtomIBCConnId) if err != nil { connectionChannelsOk = false } - gaiaNeutronICSChannelId, neutronGaiaICSChannelId, gaiaNeutronICSConnectionId, neutronGaiaICSConnectionId, err = getPairwiseCCVChannelIds(gaiaChannelInfo, neutronChannelInfo, gaiaNeutronConnectionIds, neutronGaiaConnectionIds) + gaiaNeutronICSChannelId, neutronGaiaICSChannelId, err = getPairwiseCCVChannelIds(gaiaChannelInfo, neutronChannelInfo, atomNeutronICSConnectionId, neutronAtomICSConnectionId) if err != nil { connectionChannelsOk = false } // Print out connections and channels for debugging - print("\n strideGaiaConnectionId: ", strideGaiaConnectionId, " vs. ", strideGaiaIBCConnId) - print("\n strideNeutronConnectionId: ", strideNeutronConnectionId, " vs. ", strideNeutronIBCConnId) - print("\n neutronStrideConnectionId: ", neutronStrideConnectionId, " vs. ", neutronStrideIBCConnId) - print("\n neutronGaiaTransferConnectionId: ", neutronGaiaTransferConnectionId, " vs. ", neutronAtomIBCConnId) - print("\n neutronGaiaICSConnectionId: ", neutronGaiaICSConnectionId, " vs. ", neutronGaiaICSConnectionId) - print("\n gaiaStrideConnectionId: ", gaiaStrideConnectionId, " vs. ", gaiaStrideIBCConnId) - print("\n gaiaNeutronTransferConnectionId: ", gaiaNeutronTransferConnectionId, " vs. ", atomNeutronIBCConnId) - print("\n gaiaNeutronICSConnectionId: ", gaiaNeutronICSConnectionId, " vs. ", atomNeutronICSConnectionId) + print("\n strideGaiaConnectionId: ", strideGaiaIBCConnId) + print("\n strideNeutronConnectionId: ", strideNeutronIBCConnId) + print("\n neutronStrideConnectionId: ", neutronStrideIBCConnId) + print("\n neutronGaiaTransferConnectionId: ", neutronAtomIBCConnId) + print("\n neutronGaiaICSConnectionId: ", neutronAtomICSConnectionId) + print("\n gaiaStrideConnectionId: ", gaiaStrideIBCConnId) + print("\n gaiaNeutronTransferConnectionId: ", atomNeutronIBCConnId) + print("\n gaiaNeutronICSConnectionId: ", atomNeutronICSConnectionId) print("\n strideGaiaChannelId: ", strideGaiaChannelId) print("\n strideNeutronChannelId: ", strideNeutronChannelId) print("\n neutronStrideChannelId: ", neutronStrideChannelId) @@ -400,7 +353,6 @@ func TestICS(t *testing.T) { } } _, _, _, _, _ = neutronGaiaTransferChannelId, gaiaNeutronTransferChannelId, neutronGaiaICSChannelId, gaiaNeutronICSChannelId, neutronStrideChannelId - _, _, _ = gaiaStrideConnectionId, strideGaiaConnectionId, strideNeutronConnectionId // We can determine the ibc denoms of: // 1. ATOM on Neutron @@ -562,7 +514,7 @@ func TestICS(t *testing.T) { t.Run("register stride host zone", func(t *testing.T) { cmd := []string{"strided", "tx", "stakeibc", "register-host-zone", - strideGaiaConnectionId, + strideGaiaIBCConnId, cosmosAtom.Config().Denom, cosmosAtom.Config().Bech32Prefix, strideAtomIbcDenom, @@ -1058,9 +1010,10 @@ func TestICS(t *testing.T) { atomWeightedReceiverAmount = WeightedReceiverAmount{ Amount: strconv.FormatUint(atomFunds, 10), } + depositorMsg := PresetDepositorFields{ GaiaNeutronIBCTransferChannelId: gaiaNeutronTransferChannelId, - NeutronGaiaConnectionId: neutronGaiaTransferConnectionId, + NeutronGaiaConnectionId: neutronAtomIBCConnId, GaiaStrideIBCTransferChannelId: gaiaStrideChannelId, DepositorCode: depositorCodeId, Label: "covenant-depositor", @@ -1075,7 +1028,7 @@ func TestICS(t *testing.T) { Label: "covenant-ls", LsDenom: "stuatom", StrideNeutronIBCTransferChannelId: strideNeutronChannelId, - NeutronStrideIBCConnectionId: neutronStrideConnectionId, + NeutronStrideIBCConnectionId: neutronStrideIBCConnId, } // For LPer, we need to first gather astroport information @@ -1217,7 +1170,7 @@ func TestICS(t *testing.T) { }) tickClock := func() (string, string, string) { - cmd = []string{"neutrond", "tx", "wasm", "execute", clockContractAddress, + cmd := []string{"neutrond", "tx", "wasm", "execute", clockContractAddress, `{"tick":{}}`, "--from", neutronUser.KeyName, "--gas-prices", "0.0untrn", @@ -1564,7 +1517,7 @@ func TestICS(t *testing.T) { } str, _ := json.Marshal(withdrawLiquidityMsg) print("\n withdrawing liquidity from LP position...\n") - cmd = []string{"neutrond", "tx", "wasm", "execute", holderContractAddress, + cmd := []string{"neutrond", "tx", "wasm", "execute", holderContractAddress, string(str), "--from", neutronUser.KeyName, "--gas-prices", "0.0untrn", @@ -1613,7 +1566,7 @@ func TestICS(t *testing.T) { str, _ := json.Marshal(withdrawMsg) err = testutil.WaitForBlocks(ctx, 10, atom, neutron, stride) require.NoError(t, err) - cmd = []string{"neutrond", "tx", "wasm", "execute", holderContractAddress, + cmd := []string{"neutrond", "tx", "wasm", "execute", holderContractAddress, string(str), "--from", neutronUser.KeyName, "--gas-prices", "0.0untrn", From 73f2a1c6588e179286b12371201cd4d358ea4929 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 8 Aug 2023 00:28:14 +0200 Subject: [PATCH 015/586] cleaning up ictest setup --- .../interchaintest/connection_helpers.go | 136 ++++++++++++++++-- .../tests/interchaintest/ics_test.go | 20 +-- 2 files changed, 131 insertions(+), 25 deletions(-) diff --git a/stride-covenant/tests/interchaintest/connection_helpers.go b/stride-covenant/tests/interchaintest/connection_helpers.go index 06941475..134bad26 100644 --- a/stride-covenant/tests/interchaintest/connection_helpers.go +++ b/stride-covenant/tests/interchaintest/connection_helpers.go @@ -12,12 +12,48 @@ import ( "github.com/stretchr/testify/require" ) -func generatePath(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, chainAId string, chainBId string, path string) { +func generatePath( + t *testing.T, + ctx context.Context, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + chainAId string, + chainBId string, + path string, +) { err := r.GeneratePath(ctx, eRep, chainAId, chainBId, path) require.NoError(t, err) } -func createValidator(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, chain ibc.Chain, counterparty ibc.Chain) { +func generateICSChannel( + t *testing.T, + ctx context.Context, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + icsPath string, + chainA ibc.Chain, + chainB ibc.Chain, +) { + + err := r.CreateChannel(ctx, eRep, icsPath, ibc.CreateChannelOptions{ + SourcePortName: "consumer", + DestPortName: "provider", + Order: ibc.Ordered, + Version: "1", + }) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 2, chainA, chainB) + require.NoError(t, err, "failed to wait for blocks") +} + +func createValidator( + t *testing.T, + ctx context.Context, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + chain ibc.Chain, + counterparty ibc.Chain, +) { cmd := getCreateValidatorCmd(chain) _, _, err := chain.Exec(ctx, cmd, nil) require.NoError(t, err) @@ -27,21 +63,70 @@ func createValidator(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *tes require.NoError(t, err, "failed to wait for blocks") } -func linkPath(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, chainA ibc.Chain, chainB ibc.Chain, path string) { +func linkPath( + t *testing.T, + ctx context.Context, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + chainA ibc.Chain, + chainB ibc.Chain, + path string, +) { err := r.LinkPath(ctx, eRep, path, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) require.NoError(t, err) err = testutil.WaitForBlocks(ctx, 2, chainA, chainB) require.NoError(t, err, "failed to wait for blocks") } -func generateClient(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, path string, chainA ibc.Chain, chainB ibc.Chain) { +func generateClient( + t *testing.T, + ctx context.Context, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + path string, + chainA ibc.Chain, + chainB ibc.Chain, +) (string, string) { + chainAClients, _ := r.GetClients(ctx, eRep, chainA.Config().ChainID) + chainBClients, _ := r.GetClients(ctx, eRep, chainB.Config().ChainID) + err := r.CreateClients(ctx, eRep, path, ibc.CreateClientOptions{TrustingPeriod: "330h"}) require.NoError(t, err) err = testutil.WaitForBlocks(ctx, 2, chainA, chainB) require.NoError(t, err, "failed to wait for blocks") + + newChainAClients, _ := r.GetClients(ctx, eRep, chainA.Config().ChainID) + newChainBClients, _ := r.GetClients(ctx, eRep, chainB.Config().ChainID) + var newClientA, newClientB string + + aClientDiff := clientDifference(chainAClients, newChainAClients) + bClientDiff := clientDifference(chainBClients, newChainBClients) + + if len(aClientDiff) > 0 { + newClientA = aClientDiff[0] + } else { + newClientA = "" + } + + if len(bClientDiff) > 0 { + newClientB = bClientDiff[0] + } else { + newClientB = "" + } + + print("\n found client differences. new client A: ", newClientA, "b:") + return newClientA, newClientB } -func generateConnections(t *testing.T, ctx context.Context, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, path string, chainA ibc.Chain, chainB ibc.Chain) (string, string) { +func generateConnections( + t *testing.T, + ctx context.Context, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + path string, + chainA ibc.Chain, + chainB ibc.Chain, +) (string, string) { chainAConns, _ := r.GetConnections(ctx, eRep, chainA.Config().ChainID) chainBConns, _ := r.GetConnections(ctx, eRep, chainB.Config().ChainID) @@ -62,7 +147,10 @@ func generateConnections(t *testing.T, ctx context.Context, r ibc.Relayer, eRep return newChainAConnection[0], newChainBConnection[0] } -func connectionDifference(a []*ibc.ConnectionOutput, b []*ibc.ConnectionOutput) (diff []string) { +func connectionDifference( + a []*ibc.ConnectionOutput, + b []*ibc.ConnectionOutput, +) (diff []string) { m := make(map[string]bool) @@ -80,6 +168,23 @@ func connectionDifference(a []*ibc.ConnectionOutput, b []*ibc.ConnectionOutput) return } +func clientDifference(a, b []*ibc.ClientOutput) (diff []string) { + m := make(map[string]bool) + + // we first mark all existing clients + for _, item := range a { + m[item.ClientID] = true + } + + // and append all new ones + for _, item := range b { + if _, ok := m[item.ClientID]; !ok { + diff = append(diff, item.ClientID) + } + } + return +} + func printChannels(channels []ibc.ChannelOutput, chain string) { for _, channel := range channels { print("\n\n", chain, " channels after create channel :", channel.ChannelID, " to ", channel.Counterparty.ChannelID, "\n") @@ -109,7 +214,10 @@ func channelDifference(oldChannels, newChannels []ibc.ChannelOutput) (diff []str return } -func getPairwiseConnectionIds(aconns ibc.ConnectionOutputs, bconns ibc.ConnectionOutputs) ([]string, []string, error) { +func getPairwiseConnectionIds( + aconns ibc.ConnectionOutputs, + bconns ibc.ConnectionOutputs, +) ([]string, []string, error) { abconnids := make([]string, 0) baconnids := make([]string, 0) found := false @@ -133,7 +241,12 @@ func getPairwiseConnectionIds(aconns ibc.ConnectionOutputs, bconns ibc.Connectio } // returns transfer channel ids -func getPairwiseTransferChannelIds(achans []ibc.ChannelOutput, bchans []ibc.ChannelOutput, aToBConnId string, bToAConnId string) (string, string, error) { +func getPairwiseTransferChannelIds( + achans []ibc.ChannelOutput, + bchans []ibc.ChannelOutput, + aToBConnId string, + bToAConnId string, +) (string, string, error) { for _, a := range achans { for _, b := range bchans { @@ -155,7 +268,12 @@ func getPairwiseTransferChannelIds(achans []ibc.ChannelOutput, bchans []ibc.Chan } // returns ccv channel ids -func getPairwiseCCVChannelIds(achans []ibc.ChannelOutput, bchans []ibc.ChannelOutput, aToBConnId string, bToAConnId string) (string, string, error) { +func getPairwiseCCVChannelIds( + achans []ibc.ChannelOutput, + bchans []ibc.ChannelOutput, + aToBConnId string, + bToAConnId string, +) (string, string, error) { for _, a := range achans { for _, b := range bchans { if a.ChannelID == b.Counterparty.ChannelID && diff --git a/stride-covenant/tests/interchaintest/ics_test.go b/stride-covenant/tests/interchaintest/ics_test.go index e12e9842..b75f9f48 100644 --- a/stride-covenant/tests/interchaintest/ics_test.go +++ b/stride-covenant/tests/interchaintest/ics_test.go @@ -203,30 +203,18 @@ func TestICS(t *testing.T) { // create clients generateClient(t, ctx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) - neutronClients, _ := r.GetClients(ctx, eRep, cosmosNeutron.Config().ChainID) atomClients, _ := r.GetClients(ctx, eRep, cosmosAtom.Config().ChainID) - atomNeutronICSClient := atomClients[0] - neutronAtomICSClient := neutronClients[0] err = r.UpdatePath(ctx, eRep, gaiaNeutronICSPath, ibc.PathUpdateOptions{ - SrcClientID: &neutronAtomICSClient.ClientID, - DstClientID: &atomNeutronICSClient.ClientID, + SrcClientID: &neutronClients[0].ClientID, + DstClientID: &atomClients[0].ClientID, }) require.NoError(t, err) atomNeutronICSConnectionId, neutronAtomICSConnectionId := generateConnections(t, ctx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) - print("\n atomNeutronICSConnectionId: ", atomNeutronICSConnectionId, ", neutronAtomICSConnectionId: ", neutronAtomICSConnectionId, "\n") - err = r.CreateChannel(ctx, eRep, gaiaNeutronICSPath, ibc.CreateChannelOptions{ - SourcePortName: "consumer", - DestPortName: "provider", - Order: ibc.Ordered, - Version: "1", - }) - require.NoError(t, err) - err = testutil.WaitForBlocks(ctx, 2, atom, neutron) - require.NoError(t, err, "failed to wait for blocks") + generateICSChannel(t, ctx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) // create connections and link everything up generateClient(t, ctx, r, eRep, neutronStrideIBCPath, cosmosNeutron, cosmosStride) @@ -272,7 +260,7 @@ func TestICS(t *testing.T) { Amount: 10000000, }) - err = testutil.WaitForBlocks(ctx, 30, atom, neutron, stride) + err = testutil.WaitForBlocks(ctx, 10, atom, neutron, stride) require.NoError(t, err, "failed to wait for blocks") neutronUserBal, err := neutron.GetBalance( From 82260277d13e13f1808651ee59e76657a8d8a8af Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 8 Aug 2023 19:14:22 +0200 Subject: [PATCH 016/586] DepositorQuery macro --- Cargo.lock | 38 ++++++++++++++ Cargo.toml | 1 + contracts/ibc-forwarder/Cargo.toml | 1 + contracts/ibc-forwarder/src/contract.rs | 27 ++++++++-- contracts/ibc-forwarder/src/msg.rs | 9 ++-- packages/covenant-depositor-derive/Cargo.toml | 16 ++++++ packages/covenant-depositor-derive/src/lib.rs | 51 +++++++++++++++++++ 7 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 packages/covenant-depositor-derive/Cargo.toml create mode 100644 packages/covenant-depositor-derive/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index e73bd3e7..16e11a77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -411,6 +411,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "covenant-depositor-derive" +version = "0.0.1" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "covenant-holder" version = "1.0.0" @@ -432,6 +441,35 @@ dependencies = [ "thiserror", ] +[[package]] +name = "covenant-ibc-forwarder" +version = "1.0.0" +dependencies = [ + "anyhow", + "base64 0.13.1", + "bech32", + "cosmos-sdk-proto 0.14.0", + "cosmwasm-schema", + "cosmwasm-std", + "covenant-clock", + "covenant-clock-derive", + "covenant-depositor-derive", + "covenant-ls", + "cw-multi-test", + "cw-storage-plus 1.1.0", + "cw-utils 1.0.1", + "cw2 1.1.0", + "neutron-sdk", + "prost 0.11.9", + "prost-types", + "protobuf 3.2.0", + "schemars", + "serde", + "serde-json-wasm 0.4.1", + "sha2 0.10.7", + "thiserror", +] + [[package]] name = "covenant-lp" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index ade7e2b4..ae832622 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ covenant-holder = { path = "contracts/holder" } clock-derive = { path = "packages/clock-derive" } cw-fifo = { path = "packages/cw-fifo" } covenant-clock-derive = { path = "packages/clock-derive" } +covenant-depositor-derive = { path = "packages/covenant-depositor-derive" } # the sha2 version here is the same as the one used by # cosmwasm-std. when bumping cosmwasm-std, this should also be diff --git a/contracts/ibc-forwarder/Cargo.toml b/contracts/ibc-forwarder/Cargo.toml index c8fa381c..169541bd 100644 --- a/contracts/ibc-forwarder/Cargo.toml +++ b/contracts/ibc-forwarder/Cargo.toml @@ -23,6 +23,7 @@ library = [] [dependencies] covenant-clock-derive = { workspace = true} +covenant-depositor-derive = { workspace = true} covenant-ls = { workspace = true, features=["library"] } covenant-clock = { workspace = true, features=["library"]} cosmwasm-schema = { workspace = true } diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index 910ff330..e61bb4a7 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -2,13 +2,13 @@ use cosmos_sdk_proto::{cosmos::base::v1beta1::Coin, ibc::applications::transfer: #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{Env, MessageInfo, Response, Deps, DepsMut, StdError, Binary, Addr}; +use cosmwasm_std::{Env, MessageInfo, Response, Deps, DepsMut, StdError, Binary, Addr, to_binary}; use covenant_clock::helpers::verify_clock; use cw2::set_contract_version; use neutron_sdk::{NeutronResult, bindings::{msg::NeutronMsg, query::NeutronQuery, types::ProtobufAny}, interchain_txs::helpers::get_port_id, NeutronError,}; use prost::Message; -use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState, RemoteChainInfo}, state::{CONTRACT_STATE, CLOCK_ADDRESS, INTERCHAIN_ACCOUNTS, IBC_FEE, ICA_TIMEOUT, IBC_TRANSFER_TIMEOUT, REMOTE_CHAIN_INFO, NEXT_CONTRACT}, error::ContractError}; +use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState, RemoteChainInfo, QueryMsg}, state::{CONTRACT_STATE, CLOCK_ADDRESS, INTERCHAIN_ACCOUNTS, IBC_FEE, ICA_TIMEOUT, IBC_TRANSFER_TIMEOUT, REMOTE_CHAIN_INFO, NEXT_CONTRACT}, error::ContractError}; const CONTRACT_NAME: &str = "crates.io:covenant-ibc-forwarder"; @@ -96,8 +96,7 @@ fn try_register_ica(deps: ExecuteDeps, env: Env) -> NeutronResult NeutronResult> { - +fn try_forward_funds(env: Env, deps: ExecuteDeps) -> NeutronResult> { // first we verify whether the next contract is ready for receiving the funds let next_contract = NEXT_CONTRACT.load(deps.storage)?; @@ -179,4 +178,24 @@ fn to_proto_msg_transfer(msg: impl Message) -> NeutronResult { type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), value: Binary::from(buf), }) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult { + match msg { + // we expect to receive funds into our ICA account on the remote chain. + // if the ICA had not been opened yet, we return `None` so that the + // contract querying this will be instructed to wait and retry. + QueryMsg::DepositAddress {} => { + let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); + + let ica = if let Some((addr, _)) = INTERCHAIN_ACCOUNTS.load(deps.storage, key)? { + Some(addr) + } else { + None + }; + + Ok(to_binary(&ica)?) + }, + } } \ No newline at end of file diff --git a/contracts/ibc-forwarder/src/msg.rs b/contracts/ibc-forwarder/src/msg.rs index 0446c82a..b3090902 100644 --- a/contracts/ibc-forwarder/src/msg.rs +++ b/contracts/ibc-forwarder/src/msg.rs @@ -2,6 +2,7 @@ use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::Uint64; use covenant_clock_derive::clocked; +use covenant_depositor_derive::covenant_deposit_address; use neutron_sdk::bindings::msg::IbcFee; @@ -62,12 +63,10 @@ impl RemoteChainInfo { #[cw_serde] pub enum ExecuteMsg {} -#[cw_serde] +#[covenant_deposit_address] #[derive(QueryResponses)] -pub enum QueryMsg { - #[returns(Option)] - DepositAddress {}, -} +#[cw_serde] +pub enum QueryMsg {} #[cw_serde] pub enum ContractState { diff --git a/packages/covenant-depositor-derive/Cargo.toml b/packages/covenant-depositor-derive/Cargo.toml new file mode 100644 index 00000000..2f1c561d --- /dev/null +++ b/packages/covenant-depositor-derive/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "covenant-depositor-derive" +version = "0.0.1" +edition = "2021" +authors = ["benskey bekauz@protonmail.com"] +description = "A package for deriving covenant deposit variants" +license = "BSD-3" + + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = "1" diff --git a/packages/covenant-depositor-derive/src/lib.rs b/packages/covenant-depositor-derive/src/lib.rs new file mode 100644 index 00000000..40f86a0c --- /dev/null +++ b/packages/covenant-depositor-derive/src/lib.rs @@ -0,0 +1,51 @@ +use proc_macro::TokenStream; + +use quote::quote; +use syn::{parse_macro_input, AttributeArgs, DataEnum, DeriveInput}; + +// Merges the variants of two enums. +fn merge_variants(metadata: TokenStream, left: TokenStream, right: TokenStream) -> TokenStream { + use syn::Data::Enum; + + let args = parse_macro_input!(metadata as AttributeArgs); + if let Some(first_arg) = args.first() { + return syn::Error::new_spanned(first_arg, "macro takes no arguments") + .to_compile_error() + .into(); + } + + let mut left: DeriveInput = parse_macro_input!(left); + let right: DeriveInput = parse_macro_input!(right); + + if let ( + Enum(DataEnum { variants, .. }), + Enum(DataEnum { + variants: to_add, .. + }), + ) = (&mut left.data, right.data) + { + variants.extend(to_add.into_iter()); + + quote! { #left }.into() + } else { + syn::Error::new(left.ident.span(), "variants may only be added for enums") + .to_compile_error() + .into() + } +} + +#[proc_macro_attribute] +pub fn covenant_deposit_address(metadata: TokenStream, input: TokenStream) -> TokenStream { + merge_variants( + metadata, + input, + quote!( + enum Deposit { + /// Returns the address a contract expects to receive funds to + #[returns(Option)] + DepositAddress {}, + } + ) + .into(), + ) +} From 97cdb5af3ea126ff260b1bd72392094874e7e732 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 8 Aug 2023 21:33:09 +0200 Subject: [PATCH 017/586] covenant utils package --- packages/covenant-utils/Cargo.toml | 13 ++++++++++++ packages/covenant-utils/src/lib.rs | 33 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 packages/covenant-utils/Cargo.toml create mode 100644 packages/covenant-utils/src/lib.rs diff --git a/packages/covenant-utils/Cargo.toml b/packages/covenant-utils/Cargo.toml new file mode 100644 index 00000000..d3062292 --- /dev/null +++ b/packages/covenant-utils/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "covenant-utils" +version = "0.0.1" +edition = "2021" +authors = ["benskey bekauz@protonmail.com"] +description = "A package for common utils for covenants" +license = "BSD-3" + + +[lib] + +[dependencies] +cosmwasm-schema = { workspace = true } diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs new file mode 100644 index 00000000..2e31d140 --- /dev/null +++ b/packages/covenant-utils/src/lib.rs @@ -0,0 +1,33 @@ + +pub mod neutron_ica { + use cosmwasm_schema::cw_serde; + + #[cw_serde] + pub struct OpenAckVersion { + pub version: String, + pub controller_connection_id: String, + pub host_connection_id: String, + pub address: String, + pub encoding: String, + pub tx_type: String, + } + + /// SudoPayload is a type that stores information about a transaction that we try to execute + /// on the host chain. This is a type introduced for our convenience. + #[cw_serde] + pub struct SudoPayload { + pub message: String, + pub port_id: String, + } + + /// Serves for storing acknowledgement calls for interchain transactions + #[cw_serde] + pub enum AcknowledgementResult { + /// Success - Got success acknowledgement in sudo with array of message item types in it + Success(Vec), + /// Error - Got error acknowledgement in sudo with payload message in it and error details + Error((String, String)), + /// Timeout - Got timeout acknowledgement in sudo with payload message in it + Timeout(String), + } +} From fbb538c25ac1bb195e3631d18d4d6fa6ac29836f Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 8 Aug 2023 21:33:37 +0200 Subject: [PATCH 018/586] ibc forwarder sudo replies --- Cargo.lock | 8 + Cargo.toml | 2 +- contracts/ibc-forwarder/Cargo.toml | 1 + contracts/ibc-forwarder/src/contract.rs | 209 ++++++++++++++++++++++-- contracts/ibc-forwarder/src/lib.rs | 2 +- contracts/ibc-forwarder/src/msg.rs | 16 +- contracts/ibc-forwarder/src/state.rs | 3 + 7 files changed, 226 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16e11a77..eb6ca1ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -455,6 +455,7 @@ dependencies = [ "covenant-clock-derive", "covenant-depositor-derive", "covenant-ls", + "covenant-utils", "cw-multi-test", "cw-storage-plus 1.1.0", "cw-utils 1.0.1", @@ -524,6 +525,13 @@ dependencies = [ "thiserror", ] +[[package]] +name = "covenant-utils" +version = "0.0.1" +dependencies = [ + "cosmwasm-schema", +] + [[package]] name = "cpufeatures" version = "0.2.9" diff --git a/Cargo.toml b/Cargo.toml index ae832622..3a0bc803 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ clock-derive = { path = "packages/clock-derive" } cw-fifo = { path = "packages/cw-fifo" } covenant-clock-derive = { path = "packages/clock-derive" } covenant-depositor-derive = { path = "packages/covenant-depositor-derive" } - +covenant-utils = { path = "packages/covenant-utils" } # the sha2 version here is the same as the one used by # cosmwasm-std. when bumping cosmwasm-std, this should also be # updated. to find cosmwasm_std's sha function: diff --git a/contracts/ibc-forwarder/Cargo.toml b/contracts/ibc-forwarder/Cargo.toml index 169541bd..afdefc07 100644 --- a/contracts/ibc-forwarder/Cargo.toml +++ b/contracts/ibc-forwarder/Cargo.toml @@ -43,6 +43,7 @@ base64 = { workspace = true } prost = { workspace = true } prost-types = { workspace = true } bech32 = { workspace = true } +covenant-utils = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index e61bb4a7..3496c177 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -1,19 +1,20 @@ -use cosmos_sdk_proto::{cosmos::base::v1beta1::Coin, ibc::applications::transfer::v1::MsgTransfer}; +use cosmos_sdk_proto::ibc::applications::transfer::v1::MsgTransfer; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; - -use cosmwasm_std::{Env, MessageInfo, Response, Deps, DepsMut, StdError, Binary, Addr, to_binary}; +use covenant_utils::neutron_ica; +use cosmwasm_std::{Env, MessageInfo, Response, Deps, DepsMut, StdError, Binary, Addr, to_binary, StdResult, Storage, to_vec, CosmosMsg, SubMsg, Reply, from_binary}; use covenant_clock::helpers::verify_clock; use cw2::set_contract_version; -use neutron_sdk::{NeutronResult, bindings::{msg::NeutronMsg, query::NeutronQuery, types::ProtobufAny}, interchain_txs::helpers::get_port_id, NeutronError,}; +use neutron_sdk::{NeutronResult, bindings::{msg::{NeutronMsg, MsgSubmitTxResponse}, query::NeutronQuery, types::ProtobufAny}, interchain_txs::helpers::get_port_id, NeutronError, sudo::msg::{SudoMsg, RequestPacket},}; use prost::Message; -use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState, RemoteChainInfo, QueryMsg}, state::{CONTRACT_STATE, CLOCK_ADDRESS, INTERCHAIN_ACCOUNTS, IBC_FEE, ICA_TIMEOUT, IBC_TRANSFER_TIMEOUT, REMOTE_CHAIN_INFO, NEXT_CONTRACT}, error::ContractError}; +use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState, RemoteChainInfo, QueryMsg}, state::{CONTRACT_STATE, CLOCK_ADDRESS, INTERCHAIN_ACCOUNTS, IBC_FEE, ICA_TIMEOUT, IBC_TRANSFER_TIMEOUT, REMOTE_CHAIN_INFO, NEXT_CONTRACT, REPLY_ID_STORAGE, SUDO_PAYLOAD}}; const CONTRACT_NAME: &str = "crates.io:covenant-ibc-forwarder"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); const INTERCHAIN_ACCOUNT_ID: &str = "ica"; +pub const SUDO_PAYLOAD_REPLY_ID: u64 = 1; type QueryDeps<'a> = Deps<'a, NeutronQuery>; type ExecuteDeps<'a> = DepsMut<'a, NeutronQuery>; @@ -45,7 +46,8 @@ pub fn instantiate( Ok(Response::default() .add_attribute("method", "ibc_forwarder_instantiate") - + .add_attribute("next_contract", next_contract) + .add_attribute("contract_state", "instantiated") ) } @@ -61,7 +63,6 @@ pub fn execute( } } - /// attempts to advance the state machine. validates the caller to be the clock. fn try_tick(deps: ExecuteDeps, env: Env, info: MessageInfo) -> NeutronResult> { // Verify caller is the clock @@ -71,7 +72,9 @@ fn try_tick(deps: ExecuteDeps, env: Env, info: MessageInfo) -> NeutronResult try_register_ica(deps, env), ContractState::ICACreated => try_forward_funds(env, deps), - ContractState::Complete => todo!(), + ContractState::Complete => Ok(Response::default() + .add_attribute("contract_state", "completed") + ), } } @@ -96,7 +99,7 @@ fn try_register_ica(deps: ExecuteDeps, env: Env) -> NeutronResult NeutronResult> { +fn try_forward_funds(env: Env, mut deps: ExecuteDeps) -> NeutronResult> { // first we verify whether the next contract is ready for receiving the funds let next_contract = NEXT_CONTRACT.load(deps.storage)?; @@ -155,8 +158,18 @@ fn try_forward_funds(env: Env, deps: ExecuteDeps) -> NeutronResult Ok(Response::default() .add_attribute("method", "try_forward_funds") @@ -165,6 +178,15 @@ fn try_forward_funds(env: Env, deps: ExecuteDeps) -> NeutronResult>, T>( + deps: ExecuteDeps, + msg: C, + payload: neutron_ica::SudoPayload, +) -> StdResult> { + save_reply_payload(deps.storage, payload)?; + Ok(SubMsg::reply_on_success(msg, SUDO_PAYLOAD_REPLY_ID)) +} + /// helper that serializes a MsgTransfer to protobuf fn to_proto_msg_transfer(msg: impl Message) -> NeutronResult { // Serialize the Transfer message @@ -181,7 +203,7 @@ fn to_proto_msg_transfer(msg: impl Message) -> NeutronResult { } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult { +pub fn query(deps: QueryDeps, env: Env, msg: QueryMsg) -> NeutronResult { match msg { // we expect to receive funds into our ICA account on the remote chain. // if the ICA had not been opened yet, we return `None` so that the @@ -198,4 +220,167 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult Ok(to_binary(&ica)?) }, } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn sudo(deps: ExecuteDeps, env: Env, msg: SudoMsg) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo: received sudo msg: {msg:?}").as_str()); + + match msg { + // For handling successful (non-error) acknowledgements. + SudoMsg::Response { request, data } => sudo_response(deps, request, data), + + // For handling error acknowledgements. + SudoMsg::Error { request, details } => sudo_error(deps, request, details), + + // For handling error timeouts. + SudoMsg::Timeout { request } => sudo_timeout(deps, env, request), + + // For handling successful registering of ICA + SudoMsg::OpenAck { + port_id, + channel_id, + counterparty_channel_id, + counterparty_version, + } => sudo_open_ack( + deps, + env, + port_id, + channel_id, + counterparty_channel_id, + counterparty_version, + ), + _ => Ok(Response::default()), + } +} + + +// handler +fn sudo_open_ack( + deps: ExecuteDeps, + _env: Env, + port_id: String, + _channel_id: String, + _counterparty_channel_id: String, + counterparty_version: String, +) -> StdResult { + // The version variable contains a JSON value with multiple fields, + // including the generated account address. + let parsed_version: Result = + serde_json_wasm::from_str(counterparty_version.as_str()); + + // Update the storage record associated with the interchain account. + if let Ok(parsed_version) = parsed_version { + INTERCHAIN_ACCOUNTS.save( + deps.storage, + port_id, + &Some(( + parsed_version.clone().address, + parsed_version.clone().controller_connection_id, + )), + )?; + CONTRACT_STATE.save(deps.storage, &ContractState::ICACreated)?; + return Ok(Response::default().add_attribute("method", "sudo_open_ack")); + } + Err(StdError::generic_err("Can't parse counterparty_version")) +} + +fn sudo_response(deps: ExecuteDeps, request: RequestPacket, data: Binary) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo_response: sudo received: {request:?} {data:?}").as_str()); + + // either of these errors will close the channel + let seq_id = request + .sequence + .ok_or_else(|| StdError::generic_err("sequence not found"))?; + + let channel_id = request + .source_channel + .ok_or_else(|| StdError::generic_err("channel_id not found"))?; + + Ok(Response::default() + .add_attribute("method", "sudo_response") + ) +} + +fn sudo_timeout(deps: ExecuteDeps, _env: Env, request: RequestPacket) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo timeout request: {request:?}").as_str()); + + // revert the state to Instantiated to force re-creation of ICA + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + + // returning Ok as this is anticipated. channel is already closed. + Ok(Response::default()) +} + +fn sudo_error(deps: ExecuteDeps, request: RequestPacket, details: String) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo error: {details}").as_str()); + + deps.api + .debug(format!("WASMDEBUG: request packet: {request:?}").as_str()); + + // either of these errors will close the channel + request + .sequence + .ok_or_else(|| StdError::generic_err("sequence not found"))?; + + request + .source_channel + .ok_or_else(|| StdError::generic_err("channel_id not found"))?; + + Ok(Response::default().add_attribute("method", "sudo_error")) +} + + +pub fn save_reply_payload(store: &mut dyn Storage, payload: neutron_ica::SudoPayload) -> StdResult<()> { + REPLY_ID_STORAGE.save(store, &to_vec(&payload)?) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: ExecuteDeps, env: Env, msg: Reply) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: reply msg: {msg:?}").as_str()); + match msg.id { + SUDO_PAYLOAD_REPLY_ID => prepare_sudo_payload(deps, env, msg), + _ => Err(StdError::generic_err(format!( + "unsupported reply message id {}", + msg.id + ))), + } +} + +fn prepare_sudo_payload(mut deps: ExecuteDeps, _env: Env, msg: Reply) -> StdResult { + let payload = read_reply_payload(deps.storage)?; + let resp: MsgSubmitTxResponse = serde_json_wasm::from_slice( + msg.result + .into_result() + .map_err(StdError::generic_err)? + .data + .ok_or_else(|| StdError::generic_err("no result"))? + .as_slice(), + ) + .map_err(|e| StdError::generic_err(format!("failed to parse response: {e:?}")))?; + deps.api + .debug(format!("WASMDEBUG: reply msg: {resp:?}").as_str()); + let seq_id = resp.sequence_id; + let channel_id = resp.channel; + save_sudo_payload(deps.branch().storage, channel_id, seq_id, payload)?; + Ok(Response::new()) +} + +pub fn read_reply_payload(store: &mut dyn Storage) -> StdResult { + let data = REPLY_ID_STORAGE.load(store)?; + from_binary(&Binary(data)) +} + +pub fn save_sudo_payload( + store: &mut dyn Storage, + channel_id: String, + seq_id: u64, + payload: neutron_ica::SudoPayload, +) -> StdResult<()> { + SUDO_PAYLOAD.save(store, (channel_id, seq_id), &to_vec(&payload)?) } \ No newline at end of file diff --git a/contracts/ibc-forwarder/src/lib.rs b/contracts/ibc-forwarder/src/lib.rs index 52fbe389..0faea8f4 100644 --- a/contracts/ibc-forwarder/src/lib.rs +++ b/contracts/ibc-forwarder/src/lib.rs @@ -5,4 +5,4 @@ extern crate core; pub mod contract; pub mod error; pub mod msg; -pub mod state; \ No newline at end of file +pub mod state; diff --git a/contracts/ibc-forwarder/src/msg.rs b/contracts/ibc-forwarder/src/msg.rs index b3090902..b9de2968 100644 --- a/contracts/ibc-forwarder/src/msg.rs +++ b/contracts/ibc-forwarder/src/msg.rs @@ -1,6 +1,6 @@ use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Uint64; +use cosmwasm_std::{Uint64, Attribute}; use covenant_clock_derive::clocked; use covenant_depositor_derive::covenant_deposit_address; use neutron_sdk::bindings::msg::IbcFee; @@ -40,6 +40,20 @@ pub struct InstantiateMsg { pub ica_timeout: Uint64, } +impl InstantiateMsg { + pub fn get_response_attributes(&self) -> Vec { + vec![ + Attribute::new("clock_address", &self.clock_address), + Attribute::new("remote_chain_connection_id", &self.remote_chain_connection_id), + Attribute::new("remote_chain_channel_id", &self.remote_chain_channel_id), + Attribute::new("remote_chain_denom", &self.denom), + Attribute::new("remote_chain_amount", &self.amount), + Attribute::new("ibc_transfer_timeout", self.ibc_transfer_timeout.to_string()), + Attribute::new("ica_timeout", self.ica_timeout.to_string()), + ] + } +} + #[cw_serde] pub struct RemoteChainInfo { /// connection id from neutron to the remote chain on which diff --git a/contracts/ibc-forwarder/src/state.rs b/contracts/ibc-forwarder/src/state.rs index 02979595..b244fc2b 100644 --- a/contracts/ibc-forwarder/src/state.rs +++ b/contracts/ibc-forwarder/src/state.rs @@ -33,3 +33,6 @@ pub const IBC_FEE: Item = Item::new("ibc_fee"); /// interchain accounts storage in form of (port_id) -> (address, controller_connection_id) pub const INTERCHAIN_ACCOUNTS: Map> = Map::new("interchain_accounts"); + +pub const REPLY_ID_STORAGE: Item> = Item::new("reply_queue_id"); +pub const SUDO_PAYLOAD: Map<(String, u64), Vec> = Map::new("sudo_payload"); From 2674dcd8a329ec123de6ee0c2f6b5bee493ef6af Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 9 Aug 2023 19:50:07 +0200 Subject: [PATCH 019/586] merging ibc fields into remote chain info --- contracts/ibc-forwarder/src/contract.rs | 24 ++++++++++-------------- contracts/ibc-forwarder/src/msg.rs | 3 +++ contracts/ibc-forwarder/src/state.rs | 12 ++++++------ 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index 3496c177..2557acde 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -8,7 +8,7 @@ use cw2::set_contract_version; use neutron_sdk::{NeutronResult, bindings::{msg::{NeutronMsg, MsgSubmitTxResponse}, query::NeutronQuery, types::ProtobufAny}, interchain_txs::helpers::get_port_id, NeutronError, sudo::msg::{SudoMsg, RequestPacket},}; use prost::Message; -use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState, RemoteChainInfo, QueryMsg}, state::{CONTRACT_STATE, CLOCK_ADDRESS, INTERCHAIN_ACCOUNTS, IBC_FEE, ICA_TIMEOUT, IBC_TRANSFER_TIMEOUT, REMOTE_CHAIN_INFO, NEXT_CONTRACT, REPLY_ID_STORAGE, SUDO_PAYLOAD}}; +use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState, RemoteChainInfo, QueryMsg}, state::{CONTRACT_STATE, CLOCK_ADDRESS, INTERCHAIN_ACCOUNTS, REMOTE_CHAIN_INFO, NEXT_CONTRACT, REPLY_ID_STORAGE, SUDO_PAYLOAD}}; const CONTRACT_NAME: &str = "crates.io:covenant-ibc-forwarder"; @@ -30,10 +30,6 @@ pub fn instantiate( CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; - // ibc fees and timeouts - IBC_FEE.save(deps.storage, &msg.ibc_fee)?; - ICA_TIMEOUT.save(deps.storage, &msg.ica_timeout)?; - IBC_TRANSFER_TIMEOUT.save(deps.storage, &msg.ibc_transfer_timeout)?; let next_contract = deps.api.addr_validate(&msg.next_contract)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; @@ -42,6 +38,9 @@ pub fn instantiate( channel_id: msg.remote_chain_channel_id, denom: msg.denom, amount: msg.amount, + ibc_fee: msg.ibc_fee, + ica_timeout: msg.ica_timeout, + ibc_transfer_timeout: msg.ibc_transfer_timeout, })?; Ok(Response::default() @@ -125,9 +124,6 @@ fn try_forward_funds(env: Env, mut deps: ExecuteDeps) -> NeutronResult { - let ibc_transfer_timeout = IBC_TRANSFER_TIMEOUT.load(deps.storage)?; - let ica_timeout = ICA_TIMEOUT.load(deps.storage)?; - let fee = IBC_FEE.load(deps.storage)?; let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; let coin = remote_chain_info.proto_coin(); @@ -140,8 +136,8 @@ fn try_forward_funds(env: Env, mut deps: ExecuteDeps) -> NeutronResult NeutronResult Std .debug(format!("WASMDEBUG: sudo_response: sudo received: {request:?} {data:?}").as_str()); // either of these errors will close the channel - let seq_id = request + request .sequence .ok_or_else(|| StdError::generic_err("sequence not found"))?; - let channel_id = request + request .source_channel .ok_or_else(|| StdError::generic_err("channel_id not found"))?; diff --git a/contracts/ibc-forwarder/src/msg.rs b/contracts/ibc-forwarder/src/msg.rs index b9de2968..37bc7248 100644 --- a/contracts/ibc-forwarder/src/msg.rs +++ b/contracts/ibc-forwarder/src/msg.rs @@ -62,6 +62,9 @@ pub struct RemoteChainInfo { pub channel_id: String, pub denom: String, pub amount: String, + pub ibc_transfer_timeout: Uint64, + pub ica_timeout: Uint64, + pub ibc_fee: IbcFee, } impl RemoteChainInfo { diff --git a/contracts/ibc-forwarder/src/state.rs b/contracts/ibc-forwarder/src/state.rs index b244fc2b..b7d43f2c 100644 --- a/contracts/ibc-forwarder/src/state.rs +++ b/contracts/ibc-forwarder/src/state.rs @@ -17,12 +17,12 @@ pub const NEXT_CONTRACT: Item = Item::new("next_contract"); /// information needed for an ibc transfer to the remote chain pub const REMOTE_CHAIN_INFO: Item = Item::new("r_c_info"); -/// timeout in seconds for inner ibc MsgTransfer -pub const IBC_TRANSFER_TIMEOUT: Item = Item::new("ibc_transfer_timeout"); -/// time in seconds for ICA SubmitTX messages from neutron -pub const ICA_TIMEOUT: Item = Item::new("ica_timeout"); -/// neutron IbcFee for relayers -pub const IBC_FEE: Item = Item::new("ibc_fee"); +// /// timeout in seconds for inner ibc MsgTransfer +// pub const IBC_TRANSFER_TIMEOUT: Item = Item::new("ibc_transfer_timeout"); +// /// time in seconds for ICA SubmitTX messages from neutron +// pub const ICA_TIMEOUT: Item = Item::new("ica_timeout"); +// /// neutron IbcFee for relayers +// pub const IBC_FEE: Item = Item::new("ibc_fee"); /// id of the connection between neutron and remote chain on which we From d7fb92d2379661b62368977137a68415ae0f2291 Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 9 Aug 2023 22:48:15 +0200 Subject: [PATCH 020/586] pr feedback --- contracts/ibc-forwarder/src/contract.rs | 34 ++++++++++++++--------- contracts/ls/src/contract.rs | 36 +++++++++++++------------ 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index 2557acde..430e39d0 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -108,9 +108,7 @@ fn try_forward_funds(env: Env, mut deps: ExecuteDeps) -> NeutronResult NeutronResult Ok(Response::default() - .add_attribute("method", "try_forward_funds") - .add_attribute("error", "no_ica_found") - ), + None => { + // I can't think of a case of how we could end up here as `sudo_open_ack` + // callback advances the state to `ICACreated` and stores the ICA. + // just in case, we revert the state to `Instantiated` to restart the flow. + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + Ok(Response::default() + .add_attribute("method", "try_forward_funds") + .add_attribute("error", "no_ica_found") + ) + }, } } @@ -206,11 +210,17 @@ pub fn query(deps: QueryDeps, env: Env, msg: QueryMsg) -> NeutronResult // contract querying this will be instructed to wait and retry. QueryMsg::DepositAddress {} => { let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); - - let ica = if let Some((addr, _)) = INTERCHAIN_ACCOUNTS.load(deps.storage, key)? { - Some(addr) - } else { - None + // here we want to return None instead of any errors in case no ICA + // is registered yet + let ica = match INTERCHAIN_ACCOUNTS.may_load(deps.storage, key)? { + Some(entry) => { + if let Some((addr, _)) = entry { + Some(addr) + } else { + None + } + }, + None => None, }; Ok(to_binary(&ica)?) diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs index fb003445..bf18c289 100644 --- a/contracts/ls/src/contract.rs +++ b/contracts/ls/src/contract.rs @@ -358,24 +358,26 @@ fn sudo_open_ack( // including the generated account address. let parsed_version: Result = serde_json_wasm::from_str(counterparty_version.as_str()); - + + // get the parsed OpenAckVersion or return an error if we fail + let Ok(parsed_version) = parsed_version else { + return Err(StdError::generic_err("Can't parse counterparty_version")) + }; + // Update the storage record associated with the interchain account. - if let Ok(parsed_version) = parsed_version { - INTERCHAIN_ACCOUNTS.save( - deps.storage, - port_id, - &Some(( - parsed_version.clone().address, - parsed_version.controller_connection_id, - )), - )?; - // we advance the state now that the channel is established - CONTRACT_STATE.save(deps.storage, &ContractState::ICACreated)?; - return Ok(Response::default() - .add_attribute("method", "sudo_open_ack") - ) - } - Err(StdError::generic_err("Can't parse counterparty_version")) + INTERCHAIN_ACCOUNTS.save( + deps.storage, + port_id, + &Some(( + parsed_version.clone().address, + parsed_version.clone().controller_connection_id, + )), + )?; + CONTRACT_STATE.save(deps.storage, &ContractState::ICACreated)?; + + return Ok(Response::default() + .add_attribute("method", "sudo_open_ack") + ) } fn sudo_response(deps: DepsMut, request: RequestPacket, data: Binary) -> StdResult { From fe671427017ed83f9e1532472e7c054582398d99 Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 9 Aug 2023 18:57:13 +0200 Subject: [PATCH 021/586] ls deposit address query --- Cargo.lock | 1 + contracts/covenant/src/suite_test/suite.rs | 1 + .../covenant/src/suite_test/unit_tests.rs | 1 + contracts/ls/Cargo.toml | 1 + contracts/ls/src/contract.rs | 28 ++++++++++++++++++- contracts/ls/src/msg.rs | 7 +++++ contracts/ls/src/state.rs | 4 +++ 7 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index eb6ca1ff..7d1f7f3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -515,6 +515,7 @@ dependencies = [ "cosmwasm-std", "covenant-clock", "covenant-clock-derive", + "covenant-depositor-derive", "cw-storage-plus 1.1.0", "cw2 1.1.0", "neutron-sdk", diff --git a/contracts/covenant/src/suite_test/suite.rs b/contracts/covenant/src/suite_test/suite.rs index 2d24c2ee..5fbacc71 100644 --- a/contracts/covenant/src/suite_test/suite.rs +++ b/contracts/covenant/src/suite_test/suite.rs @@ -64,6 +64,7 @@ impl Default for SuiteBuilder { ls_denom: "stuatom".to_string(), stride_neutron_ibc_transfer_channel_id: TODO.to_string(), neutron_stride_ibc_connection_id: TODO.to_string(), + autopilot_format: "{{\"autopilot\": {{\"receiver\": \"{st_ica}\",\"stakeibc\": {{\"stride_address\": \"{st_ica}\",\"action\": \"LiquidStake\"}}}}}}".to_string(), }, preset_depositor_fields: covenant_depositor::msg::PresetDepositorFields { gaia_neutron_ibc_transfer_channel_id: TODO.to_string(), diff --git a/contracts/covenant/src/suite_test/unit_tests.rs b/contracts/covenant/src/suite_test/unit_tests.rs index e7617812..6c065a5b 100644 --- a/contracts/covenant/src/suite_test/unit_tests.rs +++ b/contracts/covenant/src/suite_test/unit_tests.rs @@ -33,6 +33,7 @@ fn get_init_msg() -> InstantiateMsg { ls_denom: "stuatom".to_string(), stride_neutron_ibc_transfer_channel_id: TODO.to_string(), neutron_stride_ibc_connection_id: TODO.to_string(), + autopilot_format: "{{\"autopilot\": {{\"receiver\": \"{st_ica}\",\"stakeibc\": {{\"stride_address\": \"{st_ica}\",\"action\": \"LiquidStake\"}}}}}}".to_string(), }, preset_depositor_fields: covenant_depositor::msg::PresetDepositorFields { gaia_neutron_ibc_transfer_channel_id: TODO.to_string(), diff --git a/contracts/ls/Cargo.toml b/contracts/ls/Cargo.toml index 6b0af025..568779ad 100644 --- a/contracts/ls/Cargo.toml +++ b/contracts/ls/Cargo.toml @@ -19,6 +19,7 @@ library = [] [dependencies] covenant-clock-derive = { workspace = true } covenant-clock = { workspace = true, features=["library"] } +covenant-depositor-derive = { workspace = true} cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs index bf18c289..336fd329 100644 --- a/contracts/ls/src/contract.rs +++ b/contracts/ls/src/contract.rs @@ -19,7 +19,7 @@ use crate::state::{ add_error_to_queue, read_errors_from_queue, read_reply_payload, read_sudo_payload, save_reply_payload, save_sudo_payload, ACKNOWLEDGEMENT_RESULTS, CLOCK_ADDRESS, CONTRACT_STATE, IBC_FEE, IBC_TRANSFER_TIMEOUT, ICA_TIMEOUT, INTERCHAIN_ACCOUNTS, LP_ADDRESS, LS_DENOM, - NEUTRON_STRIDE_IBC_CONNECTION_ID, STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID, + NEUTRON_STRIDE_IBC_CONNECTION_ID, STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID, AUTOPILOT_FORMAT, }; use neutron_sdk::{ bindings::{ @@ -266,6 +266,32 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult sequence_id, } => query_acknowledgement_result(deps, env, interchain_account_id, sequence_id), QueryMsg::ErrorsQueue {} => query_errors_queue(deps), + QueryMsg::DepositAddress {} => { + let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); + + // here we cover three cases: + let ica = match INTERCHAIN_ACCOUNTS.may_load(deps.storage, key)? { + Some(entry) => { + // 1. ICA had been created -> fetch the autopilot string and return Some(autopilot) + if let Some((addr, _)) = entry { + let autopilot_receiver = AUTOPILOT_FORMAT + .load(deps.storage)? + .replace("{st_ica}", &addr); + + Some(autopilot_receiver) + } + // 2. ICA creation request had been submitted but did not receive + // the channel_open_ack yet -> None + else { + None + } + }, + // 3. ICA creation request hadn't been submitted yet -> None + None => None, + }; + // up to the querying module to make sense of the response + Ok(to_binary(&ica)?) + }, } } diff --git a/contracts/ls/src/msg.rs b/contracts/ls/src/msg.rs index dc9f9752..1d754b35 100644 --- a/contracts/ls/src/msg.rs +++ b/contracts/ls/src/msg.rs @@ -1,6 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Binary, Uint128, Uint64}; use covenant_clock_derive::clocked; +use covenant_depositor_derive::covenant_deposit_address; use neutron_sdk::bindings::msg::IbcFee; #[cw_serde] @@ -38,6 +39,9 @@ pub struct InstantiateMsg { /// if the ICA times out, the destination chain receiving the funds /// will also receive the IBC packet with an expired timestamp. pub ibc_transfer_timeout: Uint64, + /// json formatted string meant to be used for one-click + /// liquid staking on stride + pub autopilot_format: String, } #[cw_serde] @@ -47,6 +51,7 @@ pub struct PresetLsFields { pub ls_denom: String, pub stride_neutron_ibc_transfer_channel_id: String, pub neutron_stride_ibc_connection_id: String, + pub autopilot_format: String, } impl PresetLsFields { @@ -67,6 +72,7 @@ impl PresetLsFields { ibc_fee, ica_timeout, ibc_transfer_timeout, + autopilot_format: self.autopilot_format, } } } @@ -80,6 +86,7 @@ pub enum ExecuteMsg { Transfer { amount: Uint128 }, } +#[covenant_deposit_address] #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { diff --git a/contracts/ls/src/state.rs b/contracts/ls/src/state.rs index 2f3c0ea2..2900e7c8 100644 --- a/contracts/ls/src/state.rs +++ b/contracts/ls/src/state.rs @@ -31,6 +31,10 @@ pub const ICA_TIMEOUT: Item = Item::new("ica_timeout"); /// neutron IbcFee for relayers pub const IBC_FEE: Item = Item::new("ibc_fee"); +/// formatting of stride autopilot message. +/// we use string match & replace with relevant fields to obtain the valid message. +pub const AUTOPILOT_FORMAT: Item = Item::new("autopilot_format"); + /// interchain transaction responses - ack/err/timeout state to query later pub const ACKNOWLEDGEMENT_RESULTS: Map<(String, u64), AcknowledgementResult> = Map::new("acknowledgement_results"); From c37bd1615d06da9c27956f63c446d149f2b94cb5 Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 9 Aug 2023 19:45:00 +0200 Subject: [PATCH 022/586] querying next contract for deposit address --- contracts/ls/src/contract.rs | 53 ++++++++++++++++++++---------------- contracts/ls/src/msg.rs | 13 ++++----- contracts/ls/src/state.rs | 4 +-- 3 files changed, 37 insertions(+), 33 deletions(-) diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs index 336fd329..c0d75ca8 100644 --- a/contracts/ls/src/contract.rs +++ b/contracts/ls/src/contract.rs @@ -12,21 +12,21 @@ use cw2::set_contract_version; use neutron_sdk::bindings::types::ProtobufAny; use crate::msg::{ - AcknowledgementResult, ContractState, ExecuteMsg, InstantiateMsg, MigrateMsg, OpenAckVersion, + ContractState, ExecuteMsg, InstantiateMsg, MigrateMsg, OpenAckVersion, QueryMsg, SudoPayload, }; use crate::state::{ - add_error_to_queue, read_errors_from_queue, read_reply_payload, read_sudo_payload, + read_errors_from_queue, read_reply_payload, save_reply_payload, save_sudo_payload, ACKNOWLEDGEMENT_RESULTS, CLOCK_ADDRESS, CONTRACT_STATE, - IBC_FEE, IBC_TRANSFER_TIMEOUT, ICA_TIMEOUT, INTERCHAIN_ACCOUNTS, LP_ADDRESS, LS_DENOM, - NEUTRON_STRIDE_IBC_CONNECTION_ID, STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID, AUTOPILOT_FORMAT, + IBC_FEE, IBC_TRANSFER_TIMEOUT, ICA_TIMEOUT, INTERCHAIN_ACCOUNTS, LS_DENOM, + NEUTRON_STRIDE_IBC_CONNECTION_ID, STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID, AUTOPILOT_FORMAT, NEXT_CONTRACT, }; use neutron_sdk::{ bindings::{ msg::{MsgSubmitTxResponse, NeutronMsg}, query::{NeutronQuery, QueryInterchainAccountAddressResponse}, }, - interchain_txs::helpers::{decode_acknowledgement_response, get_port_id}, + interchain_txs::helpers::get_port_id, sudo::msg::{RequestPacket, SudoMsg}, NeutronError, NeutronResult, }; @@ -53,9 +53,9 @@ pub fn instantiate( // validate and store other module addresses let clock_addr = deps.api.addr_validate(&msg.clock_address)?; - let lp_address = deps.api.addr_validate(&msg.lp_address)?; + let next_contract = deps.api.addr_validate(&msg.next_contract)?; CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; - LP_ADDRESS.save(deps.storage, &lp_address)?; + NEXT_CONTRACT.save(deps.storage, &next_contract)?; // store all fields relevant to ICA operations STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID @@ -69,7 +69,7 @@ pub fn instantiate( Ok(Response::default() .add_attribute("method", "ls_instantiate") .add_attribute("clock_address", clock_addr) - .add_attribute("lp_address", lp_address) + .add_attribute("next_contract", next_contract) .add_attribute( "stride_neutron_ibc_transfer_channel_id", msg.stride_neutron_ibc_transfer_channel_id, @@ -95,14 +95,6 @@ pub fn execute( match msg { ExecuteMsg::Tick {} => try_tick(deps, env, info), ExecuteMsg::Transfer { amount } => { - // let state = CONTRACT_STATE.load(deps.storage)?; - // match state { - // ContractState::Instantiated => Ok(Response::default() - // .add_attribute("method", "permisionless_transfer") - // .add_attribute("status", "no_ica") - // ), - // ContractState::ICACreated => try_execute_transfer(deps, env, info, amount), - // } let ica_address = get_ica(deps.as_ref(), &env, INTERCHAIN_ACCOUNT_ID); match ica_address { Ok((_, _)) => { @@ -155,6 +147,23 @@ fn try_execute_transfer( _info: MessageInfo, amount: Uint128, ) -> NeutronResult> { + + // first we verify whether the next contract is ready for receiving the funds + let next_contract = NEXT_CONTRACT.load(deps.storage)?; + let deposit_address_query: Option = deps.querier.query_wasm_smart( + next_contract, + &crate::msg::QueryMsg::DepositAddress {}, + )?; + + // if query returns None, then we error and wait + let deposit_address = if let Some(addr) = deposit_address_query { + addr + } else { + return Err(NeutronError::Std( + StdError::not_found("Next contract is not ready for receiving the funds yet") + )) + }; + let port_id = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); let interchain_account = INTERCHAIN_ACCOUNTS.load(deps.storage, port_id.clone())?; @@ -162,7 +171,6 @@ fn try_execute_transfer( Some((address, controller_conn_id)) => { let fee = IBC_FEE.load(deps.storage)?; let source_channel = STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID.load(deps.storage)?; - let lp_receiver = LP_ADDRESS.load(deps.storage)?; let denom = LS_DENOM.load(deps.storage)?; let ibc_transfer_timeout = IBC_TRANSFER_TIMEOUT.load(deps.storage)?; let ica_timeout = ICA_TIMEOUT.load(deps.storage)?; @@ -180,7 +188,7 @@ fn try_execute_transfer( source_channel, token: Some(coin), sender: address, - receiver: lp_receiver.to_string(), + receiver: deposit_address.to_string(), timeout_height: None, timeout_timestamp: env .block @@ -243,7 +251,6 @@ fn msg_with_sudo_callback>, T>( #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult { match msg { - QueryMsg::LpAddress {} => Ok(to_binary(&LP_ADDRESS.may_load(deps.storage)?)?), QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), QueryMsg::StrideICA {} => Ok(to_binary(&Addr::unchecked( get_ica(deps, &env, INTERCHAIN_ACCOUNT_ID)?.0, @@ -511,7 +518,7 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult MigrateMsg::UpdateConfig { clock_addr, stride_neutron_ibc_transfer_channel_id, - lp_address, + next_contract, neutron_stride_ibc_connection_id, ls_denom, ibc_fee, @@ -531,10 +538,10 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult resp = resp.add_attribute("stride_neutron_ibc_transfer_channel_id", channel_id); } - if let Some(addr) = lp_address { + if let Some(addr) = next_contract { let addr = deps.api.addr_validate(&addr)?; - resp = resp.add_attribute("lp_address", addr.to_string()); - LP_ADDRESS.save(deps.storage, &addr)?; + resp = resp.add_attribute("next_contract", addr.to_string()); + NEXT_CONTRACT.save(deps.storage, &addr)?; } if let Some(connection_id) = neutron_stride_ibc_connection_id { diff --git a/contracts/ls/src/msg.rs b/contracts/ls/src/msg.rs index 1d754b35..5f327fb7 100644 --- a/contracts/ls/src/msg.rs +++ b/contracts/ls/src/msg.rs @@ -16,9 +16,8 @@ pub struct InstantiateMsg { /// IBC connection ID on Neutron for Stride /// We make an Interchain Account over this connection pub neutron_stride_ibc_connection_id: String, - /// Address for the covenant's LP contract. - /// We send the liquid staked amount to this address - pub lp_address: String, + /// Address of the next contract to query for the deposit address + pub next_contract: String, /// The liquid staked denom (e.g., stuatom). This is /// required because we only allow transfers of this denom /// out of the LSer @@ -58,7 +57,7 @@ impl PresetLsFields { pub fn to_instantiate_msg( self, clock_address: String, - lp_address: String, + next_contract: String, ibc_fee: IbcFee, ica_timeout: Uint64, ibc_transfer_timeout: Uint64, @@ -67,7 +66,7 @@ impl PresetLsFields { clock_address, stride_neutron_ibc_transfer_channel_id: self.stride_neutron_ibc_transfer_channel_id, neutron_stride_ibc_connection_id: self.neutron_stride_ibc_connection_id, - lp_address, + next_contract, ls_denom: self.ls_denom, ibc_fee, ica_timeout, @@ -94,8 +93,6 @@ pub enum QueryMsg { ClockAddress {}, #[returns(Addr)] StrideICA {}, - #[returns(Addr)] - LpAddress {}, #[returns(ContractState)] ContractState {}, #[returns(String)] @@ -126,7 +123,7 @@ pub enum MigrateMsg { UpdateConfig { clock_addr: Option, stride_neutron_ibc_transfer_channel_id: Option, - lp_address: Option, + next_contract: Option, neutron_stride_ibc_connection_id: Option, ls_denom: Option, ibc_fee: Option, diff --git a/contracts/ls/src/state.rs b/contracts/ls/src/state.rs index 2900e7c8..a65460a6 100644 --- a/contracts/ls/src/state.rs +++ b/contracts/ls/src/state.rs @@ -9,8 +9,8 @@ pub const CONTRACT_STATE: Item = Item::new("contract_state"); /// clock module address to verify the sender of incoming ticks pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); -/// liquid pooler module address to forward the liquid staked funds to -pub const LP_ADDRESS: Item = Item::new("lp_address"); +/// next contract address to forward the liquid staked funds to +pub const NEXT_CONTRACT: Item = Item::new("next_contract"); /// IBC transfer channel on stride for neutron pub const STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID: Item = Item::new("sn_ibc_chann_id"); From 5505850c4962399d05017ba8457e298661ff103e Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 10 Aug 2023 12:18:05 +0200 Subject: [PATCH 023/586] merging covenant-clock-derive and covenant-depositor-derive into covenant-macros --- Cargo.lock | 39 +++++--------- Cargo.toml | 3 +- contracts/clock/Cargo.toml | 2 +- contracts/clock/src/msg.rs | 2 +- contracts/depositor/Cargo.toml | 2 +- contracts/depositor/src/msg.rs | 2 +- contracts/ibc-forwarder/Cargo.toml | 3 +- contracts/ibc-forwarder/src/msg.rs | 3 +- contracts/lper/Cargo.toml | 2 +- contracts/lper/src/msg.rs | 2 +- contracts/ls/Cargo.toml | 3 +- contracts/ls/src/msg.rs | 3 +- packages/covenant-depositor-derive/Cargo.toml | 16 ------ packages/covenant-depositor-derive/src/lib.rs | 51 ------------------- .../Cargo.toml | 6 +-- .../src/lib.rs | 16 ++++++ 16 files changed, 44 insertions(+), 111 deletions(-) delete mode 100644 packages/covenant-depositor-derive/Cargo.toml delete mode 100644 packages/covenant-depositor-derive/src/lib.rs rename packages/{clock-derive => covenant-macros}/Cargo.toml (52%) rename packages/{clock-derive => covenant-macros}/src/lib.rs (78%) diff --git a/Cargo.lock b/Cargo.lock index 7d1f7f3e..747648ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -321,8 +321,8 @@ dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", - "covenant-clock-derive", "covenant-clock-tester", + "covenant-macros", "cw-fifo", "cw-multi-test", "cw-storage-plus 1.1.0", @@ -332,15 +332,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "covenant-clock-derive" -version = "0.0.1" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "covenant-clock-tester" version = "1.0.0" @@ -394,8 +385,8 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "covenant-clock", - "covenant-clock-derive", "covenant-ls", + "covenant-macros", "cw-multi-test", "cw-storage-plus 1.1.0", "cw-utils 1.0.1", @@ -411,15 +402,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "covenant-depositor-derive" -version = "0.0.1" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "covenant-holder" version = "1.0.0" @@ -452,9 +434,8 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "covenant-clock", - "covenant-clock-derive", - "covenant-depositor-derive", "covenant-ls", + "covenant-macros", "covenant-utils", "cw-multi-test", "cw-storage-plus 1.1.0", @@ -487,8 +468,8 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "covenant-clock", - "covenant-clock-derive", "covenant-holder", + "covenant-macros", "cw-multi-test", "cw-storage-plus 1.1.0", "cw-utils 1.0.1", @@ -514,8 +495,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "covenant-clock", - "covenant-clock-derive", - "covenant-depositor-derive", + "covenant-macros", "cw-storage-plus 1.1.0", "cw2 1.1.0", "neutron-sdk", @@ -526,6 +506,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "covenant-macros" +version = "0.0.1" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "covenant-utils" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 3a0bc803..5f5ca99f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,8 +34,7 @@ covenant-holder = { path = "contracts/holder" } # packages clock-derive = { path = "packages/clock-derive" } cw-fifo = { path = "packages/cw-fifo" } -covenant-clock-derive = { path = "packages/clock-derive" } -covenant-depositor-derive = { path = "packages/covenant-depositor-derive" } +covenant-macros = { path = "packages/covenant-macros" } covenant-utils = { path = "packages/covenant-utils" } # the sha2 version here is the same as the one used by # cosmwasm-std. when bumping cosmwasm-std, this should also be diff --git a/contracts/clock/Cargo.toml b/contracts/clock/Cargo.toml index 38a69104..30d71fe6 100644 --- a/contracts/clock/Cargo.toml +++ b/contracts/clock/Cargo.toml @@ -19,7 +19,7 @@ library = [] [dependencies] cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } -covenant-clock-derive = { workspace = true } +covenant-macros = { workspace = true } covenant-clock-tester = { workspace = true, features=["library"] } cw-fifo = { workspace = true } cw-storage-plus = { workspace = true } diff --git a/contracts/clock/src/msg.rs b/contracts/clock/src/msg.rs index 3855bd87..e58d2a30 100644 --- a/contracts/clock/src/msg.rs +++ b/contracts/clock/src/msg.rs @@ -3,7 +3,7 @@ use cosmwasm_std::Addr; use cosmwasm_std::Binary; use cosmwasm_std::Uint64; -use covenant_clock_derive::clocked; +use covenant_macros::clocked; #[cw_serde] pub struct InstantiateMsg { diff --git a/contracts/depositor/Cargo.toml b/contracts/depositor/Cargo.toml index c796a0be..02ea2c05 100644 --- a/contracts/depositor/Cargo.toml +++ b/contracts/depositor/Cargo.toml @@ -22,7 +22,7 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -covenant-clock-derive = { workspace = true} +covenant-macros = { workspace = true} covenant-ls = { workspace = true, features=["library"] } covenant-clock = { workspace = true, features=["library"]} cosmwasm-schema = { workspace = true } diff --git a/contracts/depositor/src/msg.rs b/contracts/depositor/src/msg.rs index 74d82883..eab829e9 100644 --- a/contracts/depositor/src/msg.rs +++ b/contracts/depositor/src/msg.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Binary, Uint128, Uint64}; -use covenant_clock_derive::clocked; +use covenant_macros::clocked; use neutron_sdk::bindings::{msg::IbcFee, query::QueryInterchainAccountAddressResponse}; #[cw_serde] diff --git a/contracts/ibc-forwarder/Cargo.toml b/contracts/ibc-forwarder/Cargo.toml index afdefc07..480411cd 100644 --- a/contracts/ibc-forwarder/Cargo.toml +++ b/contracts/ibc-forwarder/Cargo.toml @@ -22,8 +22,7 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -covenant-clock-derive = { workspace = true} -covenant-depositor-derive = { workspace = true} +covenant-macros = { workspace = true} covenant-ls = { workspace = true, features=["library"] } covenant-clock = { workspace = true, features=["library"]} cosmwasm-schema = { workspace = true } diff --git a/contracts/ibc-forwarder/src/msg.rs b/contracts/ibc-forwarder/src/msg.rs index 37bc7248..35b662ce 100644 --- a/contracts/ibc-forwarder/src/msg.rs +++ b/contracts/ibc-forwarder/src/msg.rs @@ -1,8 +1,7 @@ use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Uint64, Attribute}; -use covenant_clock_derive::clocked; -use covenant_depositor_derive::covenant_deposit_address; +use covenant_macros::{clocked, covenant_deposit_address}; use neutron_sdk::bindings::msg::IbcFee; diff --git a/contracts/lper/Cargo.toml b/contracts/lper/Cargo.toml index 6eb1fa28..42dbf1b9 100644 --- a/contracts/lper/Cargo.toml +++ b/contracts/lper/Cargo.toml @@ -23,7 +23,7 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -covenant-clock-derive = { workspace = true } +covenant-macros = { workspace = true } covenant-clock = { workspace = true, features=["library"] } cosmwasm-schema = { workspace = true } diff --git a/contracts/lper/src/msg.rs b/contracts/lper/src/msg.rs index 5535c68d..fe3966ae 100644 --- a/contracts/lper/src/msg.rs +++ b/contracts/lper/src/msg.rs @@ -1,7 +1,7 @@ use astroport::asset::{Asset, AssetInfo}; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Binary, Decimal, Uint128}; -use covenant_clock_derive::clocked; +use covenant_macros::clocked; #[cw_serde] pub struct InstantiateMsg { diff --git a/contracts/ls/Cargo.toml b/contracts/ls/Cargo.toml index 568779ad..68061ba2 100644 --- a/contracts/ls/Cargo.toml +++ b/contracts/ls/Cargo.toml @@ -17,9 +17,8 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -covenant-clock-derive = { workspace = true } +covenant-macros = { workspace = true } covenant-clock = { workspace = true, features=["library"] } -covenant-depositor-derive = { workspace = true} cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } diff --git a/contracts/ls/src/msg.rs b/contracts/ls/src/msg.rs index 5f327fb7..34d7dcff 100644 --- a/contracts/ls/src/msg.rs +++ b/contracts/ls/src/msg.rs @@ -1,7 +1,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Binary, Uint128, Uint64}; -use covenant_clock_derive::clocked; -use covenant_depositor_derive::covenant_deposit_address; +use covenant_macros::{covenant_deposit_address, clocked}; use neutron_sdk::bindings::msg::IbcFee; #[cw_serde] diff --git a/packages/covenant-depositor-derive/Cargo.toml b/packages/covenant-depositor-derive/Cargo.toml deleted file mode 100644 index 2f1c561d..00000000 --- a/packages/covenant-depositor-derive/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "covenant-depositor-derive" -version = "0.0.1" -edition = "2021" -authors = ["benskey bekauz@protonmail.com"] -description = "A package for deriving covenant deposit variants" -license = "BSD-3" - - -[lib] -proc-macro = true - -[dependencies] -proc-macro2 = "1" -quote = "1" -syn = "1" diff --git a/packages/covenant-depositor-derive/src/lib.rs b/packages/covenant-depositor-derive/src/lib.rs deleted file mode 100644 index 40f86a0c..00000000 --- a/packages/covenant-depositor-derive/src/lib.rs +++ /dev/null @@ -1,51 +0,0 @@ -use proc_macro::TokenStream; - -use quote::quote; -use syn::{parse_macro_input, AttributeArgs, DataEnum, DeriveInput}; - -// Merges the variants of two enums. -fn merge_variants(metadata: TokenStream, left: TokenStream, right: TokenStream) -> TokenStream { - use syn::Data::Enum; - - let args = parse_macro_input!(metadata as AttributeArgs); - if let Some(first_arg) = args.first() { - return syn::Error::new_spanned(first_arg, "macro takes no arguments") - .to_compile_error() - .into(); - } - - let mut left: DeriveInput = parse_macro_input!(left); - let right: DeriveInput = parse_macro_input!(right); - - if let ( - Enum(DataEnum { variants, .. }), - Enum(DataEnum { - variants: to_add, .. - }), - ) = (&mut left.data, right.data) - { - variants.extend(to_add.into_iter()); - - quote! { #left }.into() - } else { - syn::Error::new(left.ident.span(), "variants may only be added for enums") - .to_compile_error() - .into() - } -} - -#[proc_macro_attribute] -pub fn covenant_deposit_address(metadata: TokenStream, input: TokenStream) -> TokenStream { - merge_variants( - metadata, - input, - quote!( - enum Deposit { - /// Returns the address a contract expects to receive funds to - #[returns(Option)] - DepositAddress {}, - } - ) - .into(), - ) -} diff --git a/packages/clock-derive/Cargo.toml b/packages/covenant-macros/Cargo.toml similarity index 52% rename from packages/clock-derive/Cargo.toml rename to packages/covenant-macros/Cargo.toml index 89d51404..97ae5615 100644 --- a/packages/clock-derive/Cargo.toml +++ b/packages/covenant-macros/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "covenant-clock-derive" +name = "covenant-macros" version = "0.0.1" edition = "2021" -authors = ["ekez "] -description = "A package for deriving the covenant-clock interface." +authors = ["ekez , benskey bekauz@protonmail.com"] +description = "A package for deriving the covenant interfaces." license = "BSD-3" diff --git a/packages/clock-derive/src/lib.rs b/packages/covenant-macros/src/lib.rs similarity index 78% rename from packages/clock-derive/src/lib.rs rename to packages/covenant-macros/src/lib.rs index c5b55507..e6345d88 100644 --- a/packages/clock-derive/src/lib.rs +++ b/packages/covenant-macros/src/lib.rs @@ -50,3 +50,19 @@ pub fn clocked(metadata: TokenStream, input: TokenStream) -> TokenStream { .into(), ) } + +#[proc_macro_attribute] +pub fn covenant_deposit_address(metadata: TokenStream, input: TokenStream) -> TokenStream { + merge_variants( + metadata, + input, + quote!( + enum Deposit { + /// Returns the address a contract expects to receive funds to + #[returns(Option)] + DepositAddress {}, + } + ) + .into(), + ) +} From e33921652781376fdce9bb79f79563addffc1096 Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 10 Aug 2023 15:23:13 +0200 Subject: [PATCH 024/586] general covenant query proc macros; cleanup ls --- Cargo.lock | 5 + contracts/depositor/Cargo.toml | 1 + contracts/depositor/src/contract.rs | 2 +- .../depositor/src/suite_test/unit_helpers.rs | 2 +- contracts/ibc-forwarder/src/contract.rs | 37 ++-- contracts/ibc-forwarder/src/msg.rs | 6 +- contracts/ls/Cargo.toml | 2 +- contracts/ls/src/contract.rs | 167 +++++++----------- contracts/ls/src/msg.rs | 61 +------ contracts/ls/src/state.rs | 22 +-- packages/covenant-macros/Cargo.toml | 1 + packages/covenant-macros/src/lib.rs | 54 +++++- packages/covenant-utils/Cargo.toml | 2 + packages/covenant-utils/src/lib.rs | 24 ++- 14 files changed, 181 insertions(+), 205 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 747648ab..115a08b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -387,6 +387,7 @@ dependencies = [ "covenant-clock", "covenant-ls", "covenant-macros", + "covenant-utils", "cw-multi-test", "cw-storage-plus 1.1.0", "cw-utils 1.0.1", @@ -496,6 +497,7 @@ dependencies = [ "cosmwasm-std", "covenant-clock", "covenant-macros", + "covenant-utils", "cw-storage-plus 1.1.0", "cw2 1.1.0", "neutron-sdk", @@ -510,6 +512,7 @@ dependencies = [ name = "covenant-macros" version = "0.0.1" dependencies = [ + "covenant-utils", "proc-macro2", "quote", "syn 1.0.109", @@ -520,6 +523,8 @@ name = "covenant-utils" version = "0.0.1" dependencies = [ "cosmwasm-schema", + "cosmwasm-std", + "neutron-sdk", ] [[package]] diff --git a/contracts/depositor/Cargo.toml b/contracts/depositor/Cargo.toml index 02ea2c05..629b94aa 100644 --- a/contracts/depositor/Cargo.toml +++ b/contracts/depositor/Cargo.toml @@ -25,6 +25,7 @@ library = [] covenant-macros = { workspace = true} covenant-ls = { workspace = true, features=["library"] } covenant-clock = { workspace = true, features=["library"]} +covenant-utils = { workspace = true } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw-storage-plus = { workspace = true } diff --git a/contracts/depositor/src/contract.rs b/contracts/depositor/src/contract.rs index bc62d2a0..145b4919 100644 --- a/contracts/depositor/src/contract.rs +++ b/contracts/depositor/src/contract.rs @@ -250,7 +250,7 @@ fn try_send_ls_token(env: Env, mut deps: ExecuteDeps) -> NeutronResult = deps .querier - .query_wasm_smart(ls_address, &covenant_ls::msg::QueryMsg::StrideICA {})?; + .query_wasm_smart(ls_address, &covenant_utils::neutron_ica::QueryMsg::DepositAddress {})?; let stride_ica_addr = match stride_ica_query { Some(addr) => addr, None => return Err(NeutronError::Std(StdError::not_found("no LS ica found"))), diff --git a/contracts/depositor/src/suite_test/unit_helpers.rs b/contracts/depositor/src/suite_test/unit_helpers.rs index e85bdd18..f9e8ad80 100644 --- a/contracts/depositor/src/suite_test/unit_helpers.rs +++ b/contracts/depositor/src/suite_test/unit_helpers.rs @@ -55,7 +55,7 @@ pub fn wasm_handler(wasm_query: &WasmQuery) -> SystemResult match contract_addr.as_ref() { LS_ADDR => match from_binary::(msg).unwrap() { - covenant_ls::msg::QueryMsg::StrideICA {} => SystemResult::Ok(ContractResult::Ok( + covenant_ls::msg::QueryMsg::ICAAddress {} => SystemResult::Ok(ContractResult::Ok( to_binary(&Addr::unchecked("some_ica_addr")).unwrap(), )), _ => unimplemented!(), diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index 430e39d0..07d64005 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -28,8 +28,6 @@ pub fn instantiate( ) -> NeutronResult> { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; - let next_contract = deps.api.addr_validate(&msg.next_contract)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; @@ -42,6 +40,7 @@ pub fn instantiate( ica_timeout: msg.ica_timeout, ibc_transfer_timeout: msg.ibc_transfer_timeout, })?; + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; Ok(Response::default() .add_attribute("method", "ibc_forwarder_instantiate") @@ -104,7 +103,7 @@ fn try_forward_funds(env: Env, mut deps: ExecuteDeps) -> NeutronResult = deps.querier.query_wasm_smart( next_contract, - &crate::msg::QueryMsg::DepositAddress {}, + &covenant_utils::neutron_ica::QueryMsg::DepositAddress {}, )?; // if query returns None, then we error and wait @@ -205,6 +204,7 @@ fn to_proto_msg_transfer(msg: impl Message) -> NeutronResult { #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: QueryDeps, env: Env, msg: QueryMsg) -> NeutronResult { match msg { + QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), // we expect to receive funds into our ICA account on the remote chain. // if the ICA had not been opened yet, we return `None` so that the // contract querying this will be instructed to wait and retry. @@ -276,20 +276,25 @@ fn sudo_open_ack( let parsed_version: Result = serde_json_wasm::from_str(counterparty_version.as_str()); + // get the parsed OpenAckVersion or return an error if we fail + let Ok(parsed_version) = parsed_version else { + return Err(StdError::generic_err("Can't parse counterparty_version")) + }; + // Update the storage record associated with the interchain account. - if let Ok(parsed_version) = parsed_version { - INTERCHAIN_ACCOUNTS.save( - deps.storage, - port_id, - &Some(( - parsed_version.clone().address, - parsed_version.clone().controller_connection_id, - )), - )?; - CONTRACT_STATE.save(deps.storage, &ContractState::ICACreated)?; - return Ok(Response::default().add_attribute("method", "sudo_open_ack")); - } - Err(StdError::generic_err("Can't parse counterparty_version")) + INTERCHAIN_ACCOUNTS.save( + deps.storage, + port_id, + &Some(( + parsed_version.clone().address, + parsed_version.clone().controller_connection_id, + )), + )?; + CONTRACT_STATE.save(deps.storage, &ContractState::ICACreated)?; + + return Ok(Response::default() + .add_attribute("method", "sudo_open_ack") + ) } fn sudo_response(deps: ExecuteDeps, request: RequestPacket, data: Binary) -> StdResult { diff --git a/contracts/ibc-forwarder/src/msg.rs b/contracts/ibc-forwarder/src/msg.rs index 35b662ce..5f60e949 100644 --- a/contracts/ibc-forwarder/src/msg.rs +++ b/contracts/ibc-forwarder/src/msg.rs @@ -1,10 +1,9 @@ use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Uint64, Attribute}; -use covenant_macros::{clocked, covenant_deposit_address}; +use cosmwasm_std::{Uint64, Attribute, Addr}; +use covenant_macros::{clocked, covenant_deposit_address, covenant_clock_address}; use neutron_sdk::bindings::msg::IbcFee; - #[cw_serde] pub struct InstantiateMsg { /// address for the clock. this contract verifies @@ -80,6 +79,7 @@ impl RemoteChainInfo { pub enum ExecuteMsg {} #[covenant_deposit_address] +#[covenant_clock_address] #[derive(QueryResponses)] #[cw_serde] pub enum QueryMsg {} diff --git a/contracts/ls/Cargo.toml b/contracts/ls/Cargo.toml index 68061ba2..7a135f36 100644 --- a/contracts/ls/Cargo.toml +++ b/contracts/ls/Cargo.toml @@ -19,7 +19,7 @@ library = [] [dependencies] covenant-macros = { workspace = true } covenant-clock = { workspace = true, features=["library"] } - +covenant-utils = { workspace = true } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw-storage-plus = { workspace = true } diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs index c0d75ca8..f7fecdb7 100644 --- a/contracts/ls/src/contract.rs +++ b/contracts/ls/src/contract.rs @@ -8,23 +8,22 @@ use cosmwasm_std::{ Response, StdError, StdResult, SubMsg, Uint128, }; use covenant_clock::helpers::verify_clock; +use covenant_utils::neutron_ica::{SudoPayload, OpenAckVersion, RemoteChainInfo}; use cw2::set_contract_version; use neutron_sdk::bindings::types::ProtobufAny; use crate::msg::{ - ContractState, ExecuteMsg, InstantiateMsg, MigrateMsg, OpenAckVersion, - QueryMsg, SudoPayload, + ContractState, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, }; use crate::state::{ read_errors_from_queue, read_reply_payload, save_reply_payload, save_sudo_payload, ACKNOWLEDGEMENT_RESULTS, CLOCK_ADDRESS, CONTRACT_STATE, - IBC_FEE, IBC_TRANSFER_TIMEOUT, ICA_TIMEOUT, INTERCHAIN_ACCOUNTS, LS_DENOM, - NEUTRON_STRIDE_IBC_CONNECTION_ID, STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID, AUTOPILOT_FORMAT, NEXT_CONTRACT, + INTERCHAIN_ACCOUNTS, AUTOPILOT_FORMAT, NEXT_CONTRACT, REMOTE_CHAIN_INFO, }; use neutron_sdk::{ bindings::{ msg::{MsgSubmitTxResponse, NeutronMsg}, - query::{NeutronQuery, QueryInterchainAccountAddressResponse}, + query::NeutronQuery, }, interchain_txs::helpers::get_port_id, sudo::msg::{RequestPacket, SudoMsg}, @@ -56,29 +55,19 @@ pub fn instantiate( let next_contract = deps.api.addr_validate(&msg.next_contract)?; CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; - - // store all fields relevant to ICA operations - STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID - .save(deps.storage, &msg.stride_neutron_ibc_transfer_channel_id)?; - NEUTRON_STRIDE_IBC_CONNECTION_ID.save(deps.storage, &msg.neutron_stride_ibc_connection_id)?; - LS_DENOM.save(deps.storage, &msg.ls_denom)?; - IBC_TRANSFER_TIMEOUT.save(deps.storage, &msg.ibc_transfer_timeout)?; - ICA_TIMEOUT.save(deps.storage, &msg.ica_timeout)?; - IBC_FEE.save(deps.storage, &msg.ibc_fee)?; + REMOTE_CHAIN_INFO.save(deps.storage, &RemoteChainInfo { + connection_id: msg.neutron_stride_ibc_connection_id, + channel_id: msg.stride_neutron_ibc_transfer_channel_id, + denom: msg.ls_denom, + ibc_transfer_timeout: msg.ibc_transfer_timeout, + ica_timeout: msg.ica_timeout, + ibc_fee: msg.ibc_fee, + })?; Ok(Response::default() .add_attribute("method", "ls_instantiate") .add_attribute("clock_address", clock_addr) .add_attribute("next_contract", next_contract) - .add_attribute( - "stride_neutron_ibc_transfer_channel_id", - msg.stride_neutron_ibc_transfer_channel_id, - ) - .add_attribute( - "neutron_stride_ibc_connection_id", - msg.neutron_stride_ibc_connection_id, - ) - .add_attribute("ls_denom", msg.ls_denom) .add_attribute("ibc_transfer_timeout", msg.ibc_transfer_timeout) .add_attribute("ica_timeout", msg.ica_timeout)) } @@ -125,9 +114,9 @@ fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> NeutronResult NeutronResult> { - let connection_id = NEUTRON_STRIDE_IBC_CONNECTION_ID.load(deps.storage)?; - let register = - NeutronMsg::register_interchain_account(connection_id, INTERCHAIN_ACCOUNT_ID.to_string()); + let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; + let register: NeutronMsg = + NeutronMsg::register_interchain_account(remote_chain_info.connection_id, INTERCHAIN_ACCOUNT_ID.to_string()); let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); // we are saving empty data here because we handle response of registering ICA in sudo_open_ack method @@ -152,7 +141,7 @@ fn try_execute_transfer( let next_contract = NEXT_CONTRACT.load(deps.storage)?; let deposit_address_query: Option = deps.querier.query_wasm_smart( next_contract, - &crate::msg::QueryMsg::DepositAddress {}, + &covenant_utils::neutron_ica::QueryMsg::DepositAddress {}, )?; // if query returns None, then we error and wait @@ -169,14 +158,10 @@ fn try_execute_transfer( match interchain_account { Some((address, controller_conn_id)) => { - let fee = IBC_FEE.load(deps.storage)?; - let source_channel = STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID.load(deps.storage)?; - let denom = LS_DENOM.load(deps.storage)?; - let ibc_transfer_timeout = IBC_TRANSFER_TIMEOUT.load(deps.storage)?; - let ica_timeout = ICA_TIMEOUT.load(deps.storage)?; + let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; let coin = Coin { - denom, + denom: remote_chain_info.denom, amount: amount.to_string(), }; @@ -185,7 +170,7 @@ fn try_execute_transfer( // timeout_timestamp = current block + ica timeout + ibc_transfer_timeout let msg = MsgTransfer { source_port: "transfer".to_string(), - source_channel, + source_channel: remote_chain_info.channel_id, token: Some(coin), sender: address, receiver: deposit_address.to_string(), @@ -193,8 +178,8 @@ fn try_execute_transfer( timeout_timestamp: env .block .time - .plus_seconds(ica_timeout.u64()) - .plus_seconds(ibc_transfer_timeout.u64()) + .plus_seconds(remote_chain_info.ica_timeout.u64()) + .plus_seconds(remote_chain_info.ibc_transfer_timeout.u64()) .nanos(), }; @@ -217,8 +202,8 @@ fn try_execute_transfer( INTERCHAIN_ACCOUNT_ID.to_string(), vec![protobuf], "".to_string(), - ica_timeout.u64(), - fee, + remote_chain_info.ica_timeout.u64(), + remote_chain_info.ibc_fee, ); let sudo_msg = msg_with_sudo_callback( @@ -234,7 +219,16 @@ fn try_execute_transfer( .add_attribute("method", "try_execute_transfer") ) } - None => Err(NeutronError::Std(StdError::not_found("no ica found"))), + None => { + // I can't think of a case of how we could end up here as `sudo_open_ack` + // callback advances the state to `ICACreated` and stores the ICA. + // just in case, we revert the state to `Instantiated` to restart the flow. + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + Ok(Response::default() + .add_attribute("method", "try_execute_transfer") + .add_attribute("error", "no_ica_found") + ) + }, } } @@ -252,27 +246,10 @@ fn msg_with_sudo_callback>, T>( pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult { match msg { QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), - QueryMsg::StrideICA {} => Ok(to_binary(&Addr::unchecked( + QueryMsg::ICAAddress {} => Ok(to_binary(&Addr::unchecked( get_ica(deps, &env, INTERCHAIN_ACCOUNT_ID)?.0, ))?), QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.may_load(deps.storage)?)?), - QueryMsg::StrideNeutronIbcTransferChannelId {} => Ok(to_binary( - &STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID.may_load(deps.storage)?, - )?), - QueryMsg::NeutronStrideIbcConnectionId {} => Ok(to_binary( - &NEUTRON_STRIDE_IBC_CONNECTION_ID.may_load(deps.storage)?, - )?), - QueryMsg::IbcFee {} => Ok(to_binary(&IBC_FEE.may_load(deps.storage)?)?), - QueryMsg::IcaTimeout {} => Ok(to_binary(&ICA_TIMEOUT.may_load(deps.storage)?)?), - QueryMsg::IbcTransferTimeout {} => { - Ok(to_binary(&IBC_TRANSFER_TIMEOUT.may_load(deps.storage)?)?) - } - QueryMsg::LsDenom {} => Ok(to_binary(&LS_DENOM.may_load(deps.storage)?)?), - QueryMsg::AcknowledgementResult { - interchain_account_id, - sequence_id, - } => query_acknowledgement_result(deps, env, interchain_account_id, sequence_id), - QueryMsg::ErrorsQueue {} => query_errors_queue(deps), QueryMsg::DepositAddress {} => { let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); @@ -299,52 +276,10 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult // up to the querying module to make sense of the response Ok(to_binary(&ica)?) }, + QueryMsg::RemoteChainInfo {} => Ok(to_binary(&REMOTE_CHAIN_INFO.may_load(deps.storage)?)?), } } -// returns ICA address from Neutron ICA SDK module -pub fn query_interchain_address( - deps: Deps, - env: Env, - interchain_account_id: String, - connection_id: String, -) -> NeutronResult { - let query = NeutronQuery::InterchainAccountAddress { - owner_address: env.contract.address.to_string(), - interchain_account_id, - connection_id, - }; - - let res: QueryInterchainAccountAddressResponse = deps.querier.query(&query.into())?; - Ok(to_binary(&res)?) -} - -// returns ICA address from the contract storage. The address was saved in sudo_open_ack method -pub fn query_interchain_address_contract( - deps: Deps, - env: Env, - interchain_account_id: String, -) -> NeutronResult { - Ok(to_binary(&get_ica(deps, &env, &interchain_account_id)?)?) -} - -// returns the result -pub fn query_acknowledgement_result( - deps: Deps, - env: Env, - interchain_account_id: String, - sequence_id: u64, -) -> NeutronResult { - let port_id = get_port_id(env.contract.address.as_str(), &interchain_account_id); - let res = ACKNOWLEDGEMENT_RESULTS.may_load(deps.storage, (port_id, sequence_id))?; - Ok(to_binary(&res)?) -} - -pub fn query_errors_queue(deps: Deps) -> NeutronResult { - let res = read_errors_from_queue(deps.storage)?; - Ok(to_binary(&res)?) -} - #[cfg_attr(not(feature = "library"), entry_point)] pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> StdResult { deps.api @@ -534,7 +469,10 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult } if let Some(channel_id) = stride_neutron_ibc_transfer_channel_id { - STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID.save(deps.storage, &channel_id)?; + REMOTE_CHAIN_INFO.update(deps.storage, |mut info| -> StdResult<_>{ + info.channel_id = channel_id.to_string(); + Ok(info) + })?; resp = resp.add_attribute("stride_neutron_ibc_transfer_channel_id", channel_id); } @@ -545,24 +483,34 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult } if let Some(connection_id) = neutron_stride_ibc_connection_id { - NEUTRON_STRIDE_IBC_CONNECTION_ID.save(deps.storage, &connection_id)?; + REMOTE_CHAIN_INFO.update(deps.storage, |mut info| -> StdResult<_>{ + info.connection_id = connection_id.to_string(); + Ok(info) + })?; resp = resp.add_attribute("neutron_stride_ibc_connection_id", connection_id); } if let Some(denom) = ls_denom { - LS_DENOM.save(deps.storage, &denom)?; + REMOTE_CHAIN_INFO.update(deps.storage, |mut info| -> StdResult<_>{ + info.denom = denom.to_string(); + Ok(info) + })?; resp = resp.add_attribute("ls_denom", denom); } if let Some(timeout) = ibc_transfer_timeout { - resp = resp.add_attribute("ibc_transfer_timeout", timeout); - IBC_TRANSFER_TIMEOUT.save(deps.storage, &timeout)?; + REMOTE_CHAIN_INFO.update(deps.storage, |mut info| -> StdResult<_>{ + info.ibc_transfer_timeout = timeout; + Ok(info) + })?; resp = resp.add_attribute("ibc_transfer_timeout", timeout); } if let Some(timeout) = ica_timeout { - resp = resp.add_attribute("ica_timeout", timeout); - ICA_TIMEOUT.save(deps.storage, &timeout)?; + REMOTE_CHAIN_INFO.update(deps.storage, |mut info| -> StdResult<_>{ + info.ica_timeout = timeout; + Ok(info) + })?; resp = resp.add_attribute("ica_timeout", timeout); } @@ -573,7 +521,10 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult msg: "invalid IbcFee".to_string(), }); } - IBC_FEE.save(deps.storage, &fee)?; + REMOTE_CHAIN_INFO.update(deps.storage, |mut info| -> StdResult<_>{ + info.ibc_fee = fee.clone(); + Ok(info) + })?; resp = resp.add_attribute("ibc_fee_ack", fee.ack_fee[0].to_string()); resp = resp.add_attribute("ibc_fee_timeout", fee.timeout_fee[0].to_string()); } diff --git a/contracts/ls/src/msg.rs b/contracts/ls/src/msg.rs index 34d7dcff..4c8cdb96 100644 --- a/contracts/ls/src/msg.rs +++ b/contracts/ls/src/msg.rs @@ -1,7 +1,9 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Binary, Uint128, Uint64}; -use covenant_macros::{covenant_deposit_address, clocked}; +use covenant_macros::{covenant_deposit_address, clocked, covenant_clock_address, covenant_remote_chain, covenant_ica_address}; +use covenant_utils::neutron_ica::AcknowledgementResult; use neutron_sdk::bindings::msg::IbcFee; +use covenant_utils::neutron_ica::RemoteChainInfo; #[cw_serde] pub struct InstantiateMsg { @@ -84,37 +86,15 @@ pub enum ExecuteMsg { Transfer { amount: Uint128 }, } +#[covenant_clock_address] +#[covenant_remote_chain] #[covenant_deposit_address] +#[covenant_ica_address] #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { - #[returns(Addr)] - ClockAddress {}, - #[returns(Addr)] - StrideICA {}, #[returns(ContractState)] ContractState {}, - #[returns(String)] - StrideNeutronIbcTransferChannelId {}, - #[returns(String)] - NeutronStrideIbcConnectionId {}, - #[returns(IbcFee)] - IbcFee {}, - #[returns(Uint64)] - IcaTimeout {}, - #[returns(Uint64)] - IbcTransferTimeout {}, - #[returns(String)] - LsDenom {}, - // this query returns acknowledgement result after interchain transaction - #[returns(Option)] - AcknowledgementResult { - interchain_account_id: String, - sequence_id: u64, - }, - // this query returns non-critical errors list - #[returns(Vec<(Vec, String)>)] - ErrorsQueue {}, } #[cw_serde] @@ -134,37 +114,8 @@ pub enum MigrateMsg { }, } -#[cw_serde] -pub struct OpenAckVersion { - pub version: String, - pub controller_connection_id: String, - pub host_connection_id: String, - pub address: String, - pub encoding: String, - pub tx_type: String, -} - #[cw_serde] pub enum ContractState { Instantiated, ICACreated, } - -/// SudoPayload is a type that stores information about a transaction that we try to execute -/// on the host chain. This is a type introduced for our convenience. -#[cw_serde] -pub struct SudoPayload { - pub message: String, - pub port_id: String, -} - -/// Serves for storing acknowledgement calls for interchain transactions -#[cw_serde] -pub enum AcknowledgementResult { - /// Success - Got success acknowledgement in sudo with array of message item types in it - Success(Vec), - /// Error - Got error acknowledgement in sudo with payload message in it and error details - Error((String, String)), - /// Timeout - Got timeout acknowledgement in sudo with payload message in it - Timeout(String), -} diff --git a/contracts/ls/src/state.rs b/contracts/ls/src/state.rs index a65460a6..96cca34b 100644 --- a/contracts/ls/src/state.rs +++ b/contracts/ls/src/state.rs @@ -1,8 +1,8 @@ -use cosmwasm_std::{from_binary, to_vec, Addr, Binary, Order, StdResult, Storage, Uint64}; +use cosmwasm_std::{from_binary, to_vec, Addr, Binary, Order, StdResult, Storage, Uint128}; +use covenant_utils::neutron_ica::{AcknowledgementResult, SudoPayload, RemoteChainInfo}; use cw_storage_plus::{Item, Map}; -use neutron_sdk::bindings::msg::IbcFee; -use crate::msg::{AcknowledgementResult, ContractState, SudoPayload}; +use crate::msg::ContractState; /// tracks the current state of state machine pub const CONTRACT_STATE: Item = Item::new("contract_state"); @@ -12,25 +12,15 @@ pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); /// next contract address to forward the liquid staked funds to pub const NEXT_CONTRACT: Item = Item::new("next_contract"); -/// IBC transfer channel on stride for neutron -pub const STRIDE_NEUTRON_IBC_TRANSFER_CHANNEL_ID: Item = Item::new("sn_ibc_chann_id"); -/// IBC connection ID on neutron for stride -pub const NEUTRON_STRIDE_IBC_CONNECTION_ID: Item = Item::new("ns_ibc_conn_id"); +pub const TRANSFER_AMOUNT: Item = Item::new("transfer_amount"); -/// the denom that we will permit transfers of to the liquid pooler -pub const LS_DENOM: Item = Item::new("ls_denom"); +/// information needed for an ibc transfer to the remote chain +pub const REMOTE_CHAIN_INFO: Item = Item::new("r_c_info"); /// interchain accounts storage in form of (port_id) -> (address, controller_connection_id) pub const INTERCHAIN_ACCOUNTS: Map> = Map::new("interchain_accounts"); -/// timeout in seconds for inner ibc MsgTransfer -pub const IBC_TRANSFER_TIMEOUT: Item = Item::new("ibc_transfer_timeout"); -/// time in seconds for ICA SubmitTX messages from neutron -pub const ICA_TIMEOUT: Item = Item::new("ica_timeout"); -/// neutron IbcFee for relayers -pub const IBC_FEE: Item = Item::new("ibc_fee"); - /// formatting of stride autopilot message. /// we use string match & replace with relevant fields to obtain the valid message. pub const AUTOPILOT_FORMAT: Item = Item::new("autopilot_format"); diff --git a/packages/covenant-macros/Cargo.toml b/packages/covenant-macros/Cargo.toml index 97ae5615..8dd081b2 100644 --- a/packages/covenant-macros/Cargo.toml +++ b/packages/covenant-macros/Cargo.toml @@ -14,3 +14,4 @@ proc-macro = true proc-macro2 = "1" quote = "1" syn = "1" +covenant-utils = { workspace = true } \ No newline at end of file diff --git a/packages/covenant-macros/src/lib.rs b/packages/covenant-macros/src/lib.rs index e6345d88..78b807be 100644 --- a/packages/covenant-macros/src/lib.rs +++ b/packages/covenant-macros/src/lib.rs @@ -22,7 +22,7 @@ fn merge_variants(metadata: TokenStream, left: TokenStream, right: TokenStream) Enum(DataEnum { variants: to_add, .. }), - ) = (&mut left.data, right.data) + ) = (&mut left.data, right.data,) { variants.extend(to_add.into_iter()); @@ -56,13 +56,61 @@ pub fn covenant_deposit_address(metadata: TokenStream, input: TokenStream) -> To merge_variants( metadata, input, - quote!( + quote!( enum Deposit { /// Returns the address a contract expects to receive funds to - #[returns(Option)] + #[returns(Option)] DepositAddress {}, } ) .into(), ) } + +#[proc_macro_attribute] +pub fn covenant_clock_address(metadata: TokenStream, input: TokenStream) -> TokenStream { + merge_variants( + metadata, + input, + quote!( + enum Clock { + /// Returns the associated clock address authorized to submit ticks + #[returns(Addr)] + ClockAddress {}, + } + ) + .into(), + ) +} + +#[proc_macro_attribute] +pub fn covenant_remote_chain(metadata: TokenStream, input: TokenStream) -> TokenStream { + merge_variants( + metadata, + input, + quote!( + enum RemoteChain { + /// Returns the associated remote chain information + #[returns(RemoteChainInfo)] + RemoteChainInfo {}, + } + ) + .into(), + ) +} + +#[proc_macro_attribute] +pub fn covenant_ica_address(metadata: TokenStream, input: TokenStream) -> TokenStream { + merge_variants( + metadata, + input, + quote!( + enum ICA { + /// Returns the associated remote chain information + #[returns(Option)] + ICAAddress {}, + } + ) + .into(), + ) +} \ No newline at end of file diff --git a/packages/covenant-utils/Cargo.toml b/packages/covenant-utils/Cargo.toml index d3062292..4ab87114 100644 --- a/packages/covenant-utils/Cargo.toml +++ b/packages/covenant-utils/Cargo.toml @@ -11,3 +11,5 @@ license = "BSD-3" [dependencies] cosmwasm-schema = { workspace = true } +neutron-sdk = { workspace = true } +cosmwasm-std = { workspace = true } diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index 2e31d140..d58d07c5 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -1,6 +1,8 @@ pub mod neutron_ica { - use cosmwasm_schema::cw_serde; + use cosmwasm_schema::{cw_serde, QueryResponses}; + use cosmwasm_std::{Uint64, Addr}; + use neutron_sdk::bindings::msg::IbcFee; #[cw_serde] pub struct OpenAckVersion { @@ -30,4 +32,24 @@ pub mod neutron_ica { /// Timeout - Got timeout acknowledgement in sudo with payload message in it Timeout(String), } + + #[cw_serde] + pub struct RemoteChainInfo { + /// connection id from neutron to the remote chain on which + /// we wish to open an ICA + pub connection_id: String, + pub channel_id: String, + pub denom: String, + pub ibc_transfer_timeout: Uint64, + pub ica_timeout: Uint64, + pub ibc_fee: IbcFee, + } + + #[cw_serde] + #[derive(QueryResponses)] + pub enum QueryMsg { + /// Returns the associated remote chain information + #[returns(Option)] + DepositAddress {}, + } } From 4cc3b130363ce113064f845585d0d7eb7f088895 Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 10 Aug 2023 23:36:34 +0200 Subject: [PATCH 025/586] cleanup ls --- contracts/ls/src/contract.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs index f7fecdb7..f56ceabd 100644 --- a/contracts/ls/src/contract.rs +++ b/contracts/ls/src/contract.rs @@ -16,8 +16,8 @@ use crate::msg::{ ContractState, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, }; use crate::state::{ - read_errors_from_queue, read_reply_payload, - save_reply_payload, save_sudo_payload, ACKNOWLEDGEMENT_RESULTS, CLOCK_ADDRESS, CONTRACT_STATE, + read_reply_payload, + save_reply_payload, save_sudo_payload, CLOCK_ADDRESS, CONTRACT_STATE, INTERCHAIN_ACCOUNTS, AUTOPILOT_FORMAT, NEXT_CONTRACT, REMOTE_CHAIN_INFO, }; use neutron_sdk::{ @@ -47,12 +47,10 @@ pub fn instantiate( deps.api.debug("WASMDEBUG: instantiate"); set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - // contract begins at Instantiated state - CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; - - // validate and store other module addresses + // validate the addresses let clock_addr = deps.api.addr_validate(&msg.clock_address)?; let next_contract = deps.api.addr_validate(&msg.next_contract)?; + CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; REMOTE_CHAIN_INFO.save(deps.storage, &RemoteChainInfo { @@ -63,13 +61,15 @@ pub fn instantiate( ica_timeout: msg.ica_timeout, ibc_fee: msg.ibc_fee, })?; + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; Ok(Response::default() .add_attribute("method", "ls_instantiate") .add_attribute("clock_address", clock_addr) .add_attribute("next_contract", next_contract) .add_attribute("ibc_transfer_timeout", msg.ibc_transfer_timeout) - .add_attribute("ica_timeout", msg.ica_timeout)) + .add_attribute("ica_timeout", msg.ica_timeout) + ) } #[cfg_attr(not(feature = "library"), entry_point)] From 8468c2ed7ee59ec9a01cf69b15b2dd1f421057ac Mon Sep 17 00:00:00 2001 From: bekauz Date: Sat, 12 Aug 2023 23:45:39 +0200 Subject: [PATCH 026/586] moving MsgTransfer to protobuf conversion to utils --- Cargo.lock | 1 + contracts/ibc-forwarder/src/contract.rs | 18 +----------------- contracts/ls/src/contract.rs | 18 +++--------------- packages/covenant-utils/Cargo.toml | 1 + packages/covenant-utils/src/lib.rs | 20 ++++++++++++++++++-- 5 files changed, 24 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 115a08b7..ff7c45ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -525,6 +525,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "neutron-sdk", + "prost 0.11.9", ] [[package]] diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index 07d64005..11c12982 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -6,7 +6,6 @@ use cosmwasm_std::{Env, MessageInfo, Response, Deps, DepsMut, StdError, Binary, use covenant_clock::helpers::verify_clock; use cw2::set_contract_version; use neutron_sdk::{NeutronResult, bindings::{msg::{NeutronMsg, MsgSubmitTxResponse}, query::NeutronQuery, types::ProtobufAny}, interchain_txs::helpers::get_port_id, NeutronError, sudo::msg::{SudoMsg, RequestPacket},}; -use prost::Message; use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState, RemoteChainInfo, QueryMsg}, state::{CONTRACT_STATE, CLOCK_ADDRESS, INTERCHAIN_ACCOUNTS, REMOTE_CHAIN_INFO, NEXT_CONTRACT, REPLY_ID_STORAGE, SUDO_PAYLOAD}}; @@ -138,7 +137,7 @@ fn try_forward_funds(env: Env, mut deps: ExecuteDeps) -> NeutronResult>, T>( Ok(SubMsg::reply_on_success(msg, SUDO_PAYLOAD_REPLY_ID)) } -/// helper that serializes a MsgTransfer to protobuf -fn to_proto_msg_transfer(msg: impl Message) -> NeutronResult { - // Serialize the Transfer message - let mut buf = Vec::new(); - buf.reserve(msg.encoded_len()); - if let Err(e) = msg.encode(&mut buf) { - return Err(StdError::generic_err(format!("Encode error: {e}")).into()); - } - - Ok(ProtobufAny { - type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), - value: Binary::from(buf), - }) -} - #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: QueryDeps, env: Env, msg: QueryMsg) -> NeutronResult { match msg { diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs index f56ceabd..0beadc31 100644 --- a/contracts/ls/src/contract.rs +++ b/contracts/ls/src/contract.rs @@ -8,7 +8,7 @@ use cosmwasm_std::{ Response, StdError, StdResult, SubMsg, Uint128, }; use covenant_clock::helpers::verify_clock; -use covenant_utils::neutron_ica::{SudoPayload, OpenAckVersion, RemoteChainInfo}; +use covenant_utils::neutron_ica::{SudoPayload, OpenAckVersion, RemoteChainInfo, self}; use cw2::set_contract_version; use neutron_sdk::bindings::types::ProtobufAny; @@ -145,9 +145,7 @@ fn try_execute_transfer( )?; // if query returns None, then we error and wait - let deposit_address = if let Some(addr) = deposit_address_query { - addr - } else { + let Some(deposit_address) = deposit_address_query else { return Err(NeutronError::Std( StdError::not_found("Next contract is not ready for receiving the funds yet") )) @@ -183,17 +181,7 @@ fn try_execute_transfer( .nanos(), }; - // Serialize the Transfer message - let mut buf = Vec::new(); - buf.reserve(msg.encoded_len()); - if let Err(e) = msg.encode(&mut buf) { - return Err(StdError::generic_err(format!("Encode error: {e}",)).into()); - } - - let protobuf = ProtobufAny { - type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), - value: Binary::from(buf), - }; + let protobuf = neutron_ica::to_proto_msg_transfer(msg)?; // wrap the protobuf of MsgTransfer into a message to be executed // by our interchain account diff --git a/packages/covenant-utils/Cargo.toml b/packages/covenant-utils/Cargo.toml index 4ab87114..4b4fdf1a 100644 --- a/packages/covenant-utils/Cargo.toml +++ b/packages/covenant-utils/Cargo.toml @@ -13,3 +13,4 @@ license = "BSD-3" cosmwasm-schema = { workspace = true } neutron-sdk = { workspace = true } cosmwasm-std = { workspace = true } +prost = { workspace = true } diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index d58d07c5..d5f86b3e 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -1,8 +1,9 @@ pub mod neutron_ica { use cosmwasm_schema::{cw_serde, QueryResponses}; - use cosmwasm_std::{Uint64, Addr}; - use neutron_sdk::bindings::msg::IbcFee; + use cosmwasm_std::{Uint64, Addr, Binary, StdError}; + use neutron_sdk::{bindings::{msg::IbcFee, types::ProtobufAny}, NeutronResult}; + use prost::Message; #[cw_serde] pub struct OpenAckVersion { @@ -52,4 +53,19 @@ pub mod neutron_ica { #[returns(Option)] DepositAddress {}, } + + /// helper that serializes a MsgTransfer to protobuf + pub fn to_proto_msg_transfer(msg: impl Message) -> NeutronResult { + // Serialize the Transfer message + let mut buf = Vec::new(); + buf.reserve(msg.encoded_len()); + if let Err(e) = msg.encode(&mut buf) { + return Err(StdError::generic_err(format!("Encode error: {e}")).into()); + } + + Ok(ProtobufAny { + type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), + value: Binary::from(buf), + }) + } } From 56b010a7ef680c283468d2c9242706d0306a3bd0 Mon Sep 17 00:00:00 2001 From: bekauz Date: Sat, 12 Aug 2023 23:57:57 +0200 Subject: [PATCH 027/586] updating ibc-forwarder with new queries --- contracts/ibc-forwarder/src/contract.rs | 37 +++++++++++++++++++------ contracts/ibc-forwarder/src/msg.rs | 28 +++---------------- contracts/ibc-forwarder/src/state.rs | 6 ++-- 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index 11c12982..0ff269a9 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -1,13 +1,15 @@ -use cosmos_sdk_proto::ibc::applications::transfer::v1::MsgTransfer; +use std::str::FromStr; + +use cosmos_sdk_proto::{ibc::applications::transfer::v1::MsgTransfer, cosmos::base::v1beta1::Coin}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use covenant_utils::neutron_ica; -use cosmwasm_std::{Env, MessageInfo, Response, Deps, DepsMut, StdError, Binary, Addr, to_binary, StdResult, Storage, to_vec, CosmosMsg, SubMsg, Reply, from_binary}; +use covenant_utils::neutron_ica::{self, RemoteChainInfo}; +use cosmwasm_std::{Env, MessageInfo, Response, Deps, DepsMut, StdError, Binary, Addr, to_binary, StdResult, Storage, to_vec, CosmosMsg, SubMsg, Reply, from_binary, Uint128, CustomQuery}; use covenant_clock::helpers::verify_clock; use cw2::set_contract_version; use neutron_sdk::{NeutronResult, bindings::{msg::{NeutronMsg, MsgSubmitTxResponse}, query::NeutronQuery, types::ProtobufAny}, interchain_txs::helpers::get_port_id, NeutronError, sudo::msg::{SudoMsg, RequestPacket},}; -use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState, RemoteChainInfo, QueryMsg}, state::{CONTRACT_STATE, CLOCK_ADDRESS, INTERCHAIN_ACCOUNTS, REMOTE_CHAIN_INFO, NEXT_CONTRACT, REPLY_ID_STORAGE, SUDO_PAYLOAD}}; +use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState, QueryMsg}, state::{CONTRACT_STATE, CLOCK_ADDRESS, INTERCHAIN_ACCOUNTS, REMOTE_CHAIN_INFO, NEXT_CONTRACT, REPLY_ID_STORAGE, SUDO_PAYLOAD, TRANSFER_AMOUNT}}; const CONTRACT_NAME: &str = "crates.io:covenant-ibc-forwarder"; @@ -29,12 +31,11 @@ pub fn instantiate( let next_contract = deps.api.addr_validate(&msg.next_contract)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; - + TRANSFER_AMOUNT.save(deps.storage, &Uint128::from_str(msg.amount.as_str())?)?; REMOTE_CHAIN_INFO.save(deps.storage, &RemoteChainInfo { connection_id: msg.remote_chain_connection_id, channel_id: msg.remote_chain_channel_id, denom: msg.denom, - amount: msg.amount, ibc_fee: msg.ibc_fee, ica_timeout: msg.ica_timeout, ibc_transfer_timeout: msg.ibc_transfer_timeout, @@ -121,8 +122,11 @@ fn try_forward_funds(env: Env, mut deps: ExecuteDeps) -> NeutronResult { let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; - - let coin = remote_chain_info.proto_coin(); + let amount = TRANSFER_AMOUNT.load(deps.storage)?; + let coin = Coin { + denom: remote_chain_info.denom, + amount: amount.to_string(), + }; let transfer_msg = MsgTransfer { source_port: "transfer".to_string(), @@ -209,9 +213,26 @@ pub fn query(deps: QueryDeps, env: Env, msg: QueryMsg) -> NeutronResult Ok(to_binary(&ica)?) }, + QueryMsg::ICAAddress {} => Ok(to_binary(&Addr::unchecked( + get_ica(deps, &env, INTERCHAIN_ACCOUNT_ID)?.0, + ))?), + QueryMsg::RemoteChainInfo {} => Ok(to_binary(&REMOTE_CHAIN_INFO.may_load(deps.storage)?)?), } } +fn get_ica( + deps: Deps, + env: &Env, + interchain_account_id: &str, +) -> Result<(String, String), StdError> { + let key = get_port_id(env.contract.address.as_str(), interchain_account_id); + + INTERCHAIN_ACCOUNTS + .load(deps.storage, key)? + .ok_or_else(|| StdError::generic_err("Interchain account is not created yet")) +} + + #[cfg_attr(not(feature = "library"), entry_point)] pub fn sudo(deps: ExecuteDeps, env: Env, msg: SudoMsg) -> StdResult { deps.api diff --git a/contracts/ibc-forwarder/src/msg.rs b/contracts/ibc-forwarder/src/msg.rs index 5f60e949..73fb1d81 100644 --- a/contracts/ibc-forwarder/src/msg.rs +++ b/contracts/ibc-forwarder/src/msg.rs @@ -1,8 +1,8 @@ -use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Uint64, Attribute, Addr}; -use covenant_macros::{clocked, covenant_deposit_address, covenant_clock_address}; +use covenant_macros::{clocked, covenant_deposit_address, covenant_clock_address, covenant_remote_chain, covenant_ica_address}; use neutron_sdk::bindings::msg::IbcFee; +use covenant_utils::neutron_ica::RemoteChainInfo; #[cw_serde] pub struct InstantiateMsg { @@ -52,34 +52,14 @@ impl InstantiateMsg { } } -#[cw_serde] -pub struct RemoteChainInfo { - /// connection id from neutron to the remote chain on which - /// we wish to open an ICA - pub connection_id: String, - pub channel_id: String, - pub denom: String, - pub amount: String, - pub ibc_transfer_timeout: Uint64, - pub ica_timeout: Uint64, - pub ibc_fee: IbcFee, -} - -impl RemoteChainInfo { - pub fn proto_coin(&self) -> Coin { - Coin { - denom: self.denom.to_string(), - amount: self.amount.to_string(), - } - } -} - #[clocked] #[cw_serde] pub enum ExecuteMsg {} #[covenant_deposit_address] +#[covenant_remote_chain] #[covenant_clock_address] +#[covenant_ica_address] #[derive(QueryResponses)] #[cw_serde] pub enum QueryMsg {} diff --git a/contracts/ibc-forwarder/src/state.rs b/contracts/ibc-forwarder/src/state.rs index b7d43f2c..c213f691 100644 --- a/contracts/ibc-forwarder/src/state.rs +++ b/contracts/ibc-forwarder/src/state.rs @@ -1,8 +1,9 @@ -use cosmwasm_std::{Addr, Uint64}; +use cosmwasm_std::{Addr, Uint64, Uint128}; +use covenant_utils::neutron_ica::RemoteChainInfo; use cw_storage_plus::{Item, Map}; use neutron_sdk::bindings::msg::IbcFee; -use crate::msg::{ContractState, RemoteChainInfo}; +use crate::msg::{ContractState}; @@ -11,6 +12,7 @@ pub const CONTRACT_STATE: Item = Item::new("contract_state"); /// clock module address to verify the sender of incoming ticks pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); +pub const TRANSFER_AMOUNT: Item = Item::new("transfer_amount"); pub const NEXT_CONTRACT: Item = Item::new("next_contract"); From 5819a1835650e92c2ba606003a3117be7989113d Mon Sep 17 00:00:00 2001 From: bekauz Date: Sun, 13 Aug 2023 14:18:46 +0200 Subject: [PATCH 028/586] lints on ls, depositor and ibc-forwarder --- contracts/depositor/src/contract.rs | 12 ++++++------ contracts/ibc-forwarder/src/contract.rs | 6 +++--- contracts/ibc-forwarder/src/state.rs | 21 ++------------------- contracts/ls/src/contract.rs | 6 ++---- contracts/ls/src/msg.rs | 1 - contracts/ls/src/state.rs | 6 +++--- 6 files changed, 16 insertions(+), 36 deletions(-) diff --git a/contracts/depositor/src/contract.rs b/contracts/depositor/src/contract.rs index 145b4919..754b4c1b 100644 --- a/contracts/depositor/src/contract.rs +++ b/contracts/depositor/src/contract.rs @@ -14,9 +14,9 @@ use neutron_sdk::bindings::types::ProtobufAny; use prost::Message; use crate::{ - msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, OpenAckVersion, QueryMsg, ContractState, SudoPayload, AcknowledgementResult}, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, OpenAckVersion, QueryMsg, ContractState, SudoPayload}, state::{ - IBC_TRANSFER_TIMEOUT, ICA_TIMEOUT, NEUTRON_ATOM_IBC_DENOM, PENDING_NATIVE_TRANSFER_TIMEOUT, clear_sudo_payload, + IBC_TRANSFER_TIMEOUT, ICA_TIMEOUT, NEUTRON_ATOM_IBC_DENOM, PENDING_NATIVE_TRANSFER_TIMEOUT, }, }; use neutron_sdk::{ @@ -24,13 +24,13 @@ use neutron_sdk::{ msg::{MsgSubmitTxResponse, NeutronMsg}, query::{NeutronQuery, QueryInterchainAccountAddressResponse}, }, - interchain_txs::helpers::{decode_acknowledgement_response, get_port_id}, + interchain_txs::helpers::get_port_id, sudo::msg::{RequestPacket, SudoMsg}, NeutronError, NeutronResult, }; use crate::state::{ - add_error_to_queue, read_errors_from_queue, read_reply_payload, read_sudo_payload, + read_errors_from_queue, read_reply_payload, read_sudo_payload, save_reply_payload, save_sudo_payload, ACKNOWLEDGEMENT_RESULTS, AUTOPILOT_FORMAT, CLOCK_ADDRESS, CONTRACT_STATE, GAIA_NEUTRON_IBC_TRANSFER_CHANNEL_ID, GAIA_STRIDE_IBC_TRANSFER_CHANNEL_ID, IBC_FEE, @@ -668,7 +668,7 @@ fn sudo_open_ack( port_id, &Some(( parsed_version.clone().address, - parsed_version.clone().controller_connection_id, + parsed_version.controller_connection_id, )), )?; CONTRACT_STATE.save(deps.storage, &ContractState::ICACreated)?; @@ -693,7 +693,7 @@ fn sudo_response(deps: ExecuteDeps, request: RequestPacket, data: Binary) -> Std let payload = read_sudo_payload(deps.storage, channel_id, seq_id).ok(); if let Some(payload) = payload { - if payload.message == "try_send_native_token".to_string() { + if payload.message == *"try_send_native_token" { // we advance the state machine to validation phase where we will query the balances of // LP module to confirm that funds have arrived CONTRACT_STATE.save(deps.storage, &ContractState::VerifyNativeToken)?; diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index 0ff269a9..cf8ff8fc 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -7,7 +7,7 @@ use covenant_utils::neutron_ica::{self, RemoteChainInfo}; use cosmwasm_std::{Env, MessageInfo, Response, Deps, DepsMut, StdError, Binary, Addr, to_binary, StdResult, Storage, to_vec, CosmosMsg, SubMsg, Reply, from_binary, Uint128, CustomQuery}; use covenant_clock::helpers::verify_clock; use cw2::set_contract_version; -use neutron_sdk::{NeutronResult, bindings::{msg::{NeutronMsg, MsgSubmitTxResponse}, query::NeutronQuery, types::ProtobufAny}, interchain_txs::helpers::get_port_id, NeutronError, sudo::msg::{SudoMsg, RequestPacket},}; +use neutron_sdk::{NeutronResult, bindings::{msg::{NeutronMsg, MsgSubmitTxResponse}, query::NeutronQuery}, interchain_txs::helpers::get_port_id, NeutronError, sudo::msg::{SudoMsg, RequestPacket},}; use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState, QueryMsg}, state::{CONTRACT_STATE, CLOCK_ADDRESS, INTERCHAIN_ACCOUNTS, REMOTE_CHAIN_INFO, NEXT_CONTRACT, REPLY_ID_STORAGE, SUDO_PAYLOAD, TRANSFER_AMOUNT}}; @@ -292,12 +292,12 @@ fn sudo_open_ack( port_id, &Some(( parsed_version.clone().address, - parsed_version.clone().controller_connection_id, + parsed_version.controller_connection_id, )), )?; CONTRACT_STATE.save(deps.storage, &ContractState::ICACreated)?; - return Ok(Response::default() + Ok(Response::default() .add_attribute("method", "sudo_open_ack") ) } diff --git a/contracts/ibc-forwarder/src/state.rs b/contracts/ibc-forwarder/src/state.rs index c213f691..6741f0b2 100644 --- a/contracts/ibc-forwarder/src/state.rs +++ b/contracts/ibc-forwarder/src/state.rs @@ -1,11 +1,8 @@ -use cosmwasm_std::{Addr, Uint64, Uint128}; +use cosmwasm_std::{Addr, Uint128}; use covenant_utils::neutron_ica::RemoteChainInfo; use cw_storage_plus::{Item, Map}; -use neutron_sdk::bindings::msg::IbcFee; - -use crate::msg::{ContractState}; - +use crate::msg::ContractState; /// tracks the current state of state machine pub const CONTRACT_STATE: Item = Item::new("contract_state"); @@ -19,20 +16,6 @@ pub const NEXT_CONTRACT: Item = Item::new("next_contract"); /// information needed for an ibc transfer to the remote chain pub const REMOTE_CHAIN_INFO: Item = Item::new("r_c_info"); -// /// timeout in seconds for inner ibc MsgTransfer -// pub const IBC_TRANSFER_TIMEOUT: Item = Item::new("ibc_transfer_timeout"); -// /// time in seconds for ICA SubmitTX messages from neutron -// pub const ICA_TIMEOUT: Item = Item::new("ica_timeout"); -// /// neutron IbcFee for relayers -// pub const IBC_FEE: Item = Item::new("ibc_fee"); - - -/// id of the connection between neutron and remote chain on which we -/// wish to open an ICA on -// pub const REMOTE_CHAIN_CONNECTION_ID: Item = Item::new("rc_conn_id"); -// pub const REMOTE_CHAIN_DENOM: Item = Item::new("rc_denom"); -// pub const TRANSFER_CHANNEL_ID: Item = Item::new("transfer_chann_id"); - /// interchain accounts storage in form of (port_id) -> (address, controller_connection_id) pub const INTERCHAIN_ACCOUNTS: Map> = Map::new("interchain_accounts"); diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs index 0beadc31..9db1efa6 100644 --- a/contracts/ls/src/contract.rs +++ b/contracts/ls/src/contract.rs @@ -1,6 +1,5 @@ use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; use cosmos_sdk_proto::ibc::applications::transfer::v1::MsgTransfer; -use cosmos_sdk_proto::traits::Message; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ @@ -10,7 +9,6 @@ use cosmwasm_std::{ use covenant_clock::helpers::verify_clock; use covenant_utils::neutron_ica::{SudoPayload, OpenAckVersion, RemoteChainInfo, self}; use cw2::set_contract_version; -use neutron_sdk::bindings::types::ProtobufAny; use crate::msg::{ ContractState, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, @@ -326,12 +324,12 @@ fn sudo_open_ack( port_id, &Some(( parsed_version.clone().address, - parsed_version.clone().controller_connection_id, + parsed_version.controller_connection_id, )), )?; CONTRACT_STATE.save(deps.storage, &ContractState::ICACreated)?; - return Ok(Response::default() + Ok(Response::default() .add_attribute("method", "sudo_open_ack") ) } diff --git a/contracts/ls/src/msg.rs b/contracts/ls/src/msg.rs index 4c8cdb96..aeb03532 100644 --- a/contracts/ls/src/msg.rs +++ b/contracts/ls/src/msg.rs @@ -1,7 +1,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Binary, Uint128, Uint64}; use covenant_macros::{covenant_deposit_address, clocked, covenant_clock_address, covenant_remote_chain, covenant_ica_address}; -use covenant_utils::neutron_ica::AcknowledgementResult; use neutron_sdk::bindings::msg::IbcFee; use covenant_utils::neutron_ica::RemoteChainInfo; diff --git a/contracts/ls/src/state.rs b/contracts/ls/src/state.rs index 96cca34b..95ea914e 100644 --- a/contracts/ls/src/state.rs +++ b/contracts/ls/src/state.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{from_binary, to_vec, Addr, Binary, Order, StdResult, Storage, Uint128}; -use covenant_utils::neutron_ica::{AcknowledgementResult, SudoPayload, RemoteChainInfo}; +use covenant_utils::neutron_ica::{SudoPayload, RemoteChainInfo}; use cw_storage_plus::{Item, Map}; use crate::msg::ContractState; @@ -26,8 +26,8 @@ pub const INTERCHAIN_ACCOUNTS: Map> = pub const AUTOPILOT_FORMAT: Item = Item::new("autopilot_format"); /// interchain transaction responses - ack/err/timeout state to query later -pub const ACKNOWLEDGEMENT_RESULTS: Map<(String, u64), AcknowledgementResult> = - Map::new("acknowledgement_results"); +// pub const ACKNOWLEDGEMENT_RESULTS: Map<(String, u64), AcknowledgementResult> = +// Map::new("acknowledgement_results"); pub const REPLY_ID_STORAGE: Item> = Item::new("reply_queue_id"); pub const SUDO_PAYLOAD: Map<(String, u64), Vec> = Map::new("sudo_payload"); pub const ERRORS_QUEUE: Map = Map::new("errors_queue"); From fccae362517e1e56d3236747a05e71c3f100582c Mon Sep 17 00:00:00 2001 From: bekauz Date: Sun, 13 Aug 2023 22:49:19 +0200 Subject: [PATCH 029/586] fix camelcase enum variant naming with acronyms (ICACreated -> IcaCreated) --- contracts/depositor/src/contract.rs | 8 ++++---- contracts/depositor/src/msg.rs | 2 +- contracts/depositor/src/suite_test/unit_helpers.rs | 2 +- contracts/depositor/src/suite_test/unit_test.rs | 2 +- contracts/ibc-forwarder/src/contract.rs | 6 +++--- contracts/ibc-forwarder/src/msg.rs | 2 +- contracts/ls/src/contract.rs | 6 +++--- contracts/ls/src/msg.rs | 2 +- packages/covenant-macros/src/lib.rs | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/contracts/depositor/src/contract.rs b/contracts/depositor/src/contract.rs index 754b4c1b..4a4cf975 100644 --- a/contracts/depositor/src/contract.rs +++ b/contracts/depositor/src/contract.rs @@ -126,7 +126,7 @@ fn try_tick(deps: ExecuteDeps, env: Env, info: MessageInfo) -> NeutronResult try_register_gaia_ica(deps, env), - ContractState::ICACreated => { + ContractState::IcaCreated => { let ica_address = get_ica(deps.as_ref(), &env, INTERCHAIN_ACCOUNT_ID); match ica_address { Ok((_, _)) => { @@ -359,8 +359,8 @@ fn try_verify_native_token(env: Env, deps: ExecuteDeps) -> NeutronResult= active_timeout.plus_minutes(5).nanos() { // funds are still not on the LP module and the msgTransfer timeout is due // we can safely retry sending the funds again by reverting the state - // to ICACreated - CONTRACT_STATE.save(deps.storage, &ContractState::ICACreated)?; + // to IcaCreated + CONTRACT_STATE.save(deps.storage, &ContractState::IcaCreated)?; PENDING_NATIVE_TRANSFER_TIMEOUT.remove(deps.storage); return Ok(Response::default() .add_attribute("method", "try_verify_native_token") @@ -671,7 +671,7 @@ fn sudo_open_ack( parsed_version.controller_connection_id, )), )?; - CONTRACT_STATE.save(deps.storage, &ContractState::ICACreated)?; + CONTRACT_STATE.save(deps.storage, &ContractState::IcaCreated)?; return Ok(Response::default().add_attribute("method", "sudo_open_ack")); } Err(StdError::generic_err("Can't parse counterparty_version")) diff --git a/contracts/depositor/src/msg.rs b/contracts/depositor/src/msg.rs index eab829e9..5eadc1a1 100644 --- a/contracts/depositor/src/msg.rs +++ b/contracts/depositor/src/msg.rs @@ -189,7 +189,7 @@ pub enum ContractState { /// Contract was instantiated, create ica Instantiated, /// ICA was created, send native token to lper - ICACreated, + IcaCreated, /// Verify native token was sent to lper and send ls msg VerifyNativeToken, /// Verify the lper entered a position, if not try to resend ls msg again diff --git a/contracts/depositor/src/suite_test/unit_helpers.rs b/contracts/depositor/src/suite_test/unit_helpers.rs index f9e8ad80..75d240b0 100644 --- a/contracts/depositor/src/suite_test/unit_helpers.rs +++ b/contracts/depositor/src/suite_test/unit_helpers.rs @@ -55,7 +55,7 @@ pub fn wasm_handler(wasm_query: &WasmQuery) -> SystemResult match contract_addr.as_ref() { LS_ADDR => match from_binary::(msg).unwrap() { - covenant_ls::msg::QueryMsg::ICAAddress {} => SystemResult::Ok(ContractResult::Ok( + covenant_ls::msg::QueryMsg::IcaAddress {} => SystemResult::Ok(ContractResult::Ok( to_binary(&Addr::unchecked("some_ica_addr")).unwrap(), )), _ => unimplemented!(), diff --git a/contracts/depositor/src/suite_test/unit_test.rs b/contracts/depositor/src/suite_test/unit_test.rs index 42d8bbff..db2f851d 100644 --- a/contracts/depositor/src/suite_test/unit_test.rs +++ b/contracts/depositor/src/suite_test/unit_test.rs @@ -43,7 +43,7 @@ fn test_tick_1() { let (mut deps, _) = do_instantiate(); deps = do_tick_1(deps); - verify_state(&deps, ContractState::ICACreated); + verify_state(&deps, ContractState::IcaCreated); } // This test should send the native token to the lper and set state to VerifyNativeToken diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index cf8ff8fc..8b2bdfc9 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -69,7 +69,7 @@ fn try_tick(deps: ExecuteDeps, env: Env, info: MessageInfo) -> NeutronResult try_register_ica(deps, env), - ContractState::ICACreated => try_forward_funds(env, deps), + ContractState::IcaCreated => try_forward_funds(env, deps), ContractState::Complete => Ok(Response::default() .add_attribute("contract_state", "completed") ), @@ -213,7 +213,7 @@ pub fn query(deps: QueryDeps, env: Env, msg: QueryMsg) -> NeutronResult Ok(to_binary(&ica)?) }, - QueryMsg::ICAAddress {} => Ok(to_binary(&Addr::unchecked( + QueryMsg::IcaAddress {} => Ok(to_binary(&Addr::unchecked( get_ica(deps, &env, INTERCHAIN_ACCOUNT_ID)?.0, ))?), QueryMsg::RemoteChainInfo {} => Ok(to_binary(&REMOTE_CHAIN_INFO.may_load(deps.storage)?)?), @@ -295,7 +295,7 @@ fn sudo_open_ack( parsed_version.controller_connection_id, )), )?; - CONTRACT_STATE.save(deps.storage, &ContractState::ICACreated)?; + CONTRACT_STATE.save(deps.storage, &ContractState::IcaCreated)?; Ok(Response::default() .add_attribute("method", "sudo_open_ack") diff --git a/contracts/ibc-forwarder/src/msg.rs b/contracts/ibc-forwarder/src/msg.rs index 73fb1d81..4e8efbe2 100644 --- a/contracts/ibc-forwarder/src/msg.rs +++ b/contracts/ibc-forwarder/src/msg.rs @@ -69,7 +69,7 @@ pub enum ContractState { /// Contract was instantiated, ready create ica Instantiated, /// ICA was created, funds are ready to be forwarded - ICACreated, + IcaCreated, /// forwarder is complete Complete, } diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs index 9db1efa6..f4ded2ec 100644 --- a/contracts/ls/src/contract.rs +++ b/contracts/ls/src/contract.rs @@ -106,7 +106,7 @@ fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> NeutronResult try_register_stride_ica(deps, env), - ContractState::ICACreated => Ok(Response::default()), + ContractState::IcaCreated => Ok(Response::default()), } } @@ -232,7 +232,7 @@ fn msg_with_sudo_callback>, T>( pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult { match msg { QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), - QueryMsg::ICAAddress {} => Ok(to_binary(&Addr::unchecked( + QueryMsg::IcaAddress {} => Ok(to_binary(&Addr::unchecked( get_ica(deps, &env, INTERCHAIN_ACCOUNT_ID)?.0, ))?), QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.may_load(deps.storage)?)?), @@ -327,7 +327,7 @@ fn sudo_open_ack( parsed_version.controller_connection_id, )), )?; - CONTRACT_STATE.save(deps.storage, &ContractState::ICACreated)?; + CONTRACT_STATE.save(deps.storage, &ContractState::IcaCreated)?; Ok(Response::default() .add_attribute("method", "sudo_open_ack") diff --git a/contracts/ls/src/msg.rs b/contracts/ls/src/msg.rs index aeb03532..40c906e7 100644 --- a/contracts/ls/src/msg.rs +++ b/contracts/ls/src/msg.rs @@ -116,5 +116,5 @@ pub enum MigrateMsg { #[cw_serde] pub enum ContractState { Instantiated, - ICACreated, + IcaCreated, } diff --git a/packages/covenant-macros/src/lib.rs b/packages/covenant-macros/src/lib.rs index 78b807be..f9d482e2 100644 --- a/packages/covenant-macros/src/lib.rs +++ b/packages/covenant-macros/src/lib.rs @@ -108,7 +108,7 @@ pub fn covenant_ica_address(metadata: TokenStream, input: TokenStream) -> TokenS enum ICA { /// Returns the associated remote chain information #[returns(Option)] - ICAAddress {}, + IcaAddress {}, } ) .into(), From fd150492a98bdb24c0d4ef5b4f27f2da8995a516 Mon Sep 17 00:00:00 2001 From: bekauz Date: Sun, 13 Aug 2023 23:10:27 +0200 Subject: [PATCH 030/586] expecting Uint128 amount instead of str; RemoteChainInfo response attributes helper --- contracts/ibc-forwarder/src/contract.rs | 10 ++++---- contracts/ibc-forwarder/src/msg.rs | 6 ++--- contracts/ls/src/contract.rs | 8 +++--- packages/covenant-utils/src/lib.rs | 33 ++++++++++++++++++++++++- 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index 8b2bdfc9..7d342d94 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use cosmos_sdk_proto::{ibc::applications::transfer::v1::MsgTransfer, cosmos::base::v1beta1::Coin}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -31,21 +29,23 @@ pub fn instantiate( let next_contract = deps.api.addr_validate(&msg.next_contract)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; - TRANSFER_AMOUNT.save(deps.storage, &Uint128::from_str(msg.amount.as_str())?)?; - REMOTE_CHAIN_INFO.save(deps.storage, &RemoteChainInfo { + TRANSFER_AMOUNT.save(deps.storage, &msg.amount)?; + let remote_chain_info = RemoteChainInfo { connection_id: msg.remote_chain_connection_id, channel_id: msg.remote_chain_channel_id, denom: msg.denom, ibc_fee: msg.ibc_fee, ica_timeout: msg.ica_timeout, ibc_transfer_timeout: msg.ibc_transfer_timeout, - })?; + }; + REMOTE_CHAIN_INFO.save(deps.storage, &remote_chain_info)?; CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; Ok(Response::default() .add_attribute("method", "ibc_forwarder_instantiate") .add_attribute("next_contract", next_contract) .add_attribute("contract_state", "instantiated") + .add_attributes(remote_chain_info.get_response_attributes()) ) } diff --git a/contracts/ibc-forwarder/src/msg.rs b/contracts/ibc-forwarder/src/msg.rs index 4e8efbe2..75063b7f 100644 --- a/contracts/ibc-forwarder/src/msg.rs +++ b/contracts/ibc-forwarder/src/msg.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Uint64, Attribute, Addr}; +use cosmwasm_std::{Uint64, Attribute, Addr, Uint128}; use covenant_macros::{clocked, covenant_deposit_address, covenant_clock_address, covenant_remote_chain, covenant_ica_address}; use neutron_sdk::bindings::msg::IbcFee; use covenant_utils::neutron_ica::RemoteChainInfo; @@ -16,7 +16,7 @@ pub struct InstantiateMsg { pub remote_chain_connection_id: String, pub remote_chain_channel_id: String, pub denom: String, - pub amount: String, + pub amount: Uint128, // pub remote_chain_channel_id: String, // pub remote_chain_connection_id: String, @@ -45,7 +45,7 @@ impl InstantiateMsg { Attribute::new("remote_chain_connection_id", &self.remote_chain_connection_id), Attribute::new("remote_chain_channel_id", &self.remote_chain_channel_id), Attribute::new("remote_chain_denom", &self.denom), - Attribute::new("remote_chain_amount", &self.amount), + Attribute::new("remote_chain_amount", &self.amount.to_string()), Attribute::new("ibc_transfer_timeout", self.ibc_transfer_timeout.to_string()), Attribute::new("ica_timeout", self.ica_timeout.to_string()), ] diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs index f4ded2ec..931e3bac 100644 --- a/contracts/ls/src/contract.rs +++ b/contracts/ls/src/contract.rs @@ -51,22 +51,22 @@ pub fn instantiate( CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; - REMOTE_CHAIN_INFO.save(deps.storage, &RemoteChainInfo { + let remote_chain_info = RemoteChainInfo { connection_id: msg.neutron_stride_ibc_connection_id, channel_id: msg.stride_neutron_ibc_transfer_channel_id, denom: msg.ls_denom, ibc_transfer_timeout: msg.ibc_transfer_timeout, ica_timeout: msg.ica_timeout, ibc_fee: msg.ibc_fee, - })?; + }; + REMOTE_CHAIN_INFO.save(deps.storage, &remote_chain_info)?; CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; Ok(Response::default() .add_attribute("method", "ls_instantiate") .add_attribute("clock_address", clock_addr) .add_attribute("next_contract", next_contract) - .add_attribute("ibc_transfer_timeout", msg.ibc_transfer_timeout) - .add_attribute("ica_timeout", msg.ica_timeout) + .add_attributes(remote_chain_info.get_response_attributes()) ) } diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index d5f86b3e..15a5def9 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -1,7 +1,7 @@ pub mod neutron_ica { use cosmwasm_schema::{cw_serde, QueryResponses}; - use cosmwasm_std::{Uint64, Addr, Binary, StdError}; + use cosmwasm_std::{Uint64, Addr, Binary, StdError, Attribute, Coin}; use neutron_sdk::{bindings::{msg::IbcFee, types::ProtobufAny}, NeutronResult}; use prost::Message; @@ -46,6 +46,37 @@ pub mod neutron_ica { pub ibc_fee: IbcFee, } + impl RemoteChainInfo { + pub fn get_response_attributes(&self) -> Vec { + let recv_fee = coin_vec_to_string(&self.ibc_fee.recv_fee); + let ack_fee = coin_vec_to_string(&self.ibc_fee.ack_fee); + let timeout_fee = coin_vec_to_string(&self.ibc_fee.timeout_fee); + + vec![ + Attribute::new("connection_id", &self.connection_id), + Attribute::new("channel_id", &self.channel_id), + Attribute::new("denom", &self.denom), + Attribute::new("ibc_transfer_timeout", &self.ibc_transfer_timeout.to_string()), + Attribute::new("ica_timeout", &self.ica_timeout.to_string()), + Attribute::new("ibc_recv_fee", recv_fee), + Attribute::new("ibc_ack_fee", ack_fee), + Attribute::new("ibc_timeout_fee", timeout_fee), + ] + } + } + + fn coin_vec_to_string(coins: &Vec) -> String { + let mut str = "".to_string(); + if coins.len() == 0 { + str.push_str(&"[]".to_string()); + } else { + for coin in coins { + str.push_str(&coin.to_string()); + } + } + str.to_string() + } + #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { From 642f2f63fea635576c175109fcb77345dafdcc29 Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 14 Aug 2023 14:25:16 +0200 Subject: [PATCH 031/586] moving proto coin generation to utils; merging LS update to a single config parameter --- Cargo.lock | 1 + contracts/ibc-forwarder/src/contract.rs | 12 ++-- contracts/ls/src/contract.rs | 74 +++---------------------- contracts/ls/src/msg.rs | 13 +++-- packages/covenant-utils/Cargo.toml | 1 + packages/covenant-utils/src/lib.rs | 19 ++++++- 6 files changed, 38 insertions(+), 82 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ff7c45ae..1daffcc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -522,6 +522,7 @@ dependencies = [ name = "covenant-utils" version = "0.0.1" dependencies = [ + "cosmos-sdk-proto 0.14.0", "cosmwasm-schema", "cosmwasm-std", "neutron-sdk", diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index 7d342d94..af7d94b3 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -1,8 +1,8 @@ -use cosmos_sdk_proto::{ibc::applications::transfer::v1::MsgTransfer, cosmos::base::v1beta1::Coin}; +use cosmos_sdk_proto::ibc::applications::transfer::v1::MsgTransfer; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use covenant_utils::neutron_ica::{self, RemoteChainInfo}; -use cosmwasm_std::{Env, MessageInfo, Response, Deps, DepsMut, StdError, Binary, Addr, to_binary, StdResult, Storage, to_vec, CosmosMsg, SubMsg, Reply, from_binary, Uint128, CustomQuery}; +use covenant_utils::neutron_ica::{self, RemoteChainInfo, get_proto_coin}; +use cosmwasm_std::{Env, MessageInfo, Response, Deps, DepsMut, StdError, Binary, Addr, to_binary, StdResult, Storage, to_vec, CosmosMsg, SubMsg, Reply, from_binary, CustomQuery}; use covenant_clock::helpers::verify_clock; use cw2::set_contract_version; use neutron_sdk::{NeutronResult, bindings::{msg::{NeutronMsg, MsgSubmitTxResponse}, query::NeutronQuery}, interchain_txs::helpers::get_port_id, NeutronError, sudo::msg::{SudoMsg, RequestPacket},}; @@ -123,15 +123,11 @@ fn try_forward_funds(env: Env, mut deps: ExecuteDeps) -> NeutronResult { let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; let amount = TRANSFER_AMOUNT.load(deps.storage)?; - let coin = Coin { - denom: remote_chain_info.denom, - amount: amount.to_string(), - }; let transfer_msg = MsgTransfer { source_port: "transfer".to_string(), source_channel: remote_chain_info.channel_id, - token: Some(coin), + token: Some(get_proto_coin(remote_chain_info.denom, amount)), sender: address, receiver: deposit_address.to_string(), timeout_height: None, diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs index 931e3bac..6bbcee39 100644 --- a/contracts/ls/src/contract.rs +++ b/contracts/ls/src/contract.rs @@ -1,4 +1,3 @@ -use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; use cosmos_sdk_proto::ibc::applications::transfer::v1::MsgTransfer; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -7,7 +6,7 @@ use cosmwasm_std::{ Response, StdError, StdResult, SubMsg, Uint128, }; use covenant_clock::helpers::verify_clock; -use covenant_utils::neutron_ica::{SudoPayload, OpenAckVersion, RemoteChainInfo, self}; +use covenant_utils::neutron_ica::{SudoPayload, OpenAckVersion, RemoteChainInfo, self, get_proto_coin}; use cw2::set_contract_version; use crate::msg::{ @@ -156,18 +155,13 @@ fn try_execute_transfer( Some((address, controller_conn_id)) => { let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; - let coin = Coin { - denom: remote_chain_info.denom, - amount: amount.to_string(), - }; - // inner MsgTransfer that will be sent from stride to neutron. // because of this message delivery depending on the ica wrapper below, // timeout_timestamp = current block + ica timeout + ibc_transfer_timeout let msg = MsgTransfer { source_port: "transfer".to_string(), source_channel: remote_chain_info.channel_id, - token: Some(coin), + token: Some(get_proto_coin(remote_chain_info.denom, amount)), sender: address, receiver: deposit_address.to_string(), timeout_height: None, @@ -438,13 +432,8 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult match msg { MigrateMsg::UpdateConfig { clock_addr, - stride_neutron_ibc_transfer_channel_id, next_contract, - neutron_stride_ibc_connection_id, - ls_denom, - ibc_fee, - ibc_transfer_timeout, - ica_timeout, + remote_chain_info, } => { let mut resp = Response::default().add_attribute("method", "update_config"); @@ -454,65 +443,16 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult resp = resp.add_attribute("clock_addr", addr.to_string()); } - if let Some(channel_id) = stride_neutron_ibc_transfer_channel_id { - REMOTE_CHAIN_INFO.update(deps.storage, |mut info| -> StdResult<_>{ - info.channel_id = channel_id.to_string(); - Ok(info) - })?; - resp = resp.add_attribute("stride_neutron_ibc_transfer_channel_id", channel_id); - } - if let Some(addr) = next_contract { let addr = deps.api.addr_validate(&addr)?; resp = resp.add_attribute("next_contract", addr.to_string()); NEXT_CONTRACT.save(deps.storage, &addr)?; } - if let Some(connection_id) = neutron_stride_ibc_connection_id { - REMOTE_CHAIN_INFO.update(deps.storage, |mut info| -> StdResult<_>{ - info.connection_id = connection_id.to_string(); - Ok(info) - })?; - resp = resp.add_attribute("neutron_stride_ibc_connection_id", connection_id); - } - - if let Some(denom) = ls_denom { - REMOTE_CHAIN_INFO.update(deps.storage, |mut info| -> StdResult<_>{ - info.denom = denom.to_string(); - Ok(info) - })?; - resp = resp.add_attribute("ls_denom", denom); - } - - if let Some(timeout) = ibc_transfer_timeout { - REMOTE_CHAIN_INFO.update(deps.storage, |mut info| -> StdResult<_>{ - info.ibc_transfer_timeout = timeout; - Ok(info) - })?; - resp = resp.add_attribute("ibc_transfer_timeout", timeout); - } - - if let Some(timeout) = ica_timeout { - REMOTE_CHAIN_INFO.update(deps.storage, |mut info| -> StdResult<_>{ - info.ica_timeout = timeout; - Ok(info) - })?; - resp = resp.add_attribute("ica_timeout", timeout); - } - - if let Some(fee) = ibc_fee { - if fee.ack_fee.is_empty() || fee.timeout_fee.is_empty() || !fee.recv_fee.is_empty() - { - return Err(StdError::GenericErr { - msg: "invalid IbcFee".to_string(), - }); - } - REMOTE_CHAIN_INFO.update(deps.storage, |mut info| -> StdResult<_>{ - info.ibc_fee = fee.clone(); - Ok(info) - })?; - resp = resp.add_attribute("ibc_fee_ack", fee.ack_fee[0].to_string()); - resp = resp.add_attribute("ibc_fee_timeout", fee.timeout_fee[0].to_string()); + if let Some(rci) = remote_chain_info { + let validated_rci = rci.validate()?; + REMOTE_CHAIN_INFO.save(deps.storage, &validated_rci)?; + resp = resp.add_attributes(validated_rci.get_response_attributes()); } Ok(resp) diff --git a/contracts/ls/src/msg.rs b/contracts/ls/src/msg.rs index 40c906e7..e5da4e96 100644 --- a/contracts/ls/src/msg.rs +++ b/contracts/ls/src/msg.rs @@ -100,13 +100,14 @@ pub enum QueryMsg { pub enum MigrateMsg { UpdateConfig { clock_addr: Option, - stride_neutron_ibc_transfer_channel_id: Option, + // stride_neutron_ibc_transfer_channel_id: Option, next_contract: Option, - neutron_stride_ibc_connection_id: Option, - ls_denom: Option, - ibc_fee: Option, - ibc_transfer_timeout: Option, - ica_timeout: Option, + // neutron_stride_ibc_connection_id: Option, + // ls_denom: Option, + // ibc_fee: Option, + // ibc_transfer_timeout: Option, + // ica_timeout: Option, + remote_chain_info: Option, }, UpdateCodeId { data: Option, diff --git a/packages/covenant-utils/Cargo.toml b/packages/covenant-utils/Cargo.toml index 4b4fdf1a..3ca536e9 100644 --- a/packages/covenant-utils/Cargo.toml +++ b/packages/covenant-utils/Cargo.toml @@ -14,3 +14,4 @@ cosmwasm-schema = { workspace = true } neutron-sdk = { workspace = true } cosmwasm-std = { workspace = true } prost = { workspace = true } +cosmos-sdk-proto = { workspace = true } diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index 15a5def9..b6a41b86 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -1,7 +1,7 @@ pub mod neutron_ica { use cosmwasm_schema::{cw_serde, QueryResponses}; - use cosmwasm_std::{Uint64, Addr, Binary, StdError, Attribute, Coin}; + use cosmwasm_std::{Uint64, Addr, Binary, StdError, Attribute, Coin, Uint128}; use neutron_sdk::{bindings::{msg::IbcFee, types::ProtobufAny}, NeutronResult}; use prost::Message; @@ -63,6 +63,16 @@ pub mod neutron_ica { Attribute::new("ibc_timeout_fee", timeout_fee), ] } + + pub fn validate(self) -> Result { + if self.ibc_fee.ack_fee.is_empty() || self.ibc_fee.timeout_fee.is_empty() || !self.ibc_fee.recv_fee.is_empty() { + return Err(StdError::GenericErr { + msg: "invalid IbcFee".to_string(), + }) + } + + Ok(self) + } } fn coin_vec_to_string(coins: &Vec) -> String { @@ -77,6 +87,13 @@ pub mod neutron_ica { str.to_string() } + pub fn get_proto_coin(denom: String, amount: Uint128) -> cosmos_sdk_proto::cosmos::base::v1beta1::Coin { + cosmos_sdk_proto::cosmos::base::v1beta1::Coin { + denom, + amount: amount.to_string(), + } + } + #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { From 6284c48bc3f4efff79203a6ec0f61d488eebaa65 Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 14 Aug 2023 14:37:03 +0200 Subject: [PATCH 032/586] IcaAddress and DepositAddress queries return strings --- contracts/ibc-forwarder/src/contract.rs | 10 ++++------ contracts/ls/src/contract.rs | 10 ++++------ packages/covenant-macros/src/lib.rs | 2 +- packages/covenant-utils/src/lib.rs | 4 ++-- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index af7d94b3..e1601581 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -2,7 +2,7 @@ use cosmos_sdk_proto::ibc::applications::transfer::v1::MsgTransfer; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use covenant_utils::neutron_ica::{self, RemoteChainInfo, get_proto_coin}; -use cosmwasm_std::{Env, MessageInfo, Response, Deps, DepsMut, StdError, Binary, Addr, to_binary, StdResult, Storage, to_vec, CosmosMsg, SubMsg, Reply, from_binary, CustomQuery}; +use cosmwasm_std::{Env, MessageInfo, Response, Deps, DepsMut, StdError, Binary, to_binary, StdResult, Storage, to_vec, CosmosMsg, SubMsg, Reply, from_binary, CustomQuery}; use covenant_clock::helpers::verify_clock; use cw2::set_contract_version; use neutron_sdk::{NeutronResult, bindings::{msg::{NeutronMsg, MsgSubmitTxResponse}, query::NeutronQuery}, interchain_txs::helpers::get_port_id, NeutronError, sudo::msg::{SudoMsg, RequestPacket},}; @@ -101,7 +101,7 @@ fn try_forward_funds(env: Env, mut deps: ExecuteDeps) -> NeutronResult = deps.querier.query_wasm_smart( + let deposit_address_query: Option = deps.querier.query_wasm_smart( next_contract, &covenant_utils::neutron_ica::QueryMsg::DepositAddress {}, )?; @@ -129,7 +129,7 @@ fn try_forward_funds(env: Env, mut deps: ExecuteDeps) -> NeutronResult NeutronResult Ok(to_binary(&ica)?) }, - QueryMsg::IcaAddress {} => Ok(to_binary(&Addr::unchecked( - get_ica(deps, &env, INTERCHAIN_ACCOUNT_ID)?.0, - ))?), + QueryMsg::IcaAddress {} => Ok(to_binary(&get_ica(deps, &env, INTERCHAIN_ACCOUNT_ID)?.0)?), QueryMsg::RemoteChainInfo {} => Ok(to_binary(&REMOTE_CHAIN_INFO.may_load(deps.storage)?)?), } } diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs index 6bbcee39..5b09d259 100644 --- a/contracts/ls/src/contract.rs +++ b/contracts/ls/src/contract.rs @@ -2,7 +2,7 @@ use cosmos_sdk_proto::ibc::applications::transfer::v1::MsgTransfer; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Addr, Binary, CosmosMsg, CustomQuery, Deps, DepsMut, Env, MessageInfo, Reply, + to_binary, Binary, CosmosMsg, CustomQuery, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdError, StdResult, SubMsg, Uint128, }; use covenant_clock::helpers::verify_clock; @@ -136,7 +136,7 @@ fn try_execute_transfer( // first we verify whether the next contract is ready for receiving the funds let next_contract = NEXT_CONTRACT.load(deps.storage)?; - let deposit_address_query: Option = deps.querier.query_wasm_smart( + let deposit_address_query: Option = deps.querier.query_wasm_smart( next_contract, &covenant_utils::neutron_ica::QueryMsg::DepositAddress {}, )?; @@ -163,7 +163,7 @@ fn try_execute_transfer( source_channel: remote_chain_info.channel_id, token: Some(get_proto_coin(remote_chain_info.denom, amount)), sender: address, - receiver: deposit_address.to_string(), + receiver: deposit_address, timeout_height: None, timeout_timestamp: env .block @@ -226,9 +226,7 @@ fn msg_with_sudo_callback>, T>( pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult { match msg { QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), - QueryMsg::IcaAddress {} => Ok(to_binary(&Addr::unchecked( - get_ica(deps, &env, INTERCHAIN_ACCOUNT_ID)?.0, - ))?), + QueryMsg::IcaAddress {} => Ok(to_binary(&get_ica(deps, &env, INTERCHAIN_ACCOUNT_ID)?.0)?), QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.may_load(deps.storage)?)?), QueryMsg::DepositAddress {} => { let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); diff --git a/packages/covenant-macros/src/lib.rs b/packages/covenant-macros/src/lib.rs index f9d482e2..d8d4e990 100644 --- a/packages/covenant-macros/src/lib.rs +++ b/packages/covenant-macros/src/lib.rs @@ -107,7 +107,7 @@ pub fn covenant_ica_address(metadata: TokenStream, input: TokenStream) -> TokenS quote!( enum ICA { /// Returns the associated remote chain information - #[returns(Option)] + #[returns(Option)] IcaAddress {}, } ) diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index b6a41b86..c6c28418 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -1,7 +1,7 @@ pub mod neutron_ica { use cosmwasm_schema::{cw_serde, QueryResponses}; - use cosmwasm_std::{Uint64, Addr, Binary, StdError, Attribute, Coin, Uint128}; + use cosmwasm_std::{Uint64, Binary, StdError, Attribute, Coin, Uint128}; use neutron_sdk::{bindings::{msg::IbcFee, types::ProtobufAny}, NeutronResult}; use prost::Message; @@ -98,7 +98,7 @@ pub mod neutron_ica { #[derive(QueryResponses)] pub enum QueryMsg { /// Returns the associated remote chain information - #[returns(Option)] + #[returns(Option)] DepositAddress {}, } From 9e0aca1e5b65caeaa9865c5a7212ea6c923ab845 Mon Sep 17 00:00:00 2001 From: bekauz Date: Sun, 13 Aug 2023 21:42:13 +0200 Subject: [PATCH 033/586] adding DepositAddress query; merging state vars relevant to LPing into LpConfig --- contracts/lper/src/contract.rs | 126 ++++++++----------------- contracts/lper/src/msg.rs | 42 +++++---- contracts/lper/src/state.rs | 21 +---- contracts/lper/src/suite_test/suite.rs | 9 +- 4 files changed, 71 insertions(+), 127 deletions(-) diff --git a/contracts/lper/src/contract.rs b/contracts/lper/src/contract.rs index 1862e17e..aa9c122e 100644 --- a/contracts/lper/src/contract.rs +++ b/contracts/lper/src/contract.rs @@ -15,11 +15,11 @@ use astroport::{ use crate::{ error::ContractError, - msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, ProvidedLiquidityInfo, ContractState}, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, ProvidedLiquidityInfo, ContractState, LpConfig}, state::{ - ALLOWED_RETURN_DELTA, ASSETS, AUTOSTAKE, EXPECTED_LS_TOKEN_AMOUNT, - EXPECTED_NATIVE_TOKEN_AMOUNT, HOLDER_ADDRESS, PROVIDED_LIQUIDITY_INFO, - SINGLE_SIDED_LP_LIMITS, SLIPPAGE_TOLERANCE, POOL_ADDRESS, + ASSETS, + HOLDER_ADDRESS, PROVIDED_LIQUIDITY_INFO, + LP_CONFIG, }, }; @@ -55,15 +55,20 @@ pub fn instantiate( // store the relevant module addresses CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; - POOL_ADDRESS.save(deps.storage, &pool_addr)?; HOLDER_ADDRESS.save(deps.storage, &holder_addr)?; - // store fields needed for liquidity provision ASSETS.save(deps.storage, &msg.assets)?; - SINGLE_SIDED_LP_LIMITS.save(deps.storage, &msg.single_side_lp_limits)?; - ALLOWED_RETURN_DELTA.save(deps.storage, &msg.allowed_return_delta)?; - EXPECTED_LS_TOKEN_AMOUNT.save(deps.storage, &msg.expected_ls_token_amount)?; - EXPECTED_NATIVE_TOKEN_AMOUNT.save(deps.storage, &msg.expected_native_token_amount)?; + + let lp_config = LpConfig { + expected_native_token_amount: msg.expected_native_token_amount, + expected_ls_token_amount: msg.expected_ls_token_amount, + allowed_return_delta: msg.allowed_return_delta, + pool_address: pool_addr, + single_side_lp_limits: msg.single_side_lp_limits, + autostake: msg.autostake, + slippage_tolerance: msg.slippage_tolerance, + }; + LP_CONFIG.save(deps.storage, &lp_config)?; // we begin with no liquidity provided PROVIDED_LIQUIDITY_INFO.save( @@ -77,15 +82,12 @@ pub fn instantiate( Ok(Response::default() .add_attribute("method", "lp_instantiate") .add_attribute("clock_addr", clock_addr) - .add_attribute("pool_addr", pool_addr) .add_attribute("holder_addr", holder_addr) .add_attribute("ls_asset_denom", msg.assets.ls_asset_denom) .add_attribute("native_asset_denom", msg.assets.native_asset_denom) .add_attribute("expected_native_token_amount", msg.expected_native_token_amount) .add_attribute("expected_ls_token_amount", msg.expected_ls_token_amount) .add_attribute("allowed_return_delta", msg.allowed_return_delta) - .add_attribute("single_side_ls_limit", msg.single_side_lp_limits.ls_asset_limit) - .add_attribute("single_side_native_limit", msg.single_side_lp_limits.native_asset_limit) ) } @@ -176,19 +178,14 @@ fn try_get_double_side_lp_submsg( native_bal: Coin, ls_bal: Coin, ) -> Result, ContractError> { - let pool_address = POOL_ADDRESS.load(deps.storage)?; - let slippage_tolerance = SLIPPAGE_TOLERANCE.may_load(deps.storage)?; - let auto_stake = AUTOSTAKE.may_load(deps.storage)?; + let lp_config = LP_CONFIG.load(deps.storage)?; let asset_data = ASSETS.load(deps.storage)?; let holder_address = HOLDER_ADDRESS.load(deps.storage)?; - let expected_ls_token_amount = EXPECTED_LS_TOKEN_AMOUNT.load(deps.storage)?; - let expected_native_token_amount = EXPECTED_NATIVE_TOKEN_AMOUNT.load(deps.storage)?; - let allowed_return_delta = ALLOWED_RETURN_DELTA.load(deps.storage)?; // we now query the pool to know the balances let pool_response: PoolResponse = deps .querier - .query_wasm_smart(&pool_address, &astroport::pair::QueryMsg::Pool {})?; + .query_wasm_smart(&lp_config.pool_address, &astroport::pair::QueryMsg::Pool {})?; let (pool_native_bal, pool_ls_bal) = get_pool_asset_amounts( pool_response.assets, asset_data.clone().ls_asset_denom, @@ -199,9 +196,9 @@ fn try_get_double_side_lp_submsg( validate_price_range( pool_native_bal, pool_ls_bal, - expected_native_token_amount, - expected_ls_token_amount, - allowed_return_delta, + lp_config.expected_native_token_amount, + lp_config.expected_ls_token_amount, + lp_config.allowed_return_delta, )?; // we derive the ratio of native to ls. @@ -252,8 +249,8 @@ fn try_get_double_side_lp_submsg( native_asset_double_sided.clone(), ls_asset_double_sided.clone(), ], - slippage_tolerance, - auto_stake, + slippage_tolerance: lp_config.slippage_tolerance, + auto_stake: lp_config.autostake, receiver: Some(holder_address.to_string()), }; let (native_coin, ls_coin) = ( @@ -277,7 +274,7 @@ fn try_get_double_side_lp_submsg( Ok(Some(SubMsg::reply_on_success( CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: pool_address.to_string(), + contract_addr: lp_config.pool_address.to_string(), msg: to_binary(&double_sided_liq_msg)?, funds: vec![native_coin, ls_coin], }), @@ -294,12 +291,9 @@ fn try_get_single_side_lp_submsg( native_bal: Coin, ls_bal: Coin, ) -> Result, ContractError> { - let pool_address = POOL_ADDRESS.load(deps.storage)?; - let slippage_tolerance = SLIPPAGE_TOLERANCE.may_load(deps.storage)?; - let auto_stake = AUTOSTAKE.may_load(deps.storage)?; let asset_data = ASSETS.load(deps.storage)?; - let single_side_lp_limits = SINGLE_SIDED_LP_LIMITS.load(deps.storage)?; let holder_address = HOLDER_ADDRESS.load(deps.storage)?; + let lp_config = LP_CONFIG.load(deps.storage)?; let native_asset = Asset { info: asset_data.get_native_asset_info(), @@ -313,17 +307,17 @@ fn try_get_single_side_lp_submsg( // given one non-zero asset, we build the ProvideLiquidity message let single_sided_liq_msg = ProvideLiquidity { assets: vec![ls_asset, native_asset], - slippage_tolerance, - auto_stake, + slippage_tolerance: lp_config.slippage_tolerance, + auto_stake: lp_config.autostake, receiver: Some(holder_address.to_string()), }; // now we try to submit the message for either LS or native single side liquidity - if native_bal.amount.is_zero() && ls_bal.amount <= single_side_lp_limits.ls_asset_limit { + if native_bal.amount.is_zero() && ls_bal.amount <= lp_config.single_side_lp_limits.ls_asset_limit { // if available ls token amount is within single side limits we build a single side msg let submsg = SubMsg::reply_on_success( CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: pool_address.to_string(), + contract_addr: lp_config.pool_address.to_string(), msg: to_binary(&single_sided_liq_msg)?, funds: vec![ls_bal.clone()], }), @@ -335,12 +329,12 @@ fn try_get_single_side_lp_submsg( })?; return Ok(Some(submsg)); } else if ls_bal.amount.is_zero() - && native_bal.amount <= single_side_lp_limits.native_asset_limit + && native_bal.amount <= lp_config.single_side_lp_limits.native_asset_limit { // if available native token amount is within single side limits we build a single side msg let submsg = SubMsg::reply_on_success( CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: pool_address.to_string(), + contract_addr: lp_config.pool_address.to_string(), msg: to_binary(&single_sided_liq_msg)?, funds: vec![native_bal.clone()], }), @@ -430,22 +424,15 @@ fn get_pool_asset_amounts( #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), - QueryMsg::PoolAddress {} => Ok(to_binary(&POOL_ADDRESS.may_load(deps.storage)?)?), QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.may_load(deps.storage)?)?), QueryMsg::HolderAddress {} => Ok(to_binary(&HOLDER_ADDRESS.may_load(deps.storage)?)?), QueryMsg::Assets {} => Ok(to_binary(&ASSETS.may_load(deps.storage)?)?), - QueryMsg::ExpectedLsTokenAmount {} => Ok(to_binary( - &EXPECTED_LS_TOKEN_AMOUNT.may_load(deps.storage)?, - )?), - QueryMsg::AllowedReturnDelta {} => { - Ok(to_binary(&ALLOWED_RETURN_DELTA.may_load(deps.storage)?)?) - } - QueryMsg::ExpectedNativeTokenAmount {} => Ok(to_binary( - &EXPECTED_NATIVE_TOKEN_AMOUNT.may_load(deps.storage)?, - )?), + QueryMsg::LpConfig {} => Ok(to_binary(&LP_CONFIG.may_load(deps.storage)?)?), + // the deposit address for LP module is the contract itself + QueryMsg::DepositAddress {} => Ok(to_binary(&env.contract.address)?), } } @@ -456,15 +443,9 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> NeutronResult { let mut response = Response::default().add_attribute("method", "update_config"); @@ -473,51 +454,20 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> NeutronResult, + /// slippage tolerance parameter for liquidity provisioning + pub slippage_tolerance: Option, +} + /// holds the native and ls asset denoms relevant for providing liquidity. #[cw_serde] pub struct AssetData { @@ -107,40 +125,28 @@ impl PresetLpFields { #[cw_serde] pub enum ExecuteMsg {} +#[covenant_clock_address] +#[covenant_deposit_address] #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { - #[returns(Addr)] - PoolAddress {}, - #[returns(Addr)] - ClockAddress {}, #[returns(ContractState)] ContractState {}, #[returns(Addr)] HolderAddress {}, #[returns(Vec)] Assets {}, - #[returns(Uint128)] - ExpectedLsTokenAmount {}, - #[returns(Uint128)] - AllowedReturnDelta {}, - #[returns(Uint128)] - ExpectedNativeTokenAmount {}, + #[returns(LpConfig)] + LpConfig {}, } #[cw_serde] pub enum MigrateMsg { UpdateConfig { clock_addr: Option, - pool_address: Option, holder_address: Option, - expected_ls_token_amount: Option, - allowed_return_delta: Option, - single_side_lp_limits: Option, - slippage_tolerance: Option, assets: Option, - expected_native_token_amount: Option, - autostake: Option, + lp_config: Option, }, UpdateCodeId { data: Option, diff --git a/contracts/lper/src/state.rs b/contracts/lper/src/state.rs index 2d13275f..beb8cb08 100644 --- a/contracts/lper/src/state.rs +++ b/contracts/lper/src/state.rs @@ -1,7 +1,7 @@ -use cosmwasm_std::{Addr, Decimal, Uint128}; +use cosmwasm_std::Addr; use cw_storage_plus::Item; -use crate::msg::{AssetData, SingleSideLpLimits, ProvidedLiquidityInfo, ContractState}; +use crate::msg::{AssetData, ProvidedLiquidityInfo, ContractState, LpConfig}; /// contract state tracks the state machine progress pub const CONTRACT_STATE: Item = Item::new("contract_state"); @@ -10,25 +10,12 @@ pub const ASSETS: Item = Item::new("assets"); /// clock module address to verify the incoming ticks sender pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); -/// address of the liquidity pool we plan to enter -pub const POOL_ADDRESS: Item = Item::new("pool_address"); /// holder module address to verify withdrawal requests pub const HOLDER_ADDRESS: Item = Item::new("holder_address"); -/// boolean flag for enabling autostaking of LP tokens upon liquidity provisioning -pub const AUTOSTAKE: Item = Item::new("autostake"); -/// slippage tolerance parameter for liquidity provisioning -pub const SLIPPAGE_TOLERANCE: Item = Item::new("slippage_tolerance"); - -/// amounts of native and ls tokens we consider ok to single-side lp -pub const SINGLE_SIDED_LP_LIMITS: Item = Item::new("single_side_lp_limit"); /// keeps track of ls and native token amounts we provided to the pool pub const PROVIDED_LIQUIDITY_INFO: Item = Item::new("provided_liquidity_info"); -/// the native token amount we expect to receive from depositor -pub const EXPECTED_NATIVE_TOKEN_AMOUNT: Item = Item::new("expected_native_token_amount"); -/// stride redemption rate is variable so we set the expected ls token amount -pub const EXPECTED_LS_TOKEN_AMOUNT: Item = Item::new("expected_ls_token_amount"); -/// accepted return amount fluctuation that gets applied to EXPECTED_LS_TOKEN_AMOUNT -pub const ALLOWED_RETURN_DELTA: Item = Item::new("allowed_return_delta"); +/// configuration relevant to entering into an LP position +pub const LP_CONFIG: Item = Item::new("lp_config"); \ No newline at end of file diff --git a/contracts/lper/src/suite_test/suite.rs b/contracts/lper/src/suite_test/suite.rs index 4ca95746..484ecfde 100644 --- a/contracts/lper/src/suite_test/suite.rs +++ b/contracts/lper/src/suite_test/suite.rs @@ -15,7 +15,7 @@ use cw_multi_test::{ }; use neutron_sdk::bindings::{msg::NeutronMsg, query::NeutronQuery}; -use crate::msg::{AssetData, InstantiateMsg, QueryMsg, SingleSideLpLimits}; +use crate::msg::{AssetData, InstantiateMsg, QueryMsg, SingleSideLpLimits, LpConfig}; use astroport::factory::InstantiateMsg as FactoryInstantiateMsg; use astroport::native_coin_registry::InstantiateMsg as NativeCoinRegistryInstantiateMsg; use astroport::pair::InstantiateMsg as PairInstantiateMsg; @@ -460,10 +460,11 @@ impl Suite { } pub fn query_lp_position(&self) -> String { - self.app + let lp_config: LpConfig = self.app .wrap() - .query_wasm_smart(&self.liquid_pooler.1, &QueryMsg::PoolAddress {}) - .unwrap() + .query_wasm_smart(&self.liquid_pooler.1, &QueryMsg::LpConfig {}) + .unwrap(); + lp_config.pool_address.to_string() } pub fn query_contract_state(&self) -> String { From 56592aa527f25cc59700ef6dac8e8c72ed2a20ac Mon Sep 17 00:00:00 2001 From: bekauz Date: Sun, 13 Aug 2023 22:38:00 +0200 Subject: [PATCH 034/586] adding to_response_attributes to LpConfig; validating pool address on migrate update --- contracts/lper/src/contract.rs | 7 +++---- contracts/lper/src/msg.rs | 31 ++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/contracts/lper/src/contract.rs b/contracts/lper/src/contract.rs index aa9c122e..de19d4b5 100644 --- a/contracts/lper/src/contract.rs +++ b/contracts/lper/src/contract.rs @@ -85,9 +85,7 @@ pub fn instantiate( .add_attribute("holder_addr", holder_addr) .add_attribute("ls_asset_denom", msg.assets.ls_asset_denom) .add_attribute("native_asset_denom", msg.assets.native_asset_denom) - .add_attribute("expected_native_token_amount", msg.expected_native_token_amount) - .add_attribute("expected_ls_token_amount", msg.expected_ls_token_amount) - .add_attribute("allowed_return_delta", msg.allowed_return_delta) + .add_attributes(lp_config.to_response_attributes()) ) } @@ -466,8 +464,9 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> NeutronResult, } +impl LpConfig { + pub fn to_response_attributes(self) -> Vec { + let autostake = match self.autostake { + Some(val) => val.to_string(), + None => "None".to_string(), + }; + let slippage_tolerance = match self.slippage_tolerance { + Some(val) => val.to_string(), + None => "None".to_string(), + }; + vec![ + Attribute::new("expected_native_token_amount", self.expected_native_token_amount.to_string()), + Attribute::new("expected_ls_token_amount", self.expected_ls_token_amount.to_string()), + Attribute::new("allowed_return_delta", self.allowed_return_delta.to_string()), + Attribute::new("pool_address", self.pool_address.to_string()), + Attribute::new( + "single_side_lp_limit_native", + self.single_side_lp_limits.native_asset_limit.to_string() + ), + Attribute::new( + "single_side_lp_limit_ls", + self.single_side_lp_limits.ls_asset_limit.to_string() + ), + Attribute::new("autostake", autostake), + Attribute::new("slippage_tolerance", slippage_tolerance), + ] + } +} + /// holds the native and ls asset denoms relevant for providing liquidity. #[cw_serde] pub struct AssetData { From e7a022c98eccddd8f15669f48c65c64c9e71ac14 Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 14 Aug 2023 16:22:54 +0200 Subject: [PATCH 035/586] switch to returning string in DepositAddress query --- Cargo.lock | 1 + .../covenant/src/suite_test/unit_tests.rs | 4 +- contracts/lper/src/contract.rs | 2 +- contracts/ls/src/contract.rs | 50 ++++++++++--------- packages/covenant-macros/Cargo.toml | 3 +- packages/covenant-macros/src/lib.rs | 6 +-- 6 files changed, 36 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1daffcc9..fdcf2ff1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -512,6 +512,7 @@ dependencies = [ name = "covenant-macros" version = "0.0.1" dependencies = [ + "cosmwasm-schema", "covenant-utils", "proc-macro2", "quote", diff --git a/contracts/covenant/src/suite_test/unit_tests.rs b/contracts/covenant/src/suite_test/unit_tests.rs index 6c065a5b..9c1e5a87 100644 --- a/contracts/covenant/src/suite_test/unit_tests.rs +++ b/contracts/covenant/src/suite_test/unit_tests.rs @@ -342,8 +342,8 @@ fn test_init() { ) .unwrap(); assert_eq!( - from_binary::(&depositor_addr).unwrap().as_ref(), - "contract_depositor" + from_binary::>(&depositor_addr).unwrap(), + Some("contract_depositor".to_string()) ); let lp_addr = query( diff --git a/contracts/lper/src/contract.rs b/contracts/lper/src/contract.rs index de19d4b5..4ac7a2ee 100644 --- a/contracts/lper/src/contract.rs +++ b/contracts/lper/src/contract.rs @@ -430,7 +430,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::Assets {} => Ok(to_binary(&ASSETS.may_load(deps.storage)?)?), QueryMsg::LpConfig {} => Ok(to_binary(&LP_CONFIG.may_load(deps.storage)?)?), // the deposit address for LP module is the contract itself - QueryMsg::DepositAddress {} => Ok(to_binary(&env.contract.address)?), + QueryMsg::DepositAddress {} => Ok(to_binary(&Some(&env.contract.address.to_string()))?), } } diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs index 5b09d259..3b42a207 100644 --- a/contracts/ls/src/contract.rs +++ b/contracts/ls/src/contract.rs @@ -136,7 +136,7 @@ fn try_execute_transfer( // first we verify whether the next contract is ready for receiving the funds let next_contract = NEXT_CONTRACT.load(deps.storage)?; - let deposit_address_query: Option = deps.querier.query_wasm_smart( + let deposit_address_query = deps.querier.query_wasm_smart( next_contract, &covenant_utils::neutron_ica::QueryMsg::DepositAddress {}, )?; @@ -229,28 +229,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult QueryMsg::IcaAddress {} => Ok(to_binary(&get_ica(deps, &env, INTERCHAIN_ACCOUNT_ID)?.0)?), QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.may_load(deps.storage)?)?), QueryMsg::DepositAddress {} => { - let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); - - // here we cover three cases: - let ica = match INTERCHAIN_ACCOUNTS.may_load(deps.storage, key)? { - Some(entry) => { - // 1. ICA had been created -> fetch the autopilot string and return Some(autopilot) - if let Some((addr, _)) = entry { - let autopilot_receiver = AUTOPILOT_FORMAT - .load(deps.storage)? - .replace("{st_ica}", &addr); - - Some(autopilot_receiver) - } - // 2. ICA creation request had been submitted but did not receive - // the channel_open_ack yet -> None - else { - None - } - }, - // 3. ICA creation request hadn't been submitted yet -> None - None => None, - }; + let ica = query_deposit_address(deps, env)?; // up to the querying module to make sense of the response Ok(to_binary(&ica)?) }, @@ -258,6 +237,31 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult } } +fn query_deposit_address(deps: Deps, env: Env) -> Result, StdError> { + let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); + + // here we cover three cases: + match INTERCHAIN_ACCOUNTS.may_load(deps.storage, key)? { + Some(entry) => { + // 1. ICA had been created -> fetch the autopilot string and return Some(autopilot) + if let Some((addr, _)) = entry { + let autopilot_receiver = AUTOPILOT_FORMAT + .load(deps.storage)? + .replace("{st_ica}", &addr); + + Ok(Some(autopilot_receiver)) + } + // 2. ICA creation request had been submitted but did not receive + // the channel_open_ack yet -> None + else { + Ok(None) + } + }, + // 3. ICA creation request hadn't been submitted yet -> None + None => Ok(None) + } +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> StdResult { deps.api diff --git a/packages/covenant-macros/Cargo.toml b/packages/covenant-macros/Cargo.toml index 8dd081b2..f464ca5c 100644 --- a/packages/covenant-macros/Cargo.toml +++ b/packages/covenant-macros/Cargo.toml @@ -14,4 +14,5 @@ proc-macro = true proc-macro2 = "1" quote = "1" syn = "1" -covenant-utils = { workspace = true } \ No newline at end of file +covenant-utils = { workspace = true } +cosmwasm-schema = { workspace = true } diff --git a/packages/covenant-macros/src/lib.rs b/packages/covenant-macros/src/lib.rs index d8d4e990..4c05b38a 100644 --- a/packages/covenant-macros/src/lib.rs +++ b/packages/covenant-macros/src/lib.rs @@ -1,5 +1,4 @@ use proc_macro::TokenStream; - use quote::quote; use syn::{parse_macro_input, AttributeArgs, DataEnum, DeriveInput}; @@ -56,10 +55,10 @@ pub fn covenant_deposit_address(metadata: TokenStream, input: TokenStream) -> To merge_variants( metadata, input, - quote!( + quote!( enum Deposit { /// Returns the address a contract expects to receive funds to - #[returns(Option)] + #[returns(Option)] DepositAddress {}, } ) @@ -67,6 +66,7 @@ pub fn covenant_deposit_address(metadata: TokenStream, input: TokenStream) -> To ) } + #[proc_macro_attribute] pub fn covenant_clock_address(metadata: TokenStream, input: TokenStream) -> TokenStream { merge_variants( From f701957e7cc8fb2a8d4454e83abf3eee135ed2e1 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 15 Aug 2023 12:32:38 +0200 Subject: [PATCH 036/586] removing unnecessary clones from lp contract; contract cleanup --- contracts/lper/src/contract.rs | 153 +++++++++++++-------------------- contracts/lper/src/msg.rs | 40 +++++++++ contracts/ls/src/contract.rs | 11 +-- 3 files changed, 102 insertions(+), 102 deletions(-) diff --git a/contracts/lper/src/contract.rs b/contracts/lper/src/contract.rs index 4ac7a2ee..8e22bd2e 100644 --- a/contracts/lper/src/contract.rs +++ b/contracts/lper/src/contract.rs @@ -118,13 +118,13 @@ fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> Result Result { let asset_data = ASSETS.load(deps.storage)?; - let contract = env.contract.address; + // first we query our own balances and filter out any unexpected denoms - let bal_coins = deps.querier.query_all_balances(contract.clone())?; + let bal_coins = deps.querier.query_all_balances(env.contract.address.to_string())?; let (native_bal, ls_bal) = get_relevant_balances( bal_coins, - asset_data.clone().ls_asset_denom, - asset_data.clone().native_asset_denom, + asset_data.ls_asset_denom, + asset_data.native_asset_denom, ); // depending on available balances we attempt a different action: @@ -186,17 +186,14 @@ fn try_get_double_side_lp_submsg( .query_wasm_smart(&lp_config.pool_address, &astroport::pair::QueryMsg::Pool {})?; let (pool_native_bal, pool_ls_bal) = get_pool_asset_amounts( pool_response.assets, - asset_data.clone().ls_asset_denom, - asset_data.clone().native_asset_denom, + asset_data.ls_asset_denom.as_str(), + asset_data.native_asset_denom.as_str(), )?; // we validate the pool to match our price expectations - validate_price_range( + lp_config.validate_price_range( pool_native_bal, pool_ls_bal, - lp_config.expected_native_token_amount, - lp_config.expected_ls_token_amount, - lp_config.allowed_return_delta, )?; // we derive the ratio of native to ls. @@ -213,48 +210,49 @@ fn try_get_double_side_lp_submsg( if native_bal.amount >= required_native_amount { // if we are able to satisfy the required amount, we do that: // provide all statom tokens along with required amount of native tokens - let ls_asset_double_sided = Asset { - info: asset_data.get_ls_asset_info(), - amount: ls_bal.amount, - }; - let native_asset_double_sided = Asset { - info: asset_data.get_native_asset_info(), - amount: required_native_amount, - }; - - (native_asset_double_sided, ls_asset_double_sided) + ( + Asset { + info: asset_data.get_native_asset_info(), + amount: required_native_amount, + }, + Asset { + info: asset_data.get_ls_asset_info(), + amount: ls_bal.amount, + } + ) } else { // otherwise, our native token amount is insufficient to provide double // sided liquidity using all of our ls tokens. // this means that we should provide all of our available native tokens, // and as many ls tokens as needed to satisfy the existing ratio - let native_asset_double_sided = Asset { - info: asset_data.get_native_asset_info(), - amount: native_bal.amount, - }; - let ls_asset_double_sided = Asset { - info: asset_data.get_ls_asset_info(), - amount: Decimal::from_ratio(pool_ls_bal, pool_native_bal) - .checked_mul_uint128(native_bal.amount)?, - }; - - (native_asset_double_sided, ls_asset_double_sided) + ( + Asset { + info: asset_data.get_native_asset_info(), + amount: native_bal.amount, + }, + Asset { + info: asset_data.get_ls_asset_info(), + amount: Decimal::from_ratio(pool_ls_bal, pool_native_bal) + .checked_mul_uint128(native_bal.amount)?, + }, + ) }; + let (native_coin, ls_coin) = ( + native_asset_double_sided.to_coin()?, + ls_asset_double_sided.to_coin()?, + ); + // craft a ProvideLiquidity message with the determined assets let double_sided_liq_msg = ProvideLiquidity { assets: vec![ - native_asset_double_sided.clone(), - ls_asset_double_sided.clone(), + native_asset_double_sided, + ls_asset_double_sided, ], slippage_tolerance: lp_config.slippage_tolerance, auto_stake: lp_config.autostake, receiver: Some(holder_address.to_string()), }; - let (native_coin, ls_coin) = ( - native_asset_double_sided.to_coin()?, - ls_asset_double_sided.to_coin()?, - ); // update the provided amounts and leftover assets PROVIDED_LIQUIDITY_INFO.update( @@ -262,10 +260,10 @@ fn try_get_double_side_lp_submsg( |mut info: ProvidedLiquidityInfo| -> StdResult<_> { info.provided_amount_ls = info .provided_amount_ls - .checked_add(ls_coin.clone().amount)?; + .checked_add(ls_coin.amount)?; info.provided_amount_native = info .provided_amount_native - .checked_add(native_coin.clone().amount)?; + .checked_add(native_coin.amount)?; Ok(info) }, )?; @@ -293,18 +291,11 @@ fn try_get_single_side_lp_submsg( let holder_address = HOLDER_ADDRESS.load(deps.storage)?; let lp_config = LP_CONFIG.load(deps.storage)?; - let native_asset = Asset { - info: asset_data.get_native_asset_info(), - amount: native_bal.amount, - }; - let ls_asset = Asset { - info: asset_data.get_ls_asset_info(), - amount: ls_bal.amount, - }; + let assets = asset_data.to_asset_vec(native_bal.amount, ls_bal.amount); // given one non-zero asset, we build the ProvideLiquidity message let single_sided_liq_msg = ProvideLiquidity { - assets: vec![ls_asset, native_asset], + assets, slippage_tolerance: lp_config.slippage_tolerance, auto_stake: lp_config.autostake, receiver: Some(holder_address.to_string()), @@ -312,37 +303,42 @@ fn try_get_single_side_lp_submsg( // now we try to submit the message for either LS or native single side liquidity if native_bal.amount.is_zero() && ls_bal.amount <= lp_config.single_side_lp_limits.ls_asset_limit { + // update the provided liquidity info + PROVIDED_LIQUIDITY_INFO.update(deps.storage, |mut info| -> StdResult<_> { + info.provided_amount_ls = info.provided_amount_ls.checked_add(ls_bal.amount)?; + Ok(info) + })?; + // if available ls token amount is within single side limits we build a single side msg let submsg = SubMsg::reply_on_success( CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: lp_config.pool_address.to_string(), msg: to_binary(&single_sided_liq_msg)?, - funds: vec![ls_bal.clone()], + funds: vec![ls_bal], }), SINGLE_SIDED_REPLY_ID, ); + + return Ok(Some(submsg)); + } else if ls_bal.amount.is_zero() + && native_bal.amount <= lp_config.single_side_lp_limits.native_asset_limit { + // update the provided liquidity info PROVIDED_LIQUIDITY_INFO.update(deps.storage, |mut info| -> StdResult<_> { - info.provided_amount_ls = info.provided_amount_ls.checked_add(ls_bal.amount)?; + info.provided_amount_native = + info.provided_amount_native.checked_add(native_bal.amount)?; Ok(info) })?; - return Ok(Some(submsg)); - } else if ls_bal.amount.is_zero() - && native_bal.amount <= lp_config.single_side_lp_limits.native_asset_limit - { + // if available native token amount is within single side limits we build a single side msg let submsg = SubMsg::reply_on_success( CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: lp_config.pool_address.to_string(), msg: to_binary(&single_sided_liq_msg)?, - funds: vec![native_bal.clone()], + funds: vec![native_bal], }), SINGLE_SIDED_REPLY_ID, ); - PROVIDED_LIQUIDITY_INFO.update(deps.storage, |mut info| -> StdResult<_> { - info.provided_amount_native = - info.provided_amount_native.checked_add(native_bal.amount)?; - Ok(info) - })?; + return Ok(Some(submsg)); } @@ -366,48 +362,16 @@ fn get_relevant_balances(coins: Vec, ls_denom: String, native_denom: Strin (native_bal, ls_bal) } -/// validates the existing pool balances to match our initial expectations. -/// if `PriceRangeError` is returned, it most likely means that the pool had a -/// significant shift in its balance ratio. -fn validate_price_range( - pool_native_amount: Uint128, - pool_ls_amount: Uint128, - expected_native_token_amount: Uint128, - expected_ls_token_amount: Uint128, - allowed_return_delta: Uint128, -) -> Result<(), ContractError> { - // find the min and max return amounts allowed by deviating away from expected return amount - // by allowed delta - let min_return_amount = expected_ls_token_amount.checked_sub(allowed_return_delta)?; - let max_return_amount = expected_ls_token_amount.checked_add(allowed_return_delta)?; - - // derive allowed proportions - let min_accepted_ratio = Decimal::from_ratio(min_return_amount, expected_native_token_amount); - let max_accepted_ratio = Decimal::from_ratio(max_return_amount, expected_native_token_amount); - - // we find the proportion of the price range being validated - let validation_ratio = Decimal::from_ratio(pool_ls_amount, pool_native_amount); - - // if current return to offer amount ratio falls out of [min_accepted_ratio, max_return_amount], - // return price range error - if validation_ratio < min_accepted_ratio || validation_ratio > max_accepted_ratio { - return Err(ContractError::PriceRangeError {}); - } - - Ok(()) -} - /// filters out irrelevant balances and returns ls and native amounts fn get_pool_asset_amounts( assets: Vec, - ls_denom: String, - native_denom: String, + ls_denom: &str, + native_denom: &str, ) -> Result<(Uint128, Uint128), StdError> { let (mut native_bal, mut ls_bal) = (Uint128::zero(), Uint128::zero()); for asset in assets { let coin = asset.to_coin()?; - if coin.denom == ls_denom { // found ls balance ls_bal = coin.amount; @@ -483,7 +447,6 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> NeutronResult Result { deps.api.debug("WASMDEBUG: reply"); - println!("{:?}", msg.clone().result.unwrap()); match msg.id { DOUBLE_SIDED_REPLY_ID => handle_double_sided_reply_id(deps, _env, msg), SINGLE_SIDED_REPLY_ID => handle_single_sided_reply_id(deps, _env, msg), diff --git a/contracts/lper/src/msg.rs b/contracts/lper/src/msg.rs index 1977d63a..2aa1e20f 100644 --- a/contracts/lper/src/msg.rs +++ b/contracts/lper/src/msg.rs @@ -3,6 +3,8 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Binary, Decimal, Uint128, Attribute}; use covenant_macros::{clocked, covenant_deposit_address, covenant_clock_address}; +use crate::error::ContractError; + #[cw_serde] pub struct InstantiateMsg { pub pool_address: String, @@ -62,6 +64,31 @@ impl LpConfig { Attribute::new("slippage_tolerance", slippage_tolerance), ] } + + /// validates the existing pool balances to match our initial expectations. + /// if `PriceRangeError` is returned, it most likely means that the pool had a + /// significant shift in its balance ratio. + pub fn validate_price_range(&self, pool_native_bal: Uint128, pool_ls_bal: Uint128) -> Result<(), ContractError> { + // find the min and max return amounts allowed by deviating away from expected return amount + // by allowed delta + let min_return_amount = self.expected_ls_token_amount.checked_sub(self.allowed_return_delta)?; + let max_return_amount = self.expected_ls_token_amount.checked_add(self.allowed_return_delta)?; + + // derive allowed proportions + let min_accepted_ratio = Decimal::from_ratio(min_return_amount, self.expected_native_token_amount); + let max_accepted_ratio = Decimal::from_ratio(max_return_amount, self.expected_native_token_amount); + + // we find the proportion of the price range being validated + let validation_ratio = Decimal::from_ratio(pool_ls_bal, pool_native_bal); + + // if current return to offer amount ratio falls out of [min_accepted_ratio, max_return_amount], + // return price range error + if validation_ratio < min_accepted_ratio || validation_ratio > max_accepted_ratio { + return Err(ContractError::PriceRangeError {}); + } + + Ok(()) + } } /// holds the native and ls asset denoms relevant for providing liquidity. @@ -85,6 +112,19 @@ impl AssetData { denom: self.ls_asset_denom.to_string(), } } + + pub fn to_asset_vec(&self, native_bal: Uint128, ls_bal: Uint128) -> Vec { + vec![ + Asset { + info: self.get_native_asset_info(), + amount: native_bal, + }, + Asset { + info: self.get_ls_asset_info(), + amount: ls_bal, + }, + ] + } } /// single side lp limits define the highest amount (in `Uint128`) that diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs index 3b42a207..9a002f29 100644 --- a/contracts/ls/src/contract.rs +++ b/contracts/ls/src/contract.rs @@ -83,15 +83,12 @@ pub fn execute( ExecuteMsg::Transfer { amount } => { let ica_address = get_ica(deps.as_ref(), &env, INTERCHAIN_ACCOUNT_ID); match ica_address { - Ok((_, _)) => { - try_execute_transfer(deps, env, info, amount) - }, - Err(_) => { - Ok(Response::default() + Ok(_) => try_execute_transfer(deps, env, info, amount), + Err(_) => Ok( + Response::default() .add_attribute("method", "try_permisionless_transfer") .add_attribute("ica_status", "not_created") - ) - }, + ), } }, } From 1d0ac098f1ce7c98c9497379f56a402629dbda1a Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 22 Aug 2023 09:41:13 +0200 Subject: [PATCH 037/586] comments --- contracts/lper/src/contract.rs | 1 + contracts/lper/src/msg.rs | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/contracts/lper/src/contract.rs b/contracts/lper/src/contract.rs index 8e22bd2e..6951e3fe 100644 --- a/contracts/lper/src/contract.rs +++ b/contracts/lper/src/contract.rs @@ -428,6 +428,7 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> NeutronResult Result<(), ContractError> { - // find the min and max return amounts allowed by deviating away from expected return amount - // by allowed delta - let min_return_amount = self.expected_ls_token_amount.checked_sub(self.allowed_return_delta)?; - let max_return_amount = self.expected_ls_token_amount.checked_add(self.allowed_return_delta)?; + // find the min return amount by subtracting the delta from expected amount + let min_return_amount = self.expected_ls_token_amount + .checked_sub(self.allowed_return_delta)?; + // find the max return amount by adding the delta to expected amount + let max_return_amount = self.expected_ls_token_amount + .checked_add(self.allowed_return_delta)?; // derive allowed proportions let min_accepted_ratio = Decimal::from_ratio(min_return_amount, self.expected_native_token_amount); From de354136b2d6d8900ab1be7d0d5a96ce6f116071 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 15 Aug 2023 19:19:25 +0200 Subject: [PATCH 038/586] native splitter init --- Cargo.lock | 20 ++++ Cargo.toml | 3 + contracts/ibc-forwarder/src/msg.rs | 2 - contracts/native-splitter/.cargo/config | 3 + contracts/native-splitter/Cargo.toml | 33 +++++++ contracts/native-splitter/README.md | 11 +++ contracts/native-splitter/src/contract.rs | 92 +++++++++++++++++++ contracts/native-splitter/src/error.rs | 1 + contracts/native-splitter/src/lib.rs | 12 +++ contracts/native-splitter/src/msg.rs | 74 +++++++++++++++ contracts/native-splitter/src/state.rs | 78 ++++++++++++++++ .../native-splitter/src/suite_test/mod.rs | 2 + .../native-splitter/src/suite_test/suite.rs | 1 + .../native-splitter/src/suite_test/tests.rs | 1 + 14 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 contracts/native-splitter/.cargo/config create mode 100644 contracts/native-splitter/Cargo.toml create mode 100644 contracts/native-splitter/README.md create mode 100644 contracts/native-splitter/src/contract.rs create mode 100644 contracts/native-splitter/src/error.rs create mode 100644 contracts/native-splitter/src/lib.rs create mode 100644 contracts/native-splitter/src/msg.rs create mode 100644 contracts/native-splitter/src/state.rs create mode 100644 contracts/native-splitter/src/suite_test/mod.rs create mode 100644 contracts/native-splitter/src/suite_test/suite.rs create mode 100644 contracts/native-splitter/src/suite_test/tests.rs diff --git a/Cargo.lock b/Cargo.lock index fdcf2ff1..2c2b52be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -519,6 +519,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "covenant-native-splitter" +version = "1.0.0" +dependencies = [ + "cosmos-sdk-proto 0.14.0", + "cosmwasm-schema", + "cosmwasm-std", + "covenant-clock", + "covenant-macros", + "covenant-utils", + "cw-storage-plus 1.1.0", + "cw2 1.1.0", + "neutron-sdk", + "protobuf 3.2.0", + "schemars", + "serde", + "serde-json-wasm 0.4.1", + "thiserror", +] + [[package]] name = "covenant-utils" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 5f5ca99f..66100d11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,9 @@ covenant-clock-tester = { path = "contracts/clock-tester" } covenant-ls = { path = "contracts/ls" } covenant-covenant = { path = "contracts/covenant" } covenant-holder = { path = "contracts/holder" } +covenant-ibc-forwarder = { path = "contracts/ibc-forwarder" } +covenant-native-splitter = { path = "contracts/native-splitter" } + # packages clock-derive = { path = "packages/clock-derive" } cw-fifo = { path = "packages/cw-fifo" } diff --git a/contracts/ibc-forwarder/src/msg.rs b/contracts/ibc-forwarder/src/msg.rs index 75063b7f..851abaed 100644 --- a/contracts/ibc-forwarder/src/msg.rs +++ b/contracts/ibc-forwarder/src/msg.rs @@ -18,8 +18,6 @@ pub struct InstantiateMsg { pub denom: String, pub amount: Uint128, - // pub remote_chain_channel_id: String, - // pub remote_chain_connection_id: String, /// neutron requires fees to be set to refund relayers for /// submission of ack and timeout messages. /// recv_fee and ack_fee paid in untrn from this contract diff --git a/contracts/native-splitter/.cargo/config b/contracts/native-splitter/.cargo/config new file mode 100644 index 00000000..6a6f2852 --- /dev/null +++ b/contracts/native-splitter/.cargo/config @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +schema = "run --bin schema" \ No newline at end of file diff --git a/contracts/native-splitter/Cargo.toml b/contracts/native-splitter/Cargo.toml new file mode 100644 index 00000000..11450290 --- /dev/null +++ b/contracts/native-splitter/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "covenant-native-splitter" +authors = ["benskey bekauz@protonmail.com"] +description = "Native Splitter module for covenants" +edition = { workspace = true } +license = { workspace = true } +rust-version = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# disables #[entry_point] (i.e. instantiate/execute/query) export +library = [] + +[dependencies] +covenant-macros = { workspace = true } +covenant-clock = { workspace = true, features=["library"] } +covenant-utils = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +thiserror = { workspace = true } +schemars = { workspace = true } +serde-json-wasm = { workspace = true } +serde = { workspace = true } +neutron-sdk = { workspace = true } +cosmos-sdk-proto = { workspace = true } +protobuf = { workspace = true } diff --git a/contracts/native-splitter/README.md b/contracts/native-splitter/README.md new file mode 100644 index 00000000..48b7105c --- /dev/null +++ b/contracts/native-splitter/README.md @@ -0,0 +1,11 @@ +# Native Splitter + +Native Splitter is a module meant to facilitate predefined splitting of funds on a remote chain. + +First, splitter creates an ICA on the specified chain. +Once the ICA address is known, splitter waits for the funds to arrive. + +During instantiation, a vector of forwarder modules along with their respective amounts (`Vec`) are specified. +The forwarder modules are then queried for their deposit addresses, which are going to be their respective ICA addresses. + +A combined `BankSend` is then performed to the ICAs on the same remote chain. If it suceeds, native splitter completes. diff --git a/contracts/native-splitter/src/contract.rs b/contracts/native-splitter/src/contract.rs new file mode 100644 index 00000000..1ca69913 --- /dev/null +++ b/contracts/native-splitter/src/contract.rs @@ -0,0 +1,92 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, + Response, StdResult, SubMsg, +}; +use covenant_clock::helpers::verify_clock; +use covenant_utils::neutron_ica::SudoPayload; +use cw2::set_contract_version; + +use crate::msg::{ + ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, +}; +use crate::state::{ + save_reply_payload, CLOCK_ADDRESS, CONTRACT_STATE, + REMOTE_CHAIN_INFO, +}; +use neutron_sdk::{ + bindings::{ + msg::NeutronMsg, + query::NeutronQuery, + }, + NeutronResult, +}; + + +const CONTRACT_NAME: &str = "crates.io:covenant-native-splitter"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const SUDO_PAYLOAD_REPLY_ID: u64 = 1u64; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> NeutronResult> { + deps.api.debug("WASMDEBUG: instantiate"); + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> NeutronResult> { + deps.api + .debug(format!("WASMDEBUG: execute: received msg: {msg:?}").as_str()); + match msg { + ExecuteMsg::Tick {} => try_tick(deps, env, info), + } +} + +/// attempts to advance the state machine. performs `info.sender` validation +fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> NeutronResult> { + // Verify caller is the clock + verify_clock(&info.sender, &CLOCK_ADDRESS.load(deps.storage)?)?; + + let current_state = CONTRACT_STATE.load(deps.storage)?; + match current_state { + ContractState::Instantiated => Ok(Response::default()), + ContractState::IcaCreated => Ok(Response::default()), + ContractState::Completed => Ok(Response::default()), + } +} + +#[allow(unused)] +fn msg_with_sudo_callback>, T>( + deps: DepsMut, + msg: C, + payload: SudoPayload, +) -> StdResult> { + save_reply_payload(deps.storage, payload)?; + Ok(SubMsg::reply_on_success(msg, SUDO_PAYLOAD_REPLY_ID)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> NeutronResult { + match msg { + QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), + QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.may_load(deps.storage)?)?), + QueryMsg::DepositAddress {} => { + Ok(to_binary(&Some(1))?) + }, + QueryMsg::RemoteChainInfo {} => Ok(to_binary(&REMOTE_CHAIN_INFO.may_load(deps.storage)?)?), + } +} diff --git a/contracts/native-splitter/src/error.rs b/contracts/native-splitter/src/error.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/contracts/native-splitter/src/error.rs @@ -0,0 +1 @@ + diff --git a/contracts/native-splitter/src/lib.rs b/contracts/native-splitter/src/lib.rs new file mode 100644 index 00000000..3b47dbd4 --- /dev/null +++ b/contracts/native-splitter/src/lib.rs @@ -0,0 +1,12 @@ +#![warn(clippy::unwrap_used, clippy::expect_used)] + +extern crate core; + +pub mod contract; +pub mod error; +pub mod msg; +pub mod state; + +#[allow(clippy::unwrap_used)] +#[cfg(test)] +mod suite_test; diff --git a/contracts/native-splitter/src/msg.rs b/contracts/native-splitter/src/msg.rs new file mode 100644 index 00000000..41e11b73 --- /dev/null +++ b/contracts/native-splitter/src/msg.rs @@ -0,0 +1,74 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Uint128, Uint64}; +use covenant_macros::{covenant_deposit_address, clocked, covenant_clock_address, covenant_remote_chain}; +use neutron_sdk::bindings::msg::IbcFee; +use covenant_utils::neutron_ica::RemoteChainInfo; + +#[cw_serde] +pub struct InstantiateMsg { + /// Address for the clock. This contract verifies + /// that only the clock can execute Ticks + pub clock_address: String, + + pub remote_chain_connection_id: String, + pub remote_chain_channel_id: String, + pub denom: String, + pub amount: Uint128, + + pub splits: Vec, + + /// Neutron requires fees to be set to refund relayers for + /// submission of ack and timeout messages. + /// recv_fee and ack_fee paid in untrn from this contract + pub ibc_fee: IbcFee, + /// Time in seconds for ICA SubmitTX messages from Neutron + /// Note that ICA uses ordered channels, a timeout implies + /// channel closed. We can reopen the channel by reregistering + /// the ICA with the same port id and connection id + pub ica_timeout: Uint64, + /// Timeout in seconds. This is used to craft a timeout timestamp + /// that will be attached to the IBC transfer message from the ICA + /// on the host chain (Stride) to its destination. Typically + /// this timeout should be greater than the ICA timeout, otherwise + /// if the ICA times out, the destination chain receiving the funds + /// will also receive the IBC packet with an expired timestamp. + pub ibc_transfer_timeout: Uint64, + +} + +#[cw_serde] +pub struct SplitConfig { + /// denom to be distributed + pub denom: String, + /// denom receivers and their respective shares + pub receivers: Vec, +} + +#[cw_serde] +pub struct SplitReceiver { + /// address of the receiver on remote chain + pub addr: Addr, + /// percentage share that the address is entitled to + pub share: Uint64, +} + +#[clocked] +#[cw_serde] +pub enum ExecuteMsg {} + +#[covenant_clock_address] +#[covenant_remote_chain] +#[covenant_deposit_address] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(ContractState)] + ContractState {}, +} + +#[cw_serde] +pub enum ContractState { + Instantiated, + IcaCreated, + Completed, +} diff --git a/contracts/native-splitter/src/state.rs b/contracts/native-splitter/src/state.rs new file mode 100644 index 00000000..8631d358 --- /dev/null +++ b/contracts/native-splitter/src/state.rs @@ -0,0 +1,78 @@ +use cosmwasm_std::{from_binary, to_vec, Addr, Binary, Order, StdResult, Storage, Uint128}; +use covenant_utils::neutron_ica::{SudoPayload, RemoteChainInfo}; +use cw_storage_plus::{Item, Map}; + +use crate::msg::ContractState; + +/// tracks the current state of state machine +pub const CONTRACT_STATE: Item = Item::new("contract_state"); + +/// clock module address to verify the sender of incoming ticks +pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); + +pub const TRANSFER_AMOUNT: Item = Item::new("transfer_amount"); + +pub const SPLIT_DESTINATIONS: Item> = Item::new("split_destinations"); + +/// information needed for an ibc transfer to the remote chain +pub const REMOTE_CHAIN_INFO: Item = Item::new("r_c_info"); + +/// interchain accounts storage in form of (port_id) -> (address, controller_connection_id) +pub const INTERCHAIN_ACCOUNTS: Map> = + Map::new("interchain_accounts"); + +pub const REPLY_ID_STORAGE: Item> = Item::new("reply_queue_id"); +pub const SUDO_PAYLOAD: Map<(String, u64), Vec> = Map::new("sudo_payload"); +pub const ERRORS_QUEUE: Map = Map::new("errors_queue"); + +pub fn save_reply_payload(store: &mut dyn Storage, payload: SudoPayload) -> StdResult<()> { + REPLY_ID_STORAGE.save(store, &to_vec(&payload)?) +} + +pub fn read_reply_payload(store: &mut dyn Storage) -> StdResult { + let data = REPLY_ID_STORAGE.load(store)?; + from_binary(&Binary(data)) +} + +pub fn add_error_to_queue(store: &mut dyn Storage, error_msg: String) -> Option<()> { + let result = ERRORS_QUEUE + .keys(store, None, None, Order::Descending) + .next() + .and_then(|data| data.ok()) + .map(|c| c + 1) + .or(Some(0)); + + result.and_then(|idx| ERRORS_QUEUE.save(store, idx, &error_msg).ok()) +} + +pub fn read_errors_from_queue(store: &dyn Storage) -> StdResult, String)>> { + ERRORS_QUEUE + .range_raw(store, None, None, Order::Ascending) + .collect() +} + +pub fn read_sudo_payload( + store: &mut dyn Storage, + channel_id: String, + seq_id: u64, +) -> StdResult { + let data = SUDO_PAYLOAD.load(store, (channel_id, seq_id))?; + from_binary(&Binary(data)) +} + +pub fn save_sudo_payload( + store: &mut dyn Storage, + channel_id: String, + seq_id: u64, + payload: SudoPayload, +) -> StdResult<()> { + SUDO_PAYLOAD.save(store, (channel_id, seq_id), &to_vec(&payload)?) +} + +pub fn clear_sudo_payload( + store: &mut dyn Storage, + channel_id: String, + seq_id: u64, +) { + SUDO_PAYLOAD.remove(store, (channel_id, seq_id)) +} \ No newline at end of file diff --git a/contracts/native-splitter/src/suite_test/mod.rs b/contracts/native-splitter/src/suite_test/mod.rs new file mode 100644 index 00000000..7b881830 --- /dev/null +++ b/contracts/native-splitter/src/suite_test/mod.rs @@ -0,0 +1,2 @@ +mod suite; +mod tests; diff --git a/contracts/native-splitter/src/suite_test/suite.rs b/contracts/native-splitter/src/suite_test/suite.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/contracts/native-splitter/src/suite_test/suite.rs @@ -0,0 +1 @@ + diff --git a/contracts/native-splitter/src/suite_test/tests.rs b/contracts/native-splitter/src/suite_test/tests.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/contracts/native-splitter/src/suite_test/tests.rs @@ -0,0 +1 @@ + From 242c212ab49e23a39fe759dce247782c9990f157 Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 16 Aug 2023 17:06:16 +0200 Subject: [PATCH 039/586] native splitter instantiate --- contracts/native-splitter/src/contract.rs | 35 ++++++++++++++++--- contracts/native-splitter/src/msg.rs | 41 +++++++++++++++++++++-- contracts/native-splitter/src/state.rs | 6 ++-- 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/contracts/native-splitter/src/contract.rs b/contracts/native-splitter/src/contract.rs index 1ca69913..7afdd53e 100644 --- a/contracts/native-splitter/src/contract.rs +++ b/contracts/native-splitter/src/contract.rs @@ -2,10 +2,10 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, - Response, StdResult, SubMsg, + Response, StdResult, SubMsg, Attribute, }; use covenant_clock::helpers::verify_clock; -use covenant_utils::neutron_ica::SudoPayload; +use covenant_utils::neutron_ica::{SudoPayload, RemoteChainInfo}; use cw2::set_contract_version; use crate::msg::{ @@ -13,7 +13,7 @@ use crate::msg::{ }; use crate::state::{ save_reply_payload, CLOCK_ADDRESS, CONTRACT_STATE, - REMOTE_CHAIN_INFO, + REMOTE_CHAIN_INFO, SPLIT_CONFIG_MAP, }; use neutron_sdk::{ bindings::{ @@ -39,7 +39,34 @@ pub fn instantiate( deps.api.debug("WASMDEBUG: instantiate"); set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - Ok(Response::default()) + let clock_addr = deps.api.addr_validate(&msg.clock_address)?; + CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; + + let remote_chain_info = RemoteChainInfo { + connection_id: msg.remote_chain_connection_id, + channel_id: msg.remote_chain_channel_id, + denom: msg.denom, + ibc_transfer_timeout: msg.ibc_transfer_timeout, + ica_timeout: msg.ica_timeout, + ibc_fee: msg.ibc_fee, + }; + REMOTE_CHAIN_INFO.save(deps.storage, &remote_chain_info)?; + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + + // validate each split and store it in a map + let mut split_resp_attributes: Vec = Vec::new(); + for split in msg.splits { + let validated_split = split.validate()?; + split_resp_attributes.push(validated_split.to_response_attribute()); + SPLIT_CONFIG_MAP.save(deps.storage, validated_split.denom, &validated_split.receivers)?; + } + + Ok(Response::default() + .add_attribute("method", "native_splitter_instantiate") + .add_attribute("clock_address", clock_addr) + .add_attributes(remote_chain_info.get_response_attributes()) + .add_attributes(split_resp_attributes) + ) } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/native-splitter/src/msg.rs b/contracts/native-splitter/src/msg.rs index 41e11b73..7aabd914 100644 --- a/contracts/native-splitter/src/msg.rs +++ b/contracts/native-splitter/src/msg.rs @@ -1,5 +1,7 @@ +use std::{fmt}; + use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Uint128, Uint64}; +use cosmwasm_std::{Addr, Uint128, Uint64, StdError, Attribute}; use covenant_macros::{covenant_deposit_address, clocked, covenant_clock_address, covenant_remote_chain}; use neutron_sdk::bindings::msg::IbcFee; use covenant_utils::neutron_ica::RemoteChainInfo; @@ -15,7 +17,7 @@ pub struct InstantiateMsg { pub denom: String, pub amount: Uint128, - pub splits: Vec, + pub splits: Vec, /// Neutron requires fees to be set to refund relayers for /// submission of ack and timeout messages. @@ -37,13 +39,35 @@ pub struct InstantiateMsg { } #[cw_serde] -pub struct SplitConfig { +pub struct DenomSplit { /// denom to be distributed pub denom: String, /// denom receivers and their respective shares pub receivers: Vec, } +impl DenomSplit { + pub fn validate(self) -> Result { + // here we validate that all receiver shares add up to 100 (%) + let sum: Uint64 = self.receivers.iter().map(|r| r.share).sum(); + + if sum != Uint64::new(100) { + Err(StdError::generic_err(format!("failed to validate split config for denom: {}", self.denom))) + } else { + Ok(self) + } + } + + pub fn to_response_attribute(&self) -> Attribute { + let mut str = "".to_string(); + + for rec in &self.receivers { + str += rec.to_string().as_str(); + } + Attribute::new(&self.denom, str) + } +} + #[cw_serde] pub struct SplitReceiver { /// address of the receiver on remote chain @@ -52,6 +76,17 @@ pub struct SplitReceiver { pub share: Uint64, } +impl fmt::Display for SplitReceiver { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + let mut str = "["; + fmt.write_str(str)?; + fmt.write_str(self.addr.as_str())?; + fmt.write_str(",")?; + fmt.write_str(self.share.to_string().as_str())?; + fmt.write_str("]")?; + Ok(()) + } +} #[clocked] #[cw_serde] pub enum ExecuteMsg {} diff --git a/contracts/native-splitter/src/state.rs b/contracts/native-splitter/src/state.rs index 8631d358..c04167cd 100644 --- a/contracts/native-splitter/src/state.rs +++ b/contracts/native-splitter/src/state.rs @@ -2,7 +2,7 @@ use cosmwasm_std::{from_binary, to_vec, Addr, Binary, Order, StdResult, Storage, use covenant_utils::neutron_ica::{SudoPayload, RemoteChainInfo}; use cw_storage_plus::{Item, Map}; -use crate::msg::ContractState; +use crate::msg::{ContractState, SplitReceiver}; /// tracks the current state of state machine pub const CONTRACT_STATE: Item = Item::new("contract_state"); @@ -12,7 +12,9 @@ pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); pub const TRANSFER_AMOUNT: Item = Item::new("transfer_amount"); -pub const SPLIT_DESTINATIONS: Item> = Item::new("split_destinations"); + +// maps a denom string to a vec of SplitReceivers +pub const SPLIT_CONFIG_MAP: Map> = Map::new("split_config"); /// information needed for an ibc transfer to the remote chain pub const REMOTE_CHAIN_INFO: Item = Item::new("r_c_info"); From c0cb4b40c1d58f4165cb2f1aa73990f200e2bbd5 Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 17 Aug 2023 11:58:56 +0200 Subject: [PATCH 040/586] native split via multisend --- contracts/native-splitter/src/contract.rs | 288 +++++++++++++++++++++- contracts/native-splitter/src/msg.rs | 16 +- packages/covenant-utils/src/lib.rs | 28 +++ 3 files changed, 318 insertions(+), 14 deletions(-) diff --git a/contracts/native-splitter/src/contract.rs b/contracts/native-splitter/src/contract.rs index 7afdd53e..088dbbee 100644 --- a/contracts/native-splitter/src/contract.rs +++ b/contracts/native-splitter/src/contract.rs @@ -1,19 +1,27 @@ +use std::ops::Div; + +use cosmos_sdk_proto::cosmos::bank::v1beta1::{MsgSend, MsgMultiSend, Input, Output, MsgMultiSendResponse}; +use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; +use cosmos_sdk_proto::ibc::applications::transfer::v1::MsgTransfer; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, - Response, StdResult, SubMsg, Attribute, + Response, StdResult, SubMsg, Attribute, StdError, Reply, Uint128, }; use covenant_clock::helpers::verify_clock; -use covenant_utils::neutron_ica::{SudoPayload, RemoteChainInfo}; +use covenant_utils::neutron_ica::{SudoPayload, RemoteChainInfo, OpenAckVersion, self}; use cw2::set_contract_version; +use neutron_sdk::bindings::msg::MsgSubmitTxResponse; +use neutron_sdk::interchain_txs::helpers::{get_port_id, decode_acknowledgement_response, decode_message_response}; +use neutron_sdk::sudo::msg::{SudoMsg, RequestPacket}; use crate::msg::{ ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, }; use crate::state::{ save_reply_payload, CLOCK_ADDRESS, CONTRACT_STATE, - REMOTE_CHAIN_INFO, SPLIT_CONFIG_MAP, + REMOTE_CHAIN_INFO, SPLIT_CONFIG_MAP, INTERCHAIN_ACCOUNTS, read_reply_payload, save_sudo_payload, TRANSFER_AMOUNT, add_error_to_queue, read_sudo_payload, }; use neutron_sdk::{ bindings::{ @@ -23,7 +31,7 @@ use neutron_sdk::{ NeutronResult, }; - +const INTERCHAIN_ACCOUNT_ID: &str = "rc-ica"; const CONTRACT_NAME: &str = "crates.io:covenant-native-splitter"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -90,12 +98,106 @@ fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> NeutronResult Ok(Response::default()), - ContractState::IcaCreated => Ok(Response::default()), + ContractState::Instantiated => try_register_ica(deps, env), + ContractState::IcaCreated => try_split_funds(deps, env), ContractState::Completed => Ok(Response::default()), } } +fn try_register_ica(deps: DepsMut, env: Env) -> NeutronResult> { + let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; + let register: NeutronMsg = + NeutronMsg::register_interchain_account(remote_chain_info.connection_id, INTERCHAIN_ACCOUNT_ID.to_string()); + let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); + + // we are saving empty data here because we handle response of registering ICA in sudo_open_ack method + INTERCHAIN_ACCOUNTS.save(deps.storage, key, &None)?; + + Ok(Response::new() + .add_attribute("method", "try_register_ica") + .add_message(register)) +} + +fn try_split_funds(deps: DepsMut, env: Env) -> NeutronResult> { + + let port_id = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); + let interchain_account = INTERCHAIN_ACCOUNTS.load(deps.storage, port_id.clone())?; + + match interchain_account { + Some((address, controller_conn_id)) => { + let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; + let amount = TRANSFER_AMOUNT.load(deps.storage)?; + let splits = SPLIT_CONFIG_MAP.load( + deps.storage, + remote_chain_info.denom.to_string() + )?; + + // map the splits into multi send Outputs + let outputs = splits.iter() + .map(|s| Output { + address: s.addr.to_string(), + coins: vec![Coin { + denom: remote_chain_info.denom.to_string(), + amount: amount.div(s.share).to_string(), // make this safe + }], + }) + .collect(); + + // todo: make sure output amounts add up to the input amount here + let multi_send_msg = MsgMultiSend { + inputs: vec![ + Input { + address, + coins: vec![ + Coin { + denom: remote_chain_info.denom, + amount: amount.to_string(), + }, + ] + }, + ], + outputs, + }; + + let protobuf = neutron_ica::to_proto_msg_multi_send(multi_send_msg)?; + + // wrap the protobuf of MsgTransfer into a message to be executed + // by our interchain account + let submit_msg = NeutronMsg::submit_tx( + controller_conn_id, + INTERCHAIN_ACCOUNT_ID.to_string(), + vec![protobuf], + "".to_string(), + remote_chain_info.ica_timeout.u64(), + remote_chain_info.ibc_fee, + ); + + let sudo_msg = msg_with_sudo_callback( + deps, + submit_msg, + SudoPayload { + port_id, + message: "split_funds_msg".to_string(), + }, + )?; + Ok(Response::default() + .add_submessage(sudo_msg) + .add_attribute("method", "try_execute_split_funds") + ) + } + None => { + // I can't think of a case of how we could end up here as `sudo_open_ack` + // callback advances the state to `ICACreated` and stores the ICA. + // just in case, we revert the state to `Instantiated` to restart the flow. + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + Ok(Response::default() + .add_attribute("method", "try_execute_split_funds") + .add_attribute("error", "no_ica_found") + ) + }, + } +} + #[allow(unused)] fn msg_with_sudo_callback>, T>( deps: DepsMut, @@ -117,3 +219,177 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> NeutronResul QueryMsg::RemoteChainInfo {} => Ok(to_binary(&REMOTE_CHAIN_INFO.may_load(deps.storage)?)?), } } + + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo: received sudo msg: {msg:?}").as_str()); + + match msg { + // For handling successful (non-error) acknowledgements. + SudoMsg::Response { request, data } => sudo_response(deps, request, data), + + // For handling error acknowledgements. + SudoMsg::Error { request, details } => sudo_error(deps, request, details), + + // For handling error timeouts. + SudoMsg::Timeout { request } => sudo_timeout(deps, env, request), + + // For handling successful registering of ICA + SudoMsg::OpenAck { + port_id, + channel_id, + counterparty_channel_id, + counterparty_version, + } => sudo_open_ack( + deps, + env, + port_id, + channel_id, + counterparty_channel_id, + counterparty_version, + ), + _ => Ok(Response::default()), + } +} + +// handler +fn sudo_open_ack( + deps: DepsMut, + _env: Env, + port_id: String, + _channel_id: String, + _counterparty_channel_id: String, + counterparty_version: String, +) -> StdResult { + // The version variable contains a JSON value with multiple fields, + // including the generated account address. + let parsed_version: Result = + serde_json_wasm::from_str(counterparty_version.as_str()); + + // get the parsed OpenAckVersion or return an error if we fail + let Ok(parsed_version) = parsed_version else { + return Err(StdError::generic_err("Can't parse counterparty_version")) + }; + + // Update the storage record associated with the interchain account. + INTERCHAIN_ACCOUNTS.save( + deps.storage, + port_id, + &Some(( + parsed_version.clone().address, + parsed_version.controller_connection_id, + )), + )?; + CONTRACT_STATE.save(deps.storage, &ContractState::IcaCreated)?; + + Ok(Response::default() + .add_attribute("method", "sudo_open_ack") + ) +} + +fn sudo_response(deps: DepsMut, request: RequestPacket, data: Binary) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo_response: sudo received: {request:?} {data:?}",).as_str()); + + let seq_id = request + .sequence + .ok_or_else(|| StdError::generic_err("sequence not found"))?; + + let channel_id = request + .source_channel + .ok_or_else(|| StdError::generic_err("channel_id not found"))?; + + let payload = read_sudo_payload(deps.storage, channel_id, seq_id).ok(); + if payload.is_none() { + let error_msg = "WASMDEBUG: Error: Unable to read sudo payload"; + deps.api.debug(error_msg); + add_error_to_queue(deps.storage, error_msg.to_string()); + return Ok(Response::default()); + } + + let parsed_data = decode_acknowledgement_response(data)?; + + // Iterate over the messages, parse them depending on their type & process them. + let mut item_types = vec![]; + for item in parsed_data { + let item_type = item.msg_type.as_str(); + item_types.push(item_type.to_string()); + match item_type { + "/cosmos.bank.v1beta1.MsgMultiSend" => { + + let out: MsgMultiSendResponse = decode_message_response(&item.data)?; + // TODO: look into if this successful decoding is enough to assume multi + // send was successful + CONTRACT_STATE.save(deps.storage, &ContractState::Completed)?; + } + _ => { + deps.api.debug( + format!( + "This type of acknowledgement is not implemented: {:?}", + payload + ) + .as_str(), + ); + } + } + } + + Ok(Response::default().add_attribute("method", "sudo_response")) +} + +fn sudo_timeout(deps: DepsMut, _env: Env, request: RequestPacket) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo timeout request: {request:?}").as_str()); + + // revert the state to Instantiated to force re-creation of ICA + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + + // returning Ok as this is anticipated. channel is already closed. + Ok(Response::default()) +} + +fn sudo_error(deps: DepsMut, request: RequestPacket, details: String) -> StdResult { + deps.api + .debug(format!("WASMDEBUG: sudo error: {details}").as_str()); + deps.api + .debug(format!("WASMDEBUG: request packet: {request:?}").as_str()); + + // either of these errors will close the channel + request + .sequence + .ok_or_else(|| StdError::generic_err("sequence not found"))?; + + request + .source_channel + .ok_or_else(|| StdError::generic_err("channel_id not found"))?; + + Ok(Response::default().add_attribute("method", "sudo_error")) +} + +// prepare_sudo_payload is called from reply handler +// The method is used to extract sequence id and channel from SubmitTxResponse to +// process sudo payload defined in msg_with_sudo_callback later in Sudo handler. +// Such flow msg_with_sudo_callback() -> reply() -> prepare_sudo_payload() -> sudo() +// allows you "attach" some payload to your SubmitTx message +// and process this payload when an acknowledgement for the SubmitTx message +// is received in Sudo handler +fn prepare_sudo_payload(mut deps: DepsMut, _env: Env, msg: Reply) -> StdResult { + let payload = read_reply_payload(deps.storage)?; + let resp: MsgSubmitTxResponse = serde_json_wasm::from_slice( + msg.result + .into_result() + .map_err(StdError::generic_err)? + .data + .ok_or_else(|| StdError::generic_err("no result"))? + .as_slice(), + ) + .map_err(|e| StdError::generic_err(format!("failed to parse response: {e:?}")))?; + deps.api + .debug(format!("WASMDEBUG: reply msg: {resp:?}").as_str()); + let seq_id = resp.sequence_id; + let channel_id = resp.channel; + save_sudo_payload(deps.branch().storage, channel_id, seq_id, payload)?; + Ok(Response::new()) +} diff --git a/contracts/native-splitter/src/msg.rs b/contracts/native-splitter/src/msg.rs index 7aabd914..1a89e053 100644 --- a/contracts/native-splitter/src/msg.rs +++ b/contracts/native-splitter/src/msg.rs @@ -17,7 +17,7 @@ pub struct InstantiateMsg { pub denom: String, pub amount: Uint128, - pub splits: Vec, + pub splits: Vec, /// Neutron requires fees to be set to refund relayers for /// submission of ack and timeout messages. @@ -39,19 +39,19 @@ pub struct InstantiateMsg { } #[cw_serde] -pub struct DenomSplit { +pub struct NativeDenomSplit { /// denom to be distributed pub denom: String, /// denom receivers and their respective shares pub receivers: Vec, } -impl DenomSplit { - pub fn validate(self) -> Result { +impl NativeDenomSplit { + pub fn validate(self) -> Result { // here we validate that all receiver shares add up to 100 (%) - let sum: Uint64 = self.receivers.iter().map(|r| r.share).sum(); + let sum: Uint128 = self.receivers.iter().map(|r| r.share).sum(); - if sum != Uint64::new(100) { + if sum != Uint128::new(100) { Err(StdError::generic_err(format!("failed to validate split config for denom: {}", self.denom))) } else { Ok(self) @@ -71,9 +71,9 @@ impl DenomSplit { #[cw_serde] pub struct SplitReceiver { /// address of the receiver on remote chain - pub addr: Addr, + pub addr: String, /// percentage share that the address is entitled to - pub share: Uint64, + pub share: Uint128, } impl fmt::Display for SplitReceiver { diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index c6c28418..f7e5095a 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -116,4 +116,32 @@ pub mod neutron_ica { value: Binary::from(buf), }) } + + pub fn to_proto_msg_send(msg: impl Message) -> NeutronResult { + // Serialize the Send message + let mut buf = Vec::new(); + buf.reserve(msg.encoded_len()); + if let Err(e) = msg.encode(&mut buf) { + return Err(StdError::generic_err(format!("Encode error: {e}")).into()); + } + + Ok(ProtobufAny { + type_url: "/cosmos.bank.v1beta1.MsgSend".to_string(), + value: Binary::from(buf), + }) + } + + pub fn to_proto_msg_multi_send(msg: impl Message) -> NeutronResult { + // Serialize the Send message + let mut buf = Vec::new(); + buf.reserve(msg.encoded_len()); + if let Err(e) = msg.encode(&mut buf) { + return Err(StdError::generic_err(format!("Encode error: {e}")).into()); + } + + Ok(ProtobufAny { + type_url: "/cosmos.bank.v1beta1.MsgMultiSend".to_string(), + value: Binary::from(buf), + }) + } } From 14fe21e1cdeba9e88cc838fbba162b43ea304032 Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 21 Aug 2023 14:43:12 +0200 Subject: [PATCH 041/586] native splitter queries --- contracts/native-splitter/src/contract.rs | 92 ++++++++++++++++++----- contracts/native-splitter/src/msg.rs | 19 ++++- 2 files changed, 88 insertions(+), 23 deletions(-) diff --git a/contracts/native-splitter/src/contract.rs b/contracts/native-splitter/src/contract.rs index 088dbbee..d0c60e67 100644 --- a/contracts/native-splitter/src/contract.rs +++ b/contracts/native-splitter/src/contract.rs @@ -1,23 +1,21 @@ -use std::ops::Div; - -use cosmos_sdk_proto::cosmos::bank::v1beta1::{MsgSend, MsgMultiSend, Input, Output, MsgMultiSendResponse}; +use cosmos_sdk_proto::cosmos::bank::v1beta1::{MsgMultiSend, Input, Output, MsgMultiSendResponse}; use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; -use cosmos_sdk_proto::ibc::applications::transfer::v1::MsgTransfer; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, - Response, StdResult, SubMsg, Attribute, StdError, Reply, Uint128, + Response, StdResult, SubMsg, Attribute, StdError, Reply, CustomQuery, Fraction, Uint128, Decimal, }; use covenant_clock::helpers::verify_clock; use covenant_utils::neutron_ica::{SudoPayload, RemoteChainInfo, OpenAckVersion, self}; use cw2::set_contract_version; +use neutron_sdk::NeutronError; use neutron_sdk::bindings::msg::MsgSubmitTxResponse; use neutron_sdk::interchain_txs::helpers::{get_port_id, decode_acknowledgement_response, decode_message_response}; use neutron_sdk::sudo::msg::{SudoMsg, RequestPacket}; use crate::msg::{ - ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, + ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, SplitReceiver, }; use crate::state::{ save_reply_payload, CLOCK_ADDRESS, CONTRACT_STATE, @@ -100,7 +98,9 @@ fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> NeutronResult try_register_ica(deps, env), ContractState::IcaCreated => try_split_funds(deps, env), - ContractState::Completed => Ok(Response::default()), + ContractState::Completed => Ok(Response::default() + .add_attribute("contract_state", "completed") + ), } } @@ -132,16 +132,25 @@ fn try_split_funds(deps: DepsMut, env: Env) -> NeutronResult = Vec::new(); + for split_receiver in splits.iter() { + // get the fraction dedicated to this receiver + let amt = amount + .checked_multiply_ratio(split_receiver.share, Uint128::new(100)); + + match amt { + Ok(amount) => outputs.push(Output { + address: split_receiver.addr.to_string(), + coins: vec![Coin { + denom: remote_chain_info.denom.to_string(), + amount: amount.to_string(), + }], + }), + Err(e) => return Err( + NeutronError::Std(StdError::GenericErr { msg: e.to_string() }) + ), + }; + } // todo: make sure output amounts add up to the input amount here let multi_send_msg = MsgMultiSend { @@ -174,7 +183,7 @@ fn try_split_funds(deps: DepsMut, env: Env) -> NeutronResult>, T>( } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> NeutronResult { +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult { match msg { QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.may_load(deps.storage)?)?), QueryMsg::DepositAddress {} => { - Ok(to_binary(&Some(1))?) + let ica = query_deposit_address(deps, env)?; + // up to the querying module to make sense of the response + Ok(to_binary(&ica)?) }, QueryMsg::RemoteChainInfo {} => Ok(to_binary(&REMOTE_CHAIN_INFO.may_load(deps.storage)?)?), + QueryMsg::SplitConfig {} => { + let mut vec: Vec<(String, Vec)> = Vec::new(); + + for entry in SPLIT_CONFIG_MAP.range( + deps.storage, + None, + None, + cosmwasm_std::Order::Ascending + ) { vec.push(entry?) } + + Ok(to_binary(&vec)?) + }, + QueryMsg::TransferAmount {} => Ok(to_binary(&TRANSFER_AMOUNT.may_load(deps.storage)?)?), + QueryMsg::IcaAddress {} => Ok(to_binary(&get_ica(deps, &env, INTERCHAIN_ACCOUNT_ID)?.0)?), } } +fn query_deposit_address(deps: Deps, env: Env) -> Result, StdError> { + let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); + /* + here we cover three possible cases: + - 1. ICA had been created -> nice + - 2. ICA creation request had been submitted but did not receive + the channel_open_ack yet -> None + - 3. ICA creation request hadn't been submitted yet -> None + */ + match INTERCHAIN_ACCOUNTS.may_load(deps.storage, key)? { + Some(Some((addr, _))) => Ok(Some(addr)), // case 1 + _ => Ok(None), // cases 2 and 3 + } +} + +fn get_ica( + deps: Deps, + env: &Env, + interchain_account_id: &str, +) -> Result<(String, String), StdError> { + let key = get_port_id(env.contract.address.as_str(), interchain_account_id); + + INTERCHAIN_ACCOUNTS + .load(deps.storage, key)? + .ok_or_else(|| StdError::generic_err("Interchain account is not created yet")) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> StdResult { diff --git a/contracts/native-splitter/src/msg.rs b/contracts/native-splitter/src/msg.rs index 1a89e053..02ba0e7b 100644 --- a/contracts/native-splitter/src/msg.rs +++ b/contracts/native-splitter/src/msg.rs @@ -1,10 +1,12 @@ use std::{fmt}; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Uint128, Uint64, StdError, Attribute}; -use covenant_macros::{covenant_deposit_address, clocked, covenant_clock_address, covenant_remote_chain}; +use cosmwasm_std::{Addr, Uint128, Uint64, StdError, Attribute, Fraction}; +use covenant_macros::{covenant_deposit_address, clocked, covenant_clock_address, covenant_remote_chain, covenant_ica_address}; + use neutron_sdk::bindings::msg::IbcFee; use covenant_utils::neutron_ica::RemoteChainInfo; +use schemars::Map; #[cw_serde] pub struct InstantiateMsg { @@ -73,12 +75,13 @@ pub struct SplitReceiver { /// address of the receiver on remote chain pub addr: String, /// percentage share that the address is entitled to + // TODO: convert to cw Fraction pub share: Uint128, } impl fmt::Display for SplitReceiver { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - let mut str = "["; + let str = "["; fmt.write_str(str)?; fmt.write_str(self.addr.as_str())?; fmt.write_str(",")?; @@ -91,14 +94,24 @@ impl fmt::Display for SplitReceiver { #[cw_serde] pub enum ExecuteMsg {} +#[cw_serde] +pub struct SplitConfigMap { + pub map: Map>, +} + #[covenant_clock_address] #[covenant_remote_chain] #[covenant_deposit_address] +#[covenant_ica_address] #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { #[returns(ContractState)] ContractState {}, + #[returns(Vec<(String, Vec)>)] + SplitConfig {}, + #[returns(Uint128)] + TransferAmount {}, } #[cw_serde] From 47e0bf99f131f54ff053a071652b9315ecc1bf50 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 5 Sep 2023 15:04:57 +0200 Subject: [PATCH 042/586] map_err instead of match; validating splits on instantiation for repeated entries with same denom --- contracts/native-splitter/src/contract.rs | 33 ++++++++++++++--------- contracts/native-splitter/src/msg.rs | 5 ++-- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/contracts/native-splitter/src/contract.rs b/contracts/native-splitter/src/contract.rs index d0c60e67..52615493 100644 --- a/contracts/native-splitter/src/contract.rs +++ b/contracts/native-splitter/src/contract.rs @@ -1,10 +1,12 @@ +use std::collections::HashSet; + use cosmos_sdk_proto::cosmos::bank::v1beta1::{MsgMultiSend, Input, Output, MsgMultiSendResponse}; use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, - Response, StdResult, SubMsg, Attribute, StdError, Reply, CustomQuery, Fraction, Uint128, Decimal, + Response, StdResult, SubMsg, Attribute, StdError, Reply, CustomQuery, Uint128, }; use covenant_clock::helpers::verify_clock; use covenant_utils::neutron_ica::{SudoPayload, RemoteChainInfo, OpenAckVersion, self}; @@ -61,10 +63,19 @@ pub fn instantiate( // validate each split and store it in a map let mut split_resp_attributes: Vec = Vec::new(); + let mut encountered_denoms: HashSet = HashSet::new(); + for split in msg.splits { - let validated_split = split.validate()?; - split_resp_attributes.push(validated_split.to_response_attribute()); - SPLIT_CONFIG_MAP.save(deps.storage, validated_split.denom, &validated_split.receivers)?; + // if denom had not yet been encountered we proceed, otherwise error + if encountered_denoms.insert(split.denom.to_string()) { + let validated_split = split.validate()?; + split_resp_attributes.push(validated_split.to_response_attribute()); + SPLIT_CONFIG_MAP.save(deps.storage, validated_split.denom, &validated_split.receivers)?; + } else { + return Err(NeutronError::Std(StdError::GenericErr { msg: + format!("multiple {:?} entries", split.denom) + })) + } } Ok(Response::default() @@ -136,20 +147,16 @@ fn try_split_funds(deps: DepsMut, env: Env) -> NeutronResult outputs.push(Output { + outputs.push(Output { address: split_receiver.addr.to_string(), coins: vec![Coin { denom: remote_chain_info.denom.to_string(), - amount: amount.to_string(), + amount: amt.to_string(), }], - }), - Err(e) => return Err( - NeutronError::Std(StdError::GenericErr { msg: e.to_string() }) - ), - }; + }); } // todo: make sure output amounts add up to the input amount here diff --git a/contracts/native-splitter/src/msg.rs b/contracts/native-splitter/src/msg.rs index 02ba0e7b..e3abb65c 100644 --- a/contracts/native-splitter/src/msg.rs +++ b/contracts/native-splitter/src/msg.rs @@ -1,7 +1,7 @@ -use std::{fmt}; +use std::fmt; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Uint128, Uint64, StdError, Attribute, Fraction}; +use cosmwasm_std::{Addr, Uint128, Uint64, StdError, Attribute}; use covenant_macros::{covenant_deposit_address, clocked, covenant_clock_address, covenant_remote_chain, covenant_ica_address}; use neutron_sdk::bindings::msg::IbcFee; @@ -96,6 +96,7 @@ pub enum ExecuteMsg {} #[cw_serde] pub struct SplitConfigMap { + /// maps denom to its associated receivers with their shares pub map: Map>, } From a97589d026dc711c9f1be4cab33f48581c03e048 Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 6 Sep 2023 18:59:31 +0200 Subject: [PATCH 043/586] interchain router --- contracts/interchain-router/.cargo/config | 3 + contracts/interchain-router/Cargo.toml | 49 ++++++++++++++ contracts/interchain-router/README.md | 7 ++ contracts/interchain-router/src/contract.rs | 75 +++++++++++++++++++++ contracts/interchain-router/src/error.rs | 11 +++ contracts/interchain-router/src/lib.rs | 8 +++ contracts/interchain-router/src/msg.rs | 68 +++++++++++++++++++ contracts/interchain-router/src/state.rs | 7 ++ 8 files changed, 228 insertions(+) create mode 100644 contracts/interchain-router/.cargo/config create mode 100644 contracts/interchain-router/Cargo.toml create mode 100644 contracts/interchain-router/README.md create mode 100644 contracts/interchain-router/src/contract.rs create mode 100644 contracts/interchain-router/src/error.rs create mode 100644 contracts/interchain-router/src/lib.rs create mode 100644 contracts/interchain-router/src/msg.rs create mode 100644 contracts/interchain-router/src/state.rs diff --git a/contracts/interchain-router/.cargo/config b/contracts/interchain-router/.cargo/config new file mode 100644 index 00000000..5f6aa466 --- /dev/null +++ b/contracts/interchain-router/.cargo/config @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +schema = "run --bin schema" diff --git a/contracts/interchain-router/Cargo.toml b/contracts/interchain-router/Cargo.toml new file mode 100644 index 00000000..5d500f15 --- /dev/null +++ b/contracts/interchain-router/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "covenant-interchain-router" +edition = { workspace = true } +authors = ["benskey bekauz@protonmail.com"] +description = "Interchain router contract for covenants" +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +exclude = [ + "contract.wasm", + "hash.txt", +] + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +covenant-macros = { workspace = true} +covenant-ls = { workspace = true, features=["library"] } +covenant-clock = { workspace = true, features=["library"]} +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +cw2 = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +sha2 = { workspace = true } +neutron-sdk = { workspace = true } +cosmos-sdk-proto = { workspace = true } +protobuf = { workspace = true } +schemars = { workspace = true } +serde-json-wasm = { workspace = true } +base64 = { workspace = true } +prost = { workspace = true } +prost-types = { workspace = true } +bech32 = { workspace = true } +covenant-utils = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +anyhow = { workspace = true } diff --git a/contracts/interchain-router/README.md b/contracts/interchain-router/README.md new file mode 100644 index 00000000..ef175c4e --- /dev/null +++ b/contracts/interchain-router/README.md @@ -0,0 +1,7 @@ +# Interchain Router + +Interchain Router is a contract that facilitates a predetermined routing of funds. +Each instance of the interchain router is associated with a single receiver. + +The router continuously attempts to perform IBC transfers to the receiver. +In case the IBC transfer fails, the funds will be refunded, and we can safely try again. diff --git a/contracts/interchain-router/src/contract.rs b/contracts/interchain-router/src/contract.rs new file mode 100644 index 00000000..c1b428f9 --- /dev/null +++ b/contracts/interchain-router/src/contract.rs @@ -0,0 +1,75 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{Env, MessageInfo, Response, DepsMut, Attribute}; +use cw2::set_contract_version; + +use crate::{msg::{InstantiateMsg, ExecuteMsg, DestinationConfig}, state::{CLOCK_ADDRESS, DESTINATION_CONFIG}, error::ContractError}; + + +const CONTRACT_NAME: &str = "crates.io:covenant-interchain-router"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + deps.api.debug("WASMDEBUG: instantiate"); + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let clock_addr = deps.api.addr_validate(&msg.clock_address)?; + let destination_receiver_addr = deps.api.addr_validate(&msg.destination_receiver_addr)?; + + CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; + DESTINATION_CONFIG.save(deps.storage, &DestinationConfig { + destination_chain_channel_id: msg.destination_chain_channel_id.to_string(), + destination_receiver_addr, + ibc_transfer_timeout: msg.ibc_transfer_timeout, + })?; + + Ok(Response::default() + .add_attribute("method", "interchain_router_instantiate") + .add_attributes(msg.get_response_attributes()) + ) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + deps.api + .debug(format!("WASMDEBUG: execute: received msg: {msg:?}").as_str()); + + // Verify caller is the clock + if info.sender != CLOCK_ADDRESS.load(deps.storage)? { + return Err(ContractError::Unauthorized {}) + } + + match msg { + ExecuteMsg::Tick {} => try_route_balances(deps, env), + } +} + +fn try_route_balances(deps: DepsMut, env: Env) -> Result { + + let balances = deps.querier.query_all_balances(env.contract.address)?; + let destination_config: DestinationConfig = DESTINATION_CONFIG.load(deps.storage)?; + + let balance_attributes: Vec = balances.iter() + .map(|c| Attribute::new(c.denom.to_string(), c.amount)) + .collect(); + + let messages = destination_config.get_ibc_transfer_messages_for_coins(balances, env.block.time); + + Ok(Response::default() + .add_attribute("method", "try_route_balances") + .add_attributes(balance_attributes) + .add_messages(messages) + ) +} \ No newline at end of file diff --git a/contracts/interchain-router/src/error.rs b/contracts/interchain-router/src/error.rs new file mode 100644 index 00000000..dc19f103 --- /dev/null +++ b/contracts/interchain-router/src/error.rs @@ -0,0 +1,11 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, +} diff --git a/contracts/interchain-router/src/lib.rs b/contracts/interchain-router/src/lib.rs new file mode 100644 index 00000000..0faea8f4 --- /dev/null +++ b/contracts/interchain-router/src/lib.rs @@ -0,0 +1,8 @@ +#![warn(clippy::unwrap_used, clippy::expect_used)] + +extern crate core; + +pub mod contract; +pub mod error; +pub mod msg; +pub mod state; diff --git a/contracts/interchain-router/src/msg.rs b/contracts/interchain-router/src/msg.rs new file mode 100644 index 00000000..fe857bb0 --- /dev/null +++ b/contracts/interchain-router/src/msg.rs @@ -0,0 +1,68 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Uint64, Attribute, Addr, Coin, CosmosMsg, IbcMsg, IbcTimeout, Timestamp, BlockInfo}; +use covenant_macros::{clocked, covenant_clock_address}; + +#[cw_serde] +pub struct InstantiateMsg { + /// address for the clock. this contract verifies + /// that only the clock can execute ticks + pub clock_address: String, + /// channel id of the destination chain + pub destination_chain_channel_id: String, + /// address of the receiver on destination chain + pub destination_receiver_addr: String, + /// timeout in seconds + pub ibc_transfer_timeout: Uint64, +} + +impl InstantiateMsg { + pub fn get_response_attributes(&self) -> Vec { + vec![ + Attribute::new("clock_address", &self.clock_address), + Attribute::new("destination_chain_channel_id", &self.destination_chain_channel_id), + Attribute::new("destination_receiver_addr", &self.destination_receiver_addr), + Attribute::new("ibc_transfer_timeout", self.ibc_transfer_timeout.to_string()), + ] + } +} + +#[cw_serde] +pub struct DestinationConfig { + /// channel id of the destination chain + pub destination_chain_channel_id: String, + /// address of the receiver on destination chain + pub destination_receiver_addr: Addr, + /// timeout in seconds + pub ibc_transfer_timeout: Uint64, +} + +impl DestinationConfig { + pub fn get_ibc_transfer_messages_for_coins(&self, coins: Vec, current_timestamp: Timestamp) -> Vec { + let mut messages: Vec = vec![]; + + for coin in coins { + let msg: IbcMsg = IbcMsg::Transfer { + channel_id: self.destination_chain_channel_id.to_string(), + to_address: self.destination_receiver_addr.to_string(), + amount: coin, + timeout: IbcTimeout::with_timestamp(current_timestamp.plus_seconds(self.ibc_transfer_timeout.u64())), + }; + + messages.push(CosmosMsg::Ibc(msg)); + } + + messages + } +} + +#[clocked] +#[cw_serde] +pub enum ExecuteMsg {} + +#[covenant_clock_address] +#[derive(QueryResponses)] +#[cw_serde] +pub enum QueryMsg { + #[returns(DestinationConfig)] + DestinationConfig {}, +} diff --git a/contracts/interchain-router/src/state.rs b/contracts/interchain-router/src/state.rs new file mode 100644 index 00000000..f25832ce --- /dev/null +++ b/contracts/interchain-router/src/state.rs @@ -0,0 +1,7 @@ +use cosmwasm_std::Addr; +use cw_storage_plus::Item; + +use crate::msg::DestinationConfig; + +pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); +pub const DESTINATION_CONFIG: Item = Item::new("destination_config"); \ No newline at end of file From bcfd962057810833169015d4b9967f02e673c787 Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 6 Sep 2023 20:08:00 +0200 Subject: [PATCH 044/586] queries, migration --- Cargo.lock | 29 ++++++++ contracts/interchain-router/src/contract.rs | 80 ++++++++++++++++++--- contracts/interchain-router/src/msg.rs | 33 +++++---- 3 files changed, 119 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c2b52be..37465e2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -453,6 +453,35 @@ dependencies = [ "thiserror", ] +[[package]] +name = "covenant-interchain-router" +version = "1.0.0" +dependencies = [ + "anyhow", + "base64 0.13.1", + "bech32", + "cosmos-sdk-proto 0.14.0", + "cosmwasm-schema", + "cosmwasm-std", + "covenant-clock", + "covenant-ls", + "covenant-macros", + "covenant-utils", + "cw-multi-test", + "cw-storage-plus 1.1.0", + "cw-utils 1.0.1", + "cw2 1.1.0", + "neutron-sdk", + "prost 0.11.9", + "prost-types", + "protobuf 3.2.0", + "schemars", + "serde", + "serde-json-wasm 0.4.1", + "sha2 0.10.7", + "thiserror", +] + [[package]] name = "covenant-lp" version = "1.0.0" diff --git a/contracts/interchain-router/src/contract.rs b/contracts/interchain-router/src/contract.rs index c1b428f9..152ac7b6 100644 --- a/contracts/interchain-router/src/contract.rs +++ b/contracts/interchain-router/src/contract.rs @@ -1,9 +1,9 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{Env, MessageInfo, Response, DepsMut, Attribute}; +use cosmwasm_std::{Env, MessageInfo, Response, DepsMut, Attribute, Deps, StdResult, Binary, to_binary}; use cw2::set_contract_version; -use crate::{msg::{InstantiateMsg, ExecuteMsg, DestinationConfig}, state::{CLOCK_ADDRESS, DESTINATION_CONFIG}, error::ContractError}; +use crate::{msg::{InstantiateMsg, ExecuteMsg, DestinationConfig, QueryMsg, MigrateMsg}, state::{CLOCK_ADDRESS, DESTINATION_CONFIG}, error::ContractError}; const CONTRACT_NAME: &str = "crates.io:covenant-interchain-router"; @@ -24,15 +24,18 @@ pub fn instantiate( let destination_receiver_addr = deps.api.addr_validate(&msg.destination_receiver_addr)?; CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; - DESTINATION_CONFIG.save(deps.storage, &DestinationConfig { + let destination_config = DestinationConfig { destination_chain_channel_id: msg.destination_chain_channel_id.to_string(), destination_receiver_addr, ibc_transfer_timeout: msg.ibc_transfer_timeout, - })?; + }; + + DESTINATION_CONFIG.save(deps.storage, &destination_config)?; Ok(Response::default() .add_attribute("method", "interchain_router_instantiate") - .add_attributes(msg.get_response_attributes()) + .add_attribute("clock_address", clock_addr) + .add_attributes(destination_config.get_response_attributes()) ) } @@ -56,15 +59,27 @@ pub fn execute( } } +/// method that attempts to transfer out all available balances to the receiver fn try_route_balances(deps: DepsMut, env: Env) -> Result { - - let balances = deps.querier.query_all_balances(env.contract.address)?; let destination_config: DestinationConfig = DESTINATION_CONFIG.load(deps.storage)?; - let balance_attributes: Vec = balances.iter() - .map(|c| Attribute::new(c.denom.to_string(), c.amount)) - .collect(); + // first we query all balances of the router + let balances = deps.querier.query_all_balances(env.contract.address)?; + // if there are no balances, we return early; + // otherwise build up the response attributes + let balance_attributes: Vec = if balances.len() == 0 { + return Ok(Response::default() + .add_attribute("method", "try_route_balances") + .add_attribute("balances", "[]") + ) + } else { + balances.iter() + .map(|c| Attribute::new(c.denom.to_string(), c.amount)) + .collect() + }; + + // get ibc transfer messages for each denom let messages = destination_config.get_ibc_transfer_messages_for_coins(balances, env.block.time); Ok(Response::default() @@ -72,4 +87,47 @@ fn try_route_balances(deps: DepsMut, env: Env) -> Result StdResult { + match msg { + QueryMsg::DestinationConfig {} => Ok(to_binary(&DESTINATION_CONFIG.may_load(deps.storage)?)?), + QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), + } +} + + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { + deps.api.debug("WASMDEBUG: migrate"); + + match msg { + MigrateMsg::UpdateConfig { + clock_addr, + destination_config, + } => { + let mut response = Response::default().add_attribute("method", "update_interchain_router"); + + if let Some(addr) = clock_addr { + CLOCK_ADDRESS.save(deps.storage, &deps.api.addr_validate(&addr)?)?; + response = response.add_attribute("clock_addr", addr); + } + + if let Some(config) = destination_config { + DESTINATION_CONFIG.save(deps.storage, &config)?; + response = response.add_attributes(config.get_response_attributes()); + } + + Ok(response) + } + MigrateMsg::UpdateCodeId { data: _ } => { + // This is a migrate message to update code id, + // Data is optional base64 that we can parse to any data we would like in the future + // let data: SomeStruct = from_binary(&data)?; + Ok(Response::default().add_attribute("method", "update_interchain_router")) + } + } +} + + diff --git a/contracts/interchain-router/src/msg.rs b/contracts/interchain-router/src/msg.rs index fe857bb0..a2fe778f 100644 --- a/contracts/interchain-router/src/msg.rs +++ b/contracts/interchain-router/src/msg.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Uint64, Attribute, Addr, Coin, CosmosMsg, IbcMsg, IbcTimeout, Timestamp, BlockInfo}; +use cosmwasm_std::{Uint64, Attribute, Addr, Coin, CosmosMsg, IbcMsg, IbcTimeout, Timestamp, Binary}; use covenant_macros::{clocked, covenant_clock_address}; #[cw_serde] @@ -15,17 +15,6 @@ pub struct InstantiateMsg { pub ibc_transfer_timeout: Uint64, } -impl InstantiateMsg { - pub fn get_response_attributes(&self) -> Vec { - vec![ - Attribute::new("clock_address", &self.clock_address), - Attribute::new("destination_chain_channel_id", &self.destination_chain_channel_id), - Attribute::new("destination_receiver_addr", &self.destination_receiver_addr), - Attribute::new("ibc_transfer_timeout", self.ibc_transfer_timeout.to_string()), - ] - } -} - #[cw_serde] pub struct DestinationConfig { /// channel id of the destination chain @@ -53,6 +42,14 @@ impl DestinationConfig { messages } + + pub fn get_response_attributes(&self) -> Vec { + vec![ + Attribute::new("destination_chain_channel_id", self.destination_chain_channel_id.to_string()), + Attribute::new("destination_receiver_addr", self.destination_receiver_addr.to_string()), + Attribute::new("ibc_transfer_timeout", self.ibc_transfer_timeout), + ] + } } #[clocked] @@ -66,3 +63,15 @@ pub enum QueryMsg { #[returns(DestinationConfig)] DestinationConfig {}, } + + +#[cw_serde] +pub enum MigrateMsg { + UpdateConfig { + clock_addr: Option, + destination_config: Option, + }, + UpdateCodeId { + data: Option, + }, +} From 15fee53a541637a63f49f3d36c3eef13c7df379b Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 6 Sep 2023 23:12:06 +0200 Subject: [PATCH 045/586] unit tests --- contracts/interchain-router/src/contract.rs | 40 ++--- contracts/interchain-router/src/lib.rs | 4 + contracts/interchain-router/src/msg.rs | 39 +++-- contracts/interchain-router/src/state.rs | 2 +- .../interchain-router/src/suite_tests/mod.rs | 2 + .../src/suite_tests/suite.rs | 146 ++++++++++++++++++ .../src/suite_tests/tests.rs | 131 ++++++++++++++++ 7 files changed, 331 insertions(+), 33 deletions(-) create mode 100644 contracts/interchain-router/src/suite_tests/mod.rs create mode 100644 contracts/interchain-router/src/suite_tests/suite.rs create mode 100644 contracts/interchain-router/src/suite_tests/tests.rs diff --git a/contracts/interchain-router/src/contract.rs b/contracts/interchain-router/src/contract.rs index 152ac7b6..9d8e498b 100644 --- a/contracts/interchain-router/src/contract.rs +++ b/contracts/interchain-router/src/contract.rs @@ -1,15 +1,19 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{Env, MessageInfo, Response, DepsMut, Attribute, Deps, StdResult, Binary, to_binary}; +use cosmwasm_std::{ + to_binary, Attribute, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, +}; use cw2::set_contract_version; -use crate::{msg::{InstantiateMsg, ExecuteMsg, DestinationConfig, QueryMsg, MigrateMsg}, state::{CLOCK_ADDRESS, DESTINATION_CONFIG}, error::ContractError}; - +use crate::{ + error::ContractError, + msg::{DestinationConfig, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, + state::{CLOCK_ADDRESS, DESTINATION_CONFIG}, +}; const CONTRACT_NAME: &str = "crates.io:covenant-interchain-router"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); - #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, @@ -35,8 +39,7 @@ pub fn instantiate( Ok(Response::default() .add_attribute("method", "interchain_router_instantiate") .add_attribute("clock_address", clock_addr) - .add_attributes(destination_config.get_response_attributes()) - ) + .add_attributes(destination_config.get_response_attributes())) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -51,7 +54,7 @@ pub fn execute( // Verify caller is the clock if info.sender != CLOCK_ADDRESS.load(deps.storage)? { - return Err(ContractError::Unauthorized {}) + return Err(ContractError::Unauthorized {}); } match msg { @@ -68,36 +71,36 @@ fn try_route_balances(deps: DepsMut, env: Env) -> Result = if balances.len() == 0 { + let balance_attributes: Vec = if balances.is_empty() { return Ok(Response::default() .add_attribute("method", "try_route_balances") - .add_attribute("balances", "[]") - ) + .add_attribute("balances", "[]")); } else { - balances.iter() + balances + .iter() .map(|c| Attribute::new(c.denom.to_string(), c.amount)) .collect() }; - // get ibc transfer messages for each denom + // get ibc transfer messages for each denom let messages = destination_config.get_ibc_transfer_messages_for_coins(balances, env.block.time); Ok(Response::default() .add_attribute("method", "try_route_balances") .add_attributes(balance_attributes) - .add_messages(messages) - ) + .add_messages(messages)) } #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::DestinationConfig {} => Ok(to_binary(&DESTINATION_CONFIG.may_load(deps.storage)?)?), + QueryMsg::DestinationConfig {} => { + Ok(to_binary(&DESTINATION_CONFIG.may_load(deps.storage)?)?) + } QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), } } - #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { deps.api.debug("WASMDEBUG: migrate"); @@ -107,7 +110,8 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { - let mut response = Response::default().add_attribute("method", "update_interchain_router"); + let mut response = + Response::default().add_attribute("method", "update_interchain_router"); if let Some(addr) = clock_addr { CLOCK_ADDRESS.save(deps.storage, &deps.api.addr_validate(&addr)?)?; @@ -129,5 +133,3 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result, current_timestamp: Timestamp) -> Vec { + pub fn get_ibc_transfer_messages_for_coins( + &self, + coins: Vec, + current_timestamp: Timestamp, + ) -> Vec { let mut messages: Vec = vec![]; for coin in coins { @@ -34,9 +40,11 @@ impl DestinationConfig { channel_id: self.destination_chain_channel_id.to_string(), to_address: self.destination_receiver_addr.to_string(), amount: coin, - timeout: IbcTimeout::with_timestamp(current_timestamp.plus_seconds(self.ibc_transfer_timeout.u64())), + timeout: IbcTimeout::with_timestamp( + current_timestamp.plus_seconds(self.ibc_transfer_timeout.u64()), + ), }; - + messages.push(CosmosMsg::Ibc(msg)); } @@ -45,8 +53,14 @@ impl DestinationConfig { pub fn get_response_attributes(&self) -> Vec { vec![ - Attribute::new("destination_chain_channel_id", self.destination_chain_channel_id.to_string()), - Attribute::new("destination_receiver_addr", self.destination_receiver_addr.to_string()), + Attribute::new( + "destination_chain_channel_id", + self.destination_chain_channel_id.to_string(), + ), + Attribute::new( + "destination_receiver_addr", + self.destination_receiver_addr.to_string(), + ), Attribute::new("ibc_transfer_timeout", self.ibc_transfer_timeout), ] } @@ -64,7 +78,6 @@ pub enum QueryMsg { DestinationConfig {}, } - #[cw_serde] pub enum MigrateMsg { UpdateConfig { diff --git a/contracts/interchain-router/src/state.rs b/contracts/interchain-router/src/state.rs index f25832ce..ec3f8149 100644 --- a/contracts/interchain-router/src/state.rs +++ b/contracts/interchain-router/src/state.rs @@ -4,4 +4,4 @@ use cw_storage_plus::Item; use crate::msg::DestinationConfig; pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); -pub const DESTINATION_CONFIG: Item = Item::new("destination_config"); \ No newline at end of file +pub const DESTINATION_CONFIG: Item = Item::new("destination_config"); diff --git a/contracts/interchain-router/src/suite_tests/mod.rs b/contracts/interchain-router/src/suite_tests/mod.rs new file mode 100644 index 00000000..7b881830 --- /dev/null +++ b/contracts/interchain-router/src/suite_tests/mod.rs @@ -0,0 +1,2 @@ +mod suite; +mod tests; diff --git a/contracts/interchain-router/src/suite_tests/suite.rs b/contracts/interchain-router/src/suite_tests/suite.rs new file mode 100644 index 00000000..cf94953a --- /dev/null +++ b/contracts/interchain-router/src/suite_tests/suite.rs @@ -0,0 +1,146 @@ +use crate::{ + contract::execute, + msg::{DestinationConfig, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, +}; +use cosmwasm_std::{ + testing::{MockApi, MockStorage}, + Addr, Coin, CosmosMsg, Empty, GovMsg, Uint64, +}; +use cw_multi_test::{ + App, AppResponse, BankKeeper, BasicAppBuilder, Contract, ContractWrapper, DistributionKeeper, + Executor, FailingModule, Ibc, IbcAcceptingModule, StakeKeeper, WasmKeeper, +}; + +pub const ADMIN: &str = "admin"; +pub const DEFAULT_RECEIVER: &str = "receiver"; +pub const CLOCK_ADDR: &str = "clock"; +pub const DEFAULT_CHANNEL: &str = "channel-1"; + +fn router_contract() -> Box> { + Box::new( + ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ) + .with_migrate(crate::contract::migrate), + ) +} + +pub struct Suite { + pub app: App< + BankKeeper, + MockApi, + MockStorage, + FailingModule, + WasmKeeper, + StakeKeeper, + DistributionKeeper, + IbcAcceptingModule, + FailingModule, + >, + pub router: Addr, +} + +pub struct SuiteBuilder { + pub instantiate: InstantiateMsg, + pub app: App, +} + +impl Default for SuiteBuilder { + fn default() -> Self { + Self { + instantiate: InstantiateMsg { + clock_address: CLOCK_ADDR.to_string(), + destination_chain_channel_id: DEFAULT_CHANNEL.to_string(), + destination_receiver_addr: DEFAULT_RECEIVER.to_string(), + ibc_transfer_timeout: Uint64::new(10), + }, + app: App::default(), + } + } +} + +impl SuiteBuilder { + pub fn build(self) -> Suite { + let mut app = BasicAppBuilder::default() + .with_ibc(IbcAcceptingModule) + .build(|_, _, _| ()); + + let router_code = app.store_code(router_contract()); + + let router = app + .instantiate_contract( + router_code, + Addr::unchecked(ADMIN), + &self.instantiate, + &[], + "router", + Some(ADMIN.to_string()), + ) + .unwrap(); + + Suite { app, router } + } +} + +// actions +impl Suite { + pub fn tick(&mut self, caller: &str) -> AppResponse { + self.app + .execute_contract( + Addr::unchecked(caller), + self.router.clone(), + &ExecuteMsg::Tick {}, + &[], + ) + .unwrap() + } + + pub fn migrate(&mut self, msg: MigrateMsg) -> Result { + self.app + .migrate_contract(Addr::unchecked(ADMIN), self.router.clone(), &msg, 1) + } +} + +// queries +impl Suite { + pub fn query_clock_addr(&self) -> Addr { + self.app + .wrap() + .query_wasm_smart(&self.router, &QueryMsg::ClockAddress {}) + .unwrap() + } + + pub fn query_destination_config(&self) -> DestinationConfig { + self.app + .wrap() + .query_wasm_smart(&self.router, &QueryMsg::DestinationConfig {}) + .unwrap() + } +} + +// helper +impl Suite { + pub fn fund_router(&mut self, tokens: Vec) -> AppResponse { + self.app + .sudo(cw_multi_test::SudoMsg::Bank( + cw_multi_test::BankSudo::Mint { + to_address: self.router.to_string(), + amount: tokens, + }, + )) + .unwrap() + } + + pub fn assert_router_balance(&mut self, tokens: Vec) { + for c in &tokens { + let queried_amount = self + .app + .wrap() + .query_balance(self.router.to_string(), c.denom.clone()) + .unwrap(); + assert_eq!(&queried_amount, c); + } + } +} diff --git a/contracts/interchain-router/src/suite_tests/tests.rs b/contracts/interchain-router/src/suite_tests/tests.rs new file mode 100644 index 00000000..c163434d --- /dev/null +++ b/contracts/interchain-router/src/suite_tests/tests.rs @@ -0,0 +1,131 @@ +use cosmwasm_std::{ + coins, + testing::{ + mock_dependencies, mock_dependencies_with_balance, mock_env, mock_info, MockQuerier, + }, + to_binary, Addr, Attribute, Coin, ContractInfo, ContractResult, CosmosMsg, DepsMut, Empty, Env, + IbcMsg, IbcTimeout, Never, Querier, QuerierResult, QuerierWrapper, Response, SubMsg, + SystemError, SystemResult, Timestamp, Uint128, Uint64, WasmMsg, WasmQuery, +}; + +use crate::{ + contract::{execute, instantiate}, + msg::{DestinationConfig, ExecuteMsg, InstantiateMsg, MigrateMsg}, + suite_tests::suite::{DEFAULT_CHANNEL, DEFAULT_RECEIVER}, +}; + +use super::suite::{SuiteBuilder, CLOCK_ADDR}; + +#[test] +fn test_instantiate_and_query_all() { + let suite = SuiteBuilder::default().build(); + + let clock = suite.query_clock_addr(); + let config = suite.query_destination_config(); + + assert_eq!("clock", clock); + assert_eq!( + DestinationConfig { + destination_chain_channel_id: DEFAULT_CHANNEL.to_string(), + destination_receiver_addr: Addr::unchecked(DEFAULT_RECEIVER.to_string()), + ibc_transfer_timeout: Uint64::new(10), + }, + config + ); +} + +#[test] +fn test_migrate_config() { + let mut suite = SuiteBuilder::default().build(); + + let migrate_msg = MigrateMsg::UpdateConfig { + clock_addr: Some("working_clock".to_string()), + destination_config: Some(DestinationConfig { + destination_chain_channel_id: "new_channel".to_string(), + destination_receiver_addr: Addr::unchecked("new_receiver".to_string()), + ibc_transfer_timeout: Uint64::new(100), + }), + }; + + suite.migrate(migrate_msg).unwrap(); + + let clock = suite.query_clock_addr(); + let config = suite.query_destination_config(); + + assert_eq!("working_clock", clock); + assert_eq!( + DestinationConfig { + destination_chain_channel_id: "new_channel".to_string(), + destination_receiver_addr: Addr::unchecked("new_receiver".to_string()), + ibc_transfer_timeout: Uint64::new(100), + }, + config + ); +} + +#[test] +#[should_panic(expected = "Unauthorized")] +fn test_unauthorized_tick() { + let mut suite = SuiteBuilder::default().build(); + suite.tick("not_the_clock"); +} + +#[test] +fn test_tick() { + let querier: MockQuerier = + MockQuerier::new(&[(&"cosmos2contract".to_string(), &coins(100, "usdc"))]); + + let mut deps = mock_dependencies(); + // set the custom querier on our mock deps + deps.querier = querier; + + let info = mock_info(CLOCK_ADDR, &[]); + let init_msg = SuiteBuilder::default().instantiate; + + instantiate( + deps.as_mut(), + mock_env(), + info.clone(), + SuiteBuilder::default().instantiate, + ) + .unwrap(); + + let resp = execute( + deps.as_mut(), + mock_env(), + mock_info(CLOCK_ADDR, &vec![]), + crate::msg::ExecuteMsg::Tick {}, + ) + .unwrap(); + let mock_env = mock_env(); + let expected_messages = vec![SubMsg { + id: 0, + msg: CosmosMsg::Ibc(IbcMsg::Transfer { + amount: Coin::new(100, "usdc"), + channel_id: DEFAULT_CHANNEL.to_string(), + to_address: DEFAULT_RECEIVER.to_string(), + timeout: IbcTimeout::with_timestamp( + mock_env + .block + .time + .plus_seconds(init_msg.ibc_transfer_timeout.u64()), + ), + }), + gas_limit: None, + reply_on: cosmwasm_std::ReplyOn::Never, + }]; + let expected_attributes = vec![ + Attribute { + key: "method".to_string(), + value: "try_route_balances".to_string(), + }, + Attribute { + key: "usdc".to_string(), + value: "100".to_string(), + }, + ]; + + // assert the expected response attributes and messages + assert_eq!(expected_messages, resp.messages); + assert_eq!(expected_attributes, resp.attributes); +} From 0ac95a3444b9c188c730221cd457d77532cf3b0a Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 7 Sep 2023 13:37:00 +0200 Subject: [PATCH 046/586] clarifying readme --- contracts/interchain-router/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/interchain-router/README.md b/contracts/interchain-router/README.md index ef175c4e..cd348664 100644 --- a/contracts/interchain-router/README.md +++ b/contracts/interchain-router/README.md @@ -4,4 +4,7 @@ Interchain Router is a contract that facilitates a predetermined routing of fund Each instance of the interchain router is associated with a single receiver. The router continuously attempts to perform IBC transfers to the receiver. -In case the IBC transfer fails, the funds will be refunded, and we can safely try again. +Upon receiving a `Tick`, the contract queries its own balances and uses them +to generate ibc transfer messages to the destination address. + +In case any of the IBC transfers fail, the funds will be refunded, and we can safely try again. From 6a5a1c05e5605950a2f20d4c821210974c1d2aa9 Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 23 Aug 2023 16:43:06 +0200 Subject: [PATCH 047/586] swap holder init --- Cargo.lock | 18 ++++ contracts/swap-holder/Cargo.toml | 33 ++++++ contracts/swap-holder/README.md | 9 ++ contracts/swap-holder/src/contract.rs | 139 ++++++++++++++++++++++++++ contracts/swap-holder/src/error.rs | 17 ++++ contracts/swap-holder/src/lib.rs | 5 + contracts/swap-holder/src/msg.rs | 111 ++++++++++++++++++++ contracts/swap-holder/src/state.rs | 11 ++ 8 files changed, 343 insertions(+) create mode 100644 contracts/swap-holder/Cargo.toml create mode 100644 contracts/swap-holder/README.md create mode 100644 contracts/swap-holder/src/contract.rs create mode 100644 contracts/swap-holder/src/error.rs create mode 100644 contracts/swap-holder/src/lib.rs create mode 100644 contracts/swap-holder/src/msg.rs create mode 100644 contracts/swap-holder/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 37465e2a..e8976752 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -568,6 +568,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "covenant-swap-holder" +version = "1.0.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "covenant-clock", + "covenant-macros", + "covenant-utils", + "cw-multi-test", + "cw-storage-plus 1.1.0", + "cw2 1.1.0", + "cw20 0.15.1", + "serde", + "thiserror", +] + [[package]] name = "covenant-utils" version = "0.0.1" diff --git a/contracts/swap-holder/Cargo.toml b/contracts/swap-holder/Cargo.toml new file mode 100644 index 00000000..d5131b5d --- /dev/null +++ b/contracts/swap-holder/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "covenant-swap-holder" +authors = ["benskey bekauz@protonmail.com"] +description = "covenant contract to facilitate a tokenswap" +edition = { workspace = true } +license = { workspace = true } +rust-version = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# disables #[entry_point] (i.e. instantiate/execute/query) export +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +cw20 = { version = "0.15" } +covenant-macros = { workspace = true } +covenant-clock = { workspace = true, features=["library"] } +covenant-utils = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +anyhow = { workspace = true } \ No newline at end of file diff --git a/contracts/swap-holder/README.md b/contracts/swap-holder/README.md new file mode 100644 index 00000000..3547be40 --- /dev/null +++ b/contracts/swap-holder/README.md @@ -0,0 +1,9 @@ +# Swap Holder + +Swap Holder is a contract meant to facilitate a tokenswap covenant between two parties. + +It holds a list of parties participating in the swap with amount and denom theyre expected to provide. + +If holder receives all expected tokens, it forwards them to the splitter module and completes. + +After a specified duration, parties that delivered their funds are allowed to withdraw (or are automatically refunded). diff --git a/contracts/swap-holder/src/contract.rs b/contracts/swap-holder/src/contract.rs new file mode 100644 index 00000000..d89cd655 --- /dev/null +++ b/contracts/swap-holder/src/contract.rs @@ -0,0 +1,139 @@ + +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Coin, Uint128, CosmosMsg, BankMsg, StdError}; + +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cw2::set_contract_version; + +use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, LOCKUP_CONFIG, PARTIES_CONFIG, CONTRACT_STATE}, error::ContractError}; + +const CONTRACT_NAME: &str = "crates.io:covenant-two-party-pol-holder"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + deps.api.debug("WASMDEBUG: covenant-two-party-pol-holder instantiate"); + + let next_contract = deps.api.addr_validate(&msg.next_contract)?; + let clock_addr = deps.api.addr_validate(&msg.clock_address)?; + + // let parties_config = msg.parties_config.validate_config()?; + let lockup_config = msg.lockup_config.validate(env.block)?; + + NEXT_CONTRACT.save(deps.storage, &next_contract)?; + CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; + LOCKUP_CONFIG.save(deps.storage, lockup_config)?; + PARTIES_CONFIG.save(deps.storage, &msg.parties_config)?; + + Ok(Response::default() + ) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Tick {} => try_tick(deps, env, info), + } +} + +/// attempts to advance the state machine. performs `info.sender` validation +fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + // Verify caller is the clock + let clock_addr = CLOCK_ADDRESS.load(deps.storage)?; + if clock_addr != info.sender { + return Err(ContractError::Unauthorized {}) + } + + let current_state = CONTRACT_STATE.load(deps.storage)?; + match current_state { + ContractState::Instantiated => try_forward(deps, env, info), + ContractState::Expired => try_refund(deps, env, info), + ContractState::Complete => Ok(Response::default() + .add_attribute("contract_state", "completed") + ), + } +} + +fn try_forward( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + let parties = PARTIES_CONFIG.load(deps.storage)?; + + let mut party_a_coin = Coin { + denom: parties.party_a.provided_denom, + amount: Uint128::zero(), + }; + let mut party_b_coin = Coin { + denom: parties.party_b.provided_denom, + amount: Uint128::zero(), + }; + + // query holder balances + let balances = deps.querier.query_all_balances(env.contract.address)?; + // find the existing balances of covenant coins + for coin in balances { + if coin.denom == party_a_coin.denom + && coin.amount >= parties.party_a.amount { + party_a_coin.amount = coin.amount; + } else if coin.denom == party_a_coin.denom + && coin.amount >= parties.party_b.amount { + party_b_coin.amount = coin.amount; + } + } + + // if either of the coin amounts did not get updated to non-zero, + // we are not ready for the swap yet + if party_a_coin.amount.is_zero() || party_b_coin.amount.is_zero() { + return Err(ContractError::InsufficientFunds {}) + } + + // otherwise we are ready to forward the funds to the next module + + // first we query the deposit address of next module + let next_contract = NEXT_CONTRACT.load(deps.storage)?; + let deposit_address_query = deps.querier.query_wasm_smart( + next_contract, + &covenant_utils::neutron_ica::QueryMsg::DepositAddress {}, + )?; + // if query returns None, then we error and wait + let Some(deposit_address) = deposit_address_query else { + return Err(ContractError::Std( + StdError::not_found("Next contract is not ready for receiving the funds yet") + )) + }; + + let multi_send_msg = BankMsg::Send { + to_address: deposit_address, + amount: vec![ + party_a_coin, + party_b_coin, + ] + }; + + // if bankMsg succeeds we can safely complete the holder + CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; + + Ok(Response::default().add_message(CosmosMsg::Bank(multi_send_msg))) +} + +fn try_refund( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + + Ok(Response::default()) +} \ No newline at end of file diff --git a/contracts/swap-holder/src/error.rs b/contracts/swap-holder/src/error.rs new file mode 100644 index 00000000..43d61cfb --- /dev/null +++ b/contracts/swap-holder/src/error.rs @@ -0,0 +1,17 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("No withdrawer address configured")] + NoWithdrawerError {}, + + #[error("Insufficient funds to forward")] + InsufficientFunds {}, +} diff --git a/contracts/swap-holder/src/lib.rs b/contracts/swap-holder/src/lib.rs new file mode 100644 index 00000000..fc92bd5d --- /dev/null +++ b/contracts/swap-holder/src/lib.rs @@ -0,0 +1,5 @@ +pub mod contract; +pub mod error; +pub mod msg; +pub mod state; + diff --git a/contracts/swap-holder/src/msg.rs b/contracts/swap-holder/src/msg.rs new file mode 100644 index 00000000..8049d9e6 --- /dev/null +++ b/contracts/swap-holder/src/msg.rs @@ -0,0 +1,111 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Timestamp, Addr, Attribute, BlockInfo, Uint128}; +use covenant_macros::clocked; + +use crate::error::ContractError; + + +#[cw_serde] +pub struct InstantiateMsg { + /// Address for the clock. This contract verifies + /// that only the clock can execute Ticks + pub clock_address: String, + /// address of the next contract to forward the funds to. + /// usually expected tobe the splitter. + pub next_contract: String, + /// block height of covenant expiration. Position is exited + /// automatically upon reaching that height. + pub lockup_config: LockupConfig, + /// parties engaged in the POL. + pub parties_config: PartiesConfig, +} + +#[clocked] +#[cw_serde] +pub enum ExecuteMsg {} + + +#[cw_serde] +pub enum ContractState { + Instantiated, + /// covenant has reached its expiration date. + Expired, + /// underlying funds have been withdrawn. + Complete, +} + +#[cw_serde] +pub struct PartiesConfig { + pub party_a: Party, + pub party_b: Party, +} + +#[cw_serde] +pub struct Party { + /// authorized address of the party + pub addr: Addr, + /// denom provided by the party + pub provided_denom: String, + /// amount of the denom above to be expected + pub amount: Uint128, +} + +/// enum based configuration of the lockup period. +#[cw_serde] +pub enum LockupConfig { + /// no lockup configured + None, + /// block height based lockup config + Block(u64), + /// timestamp based lockup config + Time(Timestamp), +} + + +impl LockupConfig { + pub fn get_response_attributes(self) -> Vec { + match self { + LockupConfig::None => vec![ + Attribute::new("lockup_config", "none"), + ], + LockupConfig::Block(h) => vec![ + Attribute::new("lockup_config_expiry_block_height", h.to_string()), + ], + LockupConfig::Time(t) => vec![ + Attribute::new("lockup_config_expiry_block_timestamp", t.to_string()), + ], + } + } + + /// validates that the lockup config being stored is not already expired. + pub fn validate(&self, block_info: BlockInfo) -> Result<&LockupConfig, ContractError> { + match self { + LockupConfig::None => Ok(self), + LockupConfig::Block(h) => { + if h > &block_info.height { + Ok(self) + } else { + Err(ContractError::Std(cosmwasm_std::StdError::GenericErr { msg: "invalid".to_string() })) + } + }, + LockupConfig::Time(t) => { + if t.nanos() > block_info.time.nanos() { + Ok(self) + } else { + Err(ContractError::Std(cosmwasm_std::StdError::GenericErr { msg: "invalid".to_string() })) + } + }, + } + } + + /// compares current block info with the stored lockup config. + /// returns false if no lockup configuration is stored. + /// otherwise, returns true if the current block is past the stored info. + pub fn is_due(self, block_info: BlockInfo) -> bool { + match self { + LockupConfig::None => false, // or.. true? should not be called + LockupConfig::Block(h) => h < block_info.height, + LockupConfig::Time(t) => t.nanos() < block_info.time.nanos(), + } + } +} \ No newline at end of file diff --git a/contracts/swap-holder/src/state.rs b/contracts/swap-holder/src/state.rs new file mode 100644 index 00000000..a08e2efc --- /dev/null +++ b/contracts/swap-holder/src/state.rs @@ -0,0 +1,11 @@ +use cosmwasm_std::Addr; +use cw_storage_plus::Item; + +use crate::msg::{ContractState, PartiesConfig, LockupConfig}; + + +pub const CONTRACT_STATE: Item = Item::new("contract_state"); +pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); +pub const NEXT_CONTRACT: Item = Item::new("next_contract"); +pub const PARTIES_CONFIG: Item = Item::new("parties_config"); +pub const LOCKUP_CONFIG: Item = Item::new("lockup_config"); \ No newline at end of file From 09012d430d62507213e078fafa48ec1c980ab36a Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 23 Aug 2023 20:40:25 +0200 Subject: [PATCH 048/586] swap holder try_refund --- contracts/swap-holder/src/contract.rs | 73 ++++++++++++++++++++++++++- contracts/swap-holder/src/msg.rs | 21 +++++++- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/contracts/swap-holder/src/contract.rs b/contracts/swap-holder/src/contract.rs index d89cd655..fd520e02 100644 --- a/contracts/swap-holder/src/contract.rs +++ b/contracts/swap-holder/src/contract.rs @@ -1,5 +1,5 @@ -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Coin, Uint128, CosmosMsg, BankMsg, StdError}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Coin, Uint128, CosmosMsg, BankMsg, StdError, IbcMsg}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -70,6 +70,16 @@ fn try_forward( env: Env, info: MessageInfo, ) -> Result { + let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; + // check if covenant is expired + if lockup_config.is_due(env.block) { + CONTRACT_STATE.save(deps.storage, &ContractState::Expired)?; + return Ok(Response::default() + .add_attribute("method", "try_forward") + .add_attribute("result", "covenant_expired") + ) + } + let parties = PARTIES_CONFIG.load(deps.storage)?; let mut party_a_coin = Coin { @@ -134,6 +144,65 @@ fn try_refund( env: Env, info: MessageInfo, ) -> Result { + let parties = PARTIES_CONFIG.load(deps.storage)?; - Ok(Response::default()) + let mut party_a_coin = Coin { + denom: parties.clone().party_a.provided_denom, + amount: Uint128::zero(), + }; + let mut party_b_coin = Coin { + denom: parties.clone().party_b.provided_denom, + amount: Uint128::zero(), + }; + + // query holder balances + let balances = deps.querier.query_all_balances(env.contract.address)?; + // find the existing balances of covenant coins + for coin in balances { + if coin.denom == party_a_coin.denom + && coin.amount >= parties.party_a.amount { + party_a_coin.amount = coin.amount; + } else if coin.denom == party_a_coin.denom + && coin.amount >= parties.party_b.amount { + party_b_coin.amount = coin.amount; + } + } + + let messages = match (party_a_coin.amount.is_zero(), party_b_coin.amount.is_zero()) { + // if both balances are zero, neither party deposited. + // nothing to return, we complete. + (true, true) => { + CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; + return Ok(Response::default() + .add_attribute("method", "try_refund") + .add_attribute("result", "nothing_to_refund") + .add_attribute("contract_state", "complete") + ) + }, + // party A failed to deposit. refund party B + (true, false) => { + let refund_msg: IbcMsg = parties.party_b.get_ibc_refund_msg(party_b_coin.amount, env.block); + vec![refund_msg] + }, + // party B failed to deposit. refund party A + (false, true) => { + let refund_msg: IbcMsg = parties.party_a.get_ibc_refund_msg(party_a_coin.amount, env.block); + vec![refund_msg] + + }, + // not enough balances to perform the covenant swap. + // refund denoms to both parties. + (false, false) => { + let refund_b_msg: IbcMsg = parties.party_b.get_ibc_refund_msg(party_b_coin.amount, env.block.clone()); + let refund_a_msg: IbcMsg = parties.party_a.get_ibc_refund_msg(party_a_coin.amount, env.block); + vec![refund_a_msg, refund_b_msg] + }, + }; + + CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; + + Ok(Response::default() + .add_attribute("method", "try_refund") + .add_messages(messages) + ) } \ No newline at end of file diff --git a/contracts/swap-holder/src/msg.rs b/contracts/swap-holder/src/msg.rs index 8049d9e6..43353a88 100644 --- a/contracts/swap-holder/src/msg.rs +++ b/contracts/swap-holder/src/msg.rs @@ -1,6 +1,7 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Timestamp, Addr, Attribute, BlockInfo, Uint128}; +use cosmwasm_std::{Timestamp, Addr, Attribute, BlockInfo, Uint128, IbcMsg, Coin, IbcTimeout}; use covenant_macros::clocked; +use covenant_utils::neutron_ica::RemoteChainInfo; use crate::error::ContractError; @@ -48,6 +49,24 @@ pub struct Party { pub provided_denom: String, /// amount of the denom above to be expected pub amount: Uint128, + /// remote chain info for refunds + pub remote_chain_info: RemoteChainInfo, +} + +impl Party { + pub fn get_ibc_refund_msg(self, amount: Uint128, block: BlockInfo) -> IbcMsg { + IbcMsg::Transfer { + channel_id: self.remote_chain_info.channel_id, + to_address: self.addr.to_string(), + amount: Coin { + denom: self.provided_denom, + amount, + }, + timeout: IbcTimeout::with_timestamp( + block.time.plus_seconds(self.remote_chain_info.ibc_transfer_timeout.u64()) + ), + } + } } /// enum based configuration of the lockup period. From d22be1640c1b6876baab9c8a88d9c0d54640f3da Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 24 Aug 2023 13:41:10 +0200 Subject: [PATCH 049/586] adding refund config --- contracts/swap-holder/src/contract.rs | 8 ++--- contracts/swap-holder/src/msg.rs | 47 +++++++++++++++++++-------- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/contracts/swap-holder/src/contract.rs b/contracts/swap-holder/src/contract.rs index fd520e02..1a46d617 100644 --- a/contracts/swap-holder/src/contract.rs +++ b/contracts/swap-holder/src/contract.rs @@ -181,20 +181,20 @@ fn try_refund( }, // party A failed to deposit. refund party B (true, false) => { - let refund_msg: IbcMsg = parties.party_b.get_ibc_refund_msg(party_b_coin.amount, env.block); + let refund_msg: CosmosMsg = parties.party_b.get_refund_msg(party_b_coin.amount, &env.block); vec![refund_msg] }, // party B failed to deposit. refund party A (false, true) => { - let refund_msg: IbcMsg = parties.party_a.get_ibc_refund_msg(party_a_coin.amount, env.block); + let refund_msg: CosmosMsg = parties.party_a.get_refund_msg(party_a_coin.amount, &env.block); vec![refund_msg] }, // not enough balances to perform the covenant swap. // refund denoms to both parties. (false, false) => { - let refund_b_msg: IbcMsg = parties.party_b.get_ibc_refund_msg(party_b_coin.amount, env.block.clone()); - let refund_a_msg: IbcMsg = parties.party_a.get_ibc_refund_msg(party_a_coin.amount, env.block); + let refund_b_msg: CosmosMsg = parties.party_b.get_refund_msg(party_b_coin.amount, &env.block); + let refund_a_msg: CosmosMsg = parties.party_a.get_refund_msg(party_a_coin.amount, &env.block); vec![refund_a_msg, refund_b_msg] }, }; diff --git a/contracts/swap-holder/src/msg.rs b/contracts/swap-holder/src/msg.rs index 43353a88..3e401835 100644 --- a/contracts/swap-holder/src/msg.rs +++ b/contracts/swap-holder/src/msg.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Timestamp, Addr, Attribute, BlockInfo, Uint128, IbcMsg, Coin, IbcTimeout}; +use cosmwasm_std::{Timestamp, Addr, Attribute, BlockInfo, Uint128, IbcMsg, Coin, IbcTimeout, BankMsg, CosmosMsg}; use covenant_macros::clocked; use covenant_utils::neutron_ica::RemoteChainInfo; @@ -49,22 +49,41 @@ pub struct Party { pub provided_denom: String, /// amount of the denom above to be expected pub amount: Uint128, - /// remote chain info for refunds - pub remote_chain_info: RemoteChainInfo, + /// config for refunding funds in case covenant fails to complete + pub refund_config: RefundConfig, +} + +#[cw_serde] +pub enum RefundConfig { + /// party expects a refund on the same chain + Native(Addr), + /// party expects a refund on a remote chain + Ibc(RemoteChainInfo), } impl Party { - pub fn get_ibc_refund_msg(self, amount: Uint128, block: BlockInfo) -> IbcMsg { - IbcMsg::Transfer { - channel_id: self.remote_chain_info.channel_id, - to_address: self.addr.to_string(), - amount: Coin { - denom: self.provided_denom, - amount, - }, - timeout: IbcTimeout::with_timestamp( - block.time.plus_seconds(self.remote_chain_info.ibc_transfer_timeout.u64()) - ), + pub fn get_refund_msg(self, amount: Uint128, block: &BlockInfo) -> CosmosMsg { + match self.refund_config { + RefundConfig::Native(addr) => CosmosMsg::Bank(BankMsg::Send { + to_address: addr.to_string(), + amount: vec![ + Coin { + denom: self.provided_denom, + amount, + }, + ], + }), + RefundConfig::Ibc(r_c_i) => CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: r_c_i.channel_id, + to_address: self.addr.to_string(), + amount: Coin { + denom: self.provided_denom, + amount, + }, + timeout: IbcTimeout::with_timestamp( + block.time.plus_seconds(r_c_i.ibc_transfer_timeout.u64()) + ), + }), } } } From 329ce7e2665d1b1f1613b4b30ea3fd079e136138 Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 24 Aug 2023 14:44:46 +0200 Subject: [PATCH 050/586] adding queries --- contracts/swap-holder/src/contract.rs | 38 +++++++++++++++----------- contracts/swap-holder/src/msg.rs | 39 ++++++++++++++++++++------- contracts/swap-holder/src/state.rs | 7 ++--- 3 files changed, 56 insertions(+), 28 deletions(-) diff --git a/contracts/swap-holder/src/contract.rs b/contracts/swap-holder/src/contract.rs index 1a46d617..760fe480 100644 --- a/contracts/swap-holder/src/contract.rs +++ b/contracts/swap-holder/src/contract.rs @@ -1,11 +1,11 @@ -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Coin, Uint128, CosmosMsg, BankMsg, StdError, IbcMsg}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Coin, Uint128, CosmosMsg, BankMsg, StdError, IbcMsg, Deps, StdResult, Binary, to_binary}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cw2::set_contract_version; -use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, LOCKUP_CONFIG, PARTIES_CONFIG, CONTRACT_STATE}, error::ContractError}; +use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState, QueryMsg}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, LOCKUP_CONFIG, PARTIES_CONFIG, CONTRACT_STATE, COVENANT_TERMS}, error::ContractError}; const CONTRACT_NAME: &str = "crates.io:covenant-two-party-pol-holder"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -30,6 +30,7 @@ pub fn instantiate( CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; LOCKUP_CONFIG.save(deps.storage, lockup_config)?; PARTIES_CONFIG.save(deps.storage, &msg.parties_config)?; + COVENANT_TERMS.save(deps.storage, &msg.covenant_terms)?; Ok(Response::default() ) @@ -57,8 +58,8 @@ fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> Result try_forward(deps, env, info), - ContractState::Expired => try_refund(deps, env, info), + ContractState::Instantiated => try_forward(deps, env), + ContractState::Expired => try_refund(deps, env), ContractState::Complete => Ok(Response::default() .add_attribute("contract_state", "completed") ), @@ -68,7 +69,6 @@ fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> Result Result { let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; // check if covenant is expired @@ -77,10 +77,12 @@ fn try_forward( return Ok(Response::default() .add_attribute("method", "try_forward") .add_attribute("result", "covenant_expired") + .add_attribute("contract_state", "expired") ) } let parties = PARTIES_CONFIG.load(deps.storage)?; + let covenant_terms = COVENANT_TERMS.load(deps.storage)?; let mut party_a_coin = Coin { denom: parties.party_a.provided_denom, @@ -95,11 +97,9 @@ fn try_forward( let balances = deps.querier.query_all_balances(env.contract.address)?; // find the existing balances of covenant coins for coin in balances { - if coin.denom == party_a_coin.denom - && coin.amount >= parties.party_a.amount { + if coin.denom == party_a_coin.denom && coin.amount >= covenant_terms.party_a_amount { party_a_coin.amount = coin.amount; - } else if coin.denom == party_a_coin.denom - && coin.amount >= parties.party_b.amount { + } else if coin.denom == party_a_coin.denom && coin.amount >= covenant_terms.party_b_amount { party_b_coin.amount = coin.amount; } } @@ -142,7 +142,6 @@ fn try_forward( fn try_refund( deps: DepsMut, env: Env, - info: MessageInfo, ) -> Result { let parties = PARTIES_CONFIG.load(deps.storage)?; @@ -159,11 +158,9 @@ fn try_refund( let balances = deps.querier.query_all_balances(env.contract.address)?; // find the existing balances of covenant coins for coin in balances { - if coin.denom == party_a_coin.denom - && coin.amount >= parties.party_a.amount { + if coin.denom == party_a_coin.denom { party_a_coin.amount = coin.amount; - } else if coin.denom == party_a_coin.denom - && coin.amount >= parties.party_b.amount { + } else if coin.denom == party_a_coin.denom { party_b_coin.amount = coin.amount; } } @@ -205,4 +202,15 @@ fn try_refund( .add_attribute("method", "try_refund") .add_messages(messages) ) -} \ No newline at end of file +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::NextContract {} => Ok(to_binary(&NEXT_CONTRACT.may_load(deps.storage)?)?), + QueryMsg::LockupConfig {} => Ok(to_binary(&LOCKUP_CONFIG.may_load(deps.storage)?)?), + QueryMsg::CovenantParties {} => Ok(to_binary(&PARTIES_CONFIG.may_load(deps.storage)?)?), + QueryMsg::CovenantTerms {} => Ok(to_binary(&COVENANT_TERMS.may_load(deps.storage)?)?), + QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), + } +} diff --git a/contracts/swap-holder/src/msg.rs b/contracts/swap-holder/src/msg.rs index 3e401835..ea56ee57 100644 --- a/contracts/swap-holder/src/msg.rs +++ b/contracts/swap-holder/src/msg.rs @@ -1,6 +1,6 @@ -use cosmwasm_schema::cw_serde; +use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Timestamp, Addr, Attribute, BlockInfo, Uint128, IbcMsg, Coin, IbcTimeout, BankMsg, CosmosMsg}; -use covenant_macros::clocked; +use covenant_macros::{clocked, covenant_clock_address}; use covenant_utils::neutron_ica::RemoteChainInfo; use crate::error::ContractError; @@ -18,13 +18,28 @@ pub struct InstantiateMsg { /// automatically upon reaching that height. pub lockup_config: LockupConfig, /// parties engaged in the POL. - pub parties_config: PartiesConfig, + pub parties_config: CovenantPartiesConfig, + /// terms of the covenant + pub covenant_terms: CovenantTerms, } #[clocked] #[cw_serde] pub enum ExecuteMsg {} +#[covenant_clock_address] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(String)] + NextContract {}, + #[returns(LockupConfig)] + LockupConfig {}, + #[returns(CovenantPartiesConfig)] + CovenantParties {}, + #[returns(CovenantTerms)] + CovenantTerms {}, +} #[cw_serde] pub enum ContractState { @@ -36,19 +51,23 @@ pub enum ContractState { } #[cw_serde] -pub struct PartiesConfig { - pub party_a: Party, - pub party_b: Party, +pub struct CovenantTerms { + pub party_a_amount: Uint128, + pub party_b_amount: Uint128, +} + +#[cw_serde] +pub struct CovenantPartiesConfig { + pub party_a: CovenantParty, + pub party_b: CovenantParty, } #[cw_serde] -pub struct Party { +pub struct CovenantParty { /// authorized address of the party pub addr: Addr, /// denom provided by the party pub provided_denom: String, - /// amount of the denom above to be expected - pub amount: Uint128, /// config for refunding funds in case covenant fails to complete pub refund_config: RefundConfig, } @@ -61,7 +80,7 @@ pub enum RefundConfig { Ibc(RemoteChainInfo), } -impl Party { +impl CovenantParty { pub fn get_refund_msg(self, amount: Uint128, block: &BlockInfo) -> CosmosMsg { match self.refund_config { RefundConfig::Native(addr) => CosmosMsg::Bank(BankMsg::Send { diff --git a/contracts/swap-holder/src/state.rs b/contracts/swap-holder/src/state.rs index a08e2efc..ab2814af 100644 --- a/contracts/swap-holder/src/state.rs +++ b/contracts/swap-holder/src/state.rs @@ -1,11 +1,12 @@ use cosmwasm_std::Addr; use cw_storage_plus::Item; -use crate::msg::{ContractState, PartiesConfig, LockupConfig}; +use crate::msg::{ContractState, CovenantPartiesConfig, LockupConfig, CovenantTerms}; pub const CONTRACT_STATE: Item = Item::new("contract_state"); pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); pub const NEXT_CONTRACT: Item = Item::new("next_contract"); -pub const PARTIES_CONFIG: Item = Item::new("parties_config"); -pub const LOCKUP_CONFIG: Item = Item::new("lockup_config"); \ No newline at end of file +pub const PARTIES_CONFIG: Item = Item::new("parties_config"); +pub const LOCKUP_CONFIG: Item = Item::new("lockup_config"); +pub const COVENANT_TERMS: Item = Item::new("covenant_terms"); \ No newline at end of file From 3e5ee693e22a835fbc06c54b4882915807f96550 Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 24 Aug 2023 23:38:48 +0200 Subject: [PATCH 051/586] tests --- Cargo.lock | 2 + contracts/swap-holder/Cargo.toml | 4 +- contracts/swap-holder/src/contract.rs | 4 +- contracts/swap-holder/src/lib.rs | 2 + contracts/swap-holder/src/msg.rs | 24 ++- contracts/swap-holder/src/suite_tests/mod.rs | 19 ++ .../swap-holder/src/suite_tests/suite.rs | 188 ++++++++++++++++++ .../swap-holder/src/suite_tests/tests.rs | 133 +++++++++++++ 8 files changed, 369 insertions(+), 7 deletions(-) create mode 100644 contracts/swap-holder/src/suite_tests/mod.rs create mode 100644 contracts/swap-holder/src/suite_tests/suite.rs create mode 100644 contracts/swap-holder/src/suite_tests/tests.rs diff --git a/Cargo.lock b/Cargo.lock index e8976752..e16beef6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -577,11 +577,13 @@ dependencies = [ "cosmwasm-std", "covenant-clock", "covenant-macros", + "covenant-native-splitter", "covenant-utils", "cw-multi-test", "cw-storage-plus 1.1.0", "cw2 1.1.0", "cw20 0.15.1", + "neutron-sdk", "serde", "thiserror", ] diff --git a/contracts/swap-holder/Cargo.toml b/contracts/swap-holder/Cargo.toml index d5131b5d..16c303a2 100644 --- a/contracts/swap-holder/Cargo.toml +++ b/contracts/swap-holder/Cargo.toml @@ -30,4 +30,6 @@ covenant-utils = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } -anyhow = { workspace = true } \ No newline at end of file +anyhow = { workspace = true } +covenant-native-splitter = { workspace = true } +neutron-sdk = { workspace = true } diff --git a/contracts/swap-holder/src/contract.rs b/contracts/swap-holder/src/contract.rs index 760fe480..537e6145 100644 --- a/contracts/swap-holder/src/contract.rs +++ b/contracts/swap-holder/src/contract.rs @@ -31,6 +31,7 @@ pub fn instantiate( LOCKUP_CONFIG.save(deps.storage, lockup_config)?; PARTIES_CONFIG.save(deps.storage, &msg.parties_config)?; COVENANT_TERMS.save(deps.storage, &msg.covenant_terms)?; + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; Ok(Response::default() ) @@ -72,7 +73,7 @@ fn try_forward( ) -> Result { let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; // check if covenant is expired - if lockup_config.is_due(env.block) { + if lockup_config.is_expired(env.block) { CONTRACT_STATE.save(deps.storage, &ContractState::Expired)?; return Ok(Response::default() .add_attribute("method", "try_forward") @@ -212,5 +213,6 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::CovenantParties {} => Ok(to_binary(&PARTIES_CONFIG.may_load(deps.storage)?)?), QueryMsg::CovenantTerms {} => Ok(to_binary(&COVENANT_TERMS.may_load(deps.storage)?)?), QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), + QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.may_load(deps.storage)?)?) } } diff --git a/contracts/swap-holder/src/lib.rs b/contracts/swap-holder/src/lib.rs index fc92bd5d..20b2e3bb 100644 --- a/contracts/swap-holder/src/lib.rs +++ b/contracts/swap-holder/src/lib.rs @@ -3,3 +3,5 @@ pub mod error; pub mod msg; pub mod state; +#[cfg(test)] +mod suite_tests; diff --git a/contracts/swap-holder/src/msg.rs b/contracts/swap-holder/src/msg.rs index ea56ee57..7144f20b 100644 --- a/contracts/swap-holder/src/msg.rs +++ b/contracts/swap-holder/src/msg.rs @@ -39,6 +39,8 @@ pub enum QueryMsg { CovenantParties {}, #[returns(CovenantTerms)] CovenantTerms {}, + #[returns(ContractState)] + ContractState {}, } #[cw_serde] @@ -142,14 +144,18 @@ impl LockupConfig { if h > &block_info.height { Ok(self) } else { - Err(ContractError::Std(cosmwasm_std::StdError::GenericErr { msg: "invalid".to_string() })) + Err(ContractError::Std(cosmwasm_std::StdError::GenericErr { + msg: "invalid lockup config: block height must be in the future".to_string() + })) } }, LockupConfig::Time(t) => { if t.nanos() > block_info.time.nanos() { Ok(self) } else { - Err(ContractError::Std(cosmwasm_std::StdError::GenericErr { msg: "invalid".to_string() })) + Err(ContractError::Std(cosmwasm_std::StdError::GenericErr { + msg: "invalid lockup config: block time must be in the future".to_string() + })) } }, } @@ -158,11 +164,19 @@ impl LockupConfig { /// compares current block info with the stored lockup config. /// returns false if no lockup configuration is stored. /// otherwise, returns true if the current block is past the stored info. - pub fn is_due(self, block_info: BlockInfo) -> bool { + pub fn is_expired(self, block_info: BlockInfo) -> bool { + println!("current block: {:?}", block_info); match self { LockupConfig::None => false, // or.. true? should not be called - LockupConfig::Block(h) => h < block_info.height, - LockupConfig::Time(t) => t.nanos() < block_info.time.nanos(), + LockupConfig::Block(h) => { + println!("lockup config block: {:?}", h); + h <= block_info.height + }, + LockupConfig::Time(t) => { + println!("lockup config time: {:?}", t); + + t.nanos() <= block_info.time.nanos() + }, } } } \ No newline at end of file diff --git a/contracts/swap-holder/src/suite_tests/mod.rs b/contracts/swap-holder/src/suite_tests/mod.rs new file mode 100644 index 00000000..e42f9f26 --- /dev/null +++ b/contracts/swap-holder/src/suite_tests/mod.rs @@ -0,0 +1,19 @@ +use cosmwasm_std::Empty; +use cw_multi_test::{Contract, ContractWrapper}; +use neutron_sdk::bindings::{ + msg::{IbcFee, NeutronMsg}, + query::NeutronQuery, +}; + + +mod suite; +mod tests; + +pub fn swap_holder_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ); + Box::new(contract) +} diff --git a/contracts/swap-holder/src/suite_tests/suite.rs b/contracts/swap-holder/src/suite_tests/suite.rs new file mode 100644 index 00000000..eb4b22be --- /dev/null +++ b/contracts/swap-holder/src/suite_tests/suite.rs @@ -0,0 +1,188 @@ +use crate::msg::{ExecuteMsg, InstantiateMsg, LockupConfig, CovenantPartiesConfig, CovenantTerms, CovenantParty, RefundConfig, QueryMsg, ContractState}; +use cosmwasm_std::{Addr, Uint128, Coin}; +use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; + +use super::swap_holder_contract; + +pub const ADMIN: &str = "admin"; + +pub const DENOM_A: &str = "denom_a"; +pub const DENOM_B: &str = "denom_b"; + +pub const PARTY_A_ADDR: &str = "party_a"; +pub const PARTY_B_ADDR: &str = "party_b"; + +pub const CLOCK_ADDR: &str = "clock_address"; +pub const NEXT_CONTRACT: &str = "next_contract"; + +pub const INITIAL_BLOCK_HEIGHT: u64 = 12345; +pub const INITIAL_BLOCK_NANOS: u64 = 1571797419879305533; + +pub struct Suite { + pub app: App, + // pub covenant_terms: CovenantTerms, + // pub covenant_paries: CovenantPartiesConfig, + // pub lockup_config: LockupConfig, + // pub clock_address: String, + // pub next_contract: String, + pub holder: Addr, +} + +pub struct SuiteBuilder { + pub instantiate: InstantiateMsg, + pub app: App, +} + +impl Default for SuiteBuilder { + fn default() -> Self { + Self { + instantiate: InstantiateMsg { + clock_address: CLOCK_ADDR.to_string(), + next_contract: NEXT_CONTRACT.to_string(), + lockup_config: LockupConfig::None, + parties_config: CovenantPartiesConfig { + party_a: CovenantParty { + addr: Addr::unchecked(PARTY_A_ADDR.to_string()), + provided_denom: DENOM_A.to_string(), + refund_config: RefundConfig::Native(Addr::unchecked(PARTY_A_ADDR.to_string())), + }, + party_b: CovenantParty { + addr: Addr::unchecked(PARTY_B_ADDR.to_string()), + provided_denom: DENOM_B.to_string(), + refund_config: RefundConfig::Native(Addr::unchecked(PARTY_B_ADDR.to_string())), + }, + }, + covenant_terms: CovenantTerms { + party_a_amount: Uint128::new(400), + party_b_amount: Uint128::new(20), + }, + }, + app: App::default(), + } + } +} + +impl SuiteBuilder { + pub fn with_lockup_config(mut self, config: LockupConfig) -> Self { + self.instantiate.lockup_config = config; + self + } + + pub fn with_parties_config(mut self, config: CovenantPartiesConfig) -> Self { + self.instantiate.parties_config = config; + self + } + + pub fn with_covenant_terms(mut self, terms: CovenantTerms) -> Self { + self.instantiate.covenant_terms = terms; + self + } + + pub fn build(mut self) -> Suite { + let mut app = self.app; + let holder_code = app.store_code(swap_holder_contract()); + + let holder = app + .instantiate_contract( + holder_code, + Addr::unchecked(ADMIN), + &self.instantiate, + &[], + "holder", + Some(ADMIN.to_string()), + ) + .unwrap(); + + Suite { + app, + holder, + // admin: Addr::unchecked(ADMIN), + // pool_address: self.instantiate.pool_address, + // covenant_terms: todo!(), + // covenant_paries: todo!(), + // lockup_config: todo!(), + // clock_address: todo!(), + // next_contract: todo!(), + } + } +} + +// actions +impl Suite { + pub fn tick(&mut self, caller: &str) -> Result { + self.app + .execute_contract( + Addr::unchecked(caller), + self.holder.clone(), + &ExecuteMsg::Tick {}, + &[], + ) + } +} + +// queries +impl Suite { + pub fn query_next_contract(&self) -> Addr { + self.app + .wrap() + .query_wasm_smart(&self.holder, &QueryMsg::NextContract {}) + .unwrap() + } + + pub fn query_lockup_config(&self) -> LockupConfig { + self.app + .wrap() + .query_wasm_smart(&self.holder, &QueryMsg::LockupConfig {}) + .unwrap() + } + + pub fn query_covenant_parties(&self) -> CovenantPartiesConfig { + self.app + .wrap() + .query_wasm_smart(&self.holder, &QueryMsg::CovenantParties {}) + .unwrap() + } + + pub fn query_covenant_terms(&self) -> CovenantTerms { + self.app + .wrap() + .query_wasm_smart(&self.holder, &QueryMsg::CovenantTerms {}) + .unwrap() + } + + pub fn query_clock_address(&self) -> Addr { + self.app + .wrap() + .query_wasm_smart(&self.holder, &QueryMsg::ClockAddress {}) + .unwrap() + } + + pub fn query_contract_state(&self) -> ContractState { + self.app + .wrap() + .query_wasm_smart(&self.holder, &QueryMsg::ContractState {}) + .unwrap() + } +} + +// helper +impl Suite { + pub fn pass_blocks(&mut self, n: u64) { + self.app.update_block(|mut b| b.height += n); + } + + pub fn pass_minutes(&mut self, n: u64) { + self.app.update_block(|mut b| b.time = b.time.plus_minutes(n)); + } + + pub fn fund_coin(&mut self, coin: Coin) -> AppResponse { + self.app + .sudo(SudoMsg::Bank( + cw_multi_test::BankSudo::Mint { + to_address: self.holder.to_string(), + amount: vec![coin], + }, + )) + .unwrap() + } +} diff --git a/contracts/swap-holder/src/suite_tests/tests.rs b/contracts/swap-holder/src/suite_tests/tests.rs new file mode 100644 index 00000000..945bcc5b --- /dev/null +++ b/contracts/swap-holder/src/suite_tests/tests.rs @@ -0,0 +1,133 @@ +use cosmwasm_std::{Addr, Uint128, Timestamp, Coin}; + +use crate::{msg::{LockupConfig, CovenantPartiesConfig, CovenantParty, RefundConfig, CovenantTerms, ContractState}, suite_tests::suite::{PARTY_A_ADDR, DENOM_A, PARTY_B_ADDR, DENOM_B, CLOCK_ADDR, INITIAL_BLOCK_HEIGHT, INITIAL_BLOCK_NANOS}, error::ContractError}; + +use super::suite::SuiteBuilder; + +#[test] +fn test_instantiate_happy_and_query_all() { + let suite = SuiteBuilder::default().build(); + let next_contract = suite.query_next_contract(); + let clock_address = suite.query_clock_address(); + let lockup_config = suite.query_lockup_config(); + let covenant_parties = suite.query_covenant_parties(); + let covenant_terms = suite.query_covenant_terms(); + + assert_eq!(next_contract, "next_contract"); + assert_eq!(clock_address, "clock_address"); + assert_eq!(lockup_config, LockupConfig::None); + assert_eq!(covenant_parties, CovenantPartiesConfig { + party_a: CovenantParty { + addr: Addr::unchecked(PARTY_A_ADDR.to_string()), + provided_denom: DENOM_A.to_string(), + refund_config: RefundConfig::Native(Addr::unchecked(PARTY_A_ADDR.to_string())), + }, + party_b: CovenantParty { + addr: Addr::unchecked(PARTY_B_ADDR.to_string()), + provided_denom: DENOM_B.to_string(), + refund_config: RefundConfig::Native(Addr::unchecked(PARTY_B_ADDR.to_string())), + }, + }); + assert_eq!(covenant_terms, CovenantTerms { + party_a_amount: Uint128::new(400), + party_b_amount: Uint128::new(20), + }); +} + +#[test] +#[should_panic(expected = "invalid lockup config: block height must be in the future")] +fn test_instantiate_past_lockup_block_height() { + SuiteBuilder::default() + .with_lockup_config(LockupConfig::Block(1)) + .build(); +} + +#[test] +#[should_panic(expected = "invalid lockup config: block time must be in the future")] +fn test_instantiate_past_lockup_block_time() { + SuiteBuilder::default() + .with_lockup_config(LockupConfig::Time(Timestamp::from_seconds(1))) + .build(); +} + +#[test] +fn test_tick_unauthorized() { + let mut suite = SuiteBuilder::default().build(); + println!("{}", suite.app.block_info().height); + let resp = suite.tick("not-the-clock") + .unwrap_err() + .downcast() + .unwrap(); + + assert!(matches!(resp, ContractError::Unauthorized {})) +} + +#[test] +fn test_forward_block_expired_covenant() { + let mut suite = SuiteBuilder::default() + .with_lockup_config(LockupConfig::Block(INITIAL_BLOCK_HEIGHT + 50)) + .build(); + suite.pass_blocks(100); + + let state = suite.query_contract_state(); + assert_eq!(state, ContractState::Instantiated); + suite.tick(CLOCK_ADDR).unwrap(); + + let state = suite.query_contract_state(); + assert_eq!(state, ContractState::Expired); +} + +#[test] +fn test_forward_time_expired_covenant() { + let mut suite = SuiteBuilder::default() + .with_lockup_config(LockupConfig::Time(Timestamp::from_nanos( + INITIAL_BLOCK_NANOS + 50 + ))) + .build(); + suite.pass_minutes(100); + + let state = suite.query_contract_state(); + assert_eq!(state, ContractState::Instantiated); + suite.tick(CLOCK_ADDR).unwrap(); + + let state = suite.query_contract_state(); + assert_eq!(state, ContractState::Expired); +} + + +#[test] +#[should_panic(expected = "Insufficient funds to forward")] +fn test_forward_tick_insufficient_funds() { + let mut suite = SuiteBuilder::default().build(); + + suite.fund_coin(Coin { + denom: DENOM_A.to_string(), + amount: Uint128::new(10), + }); + suite.fund_coin(Coin { + denom: DENOM_B.to_string(), + amount: Uint128::new(10), + }); + + suite.tick(CLOCK_ADDR).unwrap(); +} + +#[test] +#[should_panic(expected = "Insufficient funds to forward")] +fn test_forward_tick() { + let mut suite = SuiteBuilder::default().build(); + + suite.fund_coin(Coin { + denom: DENOM_A.to_string(), + amount: Uint128::new(10), + }); + suite.fund_coin(Coin { + denom: DENOM_B.to_string(), + amount: Uint128::new(10), + }); + + suite.tick(CLOCK_ADDR).unwrap(); + + // TODO +} + From 73de0bc7766e9d395b0eb95ad28a8e7d22ae968b Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 25 Aug 2023 13:28:07 +0200 Subject: [PATCH 052/586] unit tests passing --- contracts/swap-holder/src/contract.rs | 22 ++- contracts/swap-holder/src/suite_tests/mod.rs | 29 +++- .../swap-holder/src/suite_tests/suite.rs | 48 +++++- .../swap-holder/src/suite_tests/tests.rs | 150 +++++++++++++++++- 4 files changed, 224 insertions(+), 25 deletions(-) diff --git a/contracts/swap-holder/src/contract.rs b/contracts/swap-holder/src/contract.rs index 537e6145..20f91758 100644 --- a/contracts/swap-holder/src/contract.rs +++ b/contracts/swap-holder/src/contract.rs @@ -1,5 +1,5 @@ -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Coin, Uint128, CosmosMsg, BankMsg, StdError, IbcMsg, Deps, StdResult, Binary, to_binary}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Coin, Uint128, CosmosMsg, BankMsg, StdError, IbcMsg, Deps, StdResult, Binary, to_binary, SubMsg}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -100,7 +100,7 @@ fn try_forward( for coin in balances { if coin.denom == party_a_coin.denom && coin.amount >= covenant_terms.party_a_amount { party_a_coin.amount = coin.amount; - } else if coin.denom == party_a_coin.denom && coin.amount >= covenant_terms.party_b_amount { + } else if coin.denom == party_b_coin.denom && coin.amount >= covenant_terms.party_b_amount { party_b_coin.amount = coin.amount; } } @@ -112,6 +112,10 @@ fn try_forward( } // otherwise we are ready to forward the funds to the next module + let amount = vec![ + party_a_coin, + party_b_coin, + ]; // first we query the deposit address of next module let next_contract = NEXT_CONTRACT.load(deps.storage)?; @@ -119,6 +123,7 @@ fn try_forward( next_contract, &covenant_utils::neutron_ica::QueryMsg::DepositAddress {}, )?; + // if query returns None, then we error and wait let Some(deposit_address) = deposit_address_query else { return Err(ContractError::Std( @@ -128,16 +133,17 @@ fn try_forward( let multi_send_msg = BankMsg::Send { to_address: deposit_address, - amount: vec![ - party_a_coin, - party_b_coin, - ] + amount, }; // if bankMsg succeeds we can safely complete the holder CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; - Ok(Response::default().add_message(CosmosMsg::Bank(multi_send_msg))) + Ok(Response::default() + .add_submessage( + SubMsg::reply_on_error(CosmosMsg::Bank(multi_send_msg), 1) + ) + ) } fn try_refund( @@ -161,7 +167,7 @@ fn try_refund( for coin in balances { if coin.denom == party_a_coin.denom { party_a_coin.amount = coin.amount; - } else if coin.denom == party_a_coin.denom { + } else if coin.denom == party_b_coin.denom { party_b_coin.amount = coin.amount; } } diff --git a/contracts/swap-holder/src/suite_tests/mod.rs b/contracts/swap-holder/src/suite_tests/mod.rs index e42f9f26..81d7a080 100644 --- a/contracts/swap-holder/src/suite_tests/mod.rs +++ b/contracts/swap-holder/src/suite_tests/mod.rs @@ -1,4 +1,6 @@ -use cosmwasm_std::Empty; +use cosmwasm_schema::{QueryResponses, cw_serde}; +use cosmwasm_std::{Empty, Binary, StdResult, Env, Deps, to_binary}; +use covenant_macros::covenant_deposit_address; use cw_multi_test::{Contract, ContractWrapper}; use neutron_sdk::bindings::{ msg::{IbcFee, NeutronMsg}, @@ -17,3 +19,28 @@ pub fn swap_holder_contract() -> Box> { ); Box::new(contract) } + +pub fn mock_deposit_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + query, + ); + Box::new(contract) +} + +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; + +#[covenant_deposit_address] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::DepositAddress {} => Ok(to_binary(&"native-splitter")?) + } +} diff --git a/contracts/swap-holder/src/suite_tests/suite.rs b/contracts/swap-holder/src/suite_tests/suite.rs index eb4b22be..f8a233d3 100644 --- a/contracts/swap-holder/src/suite_tests/suite.rs +++ b/contracts/swap-holder/src/suite_tests/suite.rs @@ -1,8 +1,10 @@ use crate::msg::{ExecuteMsg, InstantiateMsg, LockupConfig, CovenantPartiesConfig, CovenantTerms, CovenantParty, RefundConfig, QueryMsg, ContractState}; -use cosmwasm_std::{Addr, Uint128, Coin}; -use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; +use cosmwasm_std::{Addr, Uint128, Coin, Uint64}; +use covenant_native_splitter::msg::{NativeDenomSplit, SplitReceiver}; +use cw_multi_test::{App, AppResponse, Executor, SudoMsg, ContractWrapper, BasicAppBuilder}; +use neutron_sdk::bindings::{msg::{NeutronMsg, IbcFee}, query::NeutronQuery}; -use super::swap_holder_contract; +use super::{swap_holder_contract, mock_deposit_contract}; pub const ADMIN: &str = "admin"; @@ -20,12 +22,10 @@ pub const INITIAL_BLOCK_NANOS: u64 = 1571797419879305533; pub struct Suite { pub app: App, - // pub covenant_terms: CovenantTerms, - // pub covenant_paries: CovenantPartiesConfig, - // pub lockup_config: LockupConfig, - // pub clock_address: String, - // pub next_contract: String, pub holder: Addr, + pub mock_deposit: Addr, + pub party_a: CovenantParty, + pub party_b: CovenantParty, } pub struct SuiteBuilder { @@ -81,6 +81,20 @@ impl SuiteBuilder { pub fn build(mut self) -> Suite { let mut app = self.app; let holder_code = app.store_code(swap_holder_contract()); + let mock_deposit_code = app.store_code(mock_deposit_contract()); + + let mock_deposit = app + .instantiate_contract( + mock_deposit_code, + Addr::unchecked(ADMIN), + &self.instantiate, + &[], + "holder", + Some(ADMIN.to_string()), + ) + .unwrap(); + + self.instantiate.next_contract = mock_deposit.to_string(); let holder = app .instantiate_contract( @@ -96,6 +110,10 @@ impl SuiteBuilder { Suite { app, holder, + mock_deposit, + party_a: self.instantiate.parties_config.party_a, + party_b: self.instantiate.parties_config.party_b, + // admin: Addr::unchecked(ADMIN), // pool_address: self.instantiate.pool_address, // covenant_terms: todo!(), @@ -163,6 +181,20 @@ impl Suite { .query_wasm_smart(&self.holder, &QueryMsg::ContractState {}) .unwrap() } + + pub fn query_native_splitter_balances(&self) -> Vec { + self.app + .wrap() + .query_all_balances("native-splitter") + .unwrap() + } + + pub fn query_party_denom(&self, denom: String, party: String) -> Coin { + self.app + .wrap() + .query_balance(party, denom) + .unwrap() + } } // helper diff --git a/contracts/swap-holder/src/suite_tests/tests.rs b/contracts/swap-holder/src/suite_tests/tests.rs index 945bcc5b..73664e76 100644 --- a/contracts/swap-holder/src/suite_tests/tests.rs +++ b/contracts/swap-holder/src/suite_tests/tests.rs @@ -13,7 +13,7 @@ fn test_instantiate_happy_and_query_all() { let covenant_parties = suite.query_covenant_parties(); let covenant_terms = suite.query_covenant_terms(); - assert_eq!(next_contract, "next_contract"); + assert_eq!(next_contract, "contract0"); assert_eq!(clock_address, "clock_address"); assert_eq!(lockup_config, LockupConfig::None); assert_eq!(covenant_parties, CovenantPartiesConfig { @@ -113,21 +113,155 @@ fn test_forward_tick_insufficient_funds() { } #[test] -#[should_panic(expected = "Insufficient funds to forward")] fn test_forward_tick() { let mut suite = SuiteBuilder::default().build(); + let coin_a = Coin { + denom: DENOM_A.to_string(), + amount: Uint128::new(500), + }; + let coin_b = Coin { + denom: DENOM_B.to_string(), + amount: Uint128::new(500), + }; - suite.fund_coin(Coin { + suite.fund_coin(coin_a.clone()); + suite.fund_coin(coin_b.clone()); + + suite.tick(CLOCK_ADDR).unwrap(); + suite.pass_blocks(10); + + let state = suite.query_contract_state(); + assert_eq!(state, ContractState::Complete); + + let splitter_balances = suite.query_native_splitter_balances(); + assert_eq!(2, splitter_balances.len()); + assert_eq!(coin_a, splitter_balances[0]); + assert_eq!(coin_b, splitter_balances[1]); +} + +#[test] +fn test_refund_nothing_to_refund() { + let mut suite = SuiteBuilder::default() + .with_lockup_config(LockupConfig::Block(21345)) + .build(); + + suite.pass_blocks(10000); + + // first tick acknowledges the expiration + suite.tick(CLOCK_ADDR).unwrap(); + let state = suite.query_contract_state(); + assert_eq!(state, ContractState::Expired); + + // second tick completes + suite.tick(CLOCK_ADDR).unwrap(); + let state = suite.query_contract_state(); + assert_eq!(state, ContractState::Complete); + + let party_a_bal = suite.query_party_denom(DENOM_A.to_string(), suite.party_a.addr.to_string()); + let party_b_bal = suite.query_party_denom(DENOM_B.to_string(), suite.party_b.addr.to_string()); + + assert_eq!(Uint128::zero(), party_a_bal.amount); + assert_eq!(Uint128::zero(), party_b_bal.amount); +} + + +#[test] +fn test_refund_party_a() { + let mut suite = SuiteBuilder::default() + .with_lockup_config(LockupConfig::Block(21345)) + .build(); + + let coin_a = Coin { denom: DENOM_A.to_string(), - amount: Uint128::new(10), - }); - suite.fund_coin(Coin { + amount: Uint128::new(500), + }; + + suite.fund_coin(coin_a.clone()); + suite.pass_blocks(10000); + + // first tick acknowledges the expiration + suite.tick(CLOCK_ADDR).unwrap(); + let state = suite.query_contract_state(); + assert_eq!(state, ContractState::Expired); + + // second tick completes + suite.tick(CLOCK_ADDR).unwrap(); + let state = suite.query_contract_state(); + assert_eq!(state, ContractState::Complete); + + let party_a_bal = suite.query_party_denom(DENOM_A.to_string(), suite.party_a.addr.to_string()); + let party_b_bal = suite.query_party_denom(DENOM_B.to_string(), suite.party_b.addr.to_string()); + + assert_eq!(Uint128::new(500), party_a_bal.amount); + assert_eq!(Uint128::zero(), party_b_bal.amount); +} + + +#[test] +fn test_refund_party_b() { + let mut suite = SuiteBuilder::default() + .with_lockup_config(LockupConfig::Block(21345)) + .build(); + + let coin_b = Coin { + denom: DENOM_B.to_string(), + amount: Uint128::new(500), + }; + suite.fund_coin(coin_b.clone()); + + suite.pass_blocks(10000); + + // first tick acknowledges the expiration + suite.tick(CLOCK_ADDR).unwrap(); + let state = suite.query_contract_state(); + assert_eq!(state, ContractState::Expired); + + // second tick completes + suite.tick(CLOCK_ADDR).unwrap(); + let state = suite.query_contract_state(); + assert_eq!(state, ContractState::Complete); + + let party_a_bal = suite.query_party_denom(DENOM_A.to_string(), suite.party_a.addr.to_string()); + let party_b_bal = suite.query_party_denom(DENOM_B.to_string(), suite.party_b.addr.to_string()); + + assert_eq!(Uint128::zero(), party_a_bal.amount); + assert_eq!(Uint128::new(500), party_b_bal.amount); +} + + + +#[test] +fn test_refund_both_parties() { + let mut suite = SuiteBuilder::default() + .with_lockup_config(LockupConfig::Block(21345)) + .build(); + let coin_a = Coin { + denom: DENOM_A.to_string(), + amount: Uint128::new(300), + }; + suite.fund_coin(coin_a.clone()); + let coin_b = Coin { denom: DENOM_B.to_string(), amount: Uint128::new(10), - }); + }; + suite.fund_coin(coin_b.clone()); + + suite.pass_blocks(10000); + // first tick acknowledges the expiration suite.tick(CLOCK_ADDR).unwrap(); + let state = suite.query_contract_state(); + assert_eq!(state, ContractState::Expired); + + // second tick completes + suite.tick(CLOCK_ADDR).unwrap(); + let state = suite.query_contract_state(); + assert_eq!(state, ContractState::Complete); + + let party_a_bal = suite.query_party_denom(DENOM_A.to_string(), suite.party_a.addr.to_string()); + let party_b_bal = suite.query_party_denom(DENOM_B.to_string(), suite.party_b.addr.to_string()); - // TODO + assert_eq!(Uint128::new(300), party_a_bal.amount); + assert_eq!(Uint128::new(10), party_b_bal.amount); } From 37b4d0343c5b324f3fe5a737b258949ab5430e82 Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 25 Aug 2023 16:01:26 +0200 Subject: [PATCH 053/586] cleanup --- Cargo.lock | 3 +- contracts/swap-holder/Cargo.toml | 3 +- contracts/swap-holder/src/contract.rs | 64 ++++++++++--------- contracts/swap-holder/src/error.rs | 3 + contracts/swap-holder/src/suite_tests/mod.rs | 13 ++-- .../swap-holder/src/suite_tests/suite.rs | 24 +------ .../swap-holder/src/suite_tests/tests.rs | 50 +++++++++++++-- 7 files changed, 90 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e16beef6..801c6622 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -573,17 +573,16 @@ name = "covenant-swap-holder" version = "1.0.0" dependencies = [ "anyhow", + "cosmos-sdk-proto 0.14.0", "cosmwasm-schema", "cosmwasm-std", "covenant-clock", "covenant-macros", - "covenant-native-splitter", "covenant-utils", "cw-multi-test", "cw-storage-plus 1.1.0", "cw2 1.1.0", "cw20 0.15.1", - "neutron-sdk", "serde", "thiserror", ] diff --git a/contracts/swap-holder/Cargo.toml b/contracts/swap-holder/Cargo.toml index 16c303a2..c5404d12 100644 --- a/contracts/swap-holder/Cargo.toml +++ b/contracts/swap-holder/Cargo.toml @@ -27,9 +27,8 @@ cw20 = { version = "0.15" } covenant-macros = { workspace = true } covenant-clock = { workspace = true, features=["library"] } covenant-utils = { workspace = true } +cosmos-sdk-proto = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } anyhow = { workspace = true } -covenant-native-splitter = { workspace = true } -neutron-sdk = { workspace = true } diff --git a/contracts/swap-holder/src/contract.rs b/contracts/swap-holder/src/contract.rs index 20f91758..084ac056 100644 --- a/contracts/swap-holder/src/contract.rs +++ b/contracts/swap-holder/src/contract.rs @@ -1,5 +1,4 @@ - -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Coin, Uint128, CosmosMsg, BankMsg, StdError, IbcMsg, Deps, StdResult, Binary, to_binary, SubMsg}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Coin, Uint128, CosmosMsg, BankMsg, StdError, Deps, StdResult, Binary, to_binary, SubMsg, Reply}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -7,8 +6,9 @@ use cw2::set_contract_version; use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState, QueryMsg}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, LOCKUP_CONFIG, PARTIES_CONFIG, CONTRACT_STATE, COVENANT_TERMS}, error::ContractError}; -const CONTRACT_NAME: &str = "crates.io:covenant-two-party-pol-holder"; +const CONTRACT_NAME: &str = "crates.io:covenant-swap-holder"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +const COMPLETION_REPLY_ID: u64 = 531; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -18,12 +18,11 @@ pub fn instantiate( msg: InstantiateMsg, ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - deps.api.debug("WASMDEBUG: covenant-two-party-pol-holder instantiate"); + deps.api.debug("WASMDEBUG: covenant-swap-holder instantiate"); let next_contract = deps.api.addr_validate(&msg.next_contract)?; let clock_addr = deps.api.addr_validate(&msg.clock_address)?; - // let parties_config = msg.parties_config.validate_config()?; let lockup_config = msg.lockup_config.validate(env.block)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; @@ -131,18 +130,13 @@ fn try_forward( )) }; - let multi_send_msg = BankMsg::Send { + let multi_send_msg = CosmosMsg::Bank(BankMsg::Send { to_address: deposit_address, amount, - }; - - // if bankMsg succeeds we can safely complete the holder - CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; + }); Ok(Response::default() - .add_submessage( - SubMsg::reply_on_error(CosmosMsg::Bank(multi_send_msg), 1) - ) + .add_submessage(SubMsg::reply_on_success(multi_send_msg, COMPLETION_REPLY_ID)) ) } @@ -173,8 +167,10 @@ fn try_refund( } let messages = match (party_a_coin.amount.is_zero(), party_b_coin.amount.is_zero()) { - // if both balances are zero, neither party deposited. - // nothing to return, we complete. + // both balances being zero means that either: + // 1. neither party deposited any funds in the first place + // 2. we have refunded both parties + // either way, this indicates completion (true, true) => { CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; return Ok(Response::default() @@ -184,27 +180,21 @@ fn try_refund( ) }, // party A failed to deposit. refund party B - (true, false) => { - let refund_msg: CosmosMsg = parties.party_b.get_refund_msg(party_b_coin.amount, &env.block); - vec![refund_msg] - }, + (true, false) => vec![ + parties.party_b.get_refund_msg(party_b_coin.amount, &env.block) + ], // party B failed to deposit. refund party A - (false, true) => { - let refund_msg: CosmosMsg = parties.party_a.get_refund_msg(party_a_coin.amount, &env.block); - vec![refund_msg] - - }, + (false, true) => vec![ + parties.party_a.get_refund_msg(party_a_coin.amount, &env.block), + ], // not enough balances to perform the covenant swap. // refund denoms to both parties. - (false, false) => { - let refund_b_msg: CosmosMsg = parties.party_b.get_refund_msg(party_b_coin.amount, &env.block); - let refund_a_msg: CosmosMsg = parties.party_a.get_refund_msg(party_a_coin.amount, &env.block); - vec![refund_a_msg, refund_b_msg] - }, + (false, false) => vec![ + parties.party_a.get_refund_msg(party_a_coin.amount, &env.block), + parties.party_b.get_refund_msg(party_b_coin.amount, &env.block), + ], }; - CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; - Ok(Response::default() .add_attribute("method", "try_refund") .add_messages(messages) @@ -222,3 +212,15 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.may_load(deps.storage)?)?) } } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { + if msg.id == COMPLETION_REPLY_ID { + CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; + Ok(Response::default() + .add_attribute("method", "reply_complete") + .add_attribute("contract_state", "complete")) + } else { + Err(ContractError::UnexpectedReplyId {}) + } +} diff --git a/contracts/swap-holder/src/error.rs b/contracts/swap-holder/src/error.rs index 43d61cfb..bf088149 100644 --- a/contracts/swap-holder/src/error.rs +++ b/contracts/swap-holder/src/error.rs @@ -14,4 +14,7 @@ pub enum ContractError { #[error("Insufficient funds to forward")] InsufficientFunds {}, + + #[error("unexpected reply id")] + UnexpectedReplyId {}, } diff --git a/contracts/swap-holder/src/suite_tests/mod.rs b/contracts/swap-holder/src/suite_tests/mod.rs index 81d7a080..e523d02d 100644 --- a/contracts/swap-holder/src/suite_tests/mod.rs +++ b/contracts/swap-holder/src/suite_tests/mod.rs @@ -2,10 +2,6 @@ use cosmwasm_schema::{QueryResponses, cw_serde}; use cosmwasm_std::{Empty, Binary, StdResult, Env, Deps, to_binary}; use covenant_macros::covenant_deposit_address; use cw_multi_test::{Contract, ContractWrapper}; -use neutron_sdk::bindings::{ - msg::{IbcFee, NeutronMsg}, - query::NeutronQuery, -}; mod suite; @@ -16,7 +12,7 @@ pub fn swap_holder_contract() -> Box> { crate::contract::execute, crate::contract::instantiate, crate::contract::query, - ); + ).with_reply(crate::contract::reply); Box::new(contract) } @@ -35,12 +31,11 @@ use cosmwasm_std::entry_point; #[covenant_deposit_address] #[cw_serde] #[derive(QueryResponses)] -pub enum QueryMsg { -} +pub enum QueryMsg {} #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::DepositAddress {} => Ok(to_binary(&"native-splitter")?) + QueryMsg::DepositAddress {} => Ok(to_binary(&"native-splitter")?), } -} +} \ No newline at end of file diff --git a/contracts/swap-holder/src/suite_tests/suite.rs b/contracts/swap-holder/src/suite_tests/suite.rs index f8a233d3..5ac35762 100644 --- a/contracts/swap-holder/src/suite_tests/suite.rs +++ b/contracts/swap-holder/src/suite_tests/suite.rs @@ -1,8 +1,6 @@ use crate::msg::{ExecuteMsg, InstantiateMsg, LockupConfig, CovenantPartiesConfig, CovenantTerms, CovenantParty, RefundConfig, QueryMsg, ContractState}; -use cosmwasm_std::{Addr, Uint128, Coin, Uint64}; -use covenant_native_splitter::msg::{NativeDenomSplit, SplitReceiver}; -use cw_multi_test::{App, AppResponse, Executor, SudoMsg, ContractWrapper, BasicAppBuilder}; -use neutron_sdk::bindings::{msg::{NeutronMsg, IbcFee}, query::NeutronQuery}; +use cosmwasm_std::{Addr, Uint128, Coin}; +use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; use super::{swap_holder_contract, mock_deposit_contract}; @@ -68,16 +66,6 @@ impl SuiteBuilder { self } - pub fn with_parties_config(mut self, config: CovenantPartiesConfig) -> Self { - self.instantiate.parties_config = config; - self - } - - pub fn with_covenant_terms(mut self, terms: CovenantTerms) -> Self { - self.instantiate.covenant_terms = terms; - self - } - pub fn build(mut self) -> Suite { let mut app = self.app; let holder_code = app.store_code(swap_holder_contract()); @@ -113,14 +101,6 @@ impl SuiteBuilder { mock_deposit, party_a: self.instantiate.parties_config.party_a, party_b: self.instantiate.parties_config.party_b, - - // admin: Addr::unchecked(ADMIN), - // pool_address: self.instantiate.pool_address, - // covenant_terms: todo!(), - // covenant_paries: todo!(), - // lockup_config: todo!(), - // clock_address: todo!(), - // next_contract: todo!(), } } } diff --git a/contracts/swap-holder/src/suite_tests/tests.rs b/contracts/swap-holder/src/suite_tests/tests.rs index 73664e76..9deb463c 100644 --- a/contracts/swap-holder/src/suite_tests/tests.rs +++ b/contracts/swap-holder/src/suite_tests/tests.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{Addr, Uint128, Timestamp, Coin}; -use crate::{msg::{LockupConfig, CovenantPartiesConfig, CovenantParty, RefundConfig, CovenantTerms, ContractState}, suite_tests::suite::{PARTY_A_ADDR, DENOM_A, PARTY_B_ADDR, DENOM_B, CLOCK_ADDR, INITIAL_BLOCK_HEIGHT, INITIAL_BLOCK_NANOS}, error::ContractError}; +use crate::{msg::{LockupConfig, CovenantPartiesConfig, CovenantParty, RefundConfig, CovenantTerms, ContractState}, suite_tests::{suite::{PARTY_A_ADDR, DENOM_A, PARTY_B_ADDR, DENOM_B, CLOCK_ADDR, INITIAL_BLOCK_HEIGHT, INITIAL_BLOCK_NANOS}, QueryMsg}, error::ContractError}; use super::suite::SuiteBuilder; @@ -112,6 +112,40 @@ fn test_forward_tick_insufficient_funds() { suite.tick(CLOCK_ADDR).unwrap(); } +#[test] +fn test_covenant_query_endpoint() { + let mut suite = SuiteBuilder::default().build(); + let coin_a = Coin { + denom: DENOM_A.to_string(), + amount: Uint128::new(500), + }; + let coin_b = Coin { + denom: DENOM_B.to_string(), + amount: Uint128::new(500), + }; + suite.fund_coin(coin_a.clone()); + suite.fund_coin(coin_b.clone()); + + suite.tick(CLOCK_ADDR).unwrap(); + suite.pass_blocks(10); + + let state = suite.query_contract_state(); + assert_eq!(state, ContractState::Complete); + + let splitter_balances = suite.query_native_splitter_balances(); + assert_eq!(2, splitter_balances.len()); + assert_eq!(coin_a, splitter_balances[0]); + assert_eq!(coin_b, splitter_balances[1]); + + let resp: String = suite.app + .wrap() + .query_wasm_smart(suite.mock_deposit, &covenant_utils::neutron_ica::QueryMsg::DepositAddress {}) + .unwrap(); + + println!("resp: {:?}", resp); +} + + #[test] fn test_forward_tick() { let mut suite = SuiteBuilder::default().build(); @@ -184,7 +218,9 @@ fn test_refund_party_a() { let state = suite.query_contract_state(); assert_eq!(state, ContractState::Expired); - // second tick completes + // second tick refunds + suite.tick(CLOCK_ADDR).unwrap(); + // third tick acknowledges the refund and completes suite.tick(CLOCK_ADDR).unwrap(); let state = suite.query_contract_state(); assert_eq!(state, ContractState::Complete); @@ -216,8 +252,11 @@ fn test_refund_party_b() { let state = suite.query_contract_state(); assert_eq!(state, ContractState::Expired); - // second tick completes + // second refunds + suite.tick(CLOCK_ADDR).unwrap(); + // third tick completes suite.tick(CLOCK_ADDR).unwrap(); + let state = suite.query_contract_state(); assert_eq!(state, ContractState::Complete); @@ -253,8 +292,11 @@ fn test_refund_both_parties() { let state = suite.query_contract_state(); assert_eq!(state, ContractState::Expired); - // second tick completes + // second tick refunds the parties suite.tick(CLOCK_ADDR).unwrap(); + // third tick acknowledges the refund and completes + suite.tick(CLOCK_ADDR).unwrap(); + let state = suite.query_contract_state(); assert_eq!(state, ContractState::Complete); From 367ffe8562d552ff1481cc9be64a90ceed34dbda Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 25 Aug 2023 17:11:59 +0200 Subject: [PATCH 054/586] moving out common logic to utils --- contracts/swap-holder/src/contract.rs | 8 +- contracts/swap-holder/src/msg.rs | 149 ++---------------- contracts/swap-holder/src/state.rs | 3 +- .../swap-holder/src/suite_tests/suite.rs | 7 +- .../swap-holder/src/suite_tests/tests.rs | 7 +- packages/covenant-utils/src/lib.rs | 149 ++++++++++++++++++ 6 files changed, 180 insertions(+), 143 deletions(-) diff --git a/contracts/swap-holder/src/contract.rs b/contracts/swap-holder/src/contract.rs index 084ac056..692d241b 100644 --- a/contracts/swap-holder/src/contract.rs +++ b/contracts/swap-holder/src/contract.rs @@ -2,6 +2,7 @@ use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Coin, Uint128, CosmosMsg #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; +use covenant_utils::CovenantTerms; use cw2::set_contract_version; use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState, QueryMsg}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, LOCKUP_CONFIG, PARTIES_CONFIG, CONTRACT_STATE, COVENANT_TERMS}, error::ContractError}; @@ -23,16 +24,17 @@ pub fn instantiate( let next_contract = deps.api.addr_validate(&msg.next_contract)?; let clock_addr = deps.api.addr_validate(&msg.clock_address)?; - let lockup_config = msg.lockup_config.validate(env.block)?; + msg.lockup_config.validate(env.block)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; - LOCKUP_CONFIG.save(deps.storage, lockup_config)?; + LOCKUP_CONFIG.save(deps.storage, &msg.lockup_config)?; PARTIES_CONFIG.save(deps.storage, &msg.parties_config)?; COVENANT_TERMS.save(deps.storage, &msg.covenant_terms)?; CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; Ok(Response::default() + .add_attributes(msg.get_response_attributes()) ) } @@ -82,7 +84,7 @@ fn try_forward( } let parties = PARTIES_CONFIG.load(deps.storage)?; - let covenant_terms = COVENANT_TERMS.load(deps.storage)?; + let CovenantTerms::TokenSwap(covenant_terms) = COVENANT_TERMS.load(deps.storage)?; let mut party_a_coin = Coin { denom: parties.party_a.provided_denom, diff --git a/contracts/swap-holder/src/msg.rs b/contracts/swap-holder/src/msg.rs index 7144f20b..e77d053e 100644 --- a/contracts/swap-holder/src/msg.rs +++ b/contracts/swap-holder/src/msg.rs @@ -1,10 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Timestamp, Addr, Attribute, BlockInfo, Uint128, IbcMsg, Coin, IbcTimeout, BankMsg, CosmosMsg}; +use cosmwasm_std::{Addr, Attribute}; use covenant_macros::{clocked, covenant_clock_address}; -use covenant_utils::neutron_ica::RemoteChainInfo; - -use crate::error::ContractError; - +use covenant_utils::{LockupConfig, CovenantTerms, CovenantPartiesConfig}; #[cw_serde] pub struct InstantiateMsg { @@ -23,6 +20,20 @@ pub struct InstantiateMsg { pub covenant_terms: CovenantTerms, } +impl InstantiateMsg { + pub fn get_response_attributes(self) -> Vec { + let mut attrs = vec![ + Attribute::new("clock_addr", self.clock_address), + Attribute::new("next_contract", self.next_contract), + ]; + // TODO: + // attrs.extend(self.parties_config.get_response_attributes()); + attrs.extend(self.covenant_terms.get_response_attributes()); + attrs.extend(self.lockup_config.get_response_attributes()); + attrs + } +} + #[clocked] #[cw_serde] pub enum ExecuteMsg {} @@ -52,131 +63,3 @@ pub enum ContractState { Complete, } -#[cw_serde] -pub struct CovenantTerms { - pub party_a_amount: Uint128, - pub party_b_amount: Uint128, -} - -#[cw_serde] -pub struct CovenantPartiesConfig { - pub party_a: CovenantParty, - pub party_b: CovenantParty, -} - -#[cw_serde] -pub struct CovenantParty { - /// authorized address of the party - pub addr: Addr, - /// denom provided by the party - pub provided_denom: String, - /// config for refunding funds in case covenant fails to complete - pub refund_config: RefundConfig, -} - -#[cw_serde] -pub enum RefundConfig { - /// party expects a refund on the same chain - Native(Addr), - /// party expects a refund on a remote chain - Ibc(RemoteChainInfo), -} - -impl CovenantParty { - pub fn get_refund_msg(self, amount: Uint128, block: &BlockInfo) -> CosmosMsg { - match self.refund_config { - RefundConfig::Native(addr) => CosmosMsg::Bank(BankMsg::Send { - to_address: addr.to_string(), - amount: vec![ - Coin { - denom: self.provided_denom, - amount, - }, - ], - }), - RefundConfig::Ibc(r_c_i) => CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: r_c_i.channel_id, - to_address: self.addr.to_string(), - amount: Coin { - denom: self.provided_denom, - amount, - }, - timeout: IbcTimeout::with_timestamp( - block.time.plus_seconds(r_c_i.ibc_transfer_timeout.u64()) - ), - }), - } - } -} - -/// enum based configuration of the lockup period. -#[cw_serde] -pub enum LockupConfig { - /// no lockup configured - None, - /// block height based lockup config - Block(u64), - /// timestamp based lockup config - Time(Timestamp), -} - - -impl LockupConfig { - pub fn get_response_attributes(self) -> Vec { - match self { - LockupConfig::None => vec![ - Attribute::new("lockup_config", "none"), - ], - LockupConfig::Block(h) => vec![ - Attribute::new("lockup_config_expiry_block_height", h.to_string()), - ], - LockupConfig::Time(t) => vec![ - Attribute::new("lockup_config_expiry_block_timestamp", t.to_string()), - ], - } - } - - /// validates that the lockup config being stored is not already expired. - pub fn validate(&self, block_info: BlockInfo) -> Result<&LockupConfig, ContractError> { - match self { - LockupConfig::None => Ok(self), - LockupConfig::Block(h) => { - if h > &block_info.height { - Ok(self) - } else { - Err(ContractError::Std(cosmwasm_std::StdError::GenericErr { - msg: "invalid lockup config: block height must be in the future".to_string() - })) - } - }, - LockupConfig::Time(t) => { - if t.nanos() > block_info.time.nanos() { - Ok(self) - } else { - Err(ContractError::Std(cosmwasm_std::StdError::GenericErr { - msg: "invalid lockup config: block time must be in the future".to_string() - })) - } - }, - } - } - - /// compares current block info with the stored lockup config. - /// returns false if no lockup configuration is stored. - /// otherwise, returns true if the current block is past the stored info. - pub fn is_expired(self, block_info: BlockInfo) -> bool { - println!("current block: {:?}", block_info); - match self { - LockupConfig::None => false, // or.. true? should not be called - LockupConfig::Block(h) => { - println!("lockup config block: {:?}", h); - h <= block_info.height - }, - LockupConfig::Time(t) => { - println!("lockup config time: {:?}", t); - - t.nanos() <= block_info.time.nanos() - }, - } - } -} \ No newline at end of file diff --git a/contracts/swap-holder/src/state.rs b/contracts/swap-holder/src/state.rs index ab2814af..b27cad3e 100644 --- a/contracts/swap-holder/src/state.rs +++ b/contracts/swap-holder/src/state.rs @@ -1,7 +1,8 @@ use cosmwasm_std::Addr; +use covenant_utils::{LockupConfig, CovenantTerms, CovenantPartiesConfig}; use cw_storage_plus::Item; -use crate::msg::{ContractState, CovenantPartiesConfig, LockupConfig, CovenantTerms}; +use crate::msg::ContractState; pub const CONTRACT_STATE: Item = Item::new("contract_state"); diff --git a/contracts/swap-holder/src/suite_tests/suite.rs b/contracts/swap-holder/src/suite_tests/suite.rs index 5ac35762..da58fd96 100644 --- a/contracts/swap-holder/src/suite_tests/suite.rs +++ b/contracts/swap-holder/src/suite_tests/suite.rs @@ -1,5 +1,6 @@ -use crate::msg::{ExecuteMsg, InstantiateMsg, LockupConfig, CovenantPartiesConfig, CovenantTerms, CovenantParty, RefundConfig, QueryMsg, ContractState}; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ContractState}; use cosmwasm_std::{Addr, Uint128, Coin}; +use covenant_utils::{CovenantParty, LockupConfig, RefundConfig, CovenantPartiesConfig, SwapCovenantTerms, CovenantTerms}; use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; use super::{swap_holder_contract, mock_deposit_contract}; @@ -50,10 +51,10 @@ impl Default for SuiteBuilder { refund_config: RefundConfig::Native(Addr::unchecked(PARTY_B_ADDR.to_string())), }, }, - covenant_terms: CovenantTerms { + covenant_terms: CovenantTerms::TokenSwap(SwapCovenantTerms { party_a_amount: Uint128::new(400), party_b_amount: Uint128::new(20), - }, + }), }, app: App::default(), } diff --git a/contracts/swap-holder/src/suite_tests/tests.rs b/contracts/swap-holder/src/suite_tests/tests.rs index 9deb463c..ef26281c 100644 --- a/contracts/swap-holder/src/suite_tests/tests.rs +++ b/contracts/swap-holder/src/suite_tests/tests.rs @@ -1,6 +1,7 @@ use cosmwasm_std::{Addr, Uint128, Timestamp, Coin}; +use covenant_utils::{LockupConfig, CovenantParty, RefundConfig, CovenantPartiesConfig, CovenantTerms, SwapCovenantTerms}; -use crate::{msg::{LockupConfig, CovenantPartiesConfig, CovenantParty, RefundConfig, CovenantTerms, ContractState}, suite_tests::{suite::{PARTY_A_ADDR, DENOM_A, PARTY_B_ADDR, DENOM_B, CLOCK_ADDR, INITIAL_BLOCK_HEIGHT, INITIAL_BLOCK_NANOS}, QueryMsg}, error::ContractError}; +use crate::{msg::ContractState, suite_tests::suite::{PARTY_A_ADDR, DENOM_A, PARTY_B_ADDR, DENOM_B, CLOCK_ADDR, INITIAL_BLOCK_HEIGHT, INITIAL_BLOCK_NANOS}, error::ContractError}; use super::suite::SuiteBuilder; @@ -28,10 +29,10 @@ fn test_instantiate_happy_and_query_all() { refund_config: RefundConfig::Native(Addr::unchecked(PARTY_B_ADDR.to_string())), }, }); - assert_eq!(covenant_terms, CovenantTerms { + assert_eq!(covenant_terms, CovenantTerms::TokenSwap(SwapCovenantTerms { party_a_amount: Uint128::new(400), party_b_amount: Uint128::new(20), - }); + })); } #[test] diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index f7e5095a..fa70015a 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -1,3 +1,7 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{BlockInfo, Attribute, Timestamp, StdError, Addr, Uint128, CosmosMsg, BankMsg, Coin, IbcTimeout, IbcMsg}; +use neutron_ica::RemoteChainInfo; + pub mod neutron_ica { use cosmwasm_schema::{cw_serde, QueryResponses}; @@ -145,3 +149,148 @@ pub mod neutron_ica { }) } } + + +/// enum based configuration of the lockup period. +#[cw_serde] +pub enum LockupConfig { + /// no lockup configured + None, + /// block height based lockup config + Block(u64), + /// timestamp based lockup config + Time(Timestamp), +} + + +impl LockupConfig { + pub fn get_response_attributes(self) -> Vec { + match self { + LockupConfig::None => vec![ + Attribute::new("lockup_config", "none"), + ], + LockupConfig::Block(h) => vec![ + Attribute::new("lockup_config_expiry_block_height", h.to_string()), + ], + LockupConfig::Time(t) => vec![ + Attribute::new("lockup_config_expiry_block_timestamp", t.to_string()), + ], + } + } + + /// validates that the lockup config being stored is not already expired. + pub fn validate(&self, block_info: BlockInfo) -> Result<(), StdError> { + match self { + LockupConfig::None => Ok(()), + LockupConfig::Block(h) => { + if h > &block_info.height { + Ok(()) + } else { + Err(StdError::GenericErr { + msg: "invalid lockup config: block height must be in the future".to_string() + }) + } + }, + LockupConfig::Time(t) => { + if t.nanos() > block_info.time.nanos() { + Ok(()) + } else { + Err(StdError::GenericErr { + msg: "invalid lockup config: block time must be in the future".to_string() + }) + } + }, + } + } + + /// compares current block info with the stored lockup config. + /// returns false if no lockup configuration is stored. + /// otherwise, returns true if the current block is past the stored info. + pub fn is_expired(self, block_info: BlockInfo) -> bool { + match self { + LockupConfig::None => false, // or.. true? should not be called tho + LockupConfig::Block(h) => h <= block_info.height, + LockupConfig::Time(t) => t.nanos() <= block_info.time.nanos(), + } + } +} + +#[cw_serde] +pub enum RefundConfig { + /// party expects a refund on the same chain + Native(Addr), + /// party expects a refund on a remote chain + Ibc(RemoteChainInfo), +} + + +#[cw_serde] +pub struct CovenantParty { + /// authorized address of the party + pub addr: Addr, + /// denom provided by the party + pub provided_denom: String, + /// config for refunding funds in case covenant fails to complete + pub refund_config: RefundConfig, +} + +impl CovenantParty { + pub fn get_refund_msg(self, amount: Uint128, block: &BlockInfo) -> CosmosMsg { + match self.refund_config { + RefundConfig::Native(addr) => CosmosMsg::Bank(BankMsg::Send { + to_address: addr.to_string(), + amount: vec![ + Coin { + denom: self.provided_denom, + amount, + }, + ], + }), + RefundConfig::Ibc(r_c_i) => CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: r_c_i.channel_id, + to_address: self.addr.to_string(), + amount: Coin { + denom: self.provided_denom, + amount, + }, + timeout: IbcTimeout::with_timestamp( + block.time.plus_seconds(r_c_i.ibc_transfer_timeout.u64()) + ), + }), + } + } +} + + +#[cw_serde] +pub struct CovenantPartiesConfig { + pub party_a: CovenantParty, + pub party_b: CovenantParty, +} + + +#[cw_serde] +pub enum CovenantTerms { + TokenSwap(SwapCovenantTerms) +} + +#[cw_serde] +pub struct SwapCovenantTerms { + pub party_a_amount: Uint128, + pub party_b_amount: Uint128, +} + +impl CovenantTerms { + pub fn get_response_attributes(self) -> Vec { + match self { + CovenantTerms::TokenSwap(terms) => { + let attrs = vec![ + Attribute::new("covenant_terms", "token_swap"), + Attribute::new("party_a_amount", terms.party_a_amount), + Attribute::new("party_b_amount", terms.party_b_amount), + ]; + attrs + } + } + } +} \ No newline at end of file From fa26cb471b822c249d64b5661cd2d9e9a0a55066 Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 25 Aug 2023 17:29:48 +0200 Subject: [PATCH 055/586] response attributes helpers --- contracts/swap-holder/src/msg.rs | 3 +-- packages/covenant-utils/src/lib.rs | 33 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/contracts/swap-holder/src/msg.rs b/contracts/swap-holder/src/msg.rs index e77d053e..1bf8d31e 100644 --- a/contracts/swap-holder/src/msg.rs +++ b/contracts/swap-holder/src/msg.rs @@ -26,8 +26,7 @@ impl InstantiateMsg { Attribute::new("clock_addr", self.clock_address), Attribute::new("next_contract", self.next_contract), ]; - // TODO: - // attrs.extend(self.parties_config.get_response_attributes()); + attrs.extend(self.parties_config.get_response_attributes()); attrs.extend(self.covenant_terms.get_response_attributes()); attrs.extend(self.lockup_config.get_response_attributes()); attrs diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index fa70015a..ce2744cb 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -224,6 +224,26 @@ pub enum RefundConfig { } +impl RefundConfig { + pub fn get_response_attributes(self, party: String) -> Vec { + match self { + RefundConfig::Native(addr) => vec![ + Attribute::new("refund_config_native_addr", addr), + ], + RefundConfig::Ibc(r_c_i) => { + let attrs = r_c_i.get_response_attributes() + .into_iter() + .map(|mut a| { + a.key = party.to_string() + &a.key; + a + }) + .collect(); + attrs + }, + } + } +} + #[cw_serde] pub struct CovenantParty { /// authorized address of the party @@ -268,6 +288,19 @@ pub struct CovenantPartiesConfig { pub party_b: CovenantParty, } +impl CovenantPartiesConfig { + pub fn get_response_attributes(self) -> Vec { + let mut attrs = vec![ + Attribute::new("party_a_address", self.party_a.addr), + Attribute::new("party_a_denom", self.party_a.provided_denom), + Attribute::new("party_b_address", self.party_b.addr), + Attribute::new("party_b_denom", self.party_b.provided_denom), + ]; + attrs.extend(self.party_a.refund_config.get_response_attributes("party_a_".to_string())); + attrs.extend(self.party_b.refund_config.get_response_attributes("party_b_".to_string())); + attrs + } +} #[cw_serde] pub enum CovenantTerms { From 1cd9d01215e1ca33d90214c1b941c3050e83b920 Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 25 Aug 2023 17:34:10 +0200 Subject: [PATCH 056/586] fmt --- contracts/swap-holder/.cargo/config | 3 + contracts/swap-holder/src/contract.rs | 87 ++++++------ contracts/swap-holder/src/msg.rs | 5 +- contracts/swap-holder/src/state.rs | 5 +- contracts/swap-holder/src/suite_tests/mod.rs | 10 +- .../swap-holder/src/suite_tests/suite.rs | 50 +++---- .../swap-holder/src/suite_tests/tests.rs | 94 +++++++------ packages/covenant-macros/src/lib.rs | 5 +- packages/covenant-utils/src/lib.rs | 125 ++++++++++-------- 9 files changed, 205 insertions(+), 179 deletions(-) create mode 100644 contracts/swap-holder/.cargo/config diff --git a/contracts/swap-holder/.cargo/config b/contracts/swap-holder/.cargo/config new file mode 100644 index 00000000..6a6f2852 --- /dev/null +++ b/contracts/swap-holder/.cargo/config @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +schema = "run --bin schema" \ No newline at end of file diff --git a/contracts/swap-holder/src/contract.rs b/contracts/swap-holder/src/contract.rs index 692d241b..549597c0 100644 --- a/contracts/swap-holder/src/contract.rs +++ b/contracts/swap-holder/src/contract.rs @@ -1,11 +1,20 @@ -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Coin, Uint128, CosmosMsg, BankMsg, StdError, Deps, StdResult, Binary, to_binary, SubMsg, Reply}; +use cosmwasm_std::{ + to_binary, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply, Response, + StdError, StdResult, SubMsg, Uint128, +}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use covenant_utils::CovenantTerms; use cw2::set_contract_version; -use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState, QueryMsg}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, LOCKUP_CONFIG, PARTIES_CONFIG, CONTRACT_STATE, COVENANT_TERMS}, error::ContractError}; +use crate::{ + error::ContractError, + msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg}, + state::{ + CLOCK_ADDRESS, CONTRACT_STATE, COVENANT_TERMS, LOCKUP_CONFIG, NEXT_CONTRACT, PARTIES_CONFIG, + }, +}; const CONTRACT_NAME: &str = "crates.io:covenant-swap-holder"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -19,7 +28,8 @@ pub fn instantiate( msg: InstantiateMsg, ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - deps.api.debug("WASMDEBUG: covenant-swap-holder instantiate"); + deps.api + .debug("WASMDEBUG: covenant-swap-holder instantiate"); let next_contract = deps.api.addr_validate(&msg.next_contract)?; let clock_addr = deps.api.addr_validate(&msg.clock_address)?; @@ -33,9 +43,7 @@ pub fn instantiate( COVENANT_TERMS.save(deps.storage, &msg.covenant_terms)?; CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; - Ok(Response::default() - .add_attributes(msg.get_response_attributes()) - ) + Ok(Response::default().add_attributes(msg.get_response_attributes())) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -55,23 +63,20 @@ fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> Result try_forward(deps, env), ContractState::Expired => try_refund(deps, env), - ContractState::Complete => Ok(Response::default() - .add_attribute("contract_state", "completed") - ), + ContractState::Complete => { + Ok(Response::default().add_attribute("contract_state", "completed")) + } } } -fn try_forward( - deps: DepsMut, - env: Env, -) -> Result { +fn try_forward(deps: DepsMut, env: Env) -> Result { let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; // check if covenant is expired if lockup_config.is_expired(env.block) { @@ -79,8 +84,7 @@ fn try_forward( return Ok(Response::default() .add_attribute("method", "try_forward") .add_attribute("result", "covenant_expired") - .add_attribute("contract_state", "expired") - ) + .add_attribute("contract_state", "expired")); } let parties = PARTIES_CONFIG.load(deps.storage)?; @@ -109,14 +113,11 @@ fn try_forward( // if either of the coin amounts did not get updated to non-zero, // we are not ready for the swap yet if party_a_coin.amount.is_zero() || party_b_coin.amount.is_zero() { - return Err(ContractError::InsufficientFunds {}) + return Err(ContractError::InsufficientFunds {}); } // otherwise we are ready to forward the funds to the next module - let amount = vec![ - party_a_coin, - party_b_coin, - ]; + let amount = vec![party_a_coin, party_b_coin]; // first we query the deposit address of next module let next_contract = NEXT_CONTRACT.load(deps.storage)?; @@ -132,20 +133,18 @@ fn try_forward( )) }; - let multi_send_msg = CosmosMsg::Bank(BankMsg::Send { + let multi_send_msg = CosmosMsg::Bank(BankMsg::Send { to_address: deposit_address, amount, }); - Ok(Response::default() - .add_submessage(SubMsg::reply_on_success(multi_send_msg, COMPLETION_REPLY_ID)) - ) + Ok(Response::default().add_submessage(SubMsg::reply_on_success( + multi_send_msg, + COMPLETION_REPLY_ID, + ))) } -fn try_refund( - deps: DepsMut, - env: Env, -) -> Result { +fn try_refund(deps: DepsMut, env: Env) -> Result { let parties = PARTIES_CONFIG.load(deps.storage)?; let mut party_a_coin = Coin { @@ -178,29 +177,31 @@ fn try_refund( return Ok(Response::default() .add_attribute("method", "try_refund") .add_attribute("result", "nothing_to_refund") - .add_attribute("contract_state", "complete") - ) - }, + .add_attribute("contract_state", "complete")); + } // party A failed to deposit. refund party B - (true, false) => vec![ - parties.party_b.get_refund_msg(party_b_coin.amount, &env.block) - ], + (true, false) => vec![parties + .party_b + .get_refund_msg(party_b_coin.amount, &env.block)], // party B failed to deposit. refund party A - (false, true) => vec![ - parties.party_a.get_refund_msg(party_a_coin.amount, &env.block), - ], + (false, true) => vec![parties + .party_a + .get_refund_msg(party_a_coin.amount, &env.block)], // not enough balances to perform the covenant swap. // refund denoms to both parties. (false, false) => vec![ - parties.party_a.get_refund_msg(party_a_coin.amount, &env.block), - parties.party_b.get_refund_msg(party_b_coin.amount, &env.block), + parties + .party_a + .get_refund_msg(party_a_coin.amount, &env.block), + parties + .party_b + .get_refund_msg(party_b_coin.amount, &env.block), ], }; Ok(Response::default() .add_attribute("method", "try_refund") - .add_messages(messages) - ) + .add_messages(messages)) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -211,7 +212,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::CovenantParties {} => Ok(to_binary(&PARTIES_CONFIG.may_load(deps.storage)?)?), QueryMsg::CovenantTerms {} => Ok(to_binary(&COVENANT_TERMS.may_load(deps.storage)?)?), QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), - QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.may_load(deps.storage)?)?) + QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.may_load(deps.storage)?)?), } } diff --git a/contracts/swap-holder/src/msg.rs b/contracts/swap-holder/src/msg.rs index 1bf8d31e..7816f247 100644 --- a/contracts/swap-holder/src/msg.rs +++ b/contracts/swap-holder/src/msg.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Attribute}; use covenant_macros::{clocked, covenant_clock_address}; -use covenant_utils::{LockupConfig, CovenantTerms, CovenantPartiesConfig}; +use covenant_utils::{CovenantPartiesConfig, CovenantTerms, LockupConfig}; #[cw_serde] pub struct InstantiateMsg { @@ -11,7 +11,7 @@ pub struct InstantiateMsg { /// address of the next contract to forward the funds to. /// usually expected tobe the splitter. pub next_contract: String, - /// block height of covenant expiration. Position is exited + /// block height of covenant expiration. Position is exited /// automatically upon reaching that height. pub lockup_config: LockupConfig, /// parties engaged in the POL. @@ -61,4 +61,3 @@ pub enum ContractState { /// underlying funds have been withdrawn. Complete, } - diff --git a/contracts/swap-holder/src/state.rs b/contracts/swap-holder/src/state.rs index b27cad3e..863b58f3 100644 --- a/contracts/swap-holder/src/state.rs +++ b/contracts/swap-holder/src/state.rs @@ -1,13 +1,12 @@ use cosmwasm_std::Addr; -use covenant_utils::{LockupConfig, CovenantTerms, CovenantPartiesConfig}; +use covenant_utils::{CovenantPartiesConfig, CovenantTerms, LockupConfig}; use cw_storage_plus::Item; use crate::msg::ContractState; - pub const CONTRACT_STATE: Item = Item::new("contract_state"); pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); pub const NEXT_CONTRACT: Item = Item::new("next_contract"); pub const PARTIES_CONFIG: Item = Item::new("parties_config"); pub const LOCKUP_CONFIG: Item = Item::new("lockup_config"); -pub const COVENANT_TERMS: Item = Item::new("covenant_terms"); \ No newline at end of file +pub const COVENANT_TERMS: Item = Item::new("covenant_terms"); diff --git a/contracts/swap-holder/src/suite_tests/mod.rs b/contracts/swap-holder/src/suite_tests/mod.rs index e523d02d..48992ba7 100644 --- a/contracts/swap-holder/src/suite_tests/mod.rs +++ b/contracts/swap-holder/src/suite_tests/mod.rs @@ -1,9 +1,8 @@ -use cosmwasm_schema::{QueryResponses, cw_serde}; -use cosmwasm_std::{Empty, Binary, StdResult, Env, Deps, to_binary}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{to_binary, Binary, Deps, Empty, Env, StdResult}; use covenant_macros::covenant_deposit_address; use cw_multi_test::{Contract, ContractWrapper}; - mod suite; mod tests; @@ -12,7 +11,8 @@ pub fn swap_holder_contract() -> Box> { crate::contract::execute, crate::contract::instantiate, crate::contract::query, - ).with_reply(crate::contract::reply); + ) + .with_reply(crate::contract::reply); Box::new(contract) } @@ -38,4 +38,4 @@ pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::DepositAddress {} => Ok(to_binary(&"native-splitter")?), } -} \ No newline at end of file +} diff --git a/contracts/swap-holder/src/suite_tests/suite.rs b/contracts/swap-holder/src/suite_tests/suite.rs index da58fd96..a470e0a1 100644 --- a/contracts/swap-holder/src/suite_tests/suite.rs +++ b/contracts/swap-holder/src/suite_tests/suite.rs @@ -1,9 +1,12 @@ -use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ContractState}; -use cosmwasm_std::{Addr, Uint128, Coin}; -use covenant_utils::{CovenantParty, LockupConfig, RefundConfig, CovenantPartiesConfig, SwapCovenantTerms, CovenantTerms}; +use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg}; +use cosmwasm_std::{Addr, Coin, Uint128}; +use covenant_utils::{ + CovenantPartiesConfig, CovenantParty, CovenantTerms, LockupConfig, RefundConfig, + SwapCovenantTerms, +}; use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; -use super::{swap_holder_contract, mock_deposit_contract}; +use super::{mock_deposit_contract, swap_holder_contract}; pub const ADMIN: &str = "admin"; @@ -43,12 +46,16 @@ impl Default for SuiteBuilder { party_a: CovenantParty { addr: Addr::unchecked(PARTY_A_ADDR.to_string()), provided_denom: DENOM_A.to_string(), - refund_config: RefundConfig::Native(Addr::unchecked(PARTY_A_ADDR.to_string())), + refund_config: RefundConfig::Native(Addr::unchecked( + PARTY_A_ADDR.to_string(), + )), }, party_b: CovenantParty { addr: Addr::unchecked(PARTY_B_ADDR.to_string()), provided_denom: DENOM_B.to_string(), - refund_config: RefundConfig::Native(Addr::unchecked(PARTY_B_ADDR.to_string())), + refund_config: RefundConfig::Native(Addr::unchecked( + PARTY_B_ADDR.to_string(), + )), }, }, covenant_terms: CovenantTerms::TokenSwap(SwapCovenantTerms { @@ -109,13 +116,12 @@ impl SuiteBuilder { // actions impl Suite { pub fn tick(&mut self, caller: &str) -> Result { - self.app - .execute_contract( - Addr::unchecked(caller), - self.holder.clone(), - &ExecuteMsg::Tick {}, - &[], - ) + self.app.execute_contract( + Addr::unchecked(caller), + self.holder.clone(), + &ExecuteMsg::Tick {}, + &[], + ) } } @@ -171,10 +177,7 @@ impl Suite { } pub fn query_party_denom(&self, denom: String, party: String) -> Coin { - self.app - .wrap() - .query_balance(party, denom) - .unwrap() + self.app.wrap().query_balance(party, denom).unwrap() } } @@ -185,17 +188,16 @@ impl Suite { } pub fn pass_minutes(&mut self, n: u64) { - self.app.update_block(|mut b| b.time = b.time.plus_minutes(n)); + self.app + .update_block(|mut b| b.time = b.time.plus_minutes(n)); } pub fn fund_coin(&mut self, coin: Coin) -> AppResponse { self.app - .sudo(SudoMsg::Bank( - cw_multi_test::BankSudo::Mint { - to_address: self.holder.to_string(), - amount: vec![coin], - }, - )) + .sudo(SudoMsg::Bank(cw_multi_test::BankSudo::Mint { + to_address: self.holder.to_string(), + amount: vec![coin], + })) .unwrap() } } diff --git a/contracts/swap-holder/src/suite_tests/tests.rs b/contracts/swap-holder/src/suite_tests/tests.rs index ef26281c..bde902c2 100644 --- a/contracts/swap-holder/src/suite_tests/tests.rs +++ b/contracts/swap-holder/src/suite_tests/tests.rs @@ -1,7 +1,17 @@ -use cosmwasm_std::{Addr, Uint128, Timestamp, Coin}; -use covenant_utils::{LockupConfig, CovenantParty, RefundConfig, CovenantPartiesConfig, CovenantTerms, SwapCovenantTerms}; - -use crate::{msg::ContractState, suite_tests::suite::{PARTY_A_ADDR, DENOM_A, PARTY_B_ADDR, DENOM_B, CLOCK_ADDR, INITIAL_BLOCK_HEIGHT, INITIAL_BLOCK_NANOS}, error::ContractError}; +use cosmwasm_std::{Addr, Coin, Timestamp, Uint128}; +use covenant_utils::{ + CovenantPartiesConfig, CovenantParty, CovenantTerms, LockupConfig, RefundConfig, + SwapCovenantTerms, +}; + +use crate::{ + error::ContractError, + msg::ContractState, + suite_tests::suite::{ + CLOCK_ADDR, DENOM_A, DENOM_B, INITIAL_BLOCK_HEIGHT, INITIAL_BLOCK_NANOS, PARTY_A_ADDR, + PARTY_B_ADDR, + }, +}; use super::suite::SuiteBuilder; @@ -17,22 +27,28 @@ fn test_instantiate_happy_and_query_all() { assert_eq!(next_contract, "contract0"); assert_eq!(clock_address, "clock_address"); assert_eq!(lockup_config, LockupConfig::None); - assert_eq!(covenant_parties, CovenantPartiesConfig { - party_a: CovenantParty { - addr: Addr::unchecked(PARTY_A_ADDR.to_string()), - provided_denom: DENOM_A.to_string(), - refund_config: RefundConfig::Native(Addr::unchecked(PARTY_A_ADDR.to_string())), - }, - party_b: CovenantParty { - addr: Addr::unchecked(PARTY_B_ADDR.to_string()), - provided_denom: DENOM_B.to_string(), - refund_config: RefundConfig::Native(Addr::unchecked(PARTY_B_ADDR.to_string())), - }, - }); - assert_eq!(covenant_terms, CovenantTerms::TokenSwap(SwapCovenantTerms { - party_a_amount: Uint128::new(400), - party_b_amount: Uint128::new(20), - })); + assert_eq!( + covenant_parties, + CovenantPartiesConfig { + party_a: CovenantParty { + addr: Addr::unchecked(PARTY_A_ADDR.to_string()), + provided_denom: DENOM_A.to_string(), + refund_config: RefundConfig::Native(Addr::unchecked(PARTY_A_ADDR.to_string())), + }, + party_b: CovenantParty { + addr: Addr::unchecked(PARTY_B_ADDR.to_string()), + provided_denom: DENOM_B.to_string(), + refund_config: RefundConfig::Native(Addr::unchecked(PARTY_B_ADDR.to_string())), + }, + } + ); + assert_eq!( + covenant_terms, + CovenantTerms::TokenSwap(SwapCovenantTerms { + party_a_amount: Uint128::new(400), + party_b_amount: Uint128::new(20), + }) + ); } #[test] @@ -55,10 +71,7 @@ fn test_instantiate_past_lockup_block_time() { fn test_tick_unauthorized() { let mut suite = SuiteBuilder::default().build(); println!("{}", suite.app.block_info().height); - let resp = suite.tick("not-the-clock") - .unwrap_err() - .downcast() - .unwrap(); + let resp = suite.tick("not-the-clock").unwrap_err().downcast().unwrap(); assert!(matches!(resp, ContractError::Unauthorized {})) } @@ -82,7 +95,7 @@ fn test_forward_block_expired_covenant() { fn test_forward_time_expired_covenant() { let mut suite = SuiteBuilder::default() .with_lockup_config(LockupConfig::Time(Timestamp::from_nanos( - INITIAL_BLOCK_NANOS + 50 + INITIAL_BLOCK_NANOS + 50, ))) .build(); suite.pass_minutes(100); @@ -95,7 +108,6 @@ fn test_forward_time_expired_covenant() { assert_eq!(state, ContractState::Expired); } - #[test] #[should_panic(expected = "Insufficient funds to forward")] fn test_forward_tick_insufficient_funds() { @@ -132,21 +144,24 @@ fn test_covenant_query_endpoint() { let state = suite.query_contract_state(); assert_eq!(state, ContractState::Complete); - + let splitter_balances = suite.query_native_splitter_balances(); assert_eq!(2, splitter_balances.len()); assert_eq!(coin_a, splitter_balances[0]); assert_eq!(coin_b, splitter_balances[1]); - let resp: String = suite.app + let resp: String = suite + .app .wrap() - .query_wasm_smart(suite.mock_deposit, &covenant_utils::neutron_ica::QueryMsg::DepositAddress {}) + .query_wasm_smart( + suite.mock_deposit, + &covenant_utils::neutron_ica::QueryMsg::DepositAddress {}, + ) .unwrap(); println!("resp: {:?}", resp); } - #[test] fn test_forward_tick() { let mut suite = SuiteBuilder::default().build(); @@ -167,7 +182,7 @@ fn test_forward_tick() { let state = suite.query_contract_state(); assert_eq!(state, ContractState::Complete); - + let splitter_balances = suite.query_native_splitter_balances(); assert_eq!(2, splitter_balances.len()); assert_eq!(coin_a, splitter_balances[0]); @@ -186,7 +201,7 @@ fn test_refund_nothing_to_refund() { suite.tick(CLOCK_ADDR).unwrap(); let state = suite.query_contract_state(); assert_eq!(state, ContractState::Expired); - + // second tick completes suite.tick(CLOCK_ADDR).unwrap(); let state = suite.query_contract_state(); @@ -199,13 +214,12 @@ fn test_refund_nothing_to_refund() { assert_eq!(Uint128::zero(), party_b_bal.amount); } - #[test] fn test_refund_party_a() { let mut suite = SuiteBuilder::default() .with_lockup_config(LockupConfig::Block(21345)) .build(); - + let coin_a = Coin { denom: DENOM_A.to_string(), amount: Uint128::new(500), @@ -218,7 +232,7 @@ fn test_refund_party_a() { suite.tick(CLOCK_ADDR).unwrap(); let state = suite.query_contract_state(); assert_eq!(state, ContractState::Expired); - + // second tick refunds suite.tick(CLOCK_ADDR).unwrap(); // third tick acknowledges the refund and completes @@ -233,19 +247,18 @@ fn test_refund_party_a() { assert_eq!(Uint128::zero(), party_b_bal.amount); } - #[test] fn test_refund_party_b() { let mut suite = SuiteBuilder::default() .with_lockup_config(LockupConfig::Block(21345)) .build(); - + let coin_b = Coin { denom: DENOM_B.to_string(), amount: Uint128::new(500), }; suite.fund_coin(coin_b.clone()); - + suite.pass_blocks(10000); // first tick acknowledges the expiration @@ -268,8 +281,6 @@ fn test_refund_party_b() { assert_eq!(Uint128::new(500), party_b_bal.amount); } - - #[test] fn test_refund_both_parties() { let mut suite = SuiteBuilder::default() @@ -284,7 +295,7 @@ fn test_refund_both_parties() { denom: DENOM_B.to_string(), amount: Uint128::new(10), }; - suite.fund_coin(coin_b.clone()); + suite.fund_coin(coin_b); suite.pass_blocks(10000); @@ -307,4 +318,3 @@ fn test_refund_both_parties() { assert_eq!(Uint128::new(300), party_a_bal.amount); assert_eq!(Uint128::new(10), party_b_bal.amount); } - diff --git a/packages/covenant-macros/src/lib.rs b/packages/covenant-macros/src/lib.rs index 4c05b38a..99e5286f 100644 --- a/packages/covenant-macros/src/lib.rs +++ b/packages/covenant-macros/src/lib.rs @@ -21,7 +21,7 @@ fn merge_variants(metadata: TokenStream, left: TokenStream, right: TokenStream) Enum(DataEnum { variants: to_add, .. }), - ) = (&mut left.data, right.data,) + ) = (&mut left.data, right.data) { variants.extend(to_add.into_iter()); @@ -66,7 +66,6 @@ pub fn covenant_deposit_address(metadata: TokenStream, input: TokenStream) -> To ) } - #[proc_macro_attribute] pub fn covenant_clock_address(metadata: TokenStream, input: TokenStream) -> TokenStream { merge_variants( @@ -113,4 +112,4 @@ pub fn covenant_ica_address(metadata: TokenStream, input: TokenStream) -> TokenS ) .into(), ) -} \ No newline at end of file +} diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index ce2744cb..14d19469 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -1,12 +1,17 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{BlockInfo, Attribute, Timestamp, StdError, Addr, Uint128, CosmosMsg, BankMsg, Coin, IbcTimeout, IbcMsg}; +use cosmwasm_std::{ + Addr, Attribute, BankMsg, BlockInfo, Coin, CosmosMsg, IbcMsg, IbcTimeout, StdError, Timestamp, + Uint128, +}; use neutron_ica::RemoteChainInfo; - pub mod neutron_ica { use cosmwasm_schema::{cw_serde, QueryResponses}; - use cosmwasm_std::{Uint64, Binary, StdError, Attribute, Coin, Uint128}; - use neutron_sdk::{bindings::{msg::IbcFee, types::ProtobufAny}, NeutronResult}; + use cosmwasm_std::{Attribute, Binary, Coin, StdError, Uint128, Uint64}; + use neutron_sdk::{ + bindings::{msg::IbcFee, types::ProtobufAny}, + NeutronResult, + }; use prost::Message; #[cw_serde] @@ -60,8 +65,11 @@ pub mod neutron_ica { Attribute::new("connection_id", &self.connection_id), Attribute::new("channel_id", &self.channel_id), Attribute::new("denom", &self.denom), - Attribute::new("ibc_transfer_timeout", &self.ibc_transfer_timeout.to_string()), - Attribute::new("ica_timeout", &self.ica_timeout.to_string()), + Attribute::new( + "ibc_transfer_timeout", + self.ibc_transfer_timeout.to_string(), + ), + Attribute::new("ica_timeout", self.ica_timeout.to_string()), Attribute::new("ibc_recv_fee", recv_fee), Attribute::new("ibc_ack_fee", ack_fee), Attribute::new("ibc_timeout_fee", timeout_fee), @@ -69,10 +77,13 @@ pub mod neutron_ica { } pub fn validate(self) -> Result { - if self.ibc_fee.ack_fee.is_empty() || self.ibc_fee.timeout_fee.is_empty() || !self.ibc_fee.recv_fee.is_empty() { + if self.ibc_fee.ack_fee.is_empty() + || self.ibc_fee.timeout_fee.is_empty() + || !self.ibc_fee.recv_fee.is_empty() + { return Err(StdError::GenericErr { msg: "invalid IbcFee".to_string(), - }) + }); } Ok(self) @@ -81,8 +92,8 @@ pub mod neutron_ica { fn coin_vec_to_string(coins: &Vec) -> String { let mut str = "".to_string(); - if coins.len() == 0 { - str.push_str(&"[]".to_string()); + if coins.is_empty() { + str.push_str("[]"); } else { for coin in coins { str.push_str(&coin.to_string()); @@ -91,7 +102,10 @@ pub mod neutron_ica { str.to_string() } - pub fn get_proto_coin(denom: String, amount: Uint128) -> cosmos_sdk_proto::cosmos::base::v1beta1::Coin { + pub fn get_proto_coin( + denom: String, + amount: Uint128, + ) -> cosmos_sdk_proto::cosmos::base::v1beta1::Coin { cosmos_sdk_proto::cosmos::base::v1beta1::Coin { denom, amount: amount.to_string(), @@ -150,7 +164,6 @@ pub mod neutron_ica { } } - /// enum based configuration of the lockup period. #[cw_serde] pub enum LockupConfig { @@ -162,19 +175,18 @@ pub enum LockupConfig { Time(Timestamp), } - impl LockupConfig { pub fn get_response_attributes(self) -> Vec { match self { - LockupConfig::None => vec![ - Attribute::new("lockup_config", "none"), - ], - LockupConfig::Block(h) => vec![ - Attribute::new("lockup_config_expiry_block_height", h.to_string()), - ], - LockupConfig::Time(t) => vec![ - Attribute::new("lockup_config_expiry_block_timestamp", t.to_string()), - ], + LockupConfig::None => vec![Attribute::new("lockup_config", "none")], + LockupConfig::Block(h) => vec![Attribute::new( + "lockup_config_expiry_block_height", + h.to_string(), + )], + LockupConfig::Time(t) => vec![Attribute::new( + "lockup_config_expiry_block_timestamp", + t.to_string(), + )], } } @@ -187,19 +199,20 @@ impl LockupConfig { Ok(()) } else { Err(StdError::GenericErr { - msg: "invalid lockup config: block height must be in the future".to_string() - }) + msg: "invalid lockup config: block height must be in the future" + .to_string(), + }) } - }, + } LockupConfig::Time(t) => { if t.nanos() > block_info.time.nanos() { Ok(()) } else { Err(StdError::GenericErr { - msg: "invalid lockup config: block time must be in the future".to_string() + msg: "invalid lockup config: block time must be in the future".to_string(), }) } - }, + } } } @@ -223,23 +236,18 @@ pub enum RefundConfig { Ibc(RemoteChainInfo), } - impl RefundConfig { - pub fn get_response_attributes(self, party: String) -> Vec { + pub fn get_response_attributes(self, party: String) -> Vec { match self { - RefundConfig::Native(addr) => vec![ - Attribute::new("refund_config_native_addr", addr), - ], - RefundConfig::Ibc(r_c_i) => { - let attrs = r_c_i.get_response_attributes() - .into_iter() - .map(|mut a| { - a.key = party.to_string() + &a.key; - a - }) - .collect(); - attrs - }, + RefundConfig::Native(addr) => vec![Attribute::new("refund_config_native_addr", addr)], + RefundConfig::Ibc(r_c_i) => r_c_i + .get_response_attributes() + .into_iter() + .map(|mut a| { + a.key = party.to_string() + &a.key; + a + }) + .collect(), } } } @@ -255,16 +263,14 @@ pub struct CovenantParty { } impl CovenantParty { - pub fn get_refund_msg(self, amount: Uint128, block: &BlockInfo) -> CosmosMsg { + pub fn get_refund_msg(self, amount: Uint128, block: &BlockInfo) -> CosmosMsg { match self.refund_config { RefundConfig::Native(addr) => CosmosMsg::Bank(BankMsg::Send { to_address: addr.to_string(), - amount: vec![ - Coin { - denom: self.provided_denom, - amount, - }, - ], + amount: vec![Coin { + denom: self.provided_denom, + amount, + }], }), RefundConfig::Ibc(r_c_i) => CosmosMsg::Ibc(IbcMsg::Transfer { channel_id: r_c_i.channel_id, @@ -274,14 +280,13 @@ impl CovenantParty { amount, }, timeout: IbcTimeout::with_timestamp( - block.time.plus_seconds(r_c_i.ibc_transfer_timeout.u64()) + block.time.plus_seconds(r_c_i.ibc_transfer_timeout.u64()), ), }), } } } - #[cw_serde] pub struct CovenantPartiesConfig { pub party_a: CovenantParty, @@ -290,21 +295,29 @@ pub struct CovenantPartiesConfig { impl CovenantPartiesConfig { pub fn get_response_attributes(self) -> Vec { - let mut attrs = vec![ + let mut attrs = vec![ Attribute::new("party_a_address", self.party_a.addr), Attribute::new("party_a_denom", self.party_a.provided_denom), Attribute::new("party_b_address", self.party_b.addr), Attribute::new("party_b_denom", self.party_b.provided_denom), ]; - attrs.extend(self.party_a.refund_config.get_response_attributes("party_a_".to_string())); - attrs.extend(self.party_b.refund_config.get_response_attributes("party_b_".to_string())); + attrs.extend( + self.party_a + .refund_config + .get_response_attributes("party_a_".to_string()), + ); + attrs.extend( + self.party_b + .refund_config + .get_response_attributes("party_b_".to_string()), + ); attrs } } #[cw_serde] pub enum CovenantTerms { - TokenSwap(SwapCovenantTerms) + TokenSwap(SwapCovenantTerms), } #[cw_serde] @@ -326,4 +339,4 @@ impl CovenantTerms { } } } -} \ No newline at end of file +} From bab7a038787f90bc7bb1bea7a98064996171fde3 Mon Sep 17 00:00:00 2001 From: bekauz Date: Sun, 27 Aug 2023 19:12:04 +0200 Subject: [PATCH 057/586] init interchain splitter --- Cargo.lock | 458 ++++++++++++------ Cargo.toml | 1 + contracts/interchain-splitter/.cargo/config | 3 + contracts/interchain-splitter/Cargo.toml | 33 ++ contracts/interchain-splitter/README.md | 6 + contracts/interchain-splitter/src/contract.rs | 46 ++ contracts/interchain-splitter/src/error.rs | 10 + contracts/interchain-splitter/src/lib.rs | 12 + contracts/interchain-splitter/src/msg.rs | 13 + contracts/interchain-splitter/src/state.rs | 9 + .../interchain-splitter/src/suite_test/mod.rs | 2 + .../src/suite_test/suite.rs | 1 + .../src/suite_test/tests.rs | 1 + contracts/native-splitter/README.md | 2 + 14 files changed, 457 insertions(+), 140 deletions(-) create mode 100644 contracts/interchain-splitter/.cargo/config create mode 100644 contracts/interchain-splitter/Cargo.toml create mode 100644 contracts/interchain-splitter/README.md create mode 100644 contracts/interchain-splitter/src/contract.rs create mode 100644 contracts/interchain-splitter/src/error.rs create mode 100644 contracts/interchain-splitter/src/lib.rs create mode 100644 contracts/interchain-splitter/src/msg.rs create mode 100644 contracts/interchain-splitter/src/state.rs create mode 100644 contracts/interchain-splitter/src/suite_test/mod.rs create mode 100644 contracts/interchain-splitter/src/suite_test/suite.rs create mode 100644 contracts/interchain-splitter/src/suite_test/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 801c6622..c87c4b94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,9 +15,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "astroport" @@ -36,8 +36,8 @@ dependencies = [ [[package]] name = "astroport" -version = "3.1.2" -source = "git+https://github.com/astroport-fi/astroport-core.git#d227ed3512f1094c11f54beaec27647387c0d7ba" +version = "3.6.0" +source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" dependencies = [ "astroport-circular-buffer", "cosmwasm-schema", @@ -53,7 +53,7 @@ dependencies = [ [[package]] name = "astroport-circular-buffer" version = "0.1.0" -source = "git+https://github.com/astroport-fi/astroport-core.git#d227ed3512f1094c11f54beaec27647387c0d7ba" +source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -63,10 +63,10 @@ dependencies = [ [[package]] name = "astroport-factory" -version = "1.5.1" -source = "git+https://github.com/astroport-fi/astroport-core.git#d227ed3512f1094c11f54beaec27647387c0d7ba" +version = "1.6.0" +source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" dependencies = [ - "astroport 3.1.2", + "astroport 3.6.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -80,9 +80,9 @@ dependencies = [ [[package]] name = "astroport-native-coin-registry" version = "1.0.1" -source = "git+https://github.com/astroport-fi/astroport-core.git#d227ed3512f1094c11f54beaec27647387c0d7ba" +source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" dependencies = [ - "astroport 3.1.2", + "astroport 3.6.0", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", @@ -93,10 +93,10 @@ dependencies = [ [[package]] name = "astroport-pair-stable" -version = "3.0.0" -source = "git+https://github.com/astroport-fi/astroport-core.git#d227ed3512f1094c11f54beaec27647387c0d7ba" +version = "3.3.0" +source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" dependencies = [ - "astroport 3.1.2", + "astroport 3.6.0", "astroport-circular-buffer", "cosmwasm-schema", "cosmwasm-std", @@ -111,9 +111,9 @@ dependencies = [ [[package]] name = "astroport-token" version = "1.1.1" -source = "git+https://github.com/astroport-fi/astroport-core.git#d227ed3512f1094c11f54beaec27647387c0d7ba" +source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" dependencies = [ - "astroport 3.1.2", + "astroport 3.6.0", "cosmwasm-schema", "cosmwasm-std", "cw2 0.15.1", @@ -125,9 +125,9 @@ dependencies = [ [[package]] name = "astroport-whitelist" version = "1.0.1" -source = "git+https://github.com/astroport-fi/astroport-core.git#d227ed3512f1094c11f54beaec27647387c0d7ba" +source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" dependencies = [ - "astroport 3.1.2", + "astroport 3.6.0", "cosmwasm-schema", "cosmwasm-std", "cw1-whitelist 0.15.1", @@ -147,6 +147,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" @@ -155,9 +161,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.20.0" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "base64ct" @@ -189,6 +195,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bnum" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128a44527fc0d6abf05f9eda748b9027536e12dff93f5acc8449f51583309350" + [[package]] name = "byteorder" version = "1.4.3" @@ -197,9 +209,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" dependencies = [ "serde", ] @@ -212,9 +224,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "const-oid" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6340df57935414636969091153f35f68d9f00bbc8fb4a9c6054706c213e6c6bc" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" [[package]] name = "cosmos-sdk-proto" @@ -240,31 +252,31 @@ dependencies = [ [[package]] name = "cosmwasm-crypto" -version = "1.2.7" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb64554a91d6a9231127f4355d351130a0b94e663d5d9dc8b3a54ca17d83de49" +checksum = "1ca101fbf2f76723711a30ea3771ef312ec3ec254ad021b237871ed802f9f175" dependencies = [ "digest 0.10.7", "ed25519-zebra", - "k256", + "k256 0.13.1", "rand_core 0.6.4", "thiserror", ] [[package]] name = "cosmwasm-derive" -version = "1.2.7" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0fb2ce09f41a3dae1a234d56a9988f9aff4c76441cd50ef1ee9a4f20415b028" +checksum = "c73d2dd292f60e42849d2b07c03d809cf31e128a4299a805abd6d24553bcaaf5" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.2.7" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230e5d1cefae5331db8934763c81b9c871db6a2cd899056a5694fa71d292c815" +checksum = "6ce34a08020433989af5cc470104f6bd22134320fe0221bd8aeb919fd5ec92d5" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -275,9 +287,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.2.7" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43dadf7c23406cb28079d69e6cb922c9c29b9157b0fe887e3b79c783b7d4bcb8" +checksum = "96694ec781a7dd6dea1f968a2529ade009c21ad999c88b5f53d6cc495b3b96f7" dependencies = [ "proc-macro2", "quote", @@ -286,11 +298,12 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.2.7" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4337eef8dfaf8572fe6b6b415d6ec25f9308c7bb09f2da63789209fb131363be" +checksum = "2a44d3f9c25b2f864737c6605a98f2e4675d53fd8bbc7cf4d7c02475661a793d" dependencies = [ - "base64 0.13.1", + "base64 0.21.4", + "bnum", "cosmwasm-crypto", "cosmwasm-derive", "derivative", @@ -299,16 +312,15 @@ dependencies = [ "schemars", "serde", "serde-json-wasm 0.5.1", - "sha2 0.10.7", + "sha2 0.10.8", "thiserror", - "uint", ] [[package]] name = "cosmwasm-storage" -version = "1.2.7" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8601d284db8776e39fe99b3416516c5636ca73cef14666b7bb9648ca32c4b89" +checksum = "ab544dfcad7c9e971933d522d99ec75cc8ddfa338854bb992b092e11bcd7e818" dependencies = [ "cosmwasm-std", "serde", @@ -326,7 +338,7 @@ dependencies = [ "cw-fifo", "cw-multi-test", "cw-storage-plus 1.1.0", - "cw2 1.1.0", + "cw2 1.1.1", "neutron-sdk", "serde", "thiserror", @@ -339,7 +351,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", - "cw2 1.1.0", + "cw2 1.1.1", "thiserror", ] @@ -362,7 +374,7 @@ dependencies = [ "cw-multi-test", "cw-storage-plus 1.1.0", "cw-utils 1.0.1", - "cw2 1.1.0", + "cw2 1.1.1", "neutron-sdk", "prost 0.11.9", "prost-types", @@ -370,7 +382,7 @@ dependencies = [ "schemars", "serde", "serde-json-wasm 0.4.1", - "sha2 0.10.7", + "sha2 0.10.8", "thiserror", ] @@ -391,7 +403,7 @@ dependencies = [ "cw-multi-test", "cw-storage-plus 1.1.0", "cw-utils 1.0.1", - "cw2 1.1.0", + "cw2 1.1.1", "neutron-sdk", "prost 0.11.9", "prost-types", @@ -399,7 +411,7 @@ dependencies = [ "schemars", "serde", "serde-json-wasm 0.4.1", - "sha2 0.10.7", + "sha2 0.10.8", "thiserror", ] @@ -418,7 +430,7 @@ dependencies = [ "cosmwasm-std", "cw-multi-test", "cw-storage-plus 1.1.0", - "cw2 1.1.0", + "cw2 1.1.1", "cw20 0.15.1", "serde", "thiserror", @@ -441,7 +453,7 @@ dependencies = [ "cw-multi-test", "cw-storage-plus 1.1.0", "cw-utils 1.0.1", - "cw2 1.1.0", + "cw2 1.1.1", "neutron-sdk", "prost 0.11.9", "prost-types", @@ -449,7 +461,7 @@ dependencies = [ "schemars", "serde", "serde-json-wasm 0.4.1", - "sha2 0.10.7", + "sha2 0.10.8", "thiserror", ] @@ -470,7 +482,7 @@ dependencies = [ "cw-multi-test", "cw-storage-plus 1.1.0", "cw-utils 1.0.1", - "cw2 1.1.0", + "cw2 1.1.1", "neutron-sdk", "prost 0.11.9", "prost-types", @@ -478,7 +490,27 @@ dependencies = [ "schemars", "serde", "serde-json-wasm 0.4.1", - "sha2 0.10.7", + "sha2 0.10.8", + "thiserror", +] + +[[package]] +name = "covenant-interchain-splitter" +version = "1.0.0" +dependencies = [ + "cosmos-sdk-proto 0.14.0", + "cosmwasm-schema", + "cosmwasm-std", + "covenant-clock", + "covenant-macros", + "covenant-utils", + "cw-storage-plus 1.1.0", + "cw2 1.1.1", + "neutron-sdk", + "protobuf 3.2.0", + "schemars", + "serde", + "serde-json-wasm 0.4.1", "thiserror", ] @@ -504,7 +536,7 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw-utils 1.0.1", "cw1-whitelist 1.1.0", - "cw2 1.1.0", + "cw2 1.1.1", "cw20 0.15.1", "neutron-sdk", "prost 0.11.9", @@ -513,7 +545,7 @@ dependencies = [ "schemars", "serde", "serde-json-wasm 0.4.1", - "sha2 0.10.7", + "sha2 0.10.8", "thiserror", ] @@ -528,7 +560,7 @@ dependencies = [ "covenant-macros", "covenant-utils", "cw-storage-plus 1.1.0", - "cw2 1.1.0", + "cw2 1.1.1", "neutron-sdk", "protobuf 3.2.0", "schemars", @@ -559,7 +591,7 @@ dependencies = [ "covenant-macros", "covenant-utils", "cw-storage-plus 1.1.0", - "cw2 1.1.0", + "cw2 1.1.1", "neutron-sdk", "protobuf 3.2.0", "schemars", @@ -581,7 +613,7 @@ dependencies = [ "covenant-utils", "cw-multi-test", "cw-storage-plus 1.1.0", - "cw2 1.1.0", + "cw2 1.1.1", "cw20 0.15.1", "serde", "thiserror", @@ -625,6 +657,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-bigint" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -670,7 +714,7 @@ dependencies = [ "cw-utils 1.0.1", "derivative", "itertools", - "k256", + "k256 0.11.6", "prost 0.9.0", "schemars", "serde", @@ -722,7 +766,7 @@ checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw2 1.1.0", + "cw2 1.1.1", "schemars", "semver", "serde", @@ -743,9 +787,9 @@ dependencies = [ [[package]] name = "cw1" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f00d6dbf69d79809b86c144d7c322844a431568d4803ded466ed547dc393208" +checksum = "4cd87b521055960569f562a143078c93031d5e8780d9520936cc97f8aa2f1101" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -780,8 +824,8 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus 1.1.0", "cw-utils 1.0.1", - "cw1 1.1.0", - "cw2 1.1.0", + "cw1 1.1.1", + "cw2 1.1.1", "schemars", "serde", "thiserror", @@ -802,9 +846,9 @@ dependencies = [ [[package]] name = "cw2" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ac2dc7a55ad64173ca1e0a46697c31b7a5c51342f55a1e84a724da4eb99908" +checksum = "9431d14f64f49e41c6ef5561ed11a5391c417d0cb16455dea8cdcb9037a8d197" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -829,9 +873,9 @@ dependencies = [ [[package]] name = "cw20" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "011c45920f8200bd5d32d4fe52502506f64f2f75651ab408054d4cfc75ca3a9b" +checksum = "786e9da5e937f473cecd2463e81384c1af65d0f6398bbd851be7655487c55492" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -860,14 +904,14 @@ dependencies = [ [[package]] name = "cw3" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171af3d9127de6805a7dd819fb070c7d2f6c3ea85f4193f42cef259f0a7f33d5" +checksum = "1d056ec33ec146554aa1d16c9535763341db75589a47743c006c377e62b54034" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-utils 1.0.1", - "cw20 1.1.0", + "cw20 1.1.1", "schemars", "serde", "thiserror", @@ -883,6 +927,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "derivative" version = "2.2.0" @@ -910,6 +964,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid", "crypto-common", "subtle", ] @@ -922,9 +977,9 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dyn-clone" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" +checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" [[package]] name = "ecdsa" @@ -932,10 +987,24 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ - "der", - "elliptic-curve", - "rfc6979", - "signature", + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + +[[package]] +name = "ecdsa" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +dependencies = [ + "der 0.7.8", + "digest 0.10.7", + "elliptic-curve 0.13.5", + "rfc6979 0.4.0", + "signature 2.1.0", + "spki 0.7.2", ] [[package]] @@ -955,9 +1024,9 @@ dependencies = [ [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "elliptic-curve" @@ -965,16 +1034,35 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" dependencies = [ - "base16ct", - "crypto-bigint", - "der", + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", + "digest 0.10.7", + "ff 0.12.1", + "generic-array", + "group 0.12.1", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "sec1 0.3.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.3", "digest 0.10.7", - "ff", + "ff 0.13.0", "generic-array", - "group", - "pkcs8", + "group 0.13.0", + "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1", + "sec1 0.7.3", "subtle", "zeroize", ] @@ -989,6 +1077,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "flex-error" version = "0.4.4" @@ -1012,6 +1110,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1031,7 +1130,18 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ - "ff", + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.0", "rand_core 0.6.4", "subtle", ] @@ -1071,9 +1181,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "k256" @@ -1082,23 +1192,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" dependencies = [ "cfg-if", - "ecdsa", - "elliptic-curve", - "sha2 0.10.7", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2 0.10.8", +] + +[[package]] +name = "k256" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +dependencies = [ + "cfg-if", + "ecdsa 0.16.8", + "elliptic-curve 0.13.5", + "once_cell", + "sha2 0.10.8", + "signature 2.1.0", ] [[package]] name = "libc" -version = "0.2.147" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "neutron-sdk" -version = "0.5.0" -source = "git+https://github.com/neutron-org/neutron-sdk#141d4b7c45c45b655a60d0d7cfee8a8e4e49f455" +version = "0.6.1" +source = "git+https://github.com/neutron-org/neutron-sdk#31af063f06bb90d46081a944a7df085d8f2ab493" dependencies = [ - "base64 0.20.0", + "base64 0.21.4", "bech32", "cosmos-sdk-proto 0.16.0", "cosmwasm-schema", @@ -1108,7 +1232,7 @@ dependencies = [ "protobuf 3.2.0", "schemars", "serde", - "serde-json-wasm 0.4.1", + "serde-json-wasm 0.5.1", "serde_json", "thiserror", ] @@ -1126,9 +1250,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", ] @@ -1156,9 +1280,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "paste" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pkcs8" @@ -1166,15 +1290,25 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ - "der", - "spki", + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.8", + "spki 0.7.2", ] [[package]] name = "proc-macro2" -version = "1.0.63" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -1266,9 +1400,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.29" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -1294,22 +1428,32 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" dependencies = [ - "crypto-bigint", + "crypto-bigint 0.4.9", "hmac", "zeroize", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ryu" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "schemars" -version = "0.8.12" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" +checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c" dependencies = [ "dyn-clone", "schemars_derive", @@ -1319,9 +1463,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.12" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c" +checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c" dependencies = [ "proc-macro2", "quote", @@ -1335,25 +1479,39 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ - "base16ct", - "der", + "base16ct 0.1.1", + "der 0.6.1", + "generic-array", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.8", "generic-array", - "pkcs8", + "pkcs8 0.10.2", "subtle", "zeroize", ] [[package]] name = "semver" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" [[package]] name = "serde" -version = "1.0.166" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] @@ -1378,22 +1536,22 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a16be4fe5320ade08736447e3198294a5ea9a6d44dde6f35f0a5e06859c427a" +checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.166" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.37", ] [[package]] @@ -1409,9 +1567,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.100" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", @@ -1433,9 +1591,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -1452,6 +1610,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "snafu" version = "0.6.10" @@ -1480,7 +1648,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ "base64ct", - "der", + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der 0.7.8", ] [[package]] @@ -1517,9 +1695,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.23" +version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", @@ -1564,22 +1742,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.41" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c16a64ba9387ef3fdae4f9c1a7f07a0997fce91985c0336f1ddc1822b3b37802" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.41" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d14928354b01c4d6a4f0e549069adef399a284e7995c7ccca94e8a07a5346c59" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.37", ] [[package]] @@ -1601,9 +1779,9 @@ checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uint" @@ -1619,9 +1797,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "version_check" diff --git a/Cargo.toml b/Cargo.toml index 66100d11..f07df102 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ covenant-covenant = { path = "contracts/covenant" } covenant-holder = { path = "contracts/holder" } covenant-ibc-forwarder = { path = "contracts/ibc-forwarder" } covenant-native-splitter = { path = "contracts/native-splitter" } +covenant-interchain-splitter = { path = "contract/interchain-splitter" } # packages clock-derive = { path = "packages/clock-derive" } diff --git a/contracts/interchain-splitter/.cargo/config b/contracts/interchain-splitter/.cargo/config new file mode 100644 index 00000000..6a6f2852 --- /dev/null +++ b/contracts/interchain-splitter/.cargo/config @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +schema = "run --bin schema" \ No newline at end of file diff --git a/contracts/interchain-splitter/Cargo.toml b/contracts/interchain-splitter/Cargo.toml new file mode 100644 index 00000000..505e7c13 --- /dev/null +++ b/contracts/interchain-splitter/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "covenant-interchain-splitter" +authors = ["benskey bekauz@protonmail.com"] +description = "Interchain Splitter module for covenants" +edition = { workspace = true } +license = { workspace = true } +rust-version = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# disables #[entry_point] (i.e. instantiate/execute/query) export +library = [] + +[dependencies] +covenant-macros = { workspace = true } +covenant-clock = { workspace = true, features=["library"] } +covenant-utils = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +thiserror = { workspace = true } +schemars = { workspace = true } +serde-json-wasm = { workspace = true } +serde = { workspace = true } +neutron-sdk = { workspace = true } +cosmos-sdk-proto = { workspace = true } +protobuf = { workspace = true } diff --git a/contracts/interchain-splitter/README.md b/contracts/interchain-splitter/README.md new file mode 100644 index 00000000..8d1b0df9 --- /dev/null +++ b/contracts/interchain-splitter/README.md @@ -0,0 +1,6 @@ +# Interchain Splitter + +Interchain Splitter is a contract meant to facilitate a pre-agreed upon way of distributing funds at its disposal. + +Splitter should remain agnostic to any price changes that may occur during the covenant lifecycle. +It should accept the tokens and distribute them according to the initial agreement. diff --git a/contracts/interchain-splitter/src/contract.rs b/contracts/interchain-splitter/src/contract.rs new file mode 100644 index 00000000..8a588df9 --- /dev/null +++ b/contracts/interchain-splitter/src/contract.rs @@ -0,0 +1,46 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + DepsMut, Env, MessageInfo, + Response, +}; +use cw2::set_contract_version; + +use crate::error::ContractError; +use crate::msg::{InstantiateMsg, ExecuteMsg}; + +const CONTRACT_NAME: &str = "crates.io:covenant-interchain-splitter"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + deps.api.debug("WASMDEBUG: instantiate"); + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::default() + .add_attribute("method", "interchain_splitter_instantiate") + ) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + deps.api + .debug(format!("WASMDEBUG: execute: received msg: {msg:?}").as_str()); + match msg { + ExecuteMsg::Tick {} => try_tick(deps, env, info), + } +} + +fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + Ok(Response::default()) +} diff --git a/contracts/interchain-splitter/src/error.rs b/contracts/interchain-splitter/src/error.rs new file mode 100644 index 00000000..7683ef87 --- /dev/null +++ b/contracts/interchain-splitter/src/error.rs @@ -0,0 +1,10 @@ + +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + +} diff --git a/contracts/interchain-splitter/src/lib.rs b/contracts/interchain-splitter/src/lib.rs new file mode 100644 index 00000000..3b47dbd4 --- /dev/null +++ b/contracts/interchain-splitter/src/lib.rs @@ -0,0 +1,12 @@ +#![warn(clippy::unwrap_used, clippy::expect_used)] + +extern crate core; + +pub mod contract; +pub mod error; +pub mod msg; +pub mod state; + +#[allow(clippy::unwrap_used)] +#[cfg(test)] +mod suite_test; diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs new file mode 100644 index 00000000..2432fa13 --- /dev/null +++ b/contracts/interchain-splitter/src/msg.rs @@ -0,0 +1,13 @@ +use cosmwasm_schema::cw_serde; +use covenant_macros::clocked; + + +#[cw_serde] +pub struct InstantiateMsg { +} + + +#[clocked] +#[cw_serde] +pub enum ExecuteMsg { +} \ No newline at end of file diff --git a/contracts/interchain-splitter/src/state.rs b/contracts/interchain-splitter/src/state.rs new file mode 100644 index 00000000..68185eac --- /dev/null +++ b/contracts/interchain-splitter/src/state.rs @@ -0,0 +1,9 @@ +use cosmwasm_std::{Addr, Uint128}; +use cw_storage_plus::{Item, Map}; + +/// clock module address to verify the sender of incoming ticks +pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); + + +// maps a denom string to a vec of SplitReceivers +// pub const SPLIT_CONFIG_MAP: Map> = Map::new("split_config"); diff --git a/contracts/interchain-splitter/src/suite_test/mod.rs b/contracts/interchain-splitter/src/suite_test/mod.rs new file mode 100644 index 00000000..7b881830 --- /dev/null +++ b/contracts/interchain-splitter/src/suite_test/mod.rs @@ -0,0 +1,2 @@ +mod suite; +mod tests; diff --git a/contracts/interchain-splitter/src/suite_test/suite.rs b/contracts/interchain-splitter/src/suite_test/suite.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/contracts/interchain-splitter/src/suite_test/suite.rs @@ -0,0 +1 @@ + diff --git a/contracts/interchain-splitter/src/suite_test/tests.rs b/contracts/interchain-splitter/src/suite_test/tests.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/contracts/interchain-splitter/src/suite_test/tests.rs @@ -0,0 +1 @@ + diff --git a/contracts/native-splitter/README.md b/contracts/native-splitter/README.md index 48b7105c..18569e13 100644 --- a/contracts/native-splitter/README.md +++ b/contracts/native-splitter/README.md @@ -9,3 +9,5 @@ During instantiation, a vector of forwarder modules along with their respective The forwarder modules are then queried for their deposit addresses, which are going to be their respective ICA addresses. A combined `BankSend` is then performed to the ICAs on the same remote chain. If it suceeds, native splitter completes. + +todo: should this be called remote-chain-splitter ~? From 613cc8611e6c76079e94ddf916e1d8cdbb5246d3 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 29 Aug 2023 14:51:45 +0200 Subject: [PATCH 058/586] base distribution --- contracts/interchain-splitter/README.md | 27 ++++ contracts/interchain-splitter/src/contract.rs | 33 ++++- contracts/interchain-splitter/src/error.rs | 2 + contracts/interchain-splitter/src/msg.rs | 120 +++++++++++++++++- contracts/interchain-splitter/src/state.rs | 8 +- 5 files changed, 181 insertions(+), 9 deletions(-) diff --git a/contracts/interchain-splitter/README.md b/contracts/interchain-splitter/README.md index 8d1b0df9..ba496226 100644 --- a/contracts/interchain-splitter/README.md +++ b/contracts/interchain-splitter/README.md @@ -4,3 +4,30 @@ Interchain Splitter is a contract meant to facilitate a pre-agreed upon way of d Splitter should remain agnostic to any price changes that may occur during the covenant lifecycle. It should accept the tokens and distribute them according to the initial agreement. + +## Split Configurations + +In general, we support a per-denom configuration as follows: +``` +OSMO -> [(osmo12323, 40), (cosmos32121, 60)] +ATOM -> [(osmo12323, 50), (cosmo32121, 50)] +USDC -> [timewave_split] +_ -> [(osmo12323, 30), (cosmo32121, 70)] +``` + +### Timewave Split + +Timewave split provides a preconfigured list of addresses. + +### Custom Split + +A custom split here refers to a list of addresses with their associated share of the split (in %). +In this example `OSMO -> [(osmo12323, 40), (cosmos32121, 60)]`, all OSMO tokens that the splitter +receives will be split between osmo12323 and cosmos32121, with 40% and 60% shares respectively. +Custom split configuration should always add up to 100 or else an error is returned. + +### Wildcard Split + +For cases where denoms don't really matter, a wildcard split can be provided. Then any denoms that +the splitter holds that do not fall under any of other configurations will be split according to this. + diff --git a/contracts/interchain-splitter/src/contract.rs b/contracts/interchain-splitter/src/contract.rs index 8a588df9..27219996 100644 --- a/contracts/interchain-splitter/src/contract.rs +++ b/contracts/interchain-splitter/src/contract.rs @@ -2,12 +2,13 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ DepsMut, Env, MessageInfo, - Response, + Response, Order, CosmosMsg, }; use cw2::set_contract_version; use crate::error::ContractError; use crate::msg::{InstantiateMsg, ExecuteMsg}; +use crate::state::SPLIT_CONFIG_MAP; const CONTRACT_NAME: &str = "crates.io:covenant-interchain-splitter"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -22,6 +23,12 @@ pub fn instantiate( deps.api.debug("WASMDEBUG: instantiate"); set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + // we validate the splits and store them per-denom + for (denom, split) in msg.splits { + let validated_split = split.validate_to_split_config()?; + SPLIT_CONFIG_MAP.save(deps.storage, denom, &validated_split)?; + } + Ok(Response::default() .add_attribute("method", "interchain_splitter_instantiate") ) @@ -37,10 +44,26 @@ pub fn execute( deps.api .debug(format!("WASMDEBUG: execute: received msg: {msg:?}").as_str()); match msg { - ExecuteMsg::Tick {} => try_tick(deps, env, info), + ExecuteMsg::Tick {} => try_distribute(deps, env, info), } } -fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> Result { - Ok(Response::default()) -} +pub fn try_distribute(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + // first we query the contract balances + let balances = deps.querier.query_all_balances(env.contract.address)?; + let mut distribution_messages: Vec = vec![]; + // then we iterate over our split config and try to match the entries to available balances + for entry in SPLIT_CONFIG_MAP.range(deps.storage, None, None, Order::Ascending) { + let (denom, config) = entry?; + + // if we have the denom in our balances we construct the split messages + if let Some(coin) = balances.iter().find(|c| c.denom == denom) { + let mut transfer_messages = config.get_transfer_messages(coin.amount, coin.denom.to_string())?; + distribution_messages.append(&mut transfer_messages); + } + } + + Ok(Response::default() + .add_messages(distribution_messages) + ) +} \ No newline at end of file diff --git a/contracts/interchain-splitter/src/error.rs b/contracts/interchain-splitter/src/error.rs index 7683ef87..3faacc5c 100644 --- a/contracts/interchain-splitter/src/error.rs +++ b/contracts/interchain-splitter/src/error.rs @@ -7,4 +7,6 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("misconfigured split")] + SplitMisconfig {}, } diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs index 2432fa13..ba3d2018 100644 --- a/contracts/interchain-splitter/src/msg.rs +++ b/contracts/interchain-splitter/src/msg.rs @@ -1,13 +1,131 @@ use cosmwasm_schema::cw_serde; use covenant_macros::clocked; +use cosmwasm_std::{IbcTimeout, Uint128, CosmosMsg, BankMsg, IbcMsg, Coin}; +use crate::error::ContractError; #[cw_serde] pub struct InstantiateMsg { + pub splits: Vec<(String, SplitType)>, } #[clocked] #[cw_serde] pub enum ExecuteMsg { -} \ No newline at end of file +} + +// for every receiver we need a few things: +#[cw_serde] +pub struct InterchainReceiver { + // 1. remote chain channel id + pub channel_id: String, + // 2. receiver address + pub address: String, + // 3. timeout info + pub ibc_timeout: IbcTimeout, +} + +#[cw_serde] +pub struct NativeReceiver { + pub address: String, +} + +#[cw_serde] +pub enum ReceiverType { + Interchain(InterchainReceiver), + Native(NativeReceiver), +} + +#[cw_serde] +pub enum SplitType { + Custom(SplitConfig), + TimewaveSplit, +} + +impl SplitType { + pub fn validate_to_split_config(self) -> Result { + match self { + SplitType::Custom(c) => c.validate(), + SplitType::TimewaveSplit => { + Ok(SplitConfig { + receivers: vec![( + ReceiverType::Native(NativeReceiver { address: "todo".to_string() }), + Uint128::new(100) + )], + }) + }, + } + } +} + +#[cw_serde] +pub struct SplitConfig { + pub receivers: Vec<(ReceiverType, Uint128)>, +} + +impl SplitConfig { + pub fn validate(self) -> Result { + let total_share: Uint128 = self.receivers + .iter() + .map(|r| r.1) + .sum(); + + if total_share == Uint128::new(100) { + Ok(self) + } else { + Err(ContractError::SplitMisconfig {}) + } + } + + /* + RefundConfig::Native(addr) => CosmosMsg::Bank(BankMsg::Send { + to_address: addr.to_string(), + amount: vec![Coin { + denom: self.provided_denom, + amount, + }], + }), + RefundConfig::Ibc(r_c_i) => CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: r_c_i.channel_id, + to_address: self.addr.to_string(), + amount: Coin { + denom: self.provided_denom, + amount, + }, + timeout: IbcTimeout::with_timestamp( + block.time.plus_seconds(r_c_i.ibc_transfer_timeout.u64()), + ), + }), + */ + pub fn get_transfer_messages(self, amount: Uint128, denom: String) -> Result, ContractError> { + let mut msgs: Vec = vec![]; + + for (receiver_type, share) in self.receivers { + let entitlement = amount.checked_multiply_ratio( + share, + Uint128::new(100), + ).map_err(|_| ContractError::SplitMisconfig {})?; + + let amount = Coin { + denom: denom.to_string(), + amount: entitlement, + }; + let msg = match receiver_type { + ReceiverType::Interchain(receiver) => CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: receiver.channel_id, + to_address: receiver.address, + amount, + timeout: receiver.ibc_timeout, + }), + ReceiverType::Native(receiver) => CosmosMsg::Bank(BankMsg::Send { + to_address: receiver.address, + amount: vec![amount], + }), + }; + msgs.push(msg); + } + + Ok(msgs) + } +} diff --git a/contracts/interchain-splitter/src/state.rs b/contracts/interchain-splitter/src/state.rs index 68185eac..b2268f5d 100644 --- a/contracts/interchain-splitter/src/state.rs +++ b/contracts/interchain-splitter/src/state.rs @@ -1,9 +1,11 @@ -use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::Addr; use cw_storage_plus::{Item, Map}; +use crate::msg::SplitConfig; + /// clock module address to verify the sender of incoming ticks pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); - // maps a denom string to a vec of SplitReceivers -// pub const SPLIT_CONFIG_MAP: Map> = Map::new("split_config"); +pub const SPLIT_CONFIG_MAP: Map = Map::new("split_config"); + From 8d99dd44cca4b338d48ee7fadbcf083e1ae82827 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 29 Aug 2023 22:51:47 +0200 Subject: [PATCH 059/586] test suite; fallback token distribution --- Cargo.lock | 2 + contracts/interchain-splitter/Cargo.toml | 4 + contracts/interchain-splitter/src/contract.rs | 93 +++++++-- contracts/interchain-splitter/src/msg.rs | 63 ++++--- .../interchain-splitter/src/suite_test/mod.rs | 51 +++++ .../src/suite_test/suite.rs | 178 ++++++++++++++++++ .../src/suite_test/tests.rs | 170 +++++++++++++++++ 7 files changed, 521 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c87c4b94..35d19184 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -498,12 +498,14 @@ dependencies = [ name = "covenant-interchain-splitter" version = "1.0.0" dependencies = [ + "anyhow", "cosmos-sdk-proto 0.14.0", "cosmwasm-schema", "cosmwasm-std", "covenant-clock", "covenant-macros", "covenant-utils", + "cw-multi-test", "cw-storage-plus 1.1.0", "cw2 1.1.1", "neutron-sdk", diff --git a/contracts/interchain-splitter/Cargo.toml b/contracts/interchain-splitter/Cargo.toml index 505e7c13..3a81624d 100644 --- a/contracts/interchain-splitter/Cargo.toml +++ b/contracts/interchain-splitter/Cargo.toml @@ -31,3 +31,7 @@ serde = { workspace = true } neutron-sdk = { workspace = true } cosmos-sdk-proto = { workspace = true } protobuf = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +anyhow = { workspace = true } diff --git a/contracts/interchain-splitter/src/contract.rs b/contracts/interchain-splitter/src/contract.rs index 27219996..b58a6fa1 100644 --- a/contracts/interchain-splitter/src/contract.rs +++ b/contracts/interchain-splitter/src/contract.rs @@ -1,14 +1,15 @@ +use cosmwasm_schema::cw_serde; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ DepsMut, Env, MessageInfo, - Response, Order, CosmosMsg, + Response, Order, CosmosMsg, Deps, StdResult, Binary, to_binary, StdError, }; use cw2::set_contract_version; use crate::error::ContractError; -use crate::msg::{InstantiateMsg, ExecuteMsg}; -use crate::state::SPLIT_CONFIG_MAP; +use crate::msg::{InstantiateMsg, ExecuteMsg, QueryMsg, SplitConfig, ProtocolGuildQueryMsg}; +use crate::state::{SPLIT_CONFIG_MAP, CLOCK_ADDRESS}; const CONTRACT_NAME: &str = "crates.io:covenant-interchain-splitter"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -23,12 +24,24 @@ pub fn instantiate( deps.api.debug("WASMDEBUG: instantiate"); set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + let clock_addr = deps.api.addr_validate(&msg.clock_address)?; + CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; + + let fallback_split = if let Some(split) = msg.fallback_split { + split.validate_to_split_config()? + } else { + deps.querier.query_wasm_smart("contract0", &ProtocolGuildQueryMsg::PublicGoodsSplit {})? + }; + // we validate the splits and store them per-denom for (denom, split) in msg.splits { let validated_split = split.validate_to_split_config()?; SPLIT_CONFIG_MAP.save(deps.storage, denom, &validated_split)?; } + // store the fallback split under emtpy key to not match any denoms + SPLIT_CONFIG_MAP.save(deps.storage, String::default(), &fallback_split)?; + Ok(Response::default() .add_attribute("method", "interchain_splitter_instantiate") ) @@ -38,32 +51,90 @@ pub fn instantiate( pub fn execute( deps: DepsMut, env: Env, - info: MessageInfo, + _info: MessageInfo, msg: ExecuteMsg, ) -> Result { deps.api .debug(format!("WASMDEBUG: execute: received msg: {msg:?}").as_str()); match msg { - ExecuteMsg::Tick {} => try_distribute(deps, env, info), + ExecuteMsg::Tick {} => try_distribute(deps, env), } } -pub fn try_distribute(deps: DepsMut, env: Env, info: MessageInfo) -> Result { +pub fn try_distribute(deps: DepsMut, env: Env) -> Result { // first we query the contract balances - let balances = deps.querier.query_all_balances(env.contract.address)?; + let mut balances = deps.querier.query_all_balances(env.contract.address)?; let mut distribution_messages: Vec = vec![]; + // then we iterate over our split config and try to match the entries to available balances - for entry in SPLIT_CONFIG_MAP.range(deps.storage, None, None, Order::Ascending) { + for entry in SPLIT_CONFIG_MAP + .range(deps.storage, None, None, Order::Ascending) { let (denom, config) = entry?; + // skip the fallback config for later + if denom == String::default() { + continue; + } - // if we have the denom in our balances we construct the split messages - if let Some(coin) = balances.iter().find(|c| c.denom == denom) { + // we try to find the index of matching coin in available balances + let balances_index = balances.iter().position(|coin| coin.denom == denom); + if let Some(index) = balances_index { + // pop the relevant coin and build the transfer messages + let coin = balances.remove(index); let mut transfer_messages = config.get_transfer_messages(coin.amount, coin.denom.to_string())?; distribution_messages.append(&mut transfer_messages); } } - + + // by now all explicitly defined denom splits have been removed from the + // balances vector so we can take the remaining balances and distribute + // them according to the fallback split + let fallback_config = SPLIT_CONFIG_MAP.load(deps.storage, String::default())?; + // get the distribution messages and add them to the list + for leftover_bal in balances { + let mut fallback_messages = fallback_config + .clone() + .get_transfer_messages(leftover_bal.amount, leftover_bal.denom)?; + distribution_messages.append(&mut fallback_messages); + } + + Ok(Response::default() .add_messages(distribution_messages) ) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::ClockAddress{}=>Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), + QueryMsg::DenomSplit { denom } => Ok(to_binary(&query_split(deps, denom)?)?), + QueryMsg::Splits {} => Ok(to_binary(&query_all_splits(deps)?)?), + QueryMsg::FallbackSplit {} => Ok(to_binary(&query_split(deps, String::default())?)?), + } +} + +pub fn query_all_splits(deps: Deps) -> Result, StdError> { + + let mut splits: Vec<(String, SplitConfig)> = vec![]; + + for entry in SPLIT_CONFIG_MAP.range(deps.storage, None, None, Order::Ascending) { + let (denom, config) = entry?; + splits.push((denom, config)); + } + + Ok(splits) +} + +pub fn query_split(deps: Deps, denom: String) -> Result { + + for entry in SPLIT_CONFIG_MAP.range(deps.storage, None, None, Order::Ascending) { + let (entry_denom, config) = entry?; + if entry_denom == denom { + return Ok(config) + } + } + + Ok(SplitConfig { + receivers: vec![], + }) } \ No newline at end of file diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs index ba3d2018..a2c0447e 100644 --- a/contracts/interchain-splitter/src/msg.rs +++ b/contracts/interchain-splitter/src/msg.rs @@ -1,19 +1,24 @@ -use cosmwasm_schema::cw_serde; -use covenant_macros::clocked; -use cosmwasm_std::{IbcTimeout, Uint128, CosmosMsg, BankMsg, IbcMsg, Coin}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use covenant_macros::{clocked, covenant_clock_address}; +use cosmwasm_std::{IbcTimeout, Uint128, CosmosMsg, BankMsg, IbcMsg, Coin, Addr}; use crate::error::ContractError; #[cw_serde] pub struct InstantiateMsg { + /// address of the associated clock + pub clock_address: String, + /// list of (denom, split) configurations pub splits: Vec<(String, SplitType)>, + /// a split for all denoms that are not covered in the + /// regular `splits` list. If no fallback is provided, + /// we default to the timewave protocol guild split + pub fallback_split: Option, } - #[clocked] #[cw_serde] -pub enum ExecuteMsg { -} +pub enum ExecuteMsg {} // for every receiver we need a few things: #[cw_serde] @@ -48,6 +53,7 @@ impl SplitType { match self { SplitType::Custom(c) => c.validate(), SplitType::TimewaveSplit => { + // TODO: query the timewave split contract here Ok(SplitConfig { receivers: vec![( ReceiverType::Native(NativeReceiver { address: "todo".to_string() }), @@ -78,30 +84,10 @@ impl SplitConfig { } } - /* - RefundConfig::Native(addr) => CosmosMsg::Bank(BankMsg::Send { - to_address: addr.to_string(), - amount: vec![Coin { - denom: self.provided_denom, - amount, - }], - }), - RefundConfig::Ibc(r_c_i) => CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: r_c_i.channel_id, - to_address: self.addr.to_string(), - amount: Coin { - denom: self.provided_denom, - amount, - }, - timeout: IbcTimeout::with_timestamp( - block.time.plus_seconds(r_c_i.ibc_transfer_timeout.u64()), - ), - }), - */ pub fn get_transfer_messages(self, amount: Uint128, denom: String) -> Result, ContractError> { let mut msgs: Vec = vec![]; - for (receiver_type, share) in self.receivers { + for (receiver_type, share) in self.receivers.into_iter() { let entitlement = amount.checked_multiply_ratio( share, Uint128::new(100), @@ -116,10 +102,10 @@ impl SplitConfig { channel_id: receiver.channel_id, to_address: receiver.address, amount, - timeout: receiver.ibc_timeout, + timeout: receiver.ibc_timeout.clone(), }), ReceiverType::Native(receiver) => CosmosMsg::Bank(BankMsg::Send { - to_address: receiver.address, + to_address: receiver.address.to_string(), amount: vec![amount], }), }; @@ -129,3 +115,22 @@ impl SplitConfig { Ok(msgs) } } + +#[covenant_clock_address] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(SplitConfig)] + DenomSplit { denom: String }, + #[returns(Vec<(String, SplitConfig)>)] + Splits {}, + #[returns(SplitConfig)] + FallbackSplit {}, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum ProtocolGuildQueryMsg { + #[returns(SplitConfig)] + PublicGoodsSplit {}, +} \ No newline at end of file diff --git a/contracts/interchain-splitter/src/suite_test/mod.rs b/contracts/interchain-splitter/src/suite_test/mod.rs index 7b881830..b22597d2 100644 --- a/contracts/interchain-splitter/src/suite_test/mod.rs +++ b/contracts/interchain-splitter/src/suite_test/mod.rs @@ -1,2 +1,53 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Empty, to_binary, Env, Deps, StdResult, Binary, Uint128}; +use cw_multi_test::{Contract, ContractWrapper}; + +use crate::msg::{SplitConfig, ReceiverType, NativeReceiver}; + mod suite; mod tests; + + +pub fn splitter_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ); + Box::new(contract) +} + +pub fn mock_protocol_guild_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + query, + ); + Box::new(contract) +} + + +// timewave protocol guild mock +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(SplitConfig)] + PublicGoodsSplit {}, +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::PublicGoodsSplit {} => Ok(to_binary(&SplitConfig { + receivers: vec![ + ( + ReceiverType::Native(NativeReceiver { address: "save_the_cats".to_string()}), + Uint128::new(100), + ), + ] + })?), + } +} diff --git a/contracts/interchain-splitter/src/suite_test/suite.rs b/contracts/interchain-splitter/src/suite_test/suite.rs index 8b137891..bd9eda97 100644 --- a/contracts/interchain-splitter/src/suite_test/suite.rs +++ b/contracts/interchain-splitter/src/suite_test/suite.rs @@ -1 +1,179 @@ +use cosmwasm_std::{Addr, Uint128, Coin}; +use cw_multi_test::{App, Executor, AppResponse, SudoMsg}; +use crate::{msg::{InstantiateMsg, SplitType, SplitConfig, ReceiverType, NativeReceiver, ExecuteMsg, QueryMsg}}; + +use super::{splitter_contract, mock_protocol_guild_contract}; + +pub const ADMIN: &str = "admin"; + +pub const DENOM_A: &str = "denom_a"; +pub const DENOM_B: &str = "denom_b"; +pub const ALT_DENOM: &str = "alt_denom"; + +pub const PARTY_A_ADDR: &str = "party_a"; +pub const PARTY_B_ADDR: &str = "party_b"; + +pub const CLOCK_ADDR: &str = "clock_addr"; + + +pub fn get_equal_split_config() -> SplitConfig { + SplitConfig { + receivers: vec![ + ( + ReceiverType::Native(NativeReceiver { + address: PARTY_A_ADDR.to_string(), + }), + Uint128::new(50), + ), + ( + ReceiverType::Native(NativeReceiver { + address: PARTY_B_ADDR.to_string(), + }), + Uint128::new(50), + ), + ], + } +} + +pub fn get_public_goods_split_config() -> SplitConfig { + SplitConfig { receivers: vec![ + ( + ReceiverType::Native(NativeReceiver { address: "save_the_cats".to_string()}), + Uint128::new(100), + ) + ]} +} + +pub struct Suite { + pub app: App, + pub splitter: Addr, + pub protocol_guild: Addr, +} + +pub struct SuiteBuilder { + pub instantiate: InstantiateMsg, + pub app: App, +} + +impl Default for SuiteBuilder { + fn default() -> Self { + Self { + instantiate: InstantiateMsg { + clock_address: CLOCK_ADDR.to_string(), + splits: vec![ + ( + DENOM_A.to_string(), + SplitType::Custom(get_equal_split_config()), + ), + ( + DENOM_B.to_string(), + SplitType::Custom(get_equal_split_config()), + ), + ], + fallback_split: None, + }, + app: App::default(), + } + } +} + +impl SuiteBuilder { + pub fn with_custom_splits(mut self, splits: Vec<(String, SplitType)>) -> Self { + self.instantiate.splits = splits; + self + } + + pub fn build(mut self) -> Suite { + let mut app = self.app; + + let protocol_guild_code = app.store_code(mock_protocol_guild_contract()); + let mock_protocol_guild = app + .instantiate_contract( + protocol_guild_code, + Addr::unchecked(ADMIN), + &self.instantiate, + &[], + "protocol_guild", + Some(ADMIN.to_string()), + ) + .unwrap(); + + let splitter_code: u64 = app.store_code(splitter_contract()); + let splitter = app + .instantiate_contract( + splitter_code, + Addr::unchecked(ADMIN), + &self.instantiate, + &[], + "splitter", + Some(ADMIN.to_string()), + ) + .unwrap(); + Suite { + app, + splitter, + protocol_guild: mock_protocol_guild, + } + } +} + +// actions +impl Suite { + pub fn tick(&mut self, caller: &str) -> Result { + self.app.execute_contract( + Addr::unchecked(caller), + self.splitter.clone(), + &ExecuteMsg::Tick {}, + &[], + ) + } +} + +// queries +impl Suite { + pub fn query_clock_address(&self) -> Addr { + self.app + .wrap() + .query_wasm_smart(&self.splitter, &QueryMsg::ClockAddress {}) + .unwrap() + } + + pub fn query_denom_split(&self, denom: String) -> SplitConfig { + self.app + .wrap() + .query_wasm_smart(&self.splitter, &QueryMsg::DenomSplit { denom }) + .unwrap() + } + + pub fn query_all_splits(&self) -> Vec<(String, SplitConfig)> { + self.app + .wrap() + .query_wasm_smart(&self.splitter, &QueryMsg::Splits {}) + .unwrap() + } +} + +// helper +impl Suite { + pub fn pass_blocks(&mut self, n: u64) { + self.app.update_block(|mut b| b.height += n); + } + + pub fn fund_coin(&mut self, coin: Coin) -> AppResponse { + self.app + .sudo(SudoMsg::Bank(cw_multi_test::BankSudo::Mint { + to_address: self.splitter.to_string(), + amount: vec![coin], + })) + .unwrap() + } + + pub fn get_party_denom_balance(&self, denom: &str, party_addr: &str) -> Uint128 { + self.app + .wrap() + .query_balance(party_addr, denom) + .unwrap() + .amount + } +} \ No newline at end of file diff --git a/contracts/interchain-splitter/src/suite_test/tests.rs b/contracts/interchain-splitter/src/suite_test/tests.rs index 8b137891..992b39db 100644 --- a/contracts/interchain-splitter/src/suite_test/tests.rs +++ b/contracts/interchain-splitter/src/suite_test/tests.rs @@ -1 +1,171 @@ +use cosmwasm_std::{Uint128, Coin}; +use crate::{suite_test::suite::{get_equal_split_config, DENOM_B, CLOCK_ADDR, get_public_goods_split_config, ALT_DENOM}, msg::{SplitConfig, SplitType, NativeReceiver, ReceiverType}}; + +use super::suite::{SuiteBuilder, DENOM_A, PARTY_A_ADDR, PARTY_B_ADDR}; + + + +#[test] +fn test_instantiate_happy_and_query_all() { + let suite = SuiteBuilder::default().build(); + + let splits = suite.query_all_splits(); + let token_a_split = suite.query_denom_split(DENOM_A.to_string()); + let token_b_split = suite.query_denom_split(DENOM_B.to_string()); + let clock_addr = suite.query_clock_address(); + + assert_eq!(get_equal_split_config(), token_a_split); + assert_eq!(get_equal_split_config(), token_b_split); + assert_eq!(CLOCK_ADDR.to_string(), clock_addr); + assert_eq!( + vec![ + ("".to_string(), get_public_goods_split_config()), + (DENOM_A.to_string(), get_equal_split_config()), + (DENOM_B.to_string(), get_equal_split_config()), + ], + splits, + ); +} + +#[test] +#[should_panic(expected = "misconfigured split")] +fn test_instantiate_split_misconfig() { + SuiteBuilder::default() + .with_custom_splits(vec![( + DENOM_A.to_string(), + SplitType::Custom(SplitConfig { + receivers: vec![ + ( + ReceiverType::Native(NativeReceiver { address: PARTY_A_ADDR.to_string() }), + Uint128::new(50), + ), + ( + ReceiverType::Native(NativeReceiver { address: PARTY_B_ADDR.to_string() }), + Uint128::new(60), + ), + ] + }), + )]) + .build(); +} + +#[test] +fn test_distribute_equal_split() { + let mut suite = SuiteBuilder::default().build(); + + // fund the splitter with 100 of each denom + suite.fund_coin(Coin::new(100, DENOM_A)); + suite.fund_coin(Coin::new(100, DENOM_B)); + + suite.pass_blocks(10); + + // assert splitter is funded + let splitter_denom_a_bal = suite.get_party_denom_balance(DENOM_A, suite.splitter.as_str()); + let splitter_denom_b_bal = suite.get_party_denom_balance(DENOM_B, suite.splitter.as_str()); + assert_eq!(Uint128::new(100), splitter_denom_a_bal); + assert_eq!(Uint128::new(100), splitter_denom_b_bal); + + // tick initiates the distribution attempt + suite.tick(CLOCK_ADDR).unwrap(); + suite.pass_blocks(10); + + let party_a_denom_a_bal = suite.get_party_denom_balance(DENOM_A, PARTY_A_ADDR); + let party_a_denom_b_bal = suite.get_party_denom_balance(DENOM_B, PARTY_A_ADDR); + let party_b_denom_a_bal = suite.get_party_denom_balance(DENOM_A, PARTY_B_ADDR); + let party_b_denom_b_bal = suite.get_party_denom_balance(DENOM_B, PARTY_B_ADDR); + let splitter_denom_a_bal = suite.get_party_denom_balance(DENOM_A, suite.splitter.as_str()); + let splitter_denom_b_bal = suite.get_party_denom_balance(DENOM_B, suite.splitter.as_str()); + + assert_eq!(Uint128::new(50), party_a_denom_a_bal); + assert_eq!(Uint128::new(50), party_a_denom_b_bal); + assert_eq!(Uint128::new(50), party_b_denom_a_bal); + assert_eq!(Uint128::new(50), party_b_denom_b_bal); + assert_eq!(Uint128::zero(), splitter_denom_a_bal); + assert_eq!(Uint128::zero(), splitter_denom_b_bal); +} + +#[test] +fn test_distribute_token_swap() { + let mut suite = SuiteBuilder::default() + .with_custom_splits(vec![ + ( + DENOM_A.to_string(), + SplitType::Custom(SplitConfig { + receivers: vec![ + ( + ReceiverType::Native(NativeReceiver { address: PARTY_B_ADDR.to_string() }), + Uint128::new(100), + ), + ] + }) + ), + ( + DENOM_B.to_string(), + SplitType::Custom(SplitConfig { + receivers: vec![ + ( + ReceiverType::Native(NativeReceiver { address: PARTY_A_ADDR.to_string() }), + Uint128::new(100), + ), + ] + }) + ), + ]) + .build(); + + // fund the splitter with 100 of each denom + suite.fund_coin(Coin::new(100, DENOM_A)); + suite.fund_coin(Coin::new(100, DENOM_B)); + + suite.pass_blocks(10); + + // assert splitter is funded + let splitter_denom_a_bal = suite.get_party_denom_balance(DENOM_A, suite.splitter.as_str()); + let splitter_denom_b_bal = suite.get_party_denom_balance(DENOM_B, suite.splitter.as_str()); + assert_eq!(Uint128::new(100), splitter_denom_a_bal); + assert_eq!(Uint128::new(100), splitter_denom_b_bal); + + // tick initiates the distribution attempt + suite.tick(CLOCK_ADDR).unwrap(); + suite.pass_blocks(10); + + let party_a_denom_a_bal = suite.get_party_denom_balance(DENOM_A, PARTY_A_ADDR); + let party_a_denom_b_bal = suite.get_party_denom_balance(DENOM_B, PARTY_A_ADDR); + let party_b_denom_a_bal = suite.get_party_denom_balance(DENOM_A, PARTY_B_ADDR); + let party_b_denom_b_bal = suite.get_party_denom_balance(DENOM_B, PARTY_B_ADDR); + let splitter_denom_a_bal = suite.get_party_denom_balance(DENOM_A, suite.splitter.as_str()); + let splitter_denom_b_bal = suite.get_party_denom_balance(DENOM_B, suite.splitter.as_str()); + + assert_eq!(Uint128::zero(), party_a_denom_a_bal); + assert_eq!(Uint128::new(100), party_a_denom_b_bal); + assert_eq!(Uint128::new(100), party_b_denom_a_bal); + assert_eq!(Uint128::zero(), party_b_denom_b_bal); + assert_eq!(Uint128::zero(), splitter_denom_a_bal); + assert_eq!(Uint128::zero(), splitter_denom_b_bal); +} + + +#[test] +fn test_distribute_fallback() { + let mut suite = SuiteBuilder::default().build(); + + // fund the splitter with 100 of some random token not part of the config + suite.fund_coin(Coin::new(100, ALT_DENOM.to_string())); + + suite.pass_blocks(10); + + // assert splitter is funded + let splitter_alt_denom_bal = suite.get_party_denom_balance(ALT_DENOM, suite.splitter.as_str()); + assert_eq!(Uint128::new(100), splitter_alt_denom_bal); + + // tick initiates the distribution attempt + suite.tick(CLOCK_ADDR).unwrap(); + suite.pass_blocks(10); + + let save_the_cats_foundation_bal = suite.get_party_denom_balance(ALT_DENOM, "save_the_cats"); + let splitter_alt_denom_bal = suite.get_party_denom_balance(ALT_DENOM, suite.splitter.as_str()); + + assert_eq!(Uint128::zero(), splitter_alt_denom_bal); + assert_eq!(Uint128::new(100), save_the_cats_foundation_bal); +} \ No newline at end of file From f7feaa292b843d6ab61673435c150a5dc1ffe2a5 Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 30 Aug 2023 23:41:23 +0200 Subject: [PATCH 060/586] reorg msg --- contracts/interchain-splitter/src/contract.rs | 16 ++++++------- contracts/interchain-splitter/src/msg.rs | 24 +++++++++++++++---- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/contracts/interchain-splitter/src/contract.rs b/contracts/interchain-splitter/src/contract.rs index b58a6fa1..1a37d2a7 100644 --- a/contracts/interchain-splitter/src/contract.rs +++ b/contracts/interchain-splitter/src/contract.rs @@ -1,4 +1,3 @@ -use cosmwasm_schema::cw_serde; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ @@ -27,18 +26,19 @@ pub fn instantiate( let clock_addr = deps.api.addr_validate(&msg.clock_address)?; CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; - let fallback_split = if let Some(split) = msg.fallback_split { - split.validate_to_split_config()? - } else { - deps.querier.query_wasm_smart("contract0", &ProtocolGuildQueryMsg::PublicGoodsSplit {})? - }; - // we validate the splits and store them per-denom for (denom, split) in msg.splits { - let validated_split = split.validate_to_split_config()?; + let validated_split = split.get_split_config()?.validate()?; SPLIT_CONFIG_MAP.save(deps.storage, denom, &validated_split)?; } + // if a fallback split is provided we use that, otherwise we default + // to the timewave split + let fallback_split = if let Some(split) = msg.fallback_split { + split.get_split_config()?.validate()? + } else { + deps.querier.query_wasm_smart("contract0", &ProtocolGuildQueryMsg::PublicGoodsSplit {})? + }; // store the fallback split under emtpy key to not match any denoms SPLIT_CONFIG_MAP.save(deps.storage, String::default(), &fallback_split)?; diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs index a2c0447e..8ce4494c 100644 --- a/contracts/interchain-splitter/src/msg.rs +++ b/contracts/interchain-splitter/src/msg.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use covenant_macros::{clocked, covenant_clock_address}; -use cosmwasm_std::{IbcTimeout, Uint128, CosmosMsg, BankMsg, IbcMsg, Coin, Addr}; +use cosmwasm_std::{IbcTimeout, Uint128, CosmosMsg, BankMsg, IbcMsg, Coin, Addr, Attribute}; use crate::error::ContractError; @@ -49,9 +49,9 @@ pub enum SplitType { } impl SplitType { - pub fn validate_to_split_config(self) -> Result { + pub fn get_split_config(self) -> Result { match self { - SplitType::Custom(c) => c.validate(), + SplitType::Custom(c) => Ok(c), SplitType::TimewaveSplit => { // TODO: query the timewave split contract here Ok(SplitConfig { @@ -111,9 +111,25 @@ impl SplitConfig { }; msgs.push(msg); } - Ok(msgs) } + + pub fn get_response_attribute(self, denom: String) -> Attribute { + let mut receivers = "[".to_string(); + self.receivers.iter().for_each(|(ty, share)| { + receivers.push_str("("); + match ty { + ReceiverType::Interchain(i) => { + receivers.push_str(&i.address) + }, + ReceiverType::Native(n) => receivers.push_str(&n.address), + }; + receivers.push_str(&share.to_string()); + receivers.push_str("),"); + }); + receivers.push_str("]"); + Attribute::new(denom, receivers) + } } #[covenant_clock_address] From 30403b0cfbc446631018ab78eaffa1fb5b5bb1de Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 31 Aug 2023 20:33:22 +0200 Subject: [PATCH 061/586] config update via migration --- contracts/interchain-splitter/src/contract.rs | 71 +++++++++++++++++-- contracts/interchain-splitter/src/msg.rs | 14 +++- contracts/interchain-splitter/src/state.rs | 4 +- .../interchain-splitter/src/suite_test/mod.rs | 3 +- .../src/suite_test/suite.rs | 18 ++++- .../src/suite_test/tests.rs | 50 +++++++++++-- 6 files changed, 145 insertions(+), 15 deletions(-) diff --git a/contracts/interchain-splitter/src/contract.rs b/contracts/interchain-splitter/src/contract.rs index 1a37d2a7..6bc61109 100644 --- a/contracts/interchain-splitter/src/contract.rs +++ b/contracts/interchain-splitter/src/contract.rs @@ -7,8 +7,8 @@ use cosmwasm_std::{ use cw2::set_contract_version; use crate::error::ContractError; -use crate::msg::{InstantiateMsg, ExecuteMsg, QueryMsg, SplitConfig, ProtocolGuildQueryMsg}; -use crate::state::{SPLIT_CONFIG_MAP, CLOCK_ADDRESS}; +use crate::msg::{InstantiateMsg, ExecuteMsg, QueryMsg, SplitConfig, ProtocolGuildQueryMsg, MigrateMsg, SplitType}; +use crate::state::{SPLIT_CONFIG_MAP, CLOCK_ADDRESS, FALLBACK_SPLIT}; const CONTRACT_NAME: &str = "crates.io:covenant-interchain-splitter"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -39,8 +39,7 @@ pub fn instantiate( } else { deps.querier.query_wasm_smart("contract0", &ProtocolGuildQueryMsg::PublicGoodsSplit {})? }; - // store the fallback split under emtpy key to not match any denoms - SPLIT_CONFIG_MAP.save(deps.storage, String::default(), &fallback_split)?; + FALLBACK_SPLIT.save(deps.storage, &fallback_split)?; Ok(Response::default() .add_attribute("method", "interchain_splitter_instantiate") @@ -88,7 +87,7 @@ pub fn try_distribute(deps: DepsMut, env: Env) -> Result StdResult { QueryMsg::ClockAddress{}=>Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), QueryMsg::DenomSplit { denom } => Ok(to_binary(&query_split(deps, denom)?)?), QueryMsg::Splits {} => Ok(to_binary(&query_all_splits(deps)?)?), - QueryMsg::FallbackSplit {} => Ok(to_binary(&query_split(deps, String::default())?)?), + QueryMsg::FallbackSplit {} => Ok(to_binary(&FALLBACK_SPLIT.may_load(deps.storage)?)?), } } @@ -137,4 +136,62 @@ pub fn query_split(deps: Deps, denom: String) -> Result { Ok(SplitConfig { receivers: vec![], }) -} \ No newline at end of file +} + + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { + deps.api.debug("WASMDEBUG: migrate"); + + match msg { + MigrateMsg::UpdateConfig { + clock_addr, + splits, + fallback_split, + } => { + let mut resp = Response::default().add_attribute("method", "update_config"); + + if let Some(clock_addr) = clock_addr { + CLOCK_ADDRESS.save(deps.storage, &deps.api.addr_validate(&clock_addr)?)?; + resp = resp.add_attribute("clock_addr", clock_addr); + } + + if let Some(splits) = splits { + // clear all current split configs + SPLIT_CONFIG_MAP.clear(deps.storage); + for (denom, split_type) in splits { + match split_type { + SplitType::Custom(split) => { + match split.validate() { + Ok(split) => { + SPLIT_CONFIG_MAP.save(deps.storage, denom.to_string(), &split)?; + resp = resp.add_attributes(vec![split.get_response_attribute(denom)]); + }, + Err(_) => return Err(StdError::generic_err("invalid split".to_string())), + } + }, + SplitType::TimewaveSplit => todo!(), + } + } + } + + if let Some(split) = fallback_split { + match split.validate() { + Ok(split) => { + FALLBACK_SPLIT.save(deps.storage, &split)?; + resp = resp.add_attributes(vec![split.get_response_attribute("fallback".to_string())]); + }, + Err(_) => return Err(StdError::generic_err("invalid split".to_string())), + } + } + + Ok(resp) + } + MigrateMsg::UpdateCodeId { data: _ } => { + // This is a migrate message to update code id, + // Data is optional base64 that we can parse to any data we would like in the future + // let data: SomeStruct = from_binary(&data)?; + Ok(Response::default()) + } + } +} diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs index 8ce4494c..6d8e1ac6 100644 --- a/contracts/interchain-splitter/src/msg.rs +++ b/contracts/interchain-splitter/src/msg.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use covenant_macros::{clocked, covenant_clock_address}; -use cosmwasm_std::{IbcTimeout, Uint128, CosmosMsg, BankMsg, IbcMsg, Coin, Addr, Attribute}; +use cosmwasm_std::{IbcTimeout, Uint128, CosmosMsg, BankMsg, IbcMsg, Coin, Addr, Attribute, Binary}; use crate::error::ContractError; @@ -149,4 +149,16 @@ pub enum QueryMsg { pub enum ProtocolGuildQueryMsg { #[returns(SplitConfig)] PublicGoodsSplit {}, +} + +#[cw_serde] +pub enum MigrateMsg { + UpdateConfig { + clock_addr: Option, + fallback_split: Option, + splits: Option>, + }, + UpdateCodeId { + data: Option, + }, } \ No newline at end of file diff --git a/contracts/interchain-splitter/src/state.rs b/contracts/interchain-splitter/src/state.rs index b2268f5d..868968c7 100644 --- a/contracts/interchain-splitter/src/state.rs +++ b/contracts/interchain-splitter/src/state.rs @@ -6,6 +6,8 @@ use crate::msg::SplitConfig; /// clock module address to verify the sender of incoming ticks pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); -// maps a denom string to a vec of SplitReceivers +/// maps a denom string to a vec of SplitReceivers pub const SPLIT_CONFIG_MAP: Map = Map::new("split_config"); +/// split for all denoms that are not explicitly defined in SPLIT_CONFIG_MAP +pub const FALLBACK_SPLIT: Item = Item::new("fallback_split"); diff --git a/contracts/interchain-splitter/src/suite_test/mod.rs b/contracts/interchain-splitter/src/suite_test/mod.rs index b22597d2..64a1b7fd 100644 --- a/contracts/interchain-splitter/src/suite_test/mod.rs +++ b/contracts/interchain-splitter/src/suite_test/mod.rs @@ -13,7 +13,8 @@ pub fn splitter_contract() -> Box> { crate::contract::execute, crate::contract::instantiate, crate::contract::query, - ); + ) + .with_migrate(crate::contract::migrate); Box::new(contract) } diff --git a/contracts/interchain-splitter/src/suite_test/suite.rs b/contracts/interchain-splitter/src/suite_test/suite.rs index bd9eda97..f05bce88 100644 --- a/contracts/interchain-splitter/src/suite_test/suite.rs +++ b/contracts/interchain-splitter/src/suite_test/suite.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{Addr, Uint128, Coin}; use cw_multi_test::{App, Executor, AppResponse, SudoMsg}; -use crate::{msg::{InstantiateMsg, SplitType, SplitConfig, ReceiverType, NativeReceiver, ExecuteMsg, QueryMsg}}; +use crate::msg::{InstantiateMsg, SplitType, SplitConfig, ReceiverType, NativeReceiver, ExecuteMsg, QueryMsg, MigrateMsg}; use super::{splitter_contract, mock_protocol_guild_contract}; @@ -128,6 +128,15 @@ impl Suite { &[], ) } + + pub fn migrate(&mut self, msg: MigrateMsg) -> Result { + self.app.migrate_contract( + Addr::unchecked(ADMIN), + self.splitter.clone(), + &msg, + 2, + ) + } } // queries @@ -152,6 +161,13 @@ impl Suite { .query_wasm_smart(&self.splitter, &QueryMsg::Splits {}) .unwrap() } + + pub fn query_fallback_split(&self) -> SplitConfig { + self.app + .wrap() + .query_wasm_smart(&self.splitter, &QueryMsg::FallbackSplit {}) + .unwrap() + } } // helper diff --git a/contracts/interchain-splitter/src/suite_test/tests.rs b/contracts/interchain-splitter/src/suite_test/tests.rs index 992b39db..c1fb7306 100644 --- a/contracts/interchain-splitter/src/suite_test/tests.rs +++ b/contracts/interchain-splitter/src/suite_test/tests.rs @@ -1,11 +1,9 @@ use cosmwasm_std::{Uint128, Coin}; -use crate::{suite_test::suite::{get_equal_split_config, DENOM_B, CLOCK_ADDR, get_public_goods_split_config, ALT_DENOM}, msg::{SplitConfig, SplitType, NativeReceiver, ReceiverType}}; +use crate::{suite_test::suite::{get_equal_split_config, DENOM_B, CLOCK_ADDR, get_public_goods_split_config, ALT_DENOM}, msg::{SplitConfig, SplitType, NativeReceiver, ReceiverType, MigrateMsg}}; use super::suite::{SuiteBuilder, DENOM_A, PARTY_A_ADDR, PARTY_B_ADDR}; - - #[test] fn test_instantiate_happy_and_query_all() { let suite = SuiteBuilder::default().build(); @@ -14,18 +12,19 @@ fn test_instantiate_happy_and_query_all() { let token_a_split = suite.query_denom_split(DENOM_A.to_string()); let token_b_split = suite.query_denom_split(DENOM_B.to_string()); let clock_addr = suite.query_clock_address(); + let fallback_split = suite.query_fallback_split(); assert_eq!(get_equal_split_config(), token_a_split); assert_eq!(get_equal_split_config(), token_b_split); assert_eq!(CLOCK_ADDR.to_string(), clock_addr); assert_eq!( vec![ - ("".to_string(), get_public_goods_split_config()), (DENOM_A.to_string(), get_equal_split_config()), (DENOM_B.to_string(), get_equal_split_config()), ], splits, ); + assert_eq!(get_public_goods_split_config(), fallback_split); } #[test] @@ -168,4 +167,47 @@ fn test_distribute_fallback() { assert_eq!(Uint128::zero(), splitter_alt_denom_bal); assert_eq!(Uint128::new(100), save_the_cats_foundation_bal); +} + +#[test] +fn test_migrate_config() { + let mut suite = SuiteBuilder::default().build(); + + let new_clock = "new_clock".to_string(); + let new_fallback_split = SplitConfig { + receivers: vec![ + (ReceiverType::Native(NativeReceiver { address: "fallback_new".to_string() }), Uint128::new(100)) + ], + }; + let new_splits = vec![( + "new_denom".to_string(), + SplitType::Custom(SplitConfig { + receivers: vec![ + (ReceiverType::Native(NativeReceiver { address: "new_receiver".to_string() }), Uint128::new(100)) + ], + }) + )]; + + let migrate_msg = MigrateMsg::UpdateConfig { + clock_addr: Some(new_clock.clone()), + fallback_split: Some(new_fallback_split.clone()), + splits: Some(new_splits.clone()), + }; + + suite.migrate(migrate_msg).unwrap(); + + let splits = suite.query_all_splits(); + let clock_addr = suite.query_clock_address(); + let fallback_split = suite.query_fallback_split(); + + assert_eq!(vec![( + "new_denom".to_string(), + SplitConfig { + receivers: vec![ + (ReceiverType::Native(NativeReceiver { address: "new_receiver".to_string() }), Uint128::new(100)) + ], + }, + )], splits); + assert_eq!(new_fallback_split, fallback_split); + assert_eq!(new_clock, clock_addr); } \ No newline at end of file From b7f632d4fcfae5934a9de0ff865ae683c11242e2 Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 31 Aug 2023 23:32:13 +0200 Subject: [PATCH 062/586] removing non-custom split types --- contracts/interchain-splitter/README.md | 5 --- contracts/interchain-splitter/src/contract.rs | 32 ++++++--------- contracts/interchain-splitter/src/msg.rs | 31 ++++---------- .../interchain-splitter/src/suite_test/mod.rs | 40 +------------------ .../src/suite_test/suite.rs | 25 ++++-------- .../src/suite_test/tests.rs | 10 +++-- 6 files changed, 35 insertions(+), 108 deletions(-) diff --git a/contracts/interchain-splitter/README.md b/contracts/interchain-splitter/README.md index ba496226..c2d253ff 100644 --- a/contracts/interchain-splitter/README.md +++ b/contracts/interchain-splitter/README.md @@ -11,14 +11,9 @@ In general, we support a per-denom configuration as follows: ``` OSMO -> [(osmo12323, 40), (cosmos32121, 60)] ATOM -> [(osmo12323, 50), (cosmo32121, 50)] -USDC -> [timewave_split] _ -> [(osmo12323, 30), (cosmo32121, 70)] ``` -### Timewave Split - -Timewave split provides a preconfigured list of addresses. - ### Custom Split A custom split here refers to a list of addresses with their associated share of the split (in %). diff --git a/contracts/interchain-splitter/src/contract.rs b/contracts/interchain-splitter/src/contract.rs index 6bc61109..16a3d3e8 100644 --- a/contracts/interchain-splitter/src/contract.rs +++ b/contracts/interchain-splitter/src/contract.rs @@ -7,7 +7,7 @@ use cosmwasm_std::{ use cw2::set_contract_version; use crate::error::ContractError; -use crate::msg::{InstantiateMsg, ExecuteMsg, QueryMsg, SplitConfig, ProtocolGuildQueryMsg, MigrateMsg, SplitType}; +use crate::msg::{InstantiateMsg, ExecuteMsg, QueryMsg, SplitConfig, MigrateMsg, SplitType}; use crate::state::{SPLIT_CONFIG_MAP, CLOCK_ADDRESS, FALLBACK_SPLIT}; const CONTRACT_NAME: &str = "crates.io:covenant-interchain-splitter"; @@ -32,14 +32,10 @@ pub fn instantiate( SPLIT_CONFIG_MAP.save(deps.storage, denom, &validated_split)?; } - // if a fallback split is provided we use that, otherwise we default - // to the timewave split - let fallback_split = if let Some(split) = msg.fallback_split { - split.get_split_config()?.validate()? - } else { - deps.querier.query_wasm_smart("contract0", &ProtocolGuildQueryMsg::PublicGoodsSplit {})? - }; - FALLBACK_SPLIT.save(deps.storage, &fallback_split)?; + // if a fallback split is provided we validate and store it + if let Some(split) = msg.fallback_split { + FALLBACK_SPLIT.save(deps.storage, &split.get_split_config()?.validate()?)?; + } Ok(Response::default() .add_attribute("method", "interchain_splitter_instantiate") @@ -86,17 +82,16 @@ pub fn try_distribute(deps: DepsMut, env: Env) -> Result StdResult Err(_) => return Err(StdError::generic_err("invalid split".to_string())), } }, - SplitType::TimewaveSplit => todo!(), } } } diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs index 6d8e1ac6..179bc200 100644 --- a/contracts/interchain-splitter/src/msg.rs +++ b/contracts/interchain-splitter/src/msg.rs @@ -11,8 +11,7 @@ pub struct InstantiateMsg { /// list of (denom, split) configurations pub splits: Vec<(String, SplitType)>, /// a split for all denoms that are not covered in the - /// regular `splits` list. If no fallback is provided, - /// we default to the timewave protocol guild split + /// regular `splits` list pub fallback_split: Option, } @@ -45,22 +44,13 @@ pub enum ReceiverType { #[cw_serde] pub enum SplitType { Custom(SplitConfig), - TimewaveSplit, + // predefined splits will go here } impl SplitType { pub fn get_split_config(self) -> Result { match self { SplitType::Custom(c) => Ok(c), - SplitType::TimewaveSplit => { - // TODO: query the timewave split contract here - Ok(SplitConfig { - receivers: vec![( - ReceiverType::Native(NativeReceiver { address: "todo".to_string() }), - Uint128::new(100) - )], - }) - }, } } } @@ -84,12 +74,12 @@ impl SplitConfig { } } - pub fn get_transfer_messages(self, amount: Uint128, denom: String) -> Result, ContractError> { + pub fn get_transfer_messages(&self, amount: Uint128, denom: String) -> Result, ContractError> { let mut msgs: Vec = vec![]; - for (receiver_type, share) in self.receivers.into_iter() { + for (receiver_type, share) in self.receivers.iter() { let entitlement = amount.checked_multiply_ratio( - share, + *share, Uint128::new(100), ).map_err(|_| ContractError::SplitMisconfig {})?; @@ -99,8 +89,8 @@ impl SplitConfig { }; let msg = match receiver_type { ReceiverType::Interchain(receiver) => CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: receiver.channel_id, - to_address: receiver.address, + channel_id: receiver.channel_id.to_string(), + to_address: receiver.address.to_string(), amount, timeout: receiver.ibc_timeout.clone(), }), @@ -144,13 +134,6 @@ pub enum QueryMsg { FallbackSplit {}, } -#[cw_serde] -#[derive(QueryResponses)] -pub enum ProtocolGuildQueryMsg { - #[returns(SplitConfig)] - PublicGoodsSplit {}, -} - #[cw_serde] pub enum MigrateMsg { UpdateConfig { diff --git a/contracts/interchain-splitter/src/suite_test/mod.rs b/contracts/interchain-splitter/src/suite_test/mod.rs index 64a1b7fd..d384303d 100644 --- a/contracts/interchain-splitter/src/suite_test/mod.rs +++ b/contracts/interchain-splitter/src/suite_test/mod.rs @@ -1,9 +1,6 @@ -use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Empty, to_binary, Env, Deps, StdResult, Binary, Uint128}; +use cosmwasm_std::Empty; use cw_multi_test::{Contract, ContractWrapper}; -use crate::msg::{SplitConfig, ReceiverType, NativeReceiver}; - mod suite; mod tests; @@ -17,38 +14,3 @@ pub fn splitter_contract() -> Box> { .with_migrate(crate::contract::migrate); Box::new(contract) } - -pub fn mock_protocol_guild_contract() -> Box> { - let contract = ContractWrapper::new( - crate::contract::execute, - crate::contract::instantiate, - query, - ); - Box::new(contract) -} - - -// timewave protocol guild mock -#[cfg(not(feature = "library"))] -use cosmwasm_std::entry_point; - -#[cw_serde] -#[derive(QueryResponses)] -pub enum QueryMsg { - #[returns(SplitConfig)] - PublicGoodsSplit {}, -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { - match msg { - QueryMsg::PublicGoodsSplit {} => Ok(to_binary(&SplitConfig { - receivers: vec![ - ( - ReceiverType::Native(NativeReceiver { address: "save_the_cats".to_string()}), - Uint128::new(100), - ), - ] - })?), - } -} diff --git a/contracts/interchain-splitter/src/suite_test/suite.rs b/contracts/interchain-splitter/src/suite_test/suite.rs index f05bce88..b26ae852 100644 --- a/contracts/interchain-splitter/src/suite_test/suite.rs +++ b/contracts/interchain-splitter/src/suite_test/suite.rs @@ -3,7 +3,7 @@ use cw_multi_test::{App, Executor, AppResponse, SudoMsg}; use crate::msg::{InstantiateMsg, SplitType, SplitConfig, ReceiverType, NativeReceiver, ExecuteMsg, QueryMsg, MigrateMsg}; -use super::{splitter_contract, mock_protocol_guild_contract}; +use super::splitter_contract; pub const ADMIN: &str = "admin"; @@ -36,7 +36,7 @@ pub fn get_equal_split_config() -> SplitConfig { } } -pub fn get_public_goods_split_config() -> SplitConfig { +pub fn get_fallback_split_config() -> SplitConfig { SplitConfig { receivers: vec![ ( ReceiverType::Native(NativeReceiver { address: "save_the_cats".to_string()}), @@ -48,7 +48,6 @@ pub fn get_public_goods_split_config() -> SplitConfig { pub struct Suite { pub app: App, pub splitter: Addr, - pub protocol_guild: Addr, } pub struct SuiteBuilder { @@ -84,21 +83,14 @@ impl SuiteBuilder { self } + pub fn with_fallback_split(mut self, split: SplitConfig) -> Self { + self.instantiate.fallback_split = Some(SplitType::Custom(split)); + self + } + pub fn build(mut self) -> Suite { let mut app = self.app; - let protocol_guild_code = app.store_code(mock_protocol_guild_contract()); - let mock_protocol_guild = app - .instantiate_contract( - protocol_guild_code, - Addr::unchecked(ADMIN), - &self.instantiate, - &[], - "protocol_guild", - Some(ADMIN.to_string()), - ) - .unwrap(); - let splitter_code: u64 = app.store_code(splitter_contract()); let splitter = app .instantiate_contract( @@ -113,7 +105,6 @@ impl SuiteBuilder { Suite { app, splitter, - protocol_guild: mock_protocol_guild, } } } @@ -162,7 +153,7 @@ impl Suite { .unwrap() } - pub fn query_fallback_split(&self) -> SplitConfig { + pub fn query_fallback_split(&self) -> Option { self.app .wrap() .query_wasm_smart(&self.splitter, &QueryMsg::FallbackSplit {}) diff --git a/contracts/interchain-splitter/src/suite_test/tests.rs b/contracts/interchain-splitter/src/suite_test/tests.rs index c1fb7306..9735bea6 100644 --- a/contracts/interchain-splitter/src/suite_test/tests.rs +++ b/contracts/interchain-splitter/src/suite_test/tests.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{Uint128, Coin}; -use crate::{suite_test::suite::{get_equal_split_config, DENOM_B, CLOCK_ADDR, get_public_goods_split_config, ALT_DENOM}, msg::{SplitConfig, SplitType, NativeReceiver, ReceiverType, MigrateMsg}}; +use crate::{suite_test::suite::{get_equal_split_config, DENOM_B, CLOCK_ADDR, ALT_DENOM, get_fallback_split_config}, msg::{SplitConfig, SplitType, NativeReceiver, ReceiverType, MigrateMsg}}; use super::suite::{SuiteBuilder, DENOM_A, PARTY_A_ADDR, PARTY_B_ADDR}; @@ -24,7 +24,7 @@ fn test_instantiate_happy_and_query_all() { ], splits, ); - assert_eq!(get_public_goods_split_config(), fallback_split); + assert_eq!(None, fallback_split); } #[test] @@ -147,7 +147,9 @@ fn test_distribute_token_swap() { #[test] fn test_distribute_fallback() { - let mut suite = SuiteBuilder::default().build(); + let mut suite = SuiteBuilder::default() + .with_fallback_split(get_fallback_split_config()) + .build(); // fund the splitter with 100 of some random token not part of the config suite.fund_coin(Coin::new(100, ALT_DENOM.to_string())); @@ -208,6 +210,6 @@ fn test_migrate_config() { ], }, )], splits); - assert_eq!(new_fallback_split, fallback_split); + assert_eq!(Some(new_fallback_split), fallback_split); assert_eq!(new_clock, clock_addr); } \ No newline at end of file From 3f985ac0355c711bd842601816dcdd1d8520bdc6 Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 1 Sep 2023 14:14:12 +0200 Subject: [PATCH 063/586] fmt, clippy, adding response attributes --- contracts/interchain-splitter/src/contract.rs | 72 +++++++------ contracts/interchain-splitter/src/error.rs | 1 - contracts/interchain-splitter/src/msg.rs | 34 +++--- .../interchain-splitter/src/suite_test/mod.rs | 1 - .../src/suite_test/suite.rs | 43 ++++---- .../src/suite_test/tests.rs | 100 +++++++++++------- 6 files changed, 134 insertions(+), 117 deletions(-) diff --git a/contracts/interchain-splitter/src/contract.rs b/contracts/interchain-splitter/src/contract.rs index 16a3d3e8..d2450417 100644 --- a/contracts/interchain-splitter/src/contract.rs +++ b/contracts/interchain-splitter/src/contract.rs @@ -1,14 +1,14 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - DepsMut, Env, MessageInfo, - Response, Order, CosmosMsg, Deps, StdResult, Binary, to_binary, StdError, + to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Order, Response, StdError, + StdResult, }; use cw2::set_contract_version; use crate::error::ContractError; -use crate::msg::{InstantiateMsg, ExecuteMsg, QueryMsg, SplitConfig, MigrateMsg, SplitType}; -use crate::state::{SPLIT_CONFIG_MAP, CLOCK_ADDRESS, FALLBACK_SPLIT}; +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, SplitConfig, SplitType}; +use crate::state::{CLOCK_ADDRESS, FALLBACK_SPLIT, SPLIT_CONFIG_MAP}; const CONTRACT_NAME: &str = "crates.io:covenant-interchain-splitter"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -23,23 +23,31 @@ pub fn instantiate( deps.api.debug("WASMDEBUG: instantiate"); set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + let mut resp = Response::default().add_attribute("method", "interchain_splitter_instantiate"); + let clock_addr = deps.api.addr_validate(&msg.clock_address)?; CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; + resp = resp.add_attribute("clock_addr", clock_addr); // we validate the splits and store them per-denom for (denom, split) in msg.splits { let validated_split = split.get_split_config()?.validate()?; - SPLIT_CONFIG_MAP.save(deps.storage, denom, &validated_split)?; + SPLIT_CONFIG_MAP.save(deps.storage, denom.to_string(), &validated_split)?; + resp = resp.add_attributes(vec![validated_split.get_response_attribute(denom)]); } // if a fallback split is provided we validate and store it if let Some(split) = msg.fallback_split { + resp = resp.add_attributes(vec![split + .clone() + .get_split_config()? + .get_response_attribute("fallback".to_string())]); FALLBACK_SPLIT.save(deps.storage, &split.get_split_config()?.validate()?)?; + } else { + resp = resp.add_attribute("fallback", "None"); } - Ok(Response::default() - .add_attribute("method", "interchain_splitter_instantiate") - ) + Ok(resp) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -62,20 +70,16 @@ pub fn try_distribute(deps: DepsMut, env: Env) -> Result = vec![]; // then we iterate over our split config and try to match the entries to available balances - for entry in SPLIT_CONFIG_MAP - .range(deps.storage, None, None, Order::Ascending) { + for entry in SPLIT_CONFIG_MAP.range(deps.storage, None, None, Order::Ascending) { let (denom, config) = entry?; - // skip the fallback config for later - if denom == String::default() { - continue; - } // we try to find the index of matching coin in available balances let balances_index = balances.iter().position(|coin| coin.denom == denom); if let Some(index) = balances_index { // pop the relevant coin and build the transfer messages let coin = balances.remove(index); - let mut transfer_messages = config.get_transfer_messages(coin.amount, coin.denom.to_string())?; + let mut transfer_messages = + config.get_transfer_messages(coin.amount, coin.denom.to_string())?; distribution_messages.append(&mut transfer_messages); } } @@ -86,13 +90,14 @@ pub fn try_distribute(deps: DepsMut, env: Env) -> Result Result StdResult { match msg { - QueryMsg::ClockAddress{}=>Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), + QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), QueryMsg::DenomSplit { denom } => Ok(to_binary(&query_split(deps, denom)?)?), QueryMsg::Splits {} => Ok(to_binary(&query_all_splits(deps)?)?), QueryMsg::FallbackSplit {} => Ok(to_binary(&FALLBACK_SPLIT.may_load(deps.storage)?)?), @@ -108,7 +113,6 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { } pub fn query_all_splits(deps: Deps) -> Result, StdError> { - let mut splits: Vec<(String, SplitConfig)> = vec![]; for entry in SPLIT_CONFIG_MAP.range(deps.storage, None, None, Order::Ascending) { @@ -120,20 +124,16 @@ pub fn query_all_splits(deps: Deps) -> Result, StdErr } pub fn query_split(deps: Deps, denom: String) -> Result { - for entry in SPLIT_CONFIG_MAP.range(deps.storage, None, None, Order::Ascending) { let (entry_denom, config) = entry?; if entry_denom == denom { - return Ok(config) + return Ok(config); } } - Ok(SplitConfig { - receivers: vec![], - }) + Ok(SplitConfig { receivers: vec![] }) } - #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { deps.api.debug("WASMDEBUG: migrate"); @@ -152,18 +152,18 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult } if let Some(splits) = splits { - // clear all current split configs + // clear all current split configs before storing new values SPLIT_CONFIG_MAP.clear(deps.storage); for (denom, split_type) in splits { match split_type { - SplitType::Custom(split) => { - match split.validate() { - Ok(split) => { - SPLIT_CONFIG_MAP.save(deps.storage, denom.to_string(), &split)?; - resp = resp.add_attributes(vec![split.get_response_attribute(denom)]); - }, - Err(_) => return Err(StdError::generic_err("invalid split".to_string())), + // we validate each split before storing it + SplitType::Custom(split) => match split.validate() { + Ok(split) => { + SPLIT_CONFIG_MAP.save(deps.storage, denom.to_string(), &split)?; + resp = + resp.add_attributes(vec![split.get_response_attribute(denom)]); } + Err(_) => return Err(StdError::generic_err("invalid split".to_string())) }, } } @@ -173,8 +173,10 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult match split.validate() { Ok(split) => { FALLBACK_SPLIT.save(deps.storage, &split)?; - resp = resp.add_attributes(vec![split.get_response_attribute("fallback".to_string())]); - }, + resp = resp.add_attributes(vec![ + split.get_response_attribute("fallback".to_string()) + ]); + } Err(_) => return Err(StdError::generic_err("invalid split".to_string())), } } diff --git a/contracts/interchain-splitter/src/error.rs b/contracts/interchain-splitter/src/error.rs index 3faacc5c..1edc85fc 100644 --- a/contracts/interchain-splitter/src/error.rs +++ b/contracts/interchain-splitter/src/error.rs @@ -1,4 +1,3 @@ - use cosmwasm_std::StdError; use thiserror::Error; diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs index 179bc200..f993560e 100644 --- a/contracts/interchain-splitter/src/msg.rs +++ b/contracts/interchain-splitter/src/msg.rs @@ -1,6 +1,8 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{ + Addr, Attribute, BankMsg, Binary, Coin, CosmosMsg, IbcMsg, IbcTimeout, Uint128, +}; use covenant_macros::{clocked, covenant_clock_address}; -use cosmwasm_std::{IbcTimeout, Uint128, CosmosMsg, BankMsg, IbcMsg, Coin, Addr, Attribute, Binary}; use crate::error::ContractError; @@ -62,10 +64,7 @@ pub struct SplitConfig { impl SplitConfig { pub fn validate(self) -> Result { - let total_share: Uint128 = self.receivers - .iter() - .map(|r| r.1) - .sum(); + let total_share: Uint128 = self.receivers.iter().map(|r| r.1).sum(); if total_share == Uint128::new(100) { Ok(self) @@ -74,15 +73,18 @@ impl SplitConfig { } } - pub fn get_transfer_messages(&self, amount: Uint128, denom: String) -> Result, ContractError> { + pub fn get_transfer_messages( + &self, + amount: Uint128, + denom: String, + ) -> Result, ContractError> { let mut msgs: Vec = vec![]; for (receiver_type, share) in self.receivers.iter() { - let entitlement = amount.checked_multiply_ratio( - *share, - Uint128::new(100), - ).map_err(|_| ContractError::SplitMisconfig {})?; - + let entitlement = amount + .checked_multiply_ratio(*share, Uint128::new(100)) + .map_err(|_| ContractError::SplitMisconfig {})?; + let amount = Coin { denom: denom.to_string(), amount: entitlement, @@ -107,17 +109,15 @@ impl SplitConfig { pub fn get_response_attribute(self, denom: String) -> Attribute { let mut receivers = "[".to_string(); self.receivers.iter().for_each(|(ty, share)| { - receivers.push_str("("); + receivers.push('('); match ty { - ReceiverType::Interchain(i) => { - receivers.push_str(&i.address) - }, + ReceiverType::Interchain(i) => receivers.push_str(&i.address), ReceiverType::Native(n) => receivers.push_str(&n.address), }; receivers.push_str(&share.to_string()); receivers.push_str("),"); }); - receivers.push_str("]"); + receivers.push(']'); Attribute::new(denom, receivers) } } @@ -144,4 +144,4 @@ pub enum MigrateMsg { UpdateCodeId { data: Option, }, -} \ No newline at end of file +} diff --git a/contracts/interchain-splitter/src/suite_test/mod.rs b/contracts/interchain-splitter/src/suite_test/mod.rs index d384303d..994cb928 100644 --- a/contracts/interchain-splitter/src/suite_test/mod.rs +++ b/contracts/interchain-splitter/src/suite_test/mod.rs @@ -4,7 +4,6 @@ use cw_multi_test::{Contract, ContractWrapper}; mod suite; mod tests; - pub fn splitter_contract() -> Box> { let contract = ContractWrapper::new( crate::contract::execute, diff --git a/contracts/interchain-splitter/src/suite_test/suite.rs b/contracts/interchain-splitter/src/suite_test/suite.rs index b26ae852..e25a7f3c 100644 --- a/contracts/interchain-splitter/src/suite_test/suite.rs +++ b/contracts/interchain-splitter/src/suite_test/suite.rs @@ -1,7 +1,10 @@ -use cosmwasm_std::{Addr, Uint128, Coin}; -use cw_multi_test::{App, Executor, AppResponse, SudoMsg}; +use cosmwasm_std::{Addr, Coin, Uint128}; +use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; -use crate::msg::{InstantiateMsg, SplitType, SplitConfig, ReceiverType, NativeReceiver, ExecuteMsg, QueryMsg, MigrateMsg}; +use crate::msg::{ + ExecuteMsg, InstantiateMsg, MigrateMsg, NativeReceiver, QueryMsg, ReceiverType, SplitConfig, + SplitType, +}; use super::splitter_contract; @@ -16,9 +19,8 @@ pub const PARTY_B_ADDR: &str = "party_b"; pub const CLOCK_ADDR: &str = "clock_addr"; - pub fn get_equal_split_config() -> SplitConfig { - SplitConfig { + SplitConfig { receivers: vec![ ( ReceiverType::Native(NativeReceiver { @@ -37,12 +39,14 @@ pub fn get_equal_split_config() -> SplitConfig { } pub fn get_fallback_split_config() -> SplitConfig { - SplitConfig { receivers: vec![ - ( - ReceiverType::Native(NativeReceiver { address: "save_the_cats".to_string()}), + SplitConfig { + receivers: vec![( + ReceiverType::Native(NativeReceiver { + address: "save_the_cats".to_string(), + }), Uint128::new(100), - ) - ]} + )], + } } pub struct Suite { @@ -88,13 +92,13 @@ impl SuiteBuilder { self } - pub fn build(mut self) -> Suite { + pub fn build(self) -> Suite { let mut app = self.app; let splitter_code: u64 = app.store_code(splitter_contract()); let splitter = app .instantiate_contract( - splitter_code, + splitter_code, Addr::unchecked(ADMIN), &self.instantiate, &[], @@ -102,10 +106,7 @@ impl SuiteBuilder { Some(ADMIN.to_string()), ) .unwrap(); - Suite { - app, - splitter, - } + Suite { app, splitter } } } @@ -121,12 +122,8 @@ impl Suite { } pub fn migrate(&mut self, msg: MigrateMsg) -> Result { - self.app.migrate_contract( - Addr::unchecked(ADMIN), - self.splitter.clone(), - &msg, - 2, - ) + self.app + .migrate_contract(Addr::unchecked(ADMIN), self.splitter.clone(), &msg, 1) } } @@ -183,4 +180,4 @@ impl Suite { .unwrap() .amount } -} \ No newline at end of file +} diff --git a/contracts/interchain-splitter/src/suite_test/tests.rs b/contracts/interchain-splitter/src/suite_test/tests.rs index 9735bea6..4e86b64f 100644 --- a/contracts/interchain-splitter/src/suite_test/tests.rs +++ b/contracts/interchain-splitter/src/suite_test/tests.rs @@ -1,6 +1,11 @@ -use cosmwasm_std::{Uint128, Coin}; +use cosmwasm_std::{Coin, Uint128}; -use crate::{suite_test::suite::{get_equal_split_config, DENOM_B, CLOCK_ADDR, ALT_DENOM, get_fallback_split_config}, msg::{SplitConfig, SplitType, NativeReceiver, ReceiverType, MigrateMsg}}; +use crate::{ + msg::{MigrateMsg, NativeReceiver, ReceiverType, SplitConfig, SplitType}, + suite_test::suite::{ + get_equal_split_config, get_fallback_split_config, ALT_DENOM, CLOCK_ADDR, DENOM_B, + }, +}; use super::suite::{SuiteBuilder, DENOM_A, PARTY_A_ADDR, PARTY_B_ADDR}; @@ -33,17 +38,21 @@ fn test_instantiate_split_misconfig() { SuiteBuilder::default() .with_custom_splits(vec![( DENOM_A.to_string(), - SplitType::Custom(SplitConfig { + SplitType::Custom(SplitConfig { receivers: vec![ ( - ReceiverType::Native(NativeReceiver { address: PARTY_A_ADDR.to_string() }), + ReceiverType::Native(NativeReceiver { + address: PARTY_A_ADDR.to_string(), + }), Uint128::new(50), ), ( - ReceiverType::Native(NativeReceiver { address: PARTY_B_ADDR.to_string() }), + ReceiverType::Native(NativeReceiver { + address: PARTY_B_ADDR.to_string(), + }), Uint128::new(60), ), - ] + ], }), )]) .build(); @@ -90,25 +99,25 @@ fn test_distribute_token_swap() { .with_custom_splits(vec![ ( DENOM_A.to_string(), - SplitType::Custom(SplitConfig { - receivers: vec![ - ( - ReceiverType::Native(NativeReceiver { address: PARTY_B_ADDR.to_string() }), - Uint128::new(100), - ), - ] - }) + SplitType::Custom(SplitConfig { + receivers: vec![( + ReceiverType::Native(NativeReceiver { + address: PARTY_B_ADDR.to_string(), + }), + Uint128::new(100), + )], + }), ), ( DENOM_B.to_string(), - SplitType::Custom(SplitConfig { - receivers: vec![ - ( - ReceiverType::Native(NativeReceiver { address: PARTY_A_ADDR.to_string() }), - Uint128::new(100), - ), - ] - }) + SplitType::Custom(SplitConfig { + receivers: vec![( + ReceiverType::Native(NativeReceiver { + address: PARTY_A_ADDR.to_string(), + }), + Uint128::new(100), + )], + }), ), ]) .build(); @@ -144,7 +153,6 @@ fn test_distribute_token_swap() { assert_eq!(Uint128::zero(), splitter_denom_b_bal); } - #[test] fn test_distribute_fallback() { let mut suite = SuiteBuilder::default() @@ -177,23 +185,29 @@ fn test_migrate_config() { let new_clock = "new_clock".to_string(); let new_fallback_split = SplitConfig { - receivers: vec![ - (ReceiverType::Native(NativeReceiver { address: "fallback_new".to_string() }), Uint128::new(100)) - ], + receivers: vec![( + ReceiverType::Native(NativeReceiver { + address: "fallback_new".to_string(), + }), + Uint128::new(100), + )], }; let new_splits = vec![( "new_denom".to_string(), SplitType::Custom(SplitConfig { - receivers: vec![ - (ReceiverType::Native(NativeReceiver { address: "new_receiver".to_string() }), Uint128::new(100)) - ], - }) + receivers: vec![( + ReceiverType::Native(NativeReceiver { + address: "new_receiver".to_string(), + }), + Uint128::new(100), + )], + }), )]; let migrate_msg = MigrateMsg::UpdateConfig { clock_addr: Some(new_clock.clone()), fallback_split: Some(new_fallback_split.clone()), - splits: Some(new_splits.clone()), + splits: Some(new_splits), }; suite.migrate(migrate_msg).unwrap(); @@ -202,14 +216,20 @@ fn test_migrate_config() { let clock_addr = suite.query_clock_address(); let fallback_split = suite.query_fallback_split(); - assert_eq!(vec![( - "new_denom".to_string(), - SplitConfig { - receivers: vec![ - (ReceiverType::Native(NativeReceiver { address: "new_receiver".to_string() }), Uint128::new(100)) - ], - }, - )], splits); + assert_eq!( + vec![( + "new_denom".to_string(), + SplitConfig { + receivers: vec![( + ReceiverType::Native(NativeReceiver { + address: "new_receiver".to_string() + }), + Uint128::new(100) + )], + }, + )], + splits + ); assert_eq!(Some(new_fallback_split), fallback_split); assert_eq!(new_clock, clock_addr); -} \ No newline at end of file +} From 945fbdff1bc52688ed1e16326a78311253d5a0aa Mon Sep 17 00:00:00 2001 From: bekauz Date: Sun, 27 Aug 2023 19:12:04 +0200 Subject: [PATCH 064/586] init interchain splitter --- Cargo.lock | 94 +++++++++++++++++++++++++++--------------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 35d19184..887668ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,7 +43,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", - "cw-utils 1.0.1", + "cw-utils 1.0.2", "cw20 0.15.1", "cw3", "itertools", @@ -70,7 +70,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", - "cw-utils 1.0.1", + "cw-utils 1.0.2", "cw2 0.15.1", "itertools", "protobuf 2.28.0", @@ -101,7 +101,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", - "cw-utils 1.0.1", + "cw-utils 1.0.2", "cw2 0.15.1", "cw20 0.15.1", "itertools", @@ -203,9 +203,9 @@ checksum = "128a44527fc0d6abf05f9eda748b9027536e12dff93f5acc8449f51583309350" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" @@ -373,12 +373,12 @@ dependencies = [ "covenant-ls", "cw-multi-test", "cw-storage-plus 1.1.0", - "cw-utils 1.0.1", + "cw-utils 1.0.2", "cw2 1.1.1", "neutron-sdk", "prost 0.11.9", "prost-types", - "protobuf 3.2.0", + "protobuf 3.3.0", "schemars", "serde", "serde-json-wasm 0.4.1", @@ -402,12 +402,12 @@ dependencies = [ "covenant-utils", "cw-multi-test", "cw-storage-plus 1.1.0", - "cw-utils 1.0.1", + "cw-utils 1.0.2", "cw2 1.1.1", "neutron-sdk", "prost 0.11.9", "prost-types", - "protobuf 3.2.0", + "protobuf 3.3.0", "schemars", "serde", "serde-json-wasm 0.4.1", @@ -452,12 +452,12 @@ dependencies = [ "covenant-utils", "cw-multi-test", "cw-storage-plus 1.1.0", - "cw-utils 1.0.1", + "cw-utils 1.0.2", "cw2 1.1.1", "neutron-sdk", "prost 0.11.9", "prost-types", - "protobuf 3.2.0", + "protobuf 3.3.0", "schemars", "serde", "serde-json-wasm 0.4.1", @@ -481,12 +481,12 @@ dependencies = [ "covenant-utils", "cw-multi-test", "cw-storage-plus 1.1.0", - "cw-utils 1.0.1", + "cw-utils 1.0.2", "cw2 1.1.1", "neutron-sdk", "prost 0.11.9", "prost-types", - "protobuf 3.2.0", + "protobuf 3.3.0", "schemars", "serde", "serde-json-wasm 0.4.1", @@ -509,7 +509,7 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw2 1.1.1", "neutron-sdk", - "protobuf 3.2.0", + "protobuf 3.3.0", "schemars", "serde", "serde-json-wasm 0.4.1", @@ -536,14 +536,14 @@ dependencies = [ "covenant-macros", "cw-multi-test", "cw-storage-plus 1.1.0", - "cw-utils 1.0.1", - "cw1-whitelist 1.1.0", + "cw-utils 1.0.2", + "cw1-whitelist 1.1.1", "cw2 1.1.1", "cw20 0.15.1", "neutron-sdk", "prost 0.11.9", "prost-types", - "protobuf 3.2.0", + "protobuf 3.3.0", "schemars", "serde", "serde-json-wasm 0.4.1", @@ -564,7 +564,7 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw2 1.1.1", "neutron-sdk", - "protobuf 3.2.0", + "protobuf 3.3.0", "schemars", "serde", "serde-json-wasm 0.4.1", @@ -595,7 +595,7 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw2 1.1.1", "neutron-sdk", - "protobuf 3.2.0", + "protobuf 3.3.0", "schemars", "serde", "serde-json-wasm 0.4.1", @@ -713,7 +713,7 @@ dependencies = [ "anyhow", "cosmwasm-std", "cw-storage-plus 1.1.0", - "cw-utils 1.0.1", + "cw-utils 1.0.2", "derivative", "itertools", "k256 0.11.6", @@ -762,9 +762,9 @@ dependencies = [ [[package]] name = "cw-utils" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7" +checksum = "1b9f351a4e4d81ef7c890e44d903f8c0bdcdc00f094fd3a181eaf70c0eec7a3a" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -818,14 +818,14 @@ dependencies = [ [[package]] name = "cw1-whitelist" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce24b204a0769ae41f5664d18be910a1745571dbc89f8643a62edbbab0fb127f" +checksum = "32c12f6e7859ad758c95e7bcd7ac59419d550b41e4e35bf8aac021070a686abc" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", - "cw-utils 1.0.1", + "cw-utils 1.0.2", "cw1 1.1.1", "cw2 1.1.1", "schemars", @@ -881,7 +881,7 @@ checksum = "786e9da5e937f473cecd2463e81384c1af65d0f6398bbd851be7655487c55492" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-utils 1.0.1", + "cw-utils 1.0.2", "schemars", "serde", ] @@ -912,7 +912,7 @@ checksum = "1d056ec33ec146554aa1d16c9535763341db75589a47743c006c377e62b54034" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-utils 1.0.1", + "cw-utils 1.0.2", "cw20 1.1.1", "schemars", "serde", @@ -1003,7 +1003,7 @@ checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" dependencies = [ "der 0.7.8", "digest 0.10.7", - "elliptic-curve 0.13.5", + "elliptic-curve 0.13.6", "rfc6979 0.4.0", "signature 2.1.0", "spki 0.7.2", @@ -1052,9 +1052,9 @@ dependencies = [ [[package]] name = "elliptic-curve" -version = "0.13.5" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914" dependencies = [ "base16ct 0.2.0", "crypto-bigint 0.5.3", @@ -1207,7 +1207,7 @@ checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" dependencies = [ "cfg-if", "ecdsa 0.16.8", - "elliptic-curve 0.13.5", + "elliptic-curve 0.13.6", "once_cell", "sha2 0.10.8", "signature 2.1.0", @@ -1215,14 +1215,14 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.148" +version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "neutron-sdk" version = "0.6.1" -source = "git+https://github.com/neutron-org/neutron-sdk#31af063f06bb90d46081a944a7df085d8f2ab493" +source = "git+https://github.com/neutron-org/neutron-sdk#74fea05e407e5ff7cdfc195c3a76d2cce6a47d20" dependencies = [ "base64 0.21.4", "bech32", @@ -1231,7 +1231,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus 1.1.0", "prost 0.11.9", - "protobuf 3.2.0", + "protobuf 3.3.0", "schemars", "serde", "serde-json-wasm 0.5.1", @@ -1252,9 +1252,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] @@ -1308,9 +1308,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.67" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c" dependencies = [ "unicode-ident", ] @@ -1381,9 +1381,9 @@ dependencies = [ [[package]] name = "protobuf" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55bad9126f378a853655831eb7363b7b01b81d19f8cb1218861086ca4a1a61e" +checksum = "b65f4a8ec18723a734e5dc09c173e0abf9690432da5340285d536edcb4dac190" dependencies = [ "bytes", "once_cell", @@ -1393,9 +1393,9 @@ dependencies = [ [[package]] name = "protobuf-support" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d4d7b8601c814cfb36bcebb79f0e61e45e1e93640cf778837833bbed05c372" +checksum = "6872f4d4f4b98303239a2b5838f5bbbb77b01ffc892d627957f37a22d7cfe69c" dependencies = [ "thiserror", ] @@ -1553,7 +1553,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1697,9 +1697,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.37" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", @@ -1759,7 +1759,7 @@ checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] From 39901ccd61bd4dde48d2eeb179f3da6effeac364 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 29 Aug 2023 22:51:47 +0200 Subject: [PATCH 065/586] test suite; fallback token distribution --- contracts/interchain-splitter/src/contract.rs | 1 + contracts/interchain-splitter/src/msg.rs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/contracts/interchain-splitter/src/contract.rs b/contracts/interchain-splitter/src/contract.rs index d2450417..2d494f69 100644 --- a/contracts/interchain-splitter/src/contract.rs +++ b/contracts/interchain-splitter/src/contract.rs @@ -1,3 +1,4 @@ +use cosmwasm_schema::cw_serde; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs index f993560e..b3c216c5 100644 --- a/contracts/interchain-splitter/src/msg.rs +++ b/contracts/interchain-splitter/src/msg.rs @@ -145,3 +145,22 @@ pub enum MigrateMsg { data: Option, }, } + +#[covenant_clock_address] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(SplitConfig)] + DenomSplit { denom: String }, + #[returns(Vec<(String, SplitConfig)>)] + Splits {}, + #[returns(SplitConfig)] + FallbackSplit {}, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum ProtocolGuildQueryMsg { + #[returns(SplitConfig)] + PublicGoodsSplit {}, +} \ No newline at end of file From f3795440092e38a92d4fd19087137693346e2987 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 5 Sep 2023 19:38:51 +0200 Subject: [PATCH 066/586] swap covenant init; presetFields on ibc forwarder, swap holder, interchain splitter --- Cargo.lock | 33 +++++ Cargo.toml | 1 + contracts/covenant/src/contract.rs | 22 ++-- contracts/covenant/src/state.rs | 6 +- contracts/depositor/src/contract.rs | 71 +++++----- contracts/depositor/src/msg.rs | 1 - contracts/depositor/src/state.rs | 16 +-- .../depositor/src/suite_test/unit_helpers.rs | 3 +- .../depositor/src/suite_test/unit_test.rs | 3 +- contracts/holder/src/contract.rs | 9 +- contracts/holder/src/msg.rs | 4 +- contracts/ibc-forwarder/src/contract.rs | 96 +++++++------- contracts/ibc-forwarder/src/msg.rs | 50 +++++++- contracts/ibc-forwarder/src/state.rs | 3 +- contracts/interchain-splitter/src/msg.rs | 23 ++++ contracts/lper/src/contract.rs | 72 +++++------ contracts/lper/src/msg.rs | 59 ++++++--- contracts/lper/src/state.rs | 4 +- contracts/lper/src/suite_test/suite.rs | 5 +- contracts/ls/src/contract.rs | 66 +++++----- contracts/ls/src/msg.rs | 7 +- contracts/ls/src/state.rs | 12 +- contracts/native-splitter/src/contract.rs | 121 ++++++++---------- contracts/native-splitter/src/msg.rs | 17 ++- contracts/native-splitter/src/state.rs | 11 +- contracts/swap-covenant/.cargo/config | 3 + contracts/swap-covenant/Cargo.toml | 56 ++++++++ contracts/swap-covenant/README.md | 3 + contracts/swap-covenant/src/contract.rs | 56 ++++++++ contracts/swap-covenant/src/error.rs | 27 ++++ contracts/swap-covenant/src/lib.rs | 14 ++ contracts/swap-covenant/src/msg.rs | 85 ++++++++++++ contracts/swap-covenant/src/state.rs | 32 +++++ contracts/swap-covenant/src/suite_test/mod.rs | 3 + .../swap-covenant/src/suite_test/suite.rs | 61 +++++++++ .../swap-covenant/src/suite_test/tests.rs | 0 .../src/suite_test/unit_tests.rs | 0 contracts/swap-holder/src/msg.rs | 29 ++++- 38 files changed, 763 insertions(+), 321 deletions(-) create mode 100644 contracts/swap-covenant/.cargo/config create mode 100644 contracts/swap-covenant/Cargo.toml create mode 100644 contracts/swap-covenant/README.md create mode 100644 contracts/swap-covenant/src/contract.rs create mode 100644 contracts/swap-covenant/src/error.rs create mode 100644 contracts/swap-covenant/src/lib.rs create mode 100644 contracts/swap-covenant/src/msg.rs create mode 100644 contracts/swap-covenant/src/state.rs create mode 100644 contracts/swap-covenant/src/suite_test/mod.rs create mode 100644 contracts/swap-covenant/src/suite_test/suite.rs create mode 100644 contracts/swap-covenant/src/suite_test/tests.rs create mode 100644 contracts/swap-covenant/src/suite_test/unit_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 887668ac..060c0ac2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -602,6 +602,39 @@ dependencies = [ "thiserror", ] +[[package]] +name = "covenant-swap" +version = "1.0.0" +dependencies = [ + "anyhow", + "astroport 2.8.0", + "base64 0.13.1", + "bech32", + "cosmos-sdk-proto 0.14.0", + "cosmwasm-schema", + "cosmwasm-std", + "covenant-clock", + "covenant-depositor", + "covenant-holder", + "covenant-lp", + "covenant-ls", + "covenant-swap-holder", + "covenant-utils", + "cw-multi-test", + "cw-storage-plus 1.1.0", + "cw-utils 1.0.1", + "cw2 1.1.0", + "neutron-sdk", + "prost 0.11.9", + "prost-types", + "protobuf 3.2.0", + "schemars", + "serde", + "serde-json-wasm 0.4.1", + "sha2 0.10.7", + "thiserror", +] + [[package]] name = "covenant-swap-holder" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index f07df102..e962974e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ covenant-holder = { path = "contracts/holder" } covenant-ibc-forwarder = { path = "contracts/ibc-forwarder" } covenant-native-splitter = { path = "contracts/native-splitter" } covenant-interchain-splitter = { path = "contract/interchain-splitter" } +covenant-swap-holder = { path = "contracts/swap-holder" } # packages clock-derive = { path = "packages/clock-derive" } diff --git a/contracts/covenant/src/contract.rs b/contracts/covenant/src/contract.rs index 3f22d236..e5074202 100644 --- a/contracts/covenant/src/contract.rs +++ b/contracts/covenant/src/contract.rs @@ -44,7 +44,7 @@ pub fn instantiate( LS_CODE.save(deps.storage, &msg.preset_ls_fields.ls_code)?; HOLDER_CODE.save(deps.storage, &msg.preset_holder_fields.holder_code)?; CLOCK_CODE.save(deps.storage, &msg.preset_clock_fields.clock_code)?; - + // validate and store the liquidity pool we wish to operate with let pool_addr = deps.api.addr_validate(&msg.pool_address)?; POOL_ADDRESS.save(deps.storage, &pool_addr)?; @@ -74,8 +74,7 @@ pub fn instantiate( .add_submessage(SubMsg::reply_on_success( clock_instantiate_tx, CLOCK_REPLY_ID, - )) - ) + ))) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -122,8 +121,7 @@ pub fn handle_clock_reply(deps: DepsMut, env: Env, msg: Reply) -> Result Err(ContractError::ContractInstantiationError { contract: "clock".to_string(), @@ -168,8 +166,7 @@ pub fn handle_holder_reply(deps: DepsMut, env: Env, msg: Reply) -> Result Err(ContractError::ContractInstantiationError { contract: "holder".to_string(), @@ -217,8 +214,7 @@ pub fn handle_lp_reply(deps: DepsMut, env: Env, msg: Reply) -> Result Err(ContractError::ContractInstantiationError { contract: "lp".to_string(), @@ -237,7 +233,7 @@ pub fn handle_ls_reply(deps: DepsMut, env: Env, msg: Reply) -> Result { // validate and store the LS address let ls_addr = deps.api.addr_validate(&response.contract_address)?; - COVENANT_LS_ADDR.save(deps.storage,&ls_addr)?; + COVENANT_LS_ADDR.save(deps.storage, &ls_addr)?; // load the fields relevant to depositor instantiation let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; @@ -272,8 +268,7 @@ pub fn handle_ls_reply(deps: DepsMut, env: Env, msg: Reply) -> Result Err(ContractError::ContractInstantiationError { contract: "ls".to_string(), @@ -322,8 +317,7 @@ pub fn handle_depositor_reply( Ok(Response::default() .add_message(update_clock_whitelist_msg) .add_attribute("depositor_address", depositor_addr) - .add_attribute("method", "handle_depositor_reply") - ) + .add_attribute("method", "handle_depositor_reply")) } Err(err) => Err(ContractError::ContractInstantiationError { contract: "depositor".to_string(), diff --git a/contracts/covenant/src/state.rs b/contracts/covenant/src/state.rs index 23a70dd1..acde790e 100644 --- a/contracts/covenant/src/state.rs +++ b/contracts/covenant/src/state.rs @@ -25,15 +25,15 @@ pub const IBC_FEE: Item = Item::new("ibc_fee"); pub const TIMEOUTS: Item = Item::new("timeouts"); /// fields related to the liquid staker module known prior to covenant instatiation. -/// remaining fields are filled and converted to an InstantiateMsg during the +/// remaining fields are filled and converted to an InstantiateMsg during the /// instantiation chain. pub const PRESET_LS_FIELDS: Item = Item::new("preset_ls_fields"); /// fields related to the liquid pooler module known prior to covenant instatiation. -/// remaining fields are filled and converted to an InstantiateMsg during the +/// remaining fields are filled and converted to an InstantiateMsg during the /// instantiation chain. pub const PRESET_LP_FIELDS: Item = Item::new("preset_lp_fields"); /// fields related to the depositor module known prior to covenant instatiation. -/// remaining fields are filled and converted to an InstantiateMsg during the +/// remaining fields are filled and converted to an InstantiateMsg during the /// instantiation chain. pub const PRESET_DEPOSITOR_FIELDS: Item = Item::new("preset_depositor_fields"); diff --git a/contracts/depositor/src/contract.rs b/contracts/depositor/src/contract.rs index 4a4cf975..a7b68ede 100644 --- a/contracts/depositor/src/contract.rs +++ b/contracts/depositor/src/contract.rs @@ -14,7 +14,10 @@ use neutron_sdk::bindings::types::ProtobufAny; use prost::Message; use crate::{ - msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, OpenAckVersion, QueryMsg, ContractState, SudoPayload}, + msg::{ + ContractState, ExecuteMsg, InstantiateMsg, MigrateMsg, OpenAckVersion, QueryMsg, + SudoPayload, + }, state::{ IBC_TRANSFER_TIMEOUT, ICA_TIMEOUT, NEUTRON_ATOM_IBC_DENOM, PENDING_NATIVE_TRANSFER_TIMEOUT, }, @@ -30,12 +33,11 @@ use neutron_sdk::{ }; use crate::state::{ - read_errors_from_queue, read_reply_payload, read_sudo_payload, - save_reply_payload, save_sudo_payload, - ACKNOWLEDGEMENT_RESULTS, AUTOPILOT_FORMAT, CLOCK_ADDRESS, CONTRACT_STATE, + read_errors_from_queue, read_reply_payload, read_sudo_payload, save_reply_payload, + save_sudo_payload, ACKNOWLEDGEMENT_RESULTS, AUTOPILOT_FORMAT, CLOCK_ADDRESS, CONTRACT_STATE, GAIA_NEUTRON_IBC_TRANSFER_CHANNEL_ID, GAIA_STRIDE_IBC_TRANSFER_CHANNEL_ID, IBC_FEE, - INTERCHAIN_ACCOUNTS, LS_ADDRESS, NATIVE_ATOM_RECEIVER, - NEUTRON_GAIA_CONNECTION_ID, STRIDE_ATOM_RECEIVER, + INTERCHAIN_ACCOUNTS, LS_ADDRESS, NATIVE_ATOM_RECEIVER, NEUTRON_GAIA_CONNECTION_ID, + STRIDE_ATOM_RECEIVER, }; type QueryDeps<'a> = Deps<'a, NeutronQuery>; @@ -79,10 +81,10 @@ pub fn instantiate( GAIA_STRIDE_IBC_TRANSFER_CHANNEL_ID .save(deps.storage, &msg.gaia_stride_ibc_transfer_channel_id)?; NEUTRON_GAIA_CONNECTION_ID.save(deps.storage, &msg.neutron_gaia_connection_id)?; - + // autopilot string formatting AUTOPILOT_FORMAT.save(deps.storage, &msg.autopilot_format)?; - + // ibc fees and timeouts IBC_FEE.save(deps.storage, &msg.ibc_fee)?; ICA_TIMEOUT.save(deps.storage, &msg.ica_timeout)?; @@ -93,14 +95,18 @@ pub fn instantiate( .add_attribute("clock_address", clock_addr) .add_attribute("ls_address", ls_addr) .add_attribute("neutron_atom_ibc_denom", msg.neutron_atom_ibc_denom) - .add_attribute("gaia_neutron_ibc_transfer_channel_id", msg.gaia_neutron_ibc_transfer_channel_id) - .add_attribute("gaia_stride_ibc_transfer_channel_id", msg.gaia_stride_ibc_transfer_channel_id) + .add_attribute( + "gaia_neutron_ibc_transfer_channel_id", + msg.gaia_neutron_ibc_transfer_channel_id, + ) + .add_attribute( + "gaia_stride_ibc_transfer_channel_id", + msg.gaia_stride_ibc_transfer_channel_id, + ) .add_attribute("neutron_gaia_connection_id", msg.neutron_gaia_connection_id) .add_attribute("autopilot_format", msg.autopilot_format) .add_attribute("ica_timeout", msg.ica_timeout) - .add_attribute("ibc_transfer_timeout", msg.ibc_transfer_timeout) - - ) + .add_attribute("ibc_transfer_timeout", msg.ibc_transfer_timeout)) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -129,22 +135,17 @@ fn try_tick(deps: ExecuteDeps, env: Env, info: MessageInfo) -> NeutronResult { let ica_address = get_ica(deps.as_ref(), &env, INTERCHAIN_ACCOUNT_ID); match ica_address { - Ok((_, _)) => { - try_send_native_token(env, deps) - }, - Err(_) => { - Ok(Response::default() - .add_attribute("method", "try_tick") - .add_attribute("ica_status", "not_created") - ) - }, + Ok((_, _)) => try_send_native_token(env, deps), + Err(_) => Ok(Response::default() + .add_attribute("method", "try_tick") + .add_attribute("ica_status", "not_created")), } } ContractState::VerifyNativeToken => try_verify_native_token(env, deps), ContractState::VerifyLp => try_verify_lp(env, deps), ContractState::Complete => { Ok(Response::default().add_attribute("status", "function_completed")) - }, + } } } @@ -248,9 +249,10 @@ fn try_send_ls_token(env: Env, mut deps: ExecuteDeps) -> NeutronResult = deps - .querier - .query_wasm_smart(ls_address, &covenant_utils::neutron_ica::QueryMsg::DepositAddress {})?; + let stride_ica_query: Option = deps.querier.query_wasm_smart( + ls_address, + &covenant_utils::neutron_ica::QueryMsg::DepositAddress {}, + )?; let stride_ica_addr = match stride_ica_query { Some(addr) => addr, None => return Err(NeutronError::Std(StdError::not_found("no LS ica found"))), @@ -350,10 +352,9 @@ fn try_verify_native_token(env: Env, deps: ExecuteDeps) -> NeutronResult= active_timeout.plus_minutes(5).nanos() { @@ -365,8 +366,7 @@ fn try_verify_native_token(env: Env, deps: ExecuteDeps) -> NeutronResult StdResult Std } } - Ok(Response::default() - .add_attribute("method", "sudo_response") - ) + Ok(Response::default().add_attribute("method", "sudo_response")) } fn sudo_timeout(deps: ExecuteDeps, _env: Env, request: RequestPacket) -> StdResult { @@ -718,7 +717,7 @@ fn sudo_timeout(deps: ExecuteDeps, _env: Env, request: RequestPacket) -> StdResu fn sudo_error(deps: ExecuteDeps, request: RequestPacket, details: String) -> StdResult { deps.api - .debug(format!("WASMDEBUG: sudo error: {details}").as_str()); + .debug(format!("WASMDEBUG: sudo error: {details}").as_str()); deps.api .debug(format!("WASMDEBUG: request packet: {request:?}").as_str()); diff --git a/contracts/depositor/src/msg.rs b/contracts/depositor/src/msg.rs index 5eadc1a1..9d50dc25 100644 --- a/contracts/depositor/src/msg.rs +++ b/contracts/depositor/src/msg.rs @@ -198,7 +198,6 @@ pub enum ContractState { Complete, } - /// SudoPayload is a type that stores information about a transaction that we try to execute /// on the host chain. This is a type introduced for our convenience. #[cw_serde] diff --git a/contracts/depositor/src/state.rs b/contracts/depositor/src/state.rs index 55d287a8..960fb84c 100644 --- a/contracts/depositor/src/state.rs +++ b/contracts/depositor/src/state.rs @@ -1,9 +1,9 @@ +use crate::msg::{AcknowledgementResult, ContractState, SudoPayload, WeightedReceiver}; use cosmwasm_std::{ from_binary, to_vec, Addr, Binary, Order, StdResult, Storage, Timestamp, Uint64, }; use cw_storage_plus::{Item, Map}; use neutron_sdk::bindings::msg::IbcFee; -use crate::msg::{WeightedReceiver, AcknowledgementResult, SudoPayload, ContractState}; /// tracks the current state of state machine pub const CONTRACT_STATE: Item = Item::new("contract_state"); @@ -16,7 +16,7 @@ pub const LS_ADDRESS: Item = Item::new("ls_address"); pub const LP_ADDRESS: Item = Item::new("lp_address"); /// formatting of stride autopilot message. -/// we use string match & replace with relevant fields to obtain the valid message. +/// we use string match & replace with relevant fields to obtain the valid message. pub const AUTOPILOT_FORMAT: Item = Item::new("autopilot_format"); /// addr and amount of atom to liquid stake on stride @@ -42,7 +42,8 @@ pub const ICA_TIMEOUT: Item = Item::new("ica_timeout"); pub const IBC_FEE: Item = Item::new("ibc_fee"); /// interchain accounts storage in form of (port_id) -> (address, controller_connection_id) -pub const INTERCHAIN_ACCOUNTS: Map> = Map::new("interchain_accounts"); +pub const INTERCHAIN_ACCOUNTS: Map> = + Map::new("interchain_accounts"); // pending transaction timeout timestamp pub const PENDING_NATIVE_TRANSFER_TIMEOUT: Item = @@ -57,7 +58,6 @@ pub const ACKNOWLEDGEMENT_RESULTS: Map<(String, u64), AcknowledgementResult> = pub const ERRORS_QUEUE: Map = Map::new("errors_queue"); - pub fn save_reply_payload(store: &mut dyn Storage, payload: SudoPayload) -> StdResult<()> { REPLY_ID_STORAGE.save(store, &to_vec(&payload)?) } @@ -102,10 +102,6 @@ pub fn save_sudo_payload( SUDO_PAYLOAD.save(store, (channel_id, seq_id), &to_vec(&payload)?) } -pub fn clear_sudo_payload( - store: &mut dyn Storage, - channel_id: String, - seq_id: u64, -) { +pub fn clear_sudo_payload(store: &mut dyn Storage, channel_id: String, seq_id: u64) { SUDO_PAYLOAD.remove(store, (channel_id, seq_id)) -} \ No newline at end of file +} diff --git a/contracts/depositor/src/suite_test/unit_helpers.rs b/contracts/depositor/src/suite_test/unit_helpers.rs index 75d240b0..bf168aa1 100644 --- a/contracts/depositor/src/suite_test/unit_helpers.rs +++ b/contracts/depositor/src/suite_test/unit_helpers.rs @@ -21,7 +21,8 @@ use prost::Message; use crate::{ contract::{execute, instantiate, INTERCHAIN_ACCOUNT_ID}, msg::{ - ExecuteMsg, InstantiateMsg, OpenAckVersion, PresetDepositorFields, WeightedReceiverAmount, ContractState, + ContractState, ExecuteMsg, InstantiateMsg, OpenAckVersion, PresetDepositorFields, + WeightedReceiverAmount, }, state::CONTRACT_STATE, }; diff --git a/contracts/depositor/src/suite_test/unit_test.rs b/contracts/depositor/src/suite_test/unit_test.rs index db2f851d..9aecf33a 100644 --- a/contracts/depositor/src/suite_test/unit_test.rs +++ b/contracts/depositor/src/suite_test/unit_test.rs @@ -3,10 +3,11 @@ use neutron_sdk::bindings::{msg::NeutronMsg, types::ProtobufAny}; use crate::{ contract::{sudo, INTERCHAIN_ACCOUNT_ID}, + msg::ContractState, suite_test::unit_helpers::{ get_default_ibc_fee, get_default_init_msg, get_default_msg_transfer, get_default_sudo_open_ack, to_proto, CLOCK_ADDR, LP_ADDR, NATIVE_ATOM_DENOM, - }, msg::ContractState, + }, }; use super::unit_helpers::{do_instantiate, do_tick, verify_state, Owned}; diff --git a/contracts/holder/src/contract.rs b/contracts/holder/src/contract.rs index c928a1a9..16979019 100644 --- a/contracts/holder/src/contract.rs +++ b/contracts/holder/src/contract.rs @@ -10,7 +10,7 @@ use cw20::{BalanceResponse, Cw20ExecuteMsg}; use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; -use crate::state::{WITHDRAWER, POOL_ADDRESS}; +use crate::state::{POOL_ADDRESS, WITHDRAWER}; const CONTRACT_NAME: &str = "crates.io:covenant-holder"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -88,9 +88,10 @@ fn try_withdraw_liquidity( // We query the pool to get the contract for the pool info // The pool info is required to fetch the address of the // liquidity token contract. The liquidity tokens are CW20 tokens - let pair_info: astroport::asset::PairInfo = deps - .querier - .query_wasm_smart(pool_address.to_string(), &astroport::pair::QueryMsg::Pair {})?; + let pair_info: astroport::asset::PairInfo = deps.querier.query_wasm_smart( + pool_address.to_string(), + &astroport::pair::QueryMsg::Pair {}, + )?; // We query our own liquidity token balance let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( diff --git a/contracts/holder/src/msg.rs b/contracts/holder/src/msg.rs index 6a9f50e2..8989aa03 100644 --- a/contracts/holder/src/msg.rs +++ b/contracts/holder/src/msg.rs @@ -37,9 +37,7 @@ pub enum ExecuteMsg { /// The withdraw can specify a quanity to be withdrawn. If no /// quantity is specified, the full balance is withdrawn /// into withdrawer account - Withdraw { - quantity: Option>, - }, + Withdraw { quantity: Option> }, /// The WithdrawLiqudity message can only be called by the withdrawer /// When it is called, the LP tokens are burned and the liquity is withdrawn /// from the pool and lands in the holder diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index e1601581..e78e7360 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -1,14 +1,30 @@ use cosmos_sdk_proto::ibc::applications::transfer::v1::MsgTransfer; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use covenant_utils::neutron_ica::{self, RemoteChainInfo, get_proto_coin}; -use cosmwasm_std::{Env, MessageInfo, Response, Deps, DepsMut, StdError, Binary, to_binary, StdResult, Storage, to_vec, CosmosMsg, SubMsg, Reply, from_binary, CustomQuery}; +use cosmwasm_std::{ + from_binary, to_binary, to_vec, Binary, CosmosMsg, CustomQuery, Deps, DepsMut, Env, + MessageInfo, Reply, Response, StdError, StdResult, Storage, SubMsg, +}; use covenant_clock::helpers::verify_clock; +use covenant_utils::neutron_ica::{self, get_proto_coin, RemoteChainInfo}; use cw2::set_contract_version; -use neutron_sdk::{NeutronResult, bindings::{msg::{NeutronMsg, MsgSubmitTxResponse}, query::NeutronQuery}, interchain_txs::helpers::get_port_id, NeutronError, sudo::msg::{SudoMsg, RequestPacket},}; - -use crate::{msg::{InstantiateMsg, ExecuteMsg, ContractState, QueryMsg}, state::{CONTRACT_STATE, CLOCK_ADDRESS, INTERCHAIN_ACCOUNTS, REMOTE_CHAIN_INFO, NEXT_CONTRACT, REPLY_ID_STORAGE, SUDO_PAYLOAD, TRANSFER_AMOUNT}}; - +use neutron_sdk::{ + bindings::{ + msg::{MsgSubmitTxResponse, NeutronMsg}, + query::NeutronQuery, + }, + interchain_txs::helpers::get_port_id, + sudo::msg::{RequestPacket, SudoMsg}, + NeutronError, NeutronResult, +}; + +use crate::{ + msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg}, + state::{ + CLOCK_ADDRESS, CONTRACT_STATE, INTERCHAIN_ACCOUNTS, NEXT_CONTRACT, REMOTE_CHAIN_INFO, + REPLY_ID_STORAGE, SUDO_PAYLOAD, TRANSFER_AMOUNT, + }, +}; const CONTRACT_NAME: &str = "crates.io:covenant-ibc-forwarder"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -40,13 +56,12 @@ pub fn instantiate( }; REMOTE_CHAIN_INFO.save(deps.storage, &remote_chain_info)?; CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; - + Ok(Response::default() .add_attribute("method", "ibc_forwarder_instantiate") .add_attribute("next_contract", next_contract) .add_attribute("contract_state", "instantiated") - .add_attributes(remote_chain_info.get_response_attributes()) - ) + .add_attributes(remote_chain_info.get_response_attributes())) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -70,35 +85,32 @@ fn try_tick(deps: ExecuteDeps, env: Env, info: MessageInfo) -> NeutronResult try_register_ica(deps, env), ContractState::IcaCreated => try_forward_funds(env, deps), - ContractState::Complete => Ok(Response::default() - .add_attribute("contract_state", "completed") - ), + ContractState::Complete => { + Ok(Response::default().add_attribute("contract_state", "completed")) + } } } /// tries to register an ICA on the remote chain fn try_register_ica(deps: ExecuteDeps, env: Env) -> NeutronResult> { - let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; - + let register_msg = NeutronMsg::register_interchain_account( remote_chain_info.connection_id, - INTERCHAIN_ACCOUNT_ID.to_string() + INTERCHAIN_ACCOUNT_ID.to_string(), ); let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); - + // we are saving empty data here because we handle response of registering ICA in sudo_open_ack method INTERCHAIN_ACCOUNTS.save(deps.storage, key, &None)?; Ok(Response::new() .add_attribute("method", "try_register_ica") - .add_message(register_msg) - ) + .add_message(register_msg)) } fn try_forward_funds(env: Env, mut deps: ExecuteDeps) -> NeutronResult> { - // first we verify whether the next contract is ready for receiving the funds let next_contract = NEXT_CONTRACT.load(deps.storage)?; let deposit_address_query: Option = deps.querier.query_wasm_smart( @@ -114,10 +126,7 @@ fn try_forward_funds(env: Env, mut deps: ExecuteDeps) -> NeutronResult { @@ -131,7 +140,9 @@ fn try_forward_funds(env: Env, mut deps: ExecuteDeps) -> NeutronResult NeutronResult { // I can't think of a case of how we could end up here as `sudo_open_ack` // callback advances the state to `ICACreated` and stores the ICA. @@ -170,9 +181,8 @@ fn try_forward_funds(env: Env, mut deps: ExecuteDeps) -> NeutronResult NeutronResult match msg { QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), // we expect to receive funds into our ICA account on the remote chain. - // if the ICA had not been opened yet, we return `None` so that the + // if the ICA had not been opened yet, we return `None` so that the // contract querying this will be instructed to wait and retry. QueryMsg::DepositAddress {} => { let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); @@ -203,12 +213,12 @@ pub fn query(deps: QueryDeps, env: Env, msg: QueryMsg) -> NeutronResult } else { None } - }, + } None => None, }; Ok(to_binary(&ica)?) - }, + } QueryMsg::IcaAddress {} => Ok(to_binary(&get_ica(deps, &env, INTERCHAIN_ACCOUNT_ID)?.0)?), QueryMsg::RemoteChainInfo {} => Ok(to_binary(&REMOTE_CHAIN_INFO.may_load(deps.storage)?)?), } @@ -226,7 +236,6 @@ fn get_ica( .ok_or_else(|| StdError::generic_err("Interchain account is not created yet")) } - #[cfg_attr(not(feature = "library"), entry_point)] pub fn sudo(deps: ExecuteDeps, env: Env, msg: SudoMsg) -> StdResult { deps.api @@ -260,7 +269,6 @@ pub fn sudo(deps: ExecuteDeps, env: Env, msg: SudoMsg) -> StdResult { } } - // handler fn sudo_open_ack( deps: ExecuteDeps, @@ -279,7 +287,7 @@ fn sudo_open_ack( let Ok(parsed_version) = parsed_version else { return Err(StdError::generic_err("Can't parse counterparty_version")) }; - + // Update the storage record associated with the interchain account. INTERCHAIN_ACCOUNTS.save( deps.storage, @@ -290,10 +298,8 @@ fn sudo_open_ack( )), )?; CONTRACT_STATE.save(deps.storage, &ContractState::IcaCreated)?; - - Ok(Response::default() - .add_attribute("method", "sudo_open_ack") - ) + + Ok(Response::default().add_attribute("method", "sudo_open_ack")) } fn sudo_response(deps: ExecuteDeps, request: RequestPacket, data: Binary) -> StdResult { @@ -309,9 +315,7 @@ fn sudo_response(deps: ExecuteDeps, request: RequestPacket, data: Binary) -> Std .source_channel .ok_or_else(|| StdError::generic_err("channel_id not found"))?; - Ok(Response::default() - .add_attribute("method", "sudo_response") - ) + Ok(Response::default().add_attribute("method", "sudo_response")) } fn sudo_timeout(deps: ExecuteDeps, _env: Env, request: RequestPacket) -> StdResult { @@ -327,7 +331,7 @@ fn sudo_timeout(deps: ExecuteDeps, _env: Env, request: RequestPacket) -> StdResu fn sudo_error(deps: ExecuteDeps, request: RequestPacket, details: String) -> StdResult { deps.api - .debug(format!("WASMDEBUG: sudo error: {details}").as_str()); + .debug(format!("WASMDEBUG: sudo error: {details}").as_str()); deps.api .debug(format!("WASMDEBUG: request packet: {request:?}").as_str()); @@ -344,8 +348,10 @@ fn sudo_error(deps: ExecuteDeps, request: RequestPacket, details: String) -> Std Ok(Response::default().add_attribute("method", "sudo_error")) } - -pub fn save_reply_payload(store: &mut dyn Storage, payload: neutron_ica::SudoPayload) -> StdResult<()> { +pub fn save_reply_payload( + store: &mut dyn Storage, + payload: neutron_ica::SudoPayload, +) -> StdResult<()> { REPLY_ID_STORAGE.save(store, &to_vec(&payload)?) } @@ -393,4 +399,4 @@ pub fn save_sudo_payload( payload: neutron_ica::SudoPayload, ) -> StdResult<()> { SUDO_PAYLOAD.save(store, (channel_id, seq_id), &to_vec(&payload)?) -} \ No newline at end of file +} diff --git a/contracts/ibc-forwarder/src/msg.rs b/contracts/ibc-forwarder/src/msg.rs index 851abaed..1beb8f26 100644 --- a/contracts/ibc-forwarder/src/msg.rs +++ b/contracts/ibc-forwarder/src/msg.rs @@ -1,8 +1,11 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Uint64, Attribute, Addr, Uint128}; -use covenant_macros::{clocked, covenant_deposit_address, covenant_clock_address, covenant_remote_chain, covenant_ica_address}; -use neutron_sdk::bindings::msg::IbcFee; +use cosmwasm_std::{Addr, Attribute, Uint128, Uint64}; +use covenant_macros::{ + clocked, covenant_clock_address, covenant_deposit_address, covenant_ica_address, + covenant_remote_chain, +}; use covenant_utils::neutron_ica::RemoteChainInfo; +use neutron_sdk::bindings::msg::IbcFee; #[cw_serde] pub struct InstantiateMsg { @@ -36,15 +39,52 @@ pub struct InstantiateMsg { pub ica_timeout: Uint64, } +#[cw_serde] +pub struct PresetIbcForwarderFields { + pub remote_chain_connection_id: String, + pub remote_chain_channel_id: String, + pub denom: String, + pub amount: Uint128, +} + +impl PresetIbcForwarderFields { + pub fn to_instantiate_msg( + self, + clock_address: String, + next_contract: String, + ibc_fee: IbcFee, + ibc_transfer_timeout: Uint64, + ica_timeout: Uint64, + ) -> InstantiateMsg { + InstantiateMsg { + clock_address, + next_contract, + remote_chain_connection_id: self.remote_chain_connection_id, + remote_chain_channel_id: self.remote_chain_channel_id, + denom: self.denom, + amount: self.amount, + ibc_fee, + ibc_transfer_timeout, + ica_timeout, + } + } +} + impl InstantiateMsg { pub fn get_response_attributes(&self) -> Vec { vec![ Attribute::new("clock_address", &self.clock_address), - Attribute::new("remote_chain_connection_id", &self.remote_chain_connection_id), + Attribute::new( + "remote_chain_connection_id", + &self.remote_chain_connection_id, + ), Attribute::new("remote_chain_channel_id", &self.remote_chain_channel_id), Attribute::new("remote_chain_denom", &self.denom), Attribute::new("remote_chain_amount", &self.amount.to_string()), - Attribute::new("ibc_transfer_timeout", self.ibc_transfer_timeout.to_string()), + Attribute::new( + "ibc_transfer_timeout", + self.ibc_transfer_timeout.to_string(), + ), Attribute::new("ica_timeout", self.ica_timeout.to_string()), ] } diff --git a/contracts/ibc-forwarder/src/state.rs b/contracts/ibc-forwarder/src/state.rs index 6741f0b2..50c643c2 100644 --- a/contracts/ibc-forwarder/src/state.rs +++ b/contracts/ibc-forwarder/src/state.rs @@ -17,7 +17,8 @@ pub const NEXT_CONTRACT: Item = Item::new("next_contract"); pub const REMOTE_CHAIN_INFO: Item = Item::new("r_c_info"); /// interchain accounts storage in form of (port_id) -> (address, controller_connection_id) -pub const INTERCHAIN_ACCOUNTS: Map> = Map::new("interchain_accounts"); +pub const INTERCHAIN_ACCOUNTS: Map> = + Map::new("interchain_accounts"); pub const REPLY_ID_STORAGE: Item> = Item::new("reply_queue_id"); pub const SUDO_PAYLOAD: Map<(String, u64), Vec> = Map::new("sudo_payload"); diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs index b3c216c5..2c19620e 100644 --- a/contracts/interchain-splitter/src/msg.rs +++ b/contracts/interchain-splitter/src/msg.rs @@ -17,6 +17,29 @@ pub struct InstantiateMsg { pub fallback_split: Option, } + +#[cw_serde] +pub struct PresetInterchainSplitterFields { + /// list of (denom, split) configurations + pub splits: Vec<(String, SplitType)>, + /// a split for all denoms that are not covered in the + /// regular `splits` list + pub fallback_split: Option, +} + +impl PresetInterchainSplitterFields { + pub fn to_instantiate_msg( + self, + clock_address: String, + ) -> InstantiateMsg { + InstantiateMsg { + clock_address, + splits: self.splits, + fallback_split: self.fallback_split, + } + } +} + #[clocked] #[cw_serde] pub enum ExecuteMsg {} diff --git a/contracts/lper/src/contract.rs b/contracts/lper/src/contract.rs index 6951e3fe..3e278358 100644 --- a/contracts/lper/src/contract.rs +++ b/contracts/lper/src/contract.rs @@ -15,12 +15,11 @@ use astroport::{ use crate::{ error::ContractError, - msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, ProvidedLiquidityInfo, ContractState, LpConfig}, - state::{ - ASSETS, - HOLDER_ADDRESS, PROVIDED_LIQUIDITY_INFO, - LP_CONFIG, + msg::{ + ContractState, ExecuteMsg, InstantiateMsg, LpConfig, MigrateMsg, ProvidedLiquidityInfo, + QueryMsg, }, + state::{ASSETS, HOLDER_ADDRESS, LP_CONFIG, PROVIDED_LIQUIDITY_INFO}, }; use neutron_sdk::NeutronResult; @@ -44,7 +43,7 @@ pub fn instantiate( ) -> Result { deps.api.debug("WASMDEBUG: lp instantiate"); set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - + // validate the contract addresses let clock_addr = deps.api.addr_validate(&msg.clock_address)?; let pool_addr = deps.api.addr_validate(&msg.pool_address)?; @@ -85,8 +84,7 @@ pub fn instantiate( .add_attribute("holder_addr", holder_addr) .add_attribute("ls_asset_denom", msg.assets.ls_asset_denom) .add_attribute("native_asset_denom", msg.assets.native_asset_denom) - .add_attributes(lp_config.to_response_attributes()) - ) + .add_attributes(lp_config.to_response_attributes())) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -114,13 +112,15 @@ fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> Result Result { let asset_data = ASSETS.load(deps.storage)?; // first we query our own balances and filter out any unexpected denoms - let bal_coins = deps.querier.query_all_balances(env.contract.address.to_string())?; + let bal_coins = deps + .querier + .query_all_balances(env.contract.address.to_string())?; let (native_bal, ls_bal) = get_relevant_balances( bal_coins, asset_data.ls_asset_denom, @@ -131,31 +131,25 @@ fn try_lp(mut deps: DepsMut, env: Env) -> Result { match (native_bal.amount.is_zero(), ls_bal.amount.is_zero()) { // one balance is non-zero, we attempt single-side (true, false) | (false, true) => { - let single_sided_submsg = try_get_single_side_lp_submsg( - deps.branch(), - native_bal, - ls_bal, - )?; + let single_sided_submsg = + try_get_single_side_lp_submsg(deps.branch(), native_bal, ls_bal)?; if let Some(msg) = single_sided_submsg { return Ok(Response::default() .add_submessage(msg) .add_attribute("method", "single_side_lp")); } - }, + } // both balances are non-zero, we attempt double-side (false, false) => { - let double_sided_submsg = try_get_double_side_lp_submsg( - deps.branch(), - native_bal, - ls_bal, - )?; - + let double_sided_submsg = + try_get_double_side_lp_submsg(deps.branch(), native_bal, ls_bal)?; + if let Some(msg) = double_sided_submsg { return Ok(Response::default() .add_submessage(msg) .add_attribute("method", "double_side_lp")); } - }, + } // both balances zero, no liquidity can be provisioned _ => (), } @@ -163,8 +157,7 @@ fn try_lp(mut deps: DepsMut, env: Env) -> Result { // if no message could be constructed, we keep waiting for funds Ok(Response::default() .add_attribute("method", "try_lp") - .add_attribute("status", "not enough funds") - ) + .add_attribute("status", "not enough funds")) } /// attempts to get a double sided ProvideLiquidity submessage. @@ -191,10 +184,7 @@ fn try_get_double_side_lp_submsg( )?; // we validate the pool to match our price expectations - lp_config.validate_price_range( - pool_native_bal, - pool_ls_bal, - )?; + lp_config.validate_price_range(pool_native_bal, pool_ls_bal)?; // we derive the ratio of native to ls. // using this ratio we know how many native tokens we should provide for every one ls token @@ -218,7 +208,7 @@ fn try_get_double_side_lp_submsg( Asset { info: asset_data.get_ls_asset_info(), amount: ls_bal.amount, - } + }, ) } else { // otherwise, our native token amount is insufficient to provide double @@ -242,13 +232,10 @@ fn try_get_double_side_lp_submsg( native_asset_double_sided.to_coin()?, ls_asset_double_sided.to_coin()?, ); - + // craft a ProvideLiquidity message with the determined assets let double_sided_liq_msg = ProvideLiquidity { - assets: vec![ - native_asset_double_sided, - ls_asset_double_sided, - ], + assets: vec![native_asset_double_sided, ls_asset_double_sided], slippage_tolerance: lp_config.slippage_tolerance, auto_stake: lp_config.autostake, receiver: Some(holder_address.to_string()), @@ -258,9 +245,7 @@ fn try_get_double_side_lp_submsg( PROVIDED_LIQUIDITY_INFO.update( deps.storage, |mut info: ProvidedLiquidityInfo| -> StdResult<_> { - info.provided_amount_ls = info - .provided_amount_ls - .checked_add(ls_coin.amount)?; + info.provided_amount_ls = info.provided_amount_ls.checked_add(ls_coin.amount)?; info.provided_amount_native = info .provided_amount_native .checked_add(native_coin.amount)?; @@ -302,7 +287,9 @@ fn try_get_single_side_lp_submsg( }; // now we try to submit the message for either LS or native single side liquidity - if native_bal.amount.is_zero() && ls_bal.amount <= lp_config.single_side_lp_limits.ls_asset_limit { + if native_bal.amount.is_zero() + && ls_bal.amount <= lp_config.single_side_lp_limits.ls_asset_limit + { // update the provided liquidity info PROVIDED_LIQUIDITY_INFO.update(deps.storage, |mut info| -> StdResult<_> { info.provided_amount_ls = info.provided_amount_ls.checked_add(ls_bal.amount)?; @@ -321,7 +308,8 @@ fn try_get_single_side_lp_submsg( return Ok(Some(submsg)); } else if ls_bal.amount.is_zero() - && native_bal.amount <= lp_config.single_side_lp_limits.native_asset_limit { + && native_bal.amount <= lp_config.single_side_lp_limits.native_asset_limit + { // update the provided liquidity info PROVIDED_LIQUIDITY_INFO.update(deps.storage, |mut info| -> StdResult<_> { info.provided_amount_native = @@ -384,7 +372,6 @@ fn get_pool_asset_amounts( Ok((native_bal, ls_bal)) } - #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { @@ -424,7 +411,8 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> NeutronResult, - /// slippage tolerance parameter for liquidity provisioning + /// slippage tolerance parameter for liquidity provisioning pub slippage_tolerance: Option, } @@ -48,17 +48,26 @@ impl LpConfig { None => "None".to_string(), }; vec![ - Attribute::new("expected_native_token_amount", self.expected_native_token_amount.to_string()), - Attribute::new("expected_ls_token_amount", self.expected_ls_token_amount.to_string()), - Attribute::new("allowed_return_delta", self.allowed_return_delta.to_string()), + Attribute::new( + "expected_native_token_amount", + self.expected_native_token_amount.to_string(), + ), + Attribute::new( + "expected_ls_token_amount", + self.expected_ls_token_amount.to_string(), + ), + Attribute::new( + "allowed_return_delta", + self.allowed_return_delta.to_string(), + ), Attribute::new("pool_address", self.pool_address.to_string()), Attribute::new( "single_side_lp_limit_native", - self.single_side_lp_limits.native_asset_limit.to_string() + self.single_side_lp_limits.native_asset_limit.to_string(), ), Attribute::new( "single_side_lp_limit_ls", - self.single_side_lp_limits.ls_asset_limit.to_string() + self.single_side_lp_limits.ls_asset_limit.to_string(), ), Attribute::new("autostake", autostake), Attribute::new("slippage_tolerance", slippage_tolerance), @@ -66,29 +75,37 @@ impl LpConfig { } /// validates the existing pool balances to match our initial expectations. - /// if `PriceRangeError` is returned, it most likely means that the pool had a + /// if `PriceRangeError` is returned, it most likely means that the pool had a /// significant shift in its balance ratio. - pub fn validate_price_range(&self, pool_native_bal: Uint128, pool_ls_bal: Uint128) -> Result<(), ContractError> { + pub fn validate_price_range( + &self, + pool_native_bal: Uint128, + pool_ls_bal: Uint128, + ) -> Result<(), ContractError> { // find the min return amount by subtracting the delta from expected amount - let min_return_amount = self.expected_ls_token_amount + let min_return_amount = self + .expected_ls_token_amount .checked_sub(self.allowed_return_delta)?; // find the max return amount by adding the delta to expected amount - let max_return_amount = self.expected_ls_token_amount + let max_return_amount = self + .expected_ls_token_amount .checked_add(self.allowed_return_delta)?; - + // derive allowed proportions - let min_accepted_ratio = Decimal::from_ratio(min_return_amount, self.expected_native_token_amount); - let max_accepted_ratio = Decimal::from_ratio(max_return_amount, self.expected_native_token_amount); - + let min_accepted_ratio = + Decimal::from_ratio(min_return_amount, self.expected_native_token_amount); + let max_accepted_ratio = + Decimal::from_ratio(max_return_amount, self.expected_native_token_amount); + // we find the proportion of the price range being validated let validation_ratio = Decimal::from_ratio(pool_ls_bal, pool_native_bal); - + // if current return to offer amount ratio falls out of [min_accepted_ratio, max_return_amount], // return price range error if validation_ratio < min_accepted_ratio || validation_ratio > max_accepted_ratio { return Err(ContractError::PriceRangeError {}); } - + Ok(()) } } @@ -130,7 +147,7 @@ impl AssetData { } /// single side lp limits define the highest amount (in `Uint128`) that -/// we consider acceptable to provide single-sided. +/// we consider acceptable to provide single-sided. /// if asset balance exceeds these limits, double-sided liquidity should be provided. #[cw_serde] pub struct SingleSideLpLimits { @@ -158,7 +175,7 @@ pub struct PresetLpFields { pub label: String, /// workaround for the current lack of stride redemption rate query. /// we set the expected amount of ls tokens we expect to receive for - /// the relevant half of the native tokens we have + /// the relevant half of the native tokens we have pub expected_ls_token_amount: Uint128, /// difference (both ways) we tolerate with regards to the `expected_ls_token_amount` pub allowed_return_delta: Uint128, diff --git a/contracts/lper/src/state.rs b/contracts/lper/src/state.rs index beb8cb08..8223dd58 100644 --- a/contracts/lper/src/state.rs +++ b/contracts/lper/src/state.rs @@ -1,7 +1,7 @@ use cosmwasm_std::Addr; use cw_storage_plus::Item; -use crate::msg::{AssetData, ProvidedLiquidityInfo, ContractState, LpConfig}; +use crate::msg::{AssetData, ContractState, LpConfig, ProvidedLiquidityInfo}; /// contract state tracks the state machine progress pub const CONTRACT_STATE: Item = Item::new("contract_state"); @@ -18,4 +18,4 @@ pub const PROVIDED_LIQUIDITY_INFO: Item = Item::new("provided_liquidity_info"); /// configuration relevant to entering into an LP position -pub const LP_CONFIG: Item = Item::new("lp_config"); \ No newline at end of file +pub const LP_CONFIG: Item = Item::new("lp_config"); diff --git a/contracts/lper/src/suite_test/suite.rs b/contracts/lper/src/suite_test/suite.rs index 484ecfde..f6266729 100644 --- a/contracts/lper/src/suite_test/suite.rs +++ b/contracts/lper/src/suite_test/suite.rs @@ -15,7 +15,7 @@ use cw_multi_test::{ }; use neutron_sdk::bindings::{msg::NeutronMsg, query::NeutronQuery}; -use crate::msg::{AssetData, InstantiateMsg, QueryMsg, SingleSideLpLimits, LpConfig}; +use crate::msg::{AssetData, InstantiateMsg, LpConfig, QueryMsg, SingleSideLpLimits}; use astroport::factory::InstantiateMsg as FactoryInstantiateMsg; use astroport::native_coin_registry::InstantiateMsg as NativeCoinRegistryInstantiateMsg; use astroport::pair::InstantiateMsg as PairInstantiateMsg; @@ -460,7 +460,8 @@ impl Suite { } pub fn query_lp_position(&self) -> String { - let lp_config: LpConfig = self.app + let lp_config: LpConfig = self + .app .wrap() .query_wasm_smart(&self.liquid_pooler.1, &QueryMsg::LpConfig {}) .unwrap(); diff --git a/contracts/ls/src/contract.rs b/contracts/ls/src/contract.rs index 9a002f29..a12b0950 100644 --- a/contracts/ls/src/contract.rs +++ b/contracts/ls/src/contract.rs @@ -2,20 +2,19 @@ use cosmos_sdk_proto::ibc::applications::transfer::v1::MsgTransfer; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Binary, CosmosMsg, CustomQuery, Deps, DepsMut, Env, MessageInfo, Reply, - Response, StdError, StdResult, SubMsg, Uint128, + to_binary, Binary, CosmosMsg, CustomQuery, Deps, DepsMut, Env, MessageInfo, Reply, Response, + StdError, StdResult, SubMsg, Uint128, }; use covenant_clock::helpers::verify_clock; -use covenant_utils::neutron_ica::{SudoPayload, OpenAckVersion, RemoteChainInfo, self, get_proto_coin}; +use covenant_utils::neutron_ica::{ + self, get_proto_coin, OpenAckVersion, RemoteChainInfo, SudoPayload, +}; use cw2::set_contract_version; -use crate::msg::{ - ContractState, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, -}; +use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use crate::state::{ - read_reply_payload, - save_reply_payload, save_sudo_payload, CLOCK_ADDRESS, CONTRACT_STATE, - INTERCHAIN_ACCOUNTS, AUTOPILOT_FORMAT, NEXT_CONTRACT, REMOTE_CHAIN_INFO, + read_reply_payload, save_reply_payload, save_sudo_payload, AUTOPILOT_FORMAT, CLOCK_ADDRESS, + CONTRACT_STATE, INTERCHAIN_ACCOUNTS, NEXT_CONTRACT, REMOTE_CHAIN_INFO, }; use neutron_sdk::{ bindings::{ @@ -65,8 +64,7 @@ pub fn instantiate( .add_attribute("method", "ls_instantiate") .add_attribute("clock_address", clock_addr) .add_attribute("next_contract", next_contract) - .add_attributes(remote_chain_info.get_response_attributes()) - ) + .add_attributes(remote_chain_info.get_response_attributes())) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -84,13 +82,11 @@ pub fn execute( let ica_address = get_ica(deps.as_ref(), &env, INTERCHAIN_ACCOUNT_ID); match ica_address { Ok(_) => try_execute_transfer(deps, env, info, amount), - Err(_) => Ok( - Response::default() - .add_attribute("method", "try_permisionless_transfer") - .add_attribute("ica_status", "not_created") - ), + Err(_) => Ok(Response::default() + .add_attribute("method", "try_permisionless_transfer") + .add_attribute("ica_status", "not_created")), } - }, + } } } @@ -109,8 +105,10 @@ fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> NeutronResult NeutronResult> { let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; - let register: NeutronMsg = - NeutronMsg::register_interchain_account(remote_chain_info.connection_id, INTERCHAIN_ACCOUNT_ID.to_string()); + let register: NeutronMsg = NeutronMsg::register_interchain_account( + remote_chain_info.connection_id, + INTERCHAIN_ACCOUNT_ID.to_string(), + ); let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); // we are saving empty data here because we handle response of registering ICA in sudo_open_ack method @@ -130,7 +128,6 @@ fn try_execute_transfer( _info: MessageInfo, amount: Uint128, ) -> NeutronResult> { - // first we verify whether the next contract is ready for receiving the funds let next_contract = NEXT_CONTRACT.load(deps.storage)?; let deposit_address_query = deps.querier.query_wasm_smart( @@ -193,8 +190,7 @@ fn try_execute_transfer( )?; Ok(Response::default() .add_submessage(sudo_msg) - .add_attribute("method", "try_execute_transfer") - ) + .add_attribute("method", "try_execute_transfer")) } None => { // I can't think of a case of how we could end up here as `sudo_open_ack` @@ -203,9 +199,8 @@ fn try_execute_transfer( CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; Ok(Response::default() .add_attribute("method", "try_execute_transfer") - .add_attribute("error", "no_ica_found") - ) - }, + .add_attribute("error", "no_ica_found")) + } } } @@ -229,14 +224,14 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult let ica = query_deposit_address(deps, env)?; // up to the querying module to make sense of the response Ok(to_binary(&ica)?) - }, + } QueryMsg::RemoteChainInfo {} => Ok(to_binary(&REMOTE_CHAIN_INFO.may_load(deps.storage)?)?), } } fn query_deposit_address(deps: Deps, env: Env) -> Result, StdError> { let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); - + // here we cover three cases: match INTERCHAIN_ACCOUNTS.may_load(deps.storage, key)? { Some(entry) => { @@ -253,9 +248,9 @@ fn query_deposit_address(deps: Deps, env: Env) -> Result None - None => Ok(None) + None => Ok(None), } } @@ -305,12 +300,12 @@ fn sudo_open_ack( // including the generated account address. let parsed_version: Result = serde_json_wasm::from_str(counterparty_version.as_str()); - + // get the parsed OpenAckVersion or return an error if we fail let Ok(parsed_version) = parsed_version else { return Err(StdError::generic_err("Can't parse counterparty_version")) }; - + // Update the storage record associated with the interchain account. INTERCHAIN_ACCOUNTS.save( deps.storage, @@ -321,10 +316,8 @@ fn sudo_open_ack( )), )?; CONTRACT_STATE.save(deps.storage, &ContractState::IcaCreated)?; - - Ok(Response::default() - .add_attribute("method", "sudo_open_ack") - ) + + Ok(Response::default().add_attribute("method", "sudo_open_ack")) } fn sudo_response(deps: DepsMut, request: RequestPacket, data: Binary) -> StdResult { @@ -423,7 +416,6 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> StdResult { } } - #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { deps.api.debug("WASMDEBUG: migrate"); @@ -463,4 +455,4 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult Ok(Response::default()) } } -} \ No newline at end of file +} diff --git a/contracts/ls/src/msg.rs b/contracts/ls/src/msg.rs index e5da4e96..d146c3e7 100644 --- a/contracts/ls/src/msg.rs +++ b/contracts/ls/src/msg.rs @@ -1,8 +1,11 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Binary, Uint128, Uint64}; -use covenant_macros::{covenant_deposit_address, clocked, covenant_clock_address, covenant_remote_chain, covenant_ica_address}; -use neutron_sdk::bindings::msg::IbcFee; +use covenant_macros::{ + clocked, covenant_clock_address, covenant_deposit_address, covenant_ica_address, + covenant_remote_chain, +}; use covenant_utils::neutron_ica::RemoteChainInfo; +use neutron_sdk::bindings::msg::IbcFee; #[cw_serde] pub struct InstantiateMsg { diff --git a/contracts/ls/src/state.rs b/contracts/ls/src/state.rs index 95ea914e..41bb244d 100644 --- a/contracts/ls/src/state.rs +++ b/contracts/ls/src/state.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{from_binary, to_vec, Addr, Binary, Order, StdResult, Storage, Uint128}; -use covenant_utils::neutron_ica::{SudoPayload, RemoteChainInfo}; +use covenant_utils::neutron_ica::{RemoteChainInfo, SudoPayload}; use cw_storage_plus::{Item, Map}; use crate::msg::ContractState; @@ -22,7 +22,7 @@ pub const INTERCHAIN_ACCOUNTS: Map> = Map::new("interchain_accounts"); /// formatting of stride autopilot message. -/// we use string match & replace with relevant fields to obtain the valid message. +/// we use string match & replace with relevant fields to obtain the valid message. pub const AUTOPILOT_FORMAT: Item = Item::new("autopilot_format"); /// interchain transaction responses - ack/err/timeout state to query later @@ -76,10 +76,6 @@ pub fn save_sudo_payload( SUDO_PAYLOAD.save(store, (channel_id, seq_id), &to_vec(&payload)?) } -pub fn clear_sudo_payload( - store: &mut dyn Storage, - channel_id: String, - seq_id: u64, -) { +pub fn clear_sudo_payload(store: &mut dyn Storage, channel_id: String, seq_id: u64) { SUDO_PAYLOAD.remove(store, (channel_id, seq_id)) -} \ No newline at end of file +} diff --git a/contracts/native-splitter/src/contract.rs b/contracts/native-splitter/src/contract.rs index 52615493..cc19429b 100644 --- a/contracts/native-splitter/src/contract.rs +++ b/contracts/native-splitter/src/contract.rs @@ -5,29 +5,27 @@ use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, - Response, StdResult, SubMsg, Attribute, StdError, Reply, CustomQuery, Uint128, + to_binary, Attribute, Binary, CosmosMsg, CustomQuery, Deps, DepsMut, Env, MessageInfo, Reply, + Response, StdError, StdResult, SubMsg, Uint128, }; use covenant_clock::helpers::verify_clock; -use covenant_utils::neutron_ica::{SudoPayload, RemoteChainInfo, OpenAckVersion, self}; +use covenant_utils::neutron_ica::{self, OpenAckVersion, RemoteChainInfo, SudoPayload}; use cw2::set_contract_version; -use neutron_sdk::NeutronError; use neutron_sdk::bindings::msg::MsgSubmitTxResponse; -use neutron_sdk::interchain_txs::helpers::{get_port_id, decode_acknowledgement_response, decode_message_response}; -use neutron_sdk::sudo::msg::{SudoMsg, RequestPacket}; - -use crate::msg::{ - ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, SplitReceiver, +use neutron_sdk::interchain_txs::helpers::{ + decode_acknowledgement_response, decode_message_response, get_port_id, }; +use neutron_sdk::sudo::msg::{RequestPacket, SudoMsg}; +use neutron_sdk::NeutronError; + +use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, SplitReceiver}; use crate::state::{ - save_reply_payload, CLOCK_ADDRESS, CONTRACT_STATE, - REMOTE_CHAIN_INFO, SPLIT_CONFIG_MAP, INTERCHAIN_ACCOUNTS, read_reply_payload, save_sudo_payload, TRANSFER_AMOUNT, add_error_to_queue, read_sudo_payload, + add_error_to_queue, read_reply_payload, read_sudo_payload, save_reply_payload, + save_sudo_payload, CLOCK_ADDRESS, CONTRACT_STATE, INTERCHAIN_ACCOUNTS, REMOTE_CHAIN_INFO, + SPLIT_CONFIG_MAP, TRANSFER_AMOUNT, }; use neutron_sdk::{ - bindings::{ - msg::NeutronMsg, - query::NeutronQuery, - }, + bindings::{msg::NeutronMsg, query::NeutronQuery}, NeutronResult, }; @@ -82,8 +80,7 @@ pub fn instantiate( .add_attribute("method", "native_splitter_instantiate") .add_attribute("clock_address", clock_addr) .add_attributes(remote_chain_info.get_response_attributes()) - .add_attributes(split_resp_attributes) - ) + .add_attributes(split_resp_attributes)) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -109,16 +106,18 @@ fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> NeutronResult try_register_ica(deps, env), ContractState::IcaCreated => try_split_funds(deps, env), - ContractState::Completed => Ok(Response::default() - .add_attribute("contract_state", "completed") - ), + ContractState::Completed => { + Ok(Response::default().add_attribute("contract_state", "completed")) + } } } fn try_register_ica(deps: DepsMut, env: Env) -> NeutronResult> { let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; - let register: NeutronMsg = - NeutronMsg::register_interchain_account(remote_chain_info.connection_id, INTERCHAIN_ACCOUNT_ID.to_string()); + let register: NeutronMsg = NeutronMsg::register_interchain_account( + remote_chain_info.connection_id, + INTERCHAIN_ACCOUNT_ID.to_string(), + ); let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); // we are saving empty data here because we handle response of registering ICA in sudo_open_ack method @@ -130,7 +129,6 @@ fn try_register_ica(deps: DepsMut, env: Env) -> NeutronResult NeutronResult> { - let port_id = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); let interchain_account = INTERCHAIN_ACCOUNTS.load(deps.storage, port_id.clone())?; @@ -138,10 +136,8 @@ fn try_split_funds(deps: DepsMut, env: Env) -> NeutronResult { let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; let amount = TRANSFER_AMOUNT.load(deps.storage)?; - let splits = SPLIT_CONFIG_MAP.load( - deps.storage, - remote_chain_info.denom.to_string() - )?; + let splits = + SPLIT_CONFIG_MAP.load(deps.storage, remote_chain_info.denom.to_string())?; let mut outputs: Vec = Vec::new(); for split_receiver in splits.iter() { @@ -161,17 +157,13 @@ fn try_split_funds(deps: DepsMut, env: Env) -> NeutronResult NeutronResult NeutronResult { // I can't think of a case of how we could end up here as `sudo_open_ack` @@ -208,9 +199,8 @@ fn try_split_funds(deps: DepsMut, env: Env) -> NeutronResult, env: Env, msg: QueryMsg) -> NeutronResult let ica = query_deposit_address(deps, env)?; // up to the querying module to make sense of the response Ok(to_binary(&ica)?) - }, + } QueryMsg::RemoteChainInfo {} => Ok(to_binary(&REMOTE_CHAIN_INFO.may_load(deps.storage)?)?), QueryMsg::SplitConfig {} => { let mut vec: Vec<(String, Vec)> = Vec::new(); - for entry in SPLIT_CONFIG_MAP.range( - deps.storage, - None, - None, - cosmwasm_std::Order::Ascending - ) { vec.push(entry?) } + for entry in + SPLIT_CONFIG_MAP.range(deps.storage, None, None, cosmwasm_std::Order::Ascending) + { + vec.push(entry?) + } Ok(to_binary(&vec)?) - }, + } QueryMsg::TransferAmount {} => Ok(to_binary(&TRANSFER_AMOUNT.may_load(deps.storage)?)?), QueryMsg::IcaAddress {} => Ok(to_binary(&get_ica(deps, &env, INTERCHAIN_ACCOUNT_ID)?.0)?), } @@ -255,15 +244,15 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> NeutronResult fn query_deposit_address(deps: Deps, env: Env) -> Result, StdError> { let key = get_port_id(env.contract.address.as_str(), INTERCHAIN_ACCOUNT_ID); /* - here we cover three possible cases: - - 1. ICA had been created -> nice - - 2. ICA creation request had been submitted but did not receive - the channel_open_ack yet -> None - - 3. ICA creation request hadn't been submitted yet -> None - */ + here we cover three possible cases: + - 1. ICA had been created -> nice + - 2. ICA creation request had been submitted but did not receive + the channel_open_ack yet -> None + - 3. ICA creation request hadn't been submitted yet -> None + */ match INTERCHAIN_ACCOUNTS.may_load(deps.storage, key)? { Some(Some((addr, _))) => Ok(Some(addr)), // case 1 - _ => Ok(None), // cases 2 and 3 + _ => Ok(None), // cases 2 and 3 } } @@ -279,7 +268,6 @@ fn get_ica( .ok_or_else(|| StdError::generic_err("Interchain account is not created yet")) } - #[cfg_attr(not(feature = "library"), entry_point)] pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> StdResult { deps.api @@ -326,12 +314,12 @@ fn sudo_open_ack( // including the generated account address. let parsed_version: Result = serde_json_wasm::from_str(counterparty_version.as_str()); - + // get the parsed OpenAckVersion or return an error if we fail let Ok(parsed_version) = parsed_version else { return Err(StdError::generic_err("Can't parse counterparty_version")) }; - + // Update the storage record associated with the interchain account. INTERCHAIN_ACCOUNTS.save( deps.storage, @@ -342,10 +330,8 @@ fn sudo_open_ack( )), )?; CONTRACT_STATE.save(deps.storage, &ContractState::IcaCreated)?; - - Ok(Response::default() - .add_attribute("method", "sudo_open_ack") - ) + + Ok(Response::default().add_attribute("method", "sudo_open_ack")) } fn sudo_response(deps: DepsMut, request: RequestPacket, data: Binary) -> StdResult { @@ -355,7 +341,7 @@ fn sudo_response(deps: DepsMut, request: RequestPacket, data: Binary) -> StdResu let seq_id = request .sequence .ok_or_else(|| StdError::generic_err("sequence not found"))?; - + let channel_id = request .source_channel .ok_or_else(|| StdError::generic_err("channel_id not found"))?; @@ -377,7 +363,6 @@ fn sudo_response(deps: DepsMut, request: RequestPacket, data: Binary) -> StdResu item_types.push(item_type.to_string()); match item_type { "/cosmos.bank.v1beta1.MsgMultiSend" => { - let out: MsgMultiSendResponse = decode_message_response(&item.data)?; // TODO: look into if this successful decoding is enough to assume multi // send was successful diff --git a/contracts/native-splitter/src/msg.rs b/contracts/native-splitter/src/msg.rs index e3abb65c..69a893f9 100644 --- a/contracts/native-splitter/src/msg.rs +++ b/contracts/native-splitter/src/msg.rs @@ -1,11 +1,14 @@ use std::fmt; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Uint128, Uint64, StdError, Attribute}; -use covenant_macros::{covenant_deposit_address, clocked, covenant_clock_address, covenant_remote_chain, covenant_ica_address}; +use cosmwasm_std::{Addr, Attribute, Fraction, StdError, Uint128, Uint64}; +use covenant_macros::{ + clocked, covenant_clock_address, covenant_deposit_address, covenant_ica_address, + covenant_remote_chain, +}; -use neutron_sdk::bindings::msg::IbcFee; use covenant_utils::neutron_ica::RemoteChainInfo; +use neutron_sdk::bindings::msg::IbcFee; use schemars::Map; #[cw_serde] @@ -13,7 +16,7 @@ pub struct InstantiateMsg { /// Address for the clock. This contract verifies /// that only the clock can execute Ticks pub clock_address: String, - + pub remote_chain_connection_id: String, pub remote_chain_channel_id: String, pub denom: String, @@ -37,7 +40,6 @@ pub struct InstantiateMsg { /// if the ICA times out, the destination chain receiving the funds /// will also receive the IBC packet with an expired timestamp. pub ibc_transfer_timeout: Uint64, - } #[cw_serde] @@ -54,7 +56,10 @@ impl NativeDenomSplit { let sum: Uint128 = self.receivers.iter().map(|r| r.share).sum(); if sum != Uint128::new(100) { - Err(StdError::generic_err(format!("failed to validate split config for denom: {}", self.denom))) + Err(StdError::generic_err(format!( + "failed to validate split config for denom: {}", + self.denom + ))) } else { Ok(self) } diff --git a/contracts/native-splitter/src/state.rs b/contracts/native-splitter/src/state.rs index c04167cd..8119b505 100644 --- a/contracts/native-splitter/src/state.rs +++ b/contracts/native-splitter/src/state.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{from_binary, to_vec, Addr, Binary, Order, StdResult, Storage, Uint128}; -use covenant_utils::neutron_ica::{SudoPayload, RemoteChainInfo}; +use covenant_utils::neutron_ica::{RemoteChainInfo, SudoPayload}; use cw_storage_plus::{Item, Map}; use crate::msg::{ContractState, SplitReceiver}; @@ -12,7 +12,6 @@ pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); pub const TRANSFER_AMOUNT: Item = Item::new("transfer_amount"); - // maps a denom string to a vec of SplitReceivers pub const SPLIT_CONFIG_MAP: Map> = Map::new("split_config"); @@ -71,10 +70,6 @@ pub fn save_sudo_payload( SUDO_PAYLOAD.save(store, (channel_id, seq_id), &to_vec(&payload)?) } -pub fn clear_sudo_payload( - store: &mut dyn Storage, - channel_id: String, - seq_id: u64, -) { +pub fn clear_sudo_payload(store: &mut dyn Storage, channel_id: String, seq_id: u64) { SUDO_PAYLOAD.remove(store, (channel_id, seq_id)) -} \ No newline at end of file +} diff --git a/contracts/swap-covenant/.cargo/config b/contracts/swap-covenant/.cargo/config new file mode 100644 index 00000000..5f6aa466 --- /dev/null +++ b/contracts/swap-covenant/.cargo/config @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +schema = "run --bin schema" diff --git a/contracts/swap-covenant/Cargo.toml b/contracts/swap-covenant/Cargo.toml new file mode 100644 index 00000000..6a74df13 --- /dev/null +++ b/contracts/swap-covenant/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "covenant-swap" +edition = { workspace = true } +authors = ["benskey bekauz@protonmail.com"] +description = "Swap covenant contract" +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +exclude = [ + "contract.wasm", + "hash.txt", +] + + +[lib] +crate-type = ["cdylib", "rlib"] + + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +cw2 = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +sha2 = { workspace = true } +neutron-sdk = { workspace = true } +cosmos-sdk-proto = { workspace = true } +protobuf = { workspace = true } +schemars = { workspace = true } +serde-json-wasm = { workspace = true } +base64 = { workspace = true } +prost = { workspace = true } +prost-types = { workspace = true } +bech32 ={ workspace = true } +covenant-ls = { workspace = true, features=["library"] } +covenant-depositor = { workspace = true, features=["library"] } +covenant-lp = { workspace = true, features=["library"] } +covenant-clock = { workspace = true, features=["library"]} +covenant-holder = { workspace = true, features=["library"] } +covenant-swap-holder = { workspace = true, features = ["library"] } +covenant-utils = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +anyhow = { workspace = true } +astroport = { workspace = true } +prost = "0.11.9" diff --git a/contracts/swap-covenant/README.md b/contracts/swap-covenant/README.md new file mode 100644 index 00000000..a7c54f6e --- /dev/null +++ b/contracts/swap-covenant/README.md @@ -0,0 +1,3 @@ +# swap covenant + +Contract responsible for orchestrating flow for a tokenswap between two parties. diff --git a/contracts/swap-covenant/src/contract.rs b/contracts/swap-covenant/src/contract.rs new file mode 100644 index 00000000..9395d030 --- /dev/null +++ b/contracts/swap-covenant/src/contract.rs @@ -0,0 +1,56 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_binary, CosmosMsg, DepsMut, Env, MessageInfo, Response, + SubMsg, WasmMsg, +}; + +use cw2::set_contract_version; + +use crate::{ + error::ContractError, + msg::InstantiateMsg, + state::{ + CLOCK_CODE, TIMEOUTS, + }, +}; + +const CONTRACT_NAME: &str = "crates.io:swap-covenant"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub(crate) const CLOCK_REPLY_ID: u64 = 1u64; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + deps.api.debug("WASMDEBUG: instantiate"); + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // store all the codes for covenant configuration + CLOCK_CODE.save(deps.storage, &msg.preset_clock_fields.clock_code)?; + + + // save ibc transfer and ica timeouts, as well as the ibc fees + TIMEOUTS.save(deps.storage, &msg.timeouts)?; + + // we start the module instantiation chain with the clock + let clock_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.to_string()), + code_id: msg.preset_clock_fields.clock_code, + msg: to_binary(&msg.preset_clock_fields.clone().to_instantiate_msg())?, + funds: vec![], + label: msg.preset_clock_fields.label, + }); + + Ok(Response::default() + .add_attribute("method", "instantiate") + .add_submessage(SubMsg::reply_on_success( + clock_instantiate_tx, + CLOCK_REPLY_ID, + )) + ) +} diff --git a/contracts/swap-covenant/src/error.rs b/contracts/swap-covenant/src/error.rs new file mode 100644 index 00000000..1e1b4d4a --- /dev/null +++ b/contracts/swap-covenant/src/error.rs @@ -0,0 +1,27 @@ +use cosmwasm_std::StdError; +use cw_utils::ParseReplyError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Attempt to deposit zero")] + ZeroDeposit {}, + + #[error("Unknown reply id")] + UnknownReplyId {}, + + #[error("SubMsg reply error")] + ReplyError { err: String }, + + #[error("Failed to instantiate {contract:?} contract")] + ContractInstantiationError { + contract: String, + err: ParseReplyError, + }, +} diff --git a/contracts/swap-covenant/src/lib.rs b/contracts/swap-covenant/src/lib.rs new file mode 100644 index 00000000..6c785d7e --- /dev/null +++ b/contracts/swap-covenant/src/lib.rs @@ -0,0 +1,14 @@ +#![warn(clippy::unwrap_used, clippy::expect_used)] + +extern crate core; + +pub mod contract; +pub mod error; +pub mod msg; +pub mod state; + +// pub mod instantiate2; + +#[allow(clippy::unwrap_used)] +#[cfg(test)] +mod suite_test; diff --git a/contracts/swap-covenant/src/msg.rs b/contracts/swap-covenant/src/msg.rs new file mode 100644 index 00000000..6d34747a --- /dev/null +++ b/contracts/swap-covenant/src/msg.rs @@ -0,0 +1,85 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Uint128, Uint64}; +use covenant_clock::msg::PresetClockFields; +use covenant_swap_holder::msg::PresetSwapHolderFields; +use covenant_utils::SwapCovenantTerms; +use neutron_sdk::bindings::msg::IbcFee; + +const NEUTRON_DENOM: &str = "untrn"; +pub const DEFAULT_TIMEOUT: u64 = 60 * 60 * 5; // 5 hours + +#[cw_serde] +pub struct InstantiateMsg { + /// contract label for this specific covenant + pub label: String, + /// neutron relayer fee structure + pub preset_ibc_fee: PresetIbcFee, + /// ibc transfer and ica timeouts passed down to relevant modules + pub timeouts: Timeouts, + + /// instantiation fields relevant to clock module known in advance + pub preset_clock_fields: PresetClockFields, + + /// instantiation fields relevant to swap holder contract known in advance + pub preset_holder_fields: PresetSwapHolderFields, + pub covenant_terms: SwapCovenantTerms, +} + +#[cw_serde] +pub struct Timeouts { + /// ica timeout in seconds + pub ica_timeout: Uint64, + /// ibc transfer timeout in seconds + pub ibc_transfer_timeout: Uint64, +} + +impl Default for Timeouts { + fn default() -> Self { + Self { + ica_timeout: Uint64::new(DEFAULT_TIMEOUT), + ibc_transfer_timeout: Uint64::new(DEFAULT_TIMEOUT), + } + } +} + +#[cw_serde] +pub struct PresetIbcFee { + pub ack_fee: Uint128, + pub timeout_fee: Uint128, +} + +impl PresetIbcFee { + pub fn to_ibc_fee(self) -> IbcFee { + IbcFee { + // must be empty + recv_fee: vec![], + ack_fee: vec![cosmwasm_std::Coin { + denom: NEUTRON_DENOM.to_string(), + amount: self.ack_fee, + }], + timeout_fee: vec![cosmwasm_std::Coin { + denom: NEUTRON_DENOM.to_string(), + amount: self.timeout_fee, + }], + } + } +} + +#[cw_serde] +pub enum ExecuteMsg {} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(Addr)] + ClockAddress {}, + #[returns(Addr)] + HolderAddress {}, +} + +#[cw_serde] +pub enum MigrateMsg { + MigrateContracts { + clock: Option, + }, +} diff --git a/contracts/swap-covenant/src/state.rs b/contracts/swap-covenant/src/state.rs new file mode 100644 index 00000000..5c54a471 --- /dev/null +++ b/contracts/swap-covenant/src/state.rs @@ -0,0 +1,32 @@ +use cosmwasm_std::Addr; +use cw_storage_plus::Item; +use neutron_sdk::bindings::msg::IbcFee; + +use crate::msg::Timeouts; + +/// contract code for the ibc forwarder +pub const IBC_FORWARDER_CODE: Item = Item::new("ibc_forwarder_code"); +/// contract code for the interchain splitter +pub const INTECHAIN_SPLITTER_CODE: Item = Item::new("interchain_splitter"); +/// contract code for the swap holder +pub const SWAP_HOLDER_CODE: Item = Item::new("swap_holder_code"); +/// contract code for the clock module +pub const CLOCK_CODE: Item = Item::new("clock_code"); + +/// ibc fee for the relayers +pub const IBC_FEE: Item = Item::new("ibc_fee"); +/// ibc transfer and ica timeouts that will be passed down to +/// modules dealing with ICA +pub const TIMEOUTS: Item = Item::new("timeouts"); + +// /// fields related to the clock module known prior to covenant instatiation. +// pub const PRESET_CLOCK_FIELDS: Item = +// Item::new("preset_clock_fields"); + + +/// address of the clock module associated with this covenant +pub const COVENANT_CLOCK_ADDR: Item = Item::new("covenant_clock_addr"); +/// address of the interchain splitter contract associated with this covenant +pub const COVENANT_INTERCHAIN_SPLITTER_ADDR: Item = Item::new("covenant_interchain_splitter_addr"); +/// address of the swap holder contract associated with this covenant +pub const COVENANT_SWAP_HOLDER_ADDR: Item = Item::new("covenant_swap_holder_addr"); diff --git a/contracts/swap-covenant/src/suite_test/mod.rs b/contracts/swap-covenant/src/suite_test/mod.rs new file mode 100644 index 00000000..93957587 --- /dev/null +++ b/contracts/swap-covenant/src/suite_test/mod.rs @@ -0,0 +1,3 @@ +mod suite; +mod tests; +mod unit_tests; diff --git a/contracts/swap-covenant/src/suite_test/suite.rs b/contracts/swap-covenant/src/suite_test/suite.rs new file mode 100644 index 00000000..e5fa7128 --- /dev/null +++ b/contracts/swap-covenant/src/suite_test/suite.rs @@ -0,0 +1,61 @@ +use cosmwasm_std::{Addr, Empty}; +use cw_multi_test::{App, Contract, ContractWrapper, Executor}; + +use crate::msg::{InstantiateMsg, QueryMsg}; + +pub const CREATOR_ADDR: &str = "admin"; +pub const TODO: &str = "replace"; + +fn covenant_clock() -> Box> { + Box::new( + ContractWrapper::new( + covenant_clock::contract::execute, + covenant_clock::contract::instantiate, + covenant_clock::contract::query, + ) + .with_reply(covenant_clock::contract::reply) + .with_migrate(covenant_clock::contract::migrate), + ) +} + +pub(crate) struct Suite { + pub app: App, + pub covenant_address: Addr, +} + +pub(crate) struct SuiteBuilder { + pub instantiate: InstantiateMsg, +} + +impl Default for SuiteBuilder { + fn default() -> Self { + Self { + instantiate: InstantiateMsg { + label: todo!(), + preset_ibc_fee: todo!(), + timeouts: todo!(), + preset_clock_fields: todo!(), + preset_holder_fields: todo!(), + covenant_terms: todo!(), + }, + } + } +} + +impl SuiteBuilder { + pub fn build(mut self) -> Suite { + let mut app = App::default(); + Suite { + app, + covenant_address: todo!(), + } + } +} + +// assertion helpers +impl Suite {} + +// queries +impl Suite { + +} diff --git a/contracts/swap-covenant/src/suite_test/tests.rs b/contracts/swap-covenant/src/suite_test/tests.rs new file mode 100644 index 00000000..e69de29b diff --git a/contracts/swap-covenant/src/suite_test/unit_tests.rs b/contracts/swap-covenant/src/suite_test/unit_tests.rs new file mode 100644 index 00000000..e69de29b diff --git a/contracts/swap-holder/src/msg.rs b/contracts/swap-holder/src/msg.rs index 7816f247..d4531425 100644 --- a/contracts/swap-holder/src/msg.rs +++ b/contracts/swap-holder/src/msg.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Attribute}; use covenant_macros::{clocked, covenant_clock_address}; -use covenant_utils::{CovenantPartiesConfig, CovenantTerms, LockupConfig}; +use covenant_utils::{LockupConfig, CovenantPartiesConfig, CovenantTerms}; #[cw_serde] pub struct InstantiateMsg { @@ -33,6 +33,33 @@ impl InstantiateMsg { } } +#[cw_serde] +pub struct PresetSwapHolderFields { + /// block height of covenant expiration. Position is exited + /// automatically upon reaching that height. + pub lockup_config: LockupConfig, + /// parties engaged in the POL. + pub parties_config: CovenantPartiesConfig, + /// terms of the covenant + pub covenant_terms: CovenantTerms, +} + +impl PresetSwapHolderFields { + pub fn to_instantiate_msg( + self, + clock_address: String, + next_contract: String, + ) -> InstantiateMsg { + InstantiateMsg { + clock_address, + next_contract, + lockup_config: self.lockup_config, + parties_config: self.parties_config, + covenant_terms: self.covenant_terms, + } + } +} + #[clocked] #[cw_serde] pub enum ExecuteMsg {} From 97546819a75d3fae13a648cfeeb56ad34510c120 Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 6 Sep 2023 15:00:11 +0200 Subject: [PATCH 067/586] swap covenant instantiation chain --- Cargo.lock | 2 + Cargo.toml | 3 +- contracts/interchain-splitter/Cargo.toml | 7 +- contracts/interchain-splitter/src/msg.rs | 2 + contracts/swap-covenant/Cargo.toml | 2 + contracts/swap-covenant/src/contract.rs | 280 ++++++++++++++++++++++- contracts/swap-covenant/src/msg.rs | 5 +- contracts/swap-covenant/src/state.rs | 18 +- contracts/swap-holder/src/msg.rs | 4 + 9 files changed, 313 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 060c0ac2..66d76e09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -616,6 +616,8 @@ dependencies = [ "covenant-clock", "covenant-depositor", "covenant-holder", + "covenant-ibc-forwarder", + "covenant-interchain-splitter", "covenant-lp", "covenant-ls", "covenant-swap-holder", diff --git a/Cargo.toml b/Cargo.toml index e962974e..0296bee5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,8 +33,9 @@ covenant-covenant = { path = "contracts/covenant" } covenant-holder = { path = "contracts/holder" } covenant-ibc-forwarder = { path = "contracts/ibc-forwarder" } covenant-native-splitter = { path = "contracts/native-splitter" } -covenant-interchain-splitter = { path = "contract/interchain-splitter" } +covenant-interchain-splitter = { path = "contracts/interchain-splitter" } covenant-swap-holder = { path = "contracts/swap-holder" } +swap-covenant = { path = "contracts/swap-covenant" } # packages clock-derive = { path = "packages/clock-derive" } diff --git a/contracts/interchain-splitter/Cargo.toml b/contracts/interchain-splitter/Cargo.toml index 3a81624d..079cb116 100644 --- a/contracts/interchain-splitter/Cargo.toml +++ b/contracts/interchain-splitter/Cargo.toml @@ -7,13 +7,18 @@ license = { workspace = true } rust-version = { workspace = true } version = { workspace = true } +exclude = [ + "contract.wasm", + "hash.txt", +] + [lib] crate-type = ["cdylib", "rlib"] [features] # for more explicit tests, cargo test --features=backtraces backtraces = ["cosmwasm-std/backtraces"] -# disables #[entry_point] (i.e. instantiate/execute/query) export +# use library feature to disable all instantiate/execute/query exports library = [] [dependencies] diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs index 2c19620e..5d7f674c 100644 --- a/contracts/interchain-splitter/src/msg.rs +++ b/contracts/interchain-splitter/src/msg.rs @@ -25,6 +25,8 @@ pub struct PresetInterchainSplitterFields { /// a split for all denoms that are not covered in the /// regular `splits` list pub fallback_split: Option, + /// contract label + pub label: String, } impl PresetInterchainSplitterFields { diff --git a/contracts/swap-covenant/Cargo.toml b/contracts/swap-covenant/Cargo.toml index 6a74df13..c563b841 100644 --- a/contracts/swap-covenant/Cargo.toml +++ b/contracts/swap-covenant/Cargo.toml @@ -48,6 +48,8 @@ covenant-clock = { workspace = true, features=["library"]} covenant-holder = { workspace = true, features=["library"] } covenant-swap-holder = { workspace = true, features = ["library"] } covenant-utils = { workspace = true } +covenant-interchain-splitter = { workspace = true, features = ["library"] } +covenant-ibc-forwarder = { workspace = true, features = ["library"] } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/swap-covenant/src/contract.rs b/contracts/swap-covenant/src/contract.rs index 9395d030..ef689fde 100644 --- a/contracts/swap-covenant/src/contract.rs +++ b/contracts/swap-covenant/src/contract.rs @@ -2,16 +2,18 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ to_binary, CosmosMsg, DepsMut, Env, MessageInfo, Response, - SubMsg, WasmMsg, + SubMsg, WasmMsg, Reply, }; +use covenant_utils::RefundConfig; use cw2::set_contract_version; +use cw_utils::parse_reply_instantiate_data; use crate::{ error::ContractError, msg::InstantiateMsg, state::{ - CLOCK_CODE, TIMEOUTS, + CLOCK_CODE, TIMEOUTS, COVENANT_CLOCK_ADDR, SWAP_HOLDER_CODE, PRESET_HOLDER_FIELDS, COVENANT_INTERCHAIN_SPLITTER_ADDR, INTECHAIN_SPLITTER_CODE, PRESET_SPLITTER_FIELDS, COVENANT_SWAP_HOLDER_ADDR, IBC_FORWARDER_CODE, IBC_FEE, COVENANT_PARTIES, COVENANT_TERMS, PARTY_A_IBC_FORWARDER_ADDR, PARTY_B_IBC_FORWARDER_ADDR, }, }; @@ -19,6 +21,10 @@ const CONTRACT_NAME: &str = "crates.io:swap-covenant"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); pub(crate) const CLOCK_REPLY_ID: u64 = 1u64; +pub const SPLITTER_REPLY_ID: u64 = 2u64; +pub const SWAP_HOLDER_REPLY_ID: u64 = 3u64; +pub const PARTY_A_FORWARDER_REPLY_ID: u64 = 4u64; +pub const PARTY_B_FORWARDER_REPLY_ID: u64 = 5u64; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -32,9 +38,11 @@ pub fn instantiate( // store all the codes for covenant configuration CLOCK_CODE.save(deps.storage, &msg.preset_clock_fields.clock_code)?; - - - // save ibc transfer and ica timeouts, as well as the ibc fees + SWAP_HOLDER_CODE.save(deps.storage, &msg.preset_holder_fields.code_id)?; + IBC_FORWARDER_CODE.save(deps.storage, &msg.ibc_forwarder_code)?; + PRESET_HOLDER_FIELDS.save(deps.storage, &msg.preset_holder_fields)?; + COVENANT_PARTIES.save(deps.storage, &msg.covenant_parties)?; + COVENANT_TERMS.save(deps.storage, &msg.covenant_terms)?; TIMEOUTS.save(deps.storage, &msg.timeouts)?; // we start the module instantiation chain with the clock @@ -54,3 +62,265 @@ pub fn instantiate( )) ) } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { + match msg.id { + CLOCK_REPLY_ID => handle_clock_reply(deps, env, msg), + SPLITTER_REPLY_ID => handle_splitter_reply(deps, env, msg), + SWAP_HOLDER_REPLY_ID => handle_swap_holder_reply(deps, env, msg), + PARTY_A_FORWARDER_REPLY_ID => handle_party_a_ibc_forwarder_reply(deps, env, msg), + PARTY_B_FORWARDER_REPLY_ID => handle_party_b_ibc_forwarder_reply(deps, env, msg), + _ => Err(ContractError::UnknownReplyId {}), + } +} + +/// clock instantiation reply means we can proceed with the instantiation chain. +/// we store the clock address and submit the splitter instantiate tx. +pub fn handle_clock_reply(deps: DepsMut, env: Env, msg: Reply) -> Result { + deps.api.debug("WASMDEBUG: clock reply"); + + let parsed_data = parse_reply_instantiate_data(msg); + match parsed_data { + Ok(response) => { + // validate and store the clock address + let clock_addr = deps.api.addr_validate(&response.contract_address)?; + COVENANT_CLOCK_ADDR.save(deps.storage, &clock_addr)?; + + // load the fields relevant to splitter + let code_id = INTECHAIN_SPLITTER_CODE.load(deps.storage)?; + let preset_splitter_fields = PRESET_SPLITTER_FIELDS.load(deps.storage)?; + + let splitter_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.to_string()), + code_id, + msg: to_binary( + &preset_splitter_fields + .clone() + .to_instantiate_msg(clock_addr.to_string()), + )?, + funds: vec![], + label: preset_splitter_fields.label, + }); + + Ok(Response::default() + .add_attribute("method", "handle_clock_reply") + .add_attribute("clock_address", clock_addr) + .add_submessage(SubMsg::reply_always(splitter_instantiate_tx, SPLITTER_REPLY_ID))) + } + Err(err) => Err(ContractError::ContractInstantiationError { + contract: "clock".to_string(), + err, + }), + } +} + +/// splitter instantiation reply means we can proceed with the instantiation chain. +/// we store the splitter address and submit the swap holder instantiate tx. +pub fn handle_splitter_reply(deps: DepsMut, env: Env, msg: Reply) -> Result { + deps.api.debug("WASMDEBUG: splitter reply"); + + let parsed_data = parse_reply_instantiate_data(msg); + match parsed_data { + Ok(response) => { + // validate and store the splitter address + let splitter_addr = deps.api.addr_validate(&response.contract_address)?; + COVENANT_INTERCHAIN_SPLITTER_ADDR.save(deps.storage, &splitter_addr)?; + + // load the fields relevant to holder instantiation + let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; + let code_id = SWAP_HOLDER_CODE.load(deps.storage)?; + let preset_holder_fields = PRESET_HOLDER_FIELDS.load(deps.storage)?; + + let holder_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.to_string()), + code_id, + msg: to_binary( + &preset_holder_fields + .clone() + .to_instantiate_msg(clock_addr.to_string(), splitter_addr.to_string()), + )?, + funds: vec![], + label: preset_holder_fields.label, + }); + + Ok(Response::default() + .add_attribute("method", "handle_splitter_reply") + .add_attribute("splitter_addr", splitter_addr) + .add_submessage(SubMsg::reply_always(holder_instantiate_tx, SWAP_HOLDER_REPLY_ID))) + } + Err(err) => Err(ContractError::ContractInstantiationError { + contract: "splitter".to_string(), + err, + }), + } +} + +/// swap instantiation reply means we can proceed with the instantiation chain. +/// we store the swap holder address and submit the party A ibc forwarder instantiate tx. +pub fn handle_swap_holder_reply(deps: DepsMut, env: Env, msg: Reply) -> Result { + deps.api.debug("WASMDEBUG: swap holder reply"); + + let parsed_data = parse_reply_instantiate_data(msg); + match parsed_data { + Ok(response) => { + // validate and store the swap holder address + let swap_holder_addr = deps.api.addr_validate(&response.contract_address)?; + COVENANT_SWAP_HOLDER_ADDR.save(deps.storage, &swap_holder_addr)?; + + // load the fields relevant to ibc forwarder instantiation + let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; + let code_id = IBC_FORWARDER_CODE.load(deps.storage)?; + let timeouts = TIMEOUTS.load(deps.storage)?; + let ibc_fee = IBC_FEE.load(deps.storage)?; + let covenant_parties = COVENANT_PARTIES.load(deps.storage)?; + let covenant_terms = COVENANT_TERMS.load(deps.storage)?; + let refund_config = match covenant_parties.party_a.refund_config { + RefundConfig::Ibc(r) => r, + _ => return Err(ContractError::ContractInstantiationError { + contract: "party_a_forwarder".to_string(), + err: cw_utils::ParseReplyError::ParseFailure("no remote chain info".to_string()), + }) + }; + + let instantiate_msg = covenant_ibc_forwarder::msg::InstantiateMsg { + clock_address: clock_addr.to_string(), + next_contract: swap_holder_addr.to_string(), + remote_chain_connection_id: refund_config.connection_id, + remote_chain_channel_id: refund_config.channel_id, + denom: covenant_parties.party_a.provided_denom, + amount: covenant_terms.party_a_amount, + ibc_fee, + ibc_transfer_timeout: timeouts.ibc_transfer_timeout, + ica_timeout: timeouts.ica_timeout, + }; + + let party_a_forwarder_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.to_string()), + code_id, + msg: to_binary(&instantiate_msg)?, + funds: vec![], + label: "party_a_forwarder".to_string(), + }); + + Ok(Response::default() + .add_attribute("method", "handle_swap_holder_reply") + .add_attribute("swap_holder_addr", swap_holder_addr) + .add_submessage(SubMsg::reply_always(party_a_forwarder_instantiate_tx, PARTY_A_FORWARDER_REPLY_ID))) + } + Err(err) => Err(ContractError::ContractInstantiationError { + contract: "swap holder".to_string(), + err, + }), + } +} + +/// party A ibc forwarder reply means we can proceed with the instantiation chain. +/// we store the party A ibc forwarder address and submit the party B ibc forwarder instantiate tx. +pub fn handle_party_a_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) -> Result { + deps.api.debug("WASMDEBUG: party A ibc forwader reply"); + + let parsed_data = parse_reply_instantiate_data(msg); + match parsed_data { + Ok(response) => { + // validate and store the party A ibc forwarder address + let party_a_ibc_forwarder_addr = deps.api.addr_validate(&response.contract_address)?; + PARTY_A_IBC_FORWARDER_ADDR.save(deps.storage, &party_a_ibc_forwarder_addr)?; + + // load the fields relevant to ibc forwarder instantiation + let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; + let code_id = IBC_FORWARDER_CODE.load(deps.storage)?; + let timeouts = TIMEOUTS.load(deps.storage)?; + let ibc_fee = IBC_FEE.load(deps.storage)?; + let covenant_parties = COVENANT_PARTIES.load(deps.storage)?; + let covenant_terms = COVENANT_TERMS.load(deps.storage)?; + let swap_holder = COVENANT_SWAP_HOLDER_ADDR.load(deps.storage)?; + + let refund_config = match covenant_parties.party_b.refund_config { + RefundConfig::Ibc(r) => r, + _ => return Err(ContractError::ContractInstantiationError { + contract: "party_b_forwarder".to_string(), + err: cw_utils::ParseReplyError::ParseFailure("no remote chain info".to_string()), + }) + }; + + let instantiate_msg = covenant_ibc_forwarder::msg::InstantiateMsg { + clock_address: clock_addr.to_string(), + next_contract: swap_holder.to_string(), + remote_chain_connection_id: refund_config.connection_id, + remote_chain_channel_id: refund_config.channel_id, + denom: covenant_parties.party_b.provided_denom, + amount: covenant_terms.party_b_amount, + ibc_fee, + ibc_transfer_timeout: timeouts.ibc_transfer_timeout, + ica_timeout: timeouts.ica_timeout, + }; + + let party_b_forwarder_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.to_string()), + code_id, + msg: to_binary(&instantiate_msg)?, + funds: vec![], + label: "party_b_forwarder".to_string(), + }); + + Ok(Response::default() + .add_attribute("method", "handle_party_a_ibc_forwader") + .add_attribute("party_a_ibc_forwarder_addr", party_a_ibc_forwarder_addr) + .add_submessage(SubMsg::reply_always(party_b_forwarder_instantiate_tx, PARTY_B_FORWARDER_REPLY_ID))) + } + Err(err) => Err(ContractError::ContractInstantiationError { + contract: "swap holder".to_string(), + err, + }), + } +} + + +/// party B ibc forwarder reply means that we instantiated all the contracts. +/// we store the party B ibc forwarder address and whitelist the contracts on our clock. +pub fn handle_party_b_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) -> Result { + deps.api.debug("WASMDEBUG: party B ibc forwader reply"); + + let parsed_data = parse_reply_instantiate_data(msg); + match parsed_data { + Ok(response) => { + // validate and store the party b ibc forwarder address + let party_b_ibc_forwarder_addr = deps.api.addr_validate(&response.contract_address)?; + PARTY_B_IBC_FORWARDER_ADDR.save(deps.storage, &party_b_ibc_forwarder_addr)?; + + // load the fields relevant to ibc forwarder instantiation + let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; + let clock_code_id = CLOCK_CODE.load(deps.storage)?; + + let swap_holder = COVENANT_SWAP_HOLDER_ADDR.load(deps.storage)?; + let party_a_forwarder = PARTY_A_IBC_FORWARDER_ADDR.load(deps.storage)?; + + let interchain_splitter = COVENANT_INTERCHAIN_SPLITTER_ADDR.load(deps.storage)?; + + + let update_clock_whitelist_msg = WasmMsg::Migrate { + contract_addr: clock_addr.to_string(), + new_code_id: clock_code_id, + msg: to_binary(&covenant_clock::msg::MigrateMsg::ManageWhitelist { + add: Some(vec![ + party_a_forwarder.to_string(), + party_b_ibc_forwarder_addr.to_string(), + swap_holder.to_string(), + interchain_splitter.to_string(), + ]), + remove: None, + })?, + }; + + Ok(Response::default() + .add_attribute("method", "handle_party_a_ibc_forwader") + .add_attribute("party_b_ibc_forwarder_addr", party_b_ibc_forwarder_addr) + .add_message(update_clock_whitelist_msg)) + } + Err(err) => Err(ContractError::ContractInstantiationError { + contract: "swap holder".to_string(), + err, + }), + } +} diff --git a/contracts/swap-covenant/src/msg.rs b/contracts/swap-covenant/src/msg.rs index 6d34747a..cb9b869f 100644 --- a/contracts/swap-covenant/src/msg.rs +++ b/contracts/swap-covenant/src/msg.rs @@ -2,7 +2,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Uint128, Uint64}; use covenant_clock::msg::PresetClockFields; use covenant_swap_holder::msg::PresetSwapHolderFields; -use covenant_utils::SwapCovenantTerms; +use covenant_utils::{SwapCovenantTerms, CovenantParty, CovenantPartiesConfig}; use neutron_sdk::bindings::msg::IbcFee; const NEUTRON_DENOM: &str = "untrn"; @@ -17,12 +17,15 @@ pub struct InstantiateMsg { /// ibc transfer and ica timeouts passed down to relevant modules pub timeouts: Timeouts, + pub ibc_forwarder_code: u64, + /// instantiation fields relevant to clock module known in advance pub preset_clock_fields: PresetClockFields, /// instantiation fields relevant to swap holder contract known in advance pub preset_holder_fields: PresetSwapHolderFields, pub covenant_terms: SwapCovenantTerms, + pub covenant_parties: CovenantPartiesConfig, } #[cw_serde] diff --git a/contracts/swap-covenant/src/state.rs b/contracts/swap-covenant/src/state.rs index 5c54a471..011d8af1 100644 --- a/contracts/swap-covenant/src/state.rs +++ b/contracts/swap-covenant/src/state.rs @@ -1,4 +1,8 @@ use cosmwasm_std::Addr; +use covenant_interchain_splitter::msg::PresetInterchainSplitterFields; +use covenant_swap_holder::msg::PresetSwapHolderFields; + +use covenant_utils::{CovenantPartiesConfig, SwapCovenantTerms}; use cw_storage_plus::Item; use neutron_sdk::bindings::msg::IbcFee; @@ -20,8 +24,11 @@ pub const IBC_FEE: Item = Item::new("ibc_fee"); pub const TIMEOUTS: Item = Item::new("timeouts"); // /// fields related to the clock module known prior to covenant instatiation. -// pub const PRESET_CLOCK_FIELDS: Item = -// Item::new("preset_clock_fields"); +pub const PRESET_CLOCK_FIELDS: Item = + Item::new("preset_clock_fields"); + +pub const PRESET_HOLDER_FIELDS: Item = Item::new("preset_holder_fields"); +pub const PRESET_SPLITTER_FIELDS: Item = Item::new("preset_splitter_fields"); /// address of the clock module associated with this covenant @@ -30,3 +37,10 @@ pub const COVENANT_CLOCK_ADDR: Item = Item::new("covenant_clock_addr"); pub const COVENANT_INTERCHAIN_SPLITTER_ADDR: Item = Item::new("covenant_interchain_splitter_addr"); /// address of the swap holder contract associated with this covenant pub const COVENANT_SWAP_HOLDER_ADDR: Item = Item::new("covenant_swap_holder_addr"); + + +pub const COVENANT_PARTIES: Item = Item::new("covenant_parties"); +pub const COVENANT_TERMS: Item = Item::new("swap_covenant_terms"); + +pub const PARTY_A_IBC_FORWARDER_ADDR: Item = Item::new("party_a_ibc_forwarder_addr"); +pub const PARTY_B_IBC_FORWARDER_ADDR: Item = Item::new("party_b_ibc_forwarder_addr"); \ No newline at end of file diff --git a/contracts/swap-holder/src/msg.rs b/contracts/swap-holder/src/msg.rs index d4531425..26b26d7c 100644 --- a/contracts/swap-holder/src/msg.rs +++ b/contracts/swap-holder/src/msg.rs @@ -42,6 +42,10 @@ pub struct PresetSwapHolderFields { pub parties_config: CovenantPartiesConfig, /// terms of the covenant pub covenant_terms: CovenantTerms, + /// code id for the contract + pub code_id: u64, + /// contract label + pub label: String, } impl PresetSwapHolderFields { From e4e71e37f1476291aaaf94aaf170978f1ccb65bd Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 6 Sep 2023 17:38:02 +0200 Subject: [PATCH 068/586] readme; suite --- contracts/swap-covenant/README.md | 10 ++++++++++ contracts/swap-covenant/src/suite_test/suite.rs | 2 ++ 2 files changed, 12 insertions(+) diff --git a/contracts/swap-covenant/README.md b/contracts/swap-covenant/README.md index a7c54f6e..2c13eee9 100644 --- a/contracts/swap-covenant/README.md +++ b/contracts/swap-covenant/README.md @@ -1,3 +1,13 @@ # swap covenant Contract responsible for orchestrating flow for a tokenswap between two parties. + +## instantiation chain flow + +Because of inter-contract dependencies, contracts in the covenant are instantiated in a specific order: +1. clock +1. splitter +1. holder +1. party A forwarder +1. party B forwarder +1. (clock whitelisting) \ No newline at end of file diff --git a/contracts/swap-covenant/src/suite_test/suite.rs b/contracts/swap-covenant/src/suite_test/suite.rs index e5fa7128..dd3e79a6 100644 --- a/contracts/swap-covenant/src/suite_test/suite.rs +++ b/contracts/swap-covenant/src/suite_test/suite.rs @@ -37,6 +37,8 @@ impl Default for SuiteBuilder { preset_clock_fields: todo!(), preset_holder_fields: todo!(), covenant_terms: todo!(), + ibc_forwarder_code: todo!(), + covenant_parties: todo!(), }, } } From 5a787716301728ce9e5fd72d1f57b22a66b39d1a Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 7 Sep 2023 15:43:08 +0200 Subject: [PATCH 069/586] including interchain router in the swap covenant flow --- Cargo.lock | 1 + Cargo.toml | 1 + contracts/interchain-router/src/contract.rs | 3 +- contracts/interchain-router/src/msg.rs | 50 +----- contracts/interchain-router/src/state.rs | 2 +- .../src/suite_tests/suite.rs | 3 +- .../src/suite_tests/tests.rs | 3 +- contracts/interchain-splitter/src/msg.rs | 92 +++++++--- .../src/suite_test/suite.rs | 23 +-- .../src/suite_test/tests.rs | 28 +-- contracts/swap-covenant/Cargo.toml | 1 + contracts/swap-covenant/README.md | 2 + contracts/swap-covenant/src/contract.rs | 164 ++++++++++++++---- contracts/swap-covenant/src/msg.rs | 28 ++- contracts/swap-covenant/src/state.rs | 11 +- .../swap-covenant/src/suite_test/suite.rs | 1 + .../swap-holder/src/suite_tests/suite.rs | 8 +- .../swap-holder/src/suite_tests/tests.rs | 8 +- packages/covenant-utils/src/lib.rs | 83 +++++++-- 19 files changed, 327 insertions(+), 185 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66d76e09..f230de2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -617,6 +617,7 @@ dependencies = [ "covenant-depositor", "covenant-holder", "covenant-ibc-forwarder", + "covenant-interchain-router", "covenant-interchain-splitter", "covenant-lp", "covenant-ls", diff --git a/Cargo.toml b/Cargo.toml index 0296bee5..ff8c4980 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ covenant-native-splitter = { path = "contracts/native-splitter" } covenant-interchain-splitter = { path = "contracts/interchain-splitter" } covenant-swap-holder = { path = "contracts/swap-holder" } swap-covenant = { path = "contracts/swap-covenant" } +covenant-interchain-router = { path = "contracts/interchain-router" } # packages clock-derive = { path = "packages/clock-derive" } diff --git a/contracts/interchain-router/src/contract.rs b/contracts/interchain-router/src/contract.rs index 9d8e498b..56e470ea 100644 --- a/contracts/interchain-router/src/contract.rs +++ b/contracts/interchain-router/src/contract.rs @@ -3,11 +3,12 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ to_binary, Attribute, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, }; +use covenant_utils::DestinationConfig; use cw2::set_contract_version; use crate::{ error::ContractError, - msg::{DestinationConfig, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, state::{CLOCK_ADDRESS, DESTINATION_CONFIG}, }; diff --git a/contracts/interchain-router/src/msg.rs b/contracts/interchain-router/src/msg.rs index f8d384a9..0e6d119c 100644 --- a/contracts/interchain-router/src/msg.rs +++ b/contracts/interchain-router/src/msg.rs @@ -3,6 +3,7 @@ use cosmwasm_std::{ Addr, Attribute, Binary, Coin, CosmosMsg, IbcMsg, IbcTimeout, Timestamp, Uint64, }; use covenant_macros::{clocked, covenant_clock_address}; +use covenant_utils::DestinationConfig; #[cw_serde] pub struct InstantiateMsg { @@ -17,55 +18,6 @@ pub struct InstantiateMsg { pub ibc_transfer_timeout: Uint64, } -#[cw_serde] -pub struct DestinationConfig { - /// channel id of the destination chain - pub destination_chain_channel_id: String, - /// address of the receiver on destination chain - pub destination_receiver_addr: Addr, - /// timeout in seconds - pub ibc_transfer_timeout: Uint64, -} - -impl DestinationConfig { - pub fn get_ibc_transfer_messages_for_coins( - &self, - coins: Vec, - current_timestamp: Timestamp, - ) -> Vec { - let mut messages: Vec = vec![]; - - for coin in coins { - let msg: IbcMsg = IbcMsg::Transfer { - channel_id: self.destination_chain_channel_id.to_string(), - to_address: self.destination_receiver_addr.to_string(), - amount: coin, - timeout: IbcTimeout::with_timestamp( - current_timestamp.plus_seconds(self.ibc_transfer_timeout.u64()), - ), - }; - - messages.push(CosmosMsg::Ibc(msg)); - } - - messages - } - - pub fn get_response_attributes(&self) -> Vec { - vec![ - Attribute::new( - "destination_chain_channel_id", - self.destination_chain_channel_id.to_string(), - ), - Attribute::new( - "destination_receiver_addr", - self.destination_receiver_addr.to_string(), - ), - Attribute::new("ibc_transfer_timeout", self.ibc_transfer_timeout), - ] - } -} - #[clocked] #[cw_serde] pub enum ExecuteMsg {} diff --git a/contracts/interchain-router/src/state.rs b/contracts/interchain-router/src/state.rs index ec3f8149..c8de5aba 100644 --- a/contracts/interchain-router/src/state.rs +++ b/contracts/interchain-router/src/state.rs @@ -1,7 +1,7 @@ use cosmwasm_std::Addr; +use covenant_utils::DestinationConfig; use cw_storage_plus::Item; -use crate::msg::DestinationConfig; pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); pub const DESTINATION_CONFIG: Item = Item::new("destination_config"); diff --git a/contracts/interchain-router/src/suite_tests/suite.rs b/contracts/interchain-router/src/suite_tests/suite.rs index cf94953a..2396bd33 100644 --- a/contracts/interchain-router/src/suite_tests/suite.rs +++ b/contracts/interchain-router/src/suite_tests/suite.rs @@ -1,11 +1,12 @@ use crate::{ contract::execute, - msg::{DestinationConfig, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, }; use cosmwasm_std::{ testing::{MockApi, MockStorage}, Addr, Coin, CosmosMsg, Empty, GovMsg, Uint64, }; +use covenant_utils::DestinationConfig; use cw_multi_test::{ App, AppResponse, BankKeeper, BasicAppBuilder, Contract, ContractWrapper, DistributionKeeper, Executor, FailingModule, Ibc, IbcAcceptingModule, StakeKeeper, WasmKeeper, diff --git a/contracts/interchain-router/src/suite_tests/tests.rs b/contracts/interchain-router/src/suite_tests/tests.rs index c163434d..82a3d0c4 100644 --- a/contracts/interchain-router/src/suite_tests/tests.rs +++ b/contracts/interchain-router/src/suite_tests/tests.rs @@ -7,10 +7,11 @@ use cosmwasm_std::{ IbcMsg, IbcTimeout, Never, Querier, QuerierResult, QuerierWrapper, Response, SubMsg, SystemError, SystemResult, Timestamp, Uint128, Uint64, WasmMsg, WasmQuery, }; +use covenant_utils::DestinationConfig; use crate::{ contract::{execute, instantiate}, - msg::{DestinationConfig, ExecuteMsg, InstantiateMsg, MigrateMsg}, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg}, suite_tests::suite::{DEFAULT_CHANNEL, DEFAULT_RECEIVER}, }; diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs index 5d7f674c..e0c10235 100644 --- a/contracts/interchain-splitter/src/msg.rs +++ b/contracts/interchain-splitter/src/msg.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{ - Addr, Attribute, BankMsg, Binary, Coin, CosmosMsg, IbcMsg, IbcTimeout, Uint128, + Addr, Attribute, BankMsg, Binary, Coin, CosmosMsg, IbcTimeout, Uint128, }; use covenant_macros::{clocked, covenant_clock_address}; @@ -30,15 +30,50 @@ pub struct PresetInterchainSplitterFields { } impl PresetInterchainSplitterFields { + /// inserts non-deterministic fields into preset config: + /// - replaces real receiver addresses with their routers + /// - adds clock address pub fn to_instantiate_msg( self, clock_address: String, - ) -> InstantiateMsg { - InstantiateMsg { - clock_address, - splits: self.splits, - fallback_split: self.fallback_split, + party_a_router: String, + party_a_addr: String, + party_b_router: String, + party_b_addr: String, + ) -> Result { + let mut remapped_splits: Vec<(String, SplitType)> = vec![]; + + for (denom, split_type) in self.splits { + match split_type { + SplitType::Custom(config) => { + let remapped_split = config.remap_receivers_to_routers( + party_a_addr.to_string(), + party_a_router.to_string(), + party_b_addr.to_string(), + party_b_router.to_string(), + )?; + remapped_splits.push((denom, remapped_split)); + }, + } } + + let remapped_fallback = match self.fallback_split { + Some(split_type) => match split_type { + SplitType::Custom(config) => Some(config.remap_receivers_to_routers( + party_a_addr.to_string(), + party_a_router.to_string(), + party_b_addr.to_string(), + party_b_router.to_string(), + )?) + }, + None => None, + }; + + Ok(InstantiateMsg { + clock_address, + splits: remapped_splits, + fallback_split: remapped_fallback, + }) } } @@ -84,10 +119,28 @@ impl SplitType { #[cw_serde] pub struct SplitConfig { - pub receivers: Vec<(ReceiverType, Uint128)>, + pub receivers: Vec<(String, Uint128)>, } impl SplitConfig { + pub fn remap_receivers_to_routers(self, receiver_a: String, router_a: String, receiver_b: String, router_b: String) -> Result { + let receivers = self.receivers.into_iter() + .map(|(addr, share)| { + if addr == receiver_a { + (router_a.to_string(), share) + } else if addr == receiver_b { + (router_b.to_string(), share) + } else { + (addr, share) + } + }) + .collect(); + + Ok(SplitType::Custom(SplitConfig { + receivers, + })) + } + pub fn validate(self) -> Result { let total_share: Uint128 = self.receivers.iter().map(|r| r.1).sum(); @@ -105,7 +158,7 @@ impl SplitConfig { ) -> Result, ContractError> { let mut msgs: Vec = vec![]; - for (receiver_type, share) in self.receivers.iter() { + for (receiver, share) in self.receivers.iter() { let entitlement = amount .checked_multiply_ratio(*share, Uint128::new(100)) .map_err(|_| ContractError::SplitMisconfig {})?; @@ -114,19 +167,11 @@ impl SplitConfig { denom: denom.to_string(), amount: entitlement, }; - let msg = match receiver_type { - ReceiverType::Interchain(receiver) => CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: receiver.channel_id.to_string(), - to_address: receiver.address.to_string(), - amount, - timeout: receiver.ibc_timeout.clone(), - }), - ReceiverType::Native(receiver) => CosmosMsg::Bank(BankMsg::Send { - to_address: receiver.address.to_string(), - amount: vec![amount], - }), - }; - msgs.push(msg); + + msgs.push(CosmosMsg::Bank(BankMsg::Send { + to_address: receiver.to_string(), + amount: vec![amount], + })); } Ok(msgs) } @@ -135,10 +180,7 @@ impl SplitConfig { let mut receivers = "[".to_string(); self.receivers.iter().for_each(|(ty, share)| { receivers.push('('); - match ty { - ReceiverType::Interchain(i) => receivers.push_str(&i.address), - ReceiverType::Native(n) => receivers.push_str(&n.address), - }; + receivers.push_str(&ty); receivers.push_str(&share.to_string()); receivers.push_str("),"); }); diff --git a/contracts/interchain-splitter/src/suite_test/suite.rs b/contracts/interchain-splitter/src/suite_test/suite.rs index e25a7f3c..2c3aaa6c 100644 --- a/contracts/interchain-splitter/src/suite_test/suite.rs +++ b/contracts/interchain-splitter/src/suite_test/suite.rs @@ -2,7 +2,7 @@ use cosmwasm_std::{Addr, Coin, Uint128}; use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; use crate::msg::{ - ExecuteMsg, InstantiateMsg, MigrateMsg, NativeReceiver, QueryMsg, ReceiverType, SplitConfig, + ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, SplitConfig, SplitType, }; @@ -22,30 +22,15 @@ pub const CLOCK_ADDR: &str = "clock_addr"; pub fn get_equal_split_config() -> SplitConfig { SplitConfig { receivers: vec![ - ( - ReceiverType::Native(NativeReceiver { - address: PARTY_A_ADDR.to_string(), - }), - Uint128::new(50), - ), - ( - ReceiverType::Native(NativeReceiver { - address: PARTY_B_ADDR.to_string(), - }), - Uint128::new(50), - ), + (PARTY_A_ADDR.to_string(), Uint128::new(50)), + (PARTY_B_ADDR.to_string(), Uint128::new(50)), ], } } pub fn get_fallback_split_config() -> SplitConfig { SplitConfig { - receivers: vec![( - ReceiverType::Native(NativeReceiver { - address: "save_the_cats".to_string(), - }), - Uint128::new(100), - )], + receivers: vec![("save_the_cats".to_string(), Uint128::new(100))], } } diff --git a/contracts/interchain-splitter/src/suite_test/tests.rs b/contracts/interchain-splitter/src/suite_test/tests.rs index 4e86b64f..1ac15b3c 100644 --- a/contracts/interchain-splitter/src/suite_test/tests.rs +++ b/contracts/interchain-splitter/src/suite_test/tests.rs @@ -41,15 +41,11 @@ fn test_instantiate_split_misconfig() { SplitType::Custom(SplitConfig { receivers: vec![ ( - ReceiverType::Native(NativeReceiver { - address: PARTY_A_ADDR.to_string(), - }), + PARTY_A_ADDR.to_string(), Uint128::new(50), ), ( - ReceiverType::Native(NativeReceiver { - address: PARTY_B_ADDR.to_string(), - }), + PARTY_B_ADDR.to_string(), Uint128::new(60), ), ], @@ -101,9 +97,7 @@ fn test_distribute_token_swap() { DENOM_A.to_string(), SplitType::Custom(SplitConfig { receivers: vec![( - ReceiverType::Native(NativeReceiver { - address: PARTY_B_ADDR.to_string(), - }), + PARTY_B_ADDR.to_string(), Uint128::new(100), )], }), @@ -112,9 +106,7 @@ fn test_distribute_token_swap() { DENOM_B.to_string(), SplitType::Custom(SplitConfig { receivers: vec![( - ReceiverType::Native(NativeReceiver { - address: PARTY_A_ADDR.to_string(), - }), + PARTY_A_ADDR.to_string(), Uint128::new(100), )], }), @@ -186,9 +178,7 @@ fn test_migrate_config() { let new_clock = "new_clock".to_string(); let new_fallback_split = SplitConfig { receivers: vec![( - ReceiverType::Native(NativeReceiver { - address: "fallback_new".to_string(), - }), + "fallback_new".to_string(), Uint128::new(100), )], }; @@ -196,9 +186,7 @@ fn test_migrate_config() { "new_denom".to_string(), SplitType::Custom(SplitConfig { receivers: vec![( - ReceiverType::Native(NativeReceiver { - address: "new_receiver".to_string(), - }), + "new_receiver".to_string(), Uint128::new(100), )], }), @@ -221,9 +209,7 @@ fn test_migrate_config() { "new_denom".to_string(), SplitConfig { receivers: vec![( - ReceiverType::Native(NativeReceiver { - address: "new_receiver".to_string() - }), + "new_receiver".to_string(), Uint128::new(100) )], }, diff --git a/contracts/swap-covenant/Cargo.toml b/contracts/swap-covenant/Cargo.toml index c563b841..83fd5c92 100644 --- a/contracts/swap-covenant/Cargo.toml +++ b/contracts/swap-covenant/Cargo.toml @@ -50,6 +50,7 @@ covenant-swap-holder = { workspace = true, features = ["library"] } covenant-utils = { workspace = true } covenant-interchain-splitter = { workspace = true, features = ["library"] } covenant-ibc-forwarder = { workspace = true, features = ["library"] } +covenant-interchain-router = { workspace = true, features = ["library"] } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/swap-covenant/README.md b/contracts/swap-covenant/README.md index 2c13eee9..bcf0c226 100644 --- a/contracts/swap-covenant/README.md +++ b/contracts/swap-covenant/README.md @@ -6,6 +6,8 @@ Contract responsible for orchestrating flow for a tokenswap between two parties. Because of inter-contract dependencies, contracts in the covenant are instantiated in a specific order: 1. clock +1. party A router +1. party B router 1. splitter 1. holder 1. party A forwarder diff --git a/contracts/swap-covenant/src/contract.rs b/contracts/swap-covenant/src/contract.rs index ef689fde..383ac5ca 100644 --- a/contracts/swap-covenant/src/contract.rs +++ b/contracts/swap-covenant/src/contract.rs @@ -5,16 +5,14 @@ use cosmwasm_std::{ SubMsg, WasmMsg, Reply, }; -use covenant_utils::RefundConfig; use cw2::set_contract_version; -use cw_utils::parse_reply_instantiate_data; +use cw_utils::{parse_reply_instantiate_data, ParseReplyError}; use crate::{ error::ContractError, - msg::InstantiateMsg, state::{ - CLOCK_CODE, TIMEOUTS, COVENANT_CLOCK_ADDR, SWAP_HOLDER_CODE, PRESET_HOLDER_FIELDS, COVENANT_INTERCHAIN_SPLITTER_ADDR, INTECHAIN_SPLITTER_CODE, PRESET_SPLITTER_FIELDS, COVENANT_SWAP_HOLDER_ADDR, IBC_FORWARDER_CODE, IBC_FEE, COVENANT_PARTIES, COVENANT_TERMS, PARTY_A_IBC_FORWARDER_ADDR, PARTY_B_IBC_FORWARDER_ADDR, - }, + CLOCK_CODE, TIMEOUTS, COVENANT_CLOCK_ADDR, SWAP_HOLDER_CODE, PRESET_HOLDER_FIELDS, COVENANT_INTERCHAIN_SPLITTER_ADDR, INTECHAIN_SPLITTER_CODE, PRESET_SPLITTER_FIELDS, COVENANT_SWAP_HOLDER_ADDR, IBC_FORWARDER_CODE, IBC_FEE, COVENANT_PARTIES, COVENANT_TERMS, PARTY_A_IBC_FORWARDER_ADDR, PARTY_B_IBC_FORWARDER_ADDR, PARTY_A_INTERCHAIN_ROUTER_ADDR, INTERCHAIN_ROUTER_CODE, PARTY_B_INTERCHAIN_ROUTER_ADDR, + }, msg::InstantiateMsg, }; const CONTRACT_NAME: &str = "crates.io:swap-covenant"; @@ -25,6 +23,9 @@ pub const SPLITTER_REPLY_ID: u64 = 2u64; pub const SWAP_HOLDER_REPLY_ID: u64 = 3u64; pub const PARTY_A_FORWARDER_REPLY_ID: u64 = 4u64; pub const PARTY_B_FORWARDER_REPLY_ID: u64 = 5u64; +pub const PARTY_A_INTERCHAIN_ROUTER_REPLY_ID: u64 = 6u64; +pub const PARTY_B_INTERCHAIN_ROUTER_REPLY_ID: u64 = 7u64; + #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -71,12 +72,14 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result handle_swap_holder_reply(deps, env, msg), PARTY_A_FORWARDER_REPLY_ID => handle_party_a_ibc_forwarder_reply(deps, env, msg), PARTY_B_FORWARDER_REPLY_ID => handle_party_b_ibc_forwarder_reply(deps, env, msg), + PARTY_A_INTERCHAIN_ROUTER_REPLY_ID => handle_party_a_interchain_router_reply(deps, env, msg), + PARTY_B_INTERCHAIN_ROUTER_REPLY_ID => handle_party_b_interchain_router_reply(deps, env, msg), _ => Err(ContractError::UnknownReplyId {}), } } /// clock instantiation reply means we can proceed with the instantiation chain. -/// we store the clock address and submit the splitter instantiate tx. +/// we store the clock address and submit the party A router instantiate tx. pub fn handle_clock_reply(deps: DepsMut, env: Env, msg: Reply) -> Result { deps.api.debug("WASMDEBUG: clock reply"); @@ -87,29 +90,127 @@ pub fn handle_clock_reply(deps: DepsMut, env: Env, msg: Reply) -> Result Err(ContractError::ContractInstantiationError { + contract: "clock".to_string(), + err, + }), + } +} + + +/// party A interchain router instantiation reply means we can proceed with the instantiation chain. +/// we store the instantiated router address and submit the party B router instantiation tx. +pub fn handle_party_a_interchain_router_reply(deps: DepsMut, env: Env, msg: Reply) -> Result { + deps.api.debug("WASMDEBUG: party A interchain router reply"); + + let parsed_data = parse_reply_instantiate_data(msg); + match parsed_data { + Ok(response) => { + // validate and store the instantiated router address + let router_addr = deps.api.addr_validate(&response.contract_address)?; + PARTY_A_INTERCHAIN_ROUTER_ADDR.save(deps.storage, &router_addr)?; + + // load the fields relevant to router instantiation + let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; + let code_id = INTERCHAIN_ROUTER_CODE.load(deps.storage)?; + let party_config = COVENANT_PARTIES.load(deps.storage)?.party_b; + + let party_b_router_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.to_string()), + code_id, + msg: to_binary( + &covenant_interchain_router::msg::InstantiateMsg { + clock_address: clock_addr.to_string(), + destination_chain_channel_id: party_config.party_chain_channel_id, + destination_receiver_addr: party_config.addr.to_string(), + ibc_transfer_timeout: party_config.ibc_transfer_timeout, + }, + )?, + funds: vec![], + label: "party b router".to_string(), + }); + + Ok(Response::default() + .add_attribute("method", "handle_party_a_interchain_router_reply") + .add_attribute("party_a_interchain_router_addr", router_addr) + .add_submessage(SubMsg::reply_always(party_b_router_instantiate_tx, PARTY_B_INTERCHAIN_ROUTER_REPLY_ID))) + } + Err(err) => Err(ContractError::ContractInstantiationError { + contract: "party a router".to_string(), + err, + }), + } +} + +/// party B interchain router instantiation reply means we can proceed with the instantiation chain. +/// we store the instantiated router address and submit the interchain splitter instantiation tx. +pub fn handle_party_b_interchain_router_reply(deps: DepsMut, env: Env, msg: Reply) -> Result { + deps.api.debug("WASMDEBUG: party B interchain router reply"); + + let parsed_data = parse_reply_instantiate_data(msg); + match parsed_data { + Ok(response) => { + // validate and store the instantiated router address + let router_addr = deps.api.addr_validate(&response.contract_address)?; + PARTY_B_INTERCHAIN_ROUTER_ADDR.save(deps.storage, &router_addr)?; + + // load the fields relevant to splitter let code_id = INTECHAIN_SPLITTER_CODE.load(deps.storage)?; let preset_splitter_fields = PRESET_SPLITTER_FIELDS.load(deps.storage)?; + let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; + let party_a_router = PARTY_A_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; + let swap_parties = COVENANT_PARTIES.load(deps.storage)?; + + let splitter_instantiate_msg = preset_splitter_fields.clone().to_instantiate_msg( + clock_addr.to_string(), + party_a_router.to_string(), + swap_parties.party_a.addr.to_string(), + router_addr.to_string(), + swap_parties.party_b.addr.to_string(), + ).map_err(|e| ContractError::ContractInstantiationError { + contract: "splitter".to_string(), + err: ParseReplyError::ParseFailure(e.to_string()), + })?; let splitter_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(env.contract.address.to_string()), code_id, - msg: to_binary( - &preset_splitter_fields - .clone() - .to_instantiate_msg(clock_addr.to_string()), - )?, + msg: to_binary(&splitter_instantiate_msg)?, funds: vec![], label: preset_splitter_fields.label, }); Ok(Response::default() - .add_attribute("method", "handle_clock_reply") - .add_attribute("clock_address", clock_addr) + .add_attribute("method", "handle_party_b_interchain_router_reply") + .add_attribute("party_b_interchain_router_addr", router_addr) .add_submessage(SubMsg::reply_always(splitter_instantiate_tx, SPLITTER_REPLY_ID))) } Err(err) => Err(ContractError::ContractInstantiationError { - contract: "clock".to_string(), + contract: "party b router".to_string(), err, }), } @@ -173,22 +274,15 @@ pub fn handle_swap_holder_reply(deps: DepsMut, env: Env, msg: Reply) -> Result r, - _ => return Err(ContractError::ContractInstantiationError { - contract: "party_a_forwarder".to_string(), - err: cw_utils::ParseReplyError::ParseFailure("no remote chain info".to_string()), - }) - }; let instantiate_msg = covenant_ibc_forwarder::msg::InstantiateMsg { clock_address: clock_addr.to_string(), next_contract: swap_holder_addr.to_string(), - remote_chain_connection_id: refund_config.connection_id, - remote_chain_channel_id: refund_config.channel_id, - denom: covenant_parties.party_a.provided_denom, + remote_chain_connection_id: covenant_party.party_chain_connection_id, + remote_chain_channel_id: covenant_party.party_chain_channel_id, + denom: covenant_party.provided_denom, amount: covenant_terms.party_a_amount, ibc_fee, ibc_transfer_timeout: timeouts.ibc_transfer_timeout, @@ -232,24 +326,16 @@ pub fn handle_party_a_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) - let code_id = IBC_FORWARDER_CODE.load(deps.storage)?; let timeouts = TIMEOUTS.load(deps.storage)?; let ibc_fee = IBC_FEE.load(deps.storage)?; - let covenant_parties = COVENANT_PARTIES.load(deps.storage)?; + let covenant_party = COVENANT_PARTIES.load(deps.storage)?.party_b; let covenant_terms = COVENANT_TERMS.load(deps.storage)?; let swap_holder = COVENANT_SWAP_HOLDER_ADDR.load(deps.storage)?; - let refund_config = match covenant_parties.party_b.refund_config { - RefundConfig::Ibc(r) => r, - _ => return Err(ContractError::ContractInstantiationError { - contract: "party_b_forwarder".to_string(), - err: cw_utils::ParseReplyError::ParseFailure("no remote chain info".to_string()), - }) - }; - let instantiate_msg = covenant_ibc_forwarder::msg::InstantiateMsg { clock_address: clock_addr.to_string(), next_contract: swap_holder.to_string(), - remote_chain_connection_id: refund_config.connection_id, - remote_chain_channel_id: refund_config.channel_id, - denom: covenant_parties.party_b.provided_denom, + remote_chain_connection_id: covenant_party.party_chain_connection_id, + remote_chain_channel_id: covenant_party.party_chain_channel_id, + denom: covenant_party.provided_denom, amount: covenant_terms.party_b_amount, ibc_fee, ibc_transfer_timeout: timeouts.ibc_transfer_timeout, @@ -297,6 +383,8 @@ pub fn handle_party_b_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) - let party_a_forwarder = PARTY_A_IBC_FORWARDER_ADDR.load(deps.storage)?; let interchain_splitter = COVENANT_INTERCHAIN_SPLITTER_ADDR.load(deps.storage)?; + let party_a_router = PARTY_A_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; + let party_b_router = PARTY_B_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; let update_clock_whitelist_msg = WasmMsg::Migrate { @@ -308,6 +396,8 @@ pub fn handle_party_b_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) - party_b_ibc_forwarder_addr.to_string(), swap_holder.to_string(), interchain_splitter.to_string(), + party_a_router.to_string(), + party_b_router.to_string(), ]), remove: None, })?, diff --git a/contracts/swap-covenant/src/msg.rs b/contracts/swap-covenant/src/msg.rs index cb9b869f..5e29f156 100644 --- a/contracts/swap-covenant/src/msg.rs +++ b/contracts/swap-covenant/src/msg.rs @@ -18,14 +18,38 @@ pub struct InstantiateMsg { pub timeouts: Timeouts, pub ibc_forwarder_code: u64, - + pub interchain_router_code: u64, + /// instantiation fields relevant to clock module known in advance pub preset_clock_fields: PresetClockFields, /// instantiation fields relevant to swap holder contract known in advance pub preset_holder_fields: PresetSwapHolderFields, pub covenant_terms: SwapCovenantTerms, - pub covenant_parties: CovenantPartiesConfig, + pub covenant_parties: SwapCovenantParties, +} + +#[cw_serde] +pub struct SwapCovenantParties { + pub party_a: SwapPartyConfig, + pub party_b: SwapPartyConfig, +} + +#[cw_serde] +pub struct SwapPartyConfig { + /// authorized address of the party + pub addr: Addr, + /// denom provided by the party + pub provided_denom: String, + /// channel id of the destination chain + pub party_chain_channel_id: String, + /// address of the receiver on destination chain + pub party_receiver_addr: Addr, + /// connection id to the party chain + pub party_chain_connection_id: String, + /// timeout in seconds + pub ibc_transfer_timeout: Uint64, + } #[cw_serde] diff --git a/contracts/swap-covenant/src/state.rs b/contracts/swap-covenant/src/state.rs index 011d8af1..88b8aaa9 100644 --- a/contracts/swap-covenant/src/state.rs +++ b/contracts/swap-covenant/src/state.rs @@ -6,7 +6,7 @@ use covenant_utils::{CovenantPartiesConfig, SwapCovenantTerms}; use cw_storage_plus::Item; use neutron_sdk::bindings::msg::IbcFee; -use crate::msg::Timeouts; +use crate::msg::{Timeouts, SwapCovenantParties}; /// contract code for the ibc forwarder pub const IBC_FORWARDER_CODE: Item = Item::new("ibc_forwarder_code"); @@ -16,6 +16,8 @@ pub const INTECHAIN_SPLITTER_CODE: Item = Item::new("interchain_splitter"); pub const SWAP_HOLDER_CODE: Item = Item::new("swap_holder_code"); /// contract code for the clock module pub const CLOCK_CODE: Item = Item::new("clock_code"); +/// contract code for the interchain router +pub const INTERCHAIN_ROUTER_CODE: Item = Item::new("interchain_router_code"); /// ibc fee for the relayers pub const IBC_FEE: Item = Item::new("ibc_fee"); @@ -39,8 +41,11 @@ pub const COVENANT_INTERCHAIN_SPLITTER_ADDR: Item = Item::new("covenant_in pub const COVENANT_SWAP_HOLDER_ADDR: Item = Item::new("covenant_swap_holder_addr"); -pub const COVENANT_PARTIES: Item = Item::new("covenant_parties"); +pub const COVENANT_PARTIES: Item = Item::new("covenant_parties"); pub const COVENANT_TERMS: Item = Item::new("swap_covenant_terms"); pub const PARTY_A_IBC_FORWARDER_ADDR: Item = Item::new("party_a_ibc_forwarder_addr"); -pub const PARTY_B_IBC_FORWARDER_ADDR: Item = Item::new("party_b_ibc_forwarder_addr"); \ No newline at end of file +pub const PARTY_B_IBC_FORWARDER_ADDR: Item = Item::new("party_b_ibc_forwarder_addr"); + +pub const PARTY_A_INTERCHAIN_ROUTER_ADDR: Item = Item::new("party_a_interchain_router_addr"); +pub const PARTY_B_INTERCHAIN_ROUTER_ADDR: Item = Item::new("party_b_interchain_router_addr"); \ No newline at end of file diff --git a/contracts/swap-covenant/src/suite_test/suite.rs b/contracts/swap-covenant/src/suite_test/suite.rs index dd3e79a6..1094244f 100644 --- a/contracts/swap-covenant/src/suite_test/suite.rs +++ b/contracts/swap-covenant/src/suite_test/suite.rs @@ -39,6 +39,7 @@ impl Default for SuiteBuilder { covenant_terms: todo!(), ibc_forwarder_code: todo!(), covenant_parties: todo!(), + interchain_router_code: todo!(), }, } } diff --git a/contracts/swap-holder/src/suite_tests/suite.rs b/contracts/swap-holder/src/suite_tests/suite.rs index a470e0a1..9103ecb9 100644 --- a/contracts/swap-holder/src/suite_tests/suite.rs +++ b/contracts/swap-holder/src/suite_tests/suite.rs @@ -1,8 +1,8 @@ use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg}; use cosmwasm_std::{Addr, Coin, Uint128}; use covenant_utils::{ - CovenantPartiesConfig, CovenantParty, CovenantTerms, LockupConfig, RefundConfig, - SwapCovenantTerms, + CovenantPartiesConfig, CovenantParty, CovenantTerms, LockupConfig, + SwapCovenantTerms, ReceiverConfig, }; use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; @@ -46,14 +46,14 @@ impl Default for SuiteBuilder { party_a: CovenantParty { addr: Addr::unchecked(PARTY_A_ADDR.to_string()), provided_denom: DENOM_A.to_string(), - refund_config: RefundConfig::Native(Addr::unchecked( + receiver_config: ReceiverConfig::Native(Addr::unchecked( PARTY_A_ADDR.to_string(), )), }, party_b: CovenantParty { addr: Addr::unchecked(PARTY_B_ADDR.to_string()), provided_denom: DENOM_B.to_string(), - refund_config: RefundConfig::Native(Addr::unchecked( + receiver_config: ReceiverConfig::Native(Addr::unchecked( PARTY_B_ADDR.to_string(), )), }, diff --git a/contracts/swap-holder/src/suite_tests/tests.rs b/contracts/swap-holder/src/suite_tests/tests.rs index bde902c2..d0be2dad 100644 --- a/contracts/swap-holder/src/suite_tests/tests.rs +++ b/contracts/swap-holder/src/suite_tests/tests.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{Addr, Coin, Timestamp, Uint128}; use covenant_utils::{ - CovenantPartiesConfig, CovenantParty, CovenantTerms, LockupConfig, RefundConfig, - SwapCovenantTerms, + CovenantPartiesConfig, CovenantParty, CovenantTerms, LockupConfig, + SwapCovenantTerms, ReceiverConfig, }; use crate::{ @@ -33,12 +33,12 @@ fn test_instantiate_happy_and_query_all() { party_a: CovenantParty { addr: Addr::unchecked(PARTY_A_ADDR.to_string()), provided_denom: DENOM_A.to_string(), - refund_config: RefundConfig::Native(Addr::unchecked(PARTY_A_ADDR.to_string())), + receiver_config: ReceiverConfig::Native(Addr::unchecked(PARTY_A_ADDR.to_string())), }, party_b: CovenantParty { addr: Addr::unchecked(PARTY_B_ADDR.to_string()), provided_denom: DENOM_B.to_string(), - refund_config: RefundConfig::Native(Addr::unchecked(PARTY_B_ADDR.to_string())), + receiver_config: ReceiverConfig::Native(Addr::unchecked(PARTY_B_ADDR.to_string())), }, } ); diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index 14d19469..02fdba2a 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{ Addr, Attribute, BankMsg, BlockInfo, Coin, CosmosMsg, IbcMsg, IbcTimeout, StdError, Timestamp, - Uint128, + Uint128, Uint64, }; use neutron_ica::RemoteChainInfo; @@ -229,18 +229,18 @@ impl LockupConfig { } #[cw_serde] -pub enum RefundConfig { - /// party expects a refund on the same chain +pub enum ReceiverConfig { + /// party expects to receive funds on the same chain Native(Addr), - /// party expects a refund on a remote chain - Ibc(RemoteChainInfo), + /// party expects to receive funds on a remote chain + Ibc(DestinationConfig), } -impl RefundConfig { +impl ReceiverConfig { pub fn get_response_attributes(self, party: String) -> Vec { match self { - RefundConfig::Native(addr) => vec![Attribute::new("refund_config_native_addr", addr)], - RefundConfig::Ibc(r_c_i) => r_c_i + ReceiverConfig::Native(addr) => vec![Attribute::new("receiver_config_native_addr", addr)], + ReceiverConfig::Ibc(destination_config) => destination_config .get_response_attributes() .into_iter() .map(|mut a| { @@ -258,29 +258,29 @@ pub struct CovenantParty { pub addr: Addr, /// denom provided by the party pub provided_denom: String, - /// config for refunding funds in case covenant fails to complete - pub refund_config: RefundConfig, + /// information about receiver address + pub receiver_config: ReceiverConfig, } impl CovenantParty { pub fn get_refund_msg(self, amount: Uint128, block: &BlockInfo) -> CosmosMsg { - match self.refund_config { - RefundConfig::Native(addr) => CosmosMsg::Bank(BankMsg::Send { + match self.receiver_config { + ReceiverConfig::Native(addr) => CosmosMsg::Bank(BankMsg::Send { to_address: addr.to_string(), amount: vec![Coin { denom: self.provided_denom, amount, }], }), - RefundConfig::Ibc(r_c_i) => CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: r_c_i.channel_id, + ReceiverConfig::Ibc(destination_config) => CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: destination_config.destination_chain_channel_id, to_address: self.addr.to_string(), amount: Coin { denom: self.provided_denom, amount, }, timeout: IbcTimeout::with_timestamp( - block.time.plus_seconds(r_c_i.ibc_transfer_timeout.u64()), + block.time.plus_seconds(destination_config.ibc_transfer_timeout.u64()), ), }), } @@ -303,12 +303,12 @@ impl CovenantPartiesConfig { ]; attrs.extend( self.party_a - .refund_config + .receiver_config .get_response_attributes("party_a_".to_string()), ); attrs.extend( self.party_b - .refund_config + .receiver_config .get_response_attributes("party_b_".to_string()), ); attrs @@ -340,3 +340,52 @@ impl CovenantTerms { } } } + +#[cw_serde] +pub struct DestinationConfig { + /// channel id of the destination chain + pub destination_chain_channel_id: String, + /// address of the receiver on destination chain + pub destination_receiver_addr: Addr, + /// timeout in seconds + pub ibc_transfer_timeout: Uint64, +} + +impl DestinationConfig { + pub fn get_ibc_transfer_messages_for_coins( + &self, + coins: Vec, + current_timestamp: Timestamp, + ) -> Vec { + let mut messages: Vec = vec![]; + + for coin in coins { + let msg: IbcMsg = IbcMsg::Transfer { + channel_id: self.destination_chain_channel_id.to_string(), + to_address: self.destination_receiver_addr.to_string(), + amount: coin, + timeout: IbcTimeout::with_timestamp( + current_timestamp.plus_seconds(self.ibc_transfer_timeout.u64()), + ), + }; + + messages.push(CosmosMsg::Ibc(msg)); + } + + messages + } + + pub fn get_response_attributes(&self) -> Vec { + vec![ + Attribute::new( + "destination_chain_channel_id", + self.destination_chain_channel_id.to_string(), + ), + Attribute::new( + "destination_receiver_addr", + self.destination_receiver_addr.to_string(), + ), + Attribute::new("ibc_transfer_timeout", self.ibc_transfer_timeout), + ] + } +} From 4500fc9f54ffd8290c77caff23ad8040ea8e1d95 Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 8 Sep 2023 15:32:10 +0200 Subject: [PATCH 070/586] interchaintest v4-ics init --- .../src/suite_test/tests.rs | 2 +- go.work | 3 + go.work.sum | 1383 +++++++++++++++ swap-covenant/README.md | 3 + swap-covenant/justfile | 35 + swap-covenant/optimize.sh | 16 + swap-covenant/tests/interchaintest/README.md | 21 + .../interchaintest/connection_helpers.go | 292 ++++ .../tests/interchaintest/genesis_helpers.go | 116 ++ swap-covenant/tests/interchaintest/go.mod | 204 +++ swap-covenant/tests/interchaintest/go.sum | 1490 +++++++++++++++++ .../tests/interchaintest/tokenswap_test.go | 282 ++++ swap-covenant/tests/interchaintest/types.go | 459 +++++ 13 files changed, 4305 insertions(+), 1 deletion(-) create mode 100644 go.work create mode 100644 go.work.sum create mode 100644 swap-covenant/README.md create mode 100644 swap-covenant/justfile create mode 100755 swap-covenant/optimize.sh create mode 100644 swap-covenant/tests/interchaintest/README.md create mode 100644 swap-covenant/tests/interchaintest/connection_helpers.go create mode 100644 swap-covenant/tests/interchaintest/genesis_helpers.go create mode 100644 swap-covenant/tests/interchaintest/go.mod create mode 100644 swap-covenant/tests/interchaintest/go.sum create mode 100644 swap-covenant/tests/interchaintest/tokenswap_test.go create mode 100644 swap-covenant/tests/interchaintest/types.go diff --git a/contracts/interchain-splitter/src/suite_test/tests.rs b/contracts/interchain-splitter/src/suite_test/tests.rs index 1ac15b3c..ab567807 100644 --- a/contracts/interchain-splitter/src/suite_test/tests.rs +++ b/contracts/interchain-splitter/src/suite_test/tests.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{Coin, Uint128}; use crate::{ - msg::{MigrateMsg, NativeReceiver, ReceiverType, SplitConfig, SplitType}, + msg::{MigrateMsg, SplitConfig, SplitType}, suite_test::suite::{ get_equal_split_config, get_fallback_split_config, ALT_DENOM, CLOCK_ADDR, DENOM_B, }, diff --git a/go.work b/go.work new file mode 100644 index 00000000..c8315910 --- /dev/null +++ b/go.work @@ -0,0 +1,3 @@ +go 1.20 + +use ./swap-covenant/tests/interchaintest diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 00000000..62a3619a --- /dev/null +++ b/go.work.sum @@ -0,0 +1,1383 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigtable v1.2.0/go.mod h1:JcVAOl45lrTmQfLj7T6TxyMzIN/3FGGcFm+2xVAli2o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE= +cosmossdk.io/api v0.2.6 h1:AoNwaLLapcLsphhMK6+o0kZl+D6MMUaHVqSdwinASGU= +cosmossdk.io/api v0.2.6/go.mod h1:u/d+GAxil0nWpl1XnQL8nkziQDIWuBDhv8VnDm/s6dI= +cosmossdk.io/core v0.5.1 h1:vQVtFrIYOQJDV3f7rw4pjjVqc1id4+mE0L9hHP66pyI= +cosmossdk.io/core v0.5.1/go.mod h1:KZtwHCLjcFuo0nmDc24Xy6CRNEL9Vl/MeimQ2aC7NLE= +cosmossdk.io/depinject v1.0.0-alpha.3 h1:6evFIgj//Y3w09bqOUOzEpFj5tsxBqdc5CfkO7z+zfw= +cosmossdk.io/depinject v1.0.0-alpha.3/go.mod h1:eRbcdQ7MRpIPEM5YUJh8k97nxHpYbc3sMUnEtt8HPWU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= +filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= +github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= +github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0/go.mod h1:tPaiy8S5bQ+S5sOiDlINkp7+Ef339+Nz5L5XO+cnOHo= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d h1:nalkkPQcITbvhmL4+C4cKA87NW0tfm3Kl9VXRoPywFg= +github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d/go.mod h1:URdX5+vg25ts3aCh8H5IFZybJYKWhJHYMTnf+ULtoC4= +github.com/ChainSafe/go-schnorrkel v1.0.0 h1:3aDA67lAykLaG1y3AOjs88dMxC88PgUuHRrLeDnvGIM= +github.com/ChainSafe/go-schnorrkel v1.0.0/go.mod h1:dpzHYVxLZcp8pjlV+O+UR8K0Hp/z7vcchBSbMBEhCw4= +github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= +github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w= +github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= +github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/StirlingMarketingGroup/go-namecase v1.0.0 h1:2CzaNtCzc4iNHirR+5ru9OzGg8rQp860gqLBFqRI02Y= +github.com/StirlingMarketingGroup/go-namecase v1.0.0/go.mod h1:ZsoSKcafcAzuBx+sndbxHu/RjDcDTrEdT4UvhniHfio= +github.com/VictoriaMetrics/fastcache v1.6.0/go.mod h1:0qHz5QP0GMX4pfmMA/zt5RgfNuXJrTP0zS7DqpHGGTw= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/arrow/go/arrow v0.0.0-20191024131854-af6fa24be0db/go.mod h1:VTxUBvSJ3s3eHAg65PNgrsn5BtqCRPdmyXh6rAfdxN0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/avast/retry-go/v4 v4.0.4 h1:38hLf0DsRXh+hOF6HbTni0+5QGTNdw9zbaMD7KAO830= +github.com/avast/retry-go/v4 v4.0.4/go.mod h1:HqmLvS2VLdStPCGDFjSuZ9pzlTqVRldCI4w2dO4m1Ms= +github.com/aws/aws-sdk-go-v2 v1.2.0/go.mod h1:zEQs02YRBw1DjK0PoJv3ygDYOFTre1ejlJWl8FwAuQo= +github.com/aws/aws-sdk-go-v2/config v1.1.1/go.mod h1:0XsVy9lBI/BCXm+2Tuvt39YmdHwS5unDQmxZOYe8F5Y= +github.com/aws/aws-sdk-go-v2/credentials v1.1.1/go.mod h1:mM2iIjwl7LULWtS6JCACyInboHirisUUdkBPoTHMOUo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.2/go.mod h1:3hGg3PpiEjHnrkrlasTfxFqUsZ2GCk/fMUn4CbKgSkM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.2/go.mod h1:45MfaXZ0cNbeuT0KQ1XJylq8A6+OpVV2E5kvY/Kq+u8= +github.com/aws/aws-sdk-go-v2/service/route53 v1.1.1/go.mod h1:rLiOUrPLW/Er5kRcQ7NkwbjlijluLsrIbu/iyl35RO4= +github.com/aws/aws-sdk-go-v2/service/sso v1.1.1/go.mod h1:SuZJxklHxLAXgLTc1iFXbEWkXs7QRTQpCLGaKIprQW0= +github.com/aws/aws-sdk-go-v2/service/sts v1.1.1/go.mod h1:Wi0EBZwiz/K44YliU0EKxqTCJGUfYTWXrrBwkq736bM= +github.com/aws/smithy-go v1.1.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw= +github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/btcsuite/btcd v0.22.2 h1:vBZ+lGGd1XubpOWO67ITJpAEsICWhA0YzqkcpkgNBfo= +github.com/btcsuite/btcd v0.22.2/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y= +github.com/btcsuite/btcd/btcec/v2 v2.1.2/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/centrifuge/go-substrate-rpc-client/v4 v4.0.4 h1:G2kCJurlIkguX0oxxI9sPPENuQqMVhIhV9RVkh/dpDg= +github.com/centrifuge/go-substrate-rpc-client/v4 v4.0.4/go.mod h1:5g1oM4Zu3BOaLpsKQ+O8PAv2kNuq+kPcA1VzFbsSqxE= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cloudflare-go v0.14.0/go.mod h1:EnwdgGMaFOruiPZRFSgn+TsQ3hQ7C/YWzIGLeu5c304= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cockroachdb/datadriven v1.0.0/go.mod h1:5Ib8Meh+jk1RlHIXej6Pzevx/NLlNvQB9pmSBZErGA4= +github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/cockroachdb/errors v1.6.1/go.mod h1:tm6FTP5G81vwJ5lC0SizQo374JNCOPrHyXGitRJoDqM= +github.com/cockroachdb/errors v1.8.1/go.mod h1:qGwQn6JmZ+oMjuLwjWzUNqblqk0xl4CVV3SQbGwK7Ac= +github.com/cockroachdb/errors v1.9.1/go.mod h1:2sxOtL2WIc096WSZqZ5h8fa17rdDq9HZOZLBCor4mBk= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v0.0.0-20220817183557-09c6e030a677/go.mod h1:890yq1fUb9b6dGNwssgeUO5vQV9qfXnCPxAJhBQfXw0= +github.com/cockroachdb/redact v1.0.8/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2/go.mod h1:8BT+cPK6xvFOcRlk0R8eg+OTkcqI6baNH4xAkpiYVvQ= +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= +github.com/cometbft/cometbft v0.34.27 h1:ri6BvmwjWR0gurYjywcBqRe4bbwc3QVs9KRcCzgh/J0= +github.com/cometbft/cometbft v0.34.27/go.mod h1:BcCbhKv7ieM0KEddnYXvQZR+pZykTKReJJYf7YC7qhw= +github.com/confio/ics23/go v0.9.0 h1:cWs+wdbS2KRPZezoaaj+qBleXgUk5WOQFMP3CQFGTr4= +github.com/confio/ics23/go v0.9.0/go.mod h1:4LPZ2NYqnYIVRklaozjNR1FScgDJ2s5Xrp+e/mYVRak= +github.com/consensys/bavard v0.1.8-0.20210406032232-f3452dc9b572/go.mod h1:Bpd0/3mZuaj6Sj+PqrmIquiOKy397AKGThQPaGzNXAQ= +github.com/consensys/gnark-crypto v0.4.1-0.20210426202927-39ac3d4b3f1f/go.mod h1:815PAHg3wvysy0SyIqanF8gZ0Y1wjk/hrDHD/iT88+Q= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cosmos/btcutil v1.0.4 h1:n7C2ngKXo7UC9gNyMNLbzqz7Asuf+7Qv4gnX/rOdQ44= +github.com/cosmos/btcutil v1.0.4/go.mod h1:Ffqc8Hn6TJUdDgHBwIZLtrLQC1KdJ9jGJl/TvgUaxbU= +github.com/cosmos/cosmos-db v0.0.0-20221226095112-f3c38ecb5e32 h1:zlCp9n3uwQieELltZWHRmwPmPaZ8+XoL2Sj+A2YJlr8= +github.com/cosmos/cosmos-db v0.0.0-20221226095112-f3c38ecb5e32/go.mod h1:kwMlEC4wWvB48zAShGKVqboJL6w4zCLesaNQ3YLU2BQ= +github.com/cosmos/cosmos-proto v1.0.0-beta.1 h1:iDL5qh++NoXxG8hSy93FdYJut4XfgbShIocllGaXx/0= +github.com/cosmos/cosmos-proto v1.0.0-beta.1/go.mod h1:8k2GNZghi5sDRFw/scPL8gMSowT1vDA+5ouxL8GjaUE= +github.com/cosmos/cosmos-sdk v0.45.16-ics h1:KsPigLNmdyyQMktAsJzW42eBFsq1uajhQF7rlnHDUgM= +github.com/cosmos/cosmos-sdk v0.45.16-ics/go.mod h1:bScuNwWAP0TZJpUf+SHXRU3xGoUPp+X9nAzfeIXts40= +github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= +github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= +github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= +github.com/cosmos/gorocksdb v1.2.0/go.mod h1:aaKvKItm514hKfNJpUJXnnOWeBnk2GL4+Qw9NHizILw= +github.com/cosmos/iavl v0.19.5 h1:rGA3hOrgNxgRM5wYcSCxgQBap7fW82WZgY78V9po/iY= +github.com/cosmos/iavl v0.19.5/go.mod h1:X9PKD3J0iFxdmgNLa7b2LYWdsGd90ToV5cAONApkEPw= +github.com/cosmos/ibc-go/v4 v4.4.2 h1:PG4Yy0/bw6Hvmha3RZbc53KYzaCwuB07Ot4GLyzcBvo= +github.com/cosmos/ibc-go/v4 v4.4.2/go.mod h1:j/kD2JCIaV5ozvJvaEkWhLxM2zva7/KTM++EtKFYcB8= +github.com/cosmos/interchain-security v1.0.0 h1:xNQjjigqH3mzEKSGQhAhKy8I0TA8XR2z5rRTxRBKK3o= +github.com/cosmos/interchain-security v1.0.0/go.mod h1:J9SbXUJT1GSe+mZy+MDCxtuAfbhwCKBEJRYnfjXsE8Q= +github.com/cosmos/ledger-cosmos-go v0.12.2/go.mod h1:ZcqYgnfNJ6lAXe4HPtWgarNEY+B74i+2/8MhZw4ziiI= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= +github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= +github.com/decred/base58 v1.0.3 h1:KGZuh8d1WEMIrK0leQRM47W85KqCAdl2N+uagbctdDI= +github.com/decred/base58 v1.0.3/go.mod h1:pXP9cXCfM2sFLb2viz2FNIdeMWmZDBKG3ZBYbiSM78E= +github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0 h1:3GIJYXQDAKpLEFriGFN8SbSffak10UXHGdIcFaMPykY= +github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0/go.mod h1:3s92l0paYkZoIHuj4X93Teg/HB7eGM9x/zokGw+u4mY= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= +github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= +github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-bitstream v0.0.0-20180413035011-3522498ce2c8/go.mod h1:VMaSuZ+SZcx/wljOQKvp5srsbCiKDEb6K2wC4+PiBmQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.19+incompatible h1:lzEmjivyNHFHMNAFLXORMBXyGIhw/UP4DvJwvyKYq64= +github.com/docker/docker v20.10.19+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dop251/goja v0.0.0-20211011172007-d99e4b8cbf48/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= +github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/ethereum/go-ethereum v1.10.17 h1:XEcumY+qSr1cZQaWsQs5Kck3FHB0V2RiMHPdTBJ+oT8= +github.com/ethereum/go-ethereum v1.10.17/go.mod h1:Lt5WzjM07XlXc95YzrhosmR4J9Ahd6X2wyEV2SvGhk0= +github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= +github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= +github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= +github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= +github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= +github.com/getsentry/sentry-go v0.17.0/go.mod h1:B82dxtBvxG0KaPD8/hfSV+VcHD+Lg/xUS4JuQn1P4cM= +github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= +github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/gateway v1.1.0 h1:u0SuhL9+Il+UbjM9VIE3ntfRujKbvVpFvNB4HbjeVQ0= +github.com/gogo/gateway v1.1.0/go.mod h1:S7rR8FRQyG3QFESeSv4l2WnsyzlCLG0CzBbUUo/mbic= +github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= +github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= +github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= +github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= +github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 h1:aSVUgRRRtOrZOC1fYmY9gV0e9z/Iu+xNVSASWjsuyGU= +github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3/go.mod h1:5PC6ZNPde8bBqU/ewGZig35+UIZtw9Ytxez8/q5ZyFE= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huin/goupnp v1.0.3-0.20220313090229-ca81a64b4204/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= +github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= +github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= +github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0 h1:nHoRIX8iXob3Y2kdt9KsjyIb7iApSvb3vgsd93xb5Ow= +github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0/go.mod h1:c1tRKs5Tx7E2+uHGSyyncziFjvGpgv4H2HrqXeUQ/Uk= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/flux v0.65.1/go.mod h1:J754/zds0vvpfwuq7Gc2wRdVwEodfpCFM7mYlOw2LqY= +github.com/influxdata/influxdb v1.8.3/go.mod h1:JugdFhsvvI8gadxOI6noqNeeBHvWNTbfYGtiAn+2jhI= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxql v1.1.1-0.20200828144457-65d3ef77d385/go.mod h1:gHp9y86a/pxhjJ+zMjNXiQAA197Xk9wLxaz+fGG+kWk= +github.com/influxdata/line-protocol v0.0.0-20180522152040-32c6aa80de5e/go.mod h1:4kt73NQhadE3daL3WhR5EJ/J2ocX0PZzwxQ0gXJ7oFE= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/influxdata/promql/v2 v2.12.0/go.mod h1:fxOPu+DY0bqCTCECchSRtWfc+0X19ybifQhZoQNF5D8= +github.com/influxdata/roaring v0.4.13-0.20180809181101-fc520f41fab6/go.mod h1:bSgUQ7q5ZLSO+bKBGqJiCBGAl+9DxyW63zLTujjUlOE= +github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0= +github.com/influxdata/usage-client v0.0.0-20160829180054-6d3895376368/go.mod h1:Wbbw6tYNvwa5dlB6304Sd+82Z3f7PmVZHVKU637d4po= +github.com/ipfs/go-cid v0.0.7 h1:ysQJVJA3fNDF1qigJbsSQOdjhVLsOEoPdh0+R97k3jY= +github.com/ipfs/go-cid v0.0.7/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= +github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= +github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= +github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI= +github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= +github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= +github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e/go.mod h1:G1CVv03EnqU1wYL2dFwXxW2An0az9JTl/ZsqXQeBlkU= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K8mysFmDaM/h+o= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= +github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/karalabe/usb v0.0.2/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= +github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4CqbAk= +github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= +github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U= +github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= +github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw= +github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= +github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0= +github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= +github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= +github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= +github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= +github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-libp2p-core v0.15.1 h1:0RY+Mi/ARK9DgG1g9xVQLb8dDaaU8tCePMtGALEfBnM= +github.com/libp2p/go-libp2p-core v0.15.1/go.mod h1:agSaboYM4hzB1cWekgVReqV5M4g5M+2eNNejV+1EEhs= +github.com/libp2p/go-openssl v0.0.7/go.mod h1:unDrJpgy3oFr+rqXsarWifmJuNnJR4chtO1HmaZjggc= +github.com/linxGnu/grocksdb v1.7.10/go.mod h1:0hTf+iA+GOr0jDX4CgIYyJZxqOH9XlBh6KVj8+zmF34= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= +github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg= +github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ= +github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= +github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= +github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 h1:QRUSJEgZn2Snx0EmT/QLXibWjSUDjKWvXIT19NBVp94= +github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= +github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI= +github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= +github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4= +github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= +github.com/multiformats/go-multiaddr v0.4.1 h1:Pq37uLx3hsyNlTDir7FZyU8+cFCTqd5y1KiM2IzOutI= +github.com/multiformats/go-multiaddr v0.4.1/go.mod h1:3afI9HfVW8csiF8UZqtpYRiDyew8pRX7qLIGHu9FLuM= +github.com/multiformats/go-multibase v0.0.3 h1:l/B6bJDQjvQ5G52jw4QGSYeOTZoAwIO77RblWplfIqk= +github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc= +github.com/multiformats/go-multicodec v0.4.1 h1:BSJbf+zpghcZMZrwTYBGwy0CPcVZGWiC72Cp8bBd4R4= +github.com/multiformats/go-multicodec v0.4.1/go.mod h1:1Hj/eHRaVWSXiSNNfcEPcwZleTmdNP81xlxDLnWU9GQ= +github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= +github.com/multiformats/go-multihash v0.0.14/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= +github.com/multiformats/go-multihash v0.1.0 h1:CgAgwqk3//SVEw3T+6DqI4mWMyRuDwZtOWcJT0q9+EA= +github.com/multiformats/go-multihash v0.1.0/go.mod h1:RJlXsxt6vHGaia+S8We0ErjhojtKzPP2AH4+kYM7k84= +github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY= +github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= +github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= +github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.0.3-0.20180606204148-bd9c31933947/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/peterh/liner v1.0.1-0.20180619022028-8c1271fcf47f/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= +github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/xxHash v0.1.5 h1:n/jBpwTHiER4xYvK3/CdPVnLDPchj8eTJFFLUb4QHBo= +github.com/pierrec/xxHash v0.1.5/go.mod h1:w2waW5Zoa/Wc4Yqe0wgrIYAGKqRMf7czn2HNKXmuL+I= +github.com/pierrre/gotestcover v0.0.0-20160517101806-924dca7d15f0/go.mod h1:4xpMLz7RBWyB+ElzHu8Llua96TRCB3YwX+l5EP1wmHk= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pkg/term v0.0.0-20180730021639-bffc007b7fd5/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= +github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/regen-network/cosmos-proto v0.3.1 h1:rV7iM4SSFAagvy8RiyhiACbWEGotmqzywPxOvwMdxcg= +github.com/regen-network/cosmos-proto v0.3.1/go.mod h1:jO0sVX6a1B36nmE8C9xBFXpNwWejXC7QqCOnH3O0+YM= +github.com/regen-network/protobuf v1.3.3-alpha.regen.1 h1:OHEc+q5iIAXpqiqFKeLpu5NwTIkVXUs48vFMwzqpqY4= +github.com/regen-network/protobuf v1.3.3-alpha.regen.1/go.mod h1:2DjTFR1HhMQhiWC5sZ4OhQ3+NtdbZ6oBDKQwq5Ou+FI= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc= +github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= +github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= +github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= +github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/segmentio/kafka-go v0.1.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= +github.com/segmentio/kafka-go v0.2.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= +github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU= +github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= +github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= +github.com/strangelove-ventures/go-subkey v1.0.7 h1:cOP/Lajg3uxV/tvspu0m6+0Cu+DJgygkEAbx/s+f35I= +github.com/strangelove-ventures/go-subkey v1.0.7/go.mod h1:E34izOIEm+sZ1YmYawYRquqBQWeZBjVB4pF7bMuhc1c= +github.com/strangelove-ventures/interchaintest/v4 v4.0.0-20230316161044-8d8c01f96b4a h1:ReDdhlzY19zH7Ql6aPUsxnO7H5H/HT5PcfXtkn2OWPc= +github.com/strangelove-ventures/interchaintest/v4 v4.0.0-20230316161044-8d8c01f96b4a/go.mod h1:1iRRVEhAIGtYMl7/UVEwGx7O1tN4x8C+SzS/MBpi+JY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= +github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= +github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= +github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= +github.com/tendermint/tm-db v0.6.7 h1:fE00Cbl0jayAoqlExN6oyQJ7fR/ZtoVOmvPJ//+shu8= +github.com/tendermint/tm-db v0.6.7/go.mod h1:byQDzFkZV1syXr/ReXS808NxA2xvyuuVgXOJ/088L6I= +github.com/tidwall/btree v1.5.0 h1:iV0yVY/frd7r6qGBXfEYs7DH0gTDgrKTrDjS7xt/IyQ= +github.com/tidwall/btree v1.5.0/go.mod h1:LGm8L/DZjPLmeWGjv5kFrY8dL4uVhMmzmmLYmsObdKE= +github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= +github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zondax/hid v0.9.1/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= +github.com/zondax/ledger-go v0.14.1/go.mod h1:fZ3Dqg6qcdXWSOJFKMG8GCTnD7slO/RL2feOQv8K320= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190909091759-094676da4a83/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= +golang.org/x/exp v0.0.0-20221019170559-20944726eadf h1:nFVjjKDgNY37+ZSYCJmtYf7tOlfQswHqplG2eosjOMg= +golang.org/x/exp v0.0.0-20221019170559-20944726eadf/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210909193231-528a39cd75f3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211107104306-e0b2ad06fe42/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200108215221-bd8f9a0ef82f/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200324203455-a04cca1dde73/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa h1:qQPhfbPO23fwm/9lQr91L1u62Zo6cm+zI+slZT+uf+o= +google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.52.3 h1:pf7sOysg4LdgBqduXveGKrcEwbStiK2rtfghdzlUYDQ= +google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8 h1:KR8+MyP7/qOlV+8Af01LtjL04bu7on42eVsxT4EyBQk= +google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= +gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c= +lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= +modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= +modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= +modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= +modernc.org/libc v1.16.7 h1:qzQtHhsZNpVPpeCu+aMIQldXeV1P0vRhSqCL0nOIJOA= +modernc.org/libc v1.16.7/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU= +modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.17.3 h1:iE+coC5g17LtByDYDWKpR6m2Z9022YrSh3bumwOnIrI= +modernc.org/sqlite v1.17.3/go.mod h1:10hPVYar9C0kfXuTWGz8s0XtB8uAGymUy51ZzStYe3k= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/swap-covenant/README.md b/swap-covenant/README.md new file mode 100644 index 00000000..4f54e29d --- /dev/null +++ b/swap-covenant/README.md @@ -0,0 +1,3 @@ +# Swap covenant + +Trust minimized way of doing token swaps diff --git a/swap-covenant/justfile b/swap-covenant/justfile new file mode 100644 index 00000000..c21a7245 --- /dev/null +++ b/swap-covenant/justfile @@ -0,0 +1,35 @@ +build: + cargo build + +gen: build gen-schema + +gen-schema: + START_DIR=$(pwd); \ + for f in ./packages/*; do \ + echo "generating schema"; \ + cd "$f"; \ + CMD="cargo run --example schema"; \ + eval ${CMD} > /dev/null; \ + rm -rf ./schema/raw; \ + cd "$START_DIR"; \ + done + +test: + cargo test + +lint: + cargo +nightly clippy --all-targets -- -D warnings && cargo +nightly fmt --all --check + +optimize: + ./optimize.sh + +simtest: optimize + mkdir -p tests/interchaintest/wasms + + cp -R ./../artifacts/*.wasm tests/interchaintest/wasms + go clean -testcache + cd tests/interchaintest/ && go test -timeout 30m -v ./... + +ictest: + go clean -testcache + cd tests/interchaintest/ && go test -timeout 20m -v ./... \ No newline at end of file diff --git a/swap-covenant/optimize.sh b/swap-covenant/optimize.sh new file mode 100755 index 00000000..0d2ed785 --- /dev/null +++ b/swap-covenant/optimize.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +cd .. +if [[ $(uname -m) =~ "arm64" ]]; then \ + docker run --rm -v "$(pwd)":/code \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/workspace-optimizer-arm64:0.12.11 + +else + docker run --rm -v "$(pwd)":/code \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + --platform linux/amd64 \ + cosmwasm/workspace-optimizer:0.12.13 +fi diff --git a/swap-covenant/tests/interchaintest/README.md b/swap-covenant/tests/interchaintest/README.md new file mode 100644 index 00000000..b5805a8a --- /dev/null +++ b/swap-covenant/tests/interchaintest/README.md @@ -0,0 +1,21 @@ +# interchaintest setup + +Prior to running the interchaintests, a modification of the stride image is needed. +We are using the [v9.2.1 tagged version](https://github.com/Stride-Labs/stride/tree/v9.2.1) image. + +In there, we alter the `utils/admins.go` as follows to allow minting tokens from our address in the tests: +```go +var Admins = map[string]bool{ +- "stride1k8c2m5cn322akk5wy8lpt87dd2f4yh9azg7jlh": true, // F5 ++ "stride1u20df3trc2c2zdhm8qvh2hdjx9ewh00sv6eyy8": true, // F5 + "stride10d07y265gmmuvt4z0w9aw880jnsr700jefnezl": true, // gov module +} +``` + +Then we use heighliner by strangelove to build a local docker image, [as described in their documentation](https://github.com/strangelove-ventures/heighliner#example-cosmos-sdk-chain-development-cycle-build-a-local-repository): +```bash +# in the stride directory +heighliner build -c stride --local +``` + +With stride image present in our local docker, we are ready to run the interchaintests. To do that, we navigate to the `stride-covenant` directory and run `just simtest`. diff --git a/swap-covenant/tests/interchaintest/connection_helpers.go b/swap-covenant/tests/interchaintest/connection_helpers.go new file mode 100644 index 00000000..caca102a --- /dev/null +++ b/swap-covenant/tests/interchaintest/connection_helpers.go @@ -0,0 +1,292 @@ +package ibc_test + +import ( + "context" + "errors" + "strings" + "testing" + + "github.com/strangelove-ventures/interchaintest/v4/ibc" + "github.com/strangelove-ventures/interchaintest/v4/testreporter" + "github.com/strangelove-ventures/interchaintest/v4/testutil" + "github.com/stretchr/testify/require" +) + +func generatePath( + t *testing.T, + ctx context.Context, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + chainAId string, + chainBId string, + path string, +) { + err := r.GeneratePath(ctx, eRep, chainAId, chainBId, path) + require.NoError(t, err) +} + +func generateICSChannel( + t *testing.T, + ctx context.Context, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + icsPath string, + chainA ibc.Chain, + chainB ibc.Chain, +) { + + err := r.CreateChannel(ctx, eRep, icsPath, ibc.CreateChannelOptions{ + SourcePortName: "consumer", + DestPortName: "provider", + Order: ibc.Ordered, + Version: "1", + }) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 2, chainA, chainB) + require.NoError(t, err, "failed to wait for blocks") +} + +func createValidator( + t *testing.T, + ctx context.Context, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + chain ibc.Chain, + counterparty ibc.Chain, +) { + cmd := getCreateValidatorCmd(chain) + _, _, err := chain.Exec(ctx, cmd, nil) + require.NoError(t, err) + + // Wait a bit for the VSC packet to get relayed. + err = testutil.WaitForBlocks(ctx, 2, chain, counterparty) + require.NoError(t, err, "failed to wait for blocks") +} + +func linkPath( + t *testing.T, + ctx context.Context, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + chainA ibc.Chain, + chainB ibc.Chain, + path string, +) { + err := r.LinkPath(ctx, eRep, path, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 2, chainA, chainB) + require.NoError(t, err, "failed to wait for blocks") +} + +func generateClient( + t *testing.T, + ctx context.Context, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + path string, + chainA ibc.Chain, + chainB ibc.Chain, +) (string, string) { + chainAClients, _ := r.GetClients(ctx, eRep, chainA.Config().ChainID) + chainBClients, _ := r.GetClients(ctx, eRep, chainB.Config().ChainID) + + err := r.CreateClients(ctx, eRep, path, ibc.CreateClientOptions{TrustingPeriod: "330h"}) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 2, chainA, chainB) + require.NoError(t, err, "failed to wait for blocks") + + newChainAClients, _ := r.GetClients(ctx, eRep, chainA.Config().ChainID) + newChainBClients, _ := r.GetClients(ctx, eRep, chainB.Config().ChainID) + var newClientA, newClientB string + + aClientDiff := clientDifference(chainAClients, newChainAClients) + bClientDiff := clientDifference(chainBClients, newChainBClients) + + if len(aClientDiff) > 0 { + newClientA = aClientDiff[0] + } else { + newClientA = "" + } + + if len(bClientDiff) > 0 { + newClientB = bClientDiff[0] + } else { + newClientB = "" + } + + print("\n found client differences. new client A: ", newClientA, "b:") + return newClientA, newClientB +} + +func generateConnections( + t *testing.T, + ctx context.Context, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + path string, + chainA ibc.Chain, + chainB ibc.Chain, +) (string, string) { + chainAConns, _ := r.GetConnections(ctx, eRep, chainA.Config().ChainID) + chainBConns, _ := r.GetConnections(ctx, eRep, chainB.Config().ChainID) + + err := r.CreateConnections(ctx, eRep, path) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 2, chainA, chainB) + require.NoError(t, err, "failed to wait for blocks") + + newChainAConns, _ := r.GetConnections(ctx, eRep, chainA.Config().ChainID) + newChainBConns, _ := r.GetConnections(ctx, eRep, chainB.Config().ChainID) + + newChainAConnection := connectionDifference(chainAConns, newChainAConns) + newChainBConnection := connectionDifference(chainBConns, newChainBConns) + + require.NotEqual(t, 0, len(newChainAConnection), "more than one connection generated", strings.Join(newChainAConnection, " ")) + require.NotEqual(t, 0, len(newChainBConnection), "more than one connection generated", strings.Join(newChainBConnection, " ")) + + return newChainAConnection[0], newChainBConnection[0] +} + +func connectionDifference( + a []*ibc.ConnectionOutput, + b []*ibc.ConnectionOutput, +) (diff []string) { + + m := make(map[string]bool) + + // we first mark all existing connections + for _, item := range a { + m[item.ID] = true + } + + // and append all new ones + for _, item := range b { + if _, ok := m[item.ID]; !ok { + diff = append(diff, item.ID) + } + } + return +} + +func clientDifference(a, b []*ibc.ClientOutput) (diff []string) { + m := make(map[string]bool) + + // we first mark all existing clients + for _, item := range a { + m[item.ClientID] = true + } + + // and append all new ones + for _, item := range b { + if _, ok := m[item.ClientID]; !ok { + diff = append(diff, item.ClientID) + } + } + return +} + +func printChannels(channels []ibc.ChannelOutput, chain string) { + for _, channel := range channels { + print("\n\n", chain, " channels after create channel :", channel.ChannelID, " to ", channel.Counterparty.ChannelID, "\n") + } +} + +func printConnections(connections ibc.ConnectionOutputs) { + for _, connection := range connections { + print(connection.ID, "\n") + } +} + +func channelDifference(oldChannels, newChannels []ibc.ChannelOutput) (diff []string) { + m := make(map[string]bool) + // we first mark all existing channels + for _, channel := range newChannels { + m[channel.ChannelID] = true + } + + // then find the new ones + for _, channel := range oldChannels { + if _, ok := m[channel.ChannelID]; !ok { + diff = append(diff, channel.ChannelID) + } + } + + return +} + +func getPairwiseConnectionIds( + aconns ibc.ConnectionOutputs, + bconns ibc.ConnectionOutputs, +) ([]string, []string, error) { + abconnids := make([]string, 0) + baconnids := make([]string, 0) + found := false + for _, a := range aconns { + for _, b := range bconns { + if a.ClientID == b.Counterparty.ClientId && + b.ClientID == a.Counterparty.ClientId && + a.ID == b.Counterparty.ConnectionId && + b.ID == a.Counterparty.ConnectionId { + found = true + abconnids = append(abconnids, a.ID) + baconnids = append(baconnids, b.ID) + } + } + } + if found { + return abconnids, baconnids, nil + } else { + return abconnids, baconnids, errors.New("no connection found") + } +} + +// returns transfer channel ids +func getPairwiseTransferChannelIds( + achans []ibc.ChannelOutput, + bchans []ibc.ChannelOutput, + aToBConnId string, + bToAConnId string, +) (string, string, error) { + + for _, a := range achans { + for _, b := range bchans { + if a.ChannelID == b.Counterparty.ChannelID && + b.ChannelID == a.Counterparty.ChannelID && + a.PortID == "transfer" && + b.PortID == "transfer" && + a.Ordering == "ORDER_UNORDERED" && + b.Ordering == "ORDER_UNORDERED" && + a.ConnectionHops[0] == aToBConnId && + b.ConnectionHops[0] == bToAConnId { + + return a.ChannelID, b.ChannelID, nil + } + } + } + + return "", "", errors.New("no transfer channel found") +} + +// returns ccv channel ids +func getPairwiseCCVChannelIds( + achans []ibc.ChannelOutput, + bchans []ibc.ChannelOutput, + aToBConnId string, + bToAConnId string, +) (string, string, error) { + for _, a := range achans { + for _, b := range bchans { + if a.ChannelID == b.Counterparty.ChannelID && + b.ChannelID == a.Counterparty.ChannelID && + a.PortID == "provider" && + b.PortID == "consumer" && + a.Ordering == "ORDER_ORDERED" && + b.Ordering == "ORDER_ORDERED" && + a.ConnectionHops[0] == aToBConnId && + b.ConnectionHops[0] == bToAConnId { + return a.ChannelID, b.ChannelID, nil + } + } + } + return "", "", errors.New("no ccv channel found") +} diff --git a/swap-covenant/tests/interchaintest/genesis_helpers.go b/swap-covenant/tests/interchaintest/genesis_helpers.go new file mode 100644 index 00000000..0f92ca1c --- /dev/null +++ b/swap-covenant/tests/interchaintest/genesis_helpers.go @@ -0,0 +1,116 @@ +package ibc_test + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/icza/dyno" + "github.com/strangelove-ventures/interchaintest/v4/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v4/ibc" + "github.com/strangelove-ventures/interchaintest/v4/testreporter" +) + +// Sets custom fields for the Neutron genesis file that interchaintest isn't aware of by default. +// +// soft_opt_out_threshold - the bottom `soft_opt_out_threshold` +// percentage of validators may opt out of running a Neutron +// node [^1]. +// +// reward_denoms - the reward denominations allowed to be sent to the +// provider (atom) from the consumer (neutron) [^2]. +// +// provider_reward_denoms - the reward denominations allowed to be +// sent to the consumer by the provider [^2]. +// +// [^1]: https://docs.neutron.org/neutron/consumer-chain-launch#relevant-parameters +// [^2]: https://github.com/cosmos/interchain-security/blob/54e9852d3c89a2513cd0170a56c6eec894fc878d/proto/interchain_security/ccv/consumer/v1/consumer.proto#L61-L66 +func setupNeutronGenesis( + soft_opt_out_threshold string, + reward_denoms []string, + provider_reward_denoms []string) func(ibc.ChainConfig, []byte) ([]byte, error) { + return func(chainConfig ibc.ChainConfig, genbz []byte) ([]byte, error) { + g := make(map[string]interface{}) + if err := json.Unmarshal(genbz, &g); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis file: %w", err) + } + + if err := dyno.Set(g, soft_opt_out_threshold, "app_state", "ccvconsumer", "params", "soft_opt_out_threshold"); err != nil { + return nil, fmt.Errorf("failed to set soft_opt_out_threshold in genesis json: %w", err) + } + + if err := dyno.Set(g, reward_denoms, "app_state", "ccvconsumer", "params", "reward_denoms"); err != nil { + return nil, fmt.Errorf("failed to set reward_denoms in genesis json: %w", err) + } + + if err := dyno.Set(g, provider_reward_denoms, "app_state", "ccvconsumer", "params", "provider_reward_denoms"); err != nil { + return nil, fmt.Errorf("failed to set provider_reward_denoms in genesis json: %w", err) + } + + out, err := json.Marshal(g) + + if err != nil { + return nil, fmt.Errorf("failed to marshal genesis bytes to json: %w", err) + } + return out, nil + } +} + +// Sets custom fields for the Gaia genesis file that interchaintest isn't aware of by default. +// +// allowed_messages - explicitly allowed messages to be accepted by the the interchainaccounts section +func setupGaiaGenesis(allowed_messages []string) func(ibc.ChainConfig, []byte) ([]byte, error) { + return func(chainConfig ibc.ChainConfig, genbz []byte) ([]byte, error) { + g := make(map[string]interface{}) + if err := json.Unmarshal(genbz, &g); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis file: %w", err) + } + + if err := dyno.Set(g, allowed_messages, "app_state", "interchainaccounts", "host_genesis_state", "params", "allow_messages"); err != nil { + return nil, fmt.Errorf("failed to set allow_messages for interchainaccount host in genesis json: %w", err) + } + + out, err := json.Marshal(g) + if err != nil { + return nil, fmt.Errorf("failed to marshal genesis bytes to json: %w", err) + } + return out, nil + } +} + +func getCreateValidatorCmd(chain ibc.Chain) []string { + // Before receiving a validator set change (VSC) packet, + // consumer chains disallow bank transfers. To trigger a VSC + // packet, this creates a validator (from a random public key) + // that will never do anything, triggering a VSC + // packet. Eventually this validator will become jailed, + // triggering another one. + cmd := []string{"gaiad", "tx", "staking", "create-validator", + "--amount", "1000000uatom", + "--pubkey", `{"@type":"/cosmos.crypto.ed25519.PubKey","key":"qwrYHaJ7sNHfYBR1nzDr851+wT4ed6p8BbwTeVhaHoA="}`, + "--moniker", "a", + "--commission-rate", "0.1", + "--commission-max-rate", "0.2", + "--commission-max-change-rate", "0.01", + "--min-self-delegation", "1000000", + "--node", chain.GetRPCAddress(), + "--home", chain.HomeDir(), + "--chain-id", chain.Config().ChainID, + "--from", "faucet", + "--fees", "20000uatom", + "--keyring-backend", keyring.BackendTest, + "-y", + } + + return cmd +} + +func getChannelMap(r ibc.Relayer, ctx context.Context, eRep *testreporter.RelayerExecReporter, + cosmosStride *cosmos.CosmosChain, cosmosNeutron *cosmos.CosmosChain, cosmosAtom *cosmos.CosmosChain) map[string]string { + channelMap := map[string]string{ + "hi": "Dog", + } + + return channelMap +} diff --git a/swap-covenant/tests/interchaintest/go.mod b/swap-covenant/tests/interchaintest/go.mod new file mode 100644 index 00000000..ff327787 --- /dev/null +++ b/swap-covenant/tests/interchaintest/go.mod @@ -0,0 +1,204 @@ +module github.com/timewave-computer/covenants + +go 1.20 + +require ( + github.com/cosmos/cosmos-sdk v0.45.16 + github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0 + github.com/strangelove-ventures/interchaintest/v4 v4.0.0-20230316161044-8d8c01f96b4a + github.com/stretchr/testify v1.8.4 + go.uber.org/zap v1.24.0 +) + +require ( + cosmossdk.io/api v0.2.6 // indirect + cosmossdk.io/core v0.5.1 // indirect + cosmossdk.io/depinject v1.0.0-alpha.3 // indirect + filippo.io/edwards25519 v1.0.0-rc.1 // indirect + github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/99designs/keyring v1.2.1 // indirect + github.com/BurntSushi/toml v1.2.1 // indirect + github.com/ChainSafe/go-schnorrkel v1.0.0 // indirect + github.com/ChainSafe/go-schnorrkel/1 v0.0.0-00010101000000-000000000000 // indirect + github.com/DataDog/zstd v1.5.0 // indirect + github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect + github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/StirlingMarketingGroup/go-namecase v1.0.0 // indirect + github.com/armon/go-metrics v0.4.1 // indirect + github.com/avast/retry-go/v4 v4.0.4 // indirect + github.com/benbjohnson/clock v1.3.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect + github.com/btcsuite/btcd v0.23.0 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/centrifuge/go-substrate-rpc-client/v4 v4.0.4 // indirect + github.com/cespare/xxhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cockroachdb/errors v1.9.1 // indirect + github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/cockroachdb/pebble v0.0.0-20220817183557-09c6e030a677 // indirect + github.com/cockroachdb/redact v1.1.3 // indirect + github.com/confio/ics23/go v0.9.0 // indirect + github.com/cosmos/btcutil v1.0.4 // indirect + github.com/cosmos/cosmos-db v0.0.0-20221226095112-f3c38ecb5e32 // indirect + github.com/cosmos/cosmos-proto v1.0.0-beta.1 // indirect + github.com/cosmos/go-bip39 v1.0.0 // indirect + github.com/cosmos/gorocksdb v1.2.0 // indirect + github.com/cosmos/iavl v0.19.5 // indirect + github.com/cosmos/ibc-go/v4 v4.4.2 // indirect + github.com/cosmos/interchain-security v1.0.0 // indirect + github.com/cosmos/ledger-cosmos-go v0.12.2 // indirect + github.com/danieljoos/wincred v1.1.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deckarep/golang-set v1.8.0 // indirect + github.com/decred/base58 v1.0.3 // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/dgraph-io/badger/v2 v2.2007.4 // indirect + github.com/dgraph-io/ristretto v0.1.0 // indirect + github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect + github.com/docker/distribution v2.8.1+incompatible // indirect + github.com/docker/docker v20.10.19+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac // indirect + github.com/dvsekhvalnov/jose2go v1.5.0 // indirect + github.com/ethereum/go-ethereum v1.10.17 // indirect + github.com/felixge/httpsnoop v1.0.2 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/getsentry/sentry-go v0.17.0 // indirect + github.com/go-kit/kit v0.12.0 // indirect + github.com/go-kit/log v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.5.1 // indirect + github.com/go-stack/stack v1.8.1 // indirect + github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/gogo/gateway v1.1.0 // indirect + github.com/gogo/protobuf v1.3.3 // indirect + github.com/golang/glog v1.0.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/handlers v1.5.1 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect + github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/gtank/merlin v0.1.1 // indirect + github.com/gtank/ristretto255 v0.1.2 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/ipfs/go-cid v0.0.7 // indirect + github.com/jmhodges/levigo v1.0.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/klauspost/compress v1.15.11 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/libp2p/go-libp2p-core v0.15.1 // indirect + github.com/libp2p/go-openssl v0.0.7 // indirect + github.com/linxGnu/grocksdb v1.7.10 // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect + github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect + github.com/minio/sha256-simd v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/mtibben/percent v0.2.1 // indirect + github.com/multiformats/go-base32 v0.0.3 // indirect + github.com/multiformats/go-base36 v0.1.0 // indirect + github.com/multiformats/go-multiaddr v0.4.1 // indirect + github.com/multiformats/go-multibase v0.0.3 // indirect + github.com/multiformats/go-multicodec v0.4.1 // indirect + github.com/multiformats/go-multihash v0.1.0 // indirect + github.com/multiformats/go-varint v0.0.6 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc2 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect + github.com/pierrec/xxHash v0.1.5 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.37.0 // indirect + github.com/prometheus/procfs v0.8.0 // indirect + github.com/rakyll/statik v0.1.7 // indirect + github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/regen-network/cosmos-proto v0.3.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/rs/cors v1.8.2 // indirect + github.com/sasha-s/go-deadlock v0.3.1 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/spf13/afero v1.9.2 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cobra v1.6.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.14.0 // indirect + github.com/subosito/gotenv v1.4.1 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect + github.com/tendermint/go-amino v0.16.0 // indirect + github.com/tendermint/tendermint v0.34.27 // indirect + github.com/tendermint/tm-db v0.6.7 // indirect + github.com/tidwall/btree v1.5.0 // indirect + github.com/vedhavyas/go-subkey v1.0.3 // indirect + github.com/zondax/hid v0.9.1 // indirect + github.com/zondax/ledger-go v0.14.1 // indirect + go.etcd.io/bbolt v1.3.6 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/multierr v1.8.0 // indirect + golang.org/x/crypto v0.5.0 // indirect + golang.org/x/exp v0.0.0-20221019170559-20944726eadf // indirect + golang.org/x/mod v0.7.0 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/term v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + golang.org/x/tools v0.4.0 // indirect + google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa // indirect + google.golang.org/grpc v1.52.3 // indirect + google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/blake3 v1.1.6 // indirect + lukechampine.com/uint128 v1.1.1 // indirect + modernc.org/cc/v3 v3.36.0 // indirect + modernc.org/ccgo/v3 v3.16.6 // indirect + modernc.org/libc v1.16.7 // indirect + modernc.org/mathutil v1.4.1 // indirect + modernc.org/memory v1.1.1 // indirect + modernc.org/opt v0.1.1 // indirect + modernc.org/sqlite v1.17.3 // indirect + modernc.org/strutil v1.1.1 // indirect + modernc.org/token v1.0.0 // indirect +) + +// replace block copied from interchaintest v3-ics branch: +// +replace ( + github.com/ChainSafe/go-schnorrkel => github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d + github.com/ChainSafe/go-schnorrkel/1 => github.com/ChainSafe/go-schnorrkel v1.0.0 + github.com/btcsuite/btcd => github.com/btcsuite/btcd v0.22.2 //indirect + github.com/cosmos/cosmos-sdk => github.com/cosmos/cosmos-sdk v0.45.16-ics + github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 + github.com/tendermint/tendermint => github.com/cometbft/cometbft v0.34.27 + // github.com/tidwall/btree => github.com/tidwall/btree v1.5.0 + github.com/vedhavyas/go-subkey => github.com/strangelove-ventures/go-subkey v1.0.7 +) diff --git a/swap-covenant/tests/interchaintest/go.sum b/swap-covenant/tests/interchaintest/go.sum new file mode 100644 index 00000000..932fcd61 --- /dev/null +++ b/swap-covenant/tests/interchaintest/go.sum @@ -0,0 +1,1490 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigtable v1.2.0/go.mod h1:JcVAOl45lrTmQfLj7T6TxyMzIN/3FGGcFm+2xVAli2o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE= +cosmossdk.io/api v0.2.6 h1:AoNwaLLapcLsphhMK6+o0kZl+D6MMUaHVqSdwinASGU= +cosmossdk.io/api v0.2.6/go.mod h1:u/d+GAxil0nWpl1XnQL8nkziQDIWuBDhv8VnDm/s6dI= +cosmossdk.io/core v0.5.1 h1:vQVtFrIYOQJDV3f7rw4pjjVqc1id4+mE0L9hHP66pyI= +cosmossdk.io/core v0.5.1/go.mod h1:KZtwHCLjcFuo0nmDc24Xy6CRNEL9Vl/MeimQ2aC7NLE= +cosmossdk.io/depinject v1.0.0-alpha.3 h1:6evFIgj//Y3w09bqOUOzEpFj5tsxBqdc5CfkO7z+zfw= +cosmossdk.io/depinject v1.0.0-alpha.3/go.mod h1:eRbcdQ7MRpIPEM5YUJh8k97nxHpYbc3sMUnEtt8HPWU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= +filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= +github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= +github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0/go.mod h1:tPaiy8S5bQ+S5sOiDlINkp7+Ef339+Nz5L5XO+cnOHo= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d h1:nalkkPQcITbvhmL4+C4cKA87NW0tfm3Kl9VXRoPywFg= +github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d/go.mod h1:URdX5+vg25ts3aCh8H5IFZybJYKWhJHYMTnf+ULtoC4= +github.com/ChainSafe/go-schnorrkel v1.0.0 h1:3aDA67lAykLaG1y3AOjs88dMxC88PgUuHRrLeDnvGIM= +github.com/ChainSafe/go-schnorrkel v1.0.0/go.mod h1:dpzHYVxLZcp8pjlV+O+UR8K0Hp/z7vcchBSbMBEhCw4= +github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= +github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w= +github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/DataDog/zstd v1.5.0 h1:+K/VEwIAaPcHiMtQvpLD4lqW7f0Gk3xdYZmI1hD+CXo= +github.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= +github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/StirlingMarketingGroup/go-namecase v1.0.0 h1:2CzaNtCzc4iNHirR+5ru9OzGg8rQp860gqLBFqRI02Y= +github.com/StirlingMarketingGroup/go-namecase v1.0.0/go.mod h1:ZsoSKcafcAzuBx+sndbxHu/RjDcDTrEdT4UvhniHfio= +github.com/VictoriaMetrics/fastcache v1.6.0/go.mod h1:0qHz5QP0GMX4pfmMA/zt5RgfNuXJrTP0zS7DqpHGGTw= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/alecthomas/participle/v2 v2.0.0-alpha7 h1:cK4vjj0VSgb3lN1nuKA5F7dw+1s1pWBe5bx7nNCnN+c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/arrow/go/arrow v0.0.0-20191024131854-af6fa24be0db/go.mod h1:VTxUBvSJ3s3eHAg65PNgrsn5BtqCRPdmyXh6rAfdxN0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/avast/retry-go/v4 v4.0.4 h1:38hLf0DsRXh+hOF6HbTni0+5QGTNdw9zbaMD7KAO830= +github.com/avast/retry-go/v4 v4.0.4/go.mod h1:HqmLvS2VLdStPCGDFjSuZ9pzlTqVRldCI4w2dO4m1Ms= +github.com/aws/aws-sdk-go-v2 v1.2.0/go.mod h1:zEQs02YRBw1DjK0PoJv3ygDYOFTre1ejlJWl8FwAuQo= +github.com/aws/aws-sdk-go-v2/config v1.1.1/go.mod h1:0XsVy9lBI/BCXm+2Tuvt39YmdHwS5unDQmxZOYe8F5Y= +github.com/aws/aws-sdk-go-v2/credentials v1.1.1/go.mod h1:mM2iIjwl7LULWtS6JCACyInboHirisUUdkBPoTHMOUo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.2/go.mod h1:3hGg3PpiEjHnrkrlasTfxFqUsZ2GCk/fMUn4CbKgSkM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.2/go.mod h1:45MfaXZ0cNbeuT0KQ1XJylq8A6+OpVV2E5kvY/Kq+u8= +github.com/aws/aws-sdk-go-v2/service/route53 v1.1.1/go.mod h1:rLiOUrPLW/Er5kRcQ7NkwbjlijluLsrIbu/iyl35RO4= +github.com/aws/aws-sdk-go-v2/service/sso v1.1.1/go.mod h1:SuZJxklHxLAXgLTc1iFXbEWkXs7QRTQpCLGaKIprQW0= +github.com/aws/aws-sdk-go-v2/service/sts v1.1.1/go.mod h1:Wi0EBZwiz/K44YliU0EKxqTCJGUfYTWXrrBwkq736bM= +github.com/aws/smithy-go v1.1.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw= +github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/btcsuite/btcd v0.22.2 h1:vBZ+lGGd1XubpOWO67ITJpAEsICWhA0YzqkcpkgNBfo= +github.com/btcsuite/btcd v0.22.2/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y= +github.com/btcsuite/btcd/btcec/v2 v2.1.2/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcutil v1.1.2 h1:XLMbX8JQEiwMcYft2EGi8zPUkoa0abKIU6/BJSRsjzQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/centrifuge/go-substrate-rpc-client/v4 v4.0.4 h1:G2kCJurlIkguX0oxxI9sPPENuQqMVhIhV9RVkh/dpDg= +github.com/centrifuge/go-substrate-rpc-client/v4 v4.0.4/go.mod h1:5g1oM4Zu3BOaLpsKQ+O8PAv2kNuq+kPcA1VzFbsSqxE= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cloudflare-go v0.14.0/go.mod h1:EnwdgGMaFOruiPZRFSgn+TsQ3hQ7C/YWzIGLeu5c304= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cockroachdb/apd/v3 v3.1.0 h1:MK3Ow7LH0W8zkd5GMKA1PvS9qG3bWFI95WaVNfyZJ/w= +github.com/cockroachdb/datadriven v1.0.0/go.mod h1:5Ib8Meh+jk1RlHIXej6Pzevx/NLlNvQB9pmSBZErGA4= +github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/cockroachdb/errors v1.6.1/go.mod h1:tm6FTP5G81vwJ5lC0SizQo374JNCOPrHyXGitRJoDqM= +github.com/cockroachdb/errors v1.8.1/go.mod h1:qGwQn6JmZ+oMjuLwjWzUNqblqk0xl4CVV3SQbGwK7Ac= +github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8= +github.com/cockroachdb/errors v1.9.1/go.mod h1:2sxOtL2WIc096WSZqZ5h8fa17rdDq9HZOZLBCor4mBk= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v0.0.0-20220817183557-09c6e030a677 h1:qbb/AE938DFhOajUYh9+OXELpSF9KZw2ZivtmW6eX1Q= +github.com/cockroachdb/pebble v0.0.0-20220817183557-09c6e030a677/go.mod h1:890yq1fUb9b6dGNwssgeUO5vQV9qfXnCPxAJhBQfXw0= +github.com/cockroachdb/redact v1.0.8/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ= +github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2/go.mod h1:8BT+cPK6xvFOcRlk0R8eg+OTkcqI6baNH4xAkpiYVvQ= +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= +github.com/coinbase/rosetta-sdk-go v0.7.9 h1:lqllBjMnazTjIqYrOGv8h8jxjg9+hJazIGZr9ZvoCcA= +github.com/cometbft/cometbft v0.34.27 h1:ri6BvmwjWR0gurYjywcBqRe4bbwc3QVs9KRcCzgh/J0= +github.com/cometbft/cometbft v0.34.27/go.mod h1:BcCbhKv7ieM0KEddnYXvQZR+pZykTKReJJYf7YC7qhw= +github.com/cometbft/cometbft-db v0.7.0 h1:uBjbrBx4QzU0zOEnU8KxoDl18dMNgDh+zZRUE0ucsbo= +github.com/confio/ics23/go v0.9.0 h1:cWs+wdbS2KRPZezoaaj+qBleXgUk5WOQFMP3CQFGTr4= +github.com/confio/ics23/go v0.9.0/go.mod h1:4LPZ2NYqnYIVRklaozjNR1FScgDJ2s5Xrp+e/mYVRak= +github.com/consensys/bavard v0.1.8-0.20210406032232-f3452dc9b572/go.mod h1:Bpd0/3mZuaj6Sj+PqrmIquiOKy397AKGThQPaGzNXAQ= +github.com/consensys/gnark-crypto v0.4.1-0.20210426202927-39ac3d4b3f1f/go.mod h1:815PAHg3wvysy0SyIqanF8gZ0Y1wjk/hrDHD/iT88+Q= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cosmos/btcutil v1.0.4 h1:n7C2ngKXo7UC9gNyMNLbzqz7Asuf+7Qv4gnX/rOdQ44= +github.com/cosmos/btcutil v1.0.4/go.mod h1:Ffqc8Hn6TJUdDgHBwIZLtrLQC1KdJ9jGJl/TvgUaxbU= +github.com/cosmos/cosmos-db v0.0.0-20221226095112-f3c38ecb5e32 h1:zlCp9n3uwQieELltZWHRmwPmPaZ8+XoL2Sj+A2YJlr8= +github.com/cosmos/cosmos-db v0.0.0-20221226095112-f3c38ecb5e32/go.mod h1:kwMlEC4wWvB48zAShGKVqboJL6w4zCLesaNQ3YLU2BQ= +github.com/cosmos/cosmos-proto v1.0.0-beta.1 h1:iDL5qh++NoXxG8hSy93FdYJut4XfgbShIocllGaXx/0= +github.com/cosmos/cosmos-proto v1.0.0-beta.1/go.mod h1:8k2GNZghi5sDRFw/scPL8gMSowT1vDA+5ouxL8GjaUE= +github.com/cosmos/cosmos-sdk v0.45.16-ics h1:KsPigLNmdyyQMktAsJzW42eBFsq1uajhQF7rlnHDUgM= +github.com/cosmos/cosmos-sdk v0.45.16-ics/go.mod h1:bScuNwWAP0TZJpUf+SHXRU3xGoUPp+X9nAzfeIXts40= +github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= +github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= +github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= +github.com/cosmos/gorocksdb v1.2.0 h1:d0l3jJG8M4hBouIZq0mDUHZ+zjOx044J3nGRskwTb4Y= +github.com/cosmos/gorocksdb v1.2.0/go.mod h1:aaKvKItm514hKfNJpUJXnnOWeBnk2GL4+Qw9NHizILw= +github.com/cosmos/iavl v0.19.5 h1:rGA3hOrgNxgRM5wYcSCxgQBap7fW82WZgY78V9po/iY= +github.com/cosmos/iavl v0.19.5/go.mod h1:X9PKD3J0iFxdmgNLa7b2LYWdsGd90ToV5cAONApkEPw= +github.com/cosmos/ibc-go/v4 v4.4.2 h1:PG4Yy0/bw6Hvmha3RZbc53KYzaCwuB07Ot4GLyzcBvo= +github.com/cosmos/ibc-go/v4 v4.4.2/go.mod h1:j/kD2JCIaV5ozvJvaEkWhLxM2zva7/KTM++EtKFYcB8= +github.com/cosmos/interchain-security v1.0.0 h1:xNQjjigqH3mzEKSGQhAhKy8I0TA8XR2z5rRTxRBKK3o= +github.com/cosmos/interchain-security v1.0.0/go.mod h1:J9SbXUJT1GSe+mZy+MDCxtuAfbhwCKBEJRYnfjXsE8Q= +github.com/cosmos/ledger-cosmos-go v0.12.2 h1:/XYaBlE2BJxtvpkHiBm97gFGSGmYGKunKyF3nNqAXZA= +github.com/cosmos/ledger-cosmos-go v0.12.2/go.mod h1:ZcqYgnfNJ6lAXe4HPtWgarNEY+B74i+2/8MhZw4ziiI= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creachadair/taskgroup v0.3.2 h1:zlfutDS+5XG40AOxcHDSThxKzns8Tnr9jnr6VqkYlkM= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cucumber/common/gherkin/go/v22 v22.0.0 h1:4K8NqptbvdOrjL9DEea6HFjSpbdT9+Q5kgLpmmsHYl0= +github.com/cucumber/common/messages/go/v17 v17.1.1 h1:RNqopvIFyLWnKv0LfATh34SWBhXeoFTJnSrgm9cT/Ts= +github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= +github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= +github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= +github.com/decred/base58 v1.0.3 h1:KGZuh8d1WEMIrK0leQRM47W85KqCAdl2N+uagbctdDI= +github.com/decred/base58 v1.0.3/go.mod h1:pXP9cXCfM2sFLb2viz2FNIdeMWmZDBKG3ZBYbiSM78E= +github.com/decred/dcrd/chaincfg/chainhash v1.0.2 h1:rt5Vlq/jM3ZawwiacWjPa+smINyLRN07EO0cNBV6DGU= +github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0 h1:3GIJYXQDAKpLEFriGFN8SbSffak10UXHGdIcFaMPykY= +github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0/go.mod h1:3s92l0paYkZoIHuj4X93Teg/HB7eGM9x/zokGw+u4mY= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= +github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= +github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= +github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= +github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-bitstream v0.0.0-20180413035011-3522498ce2c8/go.mod h1:VMaSuZ+SZcx/wljOQKvp5srsbCiKDEb6K2wC4+PiBmQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.19+incompatible h1:lzEmjivyNHFHMNAFLXORMBXyGIhw/UP4DvJwvyKYq64= +github.com/docker/docker v20.10.19+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dop251/goja v0.0.0-20211011172007-d99e4b8cbf48/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac h1:opbrjaN/L8gg6Xh5D04Tem+8xVcz6ajZlGCs49mQgyg= +github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= +github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/ethereum/go-ethereum v1.10.17 h1:XEcumY+qSr1cZQaWsQs5Kck3FHB0V2RiMHPdTBJ+oT8= +github.com/ethereum/go-ethereum v1.10.17/go.mod h1:Lt5WzjM07XlXc95YzrhosmR4J9Ahd6X2wyEV2SvGhk0= +github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= +github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= +github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= +github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= +github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= +github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= +github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= +github.com/getsentry/sentry-go v0.17.0 h1:UustVWnOoDFHBS7IJUB2QK/nB5pap748ZEp0swnQJak= +github.com/getsentry/sentry-go v0.17.0/go.mod h1:B82dxtBvxG0KaPD8/hfSV+VcHD+Lg/xUS4JuQn1P4cM= +github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= +github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc= +github.com/gogo/gateway v1.1.0 h1:u0SuhL9+Il+UbjM9VIE3ntfRujKbvVpFvNB4HbjeVQ0= +github.com/gogo/gateway v1.1.0/go.mod h1:S7rR8FRQyG3QFESeSv4l2WnsyzlCLG0CzBbUUo/mbic= +github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= +github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa h1:Q75Upo5UN4JbPFURXZ8nLKYUvF85dyFRop/vQ0Rv+64= +github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= +github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= +github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= +github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 h1:aSVUgRRRtOrZOC1fYmY9gV0e9z/Iu+xNVSASWjsuyGU= +github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3/go.mod h1:5PC6ZNPde8bBqU/ewGZig35+UIZtw9Ytxez8/q5ZyFE= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huin/goupnp v1.0.3-0.20220313090229-ca81a64b4204/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= +github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= +github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= +github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0 h1:nHoRIX8iXob3Y2kdt9KsjyIb7iApSvb3vgsd93xb5Ow= +github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0/go.mod h1:c1tRKs5Tx7E2+uHGSyyncziFjvGpgv4H2HrqXeUQ/Uk= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/flux v0.65.1/go.mod h1:J754/zds0vvpfwuq7Gc2wRdVwEodfpCFM7mYlOw2LqY= +github.com/influxdata/influxdb v1.8.3/go.mod h1:JugdFhsvvI8gadxOI6noqNeeBHvWNTbfYGtiAn+2jhI= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxql v1.1.1-0.20200828144457-65d3ef77d385/go.mod h1:gHp9y86a/pxhjJ+zMjNXiQAA197Xk9wLxaz+fGG+kWk= +github.com/influxdata/line-protocol v0.0.0-20180522152040-32c6aa80de5e/go.mod h1:4kt73NQhadE3daL3WhR5EJ/J2ocX0PZzwxQ0gXJ7oFE= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/influxdata/promql/v2 v2.12.0/go.mod h1:fxOPu+DY0bqCTCECchSRtWfc+0X19ybifQhZoQNF5D8= +github.com/influxdata/roaring v0.4.13-0.20180809181101-fc520f41fab6/go.mod h1:bSgUQ7q5ZLSO+bKBGqJiCBGAl+9DxyW63zLTujjUlOE= +github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0= +github.com/influxdata/usage-client v0.0.0-20160829180054-6d3895376368/go.mod h1:Wbbw6tYNvwa5dlB6304Sd+82Z3f7PmVZHVKU637d4po= +github.com/ipfs/go-cid v0.0.7 h1:ysQJVJA3fNDF1qigJbsSQOdjhVLsOEoPdh0+R97k3jY= +github.com/ipfs/go-cid v0.0.7/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= +github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= +github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= +github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI= +github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= +github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= +github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e/go.mod h1:G1CVv03EnqU1wYL2dFwXxW2An0az9JTl/ZsqXQeBlkU= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= +github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K8mysFmDaM/h+o= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= +github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/karalabe/usb v0.0.2/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= +github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4CqbAk= +github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= +github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U= +github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= +github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw= +github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= +github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0= +github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= +github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= +github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= +github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= +github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-libp2p-core v0.15.1 h1:0RY+Mi/ARK9DgG1g9xVQLb8dDaaU8tCePMtGALEfBnM= +github.com/libp2p/go-libp2p-core v0.15.1/go.mod h1:agSaboYM4hzB1cWekgVReqV5M4g5M+2eNNejV+1EEhs= +github.com/libp2p/go-openssl v0.0.7 h1:eCAzdLejcNVBzP/iZM9vqHnQm+XyCEbSSIheIPRGNsw= +github.com/libp2p/go-openssl v0.0.7/go.mod h1:unDrJpgy3oFr+rqXsarWifmJuNnJR4chtO1HmaZjggc= +github.com/linxGnu/grocksdb v1.7.10 h1:dz7RY7GnFUA+GJO6jodyxgkUeGMEkPp3ikt9hAcNGEw= +github.com/linxGnu/grocksdb v1.7.10/go.mod h1:0hTf+iA+GOr0jDX4CgIYyJZxqOH9XlBh6KVj8+zmF34= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= +github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= +github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg= +github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ= +github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= +github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= +github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 h1:QRUSJEgZn2Snx0EmT/QLXibWjSUDjKWvXIT19NBVp94= +github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= +github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI= +github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= +github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4= +github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= +github.com/multiformats/go-multiaddr v0.4.1 h1:Pq37uLx3hsyNlTDir7FZyU8+cFCTqd5y1KiM2IzOutI= +github.com/multiformats/go-multiaddr v0.4.1/go.mod h1:3afI9HfVW8csiF8UZqtpYRiDyew8pRX7qLIGHu9FLuM= +github.com/multiformats/go-multibase v0.0.3 h1:l/B6bJDQjvQ5G52jw4QGSYeOTZoAwIO77RblWplfIqk= +github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc= +github.com/multiformats/go-multicodec v0.4.1 h1:BSJbf+zpghcZMZrwTYBGwy0CPcVZGWiC72Cp8bBd4R4= +github.com/multiformats/go-multicodec v0.4.1/go.mod h1:1Hj/eHRaVWSXiSNNfcEPcwZleTmdNP81xlxDLnWU9GQ= +github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= +github.com/multiformats/go-multihash v0.0.14/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= +github.com/multiformats/go-multihash v0.1.0 h1:CgAgwqk3//SVEw3T+6DqI4mWMyRuDwZtOWcJT0q9+EA= +github.com/multiformats/go-multihash v0.1.0/go.mod h1:RJlXsxt6vHGaia+S8We0ErjhojtKzPP2AH4+kYM7k84= +github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY= +github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= +github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= +github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.0.3-0.20180606204148-bd9c31933947/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/otiai10/copy v1.6.0 h1:IinKAryFFuPONZ7cm6T6E2QX/vcJwSnlaA5lfoaXIiQ= +github.com/oxyno-zeta/gomock-extra-matcher v1.1.0 h1:Yyk5ov0ZPKBXtVEeIWtc4J2XVrHuNoIK+0F2BUJgtsc= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/peterh/liner v1.0.1-0.20180619022028-8c1271fcf47f/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= +github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/xxHash v0.1.5 h1:n/jBpwTHiER4xYvK3/CdPVnLDPchj8eTJFFLUb4QHBo= +github.com/pierrec/xxHash v0.1.5/go.mod h1:w2waW5Zoa/Wc4Yqe0wgrIYAGKqRMf7czn2HNKXmuL+I= +github.com/pierrre/gotestcover v0.0.0-20160517101806-924dca7d15f0/go.mod h1:4xpMLz7RBWyB+ElzHu8Llua96TRCB3YwX+l5EP1wmHk= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pkg/term v0.0.0-20180730021639-bffc007b7fd5/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= +github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/regen-network/cosmos-proto v0.3.1 h1:rV7iM4SSFAagvy8RiyhiACbWEGotmqzywPxOvwMdxcg= +github.com/regen-network/cosmos-proto v0.3.1/go.mod h1:jO0sVX6a1B36nmE8C9xBFXpNwWejXC7QqCOnH3O0+YM= +github.com/regen-network/gocuke v0.6.2 h1:pHviZ0kKAq2U2hN2q3smKNxct6hS0mGByFMHGnWA97M= +github.com/regen-network/protobuf v1.3.3-alpha.regen.1 h1:OHEc+q5iIAXpqiqFKeLpu5NwTIkVXUs48vFMwzqpqY4= +github.com/regen-network/protobuf v1.3.3-alpha.regen.1/go.mod h1:2DjTFR1HhMQhiWC5sZ4OhQ3+NtdbZ6oBDKQwq5Ou+FI= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc= +github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= +github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= +github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= +github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= +github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/segmentio/kafka-go v0.1.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= +github.com/segmentio/kafka-go v0.2.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= +github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= +github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU= +github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= +github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= +github.com/strangelove-ventures/go-subkey v1.0.7 h1:cOP/Lajg3uxV/tvspu0m6+0Cu+DJgygkEAbx/s+f35I= +github.com/strangelove-ventures/go-subkey v1.0.7/go.mod h1:E34izOIEm+sZ1YmYawYRquqBQWeZBjVB4pF7bMuhc1c= +github.com/strangelove-ventures/interchaintest/v4 v4.0.0-20230316161044-8d8c01f96b4a h1:ReDdhlzY19zH7Ql6aPUsxnO7H5H/HT5PcfXtkn2OWPc= +github.com/strangelove-ventures/interchaintest/v4 v4.0.0-20230316161044-8d8c01f96b4a/go.mod h1:1iRRVEhAIGtYMl7/UVEwGx7O1tN4x8C+SzS/MBpi+JY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= +github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= +github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok= +github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= +github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= +github.com/tendermint/tm-db v0.6.7 h1:fE00Cbl0jayAoqlExN6oyQJ7fR/ZtoVOmvPJ//+shu8= +github.com/tendermint/tm-db v0.6.7/go.mod h1:byQDzFkZV1syXr/ReXS808NxA2xvyuuVgXOJ/088L6I= +github.com/tidwall/btree v1.5.0 h1:iV0yVY/frd7r6qGBXfEYs7DH0gTDgrKTrDjS7xt/IyQ= +github.com/tidwall/btree v1.5.0/go.mod h1:LGm8L/DZjPLmeWGjv5kFrY8dL4uVhMmzmmLYmsObdKE= +github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4= +github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= +github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA= +github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zondax/hid v0.9.1 h1:gQe66rtmyZ8VeGFcOpbuH3r7erYtNEAezCAYu8LdkJo= +github.com/zondax/hid v0.9.1/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= +github.com/zondax/ledger-go v0.14.1 h1:Pip65OOl4iJ84WTpA4BKChvOufMhhbxED3BaihoZN4c= +github.com/zondax/ledger-go v0.14.1/go.mod h1:fZ3Dqg6qcdXWSOJFKMG8GCTnD7slO/RL2feOQv8K320= +go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190909091759-094676da4a83/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= +golang.org/x/exp v0.0.0-20221019170559-20944726eadf h1:nFVjjKDgNY37+ZSYCJmtYf7tOlfQswHqplG2eosjOMg= +golang.org/x/exp v0.0.0-20221019170559-20944726eadf/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210909193231-528a39cd75f3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211107104306-e0b2ad06fe42/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= +gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200108215221-bd8f9a0ef82f/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200324203455-a04cca1dde73/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa h1:qQPhfbPO23fwm/9lQr91L1u62Zo6cm+zI+slZT+uf+o= +google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.52.3 h1:pf7sOysg4LdgBqduXveGKrcEwbStiK2rtfghdzlUYDQ= +google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8 h1:KR8+MyP7/qOlV+8Af01LtjL04bu7on42eVsxT4EyBQk= +google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= +gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c= +lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.36.0 h1:0kmRkTmqNidmu3c7BNDSdVHCxXCkWLmWmCIVX4LUboo= +modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= +modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= +modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.6 h1:3l18poV+iUemQ98O3X5OMr97LOqlzis+ytivU4NqGhA= +modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= +modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= +modernc.org/libc v1.16.7 h1:qzQtHhsZNpVPpeCu+aMIQldXeV1P0vRhSqCL0nOIJOA= +modernc.org/libc v1.16.7/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU= +modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.17.3 h1:iE+coC5g17LtByDYDWKpR6m2Z9022YrSh3bumwOnIrI= +modernc.org/sqlite v1.17.3/go.mod h1:10hPVYar9C0kfXuTWGz8s0XtB8uAGymUy51ZzStYe3k= +modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao= +modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= +modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM= +modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +pgregory.net/rapid v0.5.3 h1:163N50IHFqr1phZens4FQOdPgfJscR7a562mjQqeo4M= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/swap-covenant/tests/interchaintest/tokenswap_test.go b/swap-covenant/tests/interchaintest/tokenswap_test.go new file mode 100644 index 00000000..989b949d --- /dev/null +++ b/swap-covenant/tests/interchaintest/tokenswap_test.go @@ -0,0 +1,282 @@ +package ibc_test + +import ( + "context" + "fmt" + "testing" + "time" + + ibctest "github.com/strangelove-ventures/interchaintest/v4" + "github.com/strangelove-ventures/interchaintest/v4/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v4/ibc" + "github.com/strangelove-ventures/interchaintest/v4/relayer" + "github.com/strangelove-ventures/interchaintest/v4/relayer/rly" + "github.com/strangelove-ventures/interchaintest/v4/testreporter" + "github.com/strangelove-ventures/interchaintest/v4/testutil" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zaptest" +) + +// sets up and tests a tokenswap between hub and stargaze facilitated by neutron +func TestTokenSwap(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + + ctx := context.Background() + + // Modify the the timeout_commit in the config.toml node files + // to reduce the block commit times. This speeds up the tests + // by about 35% + configFileOverrides := make(map[string]any) + configTomlOverrides := make(testutil.Toml) + consensus := make(testutil.Toml) + consensus["timeout_commit"] = "1s" + configTomlOverrides["consensus"] = consensus + configFileOverrides["config/config.toml"] = configTomlOverrides + + // Chain Factory + cf := ibctest.NewBuiltinChainFactory(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel)), []*ibctest.ChainSpec{ + {Name: "gaia", Version: "v9.1.0", ChainConfig: ibc.ChainConfig{ + GasAdjustment: 1.5, + GasPrices: "0.0atom", + ModifyGenesis: setupGaiaGenesis([]string{ + "/cosmos.bank.v1beta1.MsgSend", + "/cosmos.bank.v1beta1.MsgMultiSend", + "/cosmos.staking.v1beta1.MsgDelegate", + "/cosmos.staking.v1beta1.MsgUndelegate", + "/cosmos.staking.v1beta1.MsgBeginRedelegate", + "/cosmos.staking.v1beta1.MsgRedeemTokensforShares", + "/cosmos.staking.v1beta1.MsgTokenizeShares", + "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", + "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", + "/ibc.applications.transfer.v1.MsgTransfer", + }), + ConfigFileOverrides: configFileOverrides, + }}, + { + ChainConfig: ibc.ChainConfig{ + Type: "cosmos", + Name: "neutron", + ChainID: "neutron-2", + Images: []ibc.DockerImage{ + { + Repository: "ghcr.io/strangelove-ventures/heighliner/neutron", + Version: "v1.0.2", + UidGid: "1025:1025", + }, + }, + Bin: "neutrond", + Bech32Prefix: "neutron", + Denom: "untrn", + GasPrices: "0.0untrn,0.0uatom", + GasAdjustment: 1.3, + TrustingPeriod: "1197504s", + NoHostMount: false, + ModifyGenesis: setupNeutronGenesis("0.05", []string{"untrn"}, []string{"uatom"}), + ConfigFileOverrides: configFileOverrides, + }, + }, + + {Name: "osmosis", Version: "v11.0.0"}, + }) + + chains, err := cf.Chains(t.Name()) + require.NoError(t, err) + + // We have three chains + atom, neutron, osmosis := chains[0], chains[1], chains[2] + cosmosAtom, cosmosNeutron, cosmosOsmosis := atom.(*cosmos.CosmosChain), neutron.(*cosmos.CosmosChain), osmosis.(*cosmos.CosmosChain) + + // Relayer Factory + client, network := ibctest.DockerSetup(t) + r := ibctest.NewBuiltinRelayerFactory( + ibc.CosmosRly, + zaptest.NewLogger(t), + relayer.CustomDockerImage("ghcr.io/cosmos/relayer", "v2.3.1", rly.RlyDefaultUidGid), + relayer.RelayerOptionExtraStartFlags{Flags: []string{"-d", "--log-format", "console"}}, + ).Build(t, client, network) + + // Prep Interchain + const gaiaNeutronICSPath = "gn-ics-path" + const gaiaNeutronIBCPath = "gn-ibc-path" + const gaiaOsmosisIBCPath = "go-ibc-path" + const neutronOsmosisIBCPath = "no-ibc-path" + + ic := ibctest.NewInterchain(). + AddChain(cosmosAtom). + AddChain(cosmosNeutron). + AddChain(cosmosOsmosis). + AddRelayer(r, "relayer"). + AddProviderConsumerLink(ibctest.ProviderConsumerLink{ + Provider: cosmosAtom, + Consumer: cosmosNeutron, + Relayer: r, + Path: gaiaNeutronICSPath, + }). + AddLink(ibctest.InterchainLink{ + Chain1: cosmosAtom, + Chain2: cosmosNeutron, + Relayer: r, + Path: gaiaNeutronIBCPath, + }). + AddLink(ibctest.InterchainLink{ + Chain1: cosmosNeutron, + Chain2: cosmosOsmosis, + Relayer: r, + Path: neutronOsmosisIBCPath, + }) + + // Log location + f, err := ibctest.CreateLogFile(fmt.Sprintf("%d.json", time.Now().Unix())) + require.NoError(t, err) + // Reporter/logs + rep := testreporter.NewReporter(f) + eRep := rep.RelayerExecReporter(t) + + // Build interchain + err = ic.Build(ctx, eRep, ibctest.InterchainBuildOptions{ + TestName: t.Name(), + Client: client, + NetworkID: network, + BlockDatabaseFile: ibctest.DefaultBlockDatabaseFilepath(), + SkipPathCreation: true, + }) + require.NoError(t, err, "failed to build interchain") + + err = testutil.WaitForBlocks(ctx, 10, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + + users := ibctest.GetAndFundTestUsers(t, ctx, "default", int64(500_000_000_000), osmosis) + osmoUser := users[0] + osmoUserBalInitial, err := osmosis.GetBalance(ctx, osmoUser.Bech32Address(osmosis.Config().Bech32Prefix), osmosis.Config().Denom) + require.NoError(t, err) + require.Equal(t, int64(500_000_000_000), osmoUserBalInitial) + + // generate paths + generatePath(t, ctx, r, eRep, cosmosAtom.Config().ChainID, cosmosNeutron.Config().ChainID, gaiaNeutronIBCPath) + generatePath(t, ctx, r, eRep, cosmosAtom.Config().ChainID, cosmosOsmosis.Config().ChainID, gaiaOsmosisIBCPath) + generatePath(t, ctx, r, eRep, cosmosNeutron.Config().ChainID, cosmosOsmosis.Config().ChainID, neutronOsmosisIBCPath) + generatePath(t, ctx, r, eRep, cosmosNeutron.Config().ChainID, cosmosAtom.Config().ChainID, gaiaNeutronICSPath) + + // create clients + generateClient(t, ctx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) + neutronClients, _ := r.GetClients(ctx, eRep, cosmosNeutron.Config().ChainID) + atomClients, _ := r.GetClients(ctx, eRep, cosmosAtom.Config().ChainID) + + err = r.UpdatePath(ctx, eRep, gaiaNeutronICSPath, ibc.PathUpdateOptions{ + SrcClientID: &neutronClients[0].ClientID, + DstClientID: &atomClients[0].ClientID, + }) + require.NoError(t, err) + + atomNeutronICSConnectionId, neutronAtomICSConnectionId := generateConnections(t, ctx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) + + generateICSChannel(t, ctx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) + + // create connections and link everything up + generateClient(t, ctx, r, eRep, neutronOsmosisIBCPath, cosmosNeutron, cosmosOsmosis) + neutronOsmosisIBCConnId, osmosisNeutronIBCConnId := generateConnections(t, ctx, r, eRep, neutronOsmosisIBCPath, cosmosNeutron, cosmosOsmosis) + linkPath(t, ctx, r, eRep, cosmosNeutron, cosmosOsmosis, neutronOsmosisIBCPath) + + generateClient(t, ctx, r, eRep, gaiaOsmosisIBCPath, cosmosAtom, cosmosOsmosis) + gaiaOsmosisIBCConnId, osmosisGaiaIBCConnId := generateConnections(t, ctx, r, eRep, gaiaOsmosisIBCPath, cosmosAtom, cosmosOsmosis) + linkPath(t, ctx, r, eRep, cosmosAtom, cosmosOsmosis, gaiaOsmosisIBCPath) + + generateClient(t, ctx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) + atomNeutronIBCConnId, neutronAtomIBCConnId := generateConnections(t, ctx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) + linkPath(t, ctx, r, eRep, cosmosAtom, cosmosNeutron, gaiaNeutronIBCPath) + + // Start the relayer and clean it up when the test ends. + err = r.StartRelayer(ctx, eRep, gaiaNeutronICSPath, gaiaNeutronIBCPath, gaiaOsmosisIBCPath, neutronOsmosisIBCPath) + require.NoError(t, err, "failed to start relayer with given paths") + t.Cleanup(func() { + err = r.StopRelayer(ctx, eRep) + if err != nil { + t.Logf("failed to stop relayer: %s", err) + } + }) + + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + + createValidator(t, ctx, r, eRep, atom, neutron) + + // Once the VSC packet has been relayed, x/bank transfers are + // enabled on Neutron and we can fund its account. + // The funds for this are sent from a "faucet" account created + // by interchaintest in the genesis file. + users = ibctest.GetAndFundTestUsers(t, ctx, "default", int64(500_000_000_000), atom, neutron) + gaiaUser, neutronUser := users[0], users[1] + _, _ = gaiaUser, neutronUser + + err = testutil.WaitForBlocks(ctx, 10, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + + var osmoNeutronChannelId, neutronOsmoChannelId string + var osmoGaiaChannelId, gaiaOsmoChannelId string + var neutronGaiaICSChannelId, gaiaNeutronICSChannelId string + var neutronGaiaTransferChannelId, gaiaNeutronTransferChannelId string + + connectionChannelsOk := false + const maxAttempts = 3 + attempts := 1 + for (connectionChannelsOk != true) && (attempts <= maxAttempts) { + print("\n Finding connections and channels, attempt ", attempts, " of ", maxAttempts) + neutronChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosNeutron.Config().ChainID) + gaiaChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosAtom.Config().ChainID) + osmoChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosOsmosis.Config().ChainID) + + connectionChannelsOk = true + + // Find all pairwise channels + osmoNeutronChannelId, neutronOsmoChannelId, err = getPairwiseTransferChannelIds(osmoChannelInfo, neutronChannelInfo, osmosisNeutronIBCConnId, neutronOsmosisIBCConnId) + if err != nil { + connectionChannelsOk = false + } + osmoGaiaChannelId, gaiaOsmoChannelId, err = getPairwiseTransferChannelIds(osmoChannelInfo, gaiaChannelInfo, osmosisGaiaIBCConnId, gaiaOsmosisIBCConnId) + if err != nil { + connectionChannelsOk = false + } + gaiaNeutronTransferChannelId, neutronGaiaTransferChannelId, err = getPairwiseTransferChannelIds(gaiaChannelInfo, neutronChannelInfo, atomNeutronIBCConnId, neutronAtomIBCConnId) + if err != nil { + connectionChannelsOk = false + } + gaiaNeutronICSChannelId, neutronGaiaICSChannelId, err = getPairwiseCCVChannelIds(gaiaChannelInfo, neutronChannelInfo, atomNeutronICSConnectionId, neutronAtomICSConnectionId) + if err != nil { + connectionChannelsOk = false + } + + // Print out connections and channels for debugging + print("\n osmoGaiaChannelId: ", osmoGaiaChannelId) + print("\n osmosisNeutronIBCConnId: ", osmosisNeutronIBCConnId) + print("\n neutronOsmosisIBCConnId: ", neutronOsmosisIBCConnId) + print("\n neutronGaiaTransferConnectionId: ", neutronAtomIBCConnId) + print("\n neutronGaiaICSConnectionId: ", neutronAtomICSConnectionId) + print("\n gaiaOsmosisIBCConnId: ", gaiaOsmosisIBCConnId) + print("\n gaiaNeutronTransferConnectionId: ", atomNeutronIBCConnId) + print("\n gaiaNeutronICSConnectionId: ", atomNeutronICSConnectionId) + print("\n osmoGaiaChannelId: ", osmoGaiaChannelId) + print("\n osmoNeutronChannelId: ", osmoNeutronChannelId) + print("\n neutronOsmoChannelId: ", neutronOsmoChannelId) + print("\n neutronGaiaTransferChannelId: ", neutronGaiaTransferChannelId) + print("\n neutronGaiaICSChannelId: ", neutronGaiaICSChannelId) + print("\n gaiaOsmoChannelId: ", gaiaOsmoChannelId) + print("\n gaiaNeutronTransferChannelId: ", gaiaNeutronTransferChannelId) + print("\n gaiaNeutronICSChannelId: ", gaiaNeutronICSChannelId) + + if connectionChannelsOk { + print("\n Connections and channels found!") + + } else { + if attempts == maxAttempts { + panic("Initial connections and channels did not build") + } + print("\n Connections and channels not found! Waiting some time...") + err = testutil.WaitForBlocks(ctx, 100, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + attempts += 1 + } + } +} diff --git a/swap-covenant/tests/interchaintest/types.go b/swap-covenant/tests/interchaintest/types.go new file mode 100644 index 00000000..59ed1b3e --- /dev/null +++ b/swap-covenant/tests/interchaintest/types.go @@ -0,0 +1,459 @@ +package ibc_test + +////////////////////////////////////////////// +///// Covenant contracts +////////////////////////////////////////////// + +// ----- Covenant Instantiation ------ + +type PresetLsFields struct { + LsCode uint64 `json:"ls_code"` + Label string `json:"label"` + LsDenom string `json:"ls_denom"` + StrideNeutronIBCTransferChannelId string `json:"stride_neutron_ibc_transfer_channel_id"` + NeutronStrideIBCConnectionId string `json:"neutron_stride_ibc_connection_id"` +} + +type CovenantInstantiateMsg struct { + Label string `json:"label"` + PresetClock PresetClockFields `json:"preset_clock_fields"` + PresetLs PresetLsFields `json:"preset_ls_fields"` + PresetDepositor PresetDepositorFields `json:"preset_depositor_fields"` + PresetLp PresetLpFields `json:"preset_lp_fields"` + PresetHolder PresetHolderFields `json:"preset_holder_fields"` + PoolAddress string `json:"pool_address"` + PresetIbcFee PresetIbcFee `json:"preset_ibc_fee"` + Timeouts Timeouts `json:"timeouts"` +} + +type Timeouts struct { + IcaTimeout string `json:"ica_timeout"` + IbcTransferTimeout string `json:"ibc_transfer_timeout"` +} + +type PresetIbcFee struct { + AckFee string `json:"ack_fee"` + TimeoutFee string `json:"timeout_fee"` +} + +type PresetClockFields struct { + TickMaxGas string `json:"tick_max_gas,omitempty"` + ClockCode uint64 `json:"clock_code"` + Label string `json:"label"` + Whitelist []string `json:"whitelist"` +} + +type PresetHolderFields struct { + Withdrawer string `json:"withdrawer,omitempty"` + HolderCode uint64 `json:"holder_code"` + Label string `json:"label"` +} + +type PresetDepositorFields struct { + GaiaNeutronIBCTransferChannelId string `json:"gaia_neutron_ibc_transfer_channel_id"` + NeutronGaiaConnectionId string `json:"neutron_gaia_connection_id"` + GaiaStrideIBCTransferChannelId string `json:"gaia_stride_ibc_transfer_channel_id"` + DepositorCode uint64 `json:"depositor_code"` + Label string `json:"label"` + StAtomReceiverAmount WeightedReceiverAmount `json:"st_atom_receiver_amount"` + AtomReceiverAmount WeightedReceiverAmount `json:"atom_receiver_amount"` + AutopilotFormat string `json:"autopilot_format"` + NeutronAtomIbcDenom string `json:"neutron_atom_ibc_denom"` +} + +type PresetLpFields struct { + SlippageTolerance string `json:"slippage_tolerance,omitempty"` + Autostake bool `json:"autostake,omitempty"` + Assets AssetData `json:"assets"` + LpCode uint64 `json:"lp_code"` + Label string `json:"label"` + SingleSideLpLimits SingleSideLpLimits `json:"single_side_lp_limits"` + ExpectedLsTokenAmount string `json:"expected_ls_token_amount"` + AllowedReturnDelta string `json:"allowed_return_delta"` + ExpectedNativeTokenAmount string `json:"expected_native_token_amount"` +} + +type SingleSideLpLimits struct { + NativeAssetLimit string `json:"native_asset_limit"` + LsAssetLimit string `json:"ls_asset_limit"` +} + +type AssetData struct { + NativeAssetDenom string `json:"native_asset_denom"` + LsAssetDenom string `json:"ls_asset_denom"` +} + +// ----- Covenant Queries ------ + +type DepositorAddress struct{} +type DepositorAddressQuery struct { + DepositorAddress DepositorAddress `json:"depositor_address"` +} + +type ClockAddress struct{} +type ClockAddressQuery struct { + ClockAddress ClockAddress `json:"clock_address"` +} + +type HolderAddress struct{} +type HolderAddressQuery struct { + HolderAddress HolderAddress `json:"holder_address"` +} + +type LsAddress struct{} +type LsAddressQuery struct { + LsAddress LsAddress `json:"ls_address"` +} + +type LpAddress struct{} +type LpAddressQuery struct { + LpAddress LpAddress `json:"lp_address"` +} + +type CovenantAddressQueryResponse struct { + Data string `json:"data"` +} + +type ContractState struct{} +type ContractStateQuery struct { + ContractState ContractState `json:"contract_state"` +} + +type ContractStateQueryResponse struct { + Data string `json:"data"` +} + +////////////////////////////////////////////// +///// Depositor contract +////////////////////////////////////////////// + +// Instantiation +type WeightedReceiver struct { + Amount string `json:"amount"` + Address string `json:"address"` +} + +type WeightedReceiverAmount struct { + Amount string `json:"amount"` +} + +type StAtomWeightedReceiverQuery struct { + StAtomReceiver StAtomReceiverQuery `json:"st_atom_receiver"` +} + +type AtomWeightedReceiverQuery struct { + AtomReceiver AtomReceiverQuery `json:"atom_receiver"` +} + +type StAtomReceiverQuery struct{} +type AtomReceiverQuery struct{} + +type WeightedReceiverResponse struct { + Data WeightedReceiver `json:"data"` +} + +// Queries +type DepositorICAAddressQuery struct { + DepositorInterchainAccountAddress DepositorInterchainAccountAddress `json:"depositor_interchain_account_address"` +} +type DepositorInterchainAccountAddress struct{} + +type QueryResponse struct { + Data InterchainAccountAddressQueryResponse `json:"data"` +} + +type InterchainAccountAddressQueryResponse struct { + InterchainAccountAddress string `json:"interchain_account_address"` +} + +////////////////////////////////////////////// +///// Ls contract +////////////////////////////////////////////// + +// Execute +type TransferExecutionMsg struct { + Transfer TransferAmount `json:"transfer"` +} + +// Rust type here is Uint128 which can't safely be serialized +// to json int. It needs to go as a string over the wire. +type TransferAmount struct { + Amount uint64 `json:"amount,string"` +} + +// Queries +type LsIcaQuery struct { + StrideIca StrideIcaQuery `json:"stride_i_c_a"` +} +type StrideIcaQuery struct{} + +type StrideIcaQueryResponse struct { + Addr string `json:"data"` +} + +////////////////////////////////////////////// +///// Lp contract +////////////////////////////////////////////// + +type LPPositionQuery struct { + LpPosition LpPositionQuery `json:"lp_position"` +} +type LpPositionQuery struct{} + +type PairInfo struct { + LiquidityToken string `json:"liquidity_token"` + ContractAddr string `json:"contract_addr"` + PairType PairType `json:"pair_type"` + AssetInfos []AssetInfo `json:"asset_infos"` +} + +type Pair struct { + AssetInfos []AssetInfo `json:"asset_infos"` +} + +type PairQuery struct { + Pair Pair `json:"pair"` +} + +type CreatePair struct { + PairType PairType `json:"pair_type"` + AssetInfos []AssetInfo `json:"asset_infos"` + InitParams []byte `json:"init_params"` +} + +type CreatePairMsg struct { + CreatePair CreatePair `json:"create_pair"` +} + +////////////////////////////////////////////// +///// Holder contract +////////////////////////////////////////////// + +type CovenantHolderAddressQuery struct { + Addr string `json:"address"` +} + +type WithdrawLiquidityMessage struct { + WithdrawLiquidity WithdrawLiquidity `json:"withdraw_liquidity"` +} + +type WithdrawLiquidity struct{} + +type WithdrawMessage struct { + Withdraw Withdraw `json:"withdraw"` +} + +type Withdraw struct { + Quantity *[]CwCoin `json:"quantity"` +} + +////////////////////////////////////////////// +///// Astroport contracts +////////////////////////////////////////////// + +// astroport stableswap +type StableswapInstantiateMsg struct { + TokenCodeId uint64 `json:"token_code_id"` + FactoryAddr string `json:"factory_addr"` + AssetInfos []AssetInfo `json:"asset_infos"` + InitParams []byte `json:"init_params"` +} + +type AssetInfo struct { + Token *Token `json:"token,omitempty"` + NativeToken *NativeToken `json:"native_token,omitempty"` +} + +type StablePoolParams struct { + Amp uint64 `json:"amp"` + Owner *string `json:"owner"` +} + +type Token struct { + ContractAddr string `json:"contract_addr"` +} + +type NativeToken struct { + Denom string `json:"denom"` +} + +type CwCoin struct { + Denom string `json:"denom"` + Amount uint64 `json:"amount"` +} + +// astroport factory +type FactoryInstantiateMsg struct { + PairConfigs []PairConfig `json:"pair_configs"` + TokenCodeId uint64 `json:"token_code_id"` + FeeAddress *string `json:"fee_address"` + GeneratorAddress *string `json:"generator_address"` + Owner string `json:"owner"` + WhitelistCodeId uint64 `json:"whitelist_code_id"` + CoinRegistryAddress string `json:"coin_registry_address"` +} + +type PairConfig struct { + CodeId uint64 `json:"code_id"` + PairType PairType `json:"pair_type"` + TotalFeeBps uint64 `json:"total_fee_bps"` + MakerFeeBps uint64 `json:"maker_fee_bps"` + IsDisabled bool `json:"is_disabled"` + IsGeneratorDisabled bool `json:"is_generator_disabled"` +} + +type PairType struct { + // Xyk struct{} `json:"xyk,omitempty"` + Stable struct{} `json:"stable,omitempty"` + // Custom struct{} `json:"custom,omitempty"` +} + +// astroport native coin registry + +type NativeCoinRegistryInstantiateMsg struct { + Owner string `json:"owner"` +} + +type AddExecuteMsg struct { + Add Add `json:"add"` +} + +type Add struct { + NativeCoins []NativeCoin `json:"native_coins"` +} + +type NativeCoin struct { + Name string `json:"name"` + Value uint8 `json:"value"` +} + +// Add { native_coins: Vec<(String, u8)> }, + +// astroport native token +type NativeTokenInstantiateMsg struct { + Name string `json:"name"` + Symbol string `json:"symbol"` + Decimals uint8 `json:"decimals"` + InitialBalances []Cw20Coin `json:"initial_balances"` + Mint *MinterResponse `json:"mint"` + Marketing *InstantiateMarketingInfo `json:"marketing"` +} + +type Cw20Coin struct { + Address string `json:"address"` + Amount uint64 `json:"amount"` +} + +type MinterResponse struct { + Minter string `json:"minter"` + Cap *uint64 `json:"cap,omitempty"` +} + +type InstantiateMarketingInfo struct { + Project string `json:"project"` + Description string `json:"description"` + Marketing string `json:"marketing"` + Logo Logo `json:"logo"` +} + +type Logo struct { + Url string `json:"url"` +} + +// astroport whitelist +type WhitelistInstantiateMsg struct { + Admins []string `json:"admins"` + Mutable bool `json:"mutable"` +} + +type ProvideLiqudityMsg struct { + ProvideLiquidity ProvideLiquidityStruct `json:"provide_liquidity"` +} + +type ProvideLiquidityStruct struct { + Assets []AstroportAsset `json:"assets"` + SlippageTolerance string `json:"slippage_tolerance"` + AutoStake bool `json:"auto_stake"` + Receiver string `json:"receiver"` +} + +// factory + +type FactoryPairResponse struct { + Data PairInfo `json:"data"` +} + +///////////////////////////////////////////////////////////////////// +//--- These are here for debugging but should be likely removed ---// + +type CovenantClockAddressQuery struct { + Addr string `json:"address"` +} + +type DepositorContractQuery struct { + ClockAddress ClockAddressQuery `json:"clock_address"` +} + +type LPContractQuery struct { + ClockAddress ClockAddressQuery `json:"clock_address"` +} + +type ClockQueryResponse struct { + Data string `json:"data"` +} + +type LpPositionQueryResponse struct { + Data string `json:"data"` +} + +type AstroportAsset struct { + Info AssetInfo `json:"info"` + Amount string `json:"amount"` +} + +// A query against the Neutron example contract. Note the usage of +// `omitempty` on fields. This means that if that field has no value, +// it will not have a key in the serialized representaiton of the +// struct, thus mimicing the serialization of Rust enums. +type IcaExampleContractQuery struct { + InterchainAccountAddress InterchainAccountAddressQuery `json:"interchain_account_address,omitempty"` +} + +type InterchainAccountAddressQuery struct { + InterchainAccountId string `json:"interchain_account_id"` + ConnectionId string `json:"connection_id"` +} + +type ICAQueryResponse struct { + Data DepositorInterchainAccountAddressQueryResponse `json:"data"` +} + +type DepositorInterchainAccountAddressQueryResponse struct { + DepositorInterchainAccountAddress string `json:"depositor_interchain_account_address"` +} + +//------------------// + +type BalanceResponse struct { + Balance string `json:"balance"` +} + +type Cw20BalanceResponse struct { + Data BalanceResponse `json:"data"` +} + +type AllAccountsResponse struct { + Data []string `json:"all_accounts_response"` +} + +type Cw20QueryMsg struct { + Balance Balance `json:"balance"` + // AllAccounts *AllAccounts `json:"all_accounts"` +} + +type AllAccounts struct { +} + +type Balance struct { + Address string `json:"address"` +} From 113cf96145d61ff5b53c2b2bfb861306a1255295 Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 8 Sep 2023 18:41:09 +0200 Subject: [PATCH 071/586] introducing testContext to speed up interchaintest setup --- .../interchaintest/connection_helpers.go | 102 +++++++++++--- .../tests/interchaintest/tokenswap_test.go | 132 ++++++++---------- 2 files changed, 140 insertions(+), 94 deletions(-) diff --git a/swap-covenant/tests/interchaintest/connection_helpers.go b/swap-covenant/tests/interchaintest/connection_helpers.go index caca102a..ece4f9dd 100644 --- a/swap-covenant/tests/interchaintest/connection_helpers.go +++ b/swap-covenant/tests/interchaintest/connection_helpers.go @@ -12,6 +12,72 @@ import ( "github.com/stretchr/testify/require" ) +type TestContext struct { + OsmoClients []*ibc.ClientOutput + GaiaClients []*ibc.ClientOutput + NeutronClients []*ibc.ClientOutput + OsmoConnections []*ibc.ConnectionOutput + GaiaConnections []*ibc.ConnectionOutput + NeutronConnections []*ibc.ConnectionOutput +} + +func (testCtx *TestContext) getChainClients(chain string) []*ibc.ClientOutput { + switch chain { + case "neutron-2": + return testCtx.NeutronClients + case "gaia-1": + return testCtx.GaiaClients + case "osmosis-3": + return testCtx.OsmoClients + default: + return ibc.ClientOutputs{} + } +} + +func (testCtx *TestContext) updateChainClients(chain string, clients []*ibc.ClientOutput) { + println("updating chain clients for ", chain) + switch chain { + case "neutron-2": + testCtx.NeutronClients = clients + case "gaia-1": + testCtx.GaiaClients = clients + case "osmosis-3": + testCtx.OsmoClients = clients + default: + } +} + +func (testCtx *TestContext) getChainConnections(chain string) []*ibc.ConnectionOutput { + switch chain { + case "neutron-2": + println("getting neutron connections") + return testCtx.NeutronConnections + case "gaia-1": + println("getting gaia connections") + return testCtx.GaiaConnections + case "osmosis-3": + println("getting osmosis connections") + return testCtx.OsmoConnections + default: + println("error finding connections for chain ", chain) + return []*ibc.ConnectionOutput{} + } +} + +func (testCtx *TestContext) updateChainConnections(chain string, connections []*ibc.ConnectionOutput) { + println("updating chain connections for ", chain) + printConnections(connections) + switch chain { + case "neutron-2": + testCtx.NeutronConnections = connections + case "gaia-1": + testCtx.GaiaConnections = connections + case "osmosis-3": + testCtx.OsmoConnections = connections + default: + } +} + func generatePath( t *testing.T, ctx context.Context, @@ -81,14 +147,15 @@ func linkPath( func generateClient( t *testing.T, ctx context.Context, + testCtx *TestContext, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, path string, chainA ibc.Chain, chainB ibc.Chain, ) (string, string) { - chainAClients, _ := r.GetClients(ctx, eRep, chainA.Config().ChainID) - chainBClients, _ := r.GetClients(ctx, eRep, chainB.Config().ChainID) + chainAClients := testCtx.getChainClients(chainA.Config().Name) + chainBClients := testCtx.getChainClients(chainB.Config().Name) err := r.CreateClients(ctx, eRep, path, ibc.CreateClientOptions{TrustingPeriod: "330h"}) require.NoError(t, err) @@ -114,21 +181,24 @@ func generateClient( newClientB = "" } - print("\n found client differences. new client A: ", newClientA, "b:") + testCtx.updateChainClients(chainA.Config().Name, newChainAClients) + testCtx.updateChainClients(chainB.Config().Name, newChainBClients) + return newClientA, newClientB } func generateConnections( t *testing.T, ctx context.Context, + testCtx *TestContext, r ibc.Relayer, eRep *testreporter.RelayerExecReporter, path string, chainA ibc.Chain, chainB ibc.Chain, ) (string, string) { - chainAConns, _ := r.GetConnections(ctx, eRep, chainA.Config().ChainID) - chainBConns, _ := r.GetConnections(ctx, eRep, chainB.Config().ChainID) + chainAConns := testCtx.getChainConnections(chainA.Config().Name) + chainBConns := testCtx.getChainConnections(chainB.Config().Name) err := r.CreateConnections(ctx, eRep, path) require.NoError(t, err) @@ -144,14 +214,13 @@ func generateConnections( require.NotEqual(t, 0, len(newChainAConnection), "more than one connection generated", strings.Join(newChainAConnection, " ")) require.NotEqual(t, 0, len(newChainBConnection), "more than one connection generated", strings.Join(newChainBConnection, " ")) + testCtx.updateChainConnections(chainA.Config().Name, newChainAConns) + testCtx.updateChainConnections(chainB.Config().Name, newChainBConns) + return newChainAConnection[0], newChainBConnection[0] } -func connectionDifference( - a []*ibc.ConnectionOutput, - b []*ibc.ConnectionOutput, -) (diff []string) { - +func connectionDifference(a, b []*ibc.ConnectionOutput) (diff []string) { m := make(map[string]bool) // we first mark all existing connections @@ -246,7 +315,7 @@ func getPairwiseTransferChannelIds( bchans []ibc.ChannelOutput, aToBConnId string, bToAConnId string, -) (string, string, error) { +) (string, string) { for _, a := range achans { for _, b := range bchans { @@ -259,12 +328,11 @@ func getPairwiseTransferChannelIds( a.ConnectionHops[0] == aToBConnId && b.ConnectionHops[0] == bToAConnId { - return a.ChannelID, b.ChannelID, nil + return a.ChannelID, b.ChannelID } } } - - return "", "", errors.New("no transfer channel found") + panic("failed to match pairwise transfer channels") } // returns ccv channel ids @@ -273,7 +341,7 @@ func getPairwiseCCVChannelIds( bchans []ibc.ChannelOutput, aToBConnId string, bToAConnId string, -) (string, string, error) { +) (string, string) { for _, a := range achans { for _, b := range bchans { if a.ChannelID == b.Counterparty.ChannelID && @@ -284,9 +352,9 @@ func getPairwiseCCVChannelIds( b.Ordering == "ORDER_ORDERED" && a.ConnectionHops[0] == aToBConnId && b.ConnectionHops[0] == bToAConnId { - return a.ChannelID, b.ChannelID, nil + return a.ChannelID, b.ChannelID } } } - return "", "", errors.New("no ccv channel found") + panic("failed to match pairwise ICS channels") } diff --git a/swap-covenant/tests/interchaintest/tokenswap_test.go b/swap-covenant/tests/interchaintest/tokenswap_test.go index 989b949d..7af2610f 100644 --- a/swap-covenant/tests/interchaintest/tokenswap_test.go +++ b/swap-covenant/tests/interchaintest/tokenswap_test.go @@ -18,6 +18,11 @@ import ( "go.uber.org/zap/zaptest" ) +const gaiaNeutronICSPath = "gn-ics-path" +const gaiaNeutronIBCPath = "gn-ibc-path" +const gaiaOsmosisIBCPath = "go-ibc-path" +const neutronOsmosisIBCPath = "no-ibc-path" + // sets up and tests a tokenswap between hub and stargaze facilitated by neutron func TestTokenSwap(t *testing.T) { if testing.Short() { @@ -99,11 +104,6 @@ func TestTokenSwap(t *testing.T) { ).Build(t, client, network) // Prep Interchain - const gaiaNeutronICSPath = "gn-ics-path" - const gaiaNeutronIBCPath = "gn-ibc-path" - const gaiaOsmosisIBCPath = "go-ibc-path" - const neutronOsmosisIBCPath = "no-ibc-path" - ic := ibctest.NewInterchain(). AddChain(cosmosAtom). AddChain(cosmosNeutron). @@ -154,6 +154,15 @@ func TestTokenSwap(t *testing.T) { require.NoError(t, err) require.Equal(t, int64(500_000_000_000), osmoUserBalInitial) + testCtx := &TestContext{ + OsmoClients: []*ibc.ClientOutput{}, + GaiaClients: []*ibc.ClientOutput{}, + NeutronClients: []*ibc.ClientOutput{}, + OsmoConnections: []*ibc.ConnectionOutput{}, + GaiaConnections: []*ibc.ConnectionOutput{}, + NeutronConnections: []*ibc.ConnectionOutput{}, + } + // generate paths generatePath(t, ctx, r, eRep, cosmosAtom.Config().ChainID, cosmosNeutron.Config().ChainID, gaiaNeutronIBCPath) generatePath(t, ctx, r, eRep, cosmosAtom.Config().ChainID, cosmosOsmosis.Config().ChainID, gaiaOsmosisIBCPath) @@ -161,9 +170,9 @@ func TestTokenSwap(t *testing.T) { generatePath(t, ctx, r, eRep, cosmosNeutron.Config().ChainID, cosmosAtom.Config().ChainID, gaiaNeutronICSPath) // create clients - generateClient(t, ctx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) - neutronClients, _ := r.GetClients(ctx, eRep, cosmosNeutron.Config().ChainID) - atomClients, _ := r.GetClients(ctx, eRep, cosmosAtom.Config().ChainID) + generateClient(t, ctx, testCtx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) + neutronClients := testCtx.getChainClients(cosmosNeutron.Config().Name) + atomClients := testCtx.getChainClients(cosmosAtom.Config().Name) err = r.UpdatePath(ctx, eRep, gaiaNeutronICSPath, ibc.PathUpdateOptions{ SrcClientID: &neutronClients[0].ClientID, @@ -171,26 +180,28 @@ func TestTokenSwap(t *testing.T) { }) require.NoError(t, err) - atomNeutronICSConnectionId, neutronAtomICSConnectionId := generateConnections(t, ctx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) + atomNeutronICSConnectionId, neutronAtomICSConnectionId := generateConnections(t, ctx, testCtx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) generateICSChannel(t, ctx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) // create connections and link everything up - generateClient(t, ctx, r, eRep, neutronOsmosisIBCPath, cosmosNeutron, cosmosOsmosis) - neutronOsmosisIBCConnId, osmosisNeutronIBCConnId := generateConnections(t, ctx, r, eRep, neutronOsmosisIBCPath, cosmosNeutron, cosmosOsmosis) + generateClient(t, ctx, testCtx, r, eRep, neutronOsmosisIBCPath, cosmosNeutron, cosmosOsmosis) + neutronOsmosisIBCConnId, osmosisNeutronIBCConnId := generateConnections(t, ctx, testCtx, r, eRep, neutronOsmosisIBCPath, cosmosNeutron, cosmosOsmosis) linkPath(t, ctx, r, eRep, cosmosNeutron, cosmosOsmosis, neutronOsmosisIBCPath) - generateClient(t, ctx, r, eRep, gaiaOsmosisIBCPath, cosmosAtom, cosmosOsmosis) - gaiaOsmosisIBCConnId, osmosisGaiaIBCConnId := generateConnections(t, ctx, r, eRep, gaiaOsmosisIBCPath, cosmosAtom, cosmosOsmosis) + generateClient(t, ctx, testCtx, r, eRep, gaiaOsmosisIBCPath, cosmosAtom, cosmosOsmosis) + gaiaOsmosisIBCConnId, osmosisGaiaIBCConnId := generateConnections(t, ctx, testCtx, r, eRep, gaiaOsmosisIBCPath, cosmosAtom, cosmosOsmosis) linkPath(t, ctx, r, eRep, cosmosAtom, cosmosOsmosis, gaiaOsmosisIBCPath) - generateClient(t, ctx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) - atomNeutronIBCConnId, neutronAtomIBCConnId := generateConnections(t, ctx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) + generateClient(t, ctx, testCtx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) + atomNeutronIBCConnId, neutronAtomIBCConnId := generateConnections(t, ctx, testCtx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) linkPath(t, ctx, r, eRep, cosmosAtom, cosmosNeutron, gaiaNeutronIBCPath) // Start the relayer and clean it up when the test ends. - err = r.StartRelayer(ctx, eRep, gaiaNeutronICSPath, gaiaNeutronIBCPath, gaiaOsmosisIBCPath, neutronOsmosisIBCPath) - require.NoError(t, err, "failed to start relayer with given paths") + require.NoError(t, + r.StartRelayer(ctx, eRep, gaiaNeutronICSPath, gaiaNeutronIBCPath, gaiaOsmosisIBCPath, neutronOsmosisIBCPath), + "failed to start relayer with given paths", + ) t.Cleanup(func() { err = r.StopRelayer(ctx, eRep) if err != nil { @@ -219,64 +230,31 @@ func TestTokenSwap(t *testing.T) { var neutronGaiaICSChannelId, gaiaNeutronICSChannelId string var neutronGaiaTransferChannelId, gaiaNeutronTransferChannelId string - connectionChannelsOk := false - const maxAttempts = 3 - attempts := 1 - for (connectionChannelsOk != true) && (attempts <= maxAttempts) { - print("\n Finding connections and channels, attempt ", attempts, " of ", maxAttempts) - neutronChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosNeutron.Config().ChainID) - gaiaChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosAtom.Config().ChainID) - osmoChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosOsmosis.Config().ChainID) - - connectionChannelsOk = true - - // Find all pairwise channels - osmoNeutronChannelId, neutronOsmoChannelId, err = getPairwiseTransferChannelIds(osmoChannelInfo, neutronChannelInfo, osmosisNeutronIBCConnId, neutronOsmosisIBCConnId) - if err != nil { - connectionChannelsOk = false - } - osmoGaiaChannelId, gaiaOsmoChannelId, err = getPairwiseTransferChannelIds(osmoChannelInfo, gaiaChannelInfo, osmosisGaiaIBCConnId, gaiaOsmosisIBCConnId) - if err != nil { - connectionChannelsOk = false - } - gaiaNeutronTransferChannelId, neutronGaiaTransferChannelId, err = getPairwiseTransferChannelIds(gaiaChannelInfo, neutronChannelInfo, atomNeutronIBCConnId, neutronAtomIBCConnId) - if err != nil { - connectionChannelsOk = false - } - gaiaNeutronICSChannelId, neutronGaiaICSChannelId, err = getPairwiseCCVChannelIds(gaiaChannelInfo, neutronChannelInfo, atomNeutronICSConnectionId, neutronAtomICSConnectionId) - if err != nil { - connectionChannelsOk = false - } - - // Print out connections and channels for debugging - print("\n osmoGaiaChannelId: ", osmoGaiaChannelId) - print("\n osmosisNeutronIBCConnId: ", osmosisNeutronIBCConnId) - print("\n neutronOsmosisIBCConnId: ", neutronOsmosisIBCConnId) - print("\n neutronGaiaTransferConnectionId: ", neutronAtomIBCConnId) - print("\n neutronGaiaICSConnectionId: ", neutronAtomICSConnectionId) - print("\n gaiaOsmosisIBCConnId: ", gaiaOsmosisIBCConnId) - print("\n gaiaNeutronTransferConnectionId: ", atomNeutronIBCConnId) - print("\n gaiaNeutronICSConnectionId: ", atomNeutronICSConnectionId) - print("\n osmoGaiaChannelId: ", osmoGaiaChannelId) - print("\n osmoNeutronChannelId: ", osmoNeutronChannelId) - print("\n neutronOsmoChannelId: ", neutronOsmoChannelId) - print("\n neutronGaiaTransferChannelId: ", neutronGaiaTransferChannelId) - print("\n neutronGaiaICSChannelId: ", neutronGaiaICSChannelId) - print("\n gaiaOsmoChannelId: ", gaiaOsmoChannelId) - print("\n gaiaNeutronTransferChannelId: ", gaiaNeutronTransferChannelId) - print("\n gaiaNeutronICSChannelId: ", gaiaNeutronICSChannelId) - - if connectionChannelsOk { - print("\n Connections and channels found!") - - } else { - if attempts == maxAttempts { - panic("Initial connections and channels did not build") - } - print("\n Connections and channels not found! Waiting some time...") - err = testutil.WaitForBlocks(ctx, 100, atom, neutron, osmosis) - require.NoError(t, err, "failed to wait for blocks") - attempts += 1 - } - } + neutronChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosNeutron.Config().ChainID) + gaiaChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosAtom.Config().ChainID) + osmoChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosOsmosis.Config().ChainID) + + // Find all pairwise channels + osmoNeutronChannelId, neutronOsmoChannelId = getPairwiseTransferChannelIds(osmoChannelInfo, neutronChannelInfo, osmosisNeutronIBCConnId, neutronOsmosisIBCConnId) + osmoGaiaChannelId, gaiaOsmoChannelId = getPairwiseTransferChannelIds(osmoChannelInfo, gaiaChannelInfo, osmosisGaiaIBCConnId, gaiaOsmosisIBCConnId) + gaiaNeutronTransferChannelId, neutronGaiaTransferChannelId = getPairwiseTransferChannelIds(gaiaChannelInfo, neutronChannelInfo, atomNeutronIBCConnId, neutronAtomIBCConnId) + gaiaNeutronICSChannelId, neutronGaiaICSChannelId = getPairwiseCCVChannelIds(gaiaChannelInfo, neutronChannelInfo, atomNeutronICSConnectionId, neutronAtomICSConnectionId) + + // Print out connections and channels for debugging + print("\n osmoGaiaChannelId: ", osmoGaiaChannelId) + print("\n osmoNeutronChannelId: ", osmoNeutronChannelId) + print("\n gaiaOsmoChannelId: ", gaiaOsmoChannelId) + print("\n gaiaNeutronTransferChannelId: ", gaiaNeutronTransferChannelId) + print("\n gaiaNeutronICSChannelId: ", gaiaNeutronICSChannelId) + print("\n neutronOsmoChannelId: ", neutronOsmoChannelId) + print("\n neutronGaiaTransferChannelId: ", neutronGaiaTransferChannelId) + print("\n neutronGaiaICSChannelId: ", neutronGaiaICSChannelId) + + print("\n osmosisNeutronIBCConnId: ", osmosisNeutronIBCConnId) + print("\n neutronOsmosisIBCConnId: ", neutronOsmosisIBCConnId) + print("\n neutronGaiaTransferConnectionId: ", neutronAtomIBCConnId) + print("\n neutronGaiaICSConnectionId: ", neutronAtomICSConnectionId) + print("\n gaiaOsmosisIBCConnId: ", gaiaOsmosisIBCConnId) + print("\n gaiaNeutronTransferConnectionId: ", atomNeutronIBCConnId) + print("\n gaiaNeutronICSConnectionId: ", atomNeutronICSConnectionId) } From 9f7a1c56068016ec1fe0f4d1ee09f616c05f205d Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 8 Sep 2023 19:34:17 +0200 Subject: [PATCH 072/586] storing channel ids on testCtx --- .../interchaintest/connection_helpers.go | 50 ++++++++++++++++--- .../tests/interchaintest/tokenswap_test.go | 46 +++++++++++++---- 2 files changed, 79 insertions(+), 17 deletions(-) diff --git a/swap-covenant/tests/interchaintest/connection_helpers.go b/swap-covenant/tests/interchaintest/connection_helpers.go index ece4f9dd..80a59954 100644 --- a/swap-covenant/tests/interchaintest/connection_helpers.go +++ b/swap-covenant/tests/interchaintest/connection_helpers.go @@ -13,12 +13,17 @@ import ( ) type TestContext struct { - OsmoClients []*ibc.ClientOutput - GaiaClients []*ibc.ClientOutput - NeutronClients []*ibc.ClientOutput - OsmoConnections []*ibc.ConnectionOutput - GaiaConnections []*ibc.ConnectionOutput - NeutronConnections []*ibc.ConnectionOutput + OsmoClients []*ibc.ClientOutput + GaiaClients []*ibc.ClientOutput + NeutronClients []*ibc.ClientOutput + OsmoConnections []*ibc.ConnectionOutput + GaiaConnections []*ibc.ConnectionOutput + NeutronConnections []*ibc.ConnectionOutput + NeutronTransferChannelIds map[string]string + GaiaTransferChannelIds map[string]string + OsmoTransferChannelIds map[string]string + GaiaIcsChannelIds map[string]string + NeutronIcsChannelIds map[string]string } func (testCtx *TestContext) getChainClients(chain string) []*ibc.ClientOutput { @@ -34,6 +39,28 @@ func (testCtx *TestContext) getChainClients(chain string) []*ibc.ClientOutput { } } +func (testCtx *TestContext) setTransferChannelId(chain string, destChain string, channelId string) { + switch chain { + case "neutron-2": + testCtx.NeutronTransferChannelIds[destChain] = channelId + case "gaia-1": + testCtx.GaiaTransferChannelIds[destChain] = channelId + case "osmosis-3": + testCtx.OsmoTransferChannelIds[destChain] = channelId + default: + } +} + +func (testCtx *TestContext) setIcsChannelId(chain string, destChain string, channelId string) { + switch chain { + case "neutron-2": + testCtx.NeutronIcsChannelIds[destChain] = channelId + case "gaia-1": + testCtx.GaiaIcsChannelIds[destChain] = channelId + default: + } +} + func (testCtx *TestContext) updateChainClients(chain string, clients []*ibc.ClientOutput) { println("updating chain clients for ", chain) switch chain { @@ -311,10 +338,13 @@ func getPairwiseConnectionIds( // returns transfer channel ids func getPairwiseTransferChannelIds( + testCtx *TestContext, achans []ibc.ChannelOutput, bchans []ibc.ChannelOutput, aToBConnId string, bToAConnId string, + chainA string, + chainB string, ) (string, string) { for _, a := range achans { @@ -327,7 +357,8 @@ func getPairwiseTransferChannelIds( b.Ordering == "ORDER_UNORDERED" && a.ConnectionHops[0] == aToBConnId && b.ConnectionHops[0] == bToAConnId { - + testCtx.setTransferChannelId(chainA, chainB, a.ChannelID) + testCtx.setTransferChannelId(chainB, chainA, b.ChannelID) return a.ChannelID, b.ChannelID } } @@ -337,10 +368,13 @@ func getPairwiseTransferChannelIds( // returns ccv channel ids func getPairwiseCCVChannelIds( + testCtx *TestContext, achans []ibc.ChannelOutput, bchans []ibc.ChannelOutput, aToBConnId string, bToAConnId string, + chainA string, + chainB string, ) (string, string) { for _, a := range achans { for _, b := range bchans { @@ -352,6 +386,8 @@ func getPairwiseCCVChannelIds( b.Ordering == "ORDER_ORDERED" && a.ConnectionHops[0] == aToBConnId && b.ConnectionHops[0] == bToAConnId { + testCtx.setIcsChannelId(chainA, chainB, a.ChannelID) + testCtx.setIcsChannelId(chainB, chainA, b.ChannelID) return a.ChannelID, b.ChannelID } } diff --git a/swap-covenant/tests/interchaintest/tokenswap_test.go b/swap-covenant/tests/interchaintest/tokenswap_test.go index 7af2610f..18ea6f29 100644 --- a/swap-covenant/tests/interchaintest/tokenswap_test.go +++ b/swap-covenant/tests/interchaintest/tokenswap_test.go @@ -155,12 +155,17 @@ func TestTokenSwap(t *testing.T) { require.Equal(t, int64(500_000_000_000), osmoUserBalInitial) testCtx := &TestContext{ - OsmoClients: []*ibc.ClientOutput{}, - GaiaClients: []*ibc.ClientOutput{}, - NeutronClients: []*ibc.ClientOutput{}, - OsmoConnections: []*ibc.ConnectionOutput{}, - GaiaConnections: []*ibc.ConnectionOutput{}, - NeutronConnections: []*ibc.ConnectionOutput{}, + OsmoClients: []*ibc.ClientOutput{}, + GaiaClients: []*ibc.ClientOutput{}, + NeutronClients: []*ibc.ClientOutput{}, + OsmoConnections: []*ibc.ConnectionOutput{}, + GaiaConnections: []*ibc.ConnectionOutput{}, + NeutronConnections: []*ibc.ConnectionOutput{}, + NeutronTransferChannelIds: make(map[string]string), + GaiaTransferChannelIds: make(map[string]string), + OsmoTransferChannelIds: make(map[string]string), + GaiaIcsChannelIds: make(map[string]string), + NeutronIcsChannelIds: make(map[string]string), } // generate paths @@ -235,10 +240,10 @@ func TestTokenSwap(t *testing.T) { osmoChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosOsmosis.Config().ChainID) // Find all pairwise channels - osmoNeutronChannelId, neutronOsmoChannelId = getPairwiseTransferChannelIds(osmoChannelInfo, neutronChannelInfo, osmosisNeutronIBCConnId, neutronOsmosisIBCConnId) - osmoGaiaChannelId, gaiaOsmoChannelId = getPairwiseTransferChannelIds(osmoChannelInfo, gaiaChannelInfo, osmosisGaiaIBCConnId, gaiaOsmosisIBCConnId) - gaiaNeutronTransferChannelId, neutronGaiaTransferChannelId = getPairwiseTransferChannelIds(gaiaChannelInfo, neutronChannelInfo, atomNeutronIBCConnId, neutronAtomIBCConnId) - gaiaNeutronICSChannelId, neutronGaiaICSChannelId = getPairwiseCCVChannelIds(gaiaChannelInfo, neutronChannelInfo, atomNeutronICSConnectionId, neutronAtomICSConnectionId) + osmoNeutronChannelId, neutronOsmoChannelId = getPairwiseTransferChannelIds(testCtx, osmoChannelInfo, neutronChannelInfo, osmosisNeutronIBCConnId, neutronOsmosisIBCConnId, osmosis.Config().Name, neutron.Config().Name) + osmoGaiaChannelId, gaiaOsmoChannelId = getPairwiseTransferChannelIds(testCtx, osmoChannelInfo, gaiaChannelInfo, osmosisGaiaIBCConnId, gaiaOsmosisIBCConnId, osmosis.Config().Name, cosmosAtom.Config().Name) + gaiaNeutronTransferChannelId, neutronGaiaTransferChannelId = getPairwiseTransferChannelIds(testCtx, gaiaChannelInfo, neutronChannelInfo, atomNeutronIBCConnId, neutronAtomIBCConnId, cosmosAtom.Config().Name, neutron.Config().Name) + gaiaNeutronICSChannelId, neutronGaiaICSChannelId = getPairwiseCCVChannelIds(testCtx, gaiaChannelInfo, neutronChannelInfo, atomNeutronICSConnectionId, neutronAtomICSConnectionId, cosmosAtom.Config().Name, cosmosNeutron.Config().Name) // Print out connections and channels for debugging print("\n osmoGaiaChannelId: ", osmoGaiaChannelId) @@ -257,4 +262,25 @@ func TestTokenSwap(t *testing.T) { print("\n gaiaOsmosisIBCConnId: ", gaiaOsmosisIBCConnId) print("\n gaiaNeutronTransferConnectionId: ", atomNeutronIBCConnId) print("\n gaiaNeutronICSConnectionId: ", atomNeutronICSConnectionId) + + print("\n neutron channels: ") + for key, value := range testCtx.NeutronTransferChannelIds { + fmt.Printf("Key: %s, Value: %s\n", key, value) + } + print("\n osmo channels: ") + for key, value := range testCtx.OsmoTransferChannelIds { + fmt.Printf("Key: %s, Value: %s\n", key, value) + } + print("\n gaia channels: ") + for key, value := range testCtx.GaiaTransferChannelIds { + fmt.Printf("Key: %s, Value: %s\n", key, value) + } + print("\n gaia ics channels: ") + for key, value := range testCtx.GaiaIcsChannelIds { + fmt.Printf("Key: %s, Value: %s\n", key, value) + } + print("\n neutron ics channels: ") + for key, value := range testCtx.NeutronIcsChannelIds { + fmt.Printf("Key: %s, Value: %s\n", key, value) + } } From dc09089d95f7e8909508f6894c4f35e98db3859f Mon Sep 17 00:00:00 2001 From: bekauz Date: Sat, 9 Sep 2023 21:40:34 +0200 Subject: [PATCH 073/586] denom tracing; storing contracts on chain --- swap-covenant/justfile | 10 ++ .../interchaintest/connection_helpers.go | 7 + .../tests/interchaintest/tokenswap_test.go | 120 +++++++++++++----- 3 files changed, 105 insertions(+), 32 deletions(-) diff --git a/swap-covenant/justfile b/swap-covenant/justfile index c21a7245..760a3d93 100644 --- a/swap-covenant/justfile +++ b/swap-covenant/justfile @@ -24,9 +24,19 @@ optimize: ./optimize.sh simtest: optimize + if [[ $(uname -m) =~ "arm64" ]]; then \ + mv ./../artifacts/covenant_interchain_splitter-aarch64.wasm ./../artifacts/covenant_interchain_splitter.wasm && \ + mv ./../artifacts/covenant_ibc_forwarder-aarch64.wasm ./../artifacts/covenant_ibc_forwarder.wasm && \ + mv ./../artifacts/covenant_interchain_router-aarch64.wasm ./../artifacts/covenant_interchain_router.wasm && \ + mv ./../artifacts/covenant_clock-aarch64.wasm ./../artifacts/covenant_clock.wasm && \ + mv ./../artifacts/covenant_swap_holder-aarch64.wasm ./../artifacts/covenant_swap_holder.wasm && \ + mv ./../artifacts/covenant_swap-aarch64.wasm ./../artifacts/covenant_swap.wasm \ + ;fi + mkdir -p tests/interchaintest/wasms cp -R ./../artifacts/*.wasm tests/interchaintest/wasms + go clean -testcache cd tests/interchaintest/ && go test -timeout 30m -v ./... diff --git a/swap-covenant/tests/interchaintest/connection_helpers.go b/swap-covenant/tests/interchaintest/connection_helpers.go index 80a59954..39a7439d 100644 --- a/swap-covenant/tests/interchaintest/connection_helpers.go +++ b/swap-covenant/tests/interchaintest/connection_helpers.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + transfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types" "github.com/strangelove-ventures/interchaintest/v4/ibc" "github.com/strangelove-ventures/interchaintest/v4/testreporter" "github.com/strangelove-ventures/interchaintest/v4/testutil" @@ -26,6 +27,12 @@ type TestContext struct { NeutronIcsChannelIds map[string]string } +func (testCtx *TestContext) getIbcDenom(channelId string, denom string) string { + prefixedDenom := transfertypes.GetPrefixedDenom("transfer", channelId, denom) + srcDenomTrace := transfertypes.ParseDenomTrace(prefixedDenom) + return srcDenomTrace.IBCDenom() +} + func (testCtx *TestContext) getChainClients(chain string) []*ibc.ClientOutput { switch chain { case "neutron-2": diff --git a/swap-covenant/tests/interchaintest/tokenswap_test.go b/swap-covenant/tests/interchaintest/tokenswap_test.go index 18ea6f29..1ef39a47 100644 --- a/swap-covenant/tests/interchaintest/tokenswap_test.go +++ b/swap-covenant/tests/interchaintest/tokenswap_test.go @@ -3,6 +3,7 @@ package ibc_test import ( "context" "fmt" + "strconv" "testing" "time" @@ -230,40 +231,17 @@ func TestTokenSwap(t *testing.T) { err = testutil.WaitForBlocks(ctx, 10, atom, neutron, osmosis) require.NoError(t, err, "failed to wait for blocks") - var osmoNeutronChannelId, neutronOsmoChannelId string - var osmoGaiaChannelId, gaiaOsmoChannelId string - var neutronGaiaICSChannelId, gaiaNeutronICSChannelId string - var neutronGaiaTransferChannelId, gaiaNeutronTransferChannelId string - neutronChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosNeutron.Config().ChainID) gaiaChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosAtom.Config().ChainID) osmoChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosOsmosis.Config().ChainID) // Find all pairwise channels - osmoNeutronChannelId, neutronOsmoChannelId = getPairwiseTransferChannelIds(testCtx, osmoChannelInfo, neutronChannelInfo, osmosisNeutronIBCConnId, neutronOsmosisIBCConnId, osmosis.Config().Name, neutron.Config().Name) - osmoGaiaChannelId, gaiaOsmoChannelId = getPairwiseTransferChannelIds(testCtx, osmoChannelInfo, gaiaChannelInfo, osmosisGaiaIBCConnId, gaiaOsmosisIBCConnId, osmosis.Config().Name, cosmosAtom.Config().Name) - gaiaNeutronTransferChannelId, neutronGaiaTransferChannelId = getPairwiseTransferChannelIds(testCtx, gaiaChannelInfo, neutronChannelInfo, atomNeutronIBCConnId, neutronAtomIBCConnId, cosmosAtom.Config().Name, neutron.Config().Name) - gaiaNeutronICSChannelId, neutronGaiaICSChannelId = getPairwiseCCVChannelIds(testCtx, gaiaChannelInfo, neutronChannelInfo, atomNeutronICSConnectionId, neutronAtomICSConnectionId, cosmosAtom.Config().Name, cosmosNeutron.Config().Name) - - // Print out connections and channels for debugging - print("\n osmoGaiaChannelId: ", osmoGaiaChannelId) - print("\n osmoNeutronChannelId: ", osmoNeutronChannelId) - print("\n gaiaOsmoChannelId: ", gaiaOsmoChannelId) - print("\n gaiaNeutronTransferChannelId: ", gaiaNeutronTransferChannelId) - print("\n gaiaNeutronICSChannelId: ", gaiaNeutronICSChannelId) - print("\n neutronOsmoChannelId: ", neutronOsmoChannelId) - print("\n neutronGaiaTransferChannelId: ", neutronGaiaTransferChannelId) - print("\n neutronGaiaICSChannelId: ", neutronGaiaICSChannelId) - - print("\n osmosisNeutronIBCConnId: ", osmosisNeutronIBCConnId) - print("\n neutronOsmosisIBCConnId: ", neutronOsmosisIBCConnId) - print("\n neutronGaiaTransferConnectionId: ", neutronAtomIBCConnId) - print("\n neutronGaiaICSConnectionId: ", neutronAtomICSConnectionId) - print("\n gaiaOsmosisIBCConnId: ", gaiaOsmosisIBCConnId) - print("\n gaiaNeutronTransferConnectionId: ", atomNeutronIBCConnId) - print("\n gaiaNeutronICSConnectionId: ", atomNeutronICSConnectionId) - - print("\n neutron channels: ") + getPairwiseTransferChannelIds(testCtx, osmoChannelInfo, neutronChannelInfo, osmosisNeutronIBCConnId, neutronOsmosisIBCConnId, osmosis.Config().Name, neutron.Config().Name) + getPairwiseTransferChannelIds(testCtx, osmoChannelInfo, gaiaChannelInfo, osmosisGaiaIBCConnId, gaiaOsmosisIBCConnId, osmosis.Config().Name, cosmosAtom.Config().Name) + getPairwiseTransferChannelIds(testCtx, gaiaChannelInfo, neutronChannelInfo, atomNeutronIBCConnId, neutronAtomIBCConnId, cosmosAtom.Config().Name, neutron.Config().Name) + getPairwiseCCVChannelIds(testCtx, gaiaChannelInfo, neutronChannelInfo, atomNeutronICSConnectionId, neutronAtomICSConnectionId, cosmosAtom.Config().Name, cosmosNeutron.Config().Name) + + println("neutron channels:") for key, value := range testCtx.NeutronTransferChannelIds { fmt.Printf("Key: %s, Value: %s\n", key, value) } @@ -271,16 +249,94 @@ func TestTokenSwap(t *testing.T) { for key, value := range testCtx.OsmoTransferChannelIds { fmt.Printf("Key: %s, Value: %s\n", key, value) } - print("\n gaia channels: ") + println("gaia channels:") for key, value := range testCtx.GaiaTransferChannelIds { fmt.Printf("Key: %s, Value: %s\n", key, value) } - print("\n gaia ics channels: ") + println("gaia ics channels:") for key, value := range testCtx.GaiaIcsChannelIds { fmt.Printf("Key: %s, Value: %s\n", key, value) } - print("\n neutron ics channels: ") + println("neutron ics channels:") for key, value := range testCtx.NeutronIcsChannelIds { fmt.Printf("Key: %s, Value: %s\n", key, value) } + + // We can determine the ibc denoms of: + // 1. ATOM on Neutron + neutronAtomIbcDenom := testCtx.getIbcDenom(testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], cosmosAtom.Config().Denom) + // 2. Osmo on neutron + neutronOsmoIbcDenom := testCtx.getIbcDenom(testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], cosmosOsmosis.Config().Denom) + + print("\nneutronAtomIbcDenom: ", neutronAtomIbcDenom) + print("\nneutronOsmoIbcDenom: ", neutronOsmoIbcDenom) + + t.Run("tokenswap setup", func(t *testing.T) { + //----------------------------------------------// + // Testing parameters + //----------------------------------------------// + const osmoContributionAmount uint64 = 100_000_000_000 // in uosmo + const atomContributionAmount uint64 = 5_000_000_000 // in uatom + + //----------------------------------------------// + // Wasm code that we need to store on Neutron + const covenantContractPath = "wasms/covenant_swap.wasm" + const clockContractPath = "wasms/covenant_clock.wasm" + const routerContractPath = "wasms/covenant_interchain_router.wasm" + const splitterContractPath = "wasms/covenant_interchain_splitter.wasm" + const ibcForwarderContractPath = "wasms/covenant_ibc_forwarder.wasm" + const swapHolderContractPath = "wasms/covenant_swap_holder.wasm" + + // After storing on Neutron, we will receive a code id + // We parse all the subcontracts into uint64 + // The will be required when we instantiate the covenant. + var clockCodeId uint64 + var routerCodeId uint64 + var splitterCodeId uint64 + var ibcForwarderCodeId uint64 + var swapHolderCodeId uint64 + var covenantCodeId uint64 + + t.Run("deploy covenant contracts", func(t *testing.T) { + // store covenant and get code id + covenantCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, covenantContractPath) + require.NoError(t, err, "failed to store stride covenant contract") + covenantCodeId, err = strconv.ParseUint(covenantCodeIdStr, 10, 64) + require.NoError(t, err, "failed to parse codeId into uint64") + + // store clock and get code id + clockCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, clockContractPath) + require.NoError(t, err, "failed to store clock contract") + clockCodeId, err = strconv.ParseUint(clockCodeIdStr, 10, 64) + require.NoError(t, err, "failed to parse codeId into uint64") + + // store router and get code id + routerCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, routerContractPath) + require.NoError(t, err, "failed to store router contract") + routerCodeId, err = strconv.ParseUint(routerCodeIdStr, 10, 64) + require.NoError(t, err, "failed to parse codeId into uint64") + + // store clock and get code id + splitterCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, splitterContractPath) + require.NoError(t, err, "failed to store splitter contract") + splitterCodeId, err = strconv.ParseUint(splitterCodeIdStr, 10, 64) + require.NoError(t, err, "failed to parse codeId into uint64") + + // store clock and get code id + ibcForwarderCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, ibcForwarderContractPath) + require.NoError(t, err, "failed to store ibc forwarder contract") + ibcForwarderCodeId, err = strconv.ParseUint(ibcForwarderCodeIdStr, 10, 64) + require.NoError(t, err, "failed to parse codeId into uint64") + + // store clock and get code id + swapHolderCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, swapHolderContractPath) + require.NoError(t, err, "failed to store swap holder contract") + swapHolderCodeId, err = strconv.ParseUint(swapHolderCodeIdStr, 10, 64) + require.NoError(t, err, "failed to parse codeId into uint64") + }) + println(clockCodeId, routerCodeId, splitterCodeId, ibcForwarderCodeId, swapHolderCodeId, covenantCodeId) + t.Run("instantiate covenant", func(t *testing.T) { + // todo + }) + }) } From 45eee8c84f9e1b927e47c4c3770bca8fed98c46c Mon Sep 17 00:00:00 2001 From: bekauz Date: Sun, 10 Sep 2023 23:50:48 +0200 Subject: [PATCH 074/586] interchaintest cntd --- contracts/swap-covenant/src/msg.rs | 2 +- .../tests/interchaintest/tokenswap_test.go | 158 +++++++++++++++++- swap-covenant/tests/interchaintest/types.go | 117 ++++++------- 3 files changed, 204 insertions(+), 73 deletions(-) diff --git a/contracts/swap-covenant/src/msg.rs b/contracts/swap-covenant/src/msg.rs index 5e29f156..cd9a2363 100644 --- a/contracts/swap-covenant/src/msg.rs +++ b/contracts/swap-covenant/src/msg.rs @@ -2,7 +2,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Uint128, Uint64}; use covenant_clock::msg::PresetClockFields; use covenant_swap_holder::msg::PresetSwapHolderFields; -use covenant_utils::{SwapCovenantTerms, CovenantParty, CovenantPartiesConfig}; +use covenant_utils::SwapCovenantTerms; use neutron_sdk::bindings::msg::IbcFee; const NEUTRON_DENOM: &str = "untrn"; diff --git a/swap-covenant/tests/interchaintest/tokenswap_test.go b/swap-covenant/tests/interchaintest/tokenswap_test.go index 1ef39a47..44e3a37a 100644 --- a/swap-covenant/tests/interchaintest/tokenswap_test.go +++ b/swap-covenant/tests/interchaintest/tokenswap_test.go @@ -2,11 +2,13 @@ package ibc_test import ( "context" + "encoding/json" "fmt" "strconv" "testing" "time" + "github.com/cosmos/cosmos-sdk/crypto/keyring" ibctest "github.com/strangelove-ventures/interchaintest/v4" "github.com/strangelove-ventures/interchaintest/v4/chain/cosmos" "github.com/strangelove-ventures/interchaintest/v4/ibc" @@ -275,8 +277,12 @@ func TestTokenSwap(t *testing.T) { //----------------------------------------------// // Testing parameters //----------------------------------------------// + + // PARTY_A const osmoContributionAmount uint64 = 100_000_000_000 // in uosmo - const atomContributionAmount uint64 = 5_000_000_000 // in uatom + + // PARTY_B + const atomContributionAmount uint64 = 5_000_000_000 // in uatom //----------------------------------------------// // Wasm code that we need to store on Neutron @@ -295,11 +301,13 @@ func TestTokenSwap(t *testing.T) { var splitterCodeId uint64 var ibcForwarderCodeId uint64 var swapHolderCodeId uint64 + var covenantCodeIdStr string var covenantCodeId uint64 + _ = covenantCodeId t.Run("deploy covenant contracts", func(t *testing.T) { // store covenant and get code id - covenantCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, covenantContractPath) + covenantCodeIdStr, err = cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, covenantContractPath) require.NoError(t, err, "failed to store stride covenant contract") covenantCodeId, err = strconv.ParseUint(covenantCodeIdStr, 10, 64) require.NoError(t, err, "failed to parse codeId into uint64") @@ -334,9 +342,151 @@ func TestTokenSwap(t *testing.T) { swapHolderCodeId, err = strconv.ParseUint(swapHolderCodeIdStr, 10, 64) require.NoError(t, err, "failed to parse codeId into uint64") }) - println(clockCodeId, routerCodeId, splitterCodeId, ibcForwarderCodeId, swapHolderCodeId, covenantCodeId) + println(clockCodeId, routerCodeId, splitterCodeId, ibcForwarderCodeId, swapHolderCodeId, covenantCodeIdStr) t.Run("instantiate covenant", func(t *testing.T) { - // todo + + // Clock instantiation message + clockMsg := PresetClockFields{ + ClockCode: clockCodeId, + Label: "covenant-clock", + Whitelist: []string{}, + } + + presetIbcFee := PresetIbcFee{ + AckFee: "1000", + TimeoutFee: "1000", + } + + timeouts := Timeouts{ + IcaTimeout: "10", // sec + IbcTransferTimeout: "5", // sec + } + + swapCovenantTerms := SwapCovenantTerms{ + PartyAAmount: strconv.FormatUint(atomContributionAmount, 10), + PartyBAmount: strconv.FormatUint(osmoContributionAmount, 10), + } + + covenantPartiesConfig := CovenantPartiesConfig{ + PartyA: CovenantParty{ + Addr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + ProvidedDenom: "uatom", + ReceiverConfig: ReceiverConfig{ + Native: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + }, + }, + PartyB: CovenantParty{ + Addr: neutronUser.Bech32Address(cosmosNeutron.Config().Bech32Prefix), + ProvidedDenom: "untrn", + ReceiverConfig: ReceiverConfig{ + Native: neutronUser.Bech32Address(cosmosNeutron.Config().Bech32Prefix), + }, + }, + } + timestamp := Timestamp("1000000") + + lockupConfig := LockupConfig{ + Time: ×tamp, + } + + presetSwapHolder := PresetSwapHolderFields{ + LockupConfig: lockupConfig, + CovenantPartiesConfig: covenantPartiesConfig, + CovenantTerms: CovenantTerms{ + TokenSwap: swapCovenantTerms, + }, + CodeId: clockCodeId, + Label: "swap-holder", + } + + swapCovenantParties := SwapCovenantParties{ + PartyA: SwapPartyConfig{ + Addr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + ProvidedDenom: "uatom", + PartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], + PartyReceiverAddr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + PartyChainConnectionId: neutronAtomIBCConnId, + IbcTransferTimeout: timeouts.IbcTransferTimeout, + }, + PartyB: SwapPartyConfig{ + Addr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + ProvidedDenom: "uosmo", + PartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], + PartyReceiverAddr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + PartyChainConnectionId: neutronOsmosisIBCConnId, + IbcTransferTimeout: timeouts.IbcTransferTimeout, + }, + } + covenantMsg := CovenantInstantiateMsg{ + Label: "swap-covenant", + PresetIbcFee: presetIbcFee, + Timeouts: timeouts, + IbcForwarderCode: ibcForwarderCodeId, + InterchainRouterCode: routerCodeId, + PresetClock: clockMsg, + PresetSwapHolder: presetSwapHolder, + SwapCovenantTerms: swapCovenantTerms, + SwapCovenantParties: swapCovenantParties, + } + + str, err := json.Marshal(covenantMsg) + require.NoError(t, err, "Failed to marshall CovenantInstantiateMsg") + println("covenant instantiation msg: ", string(str)) + + cmd := []string{"neutrond", "tx", "wasm", "instantiate", covenantCodeIdStr, + string(str), + "--label", "swap-covenant", + "--no-admin", + "--from", neutronUser.KeyName, + "--output", "json", + "--home", "/var/cosmos-chain/neutron-2", + "--node", neutron.GetRPCAddress(), + "--chain-id", neutron.Config().ChainID, + "--gas", "900000", + "--keyring-backend", keyring.BackendTest, + "-y", + } + + resp, _, err := cosmosNeutron.Exec(ctx, cmd, nil) + require.NoError(t, err, err) + + println("raw resp: ", string(resp)) + println("raw err: ", err) + + require.NoError(t, testutil.WaitForBlocks(ctx, 150, cosmosNeutron)) + + jsonResp, _ := json.Marshal(resp) + jsonError, _ := json.Marshal(err) + println("instantiate response: ", string(jsonResp), "\n") + println("instantiate error: ", string(jsonError), "\n") + + queryCmd := []string{"neutrond", "query", "wasm", "list-contract-by-code", covenantCodeIdStr} + + queryResp, _, _ := cosmosNeutron.Exec(ctx, queryCmd, nil) + + type QueryContractResponse struct { + Contracts []string `json:"contracts"` + } + + println("query response: ", queryResp) + println("query response: ", string(queryResp)) + + contactsRes := QueryContractResponse{} + require.NoError(t, json.Unmarshal(queryResp, &contactsRes)) + + contractAddress := contactsRes.Contracts[len(contactsRes.Contracts)-1] + + println("covenant address: ", contractAddress) + // covenantContractAddress, err := cosmosNeutron.InstantiateContract( + // ctx, + // neutronUser.KeyName, + // covenantCodeIdStr, + // string(str), + // true, + // ) + + // require.NoError(t, err, "failed to instantiate contract: ", err) + // println("\n covenant address: ", covenantContractAddress) }) }) } diff --git a/swap-covenant/tests/interchaintest/types.go b/swap-covenant/tests/interchaintest/types.go index 59ed1b3e..1a946f9c 100644 --- a/swap-covenant/tests/interchaintest/types.go +++ b/swap-covenant/tests/interchaintest/types.go @@ -5,25 +5,16 @@ package ibc_test ////////////////////////////////////////////// // ----- Covenant Instantiation ------ - -type PresetLsFields struct { - LsCode uint64 `json:"ls_code"` - Label string `json:"label"` - LsDenom string `json:"ls_denom"` - StrideNeutronIBCTransferChannelId string `json:"stride_neutron_ibc_transfer_channel_id"` - NeutronStrideIBCConnectionId string `json:"neutron_stride_ibc_connection_id"` -} - type CovenantInstantiateMsg struct { - Label string `json:"label"` - PresetClock PresetClockFields `json:"preset_clock_fields"` - PresetLs PresetLsFields `json:"preset_ls_fields"` - PresetDepositor PresetDepositorFields `json:"preset_depositor_fields"` - PresetLp PresetLpFields `json:"preset_lp_fields"` - PresetHolder PresetHolderFields `json:"preset_holder_fields"` - PoolAddress string `json:"pool_address"` - PresetIbcFee PresetIbcFee `json:"preset_ibc_fee"` - Timeouts Timeouts `json:"timeouts"` + Label string `json:"label"` + PresetIbcFee PresetIbcFee `json:"preset_ibc_fee"` + Timeouts Timeouts `json:"timeouts"` + IbcForwarderCode uint64 `json:"ibc_forwarder_code"` + InterchainRouterCode uint64 `json:"interchain_router_code"` + PresetClock PresetClockFields `json:"preset_clock_fields"` + PresetSwapHolder PresetSwapHolderFields `json:"preset_holder_fields"` + SwapCovenantTerms SwapCovenantTerms `json:"covenant_terms"` + SwapCovenantParties SwapCovenantParties `json:"covenant_parties"` } type Timeouts struct { @@ -43,75 +34,65 @@ type PresetClockFields struct { Whitelist []string `json:"whitelist"` } -type PresetHolderFields struct { - Withdrawer string `json:"withdrawer,omitempty"` - HolderCode uint64 `json:"holder_code"` - Label string `json:"label"` +type PresetSwapHolderFields struct { + LockupConfig LockupConfig `json:"lockup_config"` + CovenantPartiesConfig CovenantPartiesConfig `json:"parties_config"` + CovenantTerms CovenantTerms `json:"covenant_terms"` + CodeId uint64 `json:"code_id"` + Label string `json:"label"` } -type PresetDepositorFields struct { - GaiaNeutronIBCTransferChannelId string `json:"gaia_neutron_ibc_transfer_channel_id"` - NeutronGaiaConnectionId string `json:"neutron_gaia_connection_id"` - GaiaStrideIBCTransferChannelId string `json:"gaia_stride_ibc_transfer_channel_id"` - DepositorCode uint64 `json:"depositor_code"` - Label string `json:"label"` - StAtomReceiverAmount WeightedReceiverAmount `json:"st_atom_receiver_amount"` - AtomReceiverAmount WeightedReceiverAmount `json:"atom_receiver_amount"` - AutopilotFormat string `json:"autopilot_format"` - NeutronAtomIbcDenom string `json:"neutron_atom_ibc_denom"` -} +type Timestamp string -type PresetLpFields struct { - SlippageTolerance string `json:"slippage_tolerance,omitempty"` - Autostake bool `json:"autostake,omitempty"` - Assets AssetData `json:"assets"` - LpCode uint64 `json:"lp_code"` - Label string `json:"label"` - SingleSideLpLimits SingleSideLpLimits `json:"single_side_lp_limits"` - ExpectedLsTokenAmount string `json:"expected_ls_token_amount"` - AllowedReturnDelta string `json:"allowed_return_delta"` - ExpectedNativeTokenAmount string `json:"expected_native_token_amount"` +type LockupConfig struct { + None bool `json:"none,omitempty"` + Block *uint64 `json:"block,omitempty"` + Time *Timestamp `json:"time,omitempty"` } -type SingleSideLpLimits struct { - NativeAssetLimit string `json:"native_asset_limit"` - LsAssetLimit string `json:"ls_asset_limit"` +type CovenantPartiesConfig struct { + PartyA CovenantParty `json:"party_a"` + PartyB CovenantParty `json:"party_b"` } -type AssetData struct { - NativeAssetDenom string `json:"native_asset_denom"` - LsAssetDenom string `json:"ls_asset_denom"` +type SwapCovenantParties struct { + PartyA SwapPartyConfig `json:"party_a"` + PartyB SwapPartyConfig `json:"party_b"` } -// ----- Covenant Queries ------ - -type DepositorAddress struct{} -type DepositorAddressQuery struct { - DepositorAddress DepositorAddress `json:"depositor_address"` +type CovenantParty struct { + Addr string `json:"addr"` + ProvidedDenom string `json:"provided_denom"` + ReceiverConfig ReceiverConfig `json:"receiver_config"` } -type ClockAddress struct{} -type ClockAddressQuery struct { - ClockAddress ClockAddress `json:"clock_address"` +type SwapPartyConfig struct { + Addr string `json:"addr"` + ProvidedDenom string `json:"provided_denom"` + PartyChainChannelId string `json:"party_chain_channel_id"` + PartyReceiverAddr string `json:"party_receiver_addr"` + PartyChainConnectionId string `json:"party_chain_connection_id"` + IbcTransferTimeout string `json:"ibc_transfer_timeout"` } -type HolderAddress struct{} -type HolderAddressQuery struct { - HolderAddress HolderAddress `json:"holder_address"` +type ReceiverConfig struct { + Native string `json:"native"` } -type LsAddress struct{} -type LsAddressQuery struct { - LsAddress LsAddress `json:"ls_address"` +type SwapCovenantTerms struct { + PartyAAmount string `json:"party_a_amount"` + PartyBAmount string `json:"party_b_amount"` } -type LpAddress struct{} -type LpAddressQuery struct { - LpAddress LpAddress `json:"lp_address"` +type CovenantTerms struct { + TokenSwap SwapCovenantTerms `json:"token_swap,omitempty"` } -type CovenantAddressQueryResponse struct { - Data string `json:"data"` +// ----- Covenant Queries ------ + +type ClockAddress struct{} +type ClockAddressQuery struct { + ClockAddress ClockAddress `json:"clock_address"` } type ContractState struct{} From edcd9afa8ec6bbc767497e2fc289a394c905194a Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 11 Sep 2023 19:38:00 +0200 Subject: [PATCH 075/586] covenant instantiate & query addr via contract code --- contracts/swap-covenant/src/contract.rs | 96 +++++------ contracts/swap-covenant/src/msg.rs | 3 +- contracts/swap-covenant/src/state.rs | 6 +- .../swap-covenant/src/suite_test/suite.rs | 2 +- .../tests/interchaintest/tokenswap_test.go | 152 +++++++++++------- swap-covenant/tests/interchaintest/types.go | 19 +-- 6 files changed, 156 insertions(+), 122 deletions(-) diff --git a/contracts/swap-covenant/src/contract.rs b/contracts/swap-covenant/src/contract.rs index 383ac5ca..30905541 100644 --- a/contracts/swap-covenant/src/contract.rs +++ b/contracts/swap-covenant/src/contract.rs @@ -11,14 +11,14 @@ use cw_utils::{parse_reply_instantiate_data, ParseReplyError}; use crate::{ error::ContractError, state::{ - CLOCK_CODE, TIMEOUTS, COVENANT_CLOCK_ADDR, SWAP_HOLDER_CODE, PRESET_HOLDER_FIELDS, COVENANT_INTERCHAIN_SPLITTER_ADDR, INTECHAIN_SPLITTER_CODE, PRESET_SPLITTER_FIELDS, COVENANT_SWAP_HOLDER_ADDR, IBC_FORWARDER_CODE, IBC_FEE, COVENANT_PARTIES, COVENANT_TERMS, PARTY_A_IBC_FORWARDER_ADDR, PARTY_B_IBC_FORWARDER_ADDR, PARTY_A_INTERCHAIN_ROUTER_ADDR, INTERCHAIN_ROUTER_CODE, PARTY_B_INTERCHAIN_ROUTER_ADDR, + CLOCK_CODE, TIMEOUTS, COVENANT_CLOCK_ADDR, SWAP_HOLDER_CODE, PRESET_HOLDER_FIELDS, COVENANT_INTERCHAIN_SPLITTER_ADDR, PRESET_SPLITTER_FIELDS, COVENANT_SWAP_HOLDER_ADDR, IBC_FORWARDER_CODE, IBC_FEE, COVENANT_PARTIES, PARTY_A_IBC_FORWARDER_ADDR, PARTY_B_IBC_FORWARDER_ADDR, PARTY_A_INTERCHAIN_ROUTER_ADDR, INTERCHAIN_ROUTER_CODE, PARTY_B_INTERCHAIN_ROUTER_ADDR, INTERCHAIN_SPLITTER_CODE, }, msg::InstantiateMsg, }; const CONTRACT_NAME: &str = "crates.io:swap-covenant"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -pub(crate) const CLOCK_REPLY_ID: u64 = 1u64; +pub const CLOCK_REPLY_ID: u64 = 1u64; pub const SPLITTER_REPLY_ID: u64 = 2u64; pub const SWAP_HOLDER_REPLY_ID: u64 = 3u64; pub const PARTY_A_FORWARDER_REPLY_ID: u64 = 4u64; @@ -38,29 +38,29 @@ pub fn instantiate( set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; // store all the codes for covenant configuration - CLOCK_CODE.save(deps.storage, &msg.preset_clock_fields.clock_code)?; - SWAP_HOLDER_CODE.save(deps.storage, &msg.preset_holder_fields.code_id)?; - IBC_FORWARDER_CODE.save(deps.storage, &msg.ibc_forwarder_code)?; - PRESET_HOLDER_FIELDS.save(deps.storage, &msg.preset_holder_fields)?; - COVENANT_PARTIES.save(deps.storage, &msg.covenant_parties)?; - COVENANT_TERMS.save(deps.storage, &msg.covenant_terms)?; - TIMEOUTS.save(deps.storage, &msg.timeouts)?; - + // CLOCK_CODE.save(deps.storage, &msg.preset_clock_fields.clock_code)?; + // SWAP_HOLDER_CODE.save(deps.storage, &msg.preset_holder_fields.code_id)?; + // IBC_FORWARDER_CODE.save(deps.storage, &msg.ibc_forwarder_code)?; + // PRESET_HOLDER_FIELDS.save(deps.storage, &msg.preset_holder_fields)?; + // COVENANT_PARTIES.save(deps.storage, &msg.covenant_parties)?; + // TIMEOUTS.save(deps.storage, &msg.timeouts)?; + // INTERCHAIN_SPLITTER_CODE.save(deps.storage, &msg.splitter_code)?; + // we start the module instantiation chain with the clock - let clock_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { - admin: Some(env.contract.address.to_string()), - code_id: msg.preset_clock_fields.clock_code, - msg: to_binary(&msg.preset_clock_fields.clone().to_instantiate_msg())?, - funds: vec![], - label: msg.preset_clock_fields.label, - }); + // let clock_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { + // admin: Some(env.contract.address.to_string()), + // code_id: msg.preset_clock_fields.clock_code, + // msg: to_binary(&msg.preset_clock_fields.clone().to_instantiate_msg())?, + // funds: vec![], + // label: msg.preset_clock_fields.label, + // }); Ok(Response::default() .add_attribute("method", "instantiate") - .add_submessage(SubMsg::reply_on_success( - clock_instantiate_tx, - CLOCK_REPLY_ID, - )) + // .add_submessage(SubMsg::reply_on_success( + // clock_instantiate_tx, + // CLOCK_REPLY_ID, + // )) ) } @@ -68,12 +68,12 @@ pub fn instantiate( pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { match msg.id { CLOCK_REPLY_ID => handle_clock_reply(deps, env, msg), - SPLITTER_REPLY_ID => handle_splitter_reply(deps, env, msg), - SWAP_HOLDER_REPLY_ID => handle_swap_holder_reply(deps, env, msg), - PARTY_A_FORWARDER_REPLY_ID => handle_party_a_ibc_forwarder_reply(deps, env, msg), - PARTY_B_FORWARDER_REPLY_ID => handle_party_b_ibc_forwarder_reply(deps, env, msg), - PARTY_A_INTERCHAIN_ROUTER_REPLY_ID => handle_party_a_interchain_router_reply(deps, env, msg), - PARTY_B_INTERCHAIN_ROUTER_REPLY_ID => handle_party_b_interchain_router_reply(deps, env, msg), + // SPLITTER_REPLY_ID => handle_splitter_reply(deps, env, msg), + // SWAP_HOLDER_REPLY_ID => handle_swap_holder_reply(deps, env, msg), + // PARTY_A_FORWARDER_REPLY_ID => handle_party_a_ibc_forwarder_reply(deps, env, msg), + // PARTY_B_FORWARDER_REPLY_ID => handle_party_b_ibc_forwarder_reply(deps, env, msg), + // PARTY_A_INTERCHAIN_ROUTER_REPLY_ID => handle_party_a_interchain_router_reply(deps, env, msg), + // PARTY_B_INTERCHAIN_ROUTER_REPLY_ID => handle_party_b_interchain_router_reply(deps, env, msg), _ => Err(ContractError::UnknownReplyId {}), } } @@ -93,25 +93,25 @@ pub fn handle_clock_reply(deps: DepsMut, env: Env, msg: Reply) -> Result Err(ContractError::ContractInstantiationError { contract: "clock".to_string(), @@ -179,7 +179,7 @@ pub fn handle_party_b_interchain_router_reply(deps: DepsMut, env: Env, msg: Repl // load the fields relevant to splitter - let code_id = INTECHAIN_SPLITTER_CODE.load(deps.storage)?; + let code_id = INTERCHAIN_SPLITTER_CODE.load(deps.storage)?; let preset_splitter_fields = PRESET_SPLITTER_FIELDS.load(deps.storage)?; let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; let party_a_router = PARTY_A_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; @@ -275,7 +275,9 @@ pub fn handle_swap_holder_reply(deps: DepsMut, env: Env, msg: Reply) -> Result terms, + }; let instantiate_msg = covenant_ibc_forwarder::msg::InstantiateMsg { clock_address: clock_addr.to_string(), @@ -327,7 +329,9 @@ pub fn handle_party_a_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) - let timeouts = TIMEOUTS.load(deps.storage)?; let ibc_fee = IBC_FEE.load(deps.storage)?; let covenant_party = COVENANT_PARTIES.load(deps.storage)?.party_b; - let covenant_terms = COVENANT_TERMS.load(deps.storage)?; + let covenant_terms = match PRESET_HOLDER_FIELDS.load(deps.storage)?.covenant_terms { + covenant_utils::CovenantTerms::TokenSwap(terms) => terms, + }; let swap_holder = COVENANT_SWAP_HOLDER_ADDR.load(deps.storage)?; let instantiate_msg = covenant_ibc_forwarder::msg::InstantiateMsg { diff --git a/contracts/swap-covenant/src/msg.rs b/contracts/swap-covenant/src/msg.rs index cd9a2363..1b06a5b9 100644 --- a/contracts/swap-covenant/src/msg.rs +++ b/contracts/swap-covenant/src/msg.rs @@ -2,7 +2,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Uint128, Uint64}; use covenant_clock::msg::PresetClockFields; use covenant_swap_holder::msg::PresetSwapHolderFields; -use covenant_utils::SwapCovenantTerms; use neutron_sdk::bindings::msg::IbcFee; const NEUTRON_DENOM: &str = "untrn"; @@ -19,13 +18,13 @@ pub struct InstantiateMsg { pub ibc_forwarder_code: u64, pub interchain_router_code: u64, + pub splitter_code: u64, /// instantiation fields relevant to clock module known in advance pub preset_clock_fields: PresetClockFields, /// instantiation fields relevant to swap holder contract known in advance pub preset_holder_fields: PresetSwapHolderFields, - pub covenant_terms: SwapCovenantTerms, pub covenant_parties: SwapCovenantParties, } diff --git a/contracts/swap-covenant/src/state.rs b/contracts/swap-covenant/src/state.rs index 88b8aaa9..ce594875 100644 --- a/contracts/swap-covenant/src/state.rs +++ b/contracts/swap-covenant/src/state.rs @@ -2,7 +2,7 @@ use cosmwasm_std::Addr; use covenant_interchain_splitter::msg::PresetInterchainSplitterFields; use covenant_swap_holder::msg::PresetSwapHolderFields; -use covenant_utils::{CovenantPartiesConfig, SwapCovenantTerms}; +use covenant_utils::SwapCovenantTerms; use cw_storage_plus::Item; use neutron_sdk::bindings::msg::IbcFee; @@ -11,7 +11,7 @@ use crate::msg::{Timeouts, SwapCovenantParties}; /// contract code for the ibc forwarder pub const IBC_FORWARDER_CODE: Item = Item::new("ibc_forwarder_code"); /// contract code for the interchain splitter -pub const INTECHAIN_SPLITTER_CODE: Item = Item::new("interchain_splitter"); +pub const INTERCHAIN_SPLITTER_CODE: Item = Item::new("interchain_splitter_code"); /// contract code for the swap holder pub const SWAP_HOLDER_CODE: Item = Item::new("swap_holder_code"); /// contract code for the clock module @@ -42,7 +42,7 @@ pub const COVENANT_SWAP_HOLDER_ADDR: Item = Item::new("covenant_swap_holde pub const COVENANT_PARTIES: Item = Item::new("covenant_parties"); -pub const COVENANT_TERMS: Item = Item::new("swap_covenant_terms"); +// pub const COVENANT_TERMS: Item = Item::new("swap_covenant_terms"); pub const PARTY_A_IBC_FORWARDER_ADDR: Item = Item::new("party_a_ibc_forwarder_addr"); pub const PARTY_B_IBC_FORWARDER_ADDR: Item = Item::new("party_b_ibc_forwarder_addr"); diff --git a/contracts/swap-covenant/src/suite_test/suite.rs b/contracts/swap-covenant/src/suite_test/suite.rs index 1094244f..fb7d3c3a 100644 --- a/contracts/swap-covenant/src/suite_test/suite.rs +++ b/contracts/swap-covenant/src/suite_test/suite.rs @@ -36,10 +36,10 @@ impl Default for SuiteBuilder { timeouts: todo!(), preset_clock_fields: todo!(), preset_holder_fields: todo!(), - covenant_terms: todo!(), ibc_forwarder_code: todo!(), covenant_parties: todo!(), interchain_router_code: todo!(), + splitter_code: todo!(), }, } } diff --git a/swap-covenant/tests/interchaintest/tokenswap_test.go b/swap-covenant/tests/interchaintest/tokenswap_test.go index 44e3a37a..3a27edbe 100644 --- a/swap-covenant/tests/interchaintest/tokenswap_test.go +++ b/swap-covenant/tests/interchaintest/tokenswap_test.go @@ -47,7 +47,7 @@ func TestTokenSwap(t *testing.T) { // Chain Factory cf := ibctest.NewBuiltinChainFactory(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel)), []*ibctest.ChainSpec{ {Name: "gaia", Version: "v9.1.0", ChainConfig: ibc.ChainConfig{ - GasAdjustment: 1.5, + GasAdjustment: 1.3, GasPrices: "0.0atom", ModifyGenesis: setupGaiaGenesis([]string{ "/cosmos.bank.v1beta1.MsgSend", @@ -86,8 +86,28 @@ func TestTokenSwap(t *testing.T) { ConfigFileOverrides: configFileOverrides, }, }, - - {Name: "osmosis", Version: "v11.0.0"}, + { + Name: "osmosis", + Version: "v11.0.0", + ChainConfig: ibc.ChainConfig{ + Type: "cosmos", + Bin: "osmosisd", + Bech32Prefix: "osmo", + Denom: "uosmo", + GasPrices: "0.0uosmo", + GasAdjustment: 1.3, + Images: []ibc.DockerImage{ + { + Repository: "ghcr.io/strangelove-ventures/heighliner/osmosis", + Version: "v11.0.0", + UidGid: "1025:1025", + }, + }, + TrustingPeriod: "336h", + NoHostMount: false, + ConfigFileOverrides: configFileOverrides, + }, + }, }) chains, err := cf.Chains(t.Name()) @@ -103,7 +123,7 @@ func TestTokenSwap(t *testing.T) { ibc.CosmosRly, zaptest.NewLogger(t), relayer.CustomDockerImage("ghcr.io/cosmos/relayer", "v2.3.1", rly.RlyDefaultUidGid), - relayer.RelayerOptionExtraStartFlags{Flags: []string{"-d", "--log-format", "console"}}, + relayer.RelayerOptionExtraStartFlags{Flags: []string{"-p", "events", "-b", "100", "-d", "--log-format", "console"}}, ).Build(t, client, network) // Prep Interchain @@ -129,6 +149,12 @@ func TestTokenSwap(t *testing.T) { Chain2: cosmosOsmosis, Relayer: r, Path: neutronOsmosisIBCPath, + }). + AddLink(ibctest.InterchainLink{ + Chain1: cosmosAtom, + Chain2: cosmosOsmosis, + Relayer: r, + Path: gaiaOsmosisIBCPath, }) // Log location @@ -151,12 +177,6 @@ func TestTokenSwap(t *testing.T) { err = testutil.WaitForBlocks(ctx, 10, atom, neutron, osmosis) require.NoError(t, err, "failed to wait for blocks") - users := ibctest.GetAndFundTestUsers(t, ctx, "default", int64(500_000_000_000), osmosis) - osmoUser := users[0] - osmoUserBalInitial, err := osmosis.GetBalance(ctx, osmoUser.Bech32Address(osmosis.Config().Bech32Prefix), osmosis.Config().Denom) - require.NoError(t, err) - require.Equal(t, int64(500_000_000_000), osmoUserBalInitial) - testCtx := &TestContext{ OsmoClients: []*ibc.ClientOutput{}, GaiaClients: []*ibc.ClientOutput{}, @@ -206,10 +226,8 @@ func TestTokenSwap(t *testing.T) { linkPath(t, ctx, r, eRep, cosmosAtom, cosmosNeutron, gaiaNeutronIBCPath) // Start the relayer and clean it up when the test ends. - require.NoError(t, - r.StartRelayer(ctx, eRep, gaiaNeutronICSPath, gaiaNeutronIBCPath, gaiaOsmosisIBCPath, neutronOsmosisIBCPath), - "failed to start relayer with given paths", - ) + err = r.StartRelayer(ctx, eRep, gaiaNeutronICSPath, gaiaNeutronIBCPath, gaiaOsmosisIBCPath, neutronOsmosisIBCPath) + require.NoError(t, err, "failed to start relayer with given paths") t.Cleanup(func() { err = r.StopRelayer(ctx, eRep) if err != nil { @@ -221,14 +239,16 @@ func TestTokenSwap(t *testing.T) { require.NoError(t, err, "failed to wait for blocks") createValidator(t, ctx, r, eRep, atom, neutron) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") // Once the VSC packet has been relayed, x/bank transfers are // enabled on Neutron and we can fund its account. // The funds for this are sent from a "faucet" account created // by interchaintest in the genesis file. - users = ibctest.GetAndFundTestUsers(t, ctx, "default", int64(500_000_000_000), atom, neutron) - gaiaUser, neutronUser := users[0], users[1] - _, _ = gaiaUser, neutronUser + users := ibctest.GetAndFundTestUsers(t, ctx, "default", int64(500_000_000_000), atom, neutron, osmosis) + gaiaUser, neutronUser, osmoUser := users[0], users[1], users[2] + _, _, _ = gaiaUser, neutronUser, osmoUser err = testutil.WaitForBlocks(ctx, 10, atom, neutron, osmosis) require.NoError(t, err, "failed to wait for blocks") @@ -341,8 +361,11 @@ func TestTokenSwap(t *testing.T) { require.NoError(t, err, "failed to store swap holder contract") swapHolderCodeId, err = strconv.ParseUint(swapHolderCodeIdStr, 10, 64) require.NoError(t, err, "failed to parse codeId into uint64") + }) - println(clockCodeId, routerCodeId, splitterCodeId, ibcForwarderCodeId, swapHolderCodeId, covenantCodeIdStr) + println(covenantCodeIdStr, clockCodeId, routerCodeId, splitterCodeId, ibcForwarderCodeId, swapHolderCodeId) + require.NoError(t, testutil.WaitForBlocks(ctx, 10, cosmosNeutron, cosmosAtom, cosmosOsmosis)) + t.Run("instantiate covenant", func(t *testing.T) { // Clock instantiation message @@ -388,15 +411,16 @@ func TestTokenSwap(t *testing.T) { lockupConfig := LockupConfig{ Time: ×tamp, } + covenantTerms := CovenantTerms{ + TokenSwap: swapCovenantTerms, + } presetSwapHolder := PresetSwapHolderFields{ LockupConfig: lockupConfig, CovenantPartiesConfig: covenantPartiesConfig, - CovenantTerms: CovenantTerms{ - TokenSwap: swapCovenantTerms, - }, - CodeId: clockCodeId, - Label: "swap-holder", + CovenantTerms: covenantTerms, + CodeId: swapHolderCodeId, + Label: "swap-holder", } swapCovenantParties := SwapCovenantParties{ @@ -418,23 +442,38 @@ func TestTokenSwap(t *testing.T) { }, } covenantMsg := CovenantInstantiateMsg{ - Label: "swap-covenant", - PresetIbcFee: presetIbcFee, - Timeouts: timeouts, - IbcForwarderCode: ibcForwarderCodeId, - InterchainRouterCode: routerCodeId, - PresetClock: clockMsg, - PresetSwapHolder: presetSwapHolder, - SwapCovenantTerms: swapCovenantTerms, - SwapCovenantParties: swapCovenantParties, + Label: "swap-covenant", + PresetIbcFee: presetIbcFee, + Timeouts: timeouts, + IbcForwarderCode: ibcForwarderCodeId, + InterchainRouterCode: routerCodeId, + InterchainSplitterCode: splitterCodeId, + PresetClock: clockMsg, + PresetSwapHolder: presetSwapHolder, + SwapCovenantParties: swapCovenantParties, } str, err := json.Marshal(covenantMsg) require.NoError(t, err, "Failed to marshall CovenantInstantiateMsg") println("covenant instantiation msg: ", string(str)) + instantiateMsg := string(str) + + // covenantContractAddress, err := cosmosNeutron.InstantiateContract( + // ctx, + // neutronUser.KeyName, + // covenantCodeIdStr, + // instantiateCmd, + // true, + // ) + // if err != nil { + // println("error: ", err) + // } else { + // println("no error: ", covenantContractAddress) + // } + // require.NoError(t, testutil.WaitForBlocks(ctx, 100, atom, neutron, osmosis)) cmd := []string{"neutrond", "tx", "wasm", "instantiate", covenantCodeIdStr, - string(str), + instantiateMsg, "--label", "swap-covenant", "--no-admin", "--from", neutronUser.KeyName, @@ -442,51 +481,42 @@ func TestTokenSwap(t *testing.T) { "--home", "/var/cosmos-chain/neutron-2", "--node", neutron.GetRPCAddress(), "--chain-id", neutron.Config().ChainID, - "--gas", "900000", + "--gas", "9000000", "--keyring-backend", keyring.BackendTest, "-y", } - resp, _, err := cosmosNeutron.Exec(ctx, cmd, nil) - require.NoError(t, err, err) - - println("raw resp: ", string(resp)) - println("raw err: ", err) + resp, _, err := neutron.Exec(ctx, cmd, nil) + require.NoError(t, err) + println("instantiated, skipping 10 blocks...") + require.NoError(t, testutil.WaitForBlocks(ctx, 10, atom, neutron, osmosis)) - require.NoError(t, testutil.WaitForBlocks(ctx, 150, cosmosNeutron)) + println("instantiate response: ", string(resp), "\n") - jsonResp, _ := json.Marshal(resp) - jsonError, _ := json.Marshal(err) - println("instantiate response: ", string(jsonResp), "\n") - println("instantiate error: ", string(jsonError), "\n") - - queryCmd := []string{"neutrond", "query", "wasm", "list-contract-by-code", covenantCodeIdStr} + queryCmd := []string{"neutrond", "query", "wasm", + "list-contract-by-code", covenantCodeIdStr, + "--output", "json", + "--home", neutron.HomeDir(), + "--node", neutron.GetRPCAddress(), + "--chain-id", neutron.Config().ChainID, + } - queryResp, _, _ := cosmosNeutron.Exec(ctx, queryCmd, nil) + queryResp, _, err := neutron.Exec(ctx, queryCmd, nil) + require.NoError(t, err, "failed to query") + println("query response: ", string(queryResp)) type QueryContractResponse struct { - Contracts []string `json:"contracts"` + Contracts []string `json:"contracts"` + Pagination any `json:"pagination"` } - println("query response: ", queryResp) - println("query response: ", string(queryResp)) - contactsRes := QueryContractResponse{} - require.NoError(t, json.Unmarshal(queryResp, &contactsRes)) + require.NoError(t, json.Unmarshal(queryResp, &contactsRes), "failed to unmarshal contract response") contractAddress := contactsRes.Contracts[len(contactsRes.Contracts)-1] println("covenant address: ", contractAddress) - // covenantContractAddress, err := cosmosNeutron.InstantiateContract( - // ctx, - // neutronUser.KeyName, - // covenantCodeIdStr, - // string(str), - // true, - // ) - // require.NoError(t, err, "failed to instantiate contract: ", err) - // println("\n covenant address: ", covenantContractAddress) }) }) } diff --git a/swap-covenant/tests/interchaintest/types.go b/swap-covenant/tests/interchaintest/types.go index 1a946f9c..8ed6df88 100644 --- a/swap-covenant/tests/interchaintest/types.go +++ b/swap-covenant/tests/interchaintest/types.go @@ -6,15 +6,16 @@ package ibc_test // ----- Covenant Instantiation ------ type CovenantInstantiateMsg struct { - Label string `json:"label"` - PresetIbcFee PresetIbcFee `json:"preset_ibc_fee"` - Timeouts Timeouts `json:"timeouts"` - IbcForwarderCode uint64 `json:"ibc_forwarder_code"` - InterchainRouterCode uint64 `json:"interchain_router_code"` - PresetClock PresetClockFields `json:"preset_clock_fields"` - PresetSwapHolder PresetSwapHolderFields `json:"preset_holder_fields"` - SwapCovenantTerms SwapCovenantTerms `json:"covenant_terms"` - SwapCovenantParties SwapCovenantParties `json:"covenant_parties"` + Label string `json:"label"` + PresetIbcFee PresetIbcFee `json:"preset_ibc_fee"` + Timeouts Timeouts `json:"timeouts"` + IbcForwarderCode uint64 `json:"ibc_forwarder_code"` + InterchainRouterCode uint64 `json:"interchain_router_code"` + InterchainSplitterCode uint64 `json:"splitter_code"` + PresetClock PresetClockFields `json:"preset_clock_fields"` + PresetSwapHolder PresetSwapHolderFields `json:"preset_holder_fields"` + // SwapCovenantTerms SwapCovenantTerms `json:"covenant_terms"` + SwapCovenantParties SwapCovenantParties `json:"covenant_parties"` } type Timeouts struct { From 7e8ad252acb2570f9b2fddd4741be8698c6e9642 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 12 Sep 2023 23:38:42 +0200 Subject: [PATCH 076/586] e2e instantiation cntd; not validating receiver addr on router --- contracts/interchain-router/src/contract.rs | 11 +- contracts/interchain-router/src/msg.rs | 2 +- contracts/swap-covenant/src/contract.rs | 179 ++++++++++-------- contracts/swap-covenant/src/msg.rs | 3 +- contracts/swap-covenant/src/state.rs | 1 + packages/covenant-utils/src/lib.rs | 2 +- .../tests/interchaintest/tokenswap_test.go | 36 +--- swap-covenant/tests/interchaintest/types.go | 1 - 8 files changed, 114 insertions(+), 121 deletions(-) diff --git a/contracts/interchain-router/src/contract.rs b/contracts/interchain-router/src/contract.rs index 56e470ea..400a091c 100644 --- a/contracts/interchain-router/src/contract.rs +++ b/contracts/interchain-router/src/contract.rs @@ -25,22 +25,23 @@ pub fn instantiate( deps.api.debug("WASMDEBUG: instantiate"); set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - let clock_addr = deps.api.addr_validate(&msg.clock_address)?; - let destination_receiver_addr = deps.api.addr_validate(&msg.destination_receiver_addr)?; + let clock_addr = deps.api.addr_validate(&msg.clock_address)?; - CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; let destination_config = DestinationConfig { destination_chain_channel_id: msg.destination_chain_channel_id.to_string(), - destination_receiver_addr, + destination_receiver_addr: msg.destination_receiver_addr.to_string(), ibc_transfer_timeout: msg.ibc_transfer_timeout, }; + CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; DESTINATION_CONFIG.save(deps.storage, &destination_config)?; Ok(Response::default() .add_attribute("method", "interchain_router_instantiate") .add_attribute("clock_address", clock_addr) - .add_attributes(destination_config.get_response_attributes())) + .add_attribute("destination_receiver_addr", msg.destination_receiver_addr) + .add_attributes(destination_config.get_response_attributes()) + ) } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/interchain-router/src/msg.rs b/contracts/interchain-router/src/msg.rs index 0e6d119c..2aa04011 100644 --- a/contracts/interchain-router/src/msg.rs +++ b/contracts/interchain-router/src/msg.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{ - Addr, Attribute, Binary, Coin, CosmosMsg, IbcMsg, IbcTimeout, Timestamp, Uint64, + Addr, Binary, Uint64, }; use covenant_macros::{clocked, covenant_clock_address}; use covenant_utils::DestinationConfig; diff --git a/contracts/swap-covenant/src/contract.rs b/contracts/swap-covenant/src/contract.rs index 30905541..147ebd06 100644 --- a/contracts/swap-covenant/src/contract.rs +++ b/contracts/swap-covenant/src/contract.rs @@ -19,12 +19,12 @@ const CONTRACT_NAME: &str = "crates.io:swap-covenant"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); pub const CLOCK_REPLY_ID: u64 = 1u64; -pub const SPLITTER_REPLY_ID: u64 = 2u64; -pub const SWAP_HOLDER_REPLY_ID: u64 = 3u64; -pub const PARTY_A_FORWARDER_REPLY_ID: u64 = 4u64; -pub const PARTY_B_FORWARDER_REPLY_ID: u64 = 5u64; -pub const PARTY_A_INTERCHAIN_ROUTER_REPLY_ID: u64 = 6u64; -pub const PARTY_B_INTERCHAIN_ROUTER_REPLY_ID: u64 = 7u64; +pub const PARTY_A_INTERCHAIN_ROUTER_REPLY_ID: u64 = 2u64; +pub const PARTY_B_INTERCHAIN_ROUTER_REPLY_ID: u64 = 3u64; +pub const SPLITTER_REPLY_ID: u64 = 4u64; +pub const SWAP_HOLDER_REPLY_ID: u64 = 5u64; +pub const PARTY_A_FORWARDER_REPLY_ID: u64 = 6u64; +pub const PARTY_B_FORWARDER_REPLY_ID: u64 = 7u64; #[cfg_attr(not(feature = "library"), entry_point)] @@ -38,29 +38,31 @@ pub fn instantiate( set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; // store all the codes for covenant configuration - // CLOCK_CODE.save(deps.storage, &msg.preset_clock_fields.clock_code)?; - // SWAP_HOLDER_CODE.save(deps.storage, &msg.preset_holder_fields.code_id)?; - // IBC_FORWARDER_CODE.save(deps.storage, &msg.ibc_forwarder_code)?; - // PRESET_HOLDER_FIELDS.save(deps.storage, &msg.preset_holder_fields)?; - // COVENANT_PARTIES.save(deps.storage, &msg.covenant_parties)?; - // TIMEOUTS.save(deps.storage, &msg.timeouts)?; - // INTERCHAIN_SPLITTER_CODE.save(deps.storage, &msg.splitter_code)?; - + CLOCK_CODE.save(deps.storage, &msg.preset_clock_fields.clock_code)?; + INTERCHAIN_SPLITTER_CODE.save(deps.storage, &msg.splitter_code)?; + INTERCHAIN_ROUTER_CODE.save(deps.storage, &msg.interchain_router_code)?; + IBC_FORWARDER_CODE.save(deps.storage, &msg.ibc_forwarder_code)?; + SWAP_HOLDER_CODE.save(deps.storage, &msg.preset_holder_fields.code_id)?; + + PRESET_HOLDER_FIELDS.save(deps.storage, &msg.preset_holder_fields)?; + COVENANT_PARTIES.save(deps.storage, &msg.covenant_parties)?; + TIMEOUTS.save(deps.storage, &msg.timeouts)?; + // we start the module instantiation chain with the clock - // let clock_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { - // admin: Some(env.contract.address.to_string()), - // code_id: msg.preset_clock_fields.clock_code, - // msg: to_binary(&msg.preset_clock_fields.clone().to_instantiate_msg())?, - // funds: vec![], - // label: msg.preset_clock_fields.label, - // }); + let clock_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.to_string()), + code_id: msg.preset_clock_fields.clock_code, + msg: to_binary(&msg.preset_clock_fields.clone().to_instantiate_msg())?, + funds: vec![], + label: msg.preset_clock_fields.label, + }); Ok(Response::default() .add_attribute("method", "instantiate") - // .add_submessage(SubMsg::reply_on_success( - // clock_instantiate_tx, - // CLOCK_REPLY_ID, - // )) + .add_submessage(SubMsg::reply_on_success( + clock_instantiate_tx, + CLOCK_REPLY_ID, + )) ) } @@ -68,12 +70,12 @@ pub fn instantiate( pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { match msg.id { CLOCK_REPLY_ID => handle_clock_reply(deps, env, msg), - // SPLITTER_REPLY_ID => handle_splitter_reply(deps, env, msg), - // SWAP_HOLDER_REPLY_ID => handle_swap_holder_reply(deps, env, msg), - // PARTY_A_FORWARDER_REPLY_ID => handle_party_a_ibc_forwarder_reply(deps, env, msg), - // PARTY_B_FORWARDER_REPLY_ID => handle_party_b_ibc_forwarder_reply(deps, env, msg), - // PARTY_A_INTERCHAIN_ROUTER_REPLY_ID => handle_party_a_interchain_router_reply(deps, env, msg), - // PARTY_B_INTERCHAIN_ROUTER_REPLY_ID => handle_party_b_interchain_router_reply(deps, env, msg), + PARTY_A_INTERCHAIN_ROUTER_REPLY_ID => handle_party_a_interchain_router_reply(deps, env, msg), + PARTY_B_INTERCHAIN_ROUTER_REPLY_ID => handle_party_b_interchain_router_reply(deps, env, msg), + SPLITTER_REPLY_ID => handle_splitter_reply(deps, env, msg), + SWAP_HOLDER_REPLY_ID => handle_swap_holder_reply(deps, env, msg), + PARTY_A_FORWARDER_REPLY_ID => handle_party_a_ibc_forwarder_reply(deps, env, msg), + PARTY_B_FORWARDER_REPLY_ID => handle_party_b_ibc_forwarder_reply(deps, env, msg), _ => Err(ContractError::UnknownReplyId {}), } } @@ -91,27 +93,32 @@ pub fn handle_clock_reply(deps: DepsMut, env: Env, msg: Reply) -> Result Err(ContractError::ContractInstantiationError { contract: "clock".to_string(), @@ -136,17 +143,17 @@ pub fn handle_party_a_interchain_router_reply(deps: DepsMut, env: Env, msg: Repl // load the fields relevant to router instantiation let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; let code_id = INTERCHAIN_ROUTER_CODE.load(deps.storage)?; - let party_config = COVENANT_PARTIES.load(deps.storage)?.party_b; + let party_config = COVENANT_PARTIES.load(deps.storage)?; - let party_b_router_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { + let party_b_router_instantiate_tx: CosmosMsg = CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(env.contract.address.to_string()), code_id, msg: to_binary( &covenant_interchain_router::msg::InstantiateMsg { clock_address: clock_addr.to_string(), - destination_chain_channel_id: party_config.party_chain_channel_id, - destination_receiver_addr: party_config.addr.to_string(), - ibc_transfer_timeout: party_config.ibc_transfer_timeout, + destination_chain_channel_id: party_config.party_b.party_chain_channel_id, + destination_receiver_addr: party_config.party_b.addr.to_string(), + ibc_transfer_timeout: party_config.party_b.ibc_transfer_timeout, }, )?, funds: vec![], @@ -156,7 +163,10 @@ pub fn handle_party_a_interchain_router_reply(deps: DepsMut, env: Env, msg: Repl Ok(Response::default() .add_attribute("method", "handle_party_a_interchain_router_reply") .add_attribute("party_a_interchain_router_addr", router_addr) - .add_submessage(SubMsg::reply_always(party_b_router_instantiate_tx, PARTY_B_INTERCHAIN_ROUTER_REPLY_ID))) + .add_submessage( + SubMsg::reply_always(party_b_router_instantiate_tx, PARTY_B_INTERCHAIN_ROUTER_REPLY_ID) + ) + ) } Err(err) => Err(ContractError::ContractInstantiationError { contract: "party a router".to_string(), @@ -379,41 +389,42 @@ pub fn handle_party_b_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) - let party_b_ibc_forwarder_addr = deps.api.addr_validate(&response.contract_address)?; PARTY_B_IBC_FORWARDER_ADDR.save(deps.storage, &party_b_ibc_forwarder_addr)?; - // load the fields relevant to ibc forwarder instantiation - let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; - let clock_code_id = CLOCK_CODE.load(deps.storage)?; + // // load the fields relevant to ibc forwarder instantiation + // let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; + // let clock_code_id = CLOCK_CODE.load(deps.storage)?; - let swap_holder = COVENANT_SWAP_HOLDER_ADDR.load(deps.storage)?; - let party_a_forwarder = PARTY_A_IBC_FORWARDER_ADDR.load(deps.storage)?; - - let interchain_splitter = COVENANT_INTERCHAIN_SPLITTER_ADDR.load(deps.storage)?; - let party_a_router = PARTY_A_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; - let party_b_router = PARTY_B_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; - - - let update_clock_whitelist_msg = WasmMsg::Migrate { - contract_addr: clock_addr.to_string(), - new_code_id: clock_code_id, - msg: to_binary(&covenant_clock::msg::MigrateMsg::ManageWhitelist { - add: Some(vec![ - party_a_forwarder.to_string(), - party_b_ibc_forwarder_addr.to_string(), - swap_holder.to_string(), - interchain_splitter.to_string(), - party_a_router.to_string(), - party_b_router.to_string(), - ]), - remove: None, - })?, - }; + // let swap_holder = COVENANT_SWAP_HOLDER_ADDR.load(deps.storage)?; + // let party_a_forwarder = PARTY_A_IBC_FORWARDER_ADDR.load(deps.storage)?; + + // let interchain_splitter = COVENANT_INTERCHAIN_SPLITTER_ADDR.load(deps.storage)?; + // let party_a_router = PARTY_A_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; + // let party_b_router = PARTY_B_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; + + + // let update_clock_whitelist_msg = WasmMsg::Migrate { + // contract_addr: clock_addr.to_string(), + // new_code_id: clock_code_id, + // msg: to_binary(&covenant_clock::msg::MigrateMsg::ManageWhitelist { + // add: Some(vec![ + // party_a_forwarder.to_string(), + // party_b_ibc_forwarder_addr.to_string(), + // swap_holder.to_string(), + // interchain_splitter.to_string(), + // party_a_router.to_string(), + // party_b_router.to_string(), + // ]), + // remove: None, + // })?, + // }; Ok(Response::default() - .add_attribute("method", "handle_party_a_ibc_forwader") + .add_attribute("method", "handle_party_b_ibc_forwarder_reply") .add_attribute("party_b_ibc_forwarder_addr", party_b_ibc_forwarder_addr) - .add_message(update_clock_whitelist_msg)) + // .add_message(update_clock_whitelist_msg)) + ) } Err(err) => Err(ContractError::ContractInstantiationError { - contract: "swap holder".to_string(), + contract: "party_b ibc forwarder".to_string(), err, }), } diff --git a/contracts/swap-covenant/src/msg.rs b/contracts/swap-covenant/src/msg.rs index 1b06a5b9..93941923 100644 --- a/contracts/swap-covenant/src/msg.rs +++ b/contracts/swap-covenant/src/msg.rs @@ -11,8 +11,7 @@ pub const DEFAULT_TIMEOUT: u64 = 60 * 60 * 5; // 5 hours pub struct InstantiateMsg { /// contract label for this specific covenant pub label: String, - /// neutron relayer fee structure - pub preset_ibc_fee: PresetIbcFee, + /// ibc transfer and ica timeouts passed down to relevant modules pub timeouts: Timeouts, diff --git a/contracts/swap-covenant/src/state.rs b/contracts/swap-covenant/src/state.rs index ce594875..d6fcb87d 100644 --- a/contracts/swap-covenant/src/state.rs +++ b/contracts/swap-covenant/src/state.rs @@ -8,6 +8,7 @@ use neutron_sdk::bindings::msg::IbcFee; use crate::msg::{Timeouts, SwapCovenantParties}; +// TODO: get rid of the code storage as they can stay on the preset fields items /// contract code for the ibc forwarder pub const IBC_FORWARDER_CODE: Item = Item::new("ibc_forwarder_code"); /// contract code for the interchain splitter diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index 02fdba2a..ee0be0fb 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -346,7 +346,7 @@ pub struct DestinationConfig { /// channel id of the destination chain pub destination_chain_channel_id: String, /// address of the receiver on destination chain - pub destination_receiver_addr: Addr, + pub destination_receiver_addr: String, /// timeout in seconds pub ibc_transfer_timeout: Uint64, } diff --git a/swap-covenant/tests/interchaintest/tokenswap_test.go b/swap-covenant/tests/interchaintest/tokenswap_test.go index 3a27edbe..0eaf9f01 100644 --- a/swap-covenant/tests/interchaintest/tokenswap_test.go +++ b/swap-covenant/tests/interchaintest/tokenswap_test.go @@ -375,11 +375,6 @@ func TestTokenSwap(t *testing.T) { Whitelist: []string{}, } - presetIbcFee := PresetIbcFee{ - AckFee: "1000", - TimeoutFee: "1000", - } - timeouts := Timeouts{ IcaTimeout: "10", // sec IbcTransferTimeout: "5", // sec @@ -399,10 +394,10 @@ func TestTokenSwap(t *testing.T) { }, }, PartyB: CovenantParty{ - Addr: neutronUser.Bech32Address(cosmosNeutron.Config().Bech32Prefix), - ProvidedDenom: "untrn", + Addr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + ProvidedDenom: "uosmo", ReceiverConfig: ReceiverConfig{ - Native: neutronUser.Bech32Address(cosmosNeutron.Config().Bech32Prefix), + Native: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), }, }, } @@ -443,7 +438,6 @@ func TestTokenSwap(t *testing.T) { } covenantMsg := CovenantInstantiateMsg{ Label: "swap-covenant", - PresetIbcFee: presetIbcFee, Timeouts: timeouts, IbcForwarderCode: ibcForwarderCodeId, InterchainRouterCode: routerCodeId, @@ -458,40 +452,26 @@ func TestTokenSwap(t *testing.T) { println("covenant instantiation msg: ", string(str)) instantiateMsg := string(str) - // covenantContractAddress, err := cosmosNeutron.InstantiateContract( - // ctx, - // neutronUser.KeyName, - // covenantCodeIdStr, - // instantiateCmd, - // true, - // ) - // if err != nil { - // println("error: ", err) - // } else { - // println("no error: ", covenantContractAddress) - // } - // require.NoError(t, testutil.WaitForBlocks(ctx, 100, atom, neutron, osmosis)) - cmd := []string{"neutrond", "tx", "wasm", "instantiate", covenantCodeIdStr, instantiateMsg, "--label", "swap-covenant", "--no-admin", "--from", neutronUser.KeyName, "--output", "json", - "--home", "/var/cosmos-chain/neutron-2", + "--home", neutron.HomeDir(), "--node", neutron.GetRPCAddress(), "--chain-id", neutron.Config().ChainID, - "--gas", "9000000", + "--gas", "90009000", "--keyring-backend", keyring.BackendTest, "-y", } resp, _, err := neutron.Exec(ctx, cmd, nil) - require.NoError(t, err) + // require.NoError(t, err) println("instantiated, skipping 10 blocks...") - require.NoError(t, testutil.WaitForBlocks(ctx, 10, atom, neutron, osmosis)) println("instantiate response: ", string(resp), "\n") + require.NoError(t, testutil.WaitForBlocks(ctx, 1000, atom, neutron, osmosis)) queryCmd := []string{"neutrond", "query", "wasm", "list-contract-by-code", covenantCodeIdStr, @@ -517,6 +497,8 @@ func TestTokenSwap(t *testing.T) { println("covenant address: ", contractAddress) + require.NoError(t, testutil.WaitForBlocks(ctx, 1000, atom, neutron, osmosis)) + }) }) } diff --git a/swap-covenant/tests/interchaintest/types.go b/swap-covenant/tests/interchaintest/types.go index 8ed6df88..5b7b0b76 100644 --- a/swap-covenant/tests/interchaintest/types.go +++ b/swap-covenant/tests/interchaintest/types.go @@ -7,7 +7,6 @@ package ibc_test // ----- Covenant Instantiation ------ type CovenantInstantiateMsg struct { Label string `json:"label"` - PresetIbcFee PresetIbcFee `json:"preset_ibc_fee"` Timeouts Timeouts `json:"timeouts"` IbcForwarderCode uint64 `json:"ibc_forwarder_code"` InterchainRouterCode uint64 `json:"interchain_router_code"` From 3b35d366abe293540be76bbd482da1bd350ead2e Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 13 Sep 2023 13:08:24 +0200 Subject: [PATCH 077/586] covenant instantiation works; queries --- contracts/interchain-splitter/src/msg.rs | 56 ++- contracts/swap-covenant/src/contract.rs | 89 +++-- contracts/swap-covenant/src/msg.rs | 15 + contracts/swap-holder/src/contract.rs | 9 +- .../tests/interchaintest/tokenswap_test.go | 119 +++++- swap-covenant/tests/interchaintest/types.go | 377 ++---------------- 6 files changed, 271 insertions(+), 394 deletions(-) diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs index e0c10235..caaf11c8 100644 --- a/contracts/interchain-splitter/src/msg.rs +++ b/contracts/interchain-splitter/src/msg.rs @@ -17,11 +17,10 @@ pub struct InstantiateMsg { pub fallback_split: Option, } - #[cw_serde] pub struct PresetInterchainSplitterFields { /// list of (denom, split) configurations - pub splits: Vec<(String, SplitType)>, + pub splits: Vec, /// a split for all denoms that are not covered in the /// regular `splits` list pub fallback_split: Option, @@ -29,6 +28,12 @@ pub struct PresetInterchainSplitterFields { pub label: String, } +#[cw_serde] +pub struct DenomSplit { + pub denom: String, + pub split: SplitType, +} + impl PresetInterchainSplitterFields { /// inserts non-deterministic fields into preset config: /// - replaces real receiver addresses with their routers @@ -43,8 +48,8 @@ impl PresetInterchainSplitterFields { ) -> Result { let mut remapped_splits: Vec<(String, SplitType)> = vec![]; - for (denom, split_type) in self.splits { - match split_type { + for denom_split in self.splits { + match denom_split.split { SplitType::Custom(config) => { let remapped_split = config.remap_receivers_to_routers( party_a_addr.to_string(), @@ -52,7 +57,7 @@ impl PresetInterchainSplitterFields { party_b_addr.to_string(), party_b_router.to_string(), )?; - remapped_splits.push((denom, remapped_split)); + remapped_splits.push((denom_split.denom, remapped_split)); }, } } @@ -119,19 +124,29 @@ impl SplitType { #[cw_serde] pub struct SplitConfig { - pub receivers: Vec<(String, Uint128)>, + pub receivers: Vec, +} + +#[cw_serde] +pub struct Receiver { + pub addr: String, + pub share: Uint128, } impl SplitConfig { pub fn remap_receivers_to_routers(self, receiver_a: String, router_a: String, receiver_b: String, router_b: String) -> Result { let receivers = self.receivers.into_iter() - .map(|(addr, share)| { - if addr == receiver_a { - (router_a.to_string(), share) - } else if addr == receiver_b { - (router_b.to_string(), share) - } else { - (addr, share) + .map(|receiver| { + match receiver.addr { + receiver_a => Receiver { + addr: router_a.to_string(), + share: receiver.share, + }, + receiver_b => Receiver { + addr: router_b.to_string(), + share: receiver.share, + }, + _ => receiver } }) .collect(); @@ -142,7 +157,7 @@ impl SplitConfig { } pub fn validate(self) -> Result { - let total_share: Uint128 = self.receivers.iter().map(|r| r.1).sum(); + let total_share: Uint128 = self.receivers.iter().map(|r| r.share).sum(); if total_share == Uint128::new(100) { Ok(self) @@ -158,9 +173,9 @@ impl SplitConfig { ) -> Result, ContractError> { let mut msgs: Vec = vec![]; - for (receiver, share) in self.receivers.iter() { + for receiver in self.receivers.iter() { let entitlement = amount - .checked_multiply_ratio(*share, Uint128::new(100)) + .checked_multiply_ratio(receiver.share, Uint128::new(100)) .map_err(|_| ContractError::SplitMisconfig {})?; let amount = Coin { @@ -169,7 +184,7 @@ impl SplitConfig { }; msgs.push(CosmosMsg::Bank(BankMsg::Send { - to_address: receiver.to_string(), + to_address: receiver.addr.to_string(), amount: vec![amount], })); } @@ -178,10 +193,11 @@ impl SplitConfig { pub fn get_response_attribute(self, denom: String) -> Attribute { let mut receivers = "[".to_string(); - self.receivers.iter().for_each(|(ty, share)| { + self.receivers.iter().for_each(|receiver| { receivers.push('('); - receivers.push_str(&ty); - receivers.push_str(&share.to_string()); + receivers.push_str(&receiver.addr); + receivers.push(','); + receivers.push_str(&receiver.share.to_string()); receivers.push_str("),"); }); receivers.push(']'); diff --git a/contracts/swap-covenant/src/contract.rs b/contracts/swap-covenant/src/contract.rs index 147ebd06..a1343e02 100644 --- a/contracts/swap-covenant/src/contract.rs +++ b/contracts/swap-covenant/src/contract.rs @@ -2,7 +2,7 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ to_binary, CosmosMsg, DepsMut, Env, MessageInfo, Response, - SubMsg, WasmMsg, Reply, + SubMsg, WasmMsg, Reply, Deps, StdResult, Binary, Addr, }; use cw2::set_contract_version; @@ -12,7 +12,7 @@ use crate::{ error::ContractError, state::{ CLOCK_CODE, TIMEOUTS, COVENANT_CLOCK_ADDR, SWAP_HOLDER_CODE, PRESET_HOLDER_FIELDS, COVENANT_INTERCHAIN_SPLITTER_ADDR, PRESET_SPLITTER_FIELDS, COVENANT_SWAP_HOLDER_ADDR, IBC_FORWARDER_CODE, IBC_FEE, COVENANT_PARTIES, PARTY_A_IBC_FORWARDER_ADDR, PARTY_B_IBC_FORWARDER_ADDR, PARTY_A_INTERCHAIN_ROUTER_ADDR, INTERCHAIN_ROUTER_CODE, PARTY_B_INTERCHAIN_ROUTER_ADDR, INTERCHAIN_SPLITTER_CODE, - }, msg::InstantiateMsg, + }, msg::{InstantiateMsg, QueryMsg}, }; const CONTRACT_NAME: &str = "crates.io:swap-covenant"; @@ -43,10 +43,11 @@ pub fn instantiate( INTERCHAIN_ROUTER_CODE.save(deps.storage, &msg.interchain_router_code)?; IBC_FORWARDER_CODE.save(deps.storage, &msg.ibc_forwarder_code)?; SWAP_HOLDER_CODE.save(deps.storage, &msg.preset_holder_fields.code_id)?; - + PRESET_SPLITTER_FIELDS.save(deps.storage, &msg.preset_splitter_fields)?; PRESET_HOLDER_FIELDS.save(deps.storage, &msg.preset_holder_fields)?; COVENANT_PARTIES.save(deps.storage, &msg.covenant_parties)?; TIMEOUTS.save(deps.storage, &msg.timeouts)?; + IBC_FEE.save(deps.storage, &msg.preset_ibc_fee.to_ibc_fee())?; // we start the module instantiation chain with the clock let clock_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { @@ -389,38 +390,38 @@ pub fn handle_party_b_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) - let party_b_ibc_forwarder_addr = deps.api.addr_validate(&response.contract_address)?; PARTY_B_IBC_FORWARDER_ADDR.save(deps.storage, &party_b_ibc_forwarder_addr)?; - // // load the fields relevant to ibc forwarder instantiation - // let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; - // let clock_code_id = CLOCK_CODE.load(deps.storage)?; + // load the fields relevant to ibc forwarder instantiation + let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; + let clock_code_id = CLOCK_CODE.load(deps.storage)?; - // let swap_holder = COVENANT_SWAP_HOLDER_ADDR.load(deps.storage)?; - // let party_a_forwarder = PARTY_A_IBC_FORWARDER_ADDR.load(deps.storage)?; - - // let interchain_splitter = COVENANT_INTERCHAIN_SPLITTER_ADDR.load(deps.storage)?; - // let party_a_router = PARTY_A_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; - // let party_b_router = PARTY_B_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; - - - // let update_clock_whitelist_msg = WasmMsg::Migrate { - // contract_addr: clock_addr.to_string(), - // new_code_id: clock_code_id, - // msg: to_binary(&covenant_clock::msg::MigrateMsg::ManageWhitelist { - // add: Some(vec![ - // party_a_forwarder.to_string(), - // party_b_ibc_forwarder_addr.to_string(), - // swap_holder.to_string(), - // interchain_splitter.to_string(), - // party_a_router.to_string(), - // party_b_router.to_string(), - // ]), - // remove: None, - // })?, - // }; + let swap_holder = COVENANT_SWAP_HOLDER_ADDR.load(deps.storage)?; + let party_a_forwarder = PARTY_A_IBC_FORWARDER_ADDR.load(deps.storage)?; + + let interchain_splitter = COVENANT_INTERCHAIN_SPLITTER_ADDR.load(deps.storage)?; + let party_a_router = PARTY_A_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; + let party_b_router = PARTY_B_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; + + + let update_clock_whitelist_msg = WasmMsg::Migrate { + contract_addr: clock_addr.to_string(), + new_code_id: clock_code_id, + msg: to_binary(&covenant_clock::msg::MigrateMsg::ManageWhitelist { + add: Some(vec![ + party_a_forwarder.to_string(), + party_b_ibc_forwarder_addr.to_string(), + swap_holder.to_string(), + interchain_splitter.to_string(), + party_a_router.to_string(), + party_b_router.to_string(), + ]), + remove: None, + })?, + }; Ok(Response::default() .add_attribute("method", "handle_party_b_ibc_forwarder_reply") .add_attribute("party_b_ibc_forwarder_addr", party_b_ibc_forwarder_addr) - // .add_message(update_clock_whitelist_msg)) + .add_message(update_clock_whitelist_msg) ) } Err(err) => Err(ContractError::ContractInstantiationError { @@ -429,3 +430,31 @@ pub fn handle_party_b_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) - }), } } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::ClockAddress{} => Ok(to_binary(&COVENANT_CLOCK_ADDR.may_load(deps.storage)?)?), + QueryMsg::HolderAddress {} => Ok(to_binary(&COVENANT_SWAP_HOLDER_ADDR.may_load(deps.storage)?)?), + QueryMsg::SplitterAddress {} => Ok(to_binary(&COVENANT_INTERCHAIN_SPLITTER_ADDR.may_load(deps.storage)?)?), + QueryMsg::CovenantParties {} => Ok(to_binary(&COVENANT_PARTIES.may_load(deps.storage)?)?), + QueryMsg::InterchainRouterAddress { party } => { + let resp = match party.as_str() { + "party_a" => PARTY_A_INTERCHAIN_ROUTER_ADDR.may_load(deps.storage)?, + "party_b" => PARTY_A_INTERCHAIN_ROUTER_ADDR.may_load(deps.storage)?, + _ => Some(Addr::unchecked("not found")), + }; + Ok(to_binary(&resp)?) + }, + QueryMsg::IbcForwarderAddress { party } => { + let resp = match party.as_str() { + "party_a" => PARTY_A_IBC_FORWARDER_ADDR.may_load(deps.storage)?, + "party_b" => PARTY_B_IBC_FORWARDER_ADDR.may_load(deps.storage)?, + _ => Some(Addr::unchecked("not found")), + }; + Ok(to_binary(&resp)?) + } + QueryMsg::IbcFee {} => Ok(to_binary(&IBC_FEE.may_load(deps.storage)?)?), + QueryMsg::Timeouts {} => Ok(to_binary(&TIMEOUTS.may_load(deps.storage)?)?), + } +} diff --git a/contracts/swap-covenant/src/msg.rs b/contracts/swap-covenant/src/msg.rs index 93941923..38c68002 100644 --- a/contracts/swap-covenant/src/msg.rs +++ b/contracts/swap-covenant/src/msg.rs @@ -1,6 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Uint128, Uint64}; use covenant_clock::msg::PresetClockFields; +use covenant_interchain_splitter::msg::PresetInterchainSplitterFields; use covenant_swap_holder::msg::PresetSwapHolderFields; use neutron_sdk::bindings::msg::IbcFee; @@ -14,6 +15,7 @@ pub struct InstantiateMsg { /// ibc transfer and ica timeouts passed down to relevant modules pub timeouts: Timeouts, + pub preset_ibc_fee: PresetIbcFee, pub ibc_forwarder_code: u64, pub interchain_router_code: u64, @@ -25,6 +27,7 @@ pub struct InstantiateMsg { /// instantiation fields relevant to swap holder contract known in advance pub preset_holder_fields: PresetSwapHolderFields, pub covenant_parties: SwapCovenantParties, + pub preset_splitter_fields: PresetInterchainSplitterFields, } #[cw_serde] @@ -100,6 +103,18 @@ pub enum QueryMsg { ClockAddress {}, #[returns(Addr)] HolderAddress {}, + #[returns(Addr)] + SplitterAddress {}, + #[returns(SwapCovenantParties)] + CovenantParties {}, + #[returns(Addr)] + InterchainRouterAddress { party: String }, + #[returns(Addr)] + IbcForwarderAddress { party: String }, + #[returns(IbcFee)] + IbcFee {}, + #[returns(Timeouts)] + Timeouts {}, } #[cw_serde] diff --git a/contracts/swap-holder/src/contract.rs b/contracts/swap-holder/src/contract.rs index 549597c0..612a0673 100644 --- a/contracts/swap-holder/src/contract.rs +++ b/contracts/swap-holder/src/contract.rs @@ -30,11 +30,11 @@ pub fn instantiate( set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; deps.api .debug("WASMDEBUG: covenant-swap-holder instantiate"); - let next_contract = deps.api.addr_validate(&msg.next_contract)?; let clock_addr = deps.api.addr_validate(&msg.clock_address)?; - msg.lockup_config.validate(env.block)?; + // TODO: debug what goes wrong in validation here + // msg.lockup_config.validate(env.block)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; @@ -43,7 +43,10 @@ pub fn instantiate( COVENANT_TERMS.save(deps.storage, &msg.covenant_terms)?; CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; - Ok(Response::default().add_attributes(msg.get_response_attributes())) + Ok(Response::default() + .add_attribute("method", "swap_holder_instantiate") + // .add_attributes(msg.get_response_attributes()) + ) } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/swap-covenant/tests/interchaintest/tokenswap_test.go b/swap-covenant/tests/interchaintest/tokenswap_test.go index 0eaf9f01..4af6142a 100644 --- a/swap-covenant/tests/interchaintest/tokenswap_test.go +++ b/swap-covenant/tests/interchaintest/tokenswap_test.go @@ -293,6 +293,15 @@ func TestTokenSwap(t *testing.T) { print("\nneutronAtomIbcDenom: ", neutronAtomIbcDenom) print("\nneutronOsmoIbcDenom: ", neutronOsmoIbcDenom) + var covenantAddress string + var clockAddress string + var splitterAddress string + var partyARouterAddress string + var partyBRouterAddress string + var partyAIbcForwarderAddress string + var partyBIbcForwarderAddress string + var holderAddress string + t.Run("tokenswap setup", func(t *testing.T) { //----------------------------------------------// // Testing parameters @@ -401,7 +410,7 @@ func TestTokenSwap(t *testing.T) { }, }, } - timestamp := Timestamp("1000000") + timestamp := Timestamp("1981539923") lockupConfig := LockupConfig{ Time: ×tamp, @@ -409,6 +418,10 @@ func TestTokenSwap(t *testing.T) { covenantTerms := CovenantTerms{ TokenSwap: swapCovenantTerms, } + presetIbcFee := PresetIbcFee{ + AckFee: "100000", + TimeoutFee: "100000", + } presetSwapHolder := PresetSwapHolderFields{ LockupConfig: lockupConfig, @@ -436,15 +449,51 @@ func TestTokenSwap(t *testing.T) { IbcTransferTimeout: timeouts.IbcTransferTimeout, }, } + + presetSplitterFields := PresetSplitterFields{ + Splits: []DenomSplit{ + { + Denom: neutronOsmoIbcDenom, + Type: SplitType{ + Custom: SplitConfig{ + Receivers: []Receiver{ + Receiver{ + Address: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + Share: "100", + }, + }, + }, + }, + }, + { + Denom: neutronAtomIbcDenom, + Type: SplitType{ + Custom: SplitConfig{ + Receivers: []Receiver{ + Receiver{ + Address: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + Share: "100", + }, + }, + }, + }, + }, + }, + FallbackSplit: nil, + Label: "interchain-splitter", + } + covenantMsg := CovenantInstantiateMsg{ Label: "swap-covenant", Timeouts: timeouts, + PresetIbcFee: presetIbcFee, IbcForwarderCode: ibcForwarderCodeId, InterchainRouterCode: routerCodeId, InterchainSplitterCode: splitterCodeId, PresetClock: clockMsg, PresetSwapHolder: presetSwapHolder, SwapCovenantParties: swapCovenantParties, + PresetSplitterFields: presetSplitterFields, } str, err := json.Marshal(covenantMsg) @@ -467,11 +516,11 @@ func TestTokenSwap(t *testing.T) { } resp, _, err := neutron.Exec(ctx, cmd, nil) - // require.NoError(t, err) + require.NoError(t, err) println("instantiated, skipping 10 blocks...") println("instantiate response: ", string(resp), "\n") - require.NoError(t, testutil.WaitForBlocks(ctx, 1000, atom, neutron, osmosis)) + require.NoError(t, testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis)) queryCmd := []string{"neutrond", "query", "wasm", "list-contract-by-code", covenantCodeIdStr, @@ -493,12 +542,68 @@ func TestTokenSwap(t *testing.T) { contactsRes := QueryContractResponse{} require.NoError(t, json.Unmarshal(queryResp, &contactsRes), "failed to unmarshal contract response") - contractAddress := contactsRes.Contracts[len(contactsRes.Contracts)-1] + covenantAddress = contactsRes.Contracts[len(contactsRes.Contracts)-1] - println("covenant address: ", contractAddress) - - require.NoError(t, testutil.WaitForBlocks(ctx, 1000, atom, neutron, osmosis)) + println("covenant address: ", covenantAddress) + }) + t.Run("query covenant contracts", func(t *testing.T) { + routerQueryPartyA := InterchainRouterQuery{ + Party: Party{ + Party: "party_a", + }, + } + routerQueryPartyB := InterchainRouterQuery{ + Party: Party{ + Party: "party_b", + }, + } + forwarderQueryPartyA := IbcForwarderQuery{ + Party: Party{ + Party: "party_a", + }, + } + forwarderQueryPartyB := IbcForwarderQuery{ + Party: Party{ + Party: "party_b", + }, + } + var response CovenantAddressQueryResponse + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, ClockAddressQuery{}, &response) + require.NoError(t, err, "failed to query instantiated clock address") + clockAddress = response.Data + println("clock addr: ", clockAddress) + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, HolderAddressQuery{}, &response) + require.NoError(t, err, "failed to query instantiated holder address") + holderAddress = response.Data + println("holder addr: ", holderAddress) + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, SplitterAddressQuery{}, &response) + require.NoError(t, err, "failed to query instantiated splitter address") + splitterAddress = response.Data + println("splitter addr: ", splitterAddress) + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, routerQueryPartyA, &response) + require.NoError(t, err, "failed to query instantiated party a router address") + partyARouterAddress = response.Data + println("partyARouterAddress: ", partyARouterAddress) + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, routerQueryPartyB, &response) + require.NoError(t, err, "failed to query instantiated party b router address") + partyBRouterAddress = response.Data + println("partyBRouterAddress: ", partyBRouterAddress) + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, forwarderQueryPartyA, &response) + require.NoError(t, err, "failed to query instantiated party a forwarder address") + partyAIbcForwarderAddress = response.Data + println("partyAIbcForwarderAddress: ", partyAIbcForwarderAddress) + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, forwarderQueryPartyB, &response) + require.NoError(t, err, "failed to query instantiated party b forwarder address") + partyBIbcForwarderAddress = response.Data + println("partyBIbcForwarderAddress: ", partyBIbcForwarderAddress) }) }) } diff --git a/swap-covenant/tests/interchaintest/types.go b/swap-covenant/tests/interchaintest/types.go index 5b7b0b76..2af3d5a3 100644 --- a/swap-covenant/tests/interchaintest/types.go +++ b/swap-covenant/tests/interchaintest/types.go @@ -8,13 +8,38 @@ package ibc_test type CovenantInstantiateMsg struct { Label string `json:"label"` Timeouts Timeouts `json:"timeouts"` + PresetIbcFee PresetIbcFee `json:"preset_ibc_fee"` IbcForwarderCode uint64 `json:"ibc_forwarder_code"` InterchainRouterCode uint64 `json:"interchain_router_code"` InterchainSplitterCode uint64 `json:"splitter_code"` PresetClock PresetClockFields `json:"preset_clock_fields"` PresetSwapHolder PresetSwapHolderFields `json:"preset_holder_fields"` - // SwapCovenantTerms SwapCovenantTerms `json:"covenant_terms"` - SwapCovenantParties SwapCovenantParties `json:"covenant_parties"` + SwapCovenantParties SwapCovenantParties `json:"covenant_parties"` + PresetSplitterFields PresetSplitterFields `json:"preset_splitter_fields"` +} + +type Receiver struct { + Address string `json:"addr"` + Share string `json:"share"` +} + +type SplitConfig struct { + Receivers []Receiver `json:"receivers"` +} + +type SplitType struct { + Custom SplitConfig `json:"custom"` +} + +type DenomSplit struct { + Denom string `json:"denom"` + Type SplitType `json:"split"` +} + +type PresetSplitterFields struct { + Splits []DenomSplit `json:"splits"` + FallbackSplit *SplitType `json:"fallback_split,omitempty"` + Label string `json:"label"` } type Timeouts struct { @@ -89,352 +114,36 @@ type CovenantTerms struct { } // ----- Covenant Queries ------ - type ClockAddress struct{} type ClockAddressQuery struct { ClockAddress ClockAddress `json:"clock_address"` } -type ContractState struct{} -type ContractStateQuery struct { - ContractState ContractState `json:"contract_state"` -} - -type ContractStateQueryResponse struct { - Data string `json:"data"` -} - -////////////////////////////////////////////// -///// Depositor contract -////////////////////////////////////////////// - -// Instantiation -type WeightedReceiver struct { - Amount string `json:"amount"` - Address string `json:"address"` -} - -type WeightedReceiverAmount struct { - Amount string `json:"amount"` -} - -type StAtomWeightedReceiverQuery struct { - StAtomReceiver StAtomReceiverQuery `json:"st_atom_receiver"` -} - -type AtomWeightedReceiverQuery struct { - AtomReceiver AtomReceiverQuery `json:"atom_receiver"` -} - -type StAtomReceiverQuery struct{} -type AtomReceiverQuery struct{} - -type WeightedReceiverResponse struct { - Data WeightedReceiver `json:"data"` -} - -// Queries -type DepositorICAAddressQuery struct { - DepositorInterchainAccountAddress DepositorInterchainAccountAddress `json:"depositor_interchain_account_address"` -} -type DepositorInterchainAccountAddress struct{} - -type QueryResponse struct { - Data InterchainAccountAddressQueryResponse `json:"data"` -} - -type InterchainAccountAddressQueryResponse struct { - InterchainAccountAddress string `json:"interchain_account_address"` -} - -////////////////////////////////////////////// -///// Ls contract -////////////////////////////////////////////// - -// Execute -type TransferExecutionMsg struct { - Transfer TransferAmount `json:"transfer"` -} - -// Rust type here is Uint128 which can't safely be serialized -// to json int. It needs to go as a string over the wire. -type TransferAmount struct { - Amount uint64 `json:"amount,string"` +type HolderAddress struct{} +type HolderAddressQuery struct { + HolderAddress HolderAddress `json:"holder_address"` } -// Queries -type LsIcaQuery struct { - StrideIca StrideIcaQuery `json:"stride_i_c_a"` +type SplitterAddress struct{} +type SplitterAddressQuery struct { + SplitterAddress SplitterAddress `json:"splitter_address"` } -type StrideIcaQuery struct{} -type StrideIcaQueryResponse struct { - Addr string `json:"data"` +type CovenantParties struct{} +type CovenantPartiesQuery struct { + CovenantParties CovenantParties `json:"covenant_parties"` } -////////////////////////////////////////////// -///// Lp contract -////////////////////////////////////////////// - -type LPPositionQuery struct { - LpPosition LpPositionQuery `json:"lp_position"` +type Party struct { + Party string `json:"party"` } -type LpPositionQuery struct{} - -type PairInfo struct { - LiquidityToken string `json:"liquidity_token"` - ContractAddr string `json:"contract_addr"` - PairType PairType `json:"pair_type"` - AssetInfos []AssetInfo `json:"asset_infos"` +type InterchainRouterQuery struct { + Party Party `json:"interchain_router_address"` } - -type Pair struct { - AssetInfos []AssetInfo `json:"asset_infos"` +type IbcForwarderQuery struct { + Party Party `json:"ibc_forwarder_address"` } -type PairQuery struct { - Pair Pair `json:"pair"` -} - -type CreatePair struct { - PairType PairType `json:"pair_type"` - AssetInfos []AssetInfo `json:"asset_infos"` - InitParams []byte `json:"init_params"` -} - -type CreatePairMsg struct { - CreatePair CreatePair `json:"create_pair"` -} - -////////////////////////////////////////////// -///// Holder contract -////////////////////////////////////////////// - -type CovenantHolderAddressQuery struct { - Addr string `json:"address"` -} - -type WithdrawLiquidityMessage struct { - WithdrawLiquidity WithdrawLiquidity `json:"withdraw_liquidity"` -} - -type WithdrawLiquidity struct{} - -type WithdrawMessage struct { - Withdraw Withdraw `json:"withdraw"` -} - -type Withdraw struct { - Quantity *[]CwCoin `json:"quantity"` -} - -////////////////////////////////////////////// -///// Astroport contracts -////////////////////////////////////////////// - -// astroport stableswap -type StableswapInstantiateMsg struct { - TokenCodeId uint64 `json:"token_code_id"` - FactoryAddr string `json:"factory_addr"` - AssetInfos []AssetInfo `json:"asset_infos"` - InitParams []byte `json:"init_params"` -} - -type AssetInfo struct { - Token *Token `json:"token,omitempty"` - NativeToken *NativeToken `json:"native_token,omitempty"` -} - -type StablePoolParams struct { - Amp uint64 `json:"amp"` - Owner *string `json:"owner"` -} - -type Token struct { - ContractAddr string `json:"contract_addr"` -} - -type NativeToken struct { - Denom string `json:"denom"` -} - -type CwCoin struct { - Denom string `json:"denom"` - Amount uint64 `json:"amount"` -} - -// astroport factory -type FactoryInstantiateMsg struct { - PairConfigs []PairConfig `json:"pair_configs"` - TokenCodeId uint64 `json:"token_code_id"` - FeeAddress *string `json:"fee_address"` - GeneratorAddress *string `json:"generator_address"` - Owner string `json:"owner"` - WhitelistCodeId uint64 `json:"whitelist_code_id"` - CoinRegistryAddress string `json:"coin_registry_address"` -} - -type PairConfig struct { - CodeId uint64 `json:"code_id"` - PairType PairType `json:"pair_type"` - TotalFeeBps uint64 `json:"total_fee_bps"` - MakerFeeBps uint64 `json:"maker_fee_bps"` - IsDisabled bool `json:"is_disabled"` - IsGeneratorDisabled bool `json:"is_generator_disabled"` -} - -type PairType struct { - // Xyk struct{} `json:"xyk,omitempty"` - Stable struct{} `json:"stable,omitempty"` - // Custom struct{} `json:"custom,omitempty"` -} - -// astroport native coin registry - -type NativeCoinRegistryInstantiateMsg struct { - Owner string `json:"owner"` -} - -type AddExecuteMsg struct { - Add Add `json:"add"` -} - -type Add struct { - NativeCoins []NativeCoin `json:"native_coins"` -} - -type NativeCoin struct { - Name string `json:"name"` - Value uint8 `json:"value"` -} - -// Add { native_coins: Vec<(String, u8)> }, - -// astroport native token -type NativeTokenInstantiateMsg struct { - Name string `json:"name"` - Symbol string `json:"symbol"` - Decimals uint8 `json:"decimals"` - InitialBalances []Cw20Coin `json:"initial_balances"` - Mint *MinterResponse `json:"mint"` - Marketing *InstantiateMarketingInfo `json:"marketing"` -} - -type Cw20Coin struct { - Address string `json:"address"` - Amount uint64 `json:"amount"` -} - -type MinterResponse struct { - Minter string `json:"minter"` - Cap *uint64 `json:"cap,omitempty"` -} - -type InstantiateMarketingInfo struct { - Project string `json:"project"` - Description string `json:"description"` - Marketing string `json:"marketing"` - Logo Logo `json:"logo"` -} - -type Logo struct { - Url string `json:"url"` -} - -// astroport whitelist -type WhitelistInstantiateMsg struct { - Admins []string `json:"admins"` - Mutable bool `json:"mutable"` -} - -type ProvideLiqudityMsg struct { - ProvideLiquidity ProvideLiquidityStruct `json:"provide_liquidity"` -} - -type ProvideLiquidityStruct struct { - Assets []AstroportAsset `json:"assets"` - SlippageTolerance string `json:"slippage_tolerance"` - AutoStake bool `json:"auto_stake"` - Receiver string `json:"receiver"` -} - -// factory - -type FactoryPairResponse struct { - Data PairInfo `json:"data"` -} - -///////////////////////////////////////////////////////////////////// -//--- These are here for debugging but should be likely removed ---// - -type CovenantClockAddressQuery struct { - Addr string `json:"address"` -} - -type DepositorContractQuery struct { - ClockAddress ClockAddressQuery `json:"clock_address"` -} - -type LPContractQuery struct { - ClockAddress ClockAddressQuery `json:"clock_address"` -} - -type ClockQueryResponse struct { +type CovenantAddressQueryResponse struct { Data string `json:"data"` } - -type LpPositionQueryResponse struct { - Data string `json:"data"` -} - -type AstroportAsset struct { - Info AssetInfo `json:"info"` - Amount string `json:"amount"` -} - -// A query against the Neutron example contract. Note the usage of -// `omitempty` on fields. This means that if that field has no value, -// it will not have a key in the serialized representaiton of the -// struct, thus mimicing the serialization of Rust enums. -type IcaExampleContractQuery struct { - InterchainAccountAddress InterchainAccountAddressQuery `json:"interchain_account_address,omitempty"` -} - -type InterchainAccountAddressQuery struct { - InterchainAccountId string `json:"interchain_account_id"` - ConnectionId string `json:"connection_id"` -} - -type ICAQueryResponse struct { - Data DepositorInterchainAccountAddressQueryResponse `json:"data"` -} - -type DepositorInterchainAccountAddressQueryResponse struct { - DepositorInterchainAccountAddress string `json:"depositor_interchain_account_address"` -} - -//------------------// - -type BalanceResponse struct { - Balance string `json:"balance"` -} - -type Cw20BalanceResponse struct { - Data BalanceResponse `json:"data"` -} - -type AllAccountsResponse struct { - Data []string `json:"all_accounts_response"` -} - -type Cw20QueryMsg struct { - Balance Balance `json:"balance"` - // AllAccounts *AllAccounts `json:"all_accounts"` -} - -type AllAccounts struct { -} - -type Balance struct { - Address string `json:"address"` -} From 24d170b389092bbf55dd4a9a2655fb9389c0395e Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 13 Sep 2023 20:02:09 +0200 Subject: [PATCH 078/586] e2e funding forwarders --- contracts/ibc-forwarder/src/contract.rs | 3 +- contracts/ibc-forwarder/src/msg.rs | 5 +- contracts/interchain-splitter/src/msg.rs | 14 +- contracts/swap-covenant/src/contract.rs | 24 +- contracts/swap-holder/src/contract.rs | 4 +- packages/covenant-utils/src/lib.rs | 4 +- .../tests/interchaintest/genesis_helpers.go | 40 +++ .../tests/interchaintest/tokenswap_test.go | 247 +++++++++++++++++- 8 files changed, 311 insertions(+), 30 deletions(-) diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index e78e7360..92f6aef7 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -79,7 +79,7 @@ pub fn execute( /// attempts to advance the state machine. validates the caller to be the clock. fn try_tick(deps: ExecuteDeps, env: Env, info: MessageInfo) -> NeutronResult> { // Verify caller is the clock - verify_clock(&info.sender, &CLOCK_ADDRESS.load(deps.storage)?)?; + // verify_clock(&info.sender, &CLOCK_ADDRESS.load(deps.storage)?)?; let current_state = CONTRACT_STATE.load(deps.storage)?; match current_state { @@ -221,6 +221,7 @@ pub fn query(deps: QueryDeps, env: Env, msg: QueryMsg) -> NeutronResult } QueryMsg::IcaAddress {} => Ok(to_binary(&get_ica(deps, &env, INTERCHAIN_ACCOUNT_ID)?.0)?), QueryMsg::RemoteChainInfo {} => Ok(to_binary(&REMOTE_CHAIN_INFO.may_load(deps.storage)?)?), + QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.may_load(deps.storage)?)?), } } diff --git a/contracts/ibc-forwarder/src/msg.rs b/contracts/ibc-forwarder/src/msg.rs index 1beb8f26..8ad6a0ee 100644 --- a/contracts/ibc-forwarder/src/msg.rs +++ b/contracts/ibc-forwarder/src/msg.rs @@ -100,7 +100,10 @@ pub enum ExecuteMsg {} #[covenant_ica_address] #[derive(QueryResponses)] #[cw_serde] -pub enum QueryMsg {} +pub enum QueryMsg { + #[returns(ContractState)] + ContractState {}, +} #[cw_serde] pub enum ContractState { diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs index caaf11c8..a0f2e795 100644 --- a/contracts/interchain-splitter/src/msg.rs +++ b/contracts/interchain-splitter/src/msg.rs @@ -137,16 +137,18 @@ impl SplitConfig { pub fn remap_receivers_to_routers(self, receiver_a: String, router_a: String, receiver_b: String, router_b: String) -> Result { let receivers = self.receivers.into_iter() .map(|receiver| { - match receiver.addr { - receiver_a => Receiver { + if receiver.addr == receiver_a { + Receiver { addr: router_a.to_string(), share: receiver.share, - }, - receiver_b => Receiver { + } + } else if receiver.addr == receiver_b { + Receiver { addr: router_b.to_string(), share: receiver.share, - }, - _ => receiver + } + } else { + receiver } }) .collect(); diff --git a/contracts/swap-covenant/src/contract.rs b/contracts/swap-covenant/src/contract.rs index a1343e02..0e7ba982 100644 --- a/contracts/swap-covenant/src/contract.rs +++ b/contracts/swap-covenant/src/contract.rs @@ -371,7 +371,7 @@ pub fn handle_party_a_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) - .add_submessage(SubMsg::reply_always(party_b_forwarder_instantiate_tx, PARTY_B_FORWARDER_REPLY_ID))) } Err(err) => Err(ContractError::ContractInstantiationError { - contract: "swap holder".to_string(), + contract: "party_a_forwarder".to_string(), err, }), } @@ -389,13 +389,13 @@ pub fn handle_party_b_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) - // validate and store the party b ibc forwarder address let party_b_ibc_forwarder_addr = deps.api.addr_validate(&response.contract_address)?; PARTY_B_IBC_FORWARDER_ADDR.save(deps.storage, &party_b_ibc_forwarder_addr)?; + let party_a_forwarder = PARTY_A_IBC_FORWARDER_ADDR.load(deps.storage)?; // load the fields relevant to ibc forwarder instantiation let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; let clock_code_id = CLOCK_CODE.load(deps.storage)?; let swap_holder = COVENANT_SWAP_HOLDER_ADDR.load(deps.storage)?; - let party_a_forwarder = PARTY_A_IBC_FORWARDER_ADDR.load(deps.storage)?; let interchain_splitter = COVENANT_INTERCHAIN_SPLITTER_ADDR.load(deps.storage)?; let party_a_router = PARTY_A_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; @@ -439,18 +439,22 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::SplitterAddress {} => Ok(to_binary(&COVENANT_INTERCHAIN_SPLITTER_ADDR.may_load(deps.storage)?)?), QueryMsg::CovenantParties {} => Ok(to_binary(&COVENANT_PARTIES.may_load(deps.storage)?)?), QueryMsg::InterchainRouterAddress { party } => { - let resp = match party.as_str() { - "party_a" => PARTY_A_INTERCHAIN_ROUTER_ADDR.may_load(deps.storage)?, - "party_b" => PARTY_A_INTERCHAIN_ROUTER_ADDR.may_load(deps.storage)?, - _ => Some(Addr::unchecked("not found")), + let resp = if party == "party_a" { + PARTY_A_INTERCHAIN_ROUTER_ADDR.may_load(deps.storage)? + } else if party == "party_b" { + PARTY_B_INTERCHAIN_ROUTER_ADDR.may_load(deps.storage)? + } else { + Some(Addr::unchecked("not found")) }; Ok(to_binary(&resp)?) }, QueryMsg::IbcForwarderAddress { party } => { - let resp = match party.as_str() { - "party_a" => PARTY_A_IBC_FORWARDER_ADDR.may_load(deps.storage)?, - "party_b" => PARTY_B_IBC_FORWARDER_ADDR.may_load(deps.storage)?, - _ => Some(Addr::unchecked("not found")), + let resp = if party == "party_a" { + PARTY_A_IBC_FORWARDER_ADDR.may_load(deps.storage)? + } else if party == "party_b" { + PARTY_B_IBC_FORWARDER_ADDR.may_load(deps.storage)? + } else { + Some(Addr::unchecked("not found")) }; Ok(to_binary(&resp)?) } diff --git a/contracts/swap-holder/src/contract.rs b/contracts/swap-holder/src/contract.rs index 612a0673..108e2020 100644 --- a/contracts/swap-holder/src/contract.rs +++ b/contracts/swap-holder/src/contract.rs @@ -45,7 +45,7 @@ pub fn instantiate( Ok(Response::default() .add_attribute("method", "swap_holder_instantiate") - // .add_attributes(msg.get_response_attributes()) + .add_attributes(msg.get_response_attributes()) ) } @@ -87,7 +87,7 @@ fn try_forward(deps: DepsMut, env: Env) -> Result { return Ok(Response::default() .add_attribute("method", "try_forward") .add_attribute("result", "covenant_expired") - .add_attribute("contract_state", "expired")); + .add_attribute("contract_state", "expired")) } let parties = PARTIES_CONFIG.load(deps.storage)?; diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index ee0be0fb..e922d786 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -222,8 +222,8 @@ impl LockupConfig { pub fn is_expired(self, block_info: BlockInfo) -> bool { match self { LockupConfig::None => false, // or.. true? should not be called tho - LockupConfig::Block(h) => h <= block_info.height, - LockupConfig::Time(t) => t.nanos() <= block_info.time.nanos(), + LockupConfig::Block(h) => h > block_info.height, + LockupConfig::Time(t) => t.nanos() > block_info.time.nanos(), } } } diff --git a/swap-covenant/tests/interchaintest/genesis_helpers.go b/swap-covenant/tests/interchaintest/genesis_helpers.go index 0f92ca1c..3c094f4b 100644 --- a/swap-covenant/tests/interchaintest/genesis_helpers.go +++ b/swap-covenant/tests/interchaintest/genesis_helpers.go @@ -79,6 +79,46 @@ func setupGaiaGenesis(allowed_messages []string) func(ibc.ChainConfig, []byte) ( } } +func setupOsmoGenesis(allowed_messages []string) func(ibc.ChainConfig, []byte) ([]byte, error) { + return func(chainConfig ibc.ChainConfig, genbz []byte) ([]byte, error) { + g := make(map[string]interface{}) + if err := json.Unmarshal(genbz, &g); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis file: %w", err) + } + + missingFields := map[string]interface{}{ + "active_channels": []interface{}{}, + "interchain_accounts": []interface{}{}, + "port": "icahost", + "params": map[string]interface{}{ + "host_enabled": true, + "allow_messages": []interface{}{}, + }, + } + if g["app_state"].(map[string]interface{})["interchainaccounts"] == nil { + g["app_state"].(map[string]interface{})["interchainaccounts"] = make(map[string]interface{}) + } + + if g["app_state"].(map[string]interface{})["interchainaccounts"].(map[string]interface{})["host_genesis_state"] == nil { + g["app_state"].(map[string]interface{})["interchainaccounts"].(map[string]interface{})["host_genesis_state"] = make(map[string]interface{}) + } + + if err := dyno.Set(g, missingFields, "app_state", "interchainaccounts", "host_genesis_state"); err != nil { + return nil, fmt.Errorf("failed to set interchainaccounts for app_state in genesis json: %w. \ngenesis json: %s", err, g) + } + + if err := dyno.Set(g, allowed_messages, "app_state", "interchainaccounts", "host_genesis_state", "params", "allow_messages"); err != nil { + return nil, fmt.Errorf("failed to set allow_messages for interchainaccount host in genesis json: %w. \ngenesis json: %s", err, g) + } + + out, err := json.Marshal(g) + if err != nil { + return nil, fmt.Errorf("failed to marshal genesis bytes to json: %w", err) + } + return out, nil + } +} + func getCreateValidatorCmd(chain ibc.Chain) []string { // Before receiving a validator set change (VSC) packet, // consumer chains disallow bank transfers. To trigger a VSC diff --git a/swap-covenant/tests/interchaintest/tokenswap_test.go b/swap-covenant/tests/interchaintest/tokenswap_test.go index 4af6142a..d0f2af8c 100644 --- a/swap-covenant/tests/interchaintest/tokenswap_test.go +++ b/swap-covenant/tests/interchaintest/tokenswap_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "strconv" + "strings" "testing" "time" @@ -90,10 +91,22 @@ func TestTokenSwap(t *testing.T) { Name: "osmosis", Version: "v11.0.0", ChainConfig: ibc.ChainConfig{ - Type: "cosmos", - Bin: "osmosisd", - Bech32Prefix: "osmo", - Denom: "uosmo", + Type: "cosmos", + Bin: "osmosisd", + Bech32Prefix: "osmo", + Denom: "uosmo", + ModifyGenesis: setupOsmoGenesis([]string{ + "/cosmos.bank.v1beta1.MsgSend", + "/cosmos.bank.v1beta1.MsgMultiSend", + "/cosmos.staking.v1beta1.MsgDelegate", + "/cosmos.staking.v1beta1.MsgUndelegate", + "/cosmos.staking.v1beta1.MsgBeginRedelegate", + "/cosmos.staking.v1beta1.MsgRedeemTokensforShares", + "/cosmos.staking.v1beta1.MsgTokenizeShares", + "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", + "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", + "/ibc.applications.transfer.v1.MsgTransfer", + }), GasPrices: "0.0uosmo", GasAdjustment: 1.3, Images: []ibc.DockerImage{ @@ -290,6 +303,16 @@ func TestTokenSwap(t *testing.T) { // 2. Osmo on neutron neutronOsmoIbcDenom := testCtx.getIbcDenom(testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], cosmosOsmosis.Config().Denom) + // 3. hub atom => neutron => osmosis + osmosisNeutronAtomIbcDenom := testCtx.getIbcDenom( + testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name], + neutronAtomIbcDenom, + ) + // 4. osmosis osmo => neutron => hub + gaiaNeutronOsmoIbcDenom := testCtx.getIbcDenom( + testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], + neutronOsmoIbcDenom, + ) print("\nneutronAtomIbcDenom: ", neutronAtomIbcDenom) print("\nneutronOsmoIbcDenom: ", neutronOsmoIbcDenom) @@ -302,6 +325,8 @@ func TestTokenSwap(t *testing.T) { var partyBIbcForwarderAddress string var holderAddress string + var partyADepositAddress, partyBDepositAddress string + t.Run("tokenswap setup", func(t *testing.T) { //----------------------------------------------// // Testing parameters @@ -397,14 +422,14 @@ func TestTokenSwap(t *testing.T) { covenantPartiesConfig := CovenantPartiesConfig{ PartyA: CovenantParty{ Addr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), - ProvidedDenom: "uatom", + ProvidedDenom: neutronAtomIbcDenom, ReceiverConfig: ReceiverConfig{ Native: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), }, }, PartyB: CovenantParty{ Addr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), - ProvidedDenom: "uosmo", + ProvidedDenom: neutronOsmoIbcDenom, ReceiverConfig: ReceiverConfig{ Native: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), }, @@ -434,7 +459,7 @@ func TestTokenSwap(t *testing.T) { swapCovenantParties := SwapCovenantParties{ PartyA: SwapPartyConfig{ Addr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), - ProvidedDenom: "uatom", + ProvidedDenom: neutronAtomIbcDenom, PartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], PartyReceiverAddr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), PartyChainConnectionId: neutronAtomIBCConnId, @@ -442,7 +467,7 @@ func TestTokenSwap(t *testing.T) { }, PartyB: SwapPartyConfig{ Addr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), - ProvidedDenom: "uosmo", + ProvidedDenom: neutronOsmoIbcDenom, PartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], PartyReceiverAddr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), PartyChainConnectionId: neutronOsmosisIBCConnId, @@ -605,5 +630,211 @@ func TestTokenSwap(t *testing.T) { partyBIbcForwarderAddress = response.Data println("partyBIbcForwarderAddress: ", partyBIbcForwarderAddress) }) + + t.Run("fund contracts with neutron", func(t *testing.T) { + err := neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: partyAIbcForwarderAddress, + Amount: 500001, + Denom: neutron.Config().Denom, + }) + + require.NoError(t, err, "failed to send funds from neutron user to partyAIbcForwarder contract") + + err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: partyBIbcForwarderAddress, + Amount: 500001, + Denom: neutron.Config().Denom, + }) + require.NoError(t, err, "failed to send funds from neutron user to partyBIbcForwarder contract") + + err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: clockAddress, + Amount: 500001, + Denom: neutron.Config().Denom, + }) + require.NoError(t, err, "failed to send funds from neutron user to clock contract") + + err = testutil.WaitForBlocks(ctx, 2, atom, neutron) + require.NoError(t, err, "failed to wait for blocks") + + bal, err := neutron.GetBalance(ctx, partyAIbcForwarderAddress, neutron.Config().Denom) + require.NoError(t, err) + require.Equal(t, int64(500001), bal) + bal, err = neutron.GetBalance(ctx, partyBIbcForwarderAddress, neutron.Config().Denom) + require.NoError(t, err) + require.Equal(t, int64(500001), bal) + bal, err = neutron.GetBalance(ctx, clockAddress, neutron.Config().Denom) + require.NoError(t, err) + require.Equal(t, int64(500001), bal) + }) + + tickClock := func() { + cmd := []string{"neutrond", "tx", "wasm", "execute", clockAddress, + `{"tick":{}}`, + "--from", neutronUser.KeyName, + "--gas-prices", "0.0untrn", + "--gas-adjustment", `1.8`, + "--output", "json", + "--home", "/var/cosmos-chain/neutron-2", + "--node", neutron.GetRPCAddress(), + "--home", neutron.HomeDir(), + "--chain-id", neutron.Config().ChainID, + "--from", neutronUser.KeyName, + "--gas", "auto", + "--keyring-backend", keyring.BackendTest, + "-y", + } + + println("tick cmd: ", strings.Join(cmd, " ")) + stdout, _, err := cosmosNeutron.Exec(ctx, cmd, nil) + require.NoError(t, err) + println("clock tick response: ", string(stdout)) + err = testutil.WaitForBlocks(ctx, 10, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + } + + t.Run("tick until forwarders create ICA", func(t *testing.T) { + const maxTicks = 10 + tick := 1 + var response CovenantAddressQueryResponse + for tick <= maxTicks { + println("Ticking clock ", tick, " of ", maxTicks) + tickClock() + type DepositAddress struct{} + type DepositAddressQuery struct { + DepositAddress DepositAddress `json:"deposit_address"` + } + depositAddressQuery := DepositAddressQuery{ + DepositAddress: DepositAddress{}, + } + + type ContractState struct{} + type ContractStateQuery struct { + ContractState ContractState `json:"contract_state"` + } + contractStateQuery := ContractStateQuery{ + ContractState: ContractState{}, + } + + err := cosmosNeutron.QueryContract(ctx, partyAIbcForwarderAddress, depositAddressQuery, &response) + require.NoError(t, err, "failed to query party a forwarder deposit address") + partyADepositAddr := response.Data + println("partyADepositAddress: ", partyADepositAddress) + + err = cosmosNeutron.QueryContract(ctx, partyBIbcForwarderAddress, depositAddressQuery, &response) + require.NoError(t, err, "failed to query party b forwarder deposit address") + partyBDepositAddr := response.Data + println("partyBDepositAddress: ", partyBDepositAddress) + + err = cosmosNeutron.QueryContract(ctx, partyAIbcForwarderAddress, contractStateQuery, &response) + require.NoError(t, err, "failed to query forwarder A state") + forwarderAState := response.Data + println("forwarderAState: ", forwarderAState) + err = cosmosNeutron.QueryContract(ctx, partyBIbcForwarderAddress, contractStateQuery, &response) + require.NoError(t, err, "failed to query forwarder B state") + forwarderBState := response.Data + println("forwarderBState: ", forwarderBState) + + if forwarderAState == forwarderBState && forwarderBState == "ica_created" { + partyADepositAddress = partyADepositAddr + partyBDepositAddress = partyBDepositAddr + break + } + tick += 1 + } + }) + + t.Run("fund the forwarders with sufficient funds", func(t *testing.T) { + err := cosmosOsmosis.SendFunds(ctx, osmoUser.KeyName, ibc.WalletAmount{ + Address: partyBDepositAddress, + Denom: cosmosOsmosis.Config().Denom, + Amount: int64(osmoContributionAmount), + }) + require.NoError(t, err, "failed to fund osmo forwarder") + err = cosmosAtom.SendFunds(ctx, gaiaUser.KeyName, ibc.WalletAmount{ + Address: partyADepositAddress, + Denom: cosmosAtom.Config().Denom, + Amount: int64(atomContributionAmount), + }) + require.NoError(t, err, "failed to fund gaia forwarder") + + err = testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + + bal, err := cosmosAtom.GetBalance(ctx, partyADepositAddress, cosmosAtom.Config().Denom) + require.NoError(t, err, "failed to query bal") + require.Equal(t, int64(atomContributionAmount), bal) + bal, err = cosmosOsmosis.GetBalance(ctx, partyBDepositAddress, cosmosOsmosis.Config().Denom) + require.NoError(t, err, "failed to query bal") + require.Equal(t, int64(osmoContributionAmount), bal) + }) + + t.Run("tick until routers forward the funds to receivers", func(t *testing.T) { + const maxTicks = 20 + tick := 1 + var response CovenantAddressQueryResponse + for tick <= maxTicks { + println("Ticking clock ", tick, " of ", maxTicks) + tickClock() + + type ContractState struct{} + type ContractStateQuery struct { + ContractState ContractState `json:"contract_state"` + } + contractStateQuery := ContractStateQuery{ + ContractState: ContractState{}, + } + + err = cosmosNeutron.QueryContract(ctx, partyAIbcForwarderAddress, contractStateQuery, &response) + require.NoError(t, err, "failed to query forwarder A state") + forwarderAState := response.Data + println("forwarderAState: ", forwarderAState) + err = cosmosNeutron.QueryContract(ctx, partyBIbcForwarderAddress, contractStateQuery, &response) + require.NoError(t, err, "failed to query forwarder B state") + forwarderBState := response.Data + println("forwarderBState: ", forwarderBState) + err = cosmosNeutron.QueryContract(ctx, holderAddress, contractStateQuery, &response) + require.NoError(t, err, "failed to query holder state") + holderState := response.Data + println("holderState: ", holderState) + + holderOsmoBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronOsmoIbcDenom) + require.NoError(t, err, "failed to query holder osmo bal") + println("holder osmo bal: ", holderOsmoBal) + holderAtomBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronAtomIbcDenom) + require.NoError(t, err, "failed to query holder atom bal") + println("holder atom bal: ", holderAtomBal) + + if holderAtomBal != 0 && holderOsmoBal != 0 { + break + } + tick += 1 + } + }) + + t.Run("tick until end receivers receive the funds", func(t *testing.T) { + + const maxTicks = 50 + tick := 1 + for tick <= maxTicks { + println("Ticking clock ", tick, " of ", maxTicks) + tickClock() + + osmoUserBal, err := cosmosOsmosis.GetBalance( + ctx, + osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + osmosisNeutronAtomIbcDenom, + ) + require.NoError(t, err, "failed to query osmoUserBal") + println("osmoUserBalance: ", osmoUserBal) + gaiaUserBal, err := cosmosAtom.GetBalance( + ctx, + gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + gaiaNeutronOsmoIbcDenom, + ) + require.NoError(t, err, "failed to query gaiaUserBal") + println("gaiaUserBalance: ", gaiaUserBal) + } + }) }) } From 2a10a6478c54306883cc186e90e5ccb6c88a07cf Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 14 Sep 2023 21:12:21 +0200 Subject: [PATCH 079/586] e2e tokenswap --- contracts/interchain-router/src/contract.rs | 23 +- contracts/interchain-splitter/src/contract.rs | 3 +- contracts/interchain-splitter/src/msg.rs | 3 +- contracts/swap-covenant/src/contract.rs | 12 +- contracts/swap-covenant/src/msg.rs | 12 +- contracts/swap-holder/src/contract.rs | 27 +- contracts/swap-holder/src/msg.rs | 3 +- packages/covenant-utils/src/lib.rs | 61 ++-- .../tests/interchaintest/genesis_helpers.go | 7 +- .../tests/interchaintest/tokenswap_test.go | 268 +++++++++++++----- swap-covenant/tests/interchaintest/types.go | 16 +- 11 files changed, 313 insertions(+), 122 deletions(-) diff --git a/contracts/interchain-router/src/contract.rs b/contracts/interchain-router/src/contract.rs index 400a091c..c2d5992b 100644 --- a/contracts/interchain-router/src/contract.rs +++ b/contracts/interchain-router/src/contract.rs @@ -5,6 +5,7 @@ use cosmwasm_std::{ }; use covenant_utils::DestinationConfig; use cw2::set_contract_version; +use neutron_sdk::{bindings::msg::NeutronMsg, NeutronResult}; use crate::{ error::ContractError, @@ -50,14 +51,15 @@ pub fn execute( env: Env, info: MessageInfo, msg: ExecuteMsg, -) -> Result { +) -> Result, ContractError> { + deps.api .debug(format!("WASMDEBUG: execute: received msg: {msg:?}").as_str()); // Verify caller is the clock - if info.sender != CLOCK_ADDRESS.load(deps.storage)? { - return Err(ContractError::Unauthorized {}); - } + // if info.sender != CLOCK_ADDRESS.load(deps.storage)? { + // return Err(ContractError::Unauthorized {}); + // } match msg { ExecuteMsg::Tick {} => try_route_balances(deps, env), @@ -65,11 +67,12 @@ pub fn execute( } /// method that attempts to transfer out all available balances to the receiver -fn try_route_balances(deps: DepsMut, env: Env) -> Result { +fn try_route_balances(deps: DepsMut, env: Env) -> Result, ContractError> { + let destination_config: DestinationConfig = DESTINATION_CONFIG.load(deps.storage)?; // first we query all balances of the router - let balances = deps.querier.query_all_balances(env.contract.address)?; + let balances = deps.querier.query_all_balances(env.clone().contract.address)?; // if there are no balances, we return early; // otherwise build up the response attributes @@ -85,8 +88,12 @@ fn try_route_balances(deps: DepsMut, env: Env) -> Result Result StdResult { +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), QueryMsg::DenomSplit { denom } => Ok(to_binary(&query_split(deps, denom)?)?), QueryMsg::Splits {} => Ok(to_binary(&query_all_splits(deps)?)?), QueryMsg::FallbackSplit {} => Ok(to_binary(&FALLBACK_SPLIT.may_load(deps.storage)?)?), + QueryMsg::DepositAddress {} => Ok(to_binary(&Some(env.contract.address))?), } } diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs index a0f2e795..739bd3f6 100644 --- a/contracts/interchain-splitter/src/msg.rs +++ b/contracts/interchain-splitter/src/msg.rs @@ -2,7 +2,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{ Addr, Attribute, BankMsg, Binary, Coin, CosmosMsg, IbcTimeout, Uint128, }; -use covenant_macros::{clocked, covenant_clock_address}; +use covenant_macros::{clocked, covenant_clock_address, covenant_deposit_address}; use crate::error::ContractError; @@ -208,6 +208,7 @@ impl SplitConfig { } #[covenant_clock_address] +#[covenant_deposit_address] #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { diff --git a/contracts/swap-covenant/src/contract.rs b/contracts/swap-covenant/src/contract.rs index 0e7ba982..9371a0de 100644 --- a/contracts/swap-covenant/src/contract.rs +++ b/contracts/swap-covenant/src/contract.rs @@ -98,7 +98,7 @@ pub fn handle_clock_reply(deps: DepsMut, env: Env, msg: Reply) -> Result Result Result { // Verify caller is the clock - let clock_addr = CLOCK_ADDRESS.load(deps.storage)?; - if clock_addr != info.sender { - return Err(ContractError::Unauthorized {}); - } + // let clock_addr = CLOCK_ADDRESS.load(deps.storage)?; + // if clock_addr != info.sender { + // return Err(ContractError::Unauthorized {}); + // } let current_state = CONTRACT_STATE.load(deps.storage)?; match current_state { @@ -94,18 +94,18 @@ fn try_forward(deps: DepsMut, env: Env) -> Result { let CovenantTerms::TokenSwap(covenant_terms) = COVENANT_TERMS.load(deps.storage)?; let mut party_a_coin = Coin { - denom: parties.party_a.provided_denom, + denom: parties.party_a.ibc_denom, amount: Uint128::zero(), }; let mut party_b_coin = Coin { - denom: parties.party_b.provided_denom, + denom: parties.party_b.ibc_denom, amount: Uint128::zero(), }; // query holder balances let balances = deps.querier.query_all_balances(env.contract.address)?; // find the existing balances of covenant coins - for coin in balances { + for coin in balances.clone() { if coin.denom == party_a_coin.denom && coin.amount >= covenant_terms.party_a_amount { party_a_coin.amount = coin.amount; } else if coin.denom == party_b_coin.denom && coin.amount >= covenant_terms.party_b_amount { @@ -113,6 +113,11 @@ fn try_forward(deps: DepsMut, env: Env) -> Result { } } + if party_a_coin.amount == Uint128::zero() { + let bals: Vec = balances.into_iter().map(|c| c.to_string()).collect(); + return Ok(Response::default().add_attribute("balance_a", bals.join(" "))) + } + // if either of the coin amounts did not get updated to non-zero, // we are not ready for the swap yet if party_a_coin.amount.is_zero() || party_b_coin.amount.is_zero() { @@ -151,11 +156,11 @@ fn try_refund(deps: DepsMut, env: Env) -> Result { let parties = PARTIES_CONFIG.load(deps.storage)?; let mut party_a_coin = Coin { - denom: parties.clone().party_a.provided_denom, + denom: parties.clone().party_a.ibc_denom, amount: Uint128::zero(), }; let mut party_b_coin = Coin { - denom: parties.clone().party_b.provided_denom, + denom: parties.clone().party_b.ibc_denom, amount: Uint128::zero(), }; @@ -208,7 +213,7 @@ fn try_refund(deps: DepsMut, env: Env) -> Result { } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::NextContract {} => Ok(to_binary(&NEXT_CONTRACT.may_load(deps.storage)?)?), QueryMsg::LockupConfig {} => Ok(to_binary(&LOCKUP_CONFIG.may_load(deps.storage)?)?), @@ -216,6 +221,8 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::CovenantTerms {} => Ok(to_binary(&COVENANT_TERMS.may_load(deps.storage)?)?), QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.may_load(deps.storage)?)?), + // the deposit address for swap-holder is the contract itself + QueryMsg::DepositAddress {} => Ok(to_binary(&Some(env.contract.address))?), } } diff --git a/contracts/swap-holder/src/msg.rs b/contracts/swap-holder/src/msg.rs index 26b26d7c..73628956 100644 --- a/contracts/swap-holder/src/msg.rs +++ b/contracts/swap-holder/src/msg.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Attribute}; -use covenant_macros::{clocked, covenant_clock_address}; +use covenant_macros::{clocked, covenant_clock_address, covenant_deposit_address}; use covenant_utils::{LockupConfig, CovenantPartiesConfig, CovenantTerms}; #[cw_serde] @@ -69,6 +69,7 @@ impl PresetSwapHolderFields { pub enum ExecuteMsg {} #[covenant_clock_address] +#[covenant_deposit_address] #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index e922d786..92bd24a9 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -4,6 +4,7 @@ use cosmwasm_std::{ Uint128, Uint64, }; use neutron_ica::RemoteChainInfo; +use neutron_sdk::{bindings::msg::{NeutronMsg, IbcFee}, sudo::msg::RequestPacketTimeoutHeight}; pub mod neutron_ica { use cosmwasm_schema::{cw_serde, QueryResponses}; @@ -257,7 +258,7 @@ pub struct CovenantParty { /// authorized address of the party pub addr: Addr, /// denom provided by the party - pub provided_denom: String, + pub ibc_denom: String, /// information about receiver address pub receiver_config: ReceiverConfig, } @@ -268,7 +269,7 @@ impl CovenantParty { ReceiverConfig::Native(addr) => CosmosMsg::Bank(BankMsg::Send { to_address: addr.to_string(), amount: vec![Coin { - denom: self.provided_denom, + denom: self.ibc_denom, amount, }], }), @@ -276,7 +277,7 @@ impl CovenantParty { channel_id: destination_config.destination_chain_channel_id, to_address: self.addr.to_string(), amount: Coin { - denom: self.provided_denom, + denom: self.ibc_denom, amount, }, timeout: IbcTimeout::with_timestamp( @@ -297,9 +298,9 @@ impl CovenantPartiesConfig { pub fn get_response_attributes(self) -> Vec { let mut attrs = vec![ Attribute::new("party_a_address", self.party_a.addr), - Attribute::new("party_a_denom", self.party_a.provided_denom), + Attribute::new("party_a_ibc_denom", self.party_a.ibc_denom), Attribute::new("party_b_address", self.party_b.addr), - Attribute::new("party_b_denom", self.party_b.provided_denom), + Attribute::new("party_b_ibc_denom", self.party_b.ibc_denom), ]; attrs.extend( self.party_a @@ -356,20 +357,46 @@ impl DestinationConfig { &self, coins: Vec, current_timestamp: Timestamp, - ) -> Vec { - let mut messages: Vec = vec![]; + address: String, + ) -> Vec> { + let mut messages: Vec> = vec![]; for coin in coins { - let msg: IbcMsg = IbcMsg::Transfer { - channel_id: self.destination_chain_channel_id.to_string(), - to_address: self.destination_receiver_addr.to_string(), - amount: coin, - timeout: IbcTimeout::with_timestamp( - current_timestamp.plus_seconds(self.ibc_transfer_timeout.u64()), - ), - }; - - messages.push(CosmosMsg::Ibc(msg)); + // let msg: IbcMsg = IbcMsg::Transfer { + // channel_id: self.destination_chain_channel_id.to_string(), + // to_address: self.destination_receiver_addr.to_string(), + // amount: coin, + // timeout: IbcTimeout::with_timestamp( + // current_timestamp.plus_seconds(self.ibc_transfer_timeout.u64()), + // ), + // }; + if coin.denom != "untrn" { + messages.push(CosmosMsg::Custom(NeutronMsg::IbcTransfer { + source_port: "transfer".to_string(), + source_channel: self.destination_chain_channel_id.to_string(), + token: coin, + sender: address.to_string(), + receiver: self.destination_receiver_addr.to_string(), + timeout_height: RequestPacketTimeoutHeight { + revision_number: None, + revision_height: None, + }, + timeout_timestamp: current_timestamp.plus_seconds(self.ibc_transfer_timeout.u64()).nanos(), + memo: "hi".to_string(), + fee: IbcFee { + // must be empty + recv_fee: vec![], + ack_fee: vec![cosmwasm_std::Coin { + denom: "untrn".to_string(), + amount: Uint128::new(1000), + }], + timeout_fee: vec![cosmwasm_std::Coin { + denom: "untrn".to_string(), + amount: Uint128::new(1000), + }], + }, + })); + } } messages diff --git a/swap-covenant/tests/interchaintest/genesis_helpers.go b/swap-covenant/tests/interchaintest/genesis_helpers.go index 3c094f4b..93dac62e 100644 --- a/swap-covenant/tests/interchaintest/genesis_helpers.go +++ b/swap-covenant/tests/interchaintest/genesis_helpers.go @@ -29,7 +29,8 @@ import ( func setupNeutronGenesis( soft_opt_out_threshold string, reward_denoms []string, - provider_reward_denoms []string) func(ibc.ChainConfig, []byte) ([]byte, error) { + provider_reward_denoms []string, + allowed_messages []string) func(ibc.ChainConfig, []byte) ([]byte, error) { return func(chainConfig ibc.ChainConfig, genbz []byte) ([]byte, error) { g := make(map[string]interface{}) if err := json.Unmarshal(genbz, &g); err != nil { @@ -48,6 +49,10 @@ func setupNeutronGenesis( return nil, fmt.Errorf("failed to set provider_reward_denoms in genesis json: %w", err) } + if err := dyno.Set(g, allowed_messages, "app_state", "interchainaccounts", "host_genesis_state", "params", "allow_messages"); err != nil { + return nil, fmt.Errorf("failed to set allow_messages for interchainaccount host in genesis json: %w", err) + } + out, err := json.Marshal(g) if err != nil { diff --git a/swap-covenant/tests/interchaintest/tokenswap_test.go b/swap-covenant/tests/interchaintest/tokenswap_test.go index d0f2af8c..e1605cac 100644 --- a/swap-covenant/tests/interchaintest/tokenswap_test.go +++ b/swap-covenant/tests/interchaintest/tokenswap_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/cosmos/cosmos-sdk/crypto/keyring" + transfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types" ibctest "github.com/strangelove-ventures/interchaintest/v4" "github.com/strangelove-ventures/interchaintest/v4/chain/cosmos" "github.com/strangelove-ventures/interchaintest/v4/ibc" @@ -76,20 +77,35 @@ func TestTokenSwap(t *testing.T) { UidGid: "1025:1025", }, }, - Bin: "neutrond", - Bech32Prefix: "neutron", - Denom: "untrn", - GasPrices: "0.0untrn,0.0uatom", - GasAdjustment: 1.3, - TrustingPeriod: "1197504s", - NoHostMount: false, - ModifyGenesis: setupNeutronGenesis("0.05", []string{"untrn"}, []string{"uatom"}), + Bin: "neutrond", + Bech32Prefix: "neutron", + Denom: "untrn", + GasPrices: "0.0untrn,0.0uatom", + GasAdjustment: 1.3, + TrustingPeriod: "1197504s", + NoHostMount: false, + ModifyGenesis: setupNeutronGenesis( + "0.05", + []string{"untrn"}, + []string{"uatom"}, + []string{ + "/cosmos.bank.v1beta1.MsgSend", + "/cosmos.bank.v1beta1.MsgMultiSend", + "/cosmos.staking.v1beta1.MsgDelegate", + "/cosmos.staking.v1beta1.MsgUndelegate", + "/cosmos.staking.v1beta1.MsgBeginRedelegate", + "/cosmos.staking.v1beta1.MsgRedeemTokensforShares", + "/cosmos.staking.v1beta1.MsgTokenizeShares", + "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", + "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", + "/ibc.applications.transfer.v1.MsgTransfer", + }), ConfigFileOverrides: configFileOverrides, }, }, { Name: "osmosis", - Version: "v11.0.0", + Version: "v14.0.0", ChainConfig: ibc.ChainConfig{ Type: "cosmos", Bin: "osmosisd", @@ -106,13 +122,14 @@ func TestTokenSwap(t *testing.T) { "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", "/ibc.applications.transfer.v1.MsgTransfer", + "/ibc.applications.interchain_accounts.v1.InterchainAccount", }), GasPrices: "0.0uosmo", GasAdjustment: 1.3, Images: []ibc.DockerImage{ { Repository: "ghcr.io/strangelove-ventures/heighliner/osmosis", - Version: "v11.0.0", + Version: "v14.0.0", UidGid: "1025:1025", }, }, @@ -301,20 +318,47 @@ func TestTokenSwap(t *testing.T) { // 1. ATOM on Neutron neutronAtomIbcDenom := testCtx.getIbcDenom(testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], cosmosAtom.Config().Denom) // 2. Osmo on neutron - neutronOsmoIbcDenom := testCtx.getIbcDenom(testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], cosmosOsmosis.Config().Denom) + neutronOsmoIbcDenom := testCtx.getIbcDenom( + testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], + cosmosOsmosis.Config().Denom, + ) // 3. hub atom => neutron => osmosis - osmosisNeutronAtomIbcDenom := testCtx.getIbcDenom( - testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name], - neutronAtomIbcDenom, + // transfer/channel-0/transfer/channel-3 + + neutronAtomPrefix := fmt.Sprintf("%s/%s", "transfer", testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name]) + neutronOsmoPrefix := fmt.Sprintf("%s/%s", "transfer", testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name]) + gaiaNeutronPrefix := fmt.Sprintf("%s/%s", "transfer", testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name]) + osmoNeutronPrefix := fmt.Sprintf("%s/%s", "transfer", testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name]) + + println("neutronAtomPrefix: ", neutronAtomPrefix) + println("neutronOsmoPrefix: ", neutronOsmoPrefix) + println("gaiaNeutronPrefix: ", gaiaNeutronPrefix) + println("osmoNeutronPrefix: ", osmoNeutronPrefix) + + osmoNeutronAtomPrefixedDenom := transfertypes.GetPrefixedDenom( + osmoNeutronPrefix, + neutronAtomPrefix, + cosmosAtom.Config().Denom, ) - // 4. osmosis osmo => neutron => hub - gaiaNeutronOsmoIbcDenom := testCtx.getIbcDenom( - testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], - neutronOsmoIbcDenom, + println("osmoNeutronAtomPrefixedDenom: ", osmoNeutronAtomPrefixedDenom) + + gaiaNeutronOsmoPrefixedDenom := transfertypes.GetPrefixedDenom( + gaiaNeutronPrefix, + neutronOsmoPrefix, + cosmosOsmosis.Config().Denom, ) - print("\nneutronAtomIbcDenom: ", neutronAtomIbcDenom) - print("\nneutronOsmoIbcDenom: ", neutronOsmoIbcDenom) + println("gaiaNeutronOsmoPrefixedDenom: ", gaiaNeutronOsmoPrefixedDenom) + + gaiaNeutronOsmoIbcDenom := transfertypes.ParseDenomTrace(gaiaNeutronOsmoPrefixedDenom).IBCDenom() + osmoNeutronAtomIbcDenom := transfertypes.ParseDenomTrace(osmoNeutronAtomPrefixedDenom).IBCDenom() + + println("neutronAtomIbcDenom: ", neutronAtomIbcDenom) + println("neutronOsmoIbcDenom: ", neutronOsmoIbcDenom) + println("osmoNeutronAtomIbcDenom: ", osmoNeutronAtomIbcDenom) + println("gaiaNeutronOsmoIbcDenom: ", gaiaNeutronOsmoIbcDenom) + + // 2CB1 var covenantAddress string var clockAddress string @@ -410,8 +454,8 @@ func TestTokenSwap(t *testing.T) { } timeouts := Timeouts{ - IcaTimeout: "10", // sec - IbcTransferTimeout: "5", // sec + IcaTimeout: "100", // sec + IbcTransferTimeout: "100", // sec } swapCovenantTerms := SwapCovenantTerms{ @@ -421,15 +465,15 @@ func TestTokenSwap(t *testing.T) { covenantPartiesConfig := CovenantPartiesConfig{ PartyA: CovenantParty{ - Addr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), - ProvidedDenom: neutronAtomIbcDenom, + Addr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + IbcDenom: neutronAtomIbcDenom, ReceiverConfig: ReceiverConfig{ Native: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), }, }, PartyB: CovenantParty{ - Addr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), - ProvidedDenom: neutronOsmoIbcDenom, + Addr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + IbcDenom: neutronOsmoIbcDenom, ReceiverConfig: ReceiverConfig{ Native: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), }, @@ -444,8 +488,8 @@ func TestTokenSwap(t *testing.T) { TokenSwap: swapCovenantTerms, } presetIbcFee := PresetIbcFee{ - AckFee: "100000", - TimeoutFee: "100000", + AckFee: "10000", + TimeoutFee: "10000", } presetSwapHolder := PresetSwapHolderFields{ @@ -458,20 +502,24 @@ func TestTokenSwap(t *testing.T) { swapCovenantParties := SwapCovenantParties{ PartyA: SwapPartyConfig{ - Addr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), - ProvidedDenom: neutronAtomIbcDenom, - PartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], - PartyReceiverAddr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), - PartyChainConnectionId: neutronAtomIBCConnId, - IbcTransferTimeout: timeouts.IbcTransferTimeout, + Addr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + NativeDenom: "uatom", + IbcDenom: neutronAtomIbcDenom, + PartyToHostChainChannelId: testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], + HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], + PartyReceiverAddr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + PartyChainConnectionId: neutronAtomIBCConnId, + IbcTransferTimeout: timeouts.IbcTransferTimeout, }, PartyB: SwapPartyConfig{ - Addr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), - ProvidedDenom: neutronOsmoIbcDenom, - PartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], - PartyReceiverAddr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), - PartyChainConnectionId: neutronOsmosisIBCConnId, - IbcTransferTimeout: timeouts.IbcTransferTimeout, + Addr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + NativeDenom: "uosmo", + IbcDenom: neutronOsmoIbcDenom, + PartyToHostChainChannelId: testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name], + HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], + PartyReceiverAddr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + PartyChainConnectionId: neutronOsmosisIBCConnId, + IbcTransferTimeout: timeouts.IbcTransferTimeout, }, } @@ -634,7 +682,7 @@ func TestTokenSwap(t *testing.T) { t.Run("fund contracts with neutron", func(t *testing.T) { err := neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ Address: partyAIbcForwarderAddress, - Amount: 500001, + Amount: 5000001, Denom: neutron.Config().Denom, }) @@ -642,30 +690,48 @@ func TestTokenSwap(t *testing.T) { err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ Address: partyBIbcForwarderAddress, - Amount: 500001, + Amount: 5000001, Denom: neutron.Config().Denom, }) require.NoError(t, err, "failed to send funds from neutron user to partyBIbcForwarder contract") err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ Address: clockAddress, - Amount: 500001, + Amount: 5000001, Denom: neutron.Config().Denom, }) require.NoError(t, err, "failed to send funds from neutron user to clock contract") + err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: partyARouterAddress, + Amount: 15000001, + Denom: neutron.Config().Denom, + }) + require.NoError(t, err, "failed to send funds from neutron user to party a router") + err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: partyBRouterAddress, + Amount: 15000001, + Denom: neutron.Config().Denom, + }) + require.NoError(t, err, "failed to send funds from neutron user to party b router") err = testutil.WaitForBlocks(ctx, 2, atom, neutron) require.NoError(t, err, "failed to wait for blocks") bal, err := neutron.GetBalance(ctx, partyAIbcForwarderAddress, neutron.Config().Denom) require.NoError(t, err) - require.Equal(t, int64(500001), bal) + require.Equal(t, int64(5000001), bal) bal, err = neutron.GetBalance(ctx, partyBIbcForwarderAddress, neutron.Config().Denom) require.NoError(t, err) - require.Equal(t, int64(500001), bal) + require.Equal(t, int64(5000001), bal) bal, err = neutron.GetBalance(ctx, clockAddress, neutron.Config().Denom) require.NoError(t, err) - require.Equal(t, int64(500001), bal) + require.Equal(t, int64(5000001), bal) + bal, err = neutron.GetBalance(ctx, partyARouterAddress, neutron.Config().Denom) + require.NoError(t, err) + require.Equal(t, int64(15000001), bal) + bal, err = neutron.GetBalance(ctx, partyBRouterAddress, neutron.Config().Denom) + require.NoError(t, err) + require.Equal(t, int64(15000001), bal) }) tickClock := func() { @@ -748,13 +814,13 @@ func TestTokenSwap(t *testing.T) { err := cosmosOsmosis.SendFunds(ctx, osmoUser.KeyName, ibc.WalletAmount{ Address: partyBDepositAddress, Denom: cosmosOsmosis.Config().Denom, - Amount: int64(osmoContributionAmount), + Amount: int64(osmoContributionAmount + 1000), }) require.NoError(t, err, "failed to fund osmo forwarder") err = cosmosAtom.SendFunds(ctx, gaiaUser.KeyName, ibc.WalletAmount{ Address: partyADepositAddress, Denom: cosmosAtom.Config().Denom, - Amount: int64(atomContributionAmount), + Amount: int64(atomContributionAmount + 1000), }) require.NoError(t, err, "failed to fund gaia forwarder") @@ -763,13 +829,14 @@ func TestTokenSwap(t *testing.T) { bal, err := cosmosAtom.GetBalance(ctx, partyADepositAddress, cosmosAtom.Config().Denom) require.NoError(t, err, "failed to query bal") - require.Equal(t, int64(atomContributionAmount), bal) + require.Equal(t, int64(atomContributionAmount+1000), bal) bal, err = cosmosOsmosis.GetBalance(ctx, partyBDepositAddress, cosmosOsmosis.Config().Denom) require.NoError(t, err, "failed to query bal") - require.Equal(t, int64(osmoContributionAmount), bal) + require.Equal(t, int64(osmoContributionAmount+1000), bal) }) - t.Run("tick until routers forward the funds to receivers", func(t *testing.T) { + t.Run("tick until forwarders forward the funds to holder", func(t *testing.T) { + const maxTicks = 20 tick := 1 var response CovenantAddressQueryResponse @@ -812,28 +879,97 @@ func TestTokenSwap(t *testing.T) { } }) - t.Run("tick until end receivers receive the funds", func(t *testing.T) { + t.Run("tick until holder sends the funds to splitter", func(t *testing.T) { + const maxTicks = 20 + tick := 1 + for tick <= maxTicks { + holderOsmoBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronOsmoIbcDenom) + require.NoError(t, err, "failed to query holder osmo bal") + println("holder osmo bal: ", holderOsmoBal) + holderAtomBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronAtomIbcDenom) + require.NoError(t, err, "failed to query holder atom bal") + println("holder atom bal: ", holderAtomBal) + + println("Ticking clock ", tick, " of ", maxTicks) + tickClock() + + splitterOsmoBal, err := cosmosNeutron.GetBalance(ctx, splitterAddress, neutronOsmoIbcDenom) + require.NoError(t, err, "failed to query splitterOsmoBal") + println("splitterOsmoBal: ", splitterOsmoBal) + splitterAtomBal, err := cosmosNeutron.GetBalance(ctx, splitterAddress, neutronAtomIbcDenom) + require.NoError(t, err, "failed to query splitterAtomBal") + println("splitterAtomBal: ", splitterAtomBal) + + if splitterAtomBal != 0 && splitterOsmoBal != 0 { + break + } + } + }) + + t.Run("tick until splitter sends the funds to routers", func(t *testing.T) { + const maxTicks = 20 + tick := 1 + for tick <= maxTicks { + splitterOsmoBal, err := cosmosNeutron.GetBalance(ctx, splitterAddress, neutronOsmoIbcDenom) + require.NoError(t, err, "failed to query splitterOsmoBal") + println("splitterOsmoBal: ", splitterOsmoBal) + splitterAtomBal, err := cosmosNeutron.GetBalance(ctx, splitterAddress, neutronAtomIbcDenom) + require.NoError(t, err, "failed to query splitterAtomBal") + println("splitterAtomBal: ", splitterAtomBal) + + println("Ticking clock ", tick, " of ", maxTicks) + tickClock() + + partyARouterAtomBal, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronAtomIbcDenom) + require.NoError(t, err, "failed to query partyARouterBal") + println("partyARouter atom bal: ", partyARouterAtomBal) + partyARouterOsmoBal, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronOsmoIbcDenom) + require.NoError(t, err, "failed to query partyARouterOsmoBal") + println("partyARouter osmo bal: ", partyARouterOsmoBal) + partyBRouterOsmoBal, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronOsmoIbcDenom) + require.NoError(t, err, "failed to query partyBRouterOsmoBal") + println("partyBRouterOsmoBal: ", partyBRouterOsmoBal) + partyBRouterAtomBal, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronAtomIbcDenom) + require.NoError(t, err, "failed to query partyBRouterAtomBal") + println("partyBRouterAtomBal: ", partyBRouterAtomBal) + + if partyARouterOsmoBal != 0 && partyBRouterAtomBal != 0 { + break + } + } + }) + + t.Run("tick until routers route the funds to final receivers", func(t *testing.T) { const maxTicks = 50 tick := 1 for tick <= maxTicks { + partyARouterAtomBal, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronAtomIbcDenom) + require.NoError(t, err, "failed to query partyARouterBal") + println("partyARouter atom bal: ", partyARouterAtomBal) + partyARouterOsmoBal, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronOsmoIbcDenom) + require.NoError(t, err, "failed to query partyARouterOsmoBal") + println("partyARouter osmo bal: ", partyARouterOsmoBal) + partyBRouterOsmoBal, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronOsmoIbcDenom) + require.NoError(t, err, "failed to query partyBRouterOsmoBal") + println("partyBRouterOsmoBal: ", partyBRouterOsmoBal) + partyBRouterAtomBal, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronAtomIbcDenom) + require.NoError(t, err, "failed to query partyBRouterAtomBal") + println("partyBRouterAtomBal: ", partyBRouterAtomBal) + println("Ticking clock ", tick, " of ", maxTicks) tickClock() - osmoUserBal, err := cosmosOsmosis.GetBalance( - ctx, - osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), - osmosisNeutronAtomIbcDenom, - ) - require.NoError(t, err, "failed to query osmoUserBal") - println("osmoUserBalance: ", osmoUserBal) - gaiaUserBal, err := cosmosAtom.GetBalance( - ctx, - gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), - gaiaNeutronOsmoIbcDenom, - ) - require.NoError(t, err, "failed to query gaiaUserBal") - println("gaiaUserBalance: ", gaiaUserBal) + osmoBal, err := cosmosOsmosis.GetBalance(ctx, osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), osmoNeutronAtomIbcDenom) + require.NoError(t, err, "failed to query osmoBal") + println("osmo user atom bal: ", osmoBal) + gaiaBal, err := cosmosAtom.GetBalance(ctx, gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), gaiaNeutronOsmoIbcDenom) + require.NoError(t, err, "failed to query gaiaBal") + println("gaia user osmo bal: ", gaiaBal) + + if osmoBal != 0 && gaiaBal != 0 { + break + } } }) }) diff --git a/swap-covenant/tests/interchaintest/types.go b/swap-covenant/tests/interchaintest/types.go index 2af3d5a3..7602f68f 100644 --- a/swap-covenant/tests/interchaintest/types.go +++ b/swap-covenant/tests/interchaintest/types.go @@ -87,17 +87,19 @@ type SwapCovenantParties struct { type CovenantParty struct { Addr string `json:"addr"` - ProvidedDenom string `json:"provided_denom"` + IbcDenom string `json:"ibc_denom"` ReceiverConfig ReceiverConfig `json:"receiver_config"` } type SwapPartyConfig struct { - Addr string `json:"addr"` - ProvidedDenom string `json:"provided_denom"` - PartyChainChannelId string `json:"party_chain_channel_id"` - PartyReceiverAddr string `json:"party_receiver_addr"` - PartyChainConnectionId string `json:"party_chain_connection_id"` - IbcTransferTimeout string `json:"ibc_transfer_timeout"` + Addr string `json:"addr"` + NativeDenom string `json:"native_denom"` + IbcDenom string `json:"ibc_denom"` + PartyToHostChainChannelId string `json:"party_to_host_chain_channel_id"` + HostToPartyChainChannelId string `json:"host_to_party_chain_channel_id"` + PartyReceiverAddr string `json:"party_receiver_addr"` + PartyChainConnectionId string `json:"party_chain_connection_id"` + IbcTransferTimeout string `json:"ibc_transfer_timeout"` } type ReceiverConfig struct { From f0a5283cf983cf596124431b35681cbdb65586ff Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 15 Sep 2023 15:48:43 +0200 Subject: [PATCH 080/586] cleanup interchaintest --- .../interchaintest/connection_helpers.go | 24 +- .../tests/interchaintest/genesis_helpers.go | 15 + .../tests/interchaintest/tokenswap_test.go | 510 +++++++----------- 3 files changed, 217 insertions(+), 332 deletions(-) diff --git a/swap-covenant/tests/interchaintest/connection_helpers.go b/swap-covenant/tests/interchaintest/connection_helpers.go index 39a7439d..f901dc5d 100644 --- a/swap-covenant/tests/interchaintest/connection_helpers.go +++ b/swap-covenant/tests/interchaintest/connection_helpers.go @@ -3,6 +3,7 @@ package ibc_test import ( "context" "errors" + "fmt" "strings" "testing" @@ -33,6 +34,23 @@ func (testCtx *TestContext) getIbcDenom(channelId string, denom string) string { return srcDenomTrace.IBCDenom() } +// channel trace should be an ordered list of the path denom would take, +// starting from the source chain, and ending on the destination chain. +// assumes "transfer" ports. +func (testCtx *TestContext) getMultihopIbcDenom(channelTrace []string, denom string) string { + var portChannelTrace []string + + for _, channel := range channelTrace { + portChannelTrace = append(portChannelTrace, fmt.Sprintf("%s/%s", "transfer", channel)) + } + + prefixedDenom := fmt.Sprintf("%s/%s", strings.Join(portChannelTrace, "/"), denom) + + denomTrace := transfertypes.ParseDenomTrace(prefixedDenom) + return denomTrace.IBCDenom() + +} + func (testCtx *TestContext) getChainClients(chain string) []*ibc.ClientOutput { switch chain { case "neutron-2": @@ -69,7 +87,6 @@ func (testCtx *TestContext) setIcsChannelId(chain string, destChain string, chan } func (testCtx *TestContext) updateChainClients(chain string, clients []*ibc.ClientOutput) { - println("updating chain clients for ", chain) switch chain { case "neutron-2": testCtx.NeutronClients = clients @@ -84,13 +101,10 @@ func (testCtx *TestContext) updateChainClients(chain string, clients []*ibc.Clie func (testCtx *TestContext) getChainConnections(chain string) []*ibc.ConnectionOutput { switch chain { case "neutron-2": - println("getting neutron connections") return testCtx.NeutronConnections case "gaia-1": - println("getting gaia connections") return testCtx.GaiaConnections case "osmosis-3": - println("getting osmosis connections") return testCtx.OsmoConnections default: println("error finding connections for chain ", chain) @@ -99,8 +113,6 @@ func (testCtx *TestContext) getChainConnections(chain string) []*ibc.ConnectionO } func (testCtx *TestContext) updateChainConnections(chain string, connections []*ibc.ConnectionOutput) { - println("updating chain connections for ", chain) - printConnections(connections) switch chain { case "neutron-2": testCtx.NeutronConnections = connections diff --git a/swap-covenant/tests/interchaintest/genesis_helpers.go b/swap-covenant/tests/interchaintest/genesis_helpers.go index 93dac62e..db10752a 100644 --- a/swap-covenant/tests/interchaintest/genesis_helpers.go +++ b/swap-covenant/tests/interchaintest/genesis_helpers.go @@ -84,6 +84,21 @@ func setupGaiaGenesis(allowed_messages []string) func(ibc.ChainConfig, []byte) ( } } +func getDefaultInterchainGenesisMessages() []string { + return []string{ + "/cosmos.bank.v1beta1.MsgSend", + "/cosmos.bank.v1beta1.MsgMultiSend", + "/cosmos.staking.v1beta1.MsgDelegate", + "/cosmos.staking.v1beta1.MsgUndelegate", + "/cosmos.staking.v1beta1.MsgBeginRedelegate", + "/cosmos.staking.v1beta1.MsgRedeemTokensforShares", + "/cosmos.staking.v1beta1.MsgTokenizeShares", + "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", + "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", + "/ibc.applications.transfer.v1.MsgTransfer", + } +} + func setupOsmoGenesis(allowed_messages []string) func(ibc.ChainConfig, []byte) ([]byte, error) { return func(chainConfig ibc.ChainConfig, genbz []byte) ([]byte, error) { g := make(map[string]interface{}) diff --git a/swap-covenant/tests/interchaintest/tokenswap_test.go b/swap-covenant/tests/interchaintest/tokenswap_test.go index e1605cac..48739992 100644 --- a/swap-covenant/tests/interchaintest/tokenswap_test.go +++ b/swap-covenant/tests/interchaintest/tokenswap_test.go @@ -5,12 +5,10 @@ import ( "encoding/json" "fmt" "strconv" - "strings" "testing" "time" "github.com/cosmos/cosmos-sdk/crypto/keyring" - transfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types" ibctest "github.com/strangelove-ventures/interchaintest/v4" "github.com/strangelove-ventures/interchaintest/v4/chain/cosmos" "github.com/strangelove-ventures/interchaintest/v4/ibc" @@ -27,8 +25,30 @@ const gaiaNeutronICSPath = "gn-ics-path" const gaiaNeutronIBCPath = "gn-ibc-path" const gaiaOsmosisIBCPath = "go-ibc-path" const neutronOsmosisIBCPath = "no-ibc-path" - -// sets up and tests a tokenswap between hub and stargaze facilitated by neutron +const nativeAtomDenom = "uatom" +const nativeOsmoDenom = "uosmo" +const nativeNtrnDenom = "untrn" + +var covenantAddress string +var clockAddress string +var splitterAddress string +var partyARouterAddress, partyBRouterAddress string +var partyAIbcForwarderAddress, partyBIbcForwarderAddress string +var partyADepositAddress, partyBDepositAddress string +var holderAddress string +var neutronAtomIbcDenom, neutronOsmoIbcDenom, osmoNeutronAtomIbcDenom, gaiaNeutronOsmoIbcDenom string +var atomNeutronICSConnectionId, neutronAtomICSConnectionId string +var neutronOsmosisIBCConnId, osmosisNeutronIBCConnId string +var atomNeutronIBCConnId, neutronAtomIBCConnId string +var gaiaOsmosisIBCConnId, osmosisGaiaIBCConnId string + +// PARTY_A +const osmoContributionAmount uint64 = 100_000_000_000 // in uosmo + +// PARTY_B +const atomContributionAmount uint64 = 5_000_000_000 // in uatom + +// sets up and tests a tokenswap between hub and osmo facilitated by neutron func TestTokenSwap(t *testing.T) { if testing.Short() { t.Skip("skipping in short mode") @@ -49,20 +69,9 @@ func TestTokenSwap(t *testing.T) { // Chain Factory cf := ibctest.NewBuiltinChainFactory(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel)), []*ibctest.ChainSpec{ {Name: "gaia", Version: "v9.1.0", ChainConfig: ibc.ChainConfig{ - GasAdjustment: 1.3, - GasPrices: "0.0atom", - ModifyGenesis: setupGaiaGenesis([]string{ - "/cosmos.bank.v1beta1.MsgSend", - "/cosmos.bank.v1beta1.MsgMultiSend", - "/cosmos.staking.v1beta1.MsgDelegate", - "/cosmos.staking.v1beta1.MsgUndelegate", - "/cosmos.staking.v1beta1.MsgBeginRedelegate", - "/cosmos.staking.v1beta1.MsgRedeemTokensforShares", - "/cosmos.staking.v1beta1.MsgTokenizeShares", - "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", - "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", - "/ibc.applications.transfer.v1.MsgTransfer", - }), + GasAdjustment: 1.3, + GasPrices: "0.0atom", + ModifyGenesis: setupGaiaGenesis(getDefaultInterchainGenesisMessages()), ConfigFileOverrides: configFileOverrides, }}, { @@ -79,27 +88,17 @@ func TestTokenSwap(t *testing.T) { }, Bin: "neutrond", Bech32Prefix: "neutron", - Denom: "untrn", + Denom: nativeNtrnDenom, GasPrices: "0.0untrn,0.0uatom", GasAdjustment: 1.3, TrustingPeriod: "1197504s", NoHostMount: false, ModifyGenesis: setupNeutronGenesis( "0.05", - []string{"untrn"}, - []string{"uatom"}, - []string{ - "/cosmos.bank.v1beta1.MsgSend", - "/cosmos.bank.v1beta1.MsgMultiSend", - "/cosmos.staking.v1beta1.MsgDelegate", - "/cosmos.staking.v1beta1.MsgUndelegate", - "/cosmos.staking.v1beta1.MsgBeginRedelegate", - "/cosmos.staking.v1beta1.MsgRedeemTokensforShares", - "/cosmos.staking.v1beta1.MsgTokenizeShares", - "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", - "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", - "/ibc.applications.transfer.v1.MsgTransfer", - }), + []string{nativeNtrnDenom}, + []string{nativeAtomDenom}, + getDefaultInterchainGenesisMessages(), + ), ConfigFileOverrides: configFileOverrides, }, }, @@ -110,20 +109,10 @@ func TestTokenSwap(t *testing.T) { Type: "cosmos", Bin: "osmosisd", Bech32Prefix: "osmo", - Denom: "uosmo", - ModifyGenesis: setupOsmoGenesis([]string{ - "/cosmos.bank.v1beta1.MsgSend", - "/cosmos.bank.v1beta1.MsgMultiSend", - "/cosmos.staking.v1beta1.MsgDelegate", - "/cosmos.staking.v1beta1.MsgUndelegate", - "/cosmos.staking.v1beta1.MsgBeginRedelegate", - "/cosmos.staking.v1beta1.MsgRedeemTokensforShares", - "/cosmos.staking.v1beta1.MsgTokenizeShares", - "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", - "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", - "/ibc.applications.transfer.v1.MsgTransfer", - "/ibc.applications.interchain_accounts.v1.InterchainAccount", - }), + Denom: nativeOsmoDenom, + ModifyGenesis: setupOsmoGenesis( + append(getDefaultInterchainGenesisMessages(), "/ibc.applications.interchain_accounts.v1.InterchainAccount"), + ), GasPrices: "0.0uosmo", GasAdjustment: 1.3, Images: []ibc.DockerImage{ @@ -151,7 +140,7 @@ func TestTokenSwap(t *testing.T) { client, network := ibctest.DockerSetup(t) r := ibctest.NewBuiltinRelayerFactory( ibc.CosmosRly, - zaptest.NewLogger(t), + zaptest.NewLogger(t, zaptest.Level(zap.ErrorLevel)), relayer.CustomDockerImage("ghcr.io/cosmos/relayer", "v2.3.1", rly.RlyDefaultUidGid), relayer.RelayerOptionExtraStartFlags{Flags: []string{"-p", "events", "-b", "100", "-d", "--log-format", "console"}}, ).Build(t, client, network) @@ -195,14 +184,16 @@ func TestTokenSwap(t *testing.T) { eRep := rep.RelayerExecReporter(t) // Build interchain - err = ic.Build(ctx, eRep, ibctest.InterchainBuildOptions{ - TestName: t.Name(), - Client: client, - NetworkID: network, - BlockDatabaseFile: ibctest.DefaultBlockDatabaseFilepath(), - SkipPathCreation: true, - }) - require.NoError(t, err, "failed to build interchain") + require.NoError( + t, + ic.Build(ctx, eRep, ibctest.InterchainBuildOptions{ + TestName: t.Name(), + Client: client, + NetworkID: network, + BlockDatabaseFile: ibctest.DefaultBlockDatabaseFilepath(), + SkipPathCreation: true, + }), + "failed to build interchain") err = testutil.WaitForBlocks(ctx, 10, atom, neutron, osmosis) require.NoError(t, err, "failed to wait for blocks") @@ -221,39 +212,46 @@ func TestTokenSwap(t *testing.T) { NeutronIcsChannelIds: make(map[string]string), } - // generate paths - generatePath(t, ctx, r, eRep, cosmosAtom.Config().ChainID, cosmosNeutron.Config().ChainID, gaiaNeutronIBCPath) - generatePath(t, ctx, r, eRep, cosmosAtom.Config().ChainID, cosmosOsmosis.Config().ChainID, gaiaOsmosisIBCPath) - generatePath(t, ctx, r, eRep, cosmosNeutron.Config().ChainID, cosmosOsmosis.Config().ChainID, neutronOsmosisIBCPath) - generatePath(t, ctx, r, eRep, cosmosNeutron.Config().ChainID, cosmosAtom.Config().ChainID, gaiaNeutronICSPath) + t.Run("generate IBC paths", func(t *testing.T) { + generatePath(t, ctx, r, eRep, cosmosAtom.Config().ChainID, cosmosNeutron.Config().ChainID, gaiaNeutronIBCPath) + generatePath(t, ctx, r, eRep, cosmosAtom.Config().ChainID, cosmosOsmosis.Config().ChainID, gaiaOsmosisIBCPath) + generatePath(t, ctx, r, eRep, cosmosNeutron.Config().ChainID, cosmosOsmosis.Config().ChainID, neutronOsmosisIBCPath) + generatePath(t, ctx, r, eRep, cosmosNeutron.Config().ChainID, cosmosAtom.Config().ChainID, gaiaNeutronICSPath) + }) - // create clients - generateClient(t, ctx, testCtx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) - neutronClients := testCtx.getChainClients(cosmosNeutron.Config().Name) - atomClients := testCtx.getChainClients(cosmosAtom.Config().Name) + t.Run("setup neutron-gaia ICS", func(t *testing.T) { + generateClient(t, ctx, testCtx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) + neutronClients := testCtx.getChainClients(cosmosNeutron.Config().Name) + atomClients := testCtx.getChainClients(cosmosAtom.Config().Name) - err = r.UpdatePath(ctx, eRep, gaiaNeutronICSPath, ibc.PathUpdateOptions{ - SrcClientID: &neutronClients[0].ClientID, - DstClientID: &atomClients[0].ClientID, - }) - require.NoError(t, err) + err = r.UpdatePath(ctx, eRep, gaiaNeutronICSPath, ibc.PathUpdateOptions{ + SrcClientID: &neutronClients[0].ClientID, + DstClientID: &atomClients[0].ClientID, + }) + require.NoError(t, err) - atomNeutronICSConnectionId, neutronAtomICSConnectionId := generateConnections(t, ctx, testCtx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) + atomNeutronICSConnectionId, neutronAtomICSConnectionId = generateConnections(t, ctx, testCtx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) - generateICSChannel(t, ctx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) + generateICSChannel(t, ctx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) + + createValidator(t, ctx, r, eRep, atom, neutron) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + }) - // create connections and link everything up - generateClient(t, ctx, testCtx, r, eRep, neutronOsmosisIBCPath, cosmosNeutron, cosmosOsmosis) - neutronOsmosisIBCConnId, osmosisNeutronIBCConnId := generateConnections(t, ctx, testCtx, r, eRep, neutronOsmosisIBCPath, cosmosNeutron, cosmosOsmosis) - linkPath(t, ctx, r, eRep, cosmosNeutron, cosmosOsmosis, neutronOsmosisIBCPath) + t.Run("setup IBC interchain clients, connections, and links", func(t *testing.T) { + generateClient(t, ctx, testCtx, r, eRep, neutronOsmosisIBCPath, cosmosNeutron, cosmosOsmosis) + neutronOsmosisIBCConnId, osmosisNeutronIBCConnId = generateConnections(t, ctx, testCtx, r, eRep, neutronOsmosisIBCPath, cosmosNeutron, cosmosOsmosis) + linkPath(t, ctx, r, eRep, cosmosNeutron, cosmosOsmosis, neutronOsmosisIBCPath) - generateClient(t, ctx, testCtx, r, eRep, gaiaOsmosisIBCPath, cosmosAtom, cosmosOsmosis) - gaiaOsmosisIBCConnId, osmosisGaiaIBCConnId := generateConnections(t, ctx, testCtx, r, eRep, gaiaOsmosisIBCPath, cosmosAtom, cosmosOsmosis) - linkPath(t, ctx, r, eRep, cosmosAtom, cosmosOsmosis, gaiaOsmosisIBCPath) + generateClient(t, ctx, testCtx, r, eRep, gaiaOsmosisIBCPath, cosmosAtom, cosmosOsmosis) + gaiaOsmosisIBCConnId, osmosisGaiaIBCConnId = generateConnections(t, ctx, testCtx, r, eRep, gaiaOsmosisIBCPath, cosmosAtom, cosmosOsmosis) + linkPath(t, ctx, r, eRep, cosmosAtom, cosmosOsmosis, gaiaOsmosisIBCPath) - generateClient(t, ctx, testCtx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) - atomNeutronIBCConnId, neutronAtomIBCConnId := generateConnections(t, ctx, testCtx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) - linkPath(t, ctx, r, eRep, cosmosAtom, cosmosNeutron, gaiaNeutronIBCPath) + generateClient(t, ctx, testCtx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) + atomNeutronIBCConnId, neutronAtomIBCConnId = generateConnections(t, ctx, testCtx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) + linkPath(t, ctx, r, eRep, cosmosAtom, cosmosNeutron, gaiaNeutronIBCPath) + }) // Start the relayer and clean it up when the test ends. err = r.StartRelayer(ctx, eRep, gaiaNeutronICSPath, gaiaNeutronIBCPath, gaiaOsmosisIBCPath, neutronOsmosisIBCPath) @@ -268,10 +266,6 @@ func TestTokenSwap(t *testing.T) { err = testutil.WaitForBlocks(ctx, 2, atom, neutron, osmosis) require.NoError(t, err, "failed to wait for blocks") - createValidator(t, ctx, r, eRep, atom, neutron) - err = testutil.WaitForBlocks(ctx, 2, atom, neutron, osmosis) - require.NoError(t, err, "failed to wait for blocks") - // Once the VSC packet has been relayed, x/bank transfers are // enabled on Neutron and we can fund its account. // The funds for this are sent from a "faucet" account created @@ -283,106 +277,49 @@ func TestTokenSwap(t *testing.T) { err = testutil.WaitForBlocks(ctx, 10, atom, neutron, osmosis) require.NoError(t, err, "failed to wait for blocks") - neutronChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosNeutron.Config().ChainID) - gaiaChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosAtom.Config().ChainID) - osmoChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosOsmosis.Config().ChainID) + t.Run("determine ibc channels", func(t *testing.T) { + neutronChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosNeutron.Config().ChainID) + gaiaChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosAtom.Config().ChainID) + osmoChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosOsmosis.Config().ChainID) - // Find all pairwise channels - getPairwiseTransferChannelIds(testCtx, osmoChannelInfo, neutronChannelInfo, osmosisNeutronIBCConnId, neutronOsmosisIBCConnId, osmosis.Config().Name, neutron.Config().Name) - getPairwiseTransferChannelIds(testCtx, osmoChannelInfo, gaiaChannelInfo, osmosisGaiaIBCConnId, gaiaOsmosisIBCConnId, osmosis.Config().Name, cosmosAtom.Config().Name) - getPairwiseTransferChannelIds(testCtx, gaiaChannelInfo, neutronChannelInfo, atomNeutronIBCConnId, neutronAtomIBCConnId, cosmosAtom.Config().Name, neutron.Config().Name) - getPairwiseCCVChannelIds(testCtx, gaiaChannelInfo, neutronChannelInfo, atomNeutronICSConnectionId, neutronAtomICSConnectionId, cosmosAtom.Config().Name, cosmosNeutron.Config().Name) + // Find all pairwise channels + getPairwiseTransferChannelIds(testCtx, osmoChannelInfo, neutronChannelInfo, osmosisNeutronIBCConnId, neutronOsmosisIBCConnId, osmosis.Config().Name, neutron.Config().Name) + getPairwiseTransferChannelIds(testCtx, osmoChannelInfo, gaiaChannelInfo, osmosisGaiaIBCConnId, gaiaOsmosisIBCConnId, osmosis.Config().Name, cosmosAtom.Config().Name) + getPairwiseTransferChannelIds(testCtx, gaiaChannelInfo, neutronChannelInfo, atomNeutronIBCConnId, neutronAtomIBCConnId, cosmosAtom.Config().Name, neutron.Config().Name) + getPairwiseCCVChannelIds(testCtx, gaiaChannelInfo, neutronChannelInfo, atomNeutronICSConnectionId, neutronAtomICSConnectionId, cosmosAtom.Config().Name, cosmosNeutron.Config().Name) + }) - println("neutron channels:") - for key, value := range testCtx.NeutronTransferChannelIds { - fmt.Printf("Key: %s, Value: %s\n", key, value) - } - print("\n osmo channels: ") - for key, value := range testCtx.OsmoTransferChannelIds { - fmt.Printf("Key: %s, Value: %s\n", key, value) - } - println("gaia channels:") - for key, value := range testCtx.GaiaTransferChannelIds { - fmt.Printf("Key: %s, Value: %s\n", key, value) - } - println("gaia ics channels:") - for key, value := range testCtx.GaiaIcsChannelIds { - fmt.Printf("Key: %s, Value: %s\n", key, value) - } - println("neutron ics channels:") - for key, value := range testCtx.NeutronIcsChannelIds { - fmt.Printf("Key: %s, Value: %s\n", key, value) - } + t.Run("determine ibc denoms", func(t *testing.T) { + // We can determine the ibc denoms of: + // 1. ATOM on Neutron + neutronAtomIbcDenom = testCtx.getIbcDenom( + testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], + nativeAtomDenom, + ) + // 2. Osmo on neutron + neutronOsmoIbcDenom = testCtx.getIbcDenom( + testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], + nativeOsmoDenom, + ) + // 3. hub atom => neutron => osmosis + osmoNeutronAtomIbcDenom = testCtx.getMultihopIbcDenom( + []string{ + testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name], + testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], + }, + nativeAtomDenom, + ) + // 4. osmosis osmo => neutron => hub + gaiaNeutronOsmoIbcDenom = testCtx.getMultihopIbcDenom( + []string{ + testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], + testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], + }, + nativeOsmoDenom, + ) + }) - // We can determine the ibc denoms of: - // 1. ATOM on Neutron - neutronAtomIbcDenom := testCtx.getIbcDenom(testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], cosmosAtom.Config().Denom) - // 2. Osmo on neutron - neutronOsmoIbcDenom := testCtx.getIbcDenom( - testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], - cosmosOsmosis.Config().Denom, - ) - - // 3. hub atom => neutron => osmosis - // transfer/channel-0/transfer/channel-3 - - neutronAtomPrefix := fmt.Sprintf("%s/%s", "transfer", testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name]) - neutronOsmoPrefix := fmt.Sprintf("%s/%s", "transfer", testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name]) - gaiaNeutronPrefix := fmt.Sprintf("%s/%s", "transfer", testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name]) - osmoNeutronPrefix := fmt.Sprintf("%s/%s", "transfer", testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name]) - - println("neutronAtomPrefix: ", neutronAtomPrefix) - println("neutronOsmoPrefix: ", neutronOsmoPrefix) - println("gaiaNeutronPrefix: ", gaiaNeutronPrefix) - println("osmoNeutronPrefix: ", osmoNeutronPrefix) - - osmoNeutronAtomPrefixedDenom := transfertypes.GetPrefixedDenom( - osmoNeutronPrefix, - neutronAtomPrefix, - cosmosAtom.Config().Denom, - ) - println("osmoNeutronAtomPrefixedDenom: ", osmoNeutronAtomPrefixedDenom) - - gaiaNeutronOsmoPrefixedDenom := transfertypes.GetPrefixedDenom( - gaiaNeutronPrefix, - neutronOsmoPrefix, - cosmosOsmosis.Config().Denom, - ) - println("gaiaNeutronOsmoPrefixedDenom: ", gaiaNeutronOsmoPrefixedDenom) - - gaiaNeutronOsmoIbcDenom := transfertypes.ParseDenomTrace(gaiaNeutronOsmoPrefixedDenom).IBCDenom() - osmoNeutronAtomIbcDenom := transfertypes.ParseDenomTrace(osmoNeutronAtomPrefixedDenom).IBCDenom() - - println("neutronAtomIbcDenom: ", neutronAtomIbcDenom) - println("neutronOsmoIbcDenom: ", neutronOsmoIbcDenom) - println("osmoNeutronAtomIbcDenom: ", osmoNeutronAtomIbcDenom) - println("gaiaNeutronOsmoIbcDenom: ", gaiaNeutronOsmoIbcDenom) - - // 2CB1 - - var covenantAddress string - var clockAddress string - var splitterAddress string - var partyARouterAddress string - var partyBRouterAddress string - var partyAIbcForwarderAddress string - var partyBIbcForwarderAddress string - var holderAddress string - - var partyADepositAddress, partyBDepositAddress string - - t.Run("tokenswap setup", func(t *testing.T) { - //----------------------------------------------// - // Testing parameters - //----------------------------------------------// - - // PARTY_A - const osmoContributionAmount uint64 = 100_000_000_000 // in uosmo - - // PARTY_B - const atomContributionAmount uint64 = 5_000_000_000 // in uatom - - //----------------------------------------------// + t.Run("tokenswap covenant setup", func(t *testing.T) { // Wasm code that we need to store on Neutron const covenantContractPath = "wasms/covenant_swap.wasm" const clockContractPath = "wasms/covenant_clock.wasm" @@ -440,9 +377,8 @@ func TestTokenSwap(t *testing.T) { swapHolderCodeId, err = strconv.ParseUint(swapHolderCodeIdStr, 10, 64) require.NoError(t, err, "failed to parse codeId into uint64") + require.NoError(t, testutil.WaitForBlocks(ctx, 5, cosmosNeutron, cosmosAtom, cosmosOsmosis)) }) - println(covenantCodeIdStr, clockCodeId, routerCodeId, splitterCodeId, ibcForwarderCodeId, swapHolderCodeId) - require.NoError(t, testutil.WaitForBlocks(ctx, 10, cosmosNeutron, cosmosAtom, cosmosOsmosis)) t.Run("instantiate covenant", func(t *testing.T) { @@ -503,7 +439,7 @@ func TestTokenSwap(t *testing.T) { swapCovenantParties := SwapCovenantParties{ PartyA: SwapPartyConfig{ Addr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), - NativeDenom: "uatom", + NativeDenom: nativeAtomDenom, IbcDenom: neutronAtomIbcDenom, PartyToHostChainChannelId: testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], @@ -513,7 +449,7 @@ func TestTokenSwap(t *testing.T) { }, PartyB: SwapPartyConfig{ Addr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), - NativeDenom: "uosmo", + NativeDenom: nativeOsmoDenom, IbcDenom: neutronOsmoIbcDenom, PartyToHostChainChannelId: testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name], HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], @@ -530,7 +466,7 @@ func TestTokenSwap(t *testing.T) { Type: SplitType{ Custom: SplitConfig{ Receivers: []Receiver{ - Receiver{ + { Address: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), Share: "100", }, @@ -543,7 +479,7 @@ func TestTokenSwap(t *testing.T) { Type: SplitType{ Custom: SplitConfig{ Receivers: []Receiver{ - Receiver{ + { Address: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), Share: "100", }, @@ -571,7 +507,6 @@ func TestTokenSwap(t *testing.T) { str, err := json.Marshal(covenantMsg) require.NoError(t, err, "Failed to marshall CovenantInstantiateMsg") - println("covenant instantiation msg: ", string(str)) instantiateMsg := string(str) cmd := []string{"neutrond", "tx", "wasm", "instantiate", covenantCodeIdStr, @@ -588,11 +523,9 @@ func TestTokenSwap(t *testing.T) { "-y", } - resp, _, err := neutron.Exec(ctx, cmd, nil) + _, _, err = neutron.Exec(ctx, cmd, nil) require.NoError(t, err) - println("instantiated, skipping 10 blocks...") - println("instantiate response: ", string(resp), "\n") require.NoError(t, testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis)) queryCmd := []string{"neutrond", "query", "wasm", @@ -606,7 +539,6 @@ func TestTokenSwap(t *testing.T) { queryResp, _, err := neutron.Exec(ctx, queryCmd, nil) require.NoError(t, err, "failed to query") - println("query response: ", string(queryResp)) type QueryContractResponse struct { Contracts []string `json:"contracts"` Pagination any `json:"pagination"` @@ -683,7 +615,7 @@ func TestTokenSwap(t *testing.T) { err := neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ Address: partyAIbcForwarderAddress, Amount: 5000001, - Denom: neutron.Config().Denom, + Denom: nativeNtrnDenom, }) require.NoError(t, err, "failed to send funds from neutron user to partyAIbcForwarder contract") @@ -691,50 +623,53 @@ func TestTokenSwap(t *testing.T) { err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ Address: partyBIbcForwarderAddress, Amount: 5000001, - Denom: neutron.Config().Denom, + Denom: nativeNtrnDenom, }) require.NoError(t, err, "failed to send funds from neutron user to partyBIbcForwarder contract") err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ Address: clockAddress, Amount: 5000001, - Denom: neutron.Config().Denom, + Denom: nativeNtrnDenom, }) require.NoError(t, err, "failed to send funds from neutron user to clock contract") err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ Address: partyARouterAddress, Amount: 15000001, - Denom: neutron.Config().Denom, + Denom: nativeNtrnDenom, }) require.NoError(t, err, "failed to send funds from neutron user to party a router") err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ Address: partyBRouterAddress, Amount: 15000001, - Denom: neutron.Config().Denom, + Denom: nativeNtrnDenom, }) require.NoError(t, err, "failed to send funds from neutron user to party b router") err = testutil.WaitForBlocks(ctx, 2, atom, neutron) require.NoError(t, err, "failed to wait for blocks") - bal, err := neutron.GetBalance(ctx, partyAIbcForwarderAddress, neutron.Config().Denom) + bal, err := neutron.GetBalance(ctx, partyAIbcForwarderAddress, nativeNtrnDenom) require.NoError(t, err) require.Equal(t, int64(5000001), bal) - bal, err = neutron.GetBalance(ctx, partyBIbcForwarderAddress, neutron.Config().Denom) + bal, err = neutron.GetBalance(ctx, partyBIbcForwarderAddress, nativeNtrnDenom) require.NoError(t, err) require.Equal(t, int64(5000001), bal) - bal, err = neutron.GetBalance(ctx, clockAddress, neutron.Config().Denom) + bal, err = neutron.GetBalance(ctx, clockAddress, nativeNtrnDenom) require.NoError(t, err) require.Equal(t, int64(5000001), bal) - bal, err = neutron.GetBalance(ctx, partyARouterAddress, neutron.Config().Denom) + bal, err = neutron.GetBalance(ctx, partyARouterAddress, nativeNtrnDenom) require.NoError(t, err) require.Equal(t, int64(15000001), bal) - bal, err = neutron.GetBalance(ctx, partyBRouterAddress, neutron.Config().Denom) + bal, err = neutron.GetBalance(ctx, partyBRouterAddress, nativeNtrnDenom) require.NoError(t, err) require.Equal(t, int64(15000001), bal) }) + }) + t.Run("tokenswap run", func(t *testing.T) { tickClock := func() { + println("tick") cmd := []string{"neutrond", "tx", "wasm", "execute", clockAddress, `{"tick":{}}`, "--from", neutronUser.KeyName, @@ -751,29 +686,16 @@ func TestTokenSwap(t *testing.T) { "-y", } - println("tick cmd: ", strings.Join(cmd, " ")) - stdout, _, err := cosmosNeutron.Exec(ctx, cmd, nil) + _, _, err := cosmosNeutron.Exec(ctx, cmd, nil) require.NoError(t, err) - println("clock tick response: ", string(stdout)) - err = testutil.WaitForBlocks(ctx, 10, atom, neutron, osmosis) + err = testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis) require.NoError(t, err, "failed to wait for blocks") } t.Run("tick until forwarders create ICA", func(t *testing.T) { - const maxTicks = 10 - tick := 1 - var response CovenantAddressQueryResponse - for tick <= maxTicks { - println("Ticking clock ", tick, " of ", maxTicks) + for { tickClock() - type DepositAddress struct{} - type DepositAddressQuery struct { - DepositAddress DepositAddress `json:"deposit_address"` - } - depositAddressQuery := DepositAddressQuery{ - DepositAddress: DepositAddress{}, - } - + var response CovenantAddressQueryResponse type ContractState struct{} type ContractStateQuery struct { ContractState ContractState `json:"contract_state"` @@ -782,44 +704,49 @@ func TestTokenSwap(t *testing.T) { ContractState: ContractState{}, } - err := cosmosNeutron.QueryContract(ctx, partyAIbcForwarderAddress, depositAddressQuery, &response) - require.NoError(t, err, "failed to query party a forwarder deposit address") - partyADepositAddr := response.Data - println("partyADepositAddress: ", partyADepositAddress) + require.NoError(t, + cosmosNeutron.QueryContract(ctx, partyAIbcForwarderAddress, contractStateQuery, &response), + "failed to query forwarder A state") + forwarderAState := response.Data - err = cosmosNeutron.QueryContract(ctx, partyBIbcForwarderAddress, depositAddressQuery, &response) - require.NoError(t, err, "failed to query party b forwarder deposit address") - partyBDepositAddr := response.Data - println("partyBDepositAddress: ", partyBDepositAddress) + require.NoError(t, + cosmosNeutron.QueryContract(ctx, partyBIbcForwarderAddress, contractStateQuery, &response), + "failed to query forwarder B state") - err = cosmosNeutron.QueryContract(ctx, partyAIbcForwarderAddress, contractStateQuery, &response) - require.NoError(t, err, "failed to query forwarder A state") - forwarderAState := response.Data - println("forwarderAState: ", forwarderAState) - err = cosmosNeutron.QueryContract(ctx, partyBIbcForwarderAddress, contractStateQuery, &response) - require.NoError(t, err, "failed to query forwarder B state") forwarderBState := response.Data - println("forwarderBState: ", forwarderBState) if forwarderAState == forwarderBState && forwarderBState == "ica_created" { - partyADepositAddress = partyADepositAddr - partyBDepositAddress = partyBDepositAddr + type DepositAddress struct{} + type DepositAddressQuery struct { + DepositAddress DepositAddress `json:"deposit_address"` + } + depositAddressQuery := DepositAddressQuery{ + DepositAddress: DepositAddress{}, + } + + err := cosmosNeutron.QueryContract(ctx, partyAIbcForwarderAddress, depositAddressQuery, &response) + require.NoError(t, err, "failed to query party a forwarder deposit address") + partyADepositAddress = response.Data + + err = cosmosNeutron.QueryContract(ctx, partyBIbcForwarderAddress, depositAddressQuery, &response) + require.NoError(t, err, "failed to query party b forwarder deposit address") + partyBDepositAddress = response.Data + break } - tick += 1 } }) t.Run("fund the forwarders with sufficient funds", func(t *testing.T) { err := cosmosOsmosis.SendFunds(ctx, osmoUser.KeyName, ibc.WalletAmount{ Address: partyBDepositAddress, - Denom: cosmosOsmosis.Config().Denom, + Denom: nativeOsmoDenom, Amount: int64(osmoContributionAmount + 1000), }) require.NoError(t, err, "failed to fund osmo forwarder") err = cosmosAtom.SendFunds(ctx, gaiaUser.KeyName, ibc.WalletAmount{ Address: partyADepositAddress, - Denom: cosmosAtom.Config().Denom, + Denom: nativeAtomDenom, Amount: int64(atomContributionAmount + 1000), }) require.NoError(t, err, "failed to fund gaia forwarder") @@ -827,147 +754,78 @@ func TestTokenSwap(t *testing.T) { err = testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis) require.NoError(t, err, "failed to wait for blocks") - bal, err := cosmosAtom.GetBalance(ctx, partyADepositAddress, cosmosAtom.Config().Denom) + bal, err := cosmosAtom.GetBalance(ctx, partyADepositAddress, nativeAtomDenom) require.NoError(t, err, "failed to query bal") require.Equal(t, int64(atomContributionAmount+1000), bal) - bal, err = cosmosOsmosis.GetBalance(ctx, partyBDepositAddress, cosmosOsmosis.Config().Denom) + bal, err = cosmosOsmosis.GetBalance(ctx, partyBDepositAddress, nativeOsmoDenom) require.NoError(t, err, "failed to query bal") require.Equal(t, int64(osmoContributionAmount+1000), bal) }) t.Run("tick until forwarders forward the funds to holder", func(t *testing.T) { - - const maxTicks = 20 - tick := 1 - var response CovenantAddressQueryResponse - for tick <= maxTicks { - println("Ticking clock ", tick, " of ", maxTicks) + for { tickClock() - - type ContractState struct{} - type ContractStateQuery struct { - ContractState ContractState `json:"contract_state"` - } - contractStateQuery := ContractStateQuery{ - ContractState: ContractState{}, - } - - err = cosmosNeutron.QueryContract(ctx, partyAIbcForwarderAddress, contractStateQuery, &response) - require.NoError(t, err, "failed to query forwarder A state") - forwarderAState := response.Data - println("forwarderAState: ", forwarderAState) - err = cosmosNeutron.QueryContract(ctx, partyBIbcForwarderAddress, contractStateQuery, &response) - require.NoError(t, err, "failed to query forwarder B state") - forwarderBState := response.Data - println("forwarderBState: ", forwarderBState) - err = cosmosNeutron.QueryContract(ctx, holderAddress, contractStateQuery, &response) - require.NoError(t, err, "failed to query holder state") - holderState := response.Data - println("holderState: ", holderState) - holderOsmoBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronOsmoIbcDenom) require.NoError(t, err, "failed to query holder osmo bal") - println("holder osmo bal: ", holderOsmoBal) holderAtomBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronAtomIbcDenom) require.NoError(t, err, "failed to query holder atom bal") - println("holder atom bal: ", holderAtomBal) if holderAtomBal != 0 && holderOsmoBal != 0 { + println("holder atom bal: ", holderAtomBal) + println("holder osmo bal: ", holderOsmoBal) break } - tick += 1 } }) t.Run("tick until holder sends the funds to splitter", func(t *testing.T) { - const maxTicks = 20 - tick := 1 - for tick <= maxTicks { - holderOsmoBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronOsmoIbcDenom) - require.NoError(t, err, "failed to query holder osmo bal") - println("holder osmo bal: ", holderOsmoBal) - holderAtomBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronAtomIbcDenom) - require.NoError(t, err, "failed to query holder atom bal") - println("holder atom bal: ", holderAtomBal) - - println("Ticking clock ", tick, " of ", maxTicks) + for { tickClock() splitterOsmoBal, err := cosmosNeutron.GetBalance(ctx, splitterAddress, neutronOsmoIbcDenom) require.NoError(t, err, "failed to query splitterOsmoBal") - println("splitterOsmoBal: ", splitterOsmoBal) splitterAtomBal, err := cosmosNeutron.GetBalance(ctx, splitterAddress, neutronAtomIbcDenom) require.NoError(t, err, "failed to query splitterAtomBal") - println("splitterAtomBal: ", splitterAtomBal) if splitterAtomBal != 0 && splitterOsmoBal != 0 { + println("splitterOsmoBal: ", splitterOsmoBal) + println("splitterAtomBal: ", splitterAtomBal) break } } }) t.Run("tick until splitter sends the funds to routers", func(t *testing.T) { - const maxTicks = 20 - tick := 1 - for tick <= maxTicks { - splitterOsmoBal, err := cosmosNeutron.GetBalance(ctx, splitterAddress, neutronOsmoIbcDenom) - require.NoError(t, err, "failed to query splitterOsmoBal") - println("splitterOsmoBal: ", splitterOsmoBal) - splitterAtomBal, err := cosmosNeutron.GetBalance(ctx, splitterAddress, neutronAtomIbcDenom) - require.NoError(t, err, "failed to query splitterAtomBal") - println("splitterAtomBal: ", splitterAtomBal) - - println("Ticking clock ", tick, " of ", maxTicks) + for { tickClock() - partyARouterAtomBal, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronAtomIbcDenom) - require.NoError(t, err, "failed to query partyARouterBal") - println("partyARouter atom bal: ", partyARouterAtomBal) partyARouterOsmoBal, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronOsmoIbcDenom) require.NoError(t, err, "failed to query partyARouterOsmoBal") - println("partyARouter osmo bal: ", partyARouterOsmoBal) - partyBRouterOsmoBal, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronOsmoIbcDenom) - require.NoError(t, err, "failed to query partyBRouterOsmoBal") - println("partyBRouterOsmoBal: ", partyBRouterOsmoBal) + partyBRouterAtomBal, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronAtomIbcDenom) require.NoError(t, err, "failed to query partyBRouterAtomBal") - println("partyBRouterAtomBal: ", partyBRouterAtomBal) if partyARouterOsmoBal != 0 && partyBRouterAtomBal != 0 { + println("partyARouter osmo bal: ", partyARouterOsmoBal) + println("partyBRouterAtomBal: ", partyBRouterAtomBal) + break } } }) t.Run("tick until routers route the funds to final receivers", func(t *testing.T) { - - const maxTicks = 50 - tick := 1 - for tick <= maxTicks { - partyARouterAtomBal, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronAtomIbcDenom) - require.NoError(t, err, "failed to query partyARouterBal") - println("partyARouter atom bal: ", partyARouterAtomBal) - partyARouterOsmoBal, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronOsmoIbcDenom) - require.NoError(t, err, "failed to query partyARouterOsmoBal") - println("partyARouter osmo bal: ", partyARouterOsmoBal) - partyBRouterOsmoBal, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronOsmoIbcDenom) - require.NoError(t, err, "failed to query partyBRouterOsmoBal") - println("partyBRouterOsmoBal: ", partyBRouterOsmoBal) - partyBRouterAtomBal, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronAtomIbcDenom) - require.NoError(t, err, "failed to query partyBRouterAtomBal") - println("partyBRouterAtomBal: ", partyBRouterAtomBal) - - println("Ticking clock ", tick, " of ", maxTicks) + for { tickClock() osmoBal, err := cosmosOsmosis.GetBalance(ctx, osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), osmoNeutronAtomIbcDenom) require.NoError(t, err, "failed to query osmoBal") - println("osmo user atom bal: ", osmoBal) gaiaBal, err := cosmosAtom.GetBalance(ctx, gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), gaiaNeutronOsmoIbcDenom) require.NoError(t, err, "failed to query gaiaBal") - println("gaia user osmo bal: ", gaiaBal) if osmoBal != 0 && gaiaBal != 0 { + println("gaia user osmo bal: ", gaiaBal) + println("osmo user atom bal: ", osmoBal) break } } From 03094f81c1defd42d5fee4990e7f48712e68bc29 Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 15 Sep 2023 19:09:19 +0200 Subject: [PATCH 081/586] wip: rework covenant instantiation structure --- Cargo.lock | 39 +--- Cargo.toml | 5 +- contracts/clock/src/msg.rs | 6 +- contracts/ibc-forwarder/src/msg.rs | 10 +- contracts/interchain-router/src/contract.rs | 2 +- contracts/interchain-router/src/msg.rs | 23 ++ contracts/interchain-splitter/src/msg.rs | 46 ++-- .../src/suite_test/suite.rs | 10 +- .../src/suite_test/tests.rs | 42 ++-- contracts/swap-covenant/src/contract.rs | 212 ++++++++++-------- contracts/swap-covenant/src/msg.rs | 54 +++-- contracts/swap-covenant/src/state.rs | 41 +--- .../swap-covenant/src/suite_test/suite.rs | 14 +- .../swap-holder/src/suite_tests/suite.rs | 8 +- .../swap-holder/src/suite_tests/tests.rs | 8 +- packages/covenant-utils/src/lib.rs | 3 +- .../tests/interchaintest/tokenswap_test.go | 104 +++------ swap-covenant/tests/interchaintest/types.go | 29 ++- {contracts => v1}/covenant/.cargo/config | 0 {contracts => v1}/covenant/Cargo.toml | 2 +- {contracts => v1}/covenant/LICENSE | 0 {contracts => v1}/covenant/README.md | 0 {contracts => v1}/covenant/examples/schema.rs | 0 {contracts => v1}/covenant/src/contract.rs | 0 {contracts => v1}/covenant/src/error.rs | 0 .../covenant/src/instantiate2.rs | 0 {contracts => v1}/covenant/src/lib.rs | 0 {contracts => v1}/covenant/src/msg.rs | 0 {contracts => v1}/covenant/src/state.rs | 0 .../covenant/src/suite_test/mod.rs | 0 .../covenant/src/suite_test/suite.rs | 0 .../covenant/src/suite_test/tests.rs | 0 .../covenant/src/suite_test/unit_tests.rs | 0 33 files changed, 315 insertions(+), 343 deletions(-) rename {contracts => v1}/covenant/.cargo/config (100%) rename {contracts => v1}/covenant/Cargo.toml (97%) rename {contracts => v1}/covenant/LICENSE (100%) rename {contracts => v1}/covenant/README.md (100%) rename {contracts => v1}/covenant/examples/schema.rs (100%) rename {contracts => v1}/covenant/src/contract.rs (100%) rename {contracts => v1}/covenant/src/error.rs (100%) rename {contracts => v1}/covenant/src/instantiate2.rs (100%) rename {contracts => v1}/covenant/src/lib.rs (100%) rename {contracts => v1}/covenant/src/msg.rs (100%) rename {contracts => v1}/covenant/src/state.rs (100%) rename {contracts => v1}/covenant/src/suite_test/mod.rs (100%) rename {contracts => v1}/covenant/src/suite_test/suite.rs (100%) rename {contracts => v1}/covenant/src/suite_test/tests.rs (100%) rename {contracts => v1}/covenant/src/suite_test/unit_tests.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index f230de2c..9632ac0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -355,37 +355,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "covenant-covenant" -version = "1.0.0" -dependencies = [ - "anyhow", - "astroport 2.8.0", - "base64 0.13.1", - "bech32", - "cosmos-sdk-proto 0.14.0", - "cosmwasm-schema", - "cosmwasm-std", - "covenant-clock", - "covenant-depositor", - "covenant-holder", - "covenant-lp", - "covenant-ls", - "cw-multi-test", - "cw-storage-plus 1.1.0", - "cw-utils 1.0.2", - "cw2 1.1.1", - "neutron-sdk", - "prost 0.11.9", - "prost-types", - "protobuf 3.3.0", - "schemars", - "serde", - "serde-json-wasm 0.4.1", - "sha2 0.10.8", - "thiserror", -] - [[package]] name = "covenant-depositor" version = "1.0.0" @@ -625,16 +594,16 @@ dependencies = [ "covenant-utils", "cw-multi-test", "cw-storage-plus 1.1.0", - "cw-utils 1.0.1", - "cw2 1.1.0", + "cw-utils 1.0.2", + "cw2 1.1.1", "neutron-sdk", "prost 0.11.9", "prost-types", - "protobuf 3.2.0", + "protobuf 3.3.0", "schemars", "serde", "serde-json-wasm 0.4.1", - "sha2 0.10.7", + "sha2 0.10.8", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index ff8c4980..25f740d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,12 +3,13 @@ members = [ "packages/*", "contracts/*", ] +exclude = ["contracts/covenant"] [workspace.package] edition = "2021" license = "BSD-3" version = "1.0.0" -repository = "https://github.com/timewave-computer/covenants/stride-covenant" +repository = "https://github.com/timewave-computer/covenants" rust-version = "1.66" @@ -29,7 +30,7 @@ covenant-lp = { path = "contracts/lper" } covenant-clock = { path = "contracts/clock" } covenant-clock-tester = { path = "contracts/clock-tester" } covenant-ls = { path = "contracts/ls" } -covenant-covenant = { path = "contracts/covenant" } +# covenant-covenant = { path = "contracts/covenant" } covenant-holder = { path = "contracts/holder" } covenant-ibc-forwarder = { path = "contracts/ibc-forwarder" } covenant-native-splitter = { path = "contracts/native-splitter" } diff --git a/contracts/clock/src/msg.rs b/contracts/clock/src/msg.rs index e58d2a30..a971bbcc 100644 --- a/contracts/clock/src/msg.rs +++ b/contracts/clock/src/msg.rs @@ -26,12 +26,12 @@ pub struct InstantiateMsg { pub struct PresetClockFields { pub tick_max_gas: Option, pub whitelist: Vec, - pub clock_code: u64, + pub code_id: u64, pub label: String, } impl PresetClockFields { - pub fn to_instantiate_msg(self) -> InstantiateMsg { + pub fn to_instantiate_msg(&self) -> InstantiateMsg { let tick_max_gas = if let Some(tmg) = self.tick_max_gas { // double the 100k minimum seems fair tmg.min(Uint64::new(200000)) @@ -42,7 +42,7 @@ impl PresetClockFields { InstantiateMsg { tick_max_gas: Some(tick_max_gas), - whitelist: self.whitelist, + whitelist: self.clone().whitelist, } } } diff --git a/contracts/ibc-forwarder/src/msg.rs b/contracts/ibc-forwarder/src/msg.rs index 8ad6a0ee..85c09cb0 100644 --- a/contracts/ibc-forwarder/src/msg.rs +++ b/contracts/ibc-forwarder/src/msg.rs @@ -45,11 +45,13 @@ pub struct PresetIbcForwarderFields { pub remote_chain_channel_id: String, pub denom: String, pub amount: Uint128, + pub label: String, + pub code_id: u64, } impl PresetIbcForwarderFields { pub fn to_instantiate_msg( - self, + &self, clock_address: String, next_contract: String, ibc_fee: IbcFee, @@ -59,9 +61,9 @@ impl PresetIbcForwarderFields { InstantiateMsg { clock_address, next_contract, - remote_chain_connection_id: self.remote_chain_connection_id, - remote_chain_channel_id: self.remote_chain_channel_id, - denom: self.denom, + remote_chain_connection_id: self.remote_chain_connection_id.to_string(), + remote_chain_channel_id: self.remote_chain_channel_id.to_string(), + denom: self.denom.to_string(), amount: self.amount, ibc_fee, ibc_transfer_timeout, diff --git a/contracts/interchain-router/src/contract.rs b/contracts/interchain-router/src/contract.rs index c2d5992b..d2b44eb2 100644 --- a/contracts/interchain-router/src/contract.rs +++ b/contracts/interchain-router/src/contract.rs @@ -5,7 +5,7 @@ use cosmwasm_std::{ }; use covenant_utils::DestinationConfig; use cw2::set_contract_version; -use neutron_sdk::{bindings::msg::NeutronMsg, NeutronResult}; +use neutron_sdk::bindings::msg::NeutronMsg; use crate::{ error::ContractError, diff --git a/contracts/interchain-router/src/msg.rs b/contracts/interchain-router/src/msg.rs index 2aa04011..ea49d115 100644 --- a/contracts/interchain-router/src/msg.rs +++ b/contracts/interchain-router/src/msg.rs @@ -18,6 +18,29 @@ pub struct InstantiateMsg { pub ibc_transfer_timeout: Uint64, } +#[cw_serde] +pub struct PresetInterchainRouterFields { + /// channel id of the destination chain + pub destination_chain_channel_id: String, + /// address of the receiver on destination chain + pub destination_receiver_addr: String, + /// timeout in seconds + pub ibc_transfer_timeout: Uint64, + pub label: String, + pub code_id: u64, +} + +impl PresetInterchainRouterFields { + pub fn to_instantiate_msg(&self, clock_address: String) -> InstantiateMsg { + InstantiateMsg { + clock_address, + destination_chain_channel_id: self.destination_chain_channel_id.to_string(), + destination_receiver_addr: self.destination_receiver_addr.to_string(), + ibc_transfer_timeout: self.ibc_transfer_timeout, + } + } +} + #[clocked] #[cw_serde] pub enum ExecuteMsg {} diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs index 739bd3f6..14d07228 100644 --- a/contracts/interchain-splitter/src/msg.rs +++ b/contracts/interchain-splitter/src/msg.rs @@ -26,6 +26,9 @@ pub struct PresetInterchainSplitterFields { pub fallback_split: Option, /// contract label pub label: String, + pub code_id: u64, + pub party_a_addr: String, + pub party_b_addr: String, } #[cw_serde] @@ -39,35 +42,33 @@ impl PresetInterchainSplitterFields { /// - replaces real receiver addresses with their routers /// - adds clock address pub fn to_instantiate_msg( - self, + &self, clock_address: String, party_a_router: String, - party_a_addr: String, party_b_router: String, - party_b_addr: String, ) -> Result { let mut remapped_splits: Vec<(String, SplitType)> = vec![]; - for denom_split in self.splits { - match denom_split.split { + for denom_split in &self.splits { + match &denom_split.split { SplitType::Custom(config) => { let remapped_split = config.remap_receivers_to_routers( - party_a_addr.to_string(), + self.party_a_addr.to_string(), party_a_router.to_string(), - party_b_addr.to_string(), + self.party_b_addr.to_string(), party_b_router.to_string(), )?; - remapped_splits.push((denom_split.denom, remapped_split)); + remapped_splits.push((denom_split.denom.to_string(), remapped_split)); }, } } - let remapped_fallback = match self.fallback_split { + let remapped_fallback = match &self.fallback_split { Some(split_type) => match split_type { SplitType::Custom(config) => Some(config.remap_receivers_to_routers( - party_a_addr.to_string(), + self.party_a_addr.to_string(), party_a_router.to_string(), - party_b_addr.to_string(), + self.party_b_addr.to_string(), party_b_router.to_string(), )?) }, @@ -134,8 +135,8 @@ pub struct Receiver { } impl SplitConfig { - pub fn remap_receivers_to_routers(self, receiver_a: String, router_a: String, receiver_b: String, router_b: String) -> Result { - let receivers = self.receivers.into_iter() + pub fn remap_receivers_to_routers(&self, receiver_a: String, router_a: String, receiver_b: String, router_b: String) -> Result { + let receivers = self.receivers.clone().into_iter() .map(|receiver| { if receiver.addr == receiver_a { Receiver { @@ -231,22 +232,3 @@ pub enum MigrateMsg { data: Option, }, } - -#[covenant_clock_address] -#[cw_serde] -#[derive(QueryResponses)] -pub enum QueryMsg { - #[returns(SplitConfig)] - DenomSplit { denom: String }, - #[returns(Vec<(String, SplitConfig)>)] - Splits {}, - #[returns(SplitConfig)] - FallbackSplit {}, -} - -#[cw_serde] -#[derive(QueryResponses)] -pub enum ProtocolGuildQueryMsg { - #[returns(SplitConfig)] - PublicGoodsSplit {}, -} \ No newline at end of file diff --git a/contracts/interchain-splitter/src/suite_test/suite.rs b/contracts/interchain-splitter/src/suite_test/suite.rs index 2c3aaa6c..cbb1e643 100644 --- a/contracts/interchain-splitter/src/suite_test/suite.rs +++ b/contracts/interchain-splitter/src/suite_test/suite.rs @@ -3,7 +3,7 @@ use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; use crate::msg::{ ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, SplitConfig, - SplitType, + SplitType, Receiver, }; use super::splitter_contract; @@ -22,15 +22,17 @@ pub const CLOCK_ADDR: &str = "clock_addr"; pub fn get_equal_split_config() -> SplitConfig { SplitConfig { receivers: vec![ - (PARTY_A_ADDR.to_string(), Uint128::new(50)), - (PARTY_B_ADDR.to_string(), Uint128::new(50)), + Receiver { addr: PARTY_A_ADDR.to_string(), share: Uint128::new(50) }, + Receiver { addr: PARTY_B_ADDR.to_string(), share: Uint128::new(50) }, ], } } pub fn get_fallback_split_config() -> SplitConfig { SplitConfig { - receivers: vec![("save_the_cats".to_string(), Uint128::new(100))], + receivers: vec![ + Receiver { addr: "save_the_cats".to_string(), share: Uint128::new(100) }, + ], } } diff --git a/contracts/interchain-splitter/src/suite_test/tests.rs b/contracts/interchain-splitter/src/suite_test/tests.rs index ab567807..8b69c078 100644 --- a/contracts/interchain-splitter/src/suite_test/tests.rs +++ b/contracts/interchain-splitter/src/suite_test/tests.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{Coin, Uint128}; use crate::{ - msg::{MigrateMsg, SplitConfig, SplitType}, + msg::{MigrateMsg, SplitConfig, SplitType, Receiver}, suite_test::suite::{ get_equal_split_config, get_fallback_split_config, ALT_DENOM, CLOCK_ADDR, DENOM_B, }, @@ -40,14 +40,8 @@ fn test_instantiate_split_misconfig() { DENOM_A.to_string(), SplitType::Custom(SplitConfig { receivers: vec![ - ( - PARTY_A_ADDR.to_string(), - Uint128::new(50), - ), - ( - PARTY_B_ADDR.to_string(), - Uint128::new(60), - ), + Receiver { addr: PARTY_A_ADDR.to_string(), share: Uint128::new(50) }, + Receiver { addr: PARTY_B_ADDR.to_string(), share: Uint128::new(50) }, ], }), )]) @@ -96,19 +90,15 @@ fn test_distribute_token_swap() { ( DENOM_A.to_string(), SplitType::Custom(SplitConfig { - receivers: vec![( - PARTY_B_ADDR.to_string(), - Uint128::new(100), - )], + receivers: vec![ + Receiver { addr: PARTY_B_ADDR.to_string(), share: Uint128::new(100) }], }), ), ( DENOM_B.to_string(), SplitType::Custom(SplitConfig { - receivers: vec![( - PARTY_A_ADDR.to_string(), - Uint128::new(100), - )], + receivers: vec![ + Receiver { addr: PARTY_A_ADDR.to_string(), share: Uint128::new(100) }], }), ), ]) @@ -177,18 +167,14 @@ fn test_migrate_config() { let new_clock = "new_clock".to_string(); let new_fallback_split = SplitConfig { - receivers: vec![( - "fallback_new".to_string(), - Uint128::new(100), - )], + receivers: vec![ + Receiver { addr: "fallback_new".to_string(), share: Uint128::new(100) }, + ], }; let new_splits = vec![( "new_denom".to_string(), SplitType::Custom(SplitConfig { - receivers: vec![( - "new_receiver".to_string(), - Uint128::new(100), - )], + receivers: vec![Receiver { addr: "new_receiver".to_string(), share: Uint128::new(100) }], }), )]; @@ -208,10 +194,8 @@ fn test_migrate_config() { vec![( "new_denom".to_string(), SplitConfig { - receivers: vec![( - "new_receiver".to_string(), - Uint128::new(100) - )], + receivers: vec![ + Receiver { addr: "new_receiver".to_string(), share: Uint128::new(100) }], }, )], splits diff --git a/contracts/swap-covenant/src/contract.rs b/contracts/swap-covenant/src/contract.rs index 9371a0de..8124ecdb 100644 --- a/contracts/swap-covenant/src/contract.rs +++ b/contracts/swap-covenant/src/contract.rs @@ -1,3 +1,4 @@ + #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ @@ -5,13 +6,19 @@ use cosmwasm_std::{ SubMsg, WasmMsg, Reply, Deps, StdResult, Binary, Addr, }; +use covenant_clock::msg::PresetClockFields; +use covenant_ibc_forwarder::msg::PresetIbcForwarderFields; +use covenant_interchain_router::msg::PresetInterchainRouterFields; +use covenant_interchain_splitter::msg::PresetInterchainSplitterFields; +use covenant_swap_holder::msg::PresetSwapHolderFields; +use covenant_utils::{CovenantPartiesConfig, CovenantParty, ReceiverConfig, CovenantTerms}; use cw2::set_contract_version; use cw_utils::{parse_reply_instantiate_data, ParseReplyError}; use crate::{ error::ContractError, state::{ - CLOCK_CODE, TIMEOUTS, COVENANT_CLOCK_ADDR, SWAP_HOLDER_CODE, PRESET_HOLDER_FIELDS, COVENANT_INTERCHAIN_SPLITTER_ADDR, PRESET_SPLITTER_FIELDS, COVENANT_SWAP_HOLDER_ADDR, IBC_FORWARDER_CODE, IBC_FEE, COVENANT_PARTIES, PARTY_A_IBC_FORWARDER_ADDR, PARTY_B_IBC_FORWARDER_ADDR, PARTY_A_INTERCHAIN_ROUTER_ADDR, INTERCHAIN_ROUTER_CODE, PARTY_B_INTERCHAIN_ROUTER_ADDR, INTERCHAIN_SPLITTER_CODE, + TIMEOUTS, COVENANT_CLOCK_ADDR, PRESET_HOLDER_FIELDS, COVENANT_INTERCHAIN_SPLITTER_ADDR, PRESET_SPLITTER_FIELDS, COVENANT_SWAP_HOLDER_ADDR, IBC_FEE, PARTY_A_IBC_FORWARDER_ADDR, PARTY_B_IBC_FORWARDER_ADDR, PARTY_A_INTERCHAIN_ROUTER_ADDR, PARTY_B_INTERCHAIN_ROUTER_ADDR, PRESET_PARTY_A_FORWARDER_FIELDS, PRESET_PARTY_B_FORWARDER_FIELDS, PRESET_PARTY_A_ROUTER_FIELDS, PRESET_PARTY_B_ROUTER_FIELDS, PRESET_CLOCK_FIELDS, }, msg::{InstantiateMsg, QueryMsg}, }; @@ -37,25 +44,87 @@ pub fn instantiate( deps.api.debug("WASMDEBUG: instantiate"); set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - // store all the codes for covenant configuration - CLOCK_CODE.save(deps.storage, &msg.preset_clock_fields.clock_code)?; - INTERCHAIN_SPLITTER_CODE.save(deps.storage, &msg.splitter_code)?; - INTERCHAIN_ROUTER_CODE.save(deps.storage, &msg.interchain_router_code)?; - IBC_FORWARDER_CODE.save(deps.storage, &msg.ibc_forwarder_code)?; - SWAP_HOLDER_CODE.save(deps.storage, &msg.preset_holder_fields.code_id)?; - PRESET_SPLITTER_FIELDS.save(deps.storage, &msg.preset_splitter_fields)?; - PRESET_HOLDER_FIELDS.save(deps.storage, &msg.preset_holder_fields)?; - COVENANT_PARTIES.save(deps.storage, &msg.covenant_parties)?; + let preset_party_a_forwarder_fields = PresetIbcForwarderFields { + remote_chain_connection_id: msg.party_a_config.party_chain_connection_id, + remote_chain_channel_id: msg.party_a_config.party_to_host_chain_channel_id, + denom: msg.party_a_config.native_denom, + amount: msg.covenant_terms.party_a_amount, + label: format!("{}_party_a_ibc_forwarder", msg.label), + code_id: msg.contract_codes.ibc_forwarder_code, + }; + let preset_party_b_forwarder_fields = PresetIbcForwarderFields { + remote_chain_connection_id: msg.party_b_config.party_chain_connection_id, + remote_chain_channel_id: msg.party_b_config.party_to_host_chain_channel_id, + denom: msg.party_b_config.native_denom, + amount: msg.covenant_terms.party_b_amount, + label: format!("{}_party_b_ibc_forwarder", msg.label), + code_id: msg.contract_codes.ibc_forwarder_code, + }; + let preset_party_a_router_fields = PresetInterchainRouterFields { + destination_chain_channel_id: msg.party_a_config.host_to_party_chain_channel_id, + destination_receiver_addr: msg.party_a_config.party_receiver_addr, + ibc_transfer_timeout: msg.party_a_config.ibc_transfer_timeout, + label: format!("{}_party_a_interchain_router", msg.label), + code_id: msg.contract_codes.interchain_router_code, + }; + let preset_party_b_router_fields = PresetInterchainRouterFields { + destination_chain_channel_id: msg.party_b_config.host_to_party_chain_channel_id, + destination_receiver_addr: msg.party_b_config.party_receiver_addr, + ibc_transfer_timeout: msg.party_b_config.ibc_transfer_timeout, + label: format!("{}_party_b_interchain_router", msg.label), + code_id: msg.contract_codes.interchain_router_code, + }; + let preset_splitter_fields = PresetInterchainSplitterFields { + splits: msg.splits, + fallback_split: msg.fallback_split, + label: format!("{}_interchain_splitter", msg.label), + code_id: msg.contract_codes.splitter_code, + party_a_addr: msg.party_a_config.addr.to_string(), + party_b_addr: msg.party_b_config.addr.to_string(), + }; + let preset_holder_fields = PresetSwapHolderFields { + lockup_config: msg.lockup_config, + parties_config: CovenantPartiesConfig { + party_a: CovenantParty { + addr: msg.party_a_config.addr.to_string(), + ibc_denom: msg.party_a_config.ibc_denom, + receiver_config: ReceiverConfig::Native(Addr::unchecked(msg.party_a_config.addr)), + }, + party_b: CovenantParty { + addr: msg.party_b_config.addr.to_string(), + ibc_denom: msg.party_b_config.ibc_denom, + receiver_config: ReceiverConfig::Native(Addr::unchecked(msg.party_b_config.addr)), + }, + }, + covenant_terms: CovenantTerms::TokenSwap(msg.covenant_terms), + code_id: msg.contract_codes.holder_code, + label: format!("{}_swap_holder", msg.label), + }; + let preset_clock_fields = PresetClockFields { + tick_max_gas: msg.clock_tick_max_gas, + whitelist: vec![], + code_id: msg.contract_codes.clock_code, + label: format!("{}-clock", msg.label), + }; + + PRESET_SPLITTER_FIELDS.save(deps.storage, &preset_splitter_fields)?; + PRESET_HOLDER_FIELDS.save(deps.storage, &preset_holder_fields)?; + PRESET_PARTY_A_FORWARDER_FIELDS.save(deps.storage, &preset_party_a_forwarder_fields)?; + PRESET_PARTY_B_FORWARDER_FIELDS.save(deps.storage, &preset_party_b_forwarder_fields)?; + PRESET_PARTY_A_ROUTER_FIELDS.save(deps.storage, &preset_party_a_router_fields)?; + PRESET_PARTY_B_ROUTER_FIELDS.save(deps.storage, &preset_party_b_router_fields)?; + PRESET_CLOCK_FIELDS.save(deps.storage, &preset_clock_fields)?; + TIMEOUTS.save(deps.storage, &msg.timeouts)?; IBC_FEE.save(deps.storage, &msg.preset_ibc_fee.to_ibc_fee())?; // we start the module instantiation chain with the clock let clock_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(env.contract.address.to_string()), - code_id: msg.preset_clock_fields.clock_code, - msg: to_binary(&msg.preset_clock_fields.clone().to_instantiate_msg())?, + code_id: preset_clock_fields.code_id, + msg: to_binary(&preset_clock_fields.to_instantiate_msg())?, funds: vec![], - label: msg.preset_clock_fields.label, + label: preset_clock_fields.label, }); Ok(Response::default() @@ -93,29 +162,21 @@ pub fn handle_clock_reply(deps: DepsMut, env: Env, msg: Reply) -> Result Result Result terms, - }; + let preset_party_a_forwarder_fields = PRESET_PARTY_A_FORWARDER_FIELDS.load(deps.storage)?; - let instantiate_msg = covenant_ibc_forwarder::msg::InstantiateMsg { - clock_address: clock_addr.to_string(), - next_contract: swap_holder_addr.to_string(), - remote_chain_connection_id: covenant_party.party_chain_connection_id, - remote_chain_channel_id: covenant_party.party_to_host_chain_channel_id, - denom: covenant_party.native_denom, - amount: covenant_terms.party_a_amount, + let instantiate_msg = preset_party_a_forwarder_fields.to_instantiate_msg( + clock_addr.to_string(), + swap_holder_addr.to_string(), ibc_fee, - ibc_transfer_timeout: timeouts.ibc_transfer_timeout, - ica_timeout: timeouts.ica_timeout, - }; - + timeouts.ibc_transfer_timeout, + timeouts.ica_timeout + ); let party_a_forwarder_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(env.contract.address.to_string()), - code_id, + code_id: preset_party_a_forwarder_fields.code_id, msg: to_binary(&instantiate_msg)?, funds: vec![], - label: "party_a_forwarder".to_string(), + label: preset_party_a_forwarder_fields.label, }); Ok(Response::default() @@ -336,33 +372,25 @@ pub fn handle_party_a_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) - // load the fields relevant to ibc forwarder instantiation let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; - let code_id = IBC_FORWARDER_CODE.load(deps.storage)?; let timeouts = TIMEOUTS.load(deps.storage)?; let ibc_fee = IBC_FEE.load(deps.storage)?; - let covenant_party = COVENANT_PARTIES.load(deps.storage)?.party_b; - let covenant_terms = match PRESET_HOLDER_FIELDS.load(deps.storage)?.covenant_terms { - covenant_utils::CovenantTerms::TokenSwap(terms) => terms, - }; + let preset_party_b_forwarder_fields = PRESET_PARTY_B_FORWARDER_FIELDS.load(deps.storage)?; let swap_holder = COVENANT_SWAP_HOLDER_ADDR.load(deps.storage)?; - let instantiate_msg = covenant_ibc_forwarder::msg::InstantiateMsg { - clock_address: clock_addr.to_string(), - next_contract: swap_holder.to_string(), - remote_chain_connection_id: covenant_party.party_chain_connection_id, - remote_chain_channel_id: covenant_party.party_to_host_chain_channel_id, - denom: covenant_party.native_denom, - amount: covenant_terms.party_b_amount, + let instantiate_msg = preset_party_b_forwarder_fields.to_instantiate_msg( + clock_addr.to_string(), + swap_holder.to_string(), ibc_fee, - ibc_transfer_timeout: timeouts.ibc_transfer_timeout, - ica_timeout: timeouts.ica_timeout, - }; + timeouts.ibc_transfer_timeout, + timeouts.ica_timeout, + ); let party_b_forwarder_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(env.contract.address.to_string()), - code_id, + code_id: preset_party_b_forwarder_fields.code_id, msg: to_binary(&instantiate_msg)?, funds: vec![], - label: "party_b_forwarder".to_string(), + label: preset_party_b_forwarder_fields.label, }); Ok(Response::default() @@ -389,14 +417,11 @@ pub fn handle_party_b_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) - // validate and store the party b ibc forwarder address let party_b_ibc_forwarder_addr = deps.api.addr_validate(&response.contract_address)?; PARTY_B_IBC_FORWARDER_ADDR.save(deps.storage, &party_b_ibc_forwarder_addr)?; - let party_a_forwarder = PARTY_A_IBC_FORWARDER_ADDR.load(deps.storage)?; - // load the fields relevant to ibc forwarder instantiation + let party_a_forwarder = PARTY_A_IBC_FORWARDER_ADDR.load(deps.storage)?; let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; - let clock_code_id = CLOCK_CODE.load(deps.storage)?; - + let preset_clock_fields = PRESET_CLOCK_FIELDS.load(deps.storage)?; let swap_holder = COVENANT_SWAP_HOLDER_ADDR.load(deps.storage)?; - let interchain_splitter = COVENANT_INTERCHAIN_SPLITTER_ADDR.load(deps.storage)?; let party_a_router = PARTY_A_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; let party_b_router = PARTY_B_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; @@ -404,7 +429,7 @@ pub fn handle_party_b_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) - let update_clock_whitelist_msg = WasmMsg::Migrate { contract_addr: clock_addr.to_string(), - new_code_id: clock_code_id, + new_code_id: preset_clock_fields.code_id, msg: to_binary(&covenant_clock::msg::MigrateMsg::ManageWhitelist { add: Some(vec![ party_a_forwarder.to_string(), @@ -437,7 +462,6 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::ClockAddress{} => Ok(to_binary(&COVENANT_CLOCK_ADDR.may_load(deps.storage)?)?), QueryMsg::HolderAddress {} => Ok(to_binary(&COVENANT_SWAP_HOLDER_ADDR.may_load(deps.storage)?)?), QueryMsg::SplitterAddress {} => Ok(to_binary(&COVENANT_INTERCHAIN_SPLITTER_ADDR.may_load(deps.storage)?)?), - QueryMsg::CovenantParties {} => Ok(to_binary(&COVENANT_PARTIES.may_load(deps.storage)?)?), QueryMsg::InterchainRouterAddress { party } => { let resp = if party == "party_a" { PARTY_A_INTERCHAIN_ROUTER_ADDR.may_load(deps.storage)? diff --git a/contracts/swap-covenant/src/msg.rs b/contracts/swap-covenant/src/msg.rs index 6ba362e4..a609210f 100644 --- a/contracts/swap-covenant/src/msg.rs +++ b/contracts/swap-covenant/src/msg.rs @@ -1,8 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Uint128, Uint64}; -use covenant_clock::msg::PresetClockFields; -use covenant_interchain_splitter::msg::PresetInterchainSplitterFields; -use covenant_swap_holder::msg::PresetSwapHolderFields; +use covenant_interchain_splitter::msg::{DenomSplit, SplitType}; +use covenant_utils::{LockupConfig, SwapCovenantTerms}; use neutron_sdk::bindings::msg::IbcFee; const NEUTRON_DENOM: &str = "untrn"; @@ -10,24 +9,47 @@ pub const DEFAULT_TIMEOUT: u64 = 60 * 60 * 5; // 5 hours #[cw_serde] pub struct InstantiateMsg { - /// contract label for this specific covenant pub label: String, - - /// ibc transfer and ica timeouts passed down to relevant modules pub timeouts: Timeouts, pub preset_ibc_fee: PresetIbcFee, + pub contract_codes: SwapCovenantContractCodeIds, + pub clock_tick_max_gas: Option, + pub lockup_config: LockupConfig, + pub covenant_terms: SwapCovenantTerms, + pub party_a_config: CovenantPartyConfig, + pub party_b_config: CovenantPartyConfig, + pub splits: Vec, + pub fallback_split: Option, +} + +#[cw_serde] +pub struct CovenantPartyConfig { + /// authorized address of the party + pub addr: String, + /// denom provided by the party on its native chain + pub native_denom: String, + /// ibc denom provided by the party on neutron + pub ibc_denom: String, + /// channel id from party to host chain + pub party_to_host_chain_channel_id: String, + /// channel id from host chain to the party chain + pub host_to_party_chain_channel_id: String, + /// address of the receiver on destination chain + pub party_receiver_addr: String, + /// connection id to the party chain + pub party_chain_connection_id: String, + /// timeout in seconds + pub ibc_transfer_timeout: Uint64, + +} +#[cw_serde] +pub struct SwapCovenantContractCodeIds { pub ibc_forwarder_code: u64, pub interchain_router_code: u64, pub splitter_code: u64, - - /// instantiation fields relevant to clock module known in advance - pub preset_clock_fields: PresetClockFields, - - /// instantiation fields relevant to swap holder contract known in advance - pub preset_holder_fields: PresetSwapHolderFields, - pub covenant_parties: SwapCovenantParties, - pub preset_splitter_fields: PresetInterchainSplitterFields, + pub holder_code: u64, + pub clock_code: u64, } #[cw_serde] @@ -109,8 +131,8 @@ pub enum QueryMsg { HolderAddress {}, #[returns(Addr)] SplitterAddress {}, - #[returns(SwapCovenantParties)] - CovenantParties {}, + // #[returns(SwapCovenantParties)] + // CovenantParties {}, #[returns(Addr)] InterchainRouterAddress { party: String }, #[returns(Addr)] diff --git a/contracts/swap-covenant/src/state.rs b/contracts/swap-covenant/src/state.rs index d6fcb87d..21665c06 100644 --- a/contracts/swap-covenant/src/state.rs +++ b/contracts/swap-covenant/src/state.rs @@ -1,24 +1,14 @@ use cosmwasm_std::Addr; +use covenant_clock::msg::PresetClockFields; +use covenant_ibc_forwarder::msg::PresetIbcForwarderFields; +use covenant_interchain_router::msg::PresetInterchainRouterFields; use covenant_interchain_splitter::msg::PresetInterchainSplitterFields; use covenant_swap_holder::msg::PresetSwapHolderFields; -use covenant_utils::SwapCovenantTerms; use cw_storage_plus::Item; use neutron_sdk::bindings::msg::IbcFee; -use crate::msg::{Timeouts, SwapCovenantParties}; - -// TODO: get rid of the code storage as they can stay on the preset fields items -/// contract code for the ibc forwarder -pub const IBC_FORWARDER_CODE: Item = Item::new("ibc_forwarder_code"); -/// contract code for the interchain splitter -pub const INTERCHAIN_SPLITTER_CODE: Item = Item::new("interchain_splitter_code"); -/// contract code for the swap holder -pub const SWAP_HOLDER_CODE: Item = Item::new("swap_holder_code"); -/// contract code for the clock module -pub const CLOCK_CODE: Item = Item::new("clock_code"); -/// contract code for the interchain router -pub const INTERCHAIN_ROUTER_CODE: Item = Item::new("interchain_router_code"); +use crate::msg::Timeouts; /// ibc fee for the relayers pub const IBC_FEE: Item = Item::new("ibc_fee"); @@ -26,27 +16,20 @@ pub const IBC_FEE: Item = Item::new("ibc_fee"); /// modules dealing with ICA pub const TIMEOUTS: Item = Item::new("timeouts"); -// /// fields related to the clock module known prior to covenant instatiation. -pub const PRESET_CLOCK_FIELDS: Item = - Item::new("preset_clock_fields"); - +// /// fields related to the contracts known prior to their. +pub const PRESET_CLOCK_FIELDS: Item = Item::new("preset_clock_fields"); pub const PRESET_HOLDER_FIELDS: Item = Item::new("preset_holder_fields"); pub const PRESET_SPLITTER_FIELDS: Item = Item::new("preset_splitter_fields"); +pub const PRESET_PARTY_A_ROUTER_FIELDS: Item = Item::new("preset_party_a_router_fields"); +pub const PRESET_PARTY_B_ROUTER_FIELDS: Item = Item::new("preset_party_b_router_fields"); +pub const PRESET_PARTY_A_FORWARDER_FIELDS: Item = Item::new("preset_party_a_forwarder_fields"); +pub const PRESET_PARTY_B_FORWARDER_FIELDS: Item = Item::new("preset_party_b_forwarder_fields"); - -/// address of the clock module associated with this covenant pub const COVENANT_CLOCK_ADDR: Item = Item::new("covenant_clock_addr"); -/// address of the interchain splitter contract associated with this covenant pub const COVENANT_INTERCHAIN_SPLITTER_ADDR: Item = Item::new("covenant_interchain_splitter_addr"); -/// address of the swap holder contract associated with this covenant pub const COVENANT_SWAP_HOLDER_ADDR: Item = Item::new("covenant_swap_holder_addr"); - - -pub const COVENANT_PARTIES: Item = Item::new("covenant_parties"); -// pub const COVENANT_TERMS: Item = Item::new("swap_covenant_terms"); - pub const PARTY_A_IBC_FORWARDER_ADDR: Item = Item::new("party_a_ibc_forwarder_addr"); pub const PARTY_B_IBC_FORWARDER_ADDR: Item = Item::new("party_b_ibc_forwarder_addr"); - pub const PARTY_A_INTERCHAIN_ROUTER_ADDR: Item = Item::new("party_a_interchain_router_addr"); -pub const PARTY_B_INTERCHAIN_ROUTER_ADDR: Item = Item::new("party_b_interchain_router_addr"); \ No newline at end of file +pub const PARTY_B_INTERCHAIN_ROUTER_ADDR: Item = Item::new("party_b_interchain_router_addr"); + diff --git a/contracts/swap-covenant/src/suite_test/suite.rs b/contracts/swap-covenant/src/suite_test/suite.rs index fb7d3c3a..5c9256ba 100644 --- a/contracts/swap-covenant/src/suite_test/suite.rs +++ b/contracts/swap-covenant/src/suite_test/suite.rs @@ -34,12 +34,14 @@ impl Default for SuiteBuilder { label: todo!(), preset_ibc_fee: todo!(), timeouts: todo!(), - preset_clock_fields: todo!(), - preset_holder_fields: todo!(), - ibc_forwarder_code: todo!(), - covenant_parties: todo!(), - interchain_router_code: todo!(), - splitter_code: todo!(), + contract_codes: todo!(), + clock_tick_max_gas: todo!(), + lockup_config: todo!(), + covenant_terms: todo!(), + party_a_config: todo!(), + party_b_config: todo!(), + splits: todo!(), + fallback_split: todo!(), }, } } diff --git a/contracts/swap-holder/src/suite_tests/suite.rs b/contracts/swap-holder/src/suite_tests/suite.rs index 9103ecb9..9cfe1958 100644 --- a/contracts/swap-holder/src/suite_tests/suite.rs +++ b/contracts/swap-holder/src/suite_tests/suite.rs @@ -44,18 +44,18 @@ impl Default for SuiteBuilder { lockup_config: LockupConfig::None, parties_config: CovenantPartiesConfig { party_a: CovenantParty { - addr: Addr::unchecked(PARTY_A_ADDR.to_string()), - provided_denom: DENOM_A.to_string(), + addr: PARTY_A_ADDR.to_string(), receiver_config: ReceiverConfig::Native(Addr::unchecked( PARTY_A_ADDR.to_string(), )), + ibc_denom: DENOM_A.to_string(), }, party_b: CovenantParty { - addr: Addr::unchecked(PARTY_B_ADDR.to_string()), - provided_denom: DENOM_B.to_string(), + addr: PARTY_B_ADDR.to_string(), receiver_config: ReceiverConfig::Native(Addr::unchecked( PARTY_B_ADDR.to_string(), )), + ibc_denom: DENOM_B.to_string(), }, }, covenant_terms: CovenantTerms::TokenSwap(SwapCovenantTerms { diff --git a/contracts/swap-holder/src/suite_tests/tests.rs b/contracts/swap-holder/src/suite_tests/tests.rs index d0be2dad..63cf6240 100644 --- a/contracts/swap-holder/src/suite_tests/tests.rs +++ b/contracts/swap-holder/src/suite_tests/tests.rs @@ -31,13 +31,13 @@ fn test_instantiate_happy_and_query_all() { covenant_parties, CovenantPartiesConfig { party_a: CovenantParty { - addr: Addr::unchecked(PARTY_A_ADDR.to_string()), - provided_denom: DENOM_A.to_string(), + addr: PARTY_A_ADDR.to_string(), + ibc_denom: DENOM_A.to_string(), receiver_config: ReceiverConfig::Native(Addr::unchecked(PARTY_A_ADDR.to_string())), }, party_b: CovenantParty { - addr: Addr::unchecked(PARTY_B_ADDR.to_string()), - provided_denom: DENOM_B.to_string(), + ibc_denom: DENOM_B.to_string(), + addr: PARTY_B_ADDR.to_string(), receiver_config: ReceiverConfig::Native(Addr::unchecked(PARTY_B_ADDR.to_string())), }, } diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index 92bd24a9..fddc7248 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -3,7 +3,6 @@ use cosmwasm_std::{ Addr, Attribute, BankMsg, BlockInfo, Coin, CosmosMsg, IbcMsg, IbcTimeout, StdError, Timestamp, Uint128, Uint64, }; -use neutron_ica::RemoteChainInfo; use neutron_sdk::{bindings::msg::{NeutronMsg, IbcFee}, sudo::msg::RequestPacketTimeoutHeight}; pub mod neutron_ica { @@ -256,7 +255,7 @@ impl ReceiverConfig { #[cw_serde] pub struct CovenantParty { /// authorized address of the party - pub addr: Addr, + pub addr: String, /// denom provided by the party pub ibc_denom: String, /// information about receiver address diff --git a/swap-covenant/tests/interchaintest/tokenswap_test.go b/swap-covenant/tests/interchaintest/tokenswap_test.go index 48739992..86de0b01 100644 --- a/swap-covenant/tests/interchaintest/tokenswap_test.go +++ b/swap-covenant/tests/interchaintest/tokenswap_test.go @@ -140,7 +140,7 @@ func TestTokenSwap(t *testing.T) { client, network := ibctest.DockerSetup(t) r := ibctest.NewBuiltinRelayerFactory( ibc.CosmosRly, - zaptest.NewLogger(t, zaptest.Level(zap.ErrorLevel)), + zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel)), relayer.CustomDockerImage("ghcr.io/cosmos/relayer", "v2.3.1", rly.RlyDefaultUidGid), relayer.RelayerOptionExtraStartFlags{Flags: []string{"-p", "events", "-b", "100", "-d", "--log-format", "console"}}, ).Build(t, client, network) @@ -381,14 +381,6 @@ func TestTokenSwap(t *testing.T) { }) t.Run("instantiate covenant", func(t *testing.T) { - - // Clock instantiation message - clockMsg := PresetClockFields{ - ClockCode: clockCodeId, - Label: "covenant-clock", - Whitelist: []string{}, - } - timeouts := Timeouts{ IcaTimeout: "100", // sec IbcTransferTimeout: "100", // sec @@ -399,66 +391,16 @@ func TestTokenSwap(t *testing.T) { PartyBAmount: strconv.FormatUint(osmoContributionAmount, 10), } - covenantPartiesConfig := CovenantPartiesConfig{ - PartyA: CovenantParty{ - Addr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), - IbcDenom: neutronAtomIbcDenom, - ReceiverConfig: ReceiverConfig{ - Native: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), - }, - }, - PartyB: CovenantParty{ - Addr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), - IbcDenom: neutronOsmoIbcDenom, - ReceiverConfig: ReceiverConfig{ - Native: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), - }, - }, - } timestamp := Timestamp("1981539923") lockupConfig := LockupConfig{ Time: ×tamp, } - covenantTerms := CovenantTerms{ - TokenSwap: swapCovenantTerms, - } presetIbcFee := PresetIbcFee{ AckFee: "10000", TimeoutFee: "10000", } - presetSwapHolder := PresetSwapHolderFields{ - LockupConfig: lockupConfig, - CovenantPartiesConfig: covenantPartiesConfig, - CovenantTerms: covenantTerms, - CodeId: swapHolderCodeId, - Label: "swap-holder", - } - - swapCovenantParties := SwapCovenantParties{ - PartyA: SwapPartyConfig{ - Addr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), - NativeDenom: nativeAtomDenom, - IbcDenom: neutronAtomIbcDenom, - PartyToHostChainChannelId: testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], - HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], - PartyReceiverAddr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), - PartyChainConnectionId: neutronAtomIBCConnId, - IbcTransferTimeout: timeouts.IbcTransferTimeout, - }, - PartyB: SwapPartyConfig{ - Addr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), - NativeDenom: nativeOsmoDenom, - IbcDenom: neutronOsmoIbcDenom, - PartyToHostChainChannelId: testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name], - HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], - PartyReceiverAddr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), - PartyChainConnectionId: neutronOsmosisIBCConnId, - IbcTransferTimeout: timeouts.IbcTransferTimeout, - }, - } - presetSplitterFields := PresetSplitterFields{ Splits: []DenomSplit{ { @@ -492,23 +434,51 @@ func TestTokenSwap(t *testing.T) { Label: "interchain-splitter", } - covenantMsg := CovenantInstantiateMsg{ - Label: "swap-covenant", - Timeouts: timeouts, - PresetIbcFee: presetIbcFee, + partyAConfig := SwapPartyConfig{ + Addr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + NativeDenom: nativeAtomDenom, + IbcDenom: neutronAtomIbcDenom, + PartyToHostChainChannelId: testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], + HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], + PartyReceiverAddr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + PartyChainConnectionId: neutronAtomIBCConnId, + IbcTransferTimeout: timeouts.IbcTransferTimeout, + } + partyBConfig := SwapPartyConfig{ + Addr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + NativeDenom: nativeOsmoDenom, + IbcDenom: neutronOsmoIbcDenom, + PartyToHostChainChannelId: testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name], + HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], + PartyReceiverAddr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + PartyChainConnectionId: neutronOsmosisIBCConnId, + IbcTransferTimeout: timeouts.IbcTransferTimeout, + } + codeIds := SwapCovenantContractCodeIds{ IbcForwarderCode: ibcForwarderCodeId, InterchainRouterCode: routerCodeId, InterchainSplitterCode: splitterCodeId, - PresetClock: clockMsg, - PresetSwapHolder: presetSwapHolder, - SwapCovenantParties: swapCovenantParties, - PresetSplitterFields: presetSplitterFields, + ClockCode: clockCodeId, + HolderCode: swapHolderCodeId, } + covenantMsg := CovenantInstantiateMsg{ + Label: "swap-covenant", + Timeouts: timeouts, + PresetIbcFee: presetIbcFee, + SwapCovenantContractCodeIds: codeIds, + LockupConfig: lockupConfig, + SwapCovenantTerms: swapCovenantTerms, + PartyAConfig: partyAConfig, + PartyBConfig: partyBConfig, + Splits: presetSplitterFields.Splits, + FallbackSplit: presetSplitterFields.FallbackSplit, + } str, err := json.Marshal(covenantMsg) require.NoError(t, err, "Failed to marshall CovenantInstantiateMsg") instantiateMsg := string(str) + println("instantiation message: ", instantiateMsg) cmd := []string{"neutrond", "tx", "wasm", "instantiate", covenantCodeIdStr, instantiateMsg, "--label", "swap-covenant", diff --git a/swap-covenant/tests/interchaintest/types.go b/swap-covenant/tests/interchaintest/types.go index 7602f68f..b99b9e6d 100644 --- a/swap-covenant/tests/interchaintest/types.go +++ b/swap-covenant/tests/interchaintest/types.go @@ -6,16 +6,25 @@ package ibc_test // ----- Covenant Instantiation ------ type CovenantInstantiateMsg struct { - Label string `json:"label"` - Timeouts Timeouts `json:"timeouts"` - PresetIbcFee PresetIbcFee `json:"preset_ibc_fee"` - IbcForwarderCode uint64 `json:"ibc_forwarder_code"` - InterchainRouterCode uint64 `json:"interchain_router_code"` - InterchainSplitterCode uint64 `json:"splitter_code"` - PresetClock PresetClockFields `json:"preset_clock_fields"` - PresetSwapHolder PresetSwapHolderFields `json:"preset_holder_fields"` - SwapCovenantParties SwapCovenantParties `json:"covenant_parties"` - PresetSplitterFields PresetSplitterFields `json:"preset_splitter_fields"` + Label string `json:"label"` + Timeouts Timeouts `json:"timeouts"` + PresetIbcFee PresetIbcFee `json:"preset_ibc_fee"` + SwapCovenantContractCodeIds SwapCovenantContractCodeIds `json:"contract_codes"` + TickMaxGas string `json:"clock_tick_max_gas,omitempty"` + LockupConfig LockupConfig `json:"lockup_config"` + SwapCovenantTerms SwapCovenantTerms `json:"covenant_terms"` + PartyAConfig SwapPartyConfig `json:"party_a_config"` + PartyBConfig SwapPartyConfig `json:"party_b_config"` + Splits []DenomSplit `json:"splits"` + FallbackSplit *SplitType `json:"fallback_split,omitempty"` +} + +type SwapCovenantContractCodeIds struct { + IbcForwarderCode uint64 `json:"ibc_forwarder_code"` + InterchainRouterCode uint64 `json:"interchain_router_code"` + InterchainSplitterCode uint64 `json:"splitter_code"` + ClockCode uint64 `json:"clock_code"` + HolderCode uint64 `json:"holder_code"` } type Receiver struct { diff --git a/contracts/covenant/.cargo/config b/v1/covenant/.cargo/config similarity index 100% rename from contracts/covenant/.cargo/config rename to v1/covenant/.cargo/config diff --git a/contracts/covenant/Cargo.toml b/v1/covenant/Cargo.toml similarity index 97% rename from contracts/covenant/Cargo.toml rename to v1/covenant/Cargo.toml index a6d48702..2f95164f 100644 --- a/contracts/covenant/Cargo.toml +++ b/v1/covenant/Cargo.toml @@ -14,7 +14,7 @@ exclude = [ [lib] -crate-type = ["cdylib", "rlib"] +# crate-type = ["cdylib", "rlib"] [features] diff --git a/contracts/covenant/LICENSE b/v1/covenant/LICENSE similarity index 100% rename from contracts/covenant/LICENSE rename to v1/covenant/LICENSE diff --git a/contracts/covenant/README.md b/v1/covenant/README.md similarity index 100% rename from contracts/covenant/README.md rename to v1/covenant/README.md diff --git a/contracts/covenant/examples/schema.rs b/v1/covenant/examples/schema.rs similarity index 100% rename from contracts/covenant/examples/schema.rs rename to v1/covenant/examples/schema.rs diff --git a/contracts/covenant/src/contract.rs b/v1/covenant/src/contract.rs similarity index 100% rename from contracts/covenant/src/contract.rs rename to v1/covenant/src/contract.rs diff --git a/contracts/covenant/src/error.rs b/v1/covenant/src/error.rs similarity index 100% rename from contracts/covenant/src/error.rs rename to v1/covenant/src/error.rs diff --git a/contracts/covenant/src/instantiate2.rs b/v1/covenant/src/instantiate2.rs similarity index 100% rename from contracts/covenant/src/instantiate2.rs rename to v1/covenant/src/instantiate2.rs diff --git a/contracts/covenant/src/lib.rs b/v1/covenant/src/lib.rs similarity index 100% rename from contracts/covenant/src/lib.rs rename to v1/covenant/src/lib.rs diff --git a/contracts/covenant/src/msg.rs b/v1/covenant/src/msg.rs similarity index 100% rename from contracts/covenant/src/msg.rs rename to v1/covenant/src/msg.rs diff --git a/contracts/covenant/src/state.rs b/v1/covenant/src/state.rs similarity index 100% rename from contracts/covenant/src/state.rs rename to v1/covenant/src/state.rs diff --git a/contracts/covenant/src/suite_test/mod.rs b/v1/covenant/src/suite_test/mod.rs similarity index 100% rename from contracts/covenant/src/suite_test/mod.rs rename to v1/covenant/src/suite_test/mod.rs diff --git a/contracts/covenant/src/suite_test/suite.rs b/v1/covenant/src/suite_test/suite.rs similarity index 100% rename from contracts/covenant/src/suite_test/suite.rs rename to v1/covenant/src/suite_test/suite.rs diff --git a/contracts/covenant/src/suite_test/tests.rs b/v1/covenant/src/suite_test/tests.rs similarity index 100% rename from contracts/covenant/src/suite_test/tests.rs rename to v1/covenant/src/suite_test/tests.rs diff --git a/contracts/covenant/src/suite_test/unit_tests.rs b/v1/covenant/src/suite_test/unit_tests.rs similarity index 100% rename from contracts/covenant/src/suite_test/unit_tests.rs rename to v1/covenant/src/suite_test/unit_tests.rs From b64eee56d57f4b97c94664cdf1fff75a6e45bd7b Mon Sep 17 00:00:00 2001 From: bekauz Date: Sat, 16 Sep 2023 16:00:34 +0200 Subject: [PATCH 082/586] moving timeouts to preset fields in covenant --- contracts/ibc-forwarder/src/msg.rs | 12 +++++----- contracts/swap-covenant/src/contract.rs | 23 ++++++------------- contracts/swap-covenant/src/msg.rs | 6 +---- contracts/swap-covenant/src/state.rs | 11 +-------- .../tests/interchaintest/tokenswap_test.go | 2 +- 5 files changed, 16 insertions(+), 38 deletions(-) diff --git a/contracts/ibc-forwarder/src/msg.rs b/contracts/ibc-forwarder/src/msg.rs index 85c09cb0..f9c75dd7 100644 --- a/contracts/ibc-forwarder/src/msg.rs +++ b/contracts/ibc-forwarder/src/msg.rs @@ -47,6 +47,9 @@ pub struct PresetIbcForwarderFields { pub amount: Uint128, pub label: String, pub code_id: u64, + pub ica_timeout: Uint64, + pub ibc_transfer_timeout: Uint64, + pub ibc_fee: IbcFee, } impl PresetIbcForwarderFields { @@ -54,9 +57,6 @@ impl PresetIbcForwarderFields { &self, clock_address: String, next_contract: String, - ibc_fee: IbcFee, - ibc_transfer_timeout: Uint64, - ica_timeout: Uint64, ) -> InstantiateMsg { InstantiateMsg { clock_address, @@ -65,9 +65,9 @@ impl PresetIbcForwarderFields { remote_chain_channel_id: self.remote_chain_channel_id.to_string(), denom: self.denom.to_string(), amount: self.amount, - ibc_fee, - ibc_transfer_timeout, - ica_timeout, + ibc_fee: self.ibc_fee.clone(), + ibc_transfer_timeout: self.ibc_transfer_timeout, + ica_timeout: self.ica_timeout, } } } diff --git a/contracts/swap-covenant/src/contract.rs b/contracts/swap-covenant/src/contract.rs index 8124ecdb..e7c45ec0 100644 --- a/contracts/swap-covenant/src/contract.rs +++ b/contracts/swap-covenant/src/contract.rs @@ -18,7 +18,7 @@ use cw_utils::{parse_reply_instantiate_data, ParseReplyError}; use crate::{ error::ContractError, state::{ - TIMEOUTS, COVENANT_CLOCK_ADDR, PRESET_HOLDER_FIELDS, COVENANT_INTERCHAIN_SPLITTER_ADDR, PRESET_SPLITTER_FIELDS, COVENANT_SWAP_HOLDER_ADDR, IBC_FEE, PARTY_A_IBC_FORWARDER_ADDR, PARTY_B_IBC_FORWARDER_ADDR, PARTY_A_INTERCHAIN_ROUTER_ADDR, PARTY_B_INTERCHAIN_ROUTER_ADDR, PRESET_PARTY_A_FORWARDER_FIELDS, PRESET_PARTY_B_FORWARDER_FIELDS, PRESET_PARTY_A_ROUTER_FIELDS, PRESET_PARTY_B_ROUTER_FIELDS, PRESET_CLOCK_FIELDS, + COVENANT_CLOCK_ADDR, PRESET_HOLDER_FIELDS, COVENANT_INTERCHAIN_SPLITTER_ADDR, PRESET_SPLITTER_FIELDS, COVENANT_SWAP_HOLDER_ADDR, PARTY_A_IBC_FORWARDER_ADDR, PARTY_B_IBC_FORWARDER_ADDR, PARTY_A_INTERCHAIN_ROUTER_ADDR, PARTY_B_INTERCHAIN_ROUTER_ADDR, PRESET_PARTY_A_FORWARDER_FIELDS, PRESET_PARTY_B_FORWARDER_FIELDS, PRESET_PARTY_A_ROUTER_FIELDS, PRESET_PARTY_B_ROUTER_FIELDS, PRESET_CLOCK_FIELDS, }, msg::{InstantiateMsg, QueryMsg}, }; @@ -51,6 +51,9 @@ pub fn instantiate( amount: msg.covenant_terms.party_a_amount, label: format!("{}_party_a_ibc_forwarder", msg.label), code_id: msg.contract_codes.ibc_forwarder_code, + ica_timeout: msg.timeouts.ica_timeout, + ibc_transfer_timeout: msg.timeouts.ibc_transfer_timeout, + ibc_fee: msg.preset_ibc_fee.to_ibc_fee(), }; let preset_party_b_forwarder_fields = PresetIbcForwarderFields { remote_chain_connection_id: msg.party_b_config.party_chain_connection_id, @@ -59,6 +62,9 @@ pub fn instantiate( amount: msg.covenant_terms.party_b_amount, label: format!("{}_party_b_ibc_forwarder", msg.label), code_id: msg.contract_codes.ibc_forwarder_code, + ica_timeout: msg.timeouts.ica_timeout, + ibc_transfer_timeout: msg.timeouts.ibc_transfer_timeout, + ibc_fee: msg.preset_ibc_fee.to_ibc_fee(), }; let preset_party_a_router_fields = PresetInterchainRouterFields { destination_chain_channel_id: msg.party_a_config.host_to_party_chain_channel_id, @@ -115,9 +121,6 @@ pub fn instantiate( PRESET_PARTY_B_ROUTER_FIELDS.save(deps.storage, &preset_party_b_router_fields)?; PRESET_CLOCK_FIELDS.save(deps.storage, &preset_clock_fields)?; - TIMEOUTS.save(deps.storage, &msg.timeouts)?; - IBC_FEE.save(deps.storage, &msg.preset_ibc_fee.to_ibc_fee())?; - // we start the module instantiation chain with the clock let clock_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(env.contract.address.to_string()), @@ -327,16 +330,11 @@ pub fn handle_swap_holder_reply(deps: DepsMut, env: Env, msg: Reply) -> Result StdResult { }; Ok(to_binary(&resp)?) } - QueryMsg::IbcFee {} => Ok(to_binary(&IBC_FEE.may_load(deps.storage)?)?), - QueryMsg::Timeouts {} => Ok(to_binary(&TIMEOUTS.may_load(deps.storage)?)?), } } diff --git a/contracts/swap-covenant/src/msg.rs b/contracts/swap-covenant/src/msg.rs index a609210f..32d8c7a8 100644 --- a/contracts/swap-covenant/src/msg.rs +++ b/contracts/swap-covenant/src/msg.rs @@ -103,7 +103,7 @@ pub struct PresetIbcFee { } impl PresetIbcFee { - pub fn to_ibc_fee(self) -> IbcFee { + pub fn to_ibc_fee(&self) -> IbcFee { IbcFee { // must be empty recv_fee: vec![], @@ -137,10 +137,6 @@ pub enum QueryMsg { InterchainRouterAddress { party: String }, #[returns(Addr)] IbcForwarderAddress { party: String }, - #[returns(IbcFee)] - IbcFee {}, - #[returns(Timeouts)] - Timeouts {}, } #[cw_serde] diff --git a/contracts/swap-covenant/src/state.rs b/contracts/swap-covenant/src/state.rs index 21665c06..afb9dcec 100644 --- a/contracts/swap-covenant/src/state.rs +++ b/contracts/swap-covenant/src/state.rs @@ -6,17 +6,8 @@ use covenant_interchain_splitter::msg::PresetInterchainSplitterFields; use covenant_swap_holder::msg::PresetSwapHolderFields; use cw_storage_plus::Item; -use neutron_sdk::bindings::msg::IbcFee; -use crate::msg::Timeouts; - -/// ibc fee for the relayers -pub const IBC_FEE: Item = Item::new("ibc_fee"); -/// ibc transfer and ica timeouts that will be passed down to -/// modules dealing with ICA -pub const TIMEOUTS: Item = Item::new("timeouts"); - -// /// fields related to the contracts known prior to their. +// fields related to the contracts known prior to their. pub const PRESET_CLOCK_FIELDS: Item = Item::new("preset_clock_fields"); pub const PRESET_HOLDER_FIELDS: Item = Item::new("preset_holder_fields"); pub const PRESET_SPLITTER_FIELDS: Item = Item::new("preset_splitter_fields"); diff --git a/swap-covenant/tests/interchaintest/tokenswap_test.go b/swap-covenant/tests/interchaintest/tokenswap_test.go index 86de0b01..92523617 100644 --- a/swap-covenant/tests/interchaintest/tokenswap_test.go +++ b/swap-covenant/tests/interchaintest/tokenswap_test.go @@ -140,7 +140,7 @@ func TestTokenSwap(t *testing.T) { client, network := ibctest.DockerSetup(t) r := ibctest.NewBuiltinRelayerFactory( ibc.CosmosRly, - zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel)), + zaptest.NewLogger(t, zaptest.Level(zap.InfoLevel)), relayer.CustomDockerImage("ghcr.io/cosmos/relayer", "v2.3.1", rly.RlyDefaultUidGid), relayer.RelayerOptionExtraStartFlags{Flags: []string{"-p", "events", "-b", "100", "-d", "--log-format", "console"}}, ).Build(t, client, network) From b61a588a589fd668b7bdb91073e1aae6d91b869e Mon Sep 17 00:00:00 2001 From: bekauz Date: Sat, 16 Sep 2023 23:22:53 +0200 Subject: [PATCH 083/586] ictest holder state assertion fix --- .../tests/interchaintest/tokenswap_test.go | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/swap-covenant/tests/interchaintest/tokenswap_test.go b/swap-covenant/tests/interchaintest/tokenswap_test.go index 92523617..c82ea82a 100644 --- a/swap-covenant/tests/interchaintest/tokenswap_test.go +++ b/swap-covenant/tests/interchaintest/tokenswap_test.go @@ -639,7 +639,7 @@ func TestTokenSwap(t *testing.T) { t.Run("tokenswap run", func(t *testing.T) { tickClock := func() { - println("tick") + println("\ntick") cmd := []string{"neutrond", "tx", "wasm", "execute", clockAddress, `{"tick":{}}`, "--from", neutronUser.KeyName, @@ -660,6 +660,32 @@ func TestTokenSwap(t *testing.T) { require.NoError(t, err) err = testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis) require.NoError(t, err, "failed to wait for blocks") + var response CovenantAddressQueryResponse + type ContractState struct{} + type ContractStateQuery struct { + ContractState ContractState `json:"contract_state"` + } + contractStateQuery := ContractStateQuery{ + ContractState: ContractState{}, + } + + require.NoError(t, + cosmosNeutron.QueryContract(ctx, partyAIbcForwarderAddress, contractStateQuery, &response), + "failed to query forwarder A state") + partyAForwarderState := response.Data + require.NoError(t, + cosmosNeutron.QueryContract(ctx, partyBIbcForwarderAddress, contractStateQuery, &response), + "failed to query forwarder A state") + partyBForwarderState := response.Data + require.NoError(t, + cosmosNeutron.QueryContract(ctx, holderAddress, contractStateQuery, &response), + "failed to query forwarder A state") + holderState := response.Data + + println("partyAForwarderState: ", partyAForwarderState) + println("partyBForwarderState: ", partyBForwarderState) + println("holderState: ", holderState) + } t.Run("tick until forwarders create ICA", func(t *testing.T) { @@ -740,7 +766,21 @@ func TestTokenSwap(t *testing.T) { holderAtomBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronAtomIbcDenom) require.NoError(t, err, "failed to query holder atom bal") - if holderAtomBal != 0 && holderOsmoBal != 0 { + var response CovenantAddressQueryResponse + type ContractState struct{} + type ContractStateQuery struct { + ContractState ContractState `json:"contract_state"` + } + contractStateQuery := ContractStateQuery{ + ContractState: ContractState{}, + } + + require.NoError(t, + cosmosNeutron.QueryContract(ctx, holderAddress, contractStateQuery, &response), + "failed to query forwarder A state") + holderState := response.Data + + if holderAtomBal != 0 && holderOsmoBal != 0 || holderState == "completed" { println("holder atom bal: ", holderAtomBal) println("holder osmo bal: ", holderOsmoBal) break From b1040a1f7281ebe42462d5aa3bfcd1dd1a4b6d57 Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 18 Sep 2023 13:38:46 +0200 Subject: [PATCH 084/586] unit tests fix; enabling clock sender validation --- contracts/ibc-forwarder/src/contract.rs | 2 +- contracts/interchain-router/src/contract.rs | 24 +++++++------- .../src/suite_tests/suite.rs | 32 +++++++++---------- .../src/suite_tests/tests.rs | 19 +++++++---- contracts/interchain-splitter/src/contract.rs | 7 +++- contracts/interchain-splitter/src/error.rs | 3 ++ contracts/interchain-splitter/src/msg.rs | 27 +++------------- .../src/suite_test/tests.rs | 21 ++++++++++++ contracts/swap-covenant/src/contract.rs | 2 +- .../swap-covenant/src/suite_test/suite.rs | 18 +++++++---- contracts/swap-holder/src/contract.rs | 15 +++------ packages/covenant-utils/src/lib.rs | 11 +++++-- .../tests/interchaintest/tokenswap_test.go | 1 - 13 files changed, 100 insertions(+), 82 deletions(-) diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index 92f6aef7..59b69d3d 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -79,7 +79,7 @@ pub fn execute( /// attempts to advance the state machine. validates the caller to be the clock. fn try_tick(deps: ExecuteDeps, env: Env, info: MessageInfo) -> NeutronResult> { // Verify caller is the clock - // verify_clock(&info.sender, &CLOCK_ADDRESS.load(deps.storage)?)?; + verify_clock(&info.sender, &CLOCK_ADDRESS.load(deps.storage)?)?; let current_state = CONTRACT_STATE.load(deps.storage)?; match current_state { diff --git a/contracts/interchain-router/src/contract.rs b/contracts/interchain-router/src/contract.rs index d2b44eb2..ae688e30 100644 --- a/contracts/interchain-router/src/contract.rs +++ b/contracts/interchain-router/src/contract.rs @@ -3,26 +3,29 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ to_binary, Attribute, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, }; +use covenant_clock::helpers::verify_clock; use covenant_utils::DestinationConfig; use cw2::set_contract_version; -use neutron_sdk::bindings::msg::NeutronMsg; +use neutron_sdk::{bindings::{msg::NeutronMsg, query::NeutronQuery}, NeutronResult}; use crate::{ error::ContractError, msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, state::{CLOCK_ADDRESS, DESTINATION_CONFIG}, }; +type ExecuteDeps<'a> = DepsMut<'a, NeutronQuery>; +type QueryDeps<'a> = Deps<'a, NeutronQuery>; const CONTRACT_NAME: &str = "crates.io:covenant-interchain-router"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( - deps: DepsMut, + deps: ExecuteDeps, _env: Env, _info: MessageInfo, msg: InstantiateMsg, -) -> Result { +) -> NeutronResult> { deps.api.debug("WASMDEBUG: instantiate"); set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; @@ -47,19 +50,16 @@ pub fn instantiate( #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( - deps: DepsMut, + deps: ExecuteDeps, env: Env, info: MessageInfo, msg: ExecuteMsg, -) -> Result, ContractError> { - +) -> NeutronResult> { deps.api .debug(format!("WASMDEBUG: execute: received msg: {msg:?}").as_str()); // Verify caller is the clock - // if info.sender != CLOCK_ADDRESS.load(deps.storage)? { - // return Err(ContractError::Unauthorized {}); - // } + verify_clock(&info.sender, &CLOCK_ADDRESS.load(deps.storage)?)?; match msg { ExecuteMsg::Tick {} => try_route_balances(deps, env), @@ -67,7 +67,7 @@ pub fn execute( } /// method that attempts to transfer out all available balances to the receiver -fn try_route_balances(deps: DepsMut, env: Env) -> Result, ContractError> { +fn try_route_balances(deps: ExecuteDeps, env: Env) -> NeutronResult> { let destination_config: DestinationConfig = DESTINATION_CONFIG.load(deps.storage)?; @@ -101,7 +101,7 @@ fn try_route_balances(deps: DepsMut, env: Env) -> Result, C } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { +pub fn query(deps: QueryDeps, _env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::DestinationConfig {} => { Ok(to_binary(&DESTINATION_CONFIG.may_load(deps.storage)?)?) @@ -111,7 +111,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { +pub fn migrate(deps: ExecuteDeps, _env: Env, msg: MigrateMsg) -> NeutronResult> { deps.api.debug("WASMDEBUG: migrate"); match msg { diff --git a/contracts/interchain-router/src/suite_tests/suite.rs b/contracts/interchain-router/src/suite_tests/suite.rs index 2396bd33..cba7d970 100644 --- a/contracts/interchain-router/src/suite_tests/suite.rs +++ b/contracts/interchain-router/src/suite_tests/suite.rs @@ -1,31 +1,29 @@ -use crate::{ - contract::execute, - msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, -}; +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use cosmwasm_std::{ testing::{MockApi, MockStorage}, - Addr, Coin, CosmosMsg, Empty, GovMsg, Uint64, + Addr, Coin, Empty, GovMsg, Uint64, }; use covenant_utils::DestinationConfig; use cw_multi_test::{ App, AppResponse, BankKeeper, BasicAppBuilder, Contract, ContractWrapper, DistributionKeeper, - Executor, FailingModule, Ibc, IbcAcceptingModule, StakeKeeper, WasmKeeper, + Executor, FailingModule, IbcAcceptingModule, StakeKeeper, WasmKeeper, }; +use neutron_sdk::bindings::{msg::NeutronMsg, query::NeutronQuery}; pub const ADMIN: &str = "admin"; pub const DEFAULT_RECEIVER: &str = "receiver"; pub const CLOCK_ADDR: &str = "clock"; pub const DEFAULT_CHANNEL: &str = "channel-1"; -fn router_contract() -> Box> { - Box::new( - ContractWrapper::new( - crate::contract::execute, - crate::contract::instantiate, - crate::contract::query, - ) - .with_migrate(crate::contract::migrate), +fn router_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, ) + .with_migrate(crate::contract::migrate); + + Box::new(contract) } pub struct Suite { @@ -33,8 +31,8 @@ pub struct Suite { BankKeeper, MockApi, MockStorage, - FailingModule, - WasmKeeper, + FailingModule, + WasmKeeper, StakeKeeper, DistributionKeeper, IbcAcceptingModule, @@ -64,7 +62,7 @@ impl Default for SuiteBuilder { impl SuiteBuilder { pub fn build(self) -> Suite { - let mut app = BasicAppBuilder::default() + let mut app = BasicAppBuilder::::new_custom() .with_ibc(IbcAcceptingModule) .build(|_, _, _| ()); diff --git a/contracts/interchain-router/src/suite_tests/tests.rs b/contracts/interchain-router/src/suite_tests/tests.rs index 82a3d0c4..a34ceb20 100644 --- a/contracts/interchain-router/src/suite_tests/tests.rs +++ b/contracts/interchain-router/src/suite_tests/tests.rs @@ -1,11 +1,13 @@ +use std::marker::PhantomData; + use cosmwasm_std::{ coins, testing::{ - mock_dependencies, mock_dependencies_with_balance, mock_env, mock_info, MockQuerier, + mock_dependencies, mock_dependencies_with_balance, mock_env, mock_info, MockQuerier, MockStorage, MockApi, }, to_binary, Addr, Attribute, Coin, ContractInfo, ContractResult, CosmosMsg, DepsMut, Empty, Env, IbcMsg, IbcTimeout, Never, Querier, QuerierResult, QuerierWrapper, Response, SubMsg, - SystemError, SystemResult, Timestamp, Uint128, Uint64, WasmMsg, WasmQuery, + SystemError, SystemResult, Timestamp, Uint128, Uint64, WasmMsg, WasmQuery, OwnedDeps, }; use covenant_utils::DestinationConfig; @@ -28,7 +30,7 @@ fn test_instantiate_and_query_all() { assert_eq!( DestinationConfig { destination_chain_channel_id: DEFAULT_CHANNEL.to_string(), - destination_receiver_addr: Addr::unchecked(DEFAULT_RECEIVER.to_string()), + destination_receiver_addr: DEFAULT_RECEIVER.to_string(), ibc_transfer_timeout: Uint64::new(10), }, config @@ -43,7 +45,7 @@ fn test_migrate_config() { clock_addr: Some("working_clock".to_string()), destination_config: Some(DestinationConfig { destination_chain_channel_id: "new_channel".to_string(), - destination_receiver_addr: Addr::unchecked("new_receiver".to_string()), + destination_receiver_addr: "new_receiver".to_string(), ibc_transfer_timeout: Uint64::new(100), }), }; @@ -57,7 +59,7 @@ fn test_migrate_config() { assert_eq!( DestinationConfig { destination_chain_channel_id: "new_channel".to_string(), - destination_receiver_addr: Addr::unchecked("new_receiver".to_string()), + destination_receiver_addr: "new_receiver".to_string(), ibc_transfer_timeout: Uint64::new(100), }, config @@ -76,7 +78,12 @@ fn test_tick() { let querier: MockQuerier = MockQuerier::new(&[(&"cosmos2contract".to_string(), &coins(100, "usdc"))]); - let mut deps = mock_dependencies(); + let mut deps = OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::new(&[]), + custom_query_type: PhantomData, + }; // set the custom querier on our mock deps deps.querier = querier; diff --git a/contracts/interchain-splitter/src/contract.rs b/contracts/interchain-splitter/src/contract.rs index 8fe551c2..181acc5a 100644 --- a/contracts/interchain-splitter/src/contract.rs +++ b/contracts/interchain-splitter/src/contract.rs @@ -55,11 +55,16 @@ pub fn instantiate( pub fn execute( deps: DepsMut, env: Env, - _info: MessageInfo, + info: MessageInfo, msg: ExecuteMsg, ) -> Result { deps.api .debug(format!("WASMDEBUG: execute: received msg: {msg:?}").as_str()); + // Verify caller is the clock + if info.sender != CLOCK_ADDRESS.load(deps.storage)? { + return Err(ContractError::Unauthorized {}); + } + match msg { ExecuteMsg::Tick {} => try_distribute(deps, env), } diff --git a/contracts/interchain-splitter/src/error.rs b/contracts/interchain-splitter/src/error.rs index 1edc85fc..0702eea7 100644 --- a/contracts/interchain-splitter/src/error.rs +++ b/contracts/interchain-splitter/src/error.rs @@ -8,4 +8,7 @@ pub enum ContractError { #[error("misconfigured split")] SplitMisconfig {}, + + #[error("unauthorized caller")] + Unauthorized {}, } diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs index 14d07228..0158da11 100644 --- a/contracts/interchain-splitter/src/msg.rs +++ b/contracts/interchain-splitter/src/msg.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{ - Addr, Attribute, BankMsg, Binary, Coin, CosmosMsg, IbcTimeout, Uint128, + Addr, Attribute, BankMsg, Binary, Coin, CosmosMsg, Uint128, }; use covenant_macros::{clocked, covenant_clock_address, covenant_deposit_address}; @@ -26,8 +26,11 @@ pub struct PresetInterchainSplitterFields { pub fallback_split: Option, /// contract label pub label: String, + /// code id for the interchain splitter contract pub code_id: u64, + /// receiver address of party A pub party_a_addr: String, + /// receiver address of party B pub party_b_addr: String, } @@ -87,28 +90,6 @@ impl PresetInterchainSplitterFields { #[cw_serde] pub enum ExecuteMsg {} -// for every receiver we need a few things: -#[cw_serde] -pub struct InterchainReceiver { - // 1. remote chain channel id - pub channel_id: String, - // 2. receiver address - pub address: String, - // 3. timeout info - pub ibc_timeout: IbcTimeout, -} - -#[cw_serde] -pub struct NativeReceiver { - pub address: String, -} - -#[cw_serde] -pub enum ReceiverType { - Interchain(InterchainReceiver), - Native(NativeReceiver), -} - #[cw_serde] pub enum SplitType { Custom(SplitConfig), diff --git a/contracts/interchain-splitter/src/suite_test/tests.rs b/contracts/interchain-splitter/src/suite_test/tests.rs index 8b69c078..ed08b714 100644 --- a/contracts/interchain-splitter/src/suite_test/tests.rs +++ b/contracts/interchain-splitter/src/suite_test/tests.rs @@ -91,14 +91,24 @@ fn test_distribute_token_swap() { DENOM_A.to_string(), SplitType::Custom(SplitConfig { receivers: vec![ +<<<<<<< HEAD Receiver { addr: PARTY_B_ADDR.to_string(), share: Uint128::new(100) }], +======= + Receiver { addr: PARTY_B_ADDR.to_string(), share: Uint128::new(100) }, + ], +>>>>>>> c306039 (unit tests fix; enabling clock sender validation) }), ), ( DENOM_B.to_string(), SplitType::Custom(SplitConfig { receivers: vec![ +<<<<<<< HEAD Receiver { addr: PARTY_A_ADDR.to_string(), share: Uint128::new(100) }], +======= + Receiver { addr: PARTY_A_ADDR.to_string(), share: Uint128::new(100) }, + ], +>>>>>>> c306039 (unit tests fix; enabling clock sender validation) }), ), ]) @@ -174,7 +184,13 @@ fn test_migrate_config() { let new_splits = vec![( "new_denom".to_string(), SplitType::Custom(SplitConfig { +<<<<<<< HEAD receivers: vec![Receiver { addr: "new_receiver".to_string(), share: Uint128::new(100) }], +======= + receivers: vec![ + Receiver { addr: "new_receiver".to_string(), share: Uint128::new(100) }, + ], +>>>>>>> c306039 (unit tests fix; enabling clock sender validation) }), )]; @@ -195,7 +211,12 @@ fn test_migrate_config() { "new_denom".to_string(), SplitConfig { receivers: vec![ +<<<<<<< HEAD Receiver { addr: "new_receiver".to_string(), share: Uint128::new(100) }], +======= + Receiver { addr: "new_receiver".to_string(), share: Uint128::new(100) }, + ], +>>>>>>> c306039 (unit tests fix; enabling clock sender validation) }, )], splits diff --git a/contracts/swap-covenant/src/contract.rs b/contracts/swap-covenant/src/contract.rs index e7c45ec0..36245c34 100644 --- a/contracts/swap-covenant/src/contract.rs +++ b/contracts/swap-covenant/src/contract.rs @@ -401,7 +401,7 @@ pub fn handle_party_a_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) - /// party B ibc forwarder reply means that we instantiated all the contracts. /// we store the party B ibc forwarder address and whitelist the contracts on our clock. -pub fn handle_party_b_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) -> Result { +pub fn handle_party_b_ibc_forwarder_reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { deps.api.debug("WASMDEBUG: party B ibc forwader reply"); let parsed_data = parse_reply_instantiate_data(msg); diff --git a/contracts/swap-covenant/src/suite_test/suite.rs b/contracts/swap-covenant/src/suite_test/suite.rs index 5c9256ba..f8908ff2 100644 --- a/contracts/swap-covenant/src/suite_test/suite.rs +++ b/contracts/swap-covenant/src/suite_test/suite.rs @@ -1,7 +1,7 @@ -use cosmwasm_std::{Addr, Empty}; -use cw_multi_test::{App, Contract, ContractWrapper, Executor}; +use cosmwasm_std::{Addr, Empty, Uint128, Uint64}; +use cw_multi_test::{App, Contract, ContractWrapper}; -use crate::msg::{InstantiateMsg, QueryMsg}; +use crate::msg::{InstantiateMsg, PresetIbcFee, Timeouts}; pub const CREATOR_ADDR: &str = "admin"; pub const TODO: &str = "replace"; @@ -31,9 +31,15 @@ impl Default for SuiteBuilder { fn default() -> Self { Self { instantiate: InstantiateMsg { - label: todo!(), - preset_ibc_fee: todo!(), - timeouts: todo!(), + label: "swap-covenant".to_string(), + preset_ibc_fee: PresetIbcFee { + ack_fee: Uint128::new(1000), + timeout_fee: Uint128::new(1000), + }, + timeouts: Timeouts { + ica_timeout: Uint64::new(50), + ibc_transfer_timeout: Uint64::new(50), + }, contract_codes: todo!(), clock_tick_max_gas: todo!(), lockup_config: todo!(), diff --git a/contracts/swap-holder/src/contract.rs b/contracts/swap-holder/src/contract.rs index d4f8d396..8f2bb5ce 100644 --- a/contracts/swap-holder/src/contract.rs +++ b/contracts/swap-holder/src/contract.rs @@ -33,8 +33,7 @@ pub fn instantiate( let next_contract = deps.api.addr_validate(&msg.next_contract)?; let clock_addr = deps.api.addr_validate(&msg.clock_address)?; - // TODO: debug what goes wrong in validation here - // msg.lockup_config.validate(env.block)?; + msg.lockup_config.validate(env.block)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; @@ -64,10 +63,9 @@ pub fn execute( /// attempts to advance the state machine. performs `info.sender` validation fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> Result { // Verify caller is the clock - // let clock_addr = CLOCK_ADDRESS.load(deps.storage)?; - // if clock_addr != info.sender { - // return Err(ContractError::Unauthorized {}); - // } + if info.sender != CLOCK_ADDRESS.load(deps.storage)? { + return Err(ContractError::Unauthorized {}); + } let current_state = CONTRACT_STATE.load(deps.storage)?; match current_state { @@ -113,11 +111,6 @@ fn try_forward(deps: DepsMut, env: Env) -> Result { } } - if party_a_coin.amount == Uint128::zero() { - let bals: Vec = balances.into_iter().map(|c| c.to_string()).collect(); - return Ok(Response::default().add_attribute("balance_a", bals.join(" "))) - } - // if either of the coin amounts did not get updated to non-zero, // we are not ready for the swap yet if party_a_coin.amount.is_zero() || party_b_coin.amount.is_zero() { diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index fddc7248..46902731 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -221,9 +221,14 @@ impl LockupConfig { /// otherwise, returns true if the current block is past the stored info. pub fn is_expired(self, block_info: BlockInfo) -> bool { match self { - LockupConfig::None => false, // or.. true? should not be called tho - LockupConfig::Block(h) => h > block_info.height, - LockupConfig::Time(t) => t.nanos() > block_info.time.nanos(), + // no expiration date + LockupConfig::None => false, + // if stored expiration block height is less than or equal to the current block, + // expired + LockupConfig::Block(h) => h <= block_info.height, + // if stored expiration timestamp is more than or equal to the current timestamp, + // expired + LockupConfig::Time(t) => t.nanos() <= block_info.time.nanos(), } } } diff --git a/swap-covenant/tests/interchaintest/tokenswap_test.go b/swap-covenant/tests/interchaintest/tokenswap_test.go index c82ea82a..d7455052 100644 --- a/swap-covenant/tests/interchaintest/tokenswap_test.go +++ b/swap-covenant/tests/interchaintest/tokenswap_test.go @@ -818,7 +818,6 @@ func TestTokenSwap(t *testing.T) { if partyARouterOsmoBal != 0 && partyBRouterAtomBal != 0 { println("partyARouter osmo bal: ", partyARouterOsmoBal) println("partyBRouterAtomBal: ", partyBRouterAtomBal) - break } } From d061546f3427825e07ff3e05149e462db1239837 Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 18 Sep 2023 17:37:50 +0200 Subject: [PATCH 085/586] interchaintest fix --- contracts/ibc-forwarder/src/contract.rs | 2 ++ .../tests/interchaintest/tokenswap_test.go | 24 +++++++++---------- swap-covenant/tests/interchaintest/types.go | 7 +++--- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index 59b69d3d..3c9e0aea 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -44,6 +44,8 @@ pub fn instantiate( set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; let next_contract = deps.api.addr_validate(&msg.next_contract)?; + let clock_addr = deps.api.addr_validate(&msg.clock_address)?; + CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; TRANSFER_AMOUNT.save(deps.storage, &msg.amount)?; let remote_chain_info = RemoteChainInfo { diff --git a/swap-covenant/tests/interchaintest/tokenswap_test.go b/swap-covenant/tests/interchaintest/tokenswap_test.go index d7455052..951d0393 100644 --- a/swap-covenant/tests/interchaintest/tokenswap_test.go +++ b/swap-covenant/tests/interchaintest/tokenswap_test.go @@ -391,10 +391,11 @@ func TestTokenSwap(t *testing.T) { PartyBAmount: strconv.FormatUint(osmoContributionAmount, 10), } - timestamp := Timestamp("1981539923") + // timestamp := Timestamp("1981539923") + block := Block(500) lockupConfig := LockupConfig{ - Time: ×tamp, + BlockHeight: &block, } presetIbcFee := PresetIbcFee{ AckFee: "10000", @@ -478,7 +479,6 @@ func TestTokenSwap(t *testing.T) { require.NoError(t, err, "Failed to marshall CovenantInstantiateMsg") instantiateMsg := string(str) - println("instantiation message: ", instantiateMsg) cmd := []string{"neutrond", "tx", "wasm", "instantiate", covenantCodeIdStr, instantiateMsg, "--label", "swap-covenant", @@ -495,7 +495,6 @@ func TestTokenSwap(t *testing.T) { _, _, err = neutron.Exec(ctx, cmd, nil) require.NoError(t, err) - require.NoError(t, testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis)) queryCmd := []string{"neutrond", "query", "wasm", @@ -760,7 +759,6 @@ func TestTokenSwap(t *testing.T) { t.Run("tick until forwarders forward the funds to holder", func(t *testing.T) { for { - tickClock() holderOsmoBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronOsmoIbcDenom) require.NoError(t, err, "failed to query holder osmo bal") holderAtomBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronAtomIbcDenom) @@ -780,18 +778,18 @@ func TestTokenSwap(t *testing.T) { "failed to query forwarder A state") holderState := response.Data - if holderAtomBal != 0 && holderOsmoBal != 0 || holderState == "completed" { + if holderAtomBal != 0 && holderOsmoBal != 0 || holderState == "complete" { println("holder atom bal: ", holderAtomBal) println("holder osmo bal: ", holderOsmoBal) break + } else { + tickClock() } } }) t.Run("tick until holder sends the funds to splitter", func(t *testing.T) { for { - tickClock() - splitterOsmoBal, err := cosmosNeutron.GetBalance(ctx, splitterAddress, neutronOsmoIbcDenom) require.NoError(t, err, "failed to query splitterOsmoBal") splitterAtomBal, err := cosmosNeutron.GetBalance(ctx, splitterAddress, neutronAtomIbcDenom) @@ -801,14 +799,14 @@ func TestTokenSwap(t *testing.T) { println("splitterOsmoBal: ", splitterOsmoBal) println("splitterAtomBal: ", splitterAtomBal) break + } else { + tickClock() } } }) t.Run("tick until splitter sends the funds to routers", func(t *testing.T) { for { - tickClock() - partyARouterOsmoBal, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronOsmoIbcDenom) require.NoError(t, err, "failed to query partyARouterOsmoBal") @@ -819,14 +817,14 @@ func TestTokenSwap(t *testing.T) { println("partyARouter osmo bal: ", partyARouterOsmoBal) println("partyBRouterAtomBal: ", partyBRouterAtomBal) break + } else { + tickClock() } } }) t.Run("tick until routers route the funds to final receivers", func(t *testing.T) { for { - tickClock() - osmoBal, err := cosmosOsmosis.GetBalance(ctx, osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), osmoNeutronAtomIbcDenom) require.NoError(t, err, "failed to query osmoBal") gaiaBal, err := cosmosAtom.GetBalance(ctx, gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), gaiaNeutronOsmoIbcDenom) @@ -836,6 +834,8 @@ func TestTokenSwap(t *testing.T) { println("gaia user osmo bal: ", gaiaBal) println("osmo user atom bal: ", osmoBal) break + } else { + tickClock() } } }) diff --git a/swap-covenant/tests/interchaintest/types.go b/swap-covenant/tests/interchaintest/types.go index b99b9e6d..5e0327df 100644 --- a/swap-covenant/tests/interchaintest/types.go +++ b/swap-covenant/tests/interchaintest/types.go @@ -77,11 +77,12 @@ type PresetSwapHolderFields struct { } type Timestamp string +type Block uint64 type LockupConfig struct { - None bool `json:"none,omitempty"` - Block *uint64 `json:"block,omitempty"` - Time *Timestamp `json:"time,omitempty"` + None bool `json:"none,omitempty"` + BlockHeight *Block `json:"block,omitempty"` + Time *Timestamp `json:"time,omitempty"` } type CovenantPartiesConfig struct { From 41f51716a97b6a8fc918c0c8dca02929ca1cfabb Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 18 Sep 2023 17:39:45 +0200 Subject: [PATCH 086/586] clippy --- contracts/ibc-forwarder/src/msg.rs | 2 +- contracts/interchain-router/src/contract.rs | 3 +-- .../interchain-router/src/suite_tests/tests.rs | 16 +++++++--------- contracts/interchain-splitter/src/msg.rs | 4 ++-- contracts/native-splitter/src/contract.rs | 2 +- contracts/native-splitter/src/msg.rs | 2 +- contracts/swap-covenant/src/suite_test/suite.rs | 4 ++-- contracts/swap-holder/src/contract.rs | 2 +- contracts/swap-holder/src/suite_tests/tests.rs | 6 +++--- 9 files changed, 19 insertions(+), 22 deletions(-) diff --git a/contracts/ibc-forwarder/src/msg.rs b/contracts/ibc-forwarder/src/msg.rs index f9c75dd7..e0da20ce 100644 --- a/contracts/ibc-forwarder/src/msg.rs +++ b/contracts/ibc-forwarder/src/msg.rs @@ -82,7 +82,7 @@ impl InstantiateMsg { ), Attribute::new("remote_chain_channel_id", &self.remote_chain_channel_id), Attribute::new("remote_chain_denom", &self.denom), - Attribute::new("remote_chain_amount", &self.amount.to_string()), + Attribute::new("remote_chain_amount", self.amount.to_string()), Attribute::new( "ibc_transfer_timeout", self.ibc_transfer_timeout.to_string(), diff --git a/contracts/interchain-router/src/contract.rs b/contracts/interchain-router/src/contract.rs index ae688e30..0f397f83 100644 --- a/contracts/interchain-router/src/contract.rs +++ b/contracts/interchain-router/src/contract.rs @@ -9,7 +9,6 @@ use cw2::set_contract_version; use neutron_sdk::{bindings::{msg::NeutronMsg, query::NeutronQuery}, NeutronResult}; use crate::{ - error::ContractError, msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, state::{CLOCK_ADDRESS, DESTINATION_CONFIG}, }; @@ -90,7 +89,7 @@ fn try_route_balances(deps: ExecuteDeps, env: Env) -> NeutronResult = - MockQuerier::new(&[(&"cosmos2contract".to_string(), &coins(100, "usdc"))]); + MockQuerier::new(&[("cosmos2contract", &coins(100, "usdc"))]); let mut deps = OwnedDeps { storage: MockStorage::default(), @@ -93,7 +91,7 @@ fn test_tick() { instantiate( deps.as_mut(), mock_env(), - info.clone(), + info, SuiteBuilder::default().instantiate, ) .unwrap(); @@ -101,7 +99,7 @@ fn test_tick() { let resp = execute( deps.as_mut(), mock_env(), - mock_info(CLOCK_ADDR, &vec![]), + mock_info(CLOCK_ADDR, &[]), crate::msg::ExecuteMsg::Tick {}, ) .unwrap(); diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs index 0158da11..aa6d95ed 100644 --- a/contracts/interchain-splitter/src/msg.rs +++ b/contracts/interchain-splitter/src/msg.rs @@ -70,9 +70,9 @@ impl PresetInterchainSplitterFields { Some(split_type) => match split_type { SplitType::Custom(config) => Some(config.remap_receivers_to_routers( self.party_a_addr.to_string(), - party_a_router.to_string(), + party_a_router, self.party_b_addr.to_string(), - party_b_router.to_string(), + party_b_router, )?) }, None => None, diff --git a/contracts/native-splitter/src/contract.rs b/contracts/native-splitter/src/contract.rs index cc19429b..e6ef095d 100644 --- a/contracts/native-splitter/src/contract.rs +++ b/contracts/native-splitter/src/contract.rs @@ -363,7 +363,7 @@ fn sudo_response(deps: DepsMut, request: RequestPacket, data: Binary) -> StdResu item_types.push(item_type.to_string()); match item_type { "/cosmos.bank.v1beta1.MsgMultiSend" => { - let out: MsgMultiSendResponse = decode_message_response(&item.data)?; + let _out: MsgMultiSendResponse = decode_message_response(&item.data)?; // TODO: look into if this successful decoding is enough to assume multi // send was successful CONTRACT_STATE.save(deps.storage, &ContractState::Completed)?; diff --git a/contracts/native-splitter/src/msg.rs b/contracts/native-splitter/src/msg.rs index 69a893f9..9195deb3 100644 --- a/contracts/native-splitter/src/msg.rs +++ b/contracts/native-splitter/src/msg.rs @@ -1,7 +1,7 @@ use std::fmt; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Attribute, Fraction, StdError, Uint128, Uint64}; +use cosmwasm_std::{Addr, Attribute, StdError, Uint128, Uint64}; use covenant_macros::{ clocked, covenant_clock_address, covenant_deposit_address, covenant_ica_address, covenant_remote_chain, diff --git a/contracts/swap-covenant/src/suite_test/suite.rs b/contracts/swap-covenant/src/suite_test/suite.rs index f8908ff2..9528837b 100644 --- a/contracts/swap-covenant/src/suite_test/suite.rs +++ b/contracts/swap-covenant/src/suite_test/suite.rs @@ -54,8 +54,8 @@ impl Default for SuiteBuilder { } impl SuiteBuilder { - pub fn build(mut self) -> Suite { - let mut app = App::default(); + pub fn build(self) -> Suite { + let app = App::default(); Suite { app, covenant_address: todo!(), diff --git a/contracts/swap-holder/src/contract.rs b/contracts/swap-holder/src/contract.rs index 8f2bb5ce..2f4d9ea3 100644 --- a/contracts/swap-holder/src/contract.rs +++ b/contracts/swap-holder/src/contract.rs @@ -103,7 +103,7 @@ fn try_forward(deps: DepsMut, env: Env) -> Result { // query holder balances let balances = deps.querier.query_all_balances(env.contract.address)?; // find the existing balances of covenant coins - for coin in balances.clone() { + for coin in balances { if coin.denom == party_a_coin.denom && coin.amount >= covenant_terms.party_a_amount { party_a_coin.amount = coin.amount; } else if coin.denom == party_b_coin.denom && coin.amount >= covenant_terms.party_b_amount { diff --git a/contracts/swap-holder/src/suite_tests/tests.rs b/contracts/swap-holder/src/suite_tests/tests.rs index 63cf6240..36b5e763 100644 --- a/contracts/swap-holder/src/suite_tests/tests.rs +++ b/contracts/swap-holder/src/suite_tests/tests.rs @@ -225,7 +225,7 @@ fn test_refund_party_a() { amount: Uint128::new(500), }; - suite.fund_coin(coin_a.clone()); + suite.fund_coin(coin_a); suite.pass_blocks(10000); // first tick acknowledges the expiration @@ -257,7 +257,7 @@ fn test_refund_party_b() { denom: DENOM_B.to_string(), amount: Uint128::new(500), }; - suite.fund_coin(coin_b.clone()); + suite.fund_coin(coin_b); suite.pass_blocks(10000); @@ -290,7 +290,7 @@ fn test_refund_both_parties() { denom: DENOM_A.to_string(), amount: Uint128::new(300), }; - suite.fund_coin(coin_a.clone()); + suite.fund_coin(coin_a); let coin_b = Coin { denom: DENOM_B.to_string(), amount: Uint128::new(10), From f85a4d5556f16dd0f4188cedd5e03a46baeb131d Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 18 Sep 2023 17:44:30 +0200 Subject: [PATCH 087/586] fmt --- contracts/interchain-router/src/contract.rs | 23 ++- contracts/interchain-router/src/msg.rs | 4 +- contracts/interchain-router/src/state.rs | 1 - .../src/suite_tests/tests.rs | 11 +- contracts/interchain-splitter/src/contract.rs | 7 +- contracts/interchain-splitter/src/msg.rs | 25 +-- .../src/suite_test/suite.rs | 20 ++- .../src/suite_test/tests.rs | 63 +++---- contracts/swap-covenant/src/contract.rs | 170 ++++++++++++------ contracts/swap-covenant/src/msg.rs | 6 +- contracts/swap-covenant/src/state.rs | 19 +- .../swap-covenant/src/suite_test/suite.rs | 4 +- .../swap-covenant/src/suite_test/tests.rs | 1 + .../src/suite_test/unit_tests.rs | 1 + contracts/swap-holder/src/contract.rs | 5 +- contracts/swap-holder/src/msg.rs | 2 +- .../swap-holder/src/suite_tests/suite.rs | 4 +- .../swap-holder/src/suite_tests/tests.rs | 4 +- packages/covenant-utils/src/lib.rs | 27 +-- 19 files changed, 230 insertions(+), 167 deletions(-) diff --git a/contracts/interchain-router/src/contract.rs b/contracts/interchain-router/src/contract.rs index 0f397f83..5d851da0 100644 --- a/contracts/interchain-router/src/contract.rs +++ b/contracts/interchain-router/src/contract.rs @@ -6,7 +6,10 @@ use cosmwasm_std::{ use covenant_clock::helpers::verify_clock; use covenant_utils::DestinationConfig; use cw2::set_contract_version; -use neutron_sdk::{bindings::{msg::NeutronMsg, query::NeutronQuery}, NeutronResult}; +use neutron_sdk::{ + bindings::{msg::NeutronMsg, query::NeutronQuery}, + NeutronResult, +}; use crate::{ msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, @@ -28,7 +31,7 @@ pub fn instantiate( deps.api.debug("WASMDEBUG: instantiate"); set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - let clock_addr = deps.api.addr_validate(&msg.clock_address)?; + let clock_addr = deps.api.addr_validate(&msg.clock_address)?; let destination_config = DestinationConfig { destination_chain_channel_id: msg.destination_chain_channel_id.to_string(), @@ -43,8 +46,7 @@ pub fn instantiate( .add_attribute("method", "interchain_router_instantiate") .add_attribute("clock_address", clock_addr) .add_attribute("destination_receiver_addr", msg.destination_receiver_addr) - .add_attributes(destination_config.get_response_attributes()) - ) + .add_attributes(destination_config.get_response_attributes())) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -67,11 +69,12 @@ pub fn execute( /// method that attempts to transfer out all available balances to the receiver fn try_route_balances(deps: ExecuteDeps, env: Env) -> NeutronResult> { - let destination_config: DestinationConfig = DESTINATION_CONFIG.load(deps.storage)?; // first we query all balances of the router - let balances = deps.querier.query_all_balances(env.clone().contract.address)?; + let balances = deps + .querier + .query_all_balances(env.clone().contract.address)?; // if there are no balances, we return early; // otherwise build up the response attributes @@ -92,7 +95,7 @@ fn try_route_balances(deps: ExecuteDeps, env: Env) -> NeutronResult StdResult { } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(deps: ExecuteDeps, _env: Env, msg: MigrateMsg) -> NeutronResult> { +pub fn migrate( + deps: ExecuteDeps, + _env: Env, + msg: MigrateMsg, +) -> NeutronResult> { deps.api.debug("WASMDEBUG: migrate"); match msg { diff --git a/contracts/interchain-router/src/msg.rs b/contracts/interchain-router/src/msg.rs index ea49d115..71858d05 100644 --- a/contracts/interchain-router/src/msg.rs +++ b/contracts/interchain-router/src/msg.rs @@ -1,7 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{ - Addr, Binary, Uint64, -}; +use cosmwasm_std::{Addr, Binary, Uint64}; use covenant_macros::{clocked, covenant_clock_address}; use covenant_utils::DestinationConfig; diff --git a/contracts/interchain-router/src/state.rs b/contracts/interchain-router/src/state.rs index c8de5aba..734678d9 100644 --- a/contracts/interchain-router/src/state.rs +++ b/contracts/interchain-router/src/state.rs @@ -2,6 +2,5 @@ use cosmwasm_std::Addr; use covenant_utils::DestinationConfig; use cw_storage_plus::Item; - pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); pub const DESTINATION_CONFIG: Item = Item::new("destination_config"); diff --git a/contracts/interchain-router/src/suite_tests/tests.rs b/contracts/interchain-router/src/suite_tests/tests.rs index 9ba57ef1..ecb229b5 100644 --- a/contracts/interchain-router/src/suite_tests/tests.rs +++ b/contracts/interchain-router/src/suite_tests/tests.rs @@ -2,16 +2,14 @@ use std::marker::PhantomData; use cosmwasm_std::{ coins, - testing::{ - mock_env, mock_info, MockQuerier, MockStorage, MockApi, - }, Attribute, Coin, CosmosMsg, Empty, - IbcMsg, IbcTimeout, SubMsg, Uint64, OwnedDeps, + testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + Attribute, Coin, CosmosMsg, Empty, IbcMsg, IbcTimeout, OwnedDeps, SubMsg, Uint64, }; use covenant_utils::DestinationConfig; use crate::{ contract::{execute, instantiate}, - msg::{MigrateMsg}, + msg::MigrateMsg, suite_tests::suite::{DEFAULT_CHANNEL, DEFAULT_RECEIVER}, }; @@ -73,8 +71,7 @@ fn test_unauthorized_tick() { #[test] fn test_tick() { - let querier: MockQuerier = - MockQuerier::new(&[("cosmos2contract", &coins(100, "usdc"))]); + let querier: MockQuerier = MockQuerier::new(&[("cosmos2contract", &coins(100, "usdc"))]); let mut deps = OwnedDeps { storage: MockStorage::default(), diff --git a/contracts/interchain-splitter/src/contract.rs b/contracts/interchain-splitter/src/contract.rs index 181acc5a..9438c709 100644 --- a/contracts/interchain-splitter/src/contract.rs +++ b/contracts/interchain-splitter/src/contract.rs @@ -104,8 +104,7 @@ pub fn try_distribute(deps: DepsMut, env: Env) -> Result StdResult resp = resp.add_attributes(vec![split.get_response_attribute(denom)]); } - Err(_) => return Err(StdError::generic_err("invalid split".to_string())) + Err(_) => { + return Err(StdError::generic_err("invalid split".to_string())) + } }, } } diff --git a/contracts/interchain-splitter/src/msg.rs b/contracts/interchain-splitter/src/msg.rs index aa6d95ed..fe8d2558 100644 --- a/contracts/interchain-splitter/src/msg.rs +++ b/contracts/interchain-splitter/src/msg.rs @@ -1,7 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{ - Addr, Attribute, BankMsg, Binary, Coin, CosmosMsg, Uint128, -}; +use cosmwasm_std::{Addr, Attribute, BankMsg, Binary, Coin, CosmosMsg, Uint128}; use covenant_macros::{clocked, covenant_clock_address, covenant_deposit_address}; use crate::error::ContractError; @@ -62,7 +60,7 @@ impl PresetInterchainSplitterFields { party_b_router.to_string(), )?; remapped_splits.push((denom_split.denom.to_string(), remapped_split)); - }, + } } } @@ -73,7 +71,7 @@ impl PresetInterchainSplitterFields { party_a_router, self.party_b_addr.to_string(), party_b_router, - )?) + )?), }, None => None, }; @@ -116,8 +114,17 @@ pub struct Receiver { } impl SplitConfig { - pub fn remap_receivers_to_routers(&self, receiver_a: String, router_a: String, receiver_b: String, router_b: String) -> Result { - let receivers = self.receivers.clone().into_iter() + pub fn remap_receivers_to_routers( + &self, + receiver_a: String, + router_a: String, + receiver_b: String, + router_b: String, + ) -> Result { + let receivers = self + .receivers + .clone() + .into_iter() .map(|receiver| { if receiver.addr == receiver_a { Receiver { @@ -135,9 +142,7 @@ impl SplitConfig { }) .collect(); - Ok(SplitType::Custom(SplitConfig { - receivers, - })) + Ok(SplitType::Custom(SplitConfig { receivers })) } pub fn validate(self) -> Result { diff --git a/contracts/interchain-splitter/src/suite_test/suite.rs b/contracts/interchain-splitter/src/suite_test/suite.rs index cbb1e643..8f67bf66 100644 --- a/contracts/interchain-splitter/src/suite_test/suite.rs +++ b/contracts/interchain-splitter/src/suite_test/suite.rs @@ -2,8 +2,7 @@ use cosmwasm_std::{Addr, Coin, Uint128}; use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; use crate::msg::{ - ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, SplitConfig, - SplitType, Receiver, + ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, Receiver, SplitConfig, SplitType, }; use super::splitter_contract; @@ -22,17 +21,24 @@ pub const CLOCK_ADDR: &str = "clock_addr"; pub fn get_equal_split_config() -> SplitConfig { SplitConfig { receivers: vec![ - Receiver { addr: PARTY_A_ADDR.to_string(), share: Uint128::new(50) }, - Receiver { addr: PARTY_B_ADDR.to_string(), share: Uint128::new(50) }, + Receiver { + addr: PARTY_A_ADDR.to_string(), + share: Uint128::new(50), + }, + Receiver { + addr: PARTY_B_ADDR.to_string(), + share: Uint128::new(50), + }, ], } } pub fn get_fallback_split_config() -> SplitConfig { SplitConfig { - receivers: vec![ - Receiver { addr: "save_the_cats".to_string(), share: Uint128::new(100) }, - ], + receivers: vec![Receiver { + addr: "save_the_cats".to_string(), + share: Uint128::new(100), + }], } } diff --git a/contracts/interchain-splitter/src/suite_test/tests.rs b/contracts/interchain-splitter/src/suite_test/tests.rs index ed08b714..d91e6854 100644 --- a/contracts/interchain-splitter/src/suite_test/tests.rs +++ b/contracts/interchain-splitter/src/suite_test/tests.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{Coin, Uint128}; use crate::{ - msg::{MigrateMsg, SplitConfig, SplitType, Receiver}, + msg::{MigrateMsg, Receiver, SplitConfig, SplitType}, suite_test::suite::{ get_equal_split_config, get_fallback_split_config, ALT_DENOM, CLOCK_ADDR, DENOM_B, }, @@ -40,8 +40,14 @@ fn test_instantiate_split_misconfig() { DENOM_A.to_string(), SplitType::Custom(SplitConfig { receivers: vec![ - Receiver { addr: PARTY_A_ADDR.to_string(), share: Uint128::new(50) }, - Receiver { addr: PARTY_B_ADDR.to_string(), share: Uint128::new(50) }, + Receiver { + addr: PARTY_A_ADDR.to_string(), + share: Uint128::new(50), + }, + Receiver { + addr: PARTY_B_ADDR.to_string(), + share: Uint128::new(50), + }, ], }), )]) @@ -90,25 +96,19 @@ fn test_distribute_token_swap() { ( DENOM_A.to_string(), SplitType::Custom(SplitConfig { - receivers: vec![ -<<<<<<< HEAD - Receiver { addr: PARTY_B_ADDR.to_string(), share: Uint128::new(100) }], -======= - Receiver { addr: PARTY_B_ADDR.to_string(), share: Uint128::new(100) }, - ], ->>>>>>> c306039 (unit tests fix; enabling clock sender validation) + receivers: vec![Receiver { + addr: PARTY_B_ADDR.to_string(), + share: Uint128::new(100), + }], }), ), ( DENOM_B.to_string(), SplitType::Custom(SplitConfig { - receivers: vec![ -<<<<<<< HEAD - Receiver { addr: PARTY_A_ADDR.to_string(), share: Uint128::new(100) }], -======= - Receiver { addr: PARTY_A_ADDR.to_string(), share: Uint128::new(100) }, - ], ->>>>>>> c306039 (unit tests fix; enabling clock sender validation) + receivers: vec![Receiver { + addr: PARTY_A_ADDR.to_string(), + share: Uint128::new(100), + }], }), ), ]) @@ -177,20 +177,18 @@ fn test_migrate_config() { let new_clock = "new_clock".to_string(); let new_fallback_split = SplitConfig { - receivers: vec![ - Receiver { addr: "fallback_new".to_string(), share: Uint128::new(100) }, - ], + receivers: vec![Receiver { + addr: "fallback_new".to_string(), + share: Uint128::new(100), + }], }; let new_splits = vec![( "new_denom".to_string(), SplitType::Custom(SplitConfig { -<<<<<<< HEAD - receivers: vec![Receiver { addr: "new_receiver".to_string(), share: Uint128::new(100) }], -======= - receivers: vec![ - Receiver { addr: "new_receiver".to_string(), share: Uint128::new(100) }, - ], ->>>>>>> c306039 (unit tests fix; enabling clock sender validation) + receivers: vec![Receiver { + addr: "new_receiver".to_string(), + share: Uint128::new(100), + }], }), )]; @@ -210,13 +208,10 @@ fn test_migrate_config() { vec![( "new_denom".to_string(), SplitConfig { - receivers: vec![ -<<<<<<< HEAD - Receiver { addr: "new_receiver".to_string(), share: Uint128::new(100) }], -======= - Receiver { addr: "new_receiver".to_string(), share: Uint128::new(100) }, - ], ->>>>>>> c306039 (unit tests fix; enabling clock sender validation) + receivers: vec![Receiver { + addr: "new_receiver".to_string(), + share: Uint128::new(100) + },], }, )], splits diff --git a/contracts/swap-covenant/src/contract.rs b/contracts/swap-covenant/src/contract.rs index 36245c34..4b819e0c 100644 --- a/contracts/swap-covenant/src/contract.rs +++ b/contracts/swap-covenant/src/contract.rs @@ -1,9 +1,8 @@ - #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, CosmosMsg, DepsMut, Env, MessageInfo, Response, - SubMsg, WasmMsg, Reply, Deps, StdResult, Binary, Addr, + to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply, Response, + StdResult, SubMsg, WasmMsg, }; use covenant_clock::msg::PresetClockFields; @@ -11,15 +10,20 @@ use covenant_ibc_forwarder::msg::PresetIbcForwarderFields; use covenant_interchain_router::msg::PresetInterchainRouterFields; use covenant_interchain_splitter::msg::PresetInterchainSplitterFields; use covenant_swap_holder::msg::PresetSwapHolderFields; -use covenant_utils::{CovenantPartiesConfig, CovenantParty, ReceiverConfig, CovenantTerms}; +use covenant_utils::{CovenantPartiesConfig, CovenantParty, CovenantTerms, ReceiverConfig}; use cw2::set_contract_version; use cw_utils::{parse_reply_instantiate_data, ParseReplyError}; use crate::{ error::ContractError, + msg::{InstantiateMsg, QueryMsg}, state::{ - COVENANT_CLOCK_ADDR, PRESET_HOLDER_FIELDS, COVENANT_INTERCHAIN_SPLITTER_ADDR, PRESET_SPLITTER_FIELDS, COVENANT_SWAP_HOLDER_ADDR, PARTY_A_IBC_FORWARDER_ADDR, PARTY_B_IBC_FORWARDER_ADDR, PARTY_A_INTERCHAIN_ROUTER_ADDR, PARTY_B_INTERCHAIN_ROUTER_ADDR, PRESET_PARTY_A_FORWARDER_FIELDS, PRESET_PARTY_B_FORWARDER_FIELDS, PRESET_PARTY_A_ROUTER_FIELDS, PRESET_PARTY_B_ROUTER_FIELDS, PRESET_CLOCK_FIELDS, - }, msg::{InstantiateMsg, QueryMsg}, + COVENANT_CLOCK_ADDR, COVENANT_INTERCHAIN_SPLITTER_ADDR, COVENANT_SWAP_HOLDER_ADDR, + PARTY_A_IBC_FORWARDER_ADDR, PARTY_A_INTERCHAIN_ROUTER_ADDR, PARTY_B_IBC_FORWARDER_ADDR, + PARTY_B_INTERCHAIN_ROUTER_ADDR, PRESET_CLOCK_FIELDS, PRESET_HOLDER_FIELDS, + PRESET_PARTY_A_FORWARDER_FIELDS, PRESET_PARTY_A_ROUTER_FIELDS, + PRESET_PARTY_B_FORWARDER_FIELDS, PRESET_PARTY_B_ROUTER_FIELDS, PRESET_SPLITTER_FIELDS, + }, }; const CONTRACT_NAME: &str = "crates.io:swap-covenant"; @@ -33,7 +37,6 @@ pub const SWAP_HOLDER_REPLY_ID: u64 = 5u64; pub const PARTY_A_FORWARDER_REPLY_ID: u64 = 6u64; pub const PARTY_B_FORWARDER_REPLY_ID: u64 = 7u64; - #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, @@ -135,16 +138,19 @@ pub fn instantiate( .add_submessage(SubMsg::reply_on_success( clock_instantiate_tx, CLOCK_REPLY_ID, - )) - ) + ))) } #[cfg_attr(not(feature = "library"), entry_point)] pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { match msg.id { CLOCK_REPLY_ID => handle_clock_reply(deps, env, msg), - PARTY_A_INTERCHAIN_ROUTER_REPLY_ID => handle_party_a_interchain_router_reply(deps, env, msg), - PARTY_B_INTERCHAIN_ROUTER_REPLY_ID => handle_party_b_interchain_router_reply(deps, env, msg), + PARTY_A_INTERCHAIN_ROUTER_REPLY_ID => { + handle_party_a_interchain_router_reply(deps, env, msg) + } + PARTY_B_INTERCHAIN_ROUTER_REPLY_ID => { + handle_party_b_interchain_router_reply(deps, env, msg) + } SPLITTER_REPLY_ID => handle_splitter_reply(deps, env, msg), SWAP_HOLDER_REPLY_ID => handle_swap_holder_reply(deps, env, msg), PARTY_A_FORWARDER_REPLY_ID => handle_party_a_ibc_forwarder_reply(deps, env, msg), @@ -170,7 +176,9 @@ pub fn handle_clock_reply(deps: DepsMut, env: Env, msg: Reply) -> Result Result Err(ContractError::ContractInstantiationError { contract: "clock".to_string(), @@ -192,10 +206,13 @@ pub fn handle_clock_reply(deps: DepsMut, env: Env, msg: Reply) -> Result Result { +pub fn handle_party_a_interchain_router_reply( + deps: DepsMut, + env: Env, + msg: Reply, +) -> Result { deps.api.debug("WASMDEBUG: party A interchain router reply"); let parsed_data = parse_reply_instantiate_data(msg); @@ -212,7 +229,9 @@ pub fn handle_party_a_interchain_router_reply(deps: DepsMut, env: Env, msg: Repl let party_b_router_instantiate_tx: CosmosMsg = CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(env.contract.address.to_string()), code_id: party_b_router_preset_fields.code_id, - msg: to_binary(&party_b_router_preset_fields.to_instantiate_msg(clock_addr.to_string()))?, + msg: to_binary( + &party_b_router_preset_fields.to_instantiate_msg(clock_addr.to_string()), + )?, funds: vec![], label: party_b_router_preset_fields.label, }); @@ -220,10 +239,10 @@ pub fn handle_party_a_interchain_router_reply(deps: DepsMut, env: Env, msg: Repl Ok(Response::default() .add_attribute("method", "handle_party_a_interchain_router_reply") .add_attribute("party_a_interchain_router_addr", router_addr) - .add_submessage( - SubMsg::reply_always(party_b_router_instantiate_tx, PARTY_B_INTERCHAIN_ROUTER_REPLY_ID) - ) - ) + .add_submessage(SubMsg::reply_always( + party_b_router_instantiate_tx, + PARTY_B_INTERCHAIN_ROUTER_REPLY_ID, + ))) } Err(err) => Err(ContractError::ContractInstantiationError { contract: "party a router".to_string(), @@ -234,7 +253,11 @@ pub fn handle_party_a_interchain_router_reply(deps: DepsMut, env: Env, msg: Repl /// party B interchain router instantiation reply means we can proceed with the instantiation chain. /// we store the instantiated router address and submit the interchain splitter instantiation tx. -pub fn handle_party_b_interchain_router_reply(deps: DepsMut, env: Env, msg: Reply) -> Result { +pub fn handle_party_b_interchain_router_reply( + deps: DepsMut, + env: Env, + msg: Reply, +) -> Result { deps.api.debug("WASMDEBUG: party B interchain router reply"); let parsed_data = parse_reply_instantiate_data(msg); @@ -247,14 +270,16 @@ pub fn handle_party_b_interchain_router_reply(deps: DepsMut, env: Env, msg: Repl let preset_splitter_fields = PRESET_SPLITTER_FIELDS.load(deps.storage)?; let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; let party_a_router = PARTY_A_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; - let splitter_instantiate_msg = preset_splitter_fields.to_instantiate_msg( - clock_addr.to_string(), - party_a_router.to_string(), - router_addr.to_string() - ).map_err(|e| ContractError::ContractInstantiationError { - contract: "splitter".to_string(), - err: ParseReplyError::ParseFailure(e.to_string()), - })?; + let splitter_instantiate_msg = preset_splitter_fields + .to_instantiate_msg( + clock_addr.to_string(), + party_a_router.to_string(), + router_addr.to_string(), + ) + .map_err(|e| ContractError::ContractInstantiationError { + contract: "splitter".to_string(), + err: ParseReplyError::ParseFailure(e.to_string()), + })?; let splitter_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(env.contract.address.to_string()), @@ -267,7 +292,10 @@ pub fn handle_party_b_interchain_router_reply(deps: DepsMut, env: Env, msg: Repl Ok(Response::default() .add_attribute("method", "handle_party_b_interchain_router_reply") .add_attribute("party_b_interchain_router_addr", router_addr) - .add_submessage(SubMsg::reply_always(splitter_instantiate_tx, SPLITTER_REPLY_ID))) + .add_submessage(SubMsg::reply_always( + splitter_instantiate_tx, + SPLITTER_REPLY_ID, + ))) } Err(err) => Err(ContractError::ContractInstantiationError { contract: "party b router".to_string(), @@ -278,7 +306,11 @@ pub fn handle_party_b_interchain_router_reply(deps: DepsMut, env: Env, msg: Repl /// splitter instantiation reply means we can proceed with the instantiation chain. /// we store the splitter address and submit the swap holder instantiate tx. -pub fn handle_splitter_reply(deps: DepsMut, env: Env, msg: Reply) -> Result { +pub fn handle_splitter_reply( + deps: DepsMut, + env: Env, + msg: Reply, +) -> Result { deps.api.debug("WASMDEBUG: splitter reply"); let parsed_data = parse_reply_instantiate_data(msg); @@ -307,7 +339,10 @@ pub fn handle_splitter_reply(deps: DepsMut, env: Env, msg: Reply) -> Result Err(ContractError::ContractInstantiationError { contract: "splitter".to_string(), @@ -318,7 +353,11 @@ pub fn handle_splitter_reply(deps: DepsMut, env: Env, msg: Reply) -> Result Result { +pub fn handle_swap_holder_reply( + deps: DepsMut, + env: Env, + msg: Reply, +) -> Result { deps.api.debug("WASMDEBUG: swap holder reply"); let parsed_data = parse_reply_instantiate_data(msg); @@ -330,12 +369,11 @@ pub fn handle_swap_holder_reply(deps: DepsMut, env: Env, msg: Reply) -> Result Result Err(ContractError::ContractInstantiationError { contract: "swap holder".to_string(), @@ -358,7 +399,11 @@ pub fn handle_swap_holder_reply(deps: DepsMut, env: Env, msg: Reply) -> Result Result { +pub fn handle_party_a_ibc_forwarder_reply( + deps: DepsMut, + env: Env, + msg: Reply, +) -> Result { deps.api.debug("WASMDEBUG: party A ibc forwader reply"); let parsed_data = parse_reply_instantiate_data(msg); @@ -370,13 +415,12 @@ pub fn handle_party_a_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) - // load the fields relevant to ibc forwarder instantiation let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; - let preset_party_b_forwarder_fields = PRESET_PARTY_B_FORWARDER_FIELDS.load(deps.storage)?; + let preset_party_b_forwarder_fields = + PRESET_PARTY_B_FORWARDER_FIELDS.load(deps.storage)?; let swap_holder = COVENANT_SWAP_HOLDER_ADDR.load(deps.storage)?; - let instantiate_msg = preset_party_b_forwarder_fields.to_instantiate_msg( - clock_addr.to_string(), - swap_holder.to_string(), - ); + let instantiate_msg = preset_party_b_forwarder_fields + .to_instantiate_msg(clock_addr.to_string(), swap_holder.to_string()); let party_b_forwarder_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(env.contract.address.to_string()), @@ -389,7 +433,10 @@ pub fn handle_party_a_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) - Ok(Response::default() .add_attribute("method", "handle_party_a_ibc_forwader") .add_attribute("party_a_ibc_forwarder_addr", party_a_ibc_forwarder_addr) - .add_submessage(SubMsg::reply_always(party_b_forwarder_instantiate_tx, PARTY_B_FORWARDER_REPLY_ID))) + .add_submessage(SubMsg::reply_always( + party_b_forwarder_instantiate_tx, + PARTY_B_FORWARDER_REPLY_ID, + ))) } Err(err) => Err(ContractError::ContractInstantiationError { contract: "party_a_forwarder".to_string(), @@ -398,10 +445,13 @@ pub fn handle_party_a_ibc_forwarder_reply(deps: DepsMut, env: Env, msg: Reply) - } } - /// party B ibc forwarder reply means that we instantiated all the contracts. /// we store the party B ibc forwarder address and whitelist the contracts on our clock. -pub fn handle_party_b_ibc_forwarder_reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { +pub fn handle_party_b_ibc_forwarder_reply( + deps: DepsMut, + _env: Env, + msg: Reply, +) -> Result { deps.api.debug("WASMDEBUG: party B ibc forwader reply"); let parsed_data = parse_reply_instantiate_data(msg); @@ -419,7 +469,6 @@ pub fn handle_party_b_ibc_forwarder_reply(deps: DepsMut, _env: Env, msg: Reply) let party_a_router = PARTY_A_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; let party_b_router = PARTY_B_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; - let update_clock_whitelist_msg = WasmMsg::Migrate { contract_addr: clock_addr.to_string(), new_code_id: preset_clock_fields.code_id, @@ -439,8 +488,7 @@ pub fn handle_party_b_ibc_forwarder_reply(deps: DepsMut, _env: Env, msg: Reply) Ok(Response::default() .add_attribute("method", "handle_party_b_ibc_forwarder_reply") .add_attribute("party_b_ibc_forwarder_addr", party_b_ibc_forwarder_addr) - .add_message(update_clock_whitelist_msg) - ) + .add_message(update_clock_whitelist_msg)) } Err(err) => Err(ContractError::ContractInstantiationError { contract: "party_b ibc forwarder".to_string(), @@ -452,9 +500,13 @@ pub fn handle_party_b_ibc_forwarder_reply(deps: DepsMut, _env: Env, msg: Reply) #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::ClockAddress{} => Ok(to_binary(&COVENANT_CLOCK_ADDR.may_load(deps.storage)?)?), - QueryMsg::HolderAddress {} => Ok(to_binary(&COVENANT_SWAP_HOLDER_ADDR.may_load(deps.storage)?)?), - QueryMsg::SplitterAddress {} => Ok(to_binary(&COVENANT_INTERCHAIN_SPLITTER_ADDR.may_load(deps.storage)?)?), + QueryMsg::ClockAddress {} => Ok(to_binary(&COVENANT_CLOCK_ADDR.may_load(deps.storage)?)?), + QueryMsg::HolderAddress {} => Ok(to_binary( + &COVENANT_SWAP_HOLDER_ADDR.may_load(deps.storage)?, + )?), + QueryMsg::SplitterAddress {} => Ok(to_binary( + &COVENANT_INTERCHAIN_SPLITTER_ADDR.may_load(deps.storage)?, + )?), QueryMsg::InterchainRouterAddress { party } => { let resp = if party == "party_a" { PARTY_A_INTERCHAIN_ROUTER_ADDR.may_load(deps.storage)? @@ -464,7 +516,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { Some(Addr::unchecked("not found")) }; Ok(to_binary(&resp)?) - }, + } QueryMsg::IbcForwarderAddress { party } => { let resp = if party == "party_a" { PARTY_A_IBC_FORWARDER_ADDR.may_load(deps.storage)? diff --git a/contracts/swap-covenant/src/msg.rs b/contracts/swap-covenant/src/msg.rs index 32d8c7a8..71eae432 100644 --- a/contracts/swap-covenant/src/msg.rs +++ b/contracts/swap-covenant/src/msg.rs @@ -26,7 +26,7 @@ pub struct InstantiateMsg { pub struct CovenantPartyConfig { /// authorized address of the party pub addr: String, - /// denom provided by the party on its native chain + /// denom provided by the party on its native chain pub native_denom: String, /// ibc denom provided by the party on neutron pub ibc_denom: String, @@ -40,7 +40,6 @@ pub struct CovenantPartyConfig { pub party_chain_connection_id: String, /// timeout in seconds pub ibc_transfer_timeout: Uint64, - } #[cw_serde] @@ -62,7 +61,7 @@ pub struct SwapCovenantParties { pub struct SwapPartyConfig { /// authorized address of the party pub addr: Addr, - /// denom provided by the party on its native chain + /// denom provided by the party on its native chain pub native_denom: String, /// ibc denom provided by the party on neutron pub ibc_denom: String, @@ -76,7 +75,6 @@ pub struct SwapPartyConfig { pub party_chain_connection_id: String, /// timeout in seconds pub ibc_transfer_timeout: Uint64, - } #[cw_serde] diff --git a/contracts/swap-covenant/src/state.rs b/contracts/swap-covenant/src/state.rs index afb9dcec..7759842c 100644 --- a/contracts/swap-covenant/src/state.rs +++ b/contracts/swap-covenant/src/state.rs @@ -10,17 +10,22 @@ use cw_storage_plus::Item; // fields related to the contracts known prior to their. pub const PRESET_CLOCK_FIELDS: Item = Item::new("preset_clock_fields"); pub const PRESET_HOLDER_FIELDS: Item = Item::new("preset_holder_fields"); -pub const PRESET_SPLITTER_FIELDS: Item = Item::new("preset_splitter_fields"); -pub const PRESET_PARTY_A_ROUTER_FIELDS: Item = Item::new("preset_party_a_router_fields"); -pub const PRESET_PARTY_B_ROUTER_FIELDS: Item = Item::new("preset_party_b_router_fields"); -pub const PRESET_PARTY_A_FORWARDER_FIELDS: Item = Item::new("preset_party_a_forwarder_fields"); -pub const PRESET_PARTY_B_FORWARDER_FIELDS: Item = Item::new("preset_party_b_forwarder_fields"); +pub const PRESET_SPLITTER_FIELDS: Item = + Item::new("preset_splitter_fields"); +pub const PRESET_PARTY_A_ROUTER_FIELDS: Item = + Item::new("preset_party_a_router_fields"); +pub const PRESET_PARTY_B_ROUTER_FIELDS: Item = + Item::new("preset_party_b_router_fields"); +pub const PRESET_PARTY_A_FORWARDER_FIELDS: Item = + Item::new("preset_party_a_forwarder_fields"); +pub const PRESET_PARTY_B_FORWARDER_FIELDS: Item = + Item::new("preset_party_b_forwarder_fields"); pub const COVENANT_CLOCK_ADDR: Item = Item::new("covenant_clock_addr"); -pub const COVENANT_INTERCHAIN_SPLITTER_ADDR: Item = Item::new("covenant_interchain_splitter_addr"); +pub const COVENANT_INTERCHAIN_SPLITTER_ADDR: Item = + Item::new("covenant_interchain_splitter_addr"); pub const COVENANT_SWAP_HOLDER_ADDR: Item = Item::new("covenant_swap_holder_addr"); pub const PARTY_A_IBC_FORWARDER_ADDR: Item = Item::new("party_a_ibc_forwarder_addr"); pub const PARTY_B_IBC_FORWARDER_ADDR: Item = Item::new("party_b_ibc_forwarder_addr"); pub const PARTY_A_INTERCHAIN_ROUTER_ADDR: Item = Item::new("party_a_interchain_router_addr"); pub const PARTY_B_INTERCHAIN_ROUTER_ADDR: Item = Item::new("party_b_interchain_router_addr"); - diff --git a/contracts/swap-covenant/src/suite_test/suite.rs b/contracts/swap-covenant/src/suite_test/suite.rs index 9528837b..3370f7a6 100644 --- a/contracts/swap-covenant/src/suite_test/suite.rs +++ b/contracts/swap-covenant/src/suite_test/suite.rs @@ -67,6 +67,4 @@ impl SuiteBuilder { impl Suite {} // queries -impl Suite { - -} +impl Suite {} diff --git a/contracts/swap-covenant/src/suite_test/tests.rs b/contracts/swap-covenant/src/suite_test/tests.rs index e69de29b..8b137891 100644 --- a/contracts/swap-covenant/src/suite_test/tests.rs +++ b/contracts/swap-covenant/src/suite_test/tests.rs @@ -0,0 +1 @@ + diff --git a/contracts/swap-covenant/src/suite_test/unit_tests.rs b/contracts/swap-covenant/src/suite_test/unit_tests.rs index e69de29b..8b137891 100644 --- a/contracts/swap-covenant/src/suite_test/unit_tests.rs +++ b/contracts/swap-covenant/src/suite_test/unit_tests.rs @@ -0,0 +1 @@ + diff --git a/contracts/swap-holder/src/contract.rs b/contracts/swap-holder/src/contract.rs index 2f4d9ea3..e0d16d36 100644 --- a/contracts/swap-holder/src/contract.rs +++ b/contracts/swap-holder/src/contract.rs @@ -44,8 +44,7 @@ pub fn instantiate( Ok(Response::default() .add_attribute("method", "swap_holder_instantiate") - .add_attributes(msg.get_response_attributes()) - ) + .add_attributes(msg.get_response_attributes())) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -85,7 +84,7 @@ fn try_forward(deps: DepsMut, env: Env) -> Result { return Ok(Response::default() .add_attribute("method", "try_forward") .add_attribute("result", "covenant_expired") - .add_attribute("contract_state", "expired")) + .add_attribute("contract_state", "expired")); } let parties = PARTIES_CONFIG.load(deps.storage)?; diff --git a/contracts/swap-holder/src/msg.rs b/contracts/swap-holder/src/msg.rs index 73628956..ba8b4612 100644 --- a/contracts/swap-holder/src/msg.rs +++ b/contracts/swap-holder/src/msg.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Attribute}; use covenant_macros::{clocked, covenant_clock_address, covenant_deposit_address}; -use covenant_utils::{LockupConfig, CovenantPartiesConfig, CovenantTerms}; +use covenant_utils::{CovenantPartiesConfig, CovenantTerms, LockupConfig}; #[cw_serde] pub struct InstantiateMsg { diff --git a/contracts/swap-holder/src/suite_tests/suite.rs b/contracts/swap-holder/src/suite_tests/suite.rs index 9cfe1958..ceab3a43 100644 --- a/contracts/swap-holder/src/suite_tests/suite.rs +++ b/contracts/swap-holder/src/suite_tests/suite.rs @@ -1,8 +1,8 @@ use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg}; use cosmwasm_std::{Addr, Coin, Uint128}; use covenant_utils::{ - CovenantPartiesConfig, CovenantParty, CovenantTerms, LockupConfig, - SwapCovenantTerms, ReceiverConfig, + CovenantPartiesConfig, CovenantParty, CovenantTerms, LockupConfig, ReceiverConfig, + SwapCovenantTerms, }; use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; diff --git a/contracts/swap-holder/src/suite_tests/tests.rs b/contracts/swap-holder/src/suite_tests/tests.rs index 36b5e763..291575e9 100644 --- a/contracts/swap-holder/src/suite_tests/tests.rs +++ b/contracts/swap-holder/src/suite_tests/tests.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{Addr, Coin, Timestamp, Uint128}; use covenant_utils::{ - CovenantPartiesConfig, CovenantParty, CovenantTerms, LockupConfig, - SwapCovenantTerms, ReceiverConfig, + CovenantPartiesConfig, CovenantParty, CovenantTerms, LockupConfig, ReceiverConfig, + SwapCovenantTerms, }; use crate::{ diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index 46902731..194d206d 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -3,7 +3,10 @@ use cosmwasm_std::{ Addr, Attribute, BankMsg, BlockInfo, Coin, CosmosMsg, IbcMsg, IbcTimeout, StdError, Timestamp, Uint128, Uint64, }; -use neutron_sdk::{bindings::msg::{NeutronMsg, IbcFee}, sudo::msg::RequestPacketTimeoutHeight}; +use neutron_sdk::{ + bindings::msg::{IbcFee, NeutronMsg}, + sudo::msg::RequestPacketTimeoutHeight, +}; pub mod neutron_ica { use cosmwasm_schema::{cw_serde, QueryResponses}; @@ -244,7 +247,9 @@ pub enum ReceiverConfig { impl ReceiverConfig { pub fn get_response_attributes(self, party: String) -> Vec { match self { - ReceiverConfig::Native(addr) => vec![Attribute::new("receiver_config_native_addr", addr)], + ReceiverConfig::Native(addr) => { + vec![Attribute::new("receiver_config_native_addr", addr)] + } ReceiverConfig::Ibc(destination_config) => destination_config .get_response_attributes() .into_iter() @@ -285,7 +290,9 @@ impl CovenantParty { amount, }, timeout: IbcTimeout::with_timestamp( - block.time.plus_seconds(destination_config.ibc_transfer_timeout.u64()), + block + .time + .plus_seconds(destination_config.ibc_transfer_timeout.u64()), ), }), } @@ -366,16 +373,8 @@ impl DestinationConfig { let mut messages: Vec> = vec![]; for coin in coins { - // let msg: IbcMsg = IbcMsg::Transfer { - // channel_id: self.destination_chain_channel_id.to_string(), - // to_address: self.destination_receiver_addr.to_string(), - // amount: coin, - // timeout: IbcTimeout::with_timestamp( - // current_timestamp.plus_seconds(self.ibc_transfer_timeout.u64()), - // ), - // }; if coin.denom != "untrn" { - messages.push(CosmosMsg::Custom(NeutronMsg::IbcTransfer { + messages.push(CosmosMsg::Custom(NeutronMsg::IbcTransfer { source_port: "transfer".to_string(), source_channel: self.destination_chain_channel_id.to_string(), token: coin, @@ -385,7 +384,9 @@ impl DestinationConfig { revision_number: None, revision_height: None, }, - timeout_timestamp: current_timestamp.plus_seconds(self.ibc_transfer_timeout.u64()).nanos(), + timeout_timestamp: current_timestamp + .plus_seconds(self.ibc_transfer_timeout.u64()) + .nanos(), memo: "hi".to_string(), fee: IbcFee { // must be empty From a7f2869ed2c6f32ec32ce2a6fa8b82154f3d1243 Mon Sep 17 00:00:00 2001 From: bekauz Date: Sun, 27 Aug 2023 19:12:04 +0200 Subject: [PATCH 088/586] init interchain splitter --- Cargo.lock | 187 ++++++++++++++++++++++++ contracts/interchain-splitter/README.md | 3 + 2 files changed, 190 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 9632ac0a..4611c777 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,8 +36,13 @@ dependencies = [ [[package]] name = "astroport" +<<<<<<< HEAD version = "3.6.0" source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" +======= +version = "3.3.2" +source = "git+https://github.com/astroport-fi/astroport-core.git#52af83eab04c620ac40019f7cc9cee433d0c601e" +>>>>>>> e2dab3f (init interchain splitter) dependencies = [ "astroport-circular-buffer", "cosmwasm-schema", @@ -53,7 +58,11 @@ dependencies = [ [[package]] name = "astroport-circular-buffer" version = "0.1.0" +<<<<<<< HEAD source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" +======= +source = "git+https://github.com/astroport-fi/astroport-core.git#52af83eab04c620ac40019f7cc9cee433d0c601e" +>>>>>>> e2dab3f (init interchain splitter) dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -64,9 +73,15 @@ dependencies = [ [[package]] name = "astroport-factory" version = "1.6.0" +<<<<<<< HEAD source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" dependencies = [ "astroport 3.6.0", +======= +source = "git+https://github.com/astroport-fi/astroport-core.git#52af83eab04c620ac40019f7cc9cee433d0c601e" +dependencies = [ + "astroport 3.3.2", +>>>>>>> e2dab3f (init interchain splitter) "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -80,9 +95,15 @@ dependencies = [ [[package]] name = "astroport-native-coin-registry" version = "1.0.1" +<<<<<<< HEAD source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" dependencies = [ "astroport 3.6.0", +======= +source = "git+https://github.com/astroport-fi/astroport-core.git#52af83eab04c620ac40019f7cc9cee433d0c601e" +dependencies = [ + "astroport 3.3.2", +>>>>>>> e2dab3f (init interchain splitter) "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", @@ -93,10 +114,17 @@ dependencies = [ [[package]] name = "astroport-pair-stable" +<<<<<<< HEAD version = "3.3.0" source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" dependencies = [ "astroport 3.6.0", +======= +version = "3.1.1" +source = "git+https://github.com/astroport-fi/astroport-core.git#52af83eab04c620ac40019f7cc9cee433d0c601e" +dependencies = [ + "astroport 3.3.2", +>>>>>>> e2dab3f (init interchain splitter) "astroport-circular-buffer", "cosmwasm-schema", "cosmwasm-std", @@ -111,9 +139,15 @@ dependencies = [ [[package]] name = "astroport-token" version = "1.1.1" +<<<<<<< HEAD source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" dependencies = [ "astroport 3.6.0", +======= +source = "git+https://github.com/astroport-fi/astroport-core.git#52af83eab04c620ac40019f7cc9cee433d0c601e" +dependencies = [ + "astroport 3.3.2", +>>>>>>> e2dab3f (init interchain splitter) "cosmwasm-schema", "cosmwasm-std", "cw2 0.15.1", @@ -125,9 +159,15 @@ dependencies = [ [[package]] name = "astroport-whitelist" version = "1.0.1" +<<<<<<< HEAD source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" dependencies = [ "astroport 3.6.0", +======= +source = "git+https://github.com/astroport-fi/astroport-core.git#52af83eab04c620ac40019f7cc9cee433d0c601e" +dependencies = [ + "astroport 3.3.2", +>>>>>>> e2dab3f (init interchain splitter) "cosmwasm-schema", "cosmwasm-std", "cw1-whitelist 0.15.1", @@ -161,9 +201,15 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" +<<<<<<< HEAD version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +======= +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" +>>>>>>> e2dab3f (init interchain splitter) [[package]] name = "base64ct" @@ -198,6 +244,15 @@ dependencies = [ [[package]] name = "bnum" version = "0.8.0" +<<<<<<< HEAD +======= +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128a44527fc0d6abf05f9eda748b9027536e12dff93f5acc8449f51583309350" + +[[package]] +name = "byteorder" +version = "1.4.3" +>>>>>>> e2dab3f (init interchain splitter) source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "128a44527fc0d6abf05f9eda748b9027536e12dff93f5acc8449f51583309350" @@ -302,7 +357,11 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a44d3f9c25b2f864737c6605a98f2e4675d53fd8bbc7cf4d7c02475661a793d" dependencies = [ +<<<<<<< HEAD "base64 0.21.4", +======= + "base64 0.21.3", +>>>>>>> e2dab3f (init interchain splitter) "bnum", "cosmwasm-crypto", "cosmwasm-derive", @@ -485,6 +544,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "covenant-interchain-splitter" +version = "1.0.0" +dependencies = [ + "cosmos-sdk-proto 0.14.0", + "cosmwasm-schema", + "cosmwasm-std", + "covenant-clock", + "covenant-macros", + "covenant-utils", + "cw-storage-plus 1.1.0", + "cw2 1.1.0", + "neutron-sdk", + "protobuf 3.2.0", + "schemars", + "serde", + "serde-json-wasm 0.4.1", + "thiserror", +] + [[package]] name = "covenant-lp" version = "1.0.0" @@ -984,9 +1063,15 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dyn-clone" +<<<<<<< HEAD version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" +======= +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" +>>>>>>> e2dab3f (init interchain splitter) [[package]] name = "ecdsa" @@ -1008,7 +1093,11 @@ checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" dependencies = [ "der 0.7.8", "digest 0.10.7", +<<<<<<< HEAD "elliptic-curve 0.13.6", +======= + "elliptic-curve 0.13.5", +>>>>>>> e2dab3f (init interchain splitter) "rfc6979 0.4.0", "signature 2.1.0", "spki 0.7.2", @@ -1057,9 +1146,15 @@ dependencies = [ [[package]] name = "elliptic-curve" +<<<<<<< HEAD version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914" +======= +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +>>>>>>> e2dab3f (init interchain splitter) dependencies = [ "base16ct 0.2.0", "crypto-bigint 0.5.3", @@ -1201,6 +1296,7 @@ dependencies = [ "cfg-if", "ecdsa 0.14.8", "elliptic-curve 0.12.3", +<<<<<<< HEAD "sha2 0.10.8", ] @@ -1216,6 +1312,23 @@ dependencies = [ "once_cell", "sha2 0.10.8", "signature 2.1.0", +======= + "sha2 0.10.7", +>>>>>>> e2dab3f (init interchain splitter) +] + +[[package]] +name = "k256" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +dependencies = [ + "cfg-if", + "ecdsa 0.16.8", + "elliptic-curve 0.13.5", + "once_cell", + "sha2 0.10.7", + "signature 2.1.0", ] [[package]] @@ -1227,9 +1340,15 @@ checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "neutron-sdk" version = "0.6.1" +<<<<<<< HEAD source = "git+https://github.com/neutron-org/neutron-sdk#74fea05e407e5ff7cdfc195c3a76d2cce6a47d20" dependencies = [ "base64 0.21.4", +======= +source = "git+https://github.com/neutron-org/neutron-sdk#31af063f06bb90d46081a944a7df085d8f2ab493" +dependencies = [ + "base64 0.21.3", +>>>>>>> e2dab3f (init interchain splitter) "bech32", "cosmos-sdk-proto 0.16.0", "cosmwasm-schema", @@ -1257,9 +1376,15 @@ dependencies = [ [[package]] name = "num-traits" +<<<<<<< HEAD version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +======= +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +>>>>>>> e2dab3f (init interchain splitter) dependencies = [ "autocfg", ] @@ -1313,9 +1438,15 @@ dependencies = [ [[package]] name = "proc-macro2" +<<<<<<< HEAD version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c" +======= +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +>>>>>>> e2dab3f (init interchain splitter) dependencies = [ "unicode-ident", ] @@ -1458,9 +1589,15 @@ checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "schemars" +<<<<<<< HEAD version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c" +======= +version = "0.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763f8cd0d4c71ed8389c90cb8100cba87e763bd01a8e614d4f0af97bcd50a161" +>>>>>>> e2dab3f (init interchain splitter) dependencies = [ "dyn-clone", "schemars_derive", @@ -1470,9 +1607,15 @@ dependencies = [ [[package]] name = "schemars_derive" +<<<<<<< HEAD version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c" +======= +version = "0.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737" +>>>>>>> e2dab3f (init interchain splitter) dependencies = [ "proc-macro2", "quote", @@ -1510,9 +1653,15 @@ dependencies = [ [[package]] name = "semver" +<<<<<<< HEAD version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" +======= +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +>>>>>>> e2dab3f (init interchain splitter) [[package]] name = "serde" @@ -1558,7 +1707,11 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", +<<<<<<< HEAD "syn 2.0.38", +======= + "syn 2.0.31", +>>>>>>> e2dab3f (init interchain splitter) ] [[package]] @@ -1574,9 +1727,15 @@ dependencies = [ [[package]] name = "serde_json" +<<<<<<< HEAD version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +======= +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +>>>>>>> e2dab3f (init interchain splitter) dependencies = [ "itoa", "ryu", @@ -1702,9 +1861,15 @@ dependencies = [ [[package]] name = "syn" +<<<<<<< HEAD version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +======= +version = "2.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" +>>>>>>> e2dab3f (init interchain splitter) dependencies = [ "proc-macro2", "quote", @@ -1749,15 +1914,22 @@ dependencies = [ [[package]] name = "thiserror" +<<<<<<< HEAD version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +======= +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +>>>>>>> e2dab3f (init interchain splitter) dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" +<<<<<<< HEAD version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" @@ -1765,6 +1937,15 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.38", +======= +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.31", +>>>>>>> e2dab3f (init interchain splitter) ] [[package]] @@ -1804,9 +1985,15 @@ dependencies = [ [[package]] name = "unicode-ident" +<<<<<<< HEAD version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +======= +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +>>>>>>> e2dab3f (init interchain splitter) [[package]] name = "version_check" diff --git a/contracts/interchain-splitter/README.md b/contracts/interchain-splitter/README.md index c2d253ff..61ac0b46 100644 --- a/contracts/interchain-splitter/README.md +++ b/contracts/interchain-splitter/README.md @@ -4,6 +4,7 @@ Interchain Splitter is a contract meant to facilitate a pre-agreed upon way of d Splitter should remain agnostic to any price changes that may occur during the covenant lifecycle. It should accept the tokens and distribute them according to the initial agreement. +<<<<<<< HEAD ## Split Configurations @@ -26,3 +27,5 @@ Custom split configuration should always add up to 100 or else an error is retur For cases where denoms don't really matter, a wildcard split can be provided. Then any denoms that the splitter holds that do not fall under any of other configurations will be split according to this. +======= +>>>>>>> e2dab3f (init interchain splitter) From a57ea19a2e20fd717bd5c891032fcb3a06fc3854 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 29 Aug 2023 22:51:47 +0200 Subject: [PATCH 089/586] test suite; fallback token distribution --- Cargo.lock | 215 ++++------------------------------------------------- 1 file changed, 14 insertions(+), 201 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4611c777..7a150156 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,13 +36,8 @@ dependencies = [ [[package]] name = "astroport" -<<<<<<< HEAD version = "3.6.0" source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" -======= -version = "3.3.2" -source = "git+https://github.com/astroport-fi/astroport-core.git#52af83eab04c620ac40019f7cc9cee433d0c601e" ->>>>>>> e2dab3f (init interchain splitter) dependencies = [ "astroport-circular-buffer", "cosmwasm-schema", @@ -58,11 +53,7 @@ dependencies = [ [[package]] name = "astroport-circular-buffer" version = "0.1.0" -<<<<<<< HEAD source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" -======= -source = "git+https://github.com/astroport-fi/astroport-core.git#52af83eab04c620ac40019f7cc9cee433d0c601e" ->>>>>>> e2dab3f (init interchain splitter) dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -73,15 +64,9 @@ dependencies = [ [[package]] name = "astroport-factory" version = "1.6.0" -<<<<<<< HEAD source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" dependencies = [ "astroport 3.6.0", -======= -source = "git+https://github.com/astroport-fi/astroport-core.git#52af83eab04c620ac40019f7cc9cee433d0c601e" -dependencies = [ - "astroport 3.3.2", ->>>>>>> e2dab3f (init interchain splitter) "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -95,15 +80,9 @@ dependencies = [ [[package]] name = "astroport-native-coin-registry" version = "1.0.1" -<<<<<<< HEAD source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" dependencies = [ "astroport 3.6.0", -======= -source = "git+https://github.com/astroport-fi/astroport-core.git#52af83eab04c620ac40019f7cc9cee433d0c601e" -dependencies = [ - "astroport 3.3.2", ->>>>>>> e2dab3f (init interchain splitter) "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", @@ -114,17 +93,10 @@ dependencies = [ [[package]] name = "astroport-pair-stable" -<<<<<<< HEAD version = "3.3.0" source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" dependencies = [ "astroport 3.6.0", -======= -version = "3.1.1" -source = "git+https://github.com/astroport-fi/astroport-core.git#52af83eab04c620ac40019f7cc9cee433d0c601e" -dependencies = [ - "astroport 3.3.2", ->>>>>>> e2dab3f (init interchain splitter) "astroport-circular-buffer", "cosmwasm-schema", "cosmwasm-std", @@ -139,15 +111,9 @@ dependencies = [ [[package]] name = "astroport-token" version = "1.1.1" -<<<<<<< HEAD source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" dependencies = [ "astroport 3.6.0", -======= -source = "git+https://github.com/astroport-fi/astroport-core.git#52af83eab04c620ac40019f7cc9cee433d0c601e" -dependencies = [ - "astroport 3.3.2", ->>>>>>> e2dab3f (init interchain splitter) "cosmwasm-schema", "cosmwasm-std", "cw2 0.15.1", @@ -159,15 +125,9 @@ dependencies = [ [[package]] name = "astroport-whitelist" version = "1.0.1" -<<<<<<< HEAD source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" dependencies = [ "astroport 3.6.0", -======= -source = "git+https://github.com/astroport-fi/astroport-core.git#52af83eab04c620ac40019f7cc9cee433d0c601e" -dependencies = [ - "astroport 3.3.2", ->>>>>>> e2dab3f (init interchain splitter) "cosmwasm-schema", "cosmwasm-std", "cw1-whitelist 0.15.1", @@ -201,15 +161,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -<<<<<<< HEAD version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" -======= -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" ->>>>>>> e2dab3f (init interchain splitter) [[package]] name = "base64ct" @@ -244,15 +198,6 @@ dependencies = [ [[package]] name = "bnum" version = "0.8.0" -<<<<<<< HEAD -======= -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128a44527fc0d6abf05f9eda748b9027536e12dff93f5acc8449f51583309350" - -[[package]] -name = "byteorder" -version = "1.4.3" ->>>>>>> e2dab3f (init interchain splitter) source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "128a44527fc0d6abf05f9eda748b9027536e12dff93f5acc8449f51583309350" @@ -307,9 +252,9 @@ dependencies = [ [[package]] name = "cosmwasm-crypto" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca101fbf2f76723711a30ea3771ef312ec3ec254ad021b237871ed802f9f175" +checksum = "a6fb22494cf7d23d0c348740e06e5c742070b2991fd41db77bba0bcfbae1a723" dependencies = [ "digest 0.10.7", "ed25519-zebra", @@ -320,18 +265,18 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c73d2dd292f60e42849d2b07c03d809cf31e128a4299a805abd6d24553bcaaf5" +checksum = "6e199424486ea97d6b211db6387fd72e26b4a439d40cc23140b2d8305728055b" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce34a08020433989af5cc470104f6bd22134320fe0221bd8aeb919fd5ec92d5" +checksum = "fef683a9c1c4eabd6d31515719d0d2cc66952c4c87f7eb192bfc90384517dc34" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -342,9 +287,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96694ec781a7dd6dea1f968a2529ade009c21ad999c88b5f53d6cc495b3b96f7" +checksum = "9567025acbb4c0c008178393eb53b3ac3c2e492c25949d3bf415b9cbe80772d8" dependencies = [ "proc-macro2", "quote", @@ -353,15 +298,11 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a44d3f9c25b2f864737c6605a98f2e4675d53fd8bbc7cf4d7c02475661a793d" +checksum = "7d89d680fb60439b7c5947b15f9c84b961b88d1f8a3b20c4bd178a3f87db8bae" dependencies = [ -<<<<<<< HEAD "base64 0.21.4", -======= - "base64 0.21.3", ->>>>>>> e2dab3f (init interchain splitter) "bnum", "cosmwasm-crypto", "cosmwasm-derive", @@ -377,9 +318,9 @@ dependencies = [ [[package]] name = "cosmwasm-storage" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab544dfcad7c9e971933d522d99ec75cc8ddfa338854bb992b092e11bcd7e818" +checksum = "54a1c574d30feffe4b8121e61e839c231a5ce21901221d2fb4d5c945968a4f00" dependencies = [ "cosmwasm-std", "serde", @@ -544,26 +485,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "covenant-interchain-splitter" -version = "1.0.0" -dependencies = [ - "cosmos-sdk-proto 0.14.0", - "cosmwasm-schema", - "cosmwasm-std", - "covenant-clock", - "covenant-macros", - "covenant-utils", - "cw-storage-plus 1.1.0", - "cw2 1.1.0", - "neutron-sdk", - "protobuf 3.2.0", - "schemars", - "serde", - "serde-json-wasm 0.4.1", - "thiserror", -] - [[package]] name = "covenant-lp" version = "1.0.0" @@ -1063,15 +984,9 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dyn-clone" -<<<<<<< HEAD version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" -======= -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" ->>>>>>> e2dab3f (init interchain splitter) [[package]] name = "ecdsa" @@ -1093,11 +1008,7 @@ checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" dependencies = [ "der 0.7.8", "digest 0.10.7", -<<<<<<< HEAD "elliptic-curve 0.13.6", -======= - "elliptic-curve 0.13.5", ->>>>>>> e2dab3f (init interchain splitter) "rfc6979 0.4.0", "signature 2.1.0", "spki 0.7.2", @@ -1146,15 +1057,9 @@ dependencies = [ [[package]] name = "elliptic-curve" -<<<<<<< HEAD version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914" -======= -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" ->>>>>>> e2dab3f (init interchain splitter) dependencies = [ "base16ct 0.2.0", "crypto-bigint 0.5.3", @@ -1296,7 +1201,6 @@ dependencies = [ "cfg-if", "ecdsa 0.14.8", "elliptic-curve 0.12.3", -<<<<<<< HEAD "sha2 0.10.8", ] @@ -1312,23 +1216,6 @@ dependencies = [ "once_cell", "sha2 0.10.8", "signature 2.1.0", -======= - "sha2 0.10.7", ->>>>>>> e2dab3f (init interchain splitter) -] - -[[package]] -name = "k256" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" -dependencies = [ - "cfg-if", - "ecdsa 0.16.8", - "elliptic-curve 0.13.5", - "once_cell", - "sha2 0.10.7", - "signature 2.1.0", ] [[package]] @@ -1340,15 +1227,9 @@ checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "neutron-sdk" version = "0.6.1" -<<<<<<< HEAD source = "git+https://github.com/neutron-org/neutron-sdk#74fea05e407e5ff7cdfc195c3a76d2cce6a47d20" dependencies = [ "base64 0.21.4", -======= -source = "git+https://github.com/neutron-org/neutron-sdk#31af063f06bb90d46081a944a7df085d8f2ab493" -dependencies = [ - "base64 0.21.3", ->>>>>>> e2dab3f (init interchain splitter) "bech32", "cosmos-sdk-proto 0.16.0", "cosmwasm-schema", @@ -1376,15 +1257,9 @@ dependencies = [ [[package]] name = "num-traits" -<<<<<<< HEAD version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" -======= -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" ->>>>>>> e2dab3f (init interchain splitter) dependencies = [ "autocfg", ] @@ -1438,15 +1313,9 @@ dependencies = [ [[package]] name = "proc-macro2" -<<<<<<< HEAD -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c" -======= -version = "1.0.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" ->>>>>>> e2dab3f (init interchain splitter) +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -1589,15 +1458,9 @@ checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "schemars" -<<<<<<< HEAD version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c" -======= -version = "0.8.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763f8cd0d4c71ed8389c90cb8100cba87e763bd01a8e614d4f0af97bcd50a161" ->>>>>>> e2dab3f (init interchain splitter) dependencies = [ "dyn-clone", "schemars_derive", @@ -1607,15 +1470,9 @@ dependencies = [ [[package]] name = "schemars_derive" -<<<<<<< HEAD version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c" -======= -version = "0.8.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737" ->>>>>>> e2dab3f (init interchain splitter) dependencies = [ "proc-macro2", "quote", @@ -1653,15 +1510,9 @@ dependencies = [ [[package]] name = "semver" -<<<<<<< HEAD version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" -======= -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" ->>>>>>> e2dab3f (init interchain splitter) [[package]] name = "serde" @@ -1707,11 +1558,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", -<<<<<<< HEAD "syn 2.0.38", -======= - "syn 2.0.31", ->>>>>>> e2dab3f (init interchain splitter) ] [[package]] @@ -1727,15 +1574,9 @@ dependencies = [ [[package]] name = "serde_json" -<<<<<<< HEAD version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" -======= -version = "1.0.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" ->>>>>>> e2dab3f (init interchain splitter) dependencies = [ "itoa", "ryu", @@ -1861,15 +1702,9 @@ dependencies = [ [[package]] name = "syn" -<<<<<<< HEAD version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" -======= -version = "2.0.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" ->>>>>>> e2dab3f (init interchain splitter) dependencies = [ "proc-macro2", "quote", @@ -1914,22 +1749,15 @@ dependencies = [ [[package]] name = "thiserror" -<<<<<<< HEAD version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" -======= -version = "1.0.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" ->>>>>>> e2dab3f (init interchain splitter) dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -<<<<<<< HEAD version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" @@ -1937,15 +1765,6 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.38", -======= -version = "1.0.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.31", ->>>>>>> e2dab3f (init interchain splitter) ] [[package]] @@ -1985,15 +1804,9 @@ dependencies = [ [[package]] name = "unicode-ident" -<<<<<<< HEAD version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -======= -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" ->>>>>>> e2dab3f (init interchain splitter) [[package]] name = "version_check" From 99d7df306cb640b6834b2c4be4814485099ff3b7 Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 15 Sep 2023 19:09:19 +0200 Subject: [PATCH 090/586] wip: rework covenant instantiation structure --- contracts/swap-covenant/src/contract.rs | 29 +++++++------------ .../tests/interchaintest/tokenswap_test.go | 12 ++++++++ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/contracts/swap-covenant/src/contract.rs b/contracts/swap-covenant/src/contract.rs index 4b819e0c..84c6bee3 100644 --- a/contracts/swap-covenant/src/contract.rs +++ b/contracts/swap-covenant/src/contract.rs @@ -1,3 +1,4 @@ + #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ @@ -186,14 +187,8 @@ pub fn handle_clock_reply(deps: DepsMut, env: Env, msg: Reply) -> Result>>>>>> 2afc023 (wip: rework covenant instantiation structure) relayer.CustomDockerImage("ghcr.io/cosmos/relayer", "v2.3.1", rly.RlyDefaultUidGid), relayer.RelayerOptionExtraStartFlags{Flags: []string{"-p", "events", "-b", "100", "-d", "--log-format", "console"}}, ).Build(t, client, network) @@ -391,11 +395,18 @@ func TestTokenSwap(t *testing.T) { PartyBAmount: strconv.FormatUint(osmoContributionAmount, 10), } +<<<<<<< HEAD // timestamp := Timestamp("1981539923") block := Block(500) lockupConfig := LockupConfig{ BlockHeight: &block, +======= + timestamp := Timestamp("1981539923") + + lockupConfig := LockupConfig{ + Time: ×tamp, +>>>>>>> 2afc023 (wip: rework covenant instantiation structure) } presetIbcFee := PresetIbcFee{ AckFee: "10000", @@ -479,6 +490,7 @@ func TestTokenSwap(t *testing.T) { require.NoError(t, err, "Failed to marshall CovenantInstantiateMsg") instantiateMsg := string(str) + println("instantiation message: ", instantiateMsg) cmd := []string{"neutrond", "tx", "wasm", "instantiate", covenantCodeIdStr, instantiateMsg, "--label", "swap-covenant", From 1e2de9d93712944dc7ae3fc250015a646717ee17 Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 18 Sep 2023 17:44:30 +0200 Subject: [PATCH 091/586] fmt --- contracts/swap-covenant/src/contract.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/contracts/swap-covenant/src/contract.rs b/contracts/swap-covenant/src/contract.rs index 84c6bee3..fb58483d 100644 --- a/contracts/swap-covenant/src/contract.rs +++ b/contracts/swap-covenant/src/contract.rs @@ -1,4 +1,3 @@ - #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ @@ -265,14 +264,16 @@ pub fn handle_party_b_interchain_router_reply( let preset_splitter_fields = PRESET_SPLITTER_FIELDS.load(deps.storage)?; let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; let party_a_router = PARTY_A_INTERCHAIN_ROUTER_ADDR.load(deps.storage)?; - let splitter_instantiate_msg = preset_splitter_fields.to_instantiate_msg( - clock_addr.to_string(), - party_a_router.to_string(), - router_addr.to_string() - ).map_err(|e| ContractError::ContractInstantiationError { - contract: "splitter".to_string(), - err: ParseReplyError::ParseFailure(e.to_string()), - })?; + let splitter_instantiate_msg = preset_splitter_fields + .to_instantiate_msg( + clock_addr.to_string(), + party_a_router.to_string(), + router_addr.to_string(), + ) + .map_err(|e| ContractError::ContractInstantiationError { + contract: "splitter".to_string(), + err: ParseReplyError::ParseFailure(e.to_string()), + })?; let splitter_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(env.contract.address.to_string()), From 270927d488066fd8a76bf45287248b09a70018d6 Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 21 Aug 2023 22:53:18 +0200 Subject: [PATCH 092/586] two party pol holder init --- Cargo.lock | 20 +++++++++++ contracts/two-party-pol-holder/.cargo/config | 3 ++ contracts/two-party-pol-holder/Cargo.toml | 33 +++++++++++++++++++ contracts/two-party-pol-holder/README.md | 28 ++++++++++++++++ .../two-party-pol-holder/src/contract.rs | 0 contracts/two-party-pol-holder/src/error.rs | 0 contracts/two-party-pol-holder/src/lib.rs | 8 +++++ contracts/two-party-pol-holder/src/msg.rs | 0 contracts/two-party-pol-holder/src/state.rs | 0 9 files changed, 92 insertions(+) create mode 100644 contracts/two-party-pol-holder/.cargo/config create mode 100644 contracts/two-party-pol-holder/Cargo.toml create mode 100644 contracts/two-party-pol-holder/README.md create mode 100644 contracts/two-party-pol-holder/src/contract.rs create mode 100644 contracts/two-party-pol-holder/src/error.rs create mode 100644 contracts/two-party-pol-holder/src/lib.rs create mode 100644 contracts/two-party-pol-holder/src/msg.rs create mode 100644 contracts/two-party-pol-holder/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 7a150156..9c8d0679 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -626,6 +626,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "covenant-two-party-pol-holder" +version = "1.0.0" +dependencies = [ + "cosmos-sdk-proto 0.14.0", + "cosmwasm-schema", + "cosmwasm-std", + "covenant-clock", + "covenant-macros", + "covenant-utils", + "cw-storage-plus 1.1.0", + "cw2 1.1.1", + "neutron-sdk", + "protobuf 3.3.0", + "schemars", + "serde", + "serde-json-wasm 0.4.1", + "thiserror", +] + [[package]] name = "covenant-utils" version = "0.0.1" diff --git a/contracts/two-party-pol-holder/.cargo/config b/contracts/two-party-pol-holder/.cargo/config new file mode 100644 index 00000000..6a6f2852 --- /dev/null +++ b/contracts/two-party-pol-holder/.cargo/config @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +schema = "run --bin schema" \ No newline at end of file diff --git a/contracts/two-party-pol-holder/Cargo.toml b/contracts/two-party-pol-holder/Cargo.toml new file mode 100644 index 00000000..953e213a --- /dev/null +++ b/contracts/two-party-pol-holder/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "covenant-two-party-pol-holder" +authors = ["benskey bekauz@protonmail.com"] +description = "Two party POL holder module for covenants" +edition = { workspace = true } +license = { workspace = true } +rust-version = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# disables #[entry_point] (i.e. instantiate/execute/query) export +library = [] + +[dependencies] +covenant-macros = { workspace = true } +covenant-clock = { workspace = true, features=["library"] } +covenant-utils = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +thiserror = { workspace = true } +schemars = { workspace = true } +serde-json-wasm = { workspace = true } +serde = { workspace = true } +neutron-sdk = { workspace = true } +cosmos-sdk-proto = { workspace = true } +protobuf = { workspace = true } diff --git a/contracts/two-party-pol-holder/README.md b/contracts/two-party-pol-holder/README.md new file mode 100644 index 00000000..8abf8e15 --- /dev/null +++ b/contracts/two-party-pol-holder/README.md @@ -0,0 +1,28 @@ +# Two party POL holder + +## Multiple parties + +Multiple parties are going to be participating, so the holder should store a list of whitelisted addresses. + +## Lock Period + +A `Lock` duration should be stored to keep track of the covenant duration. + +After the `Lock` period expires, holder should withdraw the liquidity and forward the underlying funds to the configured splitter module that will deal with the distribution. + +Splitter should be instantiated on demand, when the lock expires. + +## Ragequit + +A ragequit functionality should be enabled for both parties that may wish to break their part of the covenant. +Ragequitting party is subject to a percentage based penalty agreed upon instantiation. + +Holder then withdraws the allocation of the ragequitting party (minus the penalty) and forwards the funds to the party. + +The other party may remain in their position for as long as they wish to, but not any longer than the initial duration. +Alternatively, they may also exit their position without any penalty because the covenant is no longer valid. +In both cases, the non-ragequitting party receives their allocation plus the penalties deducted from the ragequitting party. + +## Updates + +Both parties are free to update their respective whitelisted addresses and do not need counterparty permission to do so. diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs new file mode 100644 index 00000000..e69de29b diff --git a/contracts/two-party-pol-holder/src/error.rs b/contracts/two-party-pol-holder/src/error.rs new file mode 100644 index 00000000..e69de29b diff --git a/contracts/two-party-pol-holder/src/lib.rs b/contracts/two-party-pol-holder/src/lib.rs new file mode 100644 index 00000000..0faea8f4 --- /dev/null +++ b/contracts/two-party-pol-holder/src/lib.rs @@ -0,0 +1,8 @@ +#![warn(clippy::unwrap_used, clippy::expect_used)] + +extern crate core; + +pub mod contract; +pub mod error; +pub mod msg; +pub mod state; diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs new file mode 100644 index 00000000..e69de29b diff --git a/contracts/two-party-pol-holder/src/state.rs b/contracts/two-party-pol-holder/src/state.rs new file mode 100644 index 00000000..e69de29b From a59adbe5f067df2825290b1cd3cf50098e90be47 Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 21 Aug 2023 23:49:00 +0200 Subject: [PATCH 093/586] state & msg --- contracts/two-party-pol-holder/src/msg.rs | 64 +++++++++++++++++++++ contracts/two-party-pol-holder/src/state.rs | 12 ++++ packages/covenant-macros/src/lib.rs | 16 ++++++ 3 files changed, 92 insertions(+) diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index e69de29b..b8a4be5c 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -0,0 +1,64 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Uint128, Decimal, Uint64}; +use covenant_macros::{clocked, covenant_deposit_address, covenant_clock_address, covenant_next_contract}; + + +#[cw_serde] +pub struct InstantiateMsg { + /// Address for the clock. This contract verifies + /// that only the clock can execute Ticks + pub clock_address: String, + /// block height of covenant expiration. Position is exited + /// automatically upon reaching that height. + pub expiration_height: u64, + /// address of the next contract to forward the funds to (splitter). + pub next_contract: Addr, + /// optional ragequit penalty denominated in decimals + pub ragequit_penalty: Option, + /// parties engaged in the POL. + pub whitelist_parties: Vec, +} + +#[clocked] +#[cw_serde] +pub enum ExecuteMsg { + /// initiate the ragequit + Ragequit {}, + /// withdraw the liquidity party is entitled to + Claim {}, +} + +#[cw_serde] +pub enum ContractState { + Instantiated, + /// one of the parties have initiated ragequit. + /// party with an active position is free to exit at any time. + Ragequit, + /// covenant has reached its expiration date. + ExpirationReached, + /// underlying funds have been withdrawn. + Complete, +} + +#[covenant_deposit_address] +#[covenant_clock_address] +#[covenant_next_contract] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(ContractState)] + ContractState {}, + #[returns(Option)] + RagequitPenalty {}, + #[returns(Uint64)] + ExpirationHeight {}, + #[returns(Vec)] + WhitelistParties {}, +} + +#[cw_serde] +pub struct Party { + pub addr: Addr, + pub share: Uint128, + pub provided_denom: String, +} diff --git a/contracts/two-party-pol-holder/src/state.rs b/contracts/two-party-pol-holder/src/state.rs index e69de29b..201dbf19 100644 --- a/contracts/two-party-pol-holder/src/state.rs +++ b/contracts/two-party-pol-holder/src/state.rs @@ -0,0 +1,12 @@ +use cosmwasm_std::{Addr, Uint128}; +use cw_storage_plus::Item; + +use crate::msg::{ContractState, Party}; + + +pub const CONTRACT_STATE: Item = Item::new("contract_state"); +pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); +pub const NEXT_CONTRACT: Item = Item::new("next_contract"); +pub const WHITELIST_PARTIES: Item> = Item::new("whitelist_parties"); +pub const EXPIRATION_HEIGHT: Item = Item::new("expiration_height"); +pub const RAGEQUIT_PENALTY: Item = Item::new("ragequit_penalty"); \ No newline at end of file diff --git a/packages/covenant-macros/src/lib.rs b/packages/covenant-macros/src/lib.rs index 99e5286f..90529dfb 100644 --- a/packages/covenant-macros/src/lib.rs +++ b/packages/covenant-macros/src/lib.rs @@ -113,3 +113,19 @@ pub fn covenant_ica_address(metadata: TokenStream, input: TokenStream) -> TokenS .into(), ) } + +#[proc_macro_attribute] +pub fn covenant_next_contract(metadata: TokenStream, input: TokenStream) -> TokenStream { + merge_variants( + metadata, + input, + quote!( + enum NextContract { + /// Returns the associated remote chain information + #[returns(Option)] + NextContract {}, + } + ) + .into(), + ) +} From c074c7010ad7f38381dee41f9bafd3860e74a8b6 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 22 Aug 2023 21:39:57 +0200 Subject: [PATCH 094/586] messages, state --- .../two-party-pol-holder/src/contract.rs | 38 ++++++ contracts/two-party-pol-holder/src/error.rs | 18 +++ contracts/two-party-pol-holder/src/msg.rs | 122 ++++++++++++++++-- contracts/two-party-pol-holder/src/state.rs | 12 +- 4 files changed, 171 insertions(+), 19 deletions(-) diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index e69de29b..60a9f85d 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -0,0 +1,38 @@ +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; + +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cw2::set_contract_version; + +use crate::{msg::InstantiateMsg, state::{POOL_ADDRESS, NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, PARTIES_CONFIG}, error::ContractError}; + +const CONTRACT_NAME: &str = "crates.io:covenant-two-party-pol-holder"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + deps.api.debug("WASMDEBUG: covenant-two-party-pol-holder instantiate"); + + let pool_addr = deps.api.addr_validate(&msg.pool_address)?; + let next_contract = deps.api.addr_validate(&msg.next_contract)?; + let clock_addr = deps.api.addr_validate(&msg.clock_address)?; + + let parties_config = msg.parties_config.validate()?; + let lockup_config = msg.lockup_config.validate(env.block)?; + + POOL_ADDRESS.save(deps.storage, &pool_addr)?; + NEXT_CONTRACT.save(deps.storage, &next_contract)?; + CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; + LOCKUP_CONFIG.save(deps.storage, &lockup_config)?; + RAGEQUIT_CONFIG.save(deps.storage, &msg.ragequit_config)?; + PARTIES_CONFIG.save(deps.storage, &parties_config)?; + + // TODO: response atributes + Ok(Response::default()) +} \ No newline at end of file diff --git a/contracts/two-party-pol-holder/src/error.rs b/contracts/two-party-pol-holder/src/error.rs index e69de29b..b2a288c4 100644 --- a/contracts/two-party-pol-holder/src/error.rs +++ b/contracts/two-party-pol-holder/src/error.rs @@ -0,0 +1,18 @@ + +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("expiry block is already past")] + InvalidExpiryBlockHeight {}, + + #[error("lockup validation failed")] + LockupValidationError {}, + + #[error("shares of covenant parties must add up to 1.0")] + InvolvedPartiesConfigError {}, +} \ No newline at end of file diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index b8a4be5c..98770eca 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -1,22 +1,28 @@ +use std::cmp::Ordering; + use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Uint128, Decimal, Uint64}; +use cosmwasm_std::{Addr, Decimal, Timestamp, BlockInfo}; use covenant_macros::{clocked, covenant_deposit_address, covenant_clock_address, covenant_next_contract}; +use crate::error::ContractError; #[cw_serde] pub struct InstantiateMsg { /// Address for the clock. This contract verifies /// that only the clock can execute Ticks pub clock_address: String, + /// address of the pool + pub pool_address: String, + /// address of the next contract to forward the funds to. + /// usually expected tobe the splitter. + pub next_contract: String, /// block height of covenant expiration. Position is exited /// automatically upon reaching that height. - pub expiration_height: u64, - /// address of the next contract to forward the funds to (splitter). - pub next_contract: Addr, - /// optional ragequit penalty denominated in decimals - pub ragequit_penalty: Option, + pub lockup_config: LockupConfig, + /// configuration for ragequit + pub ragequit_config: RagequitConfig, /// parties engaged in the POL. - pub whitelist_parties: Vec, + pub parties_config: PartiesConfig, } #[clocked] @@ -48,17 +54,105 @@ pub enum ContractState { pub enum QueryMsg { #[returns(ContractState)] ContractState {}, - #[returns(Option)] - RagequitPenalty {}, - #[returns(Uint64)] - ExpirationHeight {}, - #[returns(Vec)] - WhitelistParties {}, + #[returns(RagequitConfig)] + RagequitConfig {}, + #[returns(LockupConfig)] + LockupConfig {}, + #[returns(PartiesConfig)] + PartiesConfig {}, +} + +#[cw_serde] +pub struct PartiesConfig { + pub party_a: Party, + pub party_b: Party, +} + +impl PartiesConfig { + /// validates the decimal shares of parties involved + /// that must add up to 1.0 + pub fn validate(self) -> Result { + if self.party_a.share + self.party_b.share == Decimal::one() { + Ok(self) + } else { + Err(ContractError::InvolvedPartiesConfigError {}) + } + } } #[cw_serde] pub struct Party { + /// authorized address of the party pub addr: Addr, - pub share: Uint128, + /// decimal share of the LP position (e.g. 1/2) + pub share: Decimal, + /// denom provided by the party pub provided_denom: String, + /// whether party is actively providing liquidity + pub active_position: bool, +} + +#[cw_serde] +pub enum RagequitConfig { + /// ragequit is disabled + Disabled, + /// ragequit is enabled with `RagequitTerms` + Enabled(RagequitTerms), +} + +#[cw_serde] +pub struct RagequitTerms { + /// decimal based penalty to be applied on a party + /// for initiating ragequit. this fraction is then + /// added to the counterparty that did not initiate + /// the ragequit + pub penalty: Decimal, + /// bool flag to indicate whether ragequit had been + /// initiated + pub active: bool, +} + +/// enum based configuration of the lockup period. +#[cw_serde] +pub enum LockupConfig { + /// no lockup configured + None, + /// block height based lockup config + Block(u64), + /// timestamp based lockup config + Time(Timestamp), +} + +impl LockupConfig { + /// validates that the lockup config being stored is not already expired. + pub fn validate(self, block_info: BlockInfo) -> Result { + match self { + LockupConfig::None => Ok(self), + LockupConfig::Block(h) => { + if h > block_info.height { + Ok(self) + } else { + Err(ContractError::LockupValidationError {}) + } + }, + LockupConfig::Time(t) => { + if t.cmp(&block_info.time) != Ordering::Less { + Ok(self) + } else { + Err(ContractError::LockupValidationError {}) + } + }, + } + } + + /// compares current block info with the stored lockup config. + /// returns false if no lockup configuration is stored. + /// otherwise, returns true if the current block is past the stored info. + pub fn is_due(self, block_info: BlockInfo) -> bool { + match self { + LockupConfig::None => false, // or.. true? + LockupConfig::Block(b) => block_info.height >= b, + LockupConfig::Time(t) => t.nanos() < block_info.time.nanos(), + } + } } diff --git a/contracts/two-party-pol-holder/src/state.rs b/contracts/two-party-pol-holder/src/state.rs index 201dbf19..9fc2043b 100644 --- a/contracts/two-party-pol-holder/src/state.rs +++ b/contracts/two-party-pol-holder/src/state.rs @@ -1,12 +1,14 @@ -use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::Addr; use cw_storage_plus::Item; -use crate::msg::{ContractState, Party}; +use crate::msg::{ContractState, LockupConfig, RagequitConfig, PartiesConfig}; pub const CONTRACT_STATE: Item = Item::new("contract_state"); pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); pub const NEXT_CONTRACT: Item = Item::new("next_contract"); -pub const WHITELIST_PARTIES: Item> = Item::new("whitelist_parties"); -pub const EXPIRATION_HEIGHT: Item = Item::new("expiration_height"); -pub const RAGEQUIT_PENALTY: Item = Item::new("ragequit_penalty"); \ No newline at end of file +pub const PARTIES_CONFIG: Item = Item::new("parties_config"); +pub const LOCKUP_CONFIG: Item = Item::new("lockup_config"); +pub const RAGEQUIT_CONFIG: Item = Item::new("ragequit_config"); +pub const POOL_ADDRESS: Item = Item::new("pool_address"); + From cfeabb2e1dfb18054b24981fcfcf9d6a4446d132 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 22 Aug 2023 22:07:25 +0200 Subject: [PATCH 095/586] instantiate msg --- .../two-party-pol-holder/src/contract.rs | 9 ++- contracts/two-party-pol-holder/src/msg.rs | 75 ++++++++++++++++--- 2 files changed, 71 insertions(+), 13 deletions(-) diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index 60a9f85d..2e0a9207 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -29,10 +29,11 @@ pub fn instantiate( POOL_ADDRESS.save(deps.storage, &pool_addr)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; - LOCKUP_CONFIG.save(deps.storage, &lockup_config)?; + LOCKUP_CONFIG.save(deps.storage, lockup_config)?; RAGEQUIT_CONFIG.save(deps.storage, &msg.ragequit_config)?; - PARTIES_CONFIG.save(deps.storage, &parties_config)?; + PARTIES_CONFIG.save(deps.storage, parties_config)?; - // TODO: response atributes - Ok(Response::default()) + Ok(Response::default() + .add_attributes(msg.get_response_attributes()) + ) } \ No newline at end of file diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index 98770eca..3dab099e 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -1,7 +1,5 @@ -use std::cmp::Ordering; - use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Decimal, Timestamp, BlockInfo}; +use cosmwasm_std::{Addr, Decimal, Timestamp, BlockInfo, Attribute}; use covenant_macros::{clocked, covenant_deposit_address, covenant_clock_address, covenant_next_contract}; use crate::error::ContractError; @@ -25,6 +23,20 @@ pub struct InstantiateMsg { pub parties_config: PartiesConfig, } +impl InstantiateMsg { + pub fn get_response_attributes(self) -> Vec { + let mut attrs = vec![ + Attribute::new("clock_addr", self.clock_address), + Attribute::new("pool_address", self.pool_address), + Attribute::new("next_contract", self.next_contract), + ]; + attrs.extend(self.parties_config.get_response_attributes()); + attrs.extend(self.ragequit_config.get_response_attributes()); + attrs.extend(self.lockup_config.get_response_attributes()); + attrs + } +} + #[clocked] #[cw_serde] pub enum ExecuteMsg { @@ -68,10 +80,11 @@ pub struct PartiesConfig { pub party_b: Party, } + impl PartiesConfig { /// validates the decimal shares of parties involved /// that must add up to 1.0 - pub fn validate(self) -> Result { + pub fn validate(&self) -> Result<&PartiesConfig, ContractError> { if self.party_a.share + self.party_b.share == Decimal::one() { Ok(self) } else { @@ -80,6 +93,21 @@ impl PartiesConfig { } } +impl PartiesConfig { + pub fn get_response_attributes(self) -> Vec { + vec![ + Attribute::new("party_a_address", self.party_a.addr), + Attribute::new("party_a_share", self.party_a.share.to_string()), + Attribute::new("party_a_provided_denom", self.party_a.provided_denom), + Attribute::new("party_a_active_position", self.party_a.active_position.to_string()), + Attribute::new("party_b_address", self.party_b.addr), + Attribute::new("party_b_share", self.party_b.share.to_string()), + Attribute::new("party_b_provided_denom", self.party_b.provided_denom), + Attribute::new("party_b_active_position", self.party_b.active_position.to_string()), + ] + } +} + #[cw_serde] pub struct Party { /// authorized address of the party @@ -100,6 +128,21 @@ pub enum RagequitConfig { Enabled(RagequitTerms), } +impl RagequitConfig { + pub fn get_response_attributes(self) -> Vec { + match self { + RagequitConfig::Disabled => vec![ + Attribute::new("ragequit_config", "disabled"), + ], + RagequitConfig::Enabled(c) => vec![ + Attribute::new("ragequit_config", "enabled"), + Attribute::new("ragequit_penalty", c.penalty.to_string()), + Attribute::new("ragequit_active", c.active.to_string()), + ], + } + } +} + #[cw_serde] pub struct RagequitTerms { /// decimal based penalty to be applied on a party @@ -124,19 +167,33 @@ pub enum LockupConfig { } impl LockupConfig { + pub fn get_response_attributes(self) -> Vec { + match self { + LockupConfig::None => vec![ + Attribute::new("lockup_config", "none"), + ], + LockupConfig::Block(h) => vec![ + Attribute::new("lockup_config_expiry_block_height", h.to_string()), + ], + LockupConfig::Time(t) => vec![ + Attribute::new("lockup_config_expiry_block_timestamp", t.to_string()), + ], + } + } + /// validates that the lockup config being stored is not already expired. - pub fn validate(self, block_info: BlockInfo) -> Result { + pub fn validate(&self, block_info: BlockInfo) -> Result<&LockupConfig, ContractError> { match self { LockupConfig::None => Ok(self), LockupConfig::Block(h) => { - if h > block_info.height { + if h > &block_info.height { Ok(self) } else { Err(ContractError::LockupValidationError {}) } }, LockupConfig::Time(t) => { - if t.cmp(&block_info.time) != Ordering::Less { + if t.nanos() > block_info.time.nanos() { Ok(self) } else { Err(ContractError::LockupValidationError {}) @@ -150,8 +207,8 @@ impl LockupConfig { /// otherwise, returns true if the current block is past the stored info. pub fn is_due(self, block_info: BlockInfo) -> bool { match self { - LockupConfig::None => false, // or.. true? - LockupConfig::Block(b) => block_info.height >= b, + LockupConfig::None => false, // or.. true? should not be called + LockupConfig::Block(h) => h < block_info.height, LockupConfig::Time(t) => t.nanos() < block_info.time.nanos(), } } From 734f61c5491e9604db518d213483d2f78a5ffdd3 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 22 Aug 2023 23:58:23 +0200 Subject: [PATCH 096/586] wip: ragequit --- Cargo.lock | 2 + contracts/two-party-pol-holder/Cargo.toml | 2 + .../two-party-pol-holder/src/contract.rs | 109 +++++++++++++++++- contracts/two-party-pol-holder/src/error.rs | 15 +++ contracts/two-party-pol-holder/src/msg.rs | 38 +++++- 5 files changed, 159 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c8d0679..6cccedfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -630,6 +630,7 @@ dependencies = [ name = "covenant-two-party-pol-holder" version = "1.0.0" dependencies = [ + "astroport 2.8.0", "cosmos-sdk-proto 0.14.0", "cosmwasm-schema", "cosmwasm-std", @@ -638,6 +639,7 @@ dependencies = [ "covenant-utils", "cw-storage-plus 1.1.0", "cw2 1.1.1", + "cw20 0.15.1", "neutron-sdk", "protobuf 3.3.0", "schemars", diff --git a/contracts/two-party-pol-holder/Cargo.toml b/contracts/two-party-pol-holder/Cargo.toml index 953e213a..022393b0 100644 --- a/contracts/two-party-pol-holder/Cargo.toml +++ b/contracts/two-party-pol-holder/Cargo.toml @@ -31,3 +31,5 @@ serde = { workspace = true } neutron-sdk = { workspace = true } cosmos-sdk-proto = { workspace = true } protobuf = { workspace = true } +astroport = "2.8.0" +cw20 = { version = "0.15" } diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index 2e0a9207..51977d0d 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -1,10 +1,10 @@ -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, BalanceResponse}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cw2::set_contract_version; -use crate::{msg::InstantiateMsg, state::{POOL_ADDRESS, NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, PARTIES_CONFIG}, error::ContractError}; +use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, RagequitConfig}, state::{POOL_ADDRESS, NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, PARTIES_CONFIG, CONTRACT_STATE}, error::ContractError}; const CONTRACT_NAME: &str = "crates.io:covenant-two-party-pol-holder"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -23,7 +23,7 @@ pub fn instantiate( let next_contract = deps.api.addr_validate(&msg.next_contract)?; let clock_addr = deps.api.addr_validate(&msg.clock_address)?; - let parties_config = msg.parties_config.validate()?; + let parties_config = msg.parties_config.validate_config()?; let lockup_config = msg.lockup_config.validate(env.block)?; POOL_ADDRESS.save(deps.storage, &pool_addr)?; @@ -36,4 +36,107 @@ pub fn instantiate( Ok(Response::default() .add_attributes(msg.get_response_attributes()) ) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Ragequit {} => try_ragequit(deps, env, info), + ExecuteMsg::Claim {} => try_claim(deps, env, info), + ExecuteMsg::Tick {} => try_tick(deps, env, info), + } +} + +fn try_ragequit( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + // if lockup period had passed, just claim the tokens instead of ragequitting + let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; + if lockup_config.is_due(env.block) { + return Err(ContractError::RagequitWithLockupPassed {}) + } + + // only the involved parties can initiate the ragequit + let parties = PARTIES_CONFIG.load(deps.storage)?; + let rq_party = parties.validate_caller(info.sender)?; + + let mut rq_terms = match RAGEQUIT_CONFIG.load(deps.storage)? { + // if ragequit is not enabled for this covenant we error + RagequitConfig::Disabled => return Err(ContractError::RagequitDisabled {}), + RagequitConfig::Enabled(terms) => { + if terms.active { + return Err(ContractError::RagequitAlreadyActive {}) + } + terms + }, + }; + + let pool_address = POOL_ADDRESS.load(deps.storage)?; + + // We query the pool to get the contract for the pool info + // The pool info is required to fetch the address of the + // liquidity token contract. The liquidity tokens are CW20 tokens + let pair_info: astroport::asset::PairInfo = deps + .querier + .query_wasm_smart(pool_address.to_string(), &astroport::pair::QueryMsg::Pair {})?; + + // We query our own liquidity token balance + let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( + pair_info.clone().liquidity_token, + &cw20::Cw20QueryMsg::Balance { + address: env.contract.address.to_string(), + }, + )?; + + // if no lp tokens are available, no point to ragequit + if liquidity_token_balance.amount.amount.is_zero() { + return Err(ContractError::NoLpTokensAvailable {}) + } + + // activate the ragequit in terms + rq_terms.active = true; + + // apply the ragequit penalty + let parties = parties.apply_ragequit_penalty(rq_party, rq_terms.penalty)?; + + + + Ok(Response::default()) +} + +fn try_claim( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + + Ok(Response::default()) +} + +fn try_tick( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.load(deps.storage)?)?), + QueryMsg::RagequitConfig {} => Ok(to_binary(&RAGEQUIT_CONFIG.load(deps.storage)?)?), + QueryMsg::LockupConfig {} => Ok(to_binary(&LOCKUP_CONFIG.load(deps.storage)?)?), + QueryMsg::PartiesConfig {} => Ok(to_binary(&PARTIES_CONFIG.load(deps.storage)?)?), + QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.load(deps.storage)?)?), + QueryMsg::NextContract {} => Ok(to_binary(&NEXT_CONTRACT.load(deps.storage)?)?), + } } \ No newline at end of file diff --git a/contracts/two-party-pol-holder/src/error.rs b/contracts/two-party-pol-holder/src/error.rs index b2a288c4..93169b88 100644 --- a/contracts/two-party-pol-holder/src/error.rs +++ b/contracts/two-party-pol-holder/src/error.rs @@ -15,4 +15,19 @@ pub enum ContractError { #[error("shares of covenant parties must add up to 1.0")] InvolvedPartiesConfigError {}, + + #[error("ragequit is disabled")] + RagequitDisabled {}, + + #[error("only covenant parties can initiate ragequit")] + RagequitUnauthorized {}, + + #[error("ragequit attempt with lockup period passed")] + RagequitWithLockupPassed {}, + + #[error("ragequit already active")] + RagequitAlreadyActive {}, + + #[error("no lp tokens available")] + NoLpTokensAvailable {}, } \ No newline at end of file diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index 3dab099e..a2305c1d 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Decimal, Timestamp, BlockInfo, Attribute}; -use covenant_macros::{clocked, covenant_deposit_address, covenant_clock_address, covenant_next_contract}; +use cosmwasm_std::{Addr, Decimal, Timestamp, BlockInfo, Attribute, OverflowError}; +use covenant_macros::{clocked, covenant_clock_address, covenant_next_contract}; use crate::error::ContractError; @@ -58,7 +58,6 @@ pub enum ContractState { Complete, } -#[covenant_deposit_address] #[covenant_clock_address] #[covenant_next_contract] #[cw_serde] @@ -84,13 +83,44 @@ pub struct PartiesConfig { impl PartiesConfig { /// validates the decimal shares of parties involved /// that must add up to 1.0 - pub fn validate(&self) -> Result<&PartiesConfig, ContractError> { + pub fn validate_config(&self) -> Result<&PartiesConfig, ContractError> { if self.party_a.share + self.party_b.share == Decimal::one() { Ok(self) } else { Err(ContractError::InvolvedPartiesConfigError {}) } } + + /// validates the caller and returns an error if caller is unauthorized, + /// or the calling party if its authorized + pub fn validate_caller(&self, caller: Addr) -> Result { + let a = self.clone().party_a; + let b = self.clone().party_b; + if a.addr == caller { + Ok(a) + } else if b.addr == caller { + Ok(b) + } else { + Err(ContractError::RagequitUnauthorized {}) + } + } + + /// subtracts the ragequit penalty to the ragequitting party + /// and adds it to the other party + pub fn apply_ragequit_penalty( + mut self, + rq_party: Party, + penalty: Decimal + ) -> Result { + if rq_party.addr == self.party_a.addr { + self.party_a.share -= penalty; + self.party_b.share += penalty; + } else { + self.party_a.share += penalty; + self.party_b.share -= penalty; + } + Ok(self) + } } impl PartiesConfig { From 48a6e1e14c072596818ae7913d65d973e86fcf00 Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 23 Aug 2023 14:52:53 +0200 Subject: [PATCH 097/586] rq continued --- .../two-party-pol-holder/src/contract.rs | 37 ++++++++++++++++--- contracts/two-party-pol-holder/src/error.rs | 6 +++ contracts/two-party-pol-holder/src/msg.rs | 10 +++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index 51977d0d..39d5e7f4 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -1,8 +1,10 @@ -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, BalanceResponse}; +use astroport::pair::Cw20HookMsg; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, StdError, OverflowError, CosmosMsg, WasmMsg}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cw2::set_contract_version; +use cw20::{Cw20ExecuteMsg, BalanceResponse}; use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, RagequitConfig}, state::{POOL_ADDRESS, NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, PARTIES_CONFIG, CONTRACT_STATE}, error::ContractError}; @@ -96,7 +98,7 @@ fn try_ragequit( )?; // if no lp tokens are available, no point to ragequit - if liquidity_token_balance.amount.amount.is_zero() { + if liquidity_token_balance.balance.is_zero() { return Err(ContractError::NoLpTokensAvailable {}) } @@ -104,11 +106,36 @@ fn try_ragequit( rq_terms.active = true; // apply the ragequit penalty - let parties = parties.apply_ragequit_penalty(rq_party, rq_terms.penalty)?; - + let parties = parties.apply_ragequit_penalty(rq_party.clone(), rq_terms.penalty)?; + let rq_party = parties.get_party_by_addr(rq_party.addr)?; + + // generate the withdraw_liquidity hook for the ragequitting party + let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; + let withdraw_msg = &Cw20ExecuteMsg::Send { + contract: pool_address.to_string(), + // take the ragequitting party share of the position + amount: liquidity_token_balance.balance.checked_mul_floor(rq_party.share) + .map_err(|_| ContractError::FractionMulError {})?, + msg: to_binary(withdraw_liquidity_hook)?, + }; + // update the state to reflect ragequit + CONTRACT_STATE.save(deps.storage, &crate::msg::ContractState::Ragequit)?; - Ok(Response::default()) + // TODO: need some kind of state representation of pending withdrawals + // to distinguish allocations of ragequitting party from the non-rq party + + Ok(Response::default() + .add_attribute("method", "ragequit") + .add_attribute("caller", rq_party.addr) + .add_message( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: pair_info.liquidity_token.to_string(), + msg: to_binary(withdraw_msg)?, + funds: vec![], + }) + ) + ) } fn try_claim( diff --git a/contracts/two-party-pol-holder/src/error.rs b/contracts/two-party-pol-holder/src/error.rs index 93169b88..d5417624 100644 --- a/contracts/two-party-pol-holder/src/error.rs +++ b/contracts/two-party-pol-holder/src/error.rs @@ -7,6 +7,9 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("failed to multiply amount by share")] + FractionMulError {}, + #[error("expiry block is already past")] InvalidExpiryBlockHeight {}, @@ -16,6 +19,9 @@ pub enum ContractError { #[error("shares of covenant parties must add up to 1.0")] InvolvedPartiesConfigError {}, + #[error("unknown party")] + PartyNotFound {}, + #[error("ragequit is disabled")] RagequitDisabled {}, diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index a2305c1d..4bb91356 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -121,6 +121,16 @@ impl PartiesConfig { } Ok(self) } + + pub fn get_party_by_addr(self, addr: Addr) -> Result { + if self.party_a.addr == addr { + Ok(self.party_a) + } else if self.party_b.addr == addr { + Ok(self.party_b) + } else { + Err(ContractError::PartyNotFound {}) + } + } } impl PartiesConfig { From 3a6f26a8d46ac7c592d58339ec01d2444a8d044f Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 19 Sep 2023 21:42:57 +0200 Subject: [PATCH 098/586] two party holder README update, adding deposit expiration --- Cargo.toml | 2 +- contracts/two-party-pol-holder/README.md | 44 ++++-- .../two-party-pol-holder/src/contract.rs | 132 ++++++++++++++++-- contracts/two-party-pol-holder/src/error.rs | 3 + contracts/two-party-pol-holder/src/msg.rs | 18 ++- contracts/two-party-pol-holder/src/state.rs | 4 +- packages/covenant-utils/src/lib.rs | 8 +- 7 files changed, 186 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 25f740d7..fa58069d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,6 @@ covenant-lp = { path = "contracts/lper" } covenant-clock = { path = "contracts/clock" } covenant-clock-tester = { path = "contracts/clock-tester" } covenant-ls = { path = "contracts/ls" } -# covenant-covenant = { path = "contracts/covenant" } covenant-holder = { path = "contracts/holder" } covenant-ibc-forwarder = { path = "contracts/ibc-forwarder" } covenant-native-splitter = { path = "contracts/native-splitter" } @@ -38,6 +37,7 @@ covenant-interchain-splitter = { path = "contracts/interchain-splitter" } covenant-swap-holder = { path = "contracts/swap-holder" } swap-covenant = { path = "contracts/swap-covenant" } covenant-interchain-router = { path = "contracts/interchain-router" } +covenant-two-party-pol-holder = { path = "contracts/two-party-pol-holder" } # packages clock-derive = { path = "packages/clock-derive" } diff --git a/contracts/two-party-pol-holder/README.md b/contracts/two-party-pol-holder/README.md index 8abf8e15..a57fbb44 100644 --- a/contracts/two-party-pol-holder/README.md +++ b/contracts/two-party-pol-holder/README.md @@ -1,10 +1,12 @@ # Two party POL holder -## Multiple parties +## Responsibilities + +### Multiple parties Multiple parties are going to be participating, so the holder should store a list of whitelisted addresses. -## Lock Period +### Lock Period A `Lock` duration should be stored to keep track of the covenant duration. @@ -12,17 +14,43 @@ After the `Lock` period expires, holder should withdraw the liquidity and forwar Splitter should be instantiated on demand, when the lock expires. -## Ragequit +### Ragequit A ragequit functionality should be enabled for both parties that may wish to break their part of the covenant. Ragequitting party is subject to a percentage based penalty agreed upon instantiation. Holder then withdraws the allocation of the ragequitting party (minus the penalty) and forwards the funds to the party. +Counterparty remains in an active position. -The other party may remain in their position for as long as they wish to, but not any longer than the initial duration. -Alternatively, they may also exit their position without any penalty because the covenant is no longer valid. -In both cases, the non-ragequitting party receives their allocation plus the penalties deducted from the ragequitting party. - -## Updates +### Updates Both parties are free to update their respective whitelisted addresses and do not need counterparty permission to do so. + +### Deposit funds to Liquid Pooler + +Both parties should deposit their funds to holder. After holder asserts the expected balances, it forwards +the funds to the Liquid Pooler which then in turn enters into a position. + +If party A delivers their part of the covenant deposit agreement but party B fails, party A is refunded. + +## Flow + +After instantiation, holder sits in `Instantiated` state and awaits for both parties to deposit funds. + +- Once both deposits are received, holder forwards the funds to the next contract and advances the state to `Active`. +- If one of the parties do deposit their part of the funds, but their counterparty does not, refund is initiated. This happens by sending the deposited funds to the respective interchain-router which then takes care of the rest. + +`Active` state is a prerequisite for initiating a `Ragequit`. In case of a ragequit, usual covenant flow is broken: + +- The initiating party forfeits part of its funds to the other party. +- After withdrawing the ragequitting party funds, holder forwards them to the respective interchain-router contract. +- Other party is no longer subject to the notion of expiry date. + - It is free to submit a `Claim` which will remove the remaining liquidity and send the underlying funds to the interchain-router. + +After holder no longer manages any funds, it advances its state to `Complete`. + +Any ticks received while holder is `Active` will trigger a check for expiration. + +If covenant is expired, holder state is advanced to `Expired`. +Both parties are free to submit `Claim` messages to the holder. + diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index 39d5e7f4..0e80f03f 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -1,12 +1,12 @@ use astroport::pair::Cw20HookMsg; -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, StdError, OverflowError, CosmosMsg, WasmMsg}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, CosmosMsg, WasmMsg, BankMsg}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cw2::set_contract_version; use cw20::{Cw20ExecuteMsg, BalanceResponse}; -use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, RagequitConfig}, state::{POOL_ADDRESS, NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, PARTIES_CONFIG, CONTRACT_STATE}, error::ContractError}; +use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, RagequitConfig, LockupConfig, ContractState}, state::{POOL_ADDRESS, NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, PARTIES_CONFIG, CONTRACT_STATE, DEPOSIT_DEADLINE, COVENANT_TERMS}, error::ContractError}; const CONTRACT_NAME: &str = "crates.io:covenant-two-party-pol-holder"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -26,7 +26,16 @@ pub fn instantiate( let clock_addr = deps.api.addr_validate(&msg.clock_address)?; let parties_config = msg.parties_config.validate_config()?; - let lockup_config = msg.lockup_config.validate(env.block)?; + let lockup_config = msg.lockup_config.validate(&env.block)?; + match msg.deposit_deadline.clone() { + Some(deadline) => { + let validated_deadline = deadline.validate(&env.block)?; + DEPOSIT_DEADLINE.save(deps.storage, validated_deadline)?; + }, + None => { + DEPOSIT_DEADLINE.save(deps.storage, &LockupConfig::None)?; + } + } POOL_ADDRESS.save(deps.storage, &pool_addr)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; @@ -34,8 +43,10 @@ pub fn instantiate( LOCKUP_CONFIG.save(deps.storage, lockup_config)?; RAGEQUIT_CONFIG.save(deps.storage, &msg.ragequit_config)?; PARTIES_CONFIG.save(deps.storage, parties_config)?; + COVENANT_TERMS.save(deps.storage, &msg.covenant_terms)?; Ok(Response::default() + .add_attribute("method", "two_party_pol_holder_instantiate") .add_attributes(msg.get_response_attributes()) ) } @@ -54,6 +65,113 @@ pub fn execute( } } +fn try_tick( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + let state = CONTRACT_STATE.load(deps.storage)?; + match state { + ContractState::Instantiated => try_deposit(deps, env, info), + ContractState::Active => check_expiration(deps, env, info), + ContractState::Ragequit => todo!(), + ContractState::Expired => todo!(), + ContractState::Complete => Ok(Response::default().add_attribute("contract_state", "complete")), + } +} + +fn try_deposit( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + + let parties = PARTIES_CONFIG.load(deps.storage)?; + let terms = COVENANT_TERMS.load(deps.storage)?; + + // assert the balances + let party_a_bal = deps.querier.query_balance(env.contract.address.to_string(), parties.party_a.provided_denom)?; + let party_b_bal = deps.querier.query_balance(env.contract.address.to_string(), parties.party_b.provided_denom)?; + + if terms.party_a_amount < party_a_bal.amount || terms.party_b_amount < party_b_bal.amount { + return Err(ContractError::InsufficientDeposits {}) + } + + // LiquidPooler is the next contract + let next_contract = NEXT_CONTRACT.load(deps.storage)?; + let msg = BankMsg::Send { + to_address: next_contract.to_string(), + amount: vec![party_a_bal, party_b_bal], + }; + + // advance the state to Active + CONTRACT_STATE.save(deps.storage, &ContractState::Active)?; + + Ok(Response::default() + .add_attribute("method", "deposit_to_next_contract") + .add_message(msg) + ) +} + +fn check_expiration( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + + let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; + + if !lockup_config.is_due(env.block) { + return Ok(Response::default() + .add_attribute("method", "check_expiration") + .add_attribute("result", "not_due") + ) + } + + let pool_address = POOL_ADDRESS.load(deps.storage)?; + + // We query the pool to get the contract for the pool info + // The pool info is required to fetch the address of the + // liquidity token contract. The liquidity tokens are CW20 tokens + let pair_info: astroport::asset::PairInfo = deps.querier.query_wasm_smart( + pool_address.to_string(), + &astroport::pair::QueryMsg::Pair {}, + )?; + + // We query our own liquidity token balance + let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( + pair_info.clone().liquidity_token, + &cw20::Cw20QueryMsg::Balance { + address: env.contract.address.to_string(), + }, + )?; + + // We withdraw our liquidity constructing a CW20 send message + // The message contains our liquidity token balance + // The pool address and a message to call the withdraw liquidity hook of the pool contract + let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; + let withdraw_msg = &Cw20ExecuteMsg::Send { + contract: pool_address.to_string(), + amount: liquidity_token_balance.balance, + msg: to_binary(withdraw_liquidity_hook)?, + }; + + // advance state to Expired + CONTRACT_STATE.save(deps.storage, &ContractState::Expired)?; + + // We execute the message on the liquidity token contract + // This will burn the LP tokens and withdraw liquidity into the holder + Ok(Response::default() + .add_attribute("method", "check_expiration") + .add_attribute("result", "expired") + .add_attribute("lp_token_amount", liquidity_token_balance.balance) + .add_message(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: pair_info.liquidity_token.to_string(), + msg: to_binary(withdraw_msg)?, + funds: vec![], + }))) +} + fn try_ragequit( deps: DepsMut, env: Env, @@ -147,14 +265,6 @@ fn try_claim( Ok(Response::default()) } -fn try_tick( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { - - Ok(Response::default()) -} #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { diff --git a/contracts/two-party-pol-holder/src/error.rs b/contracts/two-party-pol-holder/src/error.rs index d5417624..98abdca3 100644 --- a/contracts/two-party-pol-holder/src/error.rs +++ b/contracts/two-party-pol-holder/src/error.rs @@ -7,6 +7,9 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("both parties have not deposited")] + InsufficientDeposits {}, + #[error("failed to multiply amount by share")] FractionMulError {}, diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index 4bb91356..db96e98b 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -1,6 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Decimal, Timestamp, BlockInfo, Attribute, OverflowError}; +use cosmwasm_std::{Addr, Decimal, Timestamp, BlockInfo, Attribute}; use covenant_macros::{clocked, covenant_clock_address, covenant_next_contract}; +use covenant_utils::PolCovenantTerms; use crate::error::ContractError; @@ -21,6 +22,11 @@ pub struct InstantiateMsg { pub ragequit_config: RagequitConfig, /// parties engaged in the POL. pub parties_config: PartiesConfig, + /// deadline for both parties to deposit the funds + /// TODO: rename LockupConfig to something more generic + /// to represent block/time based expiration + pub deposit_deadline: Option, + pub covenant_terms: PolCovenantTerms, } impl InstantiateMsg { @@ -48,12 +54,18 @@ pub enum ExecuteMsg { #[cw_serde] pub enum ContractState { + /// contract is instantiated and awaiting for deposits from + /// both parties involved Instantiated, + /// funds have been forwarded to the LP module. from the perspective + /// of this contract that indicates an active LP position. + /// TODO: think about whether this is a fair assumption to make. + Active, /// one of the parties have initiated ragequit. /// party with an active position is free to exit at any time. Ragequit, /// covenant has reached its expiration date. - ExpirationReached, + Expired, /// underlying funds have been withdrawn. Complete, } @@ -222,7 +234,7 @@ impl LockupConfig { } /// validates that the lockup config being stored is not already expired. - pub fn validate(&self, block_info: BlockInfo) -> Result<&LockupConfig, ContractError> { + pub fn validate(&self, block_info: &BlockInfo) -> Result<&LockupConfig, ContractError> { match self { LockupConfig::None => Ok(self), LockupConfig::Block(h) => { diff --git a/contracts/two-party-pol-holder/src/state.rs b/contracts/two-party-pol-holder/src/state.rs index 9fc2043b..2214eb6e 100644 --- a/contracts/two-party-pol-holder/src/state.rs +++ b/contracts/two-party-pol-holder/src/state.rs @@ -1,4 +1,5 @@ use cosmwasm_std::Addr; +use covenant_utils::PolCovenantTerms; use cw_storage_plus::Item; use crate::msg::{ContractState, LockupConfig, RagequitConfig, PartiesConfig}; @@ -11,4 +12,5 @@ pub const PARTIES_CONFIG: Item = Item::new("parties_config"); pub const LOCKUP_CONFIG: Item = Item::new("lockup_config"); pub const RAGEQUIT_CONFIG: Item = Item::new("ragequit_config"); pub const POOL_ADDRESS: Item = Item::new("pool_address"); - +pub const DEPOSIT_DEADLINE: Item = Item::new("deposit_deadline"); +pub const COVENANT_TERMS: Item = Item::new("covenant_terms"); \ No newline at end of file diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index 194d206d..b4be2908 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -338,6 +338,12 @@ pub struct SwapCovenantTerms { pub party_b_amount: Uint128, } +#[cw_serde] +pub struct PolCovenantTerms { + pub party_a_amount: Uint128, + pub party_b_amount: Uint128, +} + impl CovenantTerms { pub fn get_response_attributes(self) -> Vec { match self { @@ -348,7 +354,7 @@ impl CovenantTerms { Attribute::new("party_b_amount", terms.party_b_amount), ]; attrs - } + }, } } } From 0854dab7ee5f436407086a908aefdfd67113712a Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 20 Sep 2023 13:25:21 +0200 Subject: [PATCH 099/586] test suite --- Cargo.lock | 2 + contracts/swap-holder/src/contract.rs | 2 +- contracts/two-party-pol-holder/Cargo.toml | 5 + .../two-party-pol-holder/src/contract.rs | 65 +++---- contracts/two-party-pol-holder/src/error.rs | 3 + contracts/two-party-pol-holder/src/lib.rs | 3 + contracts/two-party-pol-holder/src/msg.rs | 116 ++++++------ contracts/two-party-pol-holder/src/state.rs | 6 +- .../src/suite_tests/mod.rs | 40 +++++ .../src/suite_tests/suite.rs | 165 ++++++++++++++++++ .../src/suite_tests/tests.rs | 7 + packages/covenant-utils/src/lib.rs | 14 +- 12 files changed, 333 insertions(+), 95 deletions(-) create mode 100644 contracts/two-party-pol-holder/src/suite_tests/mod.rs create mode 100644 contracts/two-party-pol-holder/src/suite_tests/suite.rs create mode 100644 contracts/two-party-pol-holder/src/suite_tests/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 6cccedfe..232bf3b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -630,6 +630,7 @@ dependencies = [ name = "covenant-two-party-pol-holder" version = "1.0.0" dependencies = [ + "anyhow", "astroport 2.8.0", "cosmos-sdk-proto 0.14.0", "cosmwasm-schema", @@ -637,6 +638,7 @@ dependencies = [ "covenant-clock", "covenant-macros", "covenant-utils", + "cw-multi-test", "cw-storage-plus 1.1.0", "cw2 1.1.1", "cw20 0.15.1", diff --git a/contracts/swap-holder/src/contract.rs b/contracts/swap-holder/src/contract.rs index e0d16d36..50a972f9 100644 --- a/contracts/swap-holder/src/contract.rs +++ b/contracts/swap-holder/src/contract.rs @@ -33,7 +33,7 @@ pub fn instantiate( let next_contract = deps.api.addr_validate(&msg.next_contract)?; let clock_addr = deps.api.addr_validate(&msg.clock_address)?; - msg.lockup_config.validate(env.block)?; + msg.lockup_config.validate(&env.block)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; diff --git a/contracts/two-party-pol-holder/Cargo.toml b/contracts/two-party-pol-holder/Cargo.toml index 022393b0..0fd81010 100644 --- a/contracts/two-party-pol-holder/Cargo.toml +++ b/contracts/two-party-pol-holder/Cargo.toml @@ -33,3 +33,8 @@ cosmos-sdk-proto = { workspace = true } protobuf = { workspace = true } astroport = "2.8.0" cw20 = { version = "0.15" } + + +[dev-dependencies] +cw-multi-test = { workspace = true } +anyhow = { workspace = true } diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index 0e80f03f..6a1f8486 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -3,10 +3,11 @@ use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; +use covenant_utils::LockupConfig; use cw2::set_contract_version; use cw20::{Cw20ExecuteMsg, BalanceResponse}; -use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, RagequitConfig, LockupConfig, ContractState}, state::{POOL_ADDRESS, NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, PARTIES_CONFIG, CONTRACT_STATE, DEPOSIT_DEADLINE, COVENANT_TERMS}, error::ContractError}; +use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, RagequitConfig, ContractState}, state::{POOL_ADDRESS, NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, PARTIES_CONFIG, CONTRACT_STATE, DEPOSIT_DEADLINE, COVENANT_TERMS}, error::ContractError}; const CONTRACT_NAME: &str = "crates.io:covenant-two-party-pol-holder"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -25,12 +26,12 @@ pub fn instantiate( let next_contract = deps.api.addr_validate(&msg.next_contract)?; let clock_addr = deps.api.addr_validate(&msg.clock_address)?; - let parties_config = msg.parties_config.validate_config()?; - let lockup_config = msg.lockup_config.validate(&env.block)?; + // let parties_config = msg.parties_config.validate_config()?; + msg.lockup_config.validate(&env.block)?; match msg.deposit_deadline.clone() { Some(deadline) => { - let validated_deadline = deadline.validate(&env.block)?; - DEPOSIT_DEADLINE.save(deps.storage, validated_deadline)?; + deadline.validate(&env.block)?; + DEPOSIT_DEADLINE.save(deps.storage, &deadline)?; }, None => { DEPOSIT_DEADLINE.save(deps.storage, &LockupConfig::None)?; @@ -40,9 +41,9 @@ pub fn instantiate( POOL_ADDRESS.save(deps.storage, &pool_addr)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; - LOCKUP_CONFIG.save(deps.storage, lockup_config)?; + LOCKUP_CONFIG.save(deps.storage, &msg.lockup_config)?; RAGEQUIT_CONFIG.save(deps.storage, &msg.ragequit_config)?; - PARTIES_CONFIG.save(deps.storage, parties_config)?; + PARTIES_CONFIG.save(deps.storage, &msg.parties_config)?; COVENANT_TERMS.save(deps.storage, &msg.covenant_terms)?; Ok(Response::default() @@ -90,8 +91,8 @@ fn try_deposit( let terms = COVENANT_TERMS.load(deps.storage)?; // assert the balances - let party_a_bal = deps.querier.query_balance(env.contract.address.to_string(), parties.party_a.provided_denom)?; - let party_b_bal = deps.querier.query_balance(env.contract.address.to_string(), parties.party_b.provided_denom)?; + let party_a_bal = deps.querier.query_balance(env.contract.address.to_string(), parties.party_a.ibc_denom)?; + let party_b_bal = deps.querier.query_balance(env.contract.address.to_string(), parties.party_b.ibc_denom)?; if terms.party_a_amount < party_a_bal.amount || terms.party_b_amount < party_b_bal.amount { return Err(ContractError::InsufficientDeposits {}) @@ -121,7 +122,7 @@ fn check_expiration( let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; - if !lockup_config.is_due(env.block) { + if !lockup_config.is_expired(env.block) { return Ok(Response::default() .add_attribute("method", "check_expiration") .add_attribute("result", "not_due") @@ -179,13 +180,13 @@ fn try_ragequit( ) -> Result { // if lockup period had passed, just claim the tokens instead of ragequitting let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; - if lockup_config.is_due(env.block) { + if lockup_config.is_expired(env.block) { return Err(ContractError::RagequitWithLockupPassed {}) } // only the involved parties can initiate the ragequit let parties = PARTIES_CONFIG.load(deps.storage)?; - let rq_party = parties.validate_caller(info.sender)?; + let rq_party = parties.match_caller_party(info.sender.to_string())?; let mut rq_terms = match RAGEQUIT_CONFIG.load(deps.storage)? { // if ragequit is not enabled for this covenant we error @@ -224,21 +225,21 @@ fn try_ragequit( rq_terms.active = true; // apply the ragequit penalty - let parties = parties.apply_ragequit_penalty(rq_party.clone(), rq_terms.penalty)?; - let rq_party = parties.get_party_by_addr(rq_party.addr)?; + // TODO: let parties = parties.apply_ragequit_penalty(rq_party.clone(), rq_terms.penalty)?; + // let rq_party = parties.get_party_by_addr(rq_party.addr)?; // generate the withdraw_liquidity hook for the ragequitting party - let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; - let withdraw_msg = &Cw20ExecuteMsg::Send { - contract: pool_address.to_string(), - // take the ragequitting party share of the position - amount: liquidity_token_balance.balance.checked_mul_floor(rq_party.share) - .map_err(|_| ContractError::FractionMulError {})?, - msg: to_binary(withdraw_liquidity_hook)?, - }; - - // update the state to reflect ragequit - CONTRACT_STATE.save(deps.storage, &crate::msg::ContractState::Ragequit)?; + // let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; + // let withdraw_msg = &Cw20ExecuteMsg::Send { + // contract: pool_address.to_string(), + // // take the ragequitting party share of the position + // amount: liquidity_token_balance.balance.checked_mul_floor(rq_party.share) + // .map_err(|_| ContractError::FractionMulError {})?, + // msg: to_binary(withdraw_liquidity_hook)?, + // }; + + // // update the state to reflect ragequit + // CONTRACT_STATE.save(deps.storage, &crate::msg::ContractState::Ragequit)?; // TODO: need some kind of state representation of pending withdrawals // to distinguish allocations of ragequitting party from the non-rq party @@ -246,13 +247,13 @@ fn try_ragequit( Ok(Response::default() .add_attribute("method", "ragequit") .add_attribute("caller", rq_party.addr) - .add_message( - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: pair_info.liquidity_token.to_string(), - msg: to_binary(withdraw_msg)?, - funds: vec![], - }) - ) + // .add_message( + // CosmosMsg::Wasm(WasmMsg::Execute { + // contract_addr: pair_info.liquidity_token.to_string(), + // msg: to_binary(withdraw_msg)?, + // funds: vec![], + // }) + // ) ) } diff --git a/contracts/two-party-pol-holder/src/error.rs b/contracts/two-party-pol-holder/src/error.rs index 98abdca3..e8f4669f 100644 --- a/contracts/two-party-pol-holder/src/error.rs +++ b/contracts/two-party-pol-holder/src/error.rs @@ -7,6 +7,9 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("unauthorized")] + Unauthorized {}, + #[error("both parties have not deposited")] InsufficientDeposits {}, diff --git a/contracts/two-party-pol-holder/src/lib.rs b/contracts/two-party-pol-holder/src/lib.rs index 0faea8f4..189cbd8b 100644 --- a/contracts/two-party-pol-holder/src/lib.rs +++ b/contracts/two-party-pol-holder/src/lib.rs @@ -6,3 +6,6 @@ pub mod contract; pub mod error; pub mod msg; pub mod state; + +#[cfg(test)] +mod suite_tests; diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index db96e98b..abe74bee 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Decimal, Timestamp, BlockInfo, Attribute}; +use cosmwasm_std::{Addr, Decimal, Attribute}; use covenant_macros::{clocked, covenant_clock_address, covenant_next_contract}; -use covenant_utils::PolCovenantTerms; +use covenant_utils::{PolCovenantTerms, CovenantPartiesConfig, LockupConfig}; use crate::error::ContractError; @@ -21,7 +21,7 @@ pub struct InstantiateMsg { /// configuration for ragequit pub ragequit_config: RagequitConfig, /// parties engaged in the POL. - pub parties_config: PartiesConfig, + pub parties_config: CovenantPartiesConfig, /// deadline for both parties to deposit the funds /// TODO: rename LockupConfig to something more generic /// to represent block/time based expiration @@ -207,61 +207,61 @@ pub struct RagequitTerms { pub active: bool, } -/// enum based configuration of the lockup period. -#[cw_serde] -pub enum LockupConfig { - /// no lockup configured - None, - /// block height based lockup config - Block(u64), - /// timestamp based lockup config - Time(Timestamp), -} +// / enum based configuration of the lockup period. +// #[cw_serde] +// pub enum LockupConfig { +// /// no lockup configured +// None, +// /// block height based lockup config +// Block(u64), +// /// timestamp based lockup config +// Time(Timestamp), +// } -impl LockupConfig { - pub fn get_response_attributes(self) -> Vec { - match self { - LockupConfig::None => vec![ - Attribute::new("lockup_config", "none"), - ], - LockupConfig::Block(h) => vec![ - Attribute::new("lockup_config_expiry_block_height", h.to_string()), - ], - LockupConfig::Time(t) => vec![ - Attribute::new("lockup_config_expiry_block_timestamp", t.to_string()), - ], - } - } +// impl LockupConfig { +// pub fn get_response_attributes(self) -> Vec { +// match self { +// LockupConfig::None => vec![ +// Attribute::new("lockup_config", "none"), +// ], +// LockupConfig::Block(h) => vec![ +// Attribute::new("lockup_config_expiry_block_height", h.to_string()), +// ], +// LockupConfig::Time(t) => vec![ +// Attribute::new("lockup_config_expiry_block_timestamp", t.to_string()), +// ], +// } +// } - /// validates that the lockup config being stored is not already expired. - pub fn validate(&self, block_info: &BlockInfo) -> Result<&LockupConfig, ContractError> { - match self { - LockupConfig::None => Ok(self), - LockupConfig::Block(h) => { - if h > &block_info.height { - Ok(self) - } else { - Err(ContractError::LockupValidationError {}) - } - }, - LockupConfig::Time(t) => { - if t.nanos() > block_info.time.nanos() { - Ok(self) - } else { - Err(ContractError::LockupValidationError {}) - } - }, - } - } +// /// validates that the lockup config being stored is not already expired. +// pub fn validate(&self, block_info: &BlockInfo) -> Result<&LockupConfig, ContractError> { +// match self { +// LockupConfig::None => Ok(self), +// LockupConfig::Block(h) => { +// if h > &block_info.height { +// Ok(self) +// } else { +// Err(ContractError::LockupValidationError {}) +// } +// }, +// LockupConfig::Time(t) => { +// if t.nanos() > block_info.time.nanos() { +// Ok(self) +// } else { +// Err(ContractError::LockupValidationError {}) +// } +// }, +// } +// } - /// compares current block info with the stored lockup config. - /// returns false if no lockup configuration is stored. - /// otherwise, returns true if the current block is past the stored info. - pub fn is_due(self, block_info: BlockInfo) -> bool { - match self { - LockupConfig::None => false, // or.. true? should not be called - LockupConfig::Block(h) => h < block_info.height, - LockupConfig::Time(t) => t.nanos() < block_info.time.nanos(), - } - } -} +// /// compares current block info with the stored lockup config. +// /// returns false if no lockup configuration is stored. +// /// otherwise, returns true if the current block is past the stored info. +// pub fn is_due(self, block_info: BlockInfo) -> bool { +// match self { +// LockupConfig::None => false, // or.. true? should not be called +// LockupConfig::Block(h) => h < block_info.height, +// LockupConfig::Time(t) => t.nanos() < block_info.time.nanos(), +// } +// } +// } diff --git a/contracts/two-party-pol-holder/src/state.rs b/contracts/two-party-pol-holder/src/state.rs index 2214eb6e..ce433d54 100644 --- a/contracts/two-party-pol-holder/src/state.rs +++ b/contracts/two-party-pol-holder/src/state.rs @@ -1,14 +1,14 @@ use cosmwasm_std::Addr; -use covenant_utils::PolCovenantTerms; +use covenant_utils::{PolCovenantTerms, CovenantPartiesConfig, LockupConfig}; use cw_storage_plus::Item; -use crate::msg::{ContractState, LockupConfig, RagequitConfig, PartiesConfig}; +use crate::msg::{ContractState, RagequitConfig}; pub const CONTRACT_STATE: Item = Item::new("contract_state"); pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); pub const NEXT_CONTRACT: Item = Item::new("next_contract"); -pub const PARTIES_CONFIG: Item = Item::new("parties_config"); +pub const PARTIES_CONFIG: Item = Item::new("parties_config"); pub const LOCKUP_CONFIG: Item = Item::new("lockup_config"); pub const RAGEQUIT_CONFIG: Item = Item::new("ragequit_config"); pub const POOL_ADDRESS: Item = Item::new("pool_address"); diff --git a/contracts/two-party-pol-holder/src/suite_tests/mod.rs b/contracts/two-party-pol-holder/src/suite_tests/mod.rs new file mode 100644 index 00000000..107dd564 --- /dev/null +++ b/contracts/two-party-pol-holder/src/suite_tests/mod.rs @@ -0,0 +1,40 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{to_binary, Binary, Deps, Empty, Env, StdResult}; +use covenant_macros::covenant_deposit_address; +use cw_multi_test::{Contract, ContractWrapper}; + +mod suite; +mod tests; + +pub fn two_party_pol_holder_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ); + Box::new(contract) +} + +pub fn mock_deposit_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + query, + ); + Box::new(contract) +} + +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; + +#[covenant_deposit_address] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg {} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::DepositAddress {} => Ok(to_binary(&"splitter")?), + } +} diff --git a/contracts/two-party-pol-holder/src/suite_tests/suite.rs b/contracts/two-party-pol-holder/src/suite_tests/suite.rs new file mode 100644 index 00000000..6c9f9c08 --- /dev/null +++ b/contracts/two-party-pol-holder/src/suite_tests/suite.rs @@ -0,0 +1,165 @@ +use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg}; +use cosmwasm_std::{Addr, Coin, Uint128}; +use covenant_utils::{ + CovenantPartiesConfig, CovenantParty, LockupConfig, PolCovenantTerms, +}; +use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; + +use super::{mock_deposit_contract, two_party_pol_holder_contract}; + +pub const ADMIN: &str = "admin"; + +pub const DENOM_A: &str = "denom_a"; +pub const DENOM_B: &str = "denom_b"; + +pub const PARTY_A_ADDR: &str = "party_a"; +pub const PARTY_B_ADDR: &str = "party_b"; + +pub const CLOCK_ADDR: &str = "clock_address"; +pub const NEXT_CONTRACT: &str = "next_contract"; + +pub const INITIAL_BLOCK_HEIGHT: u64 = 12345; +pub const INITIAL_BLOCK_NANOS: u64 = 1571797419879305533; + +pub struct Suite { + pub app: App, + pub holder: Addr, + pub mock_deposit: Addr, + pub party_a: CovenantParty, + pub party_b: CovenantParty, +} + +pub struct SuiteBuilder { + pub instantiate: InstantiateMsg, + pub app: App, +} + +impl Default for SuiteBuilder { + fn default() -> Self { + Self { + instantiate: InstantiateMsg { + pool_address: todo!(), + ragequit_config: todo!(), + deposit_deadline: None, + clock_address: CLOCK_ADDR.to_string(), + next_contract: NEXT_CONTRACT.to_string(), + lockup_config: LockupConfig::None, + parties_config: todo!(), + covenant_terms: PolCovenantTerms { + party_a_amount: Uint128::new(400), + party_b_amount: Uint128::new(20), + }, + }, + app: App::default(), + } + } +} + +impl SuiteBuilder { + pub fn with_lockup_config(mut self, config: LockupConfig) -> Self { + self.instantiate.lockup_config = config; + self + } + + pub fn build(mut self) -> Suite { + let mut app = self.app; + let holder_code = app.store_code(two_party_pol_holder_contract()); + let mock_deposit_code = app.store_code(mock_deposit_contract()); + + let mock_deposit = app + .instantiate_contract( + mock_deposit_code, + Addr::unchecked(ADMIN), + &self.instantiate, + &[], + "holder", + Some(ADMIN.to_string()), + ) + .unwrap(); + + self.instantiate.next_contract = mock_deposit.to_string(); + + let holder = app + .instantiate_contract( + holder_code, + Addr::unchecked(ADMIN), + &self.instantiate, + &[], + "holder", + Some(ADMIN.to_string()), + ) + .unwrap(); + + Suite { + app, + holder, + mock_deposit, + party_a: self.instantiate.parties_config.party_a, + party_b: self.instantiate.parties_config.party_b, + } + } +} + +// actions +impl Suite { + pub fn tick(&mut self, caller: &str) -> Result { + self.app.execute_contract( + Addr::unchecked(caller), + self.holder.clone(), + &ExecuteMsg::Tick {}, + &[], + ) + } +} + +// queries +impl Suite { + pub fn query_next_contract(&self) -> Addr { + self.app + .wrap() + .query_wasm_smart(&self.holder, &QueryMsg::NextContract {}) + .unwrap() + } + + pub fn query_lockup_config(&self) -> LockupConfig { + self.app + .wrap() + .query_wasm_smart(&self.holder, &QueryMsg::LockupConfig {}) + .unwrap() + } + + pub fn query_clock_address(&self) -> Addr { + self.app + .wrap() + .query_wasm_smart(&self.holder, &QueryMsg::ClockAddress {}) + .unwrap() + } + + pub fn query_contract_state(&self) -> ContractState { + self.app + .wrap() + .query_wasm_smart(&self.holder, &QueryMsg::ContractState {}) + .unwrap() + } +} + +// helper +impl Suite { + pub fn pass_blocks(&mut self, n: u64) { + self.app.update_block(|mut b| b.height += n); + } + + pub fn pass_minutes(&mut self, n: u64) { + self.app + .update_block(|mut b| b.time = b.time.plus_minutes(n)); + } + + pub fn fund_coin(&mut self, coin: Coin) -> AppResponse { + self.app + .sudo(SudoMsg::Bank(cw_multi_test::BankSudo::Mint { + to_address: self.holder.to_string(), + amount: vec![coin], + })) + .unwrap() + } +} diff --git a/contracts/two-party-pol-holder/src/suite_tests/tests.rs b/contracts/two-party-pol-holder/src/suite_tests/tests.rs new file mode 100644 index 00000000..18e4bd1d --- /dev/null +++ b/contracts/two-party-pol-holder/src/suite_tests/tests.rs @@ -0,0 +1,7 @@ +use super::suite::SuiteBuilder; + +#[test] +fn test_instantiate_happy_and_query_all() { + let suite = SuiteBuilder::default().build(); + +} diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index b4be2908..d091a825 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -194,7 +194,7 @@ impl LockupConfig { } /// validates that the lockup config being stored is not already expired. - pub fn validate(&self, block_info: BlockInfo) -> Result<(), StdError> { + pub fn validate(&self, block_info: &BlockInfo) -> Result<(), StdError> { match self { LockupConfig::None => Ok(()), LockupConfig::Block(h) => { @@ -325,6 +325,18 @@ impl CovenantPartiesConfig { ); attrs } + + pub fn match_caller_party(&self, caller: String) -> Result { + let a = self.clone().party_a; + let b = self.clone().party_b; + if a.addr == caller { + Ok(a) + } else if b.addr == caller { + Ok(b) + } else { + Err(StdError::generic_err("unauthorized")) + } + } } #[cw_serde] From 011018a815ec1a4c452889ed3a690c0b0a7aa3b8 Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 20 Sep 2023 18:41:47 +0200 Subject: [PATCH 100/586] tests cleanup --- .../two-party-pol-holder/src/contract.rs | 432 +++++++++--------- contracts/two-party-pol-holder/src/msg.rs | 202 ++++---- contracts/two-party-pol-holder/src/state.rs | 11 +- .../src/suite_tests/suite.rs | 96 +++- .../src/suite_tests/tests.rs | 112 +++++ 5 files changed, 516 insertions(+), 337 deletions(-) diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index 6a1f8486..d71c0cb9 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -1,13 +1,11 @@ -use astroport::pair::Cw20HookMsg; -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, CosmosMsg, WasmMsg, BankMsg}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use covenant_utils::LockupConfig; use cw2::set_contract_version; -use cw20::{Cw20ExecuteMsg, BalanceResponse}; -use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, RagequitConfig, ContractState}, state::{POOL_ADDRESS, NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, PARTIES_CONFIG, CONTRACT_STATE, DEPOSIT_DEADLINE, COVENANT_TERMS}, error::ContractError}; +use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, CONTRACT_STATE, DEPOSIT_DEADLINE, POOL_ADDRESS, PARTY_A_ROUTER, PARTY_B_ROUTER}, error::ContractError}; const CONTRACT_NAME: &str = "crates.io:covenant-two-party-pol-holder"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -25,27 +23,27 @@ pub fn instantiate( let pool_addr = deps.api.addr_validate(&msg.pool_address)?; let next_contract = deps.api.addr_validate(&msg.next_contract)?; let clock_addr = deps.api.addr_validate(&msg.clock_address)?; + let party_a_router = deps.api.addr_validate(&msg.party_a_router)?; + let party_b_router = deps.api.addr_validate(&msg.party_b_router)?; - // let parties_config = msg.parties_config.validate_config()?; - msg.lockup_config.validate(&env.block)?; - match msg.deposit_deadline.clone() { + POOL_ADDRESS.save(deps.storage, &pool_addr)?; + NEXT_CONTRACT.save(deps.storage, &next_contract)?; + CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; + LOCKUP_CONFIG.save(deps.storage, &msg.lockup_config)?; + RAGEQUIT_CONFIG.save(deps.storage, &msg.ragequit_config)?; + PARTY_A_ROUTER.save(deps.storage, &party_a_router)?; + PARTY_B_ROUTER.save(deps.storage, &party_b_router)?; + + match &msg.deposit_deadline { Some(deadline) => { deadline.validate(&env.block)?; - DEPOSIT_DEADLINE.save(deps.storage, &deadline)?; + DEPOSIT_DEADLINE.save(deps.storage, deadline)?; }, None => { DEPOSIT_DEADLINE.save(deps.storage, &LockupConfig::None)?; } } - POOL_ADDRESS.save(deps.storage, &pool_addr)?; - NEXT_CONTRACT.save(deps.storage, &next_contract)?; - CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; - LOCKUP_CONFIG.save(deps.storage, &msg.lockup_config)?; - RAGEQUIT_CONFIG.save(deps.storage, &msg.ragequit_config)?; - PARTIES_CONFIG.save(deps.storage, &msg.parties_config)?; - COVENANT_TERMS.save(deps.storage, &msg.covenant_terms)?; - Ok(Response::default() .add_attribute("method", "two_party_pol_holder_instantiate") .add_attributes(msg.get_response_attributes()) @@ -59,212 +57,213 @@ pub fn execute( info: MessageInfo, msg: ExecuteMsg, ) -> Result { - match msg { - ExecuteMsg::Ragequit {} => try_ragequit(deps, env, info), - ExecuteMsg::Claim {} => try_claim(deps, env, info), - ExecuteMsg::Tick {} => try_tick(deps, env, info), - } -} - -fn try_tick( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { - let state = CONTRACT_STATE.load(deps.storage)?; - match state { - ContractState::Instantiated => try_deposit(deps, env, info), - ContractState::Active => check_expiration(deps, env, info), - ContractState::Ragequit => todo!(), - ContractState::Expired => todo!(), - ContractState::Complete => Ok(Response::default().add_attribute("contract_state", "complete")), - } + // match msg { + // ExecuteMsg::Ragequit {} => try_ragequit(deps, env, info), + // ExecuteMsg::Claim {} => try_claim(deps, env, info), + // ExecuteMsg::Tick {} => try_tick(deps, env, info), + // } + Ok(Response::default()) } -fn try_deposit( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { - - let parties = PARTIES_CONFIG.load(deps.storage)?; - let terms = COVENANT_TERMS.load(deps.storage)?; - - // assert the balances - let party_a_bal = deps.querier.query_balance(env.contract.address.to_string(), parties.party_a.ibc_denom)?; - let party_b_bal = deps.querier.query_balance(env.contract.address.to_string(), parties.party_b.ibc_denom)?; - - if terms.party_a_amount < party_a_bal.amount || terms.party_b_amount < party_b_bal.amount { - return Err(ContractError::InsufficientDeposits {}) - } +// fn try_tick( +// deps: DepsMut, +// env: Env, +// info: MessageInfo, +// ) -> Result { +// let state = CONTRACT_STATE.load(deps.storage)?; +// match state { +// ContractState::Instantiated => try_deposit(deps, env, info), +// ContractState::Active => check_expiration(deps, env, info), +// ContractState::Ragequit => todo!(), +// ContractState::Expired => todo!(), +// ContractState::Complete => Ok(Response::default().add_attribute("contract_state", "complete")), +// } +// } + +// fn try_deposit( +// deps: DepsMut, +// env: Env, +// info: MessageInfo, +// ) -> Result { + +// let parties = PARTIES_CONFIG.load(deps.storage)?; +// let terms = COVENANT_TERMS.load(deps.storage)?; + +// // assert the balances +// let party_a_bal = deps.querier.query_balance(env.contract.address.to_string(), parties.party_a.ibc_denom)?; +// let party_b_bal = deps.querier.query_balance(env.contract.address.to_string(), parties.party_b.ibc_denom)?; + +// if terms.party_a_amount < party_a_bal.amount || terms.party_b_amount < party_b_bal.amount { +// return Err(ContractError::InsufficientDeposits {}) +// } - // LiquidPooler is the next contract - let next_contract = NEXT_CONTRACT.load(deps.storage)?; - let msg = BankMsg::Send { - to_address: next_contract.to_string(), - amount: vec![party_a_bal, party_b_bal], - }; - - // advance the state to Active - CONTRACT_STATE.save(deps.storage, &ContractState::Active)?; - - Ok(Response::default() - .add_attribute("method", "deposit_to_next_contract") - .add_message(msg) - ) -} - -fn check_expiration( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { - - let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; - - if !lockup_config.is_expired(env.block) { - return Ok(Response::default() - .add_attribute("method", "check_expiration") - .add_attribute("result", "not_due") - ) - } - - let pool_address = POOL_ADDRESS.load(deps.storage)?; - - // We query the pool to get the contract for the pool info - // The pool info is required to fetch the address of the - // liquidity token contract. The liquidity tokens are CW20 tokens - let pair_info: astroport::asset::PairInfo = deps.querier.query_wasm_smart( - pool_address.to_string(), - &astroport::pair::QueryMsg::Pair {}, - )?; - - // We query our own liquidity token balance - let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( - pair_info.clone().liquidity_token, - &cw20::Cw20QueryMsg::Balance { - address: env.contract.address.to_string(), - }, - )?; - - // We withdraw our liquidity constructing a CW20 send message - // The message contains our liquidity token balance - // The pool address and a message to call the withdraw liquidity hook of the pool contract - let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; - let withdraw_msg = &Cw20ExecuteMsg::Send { - contract: pool_address.to_string(), - amount: liquidity_token_balance.balance, - msg: to_binary(withdraw_liquidity_hook)?, - }; - - // advance state to Expired - CONTRACT_STATE.save(deps.storage, &ContractState::Expired)?; - - // We execute the message on the liquidity token contract - // This will burn the LP tokens and withdraw liquidity into the holder - Ok(Response::default() - .add_attribute("method", "check_expiration") - .add_attribute("result", "expired") - .add_attribute("lp_token_amount", liquidity_token_balance.balance) - .add_message(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: pair_info.liquidity_token.to_string(), - msg: to_binary(withdraw_msg)?, - funds: vec![], - }))) -} - -fn try_ragequit( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { - // if lockup period had passed, just claim the tokens instead of ragequitting - let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; - if lockup_config.is_expired(env.block) { - return Err(ContractError::RagequitWithLockupPassed {}) - } +// // LiquidPooler is the next contract +// let next_contract = NEXT_CONTRACT.load(deps.storage)?; +// let msg = BankMsg::Send { +// to_address: next_contract.to_string(), +// amount: vec![party_a_bal, party_b_bal], +// }; + +// // advance the state to Active +// CONTRACT_STATE.save(deps.storage, &ContractState::Active)?; + +// Ok(Response::default() +// .add_attribute("method", "deposit_to_next_contract") +// .add_message(msg) +// ) +// } + +// fn check_expiration( +// deps: DepsMut, +// env: Env, +// info: MessageInfo, +// ) -> Result { + +// let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; + +// if !lockup_config.is_expired(env.block) { +// return Ok(Response::default() +// .add_attribute("method", "check_expiration") +// .add_attribute("result", "not_due") +// ) +// } + +// let pool_address = POOL_ADDRESS.load(deps.storage)?; + +// // We query the pool to get the contract for the pool info +// // The pool info is required to fetch the address of the +// // liquidity token contract. The liquidity tokens are CW20 tokens +// let pair_info: astroport::asset::PairInfo = deps.querier.query_wasm_smart( +// pool_address.to_string(), +// &astroport::pair::QueryMsg::Pair {}, +// )?; + +// // We query our own liquidity token balance +// let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( +// pair_info.clone().liquidity_token, +// &cw20::Cw20QueryMsg::Balance { +// address: env.contract.address.to_string(), +// }, +// )?; + +// // We withdraw our liquidity constructing a CW20 send message +// // The message contains our liquidity token balance +// // The pool address and a message to call the withdraw liquidity hook of the pool contract +// let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; +// let withdraw_msg = &Cw20ExecuteMsg::Send { +// contract: pool_address.to_string(), +// amount: liquidity_token_balance.balance, +// msg: to_binary(withdraw_liquidity_hook)?, +// }; + +// // advance state to Expired +// CONTRACT_STATE.save(deps.storage, &ContractState::Expired)?; + +// // We execute the message on the liquidity token contract +// // This will burn the LP tokens and withdraw liquidity into the holder +// Ok(Response::default() +// .add_attribute("method", "check_expiration") +// .add_attribute("result", "expired") +// .add_attribute("lp_token_amount", liquidity_token_balance.balance) +// .add_message(CosmosMsg::Wasm(WasmMsg::Execute { +// contract_addr: pair_info.liquidity_token.to_string(), +// msg: to_binary(withdraw_msg)?, +// funds: vec![], +// }))) +// } + +// fn try_ragequit( +// deps: DepsMut, +// env: Env, +// info: MessageInfo, +// ) -> Result { +// // if lockup period had passed, just claim the tokens instead of ragequitting +// let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; +// if lockup_config.is_expired(env.block) { +// return Err(ContractError::RagequitWithLockupPassed {}) +// } - // only the involved parties can initiate the ragequit - let parties = PARTIES_CONFIG.load(deps.storage)?; - let rq_party = parties.match_caller_party(info.sender.to_string())?; - - let mut rq_terms = match RAGEQUIT_CONFIG.load(deps.storage)? { - // if ragequit is not enabled for this covenant we error - RagequitConfig::Disabled => return Err(ContractError::RagequitDisabled {}), - RagequitConfig::Enabled(terms) => { - if terms.active { - return Err(ContractError::RagequitAlreadyActive {}) - } - terms - }, - }; - - let pool_address = POOL_ADDRESS.load(deps.storage)?; - - // We query the pool to get the contract for the pool info - // The pool info is required to fetch the address of the - // liquidity token contract. The liquidity tokens are CW20 tokens - let pair_info: astroport::asset::PairInfo = deps - .querier - .query_wasm_smart(pool_address.to_string(), &astroport::pair::QueryMsg::Pair {})?; - - // We query our own liquidity token balance - let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( - pair_info.clone().liquidity_token, - &cw20::Cw20QueryMsg::Balance { - address: env.contract.address.to_string(), - }, - )?; - - // if no lp tokens are available, no point to ragequit - if liquidity_token_balance.balance.is_zero() { - return Err(ContractError::NoLpTokensAvailable {}) - } +// // only the involved parties can initiate the ragequit +// let parties = PARTIES_CONFIG.load(deps.storage)?; +// let rq_party = parties.match_caller_party(info.sender.to_string())?; + +// let mut rq_terms = match RAGEQUIT_CONFIG.load(deps.storage)? { +// // if ragequit is not enabled for this covenant we error +// RagequitConfig::Disabled => return Err(ContractError::RagequitDisabled {}), +// RagequitConfig::Enabled(terms) => { +// if terms.active { +// return Err(ContractError::RagequitAlreadyActive {}) +// } +// terms +// }, +// }; + +// let pool_address = POOL_ADDRESS.load(deps.storage)?; + +// // We query the pool to get the contract for the pool info +// // The pool info is required to fetch the address of the +// // liquidity token contract. The liquidity tokens are CW20 tokens +// let pair_info: astroport::asset::PairInfo = deps +// .querier +// .query_wasm_smart(pool_address.to_string(), &astroport::pair::QueryMsg::Pair {})?; + +// // We query our own liquidity token balance +// let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( +// pair_info.clone().liquidity_token, +// &cw20::Cw20QueryMsg::Balance { +// address: env.contract.address.to_string(), +// }, +// )?; + +// // if no lp tokens are available, no point to ragequit +// if liquidity_token_balance.balance.is_zero() { +// return Err(ContractError::NoLpTokensAvailable {}) +// } - // activate the ragequit in terms - rq_terms.active = true; +// // activate the ragequit in terms +// rq_terms.active = true; - // apply the ragequit penalty - // TODO: let parties = parties.apply_ragequit_penalty(rq_party.clone(), rq_terms.penalty)?; - // let rq_party = parties.get_party_by_addr(rq_party.addr)?; +// // apply the ragequit penalty +// // TODO: let parties = parties.apply_ragequit_penalty(rq_party.clone(), rq_terms.penalty)?; +// // let rq_party = parties.get_party_by_addr(rq_party.addr)?; - // generate the withdraw_liquidity hook for the ragequitting party - // let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; - // let withdraw_msg = &Cw20ExecuteMsg::Send { - // contract: pool_address.to_string(), - // // take the ragequitting party share of the position - // amount: liquidity_token_balance.balance.checked_mul_floor(rq_party.share) - // .map_err(|_| ContractError::FractionMulError {})?, - // msg: to_binary(withdraw_liquidity_hook)?, - // }; - - // // update the state to reflect ragequit - // CONTRACT_STATE.save(deps.storage, &crate::msg::ContractState::Ragequit)?; - - // TODO: need some kind of state representation of pending withdrawals - // to distinguish allocations of ragequitting party from the non-rq party +// // generate the withdraw_liquidity hook for the ragequitting party +// // let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; +// // let withdraw_msg = &Cw20ExecuteMsg::Send { +// // contract: pool_address.to_string(), +// // // take the ragequitting party share of the position +// // amount: liquidity_token_balance.balance.checked_mul_floor(rq_party.share) +// // .map_err(|_| ContractError::FractionMulError {})?, +// // msg: to_binary(withdraw_liquidity_hook)?, +// // }; + +// // // update the state to reflect ragequit +// // CONTRACT_STATE.save(deps.storage, &crate::msg::ContractState::Ragequit)?; + +// // TODO: need some kind of state representation of pending withdrawals +// // to distinguish allocations of ragequitting party from the non-rq party - Ok(Response::default() - .add_attribute("method", "ragequit") - .add_attribute("caller", rq_party.addr) - // .add_message( - // CosmosMsg::Wasm(WasmMsg::Execute { - // contract_addr: pair_info.liquidity_token.to_string(), - // msg: to_binary(withdraw_msg)?, - // funds: vec![], - // }) - // ) - ) -} - -fn try_claim( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { - - Ok(Response::default()) -} +// Ok(Response::default() +// .add_attribute("method", "ragequit") +// .add_attribute("caller", rq_party.addr) +// // .add_message( +// // CosmosMsg::Wasm(WasmMsg::Execute { +// // contract_addr: pair_info.liquidity_token.to_string(), +// // msg: to_binary(withdraw_msg)?, +// // funds: vec![], +// // }) +// // ) +// ) +// } + +// fn try_claim( +// deps: DepsMut, +// env: Env, +// info: MessageInfo, +// ) -> Result { + +// Ok(Response::default()) +// } #[cfg_attr(not(feature = "library"), entry_point)] @@ -273,8 +272,11 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.load(deps.storage)?)?), QueryMsg::RagequitConfig {} => Ok(to_binary(&RAGEQUIT_CONFIG.load(deps.storage)?)?), QueryMsg::LockupConfig {} => Ok(to_binary(&LOCKUP_CONFIG.load(deps.storage)?)?), - QueryMsg::PartiesConfig {} => Ok(to_binary(&PARTIES_CONFIG.load(deps.storage)?)?), QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.load(deps.storage)?)?), QueryMsg::NextContract {} => Ok(to_binary(&NEXT_CONTRACT.load(deps.storage)?)?), + QueryMsg::PoolAddress {} => Ok(to_binary(&POOL_ADDRESS.load(deps.storage)?)?), + QueryMsg::RouterPartyA {} => Ok(to_binary(&PARTY_A_ROUTER.load(deps.storage)?)?), + QueryMsg::RouterPartyB {} => Ok(to_binary(&PARTY_B_ROUTER.load(deps.storage)?)?), + QueryMsg::DepositDeadline {} => Ok(to_binary(&DEPOSIT_DEADLINE.load(deps.storage)?)?), } } \ No newline at end of file diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index abe74bee..1982ef41 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -1,32 +1,18 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Decimal, Attribute}; use covenant_macros::{clocked, covenant_clock_address, covenant_next_contract}; -use covenant_utils::{PolCovenantTerms, CovenantPartiesConfig, LockupConfig}; - -use crate::error::ContractError; +use covenant_utils::LockupConfig; #[cw_serde] pub struct InstantiateMsg { - /// Address for the clock. This contract verifies - /// that only the clock can execute Ticks pub clock_address: String, - /// address of the pool pub pool_address: String, - /// address of the next contract to forward the funds to. - /// usually expected tobe the splitter. pub next_contract: String, - /// block height of covenant expiration. Position is exited - /// automatically upon reaching that height. pub lockup_config: LockupConfig, - /// configuration for ragequit pub ragequit_config: RagequitConfig, - /// parties engaged in the POL. - pub parties_config: CovenantPartiesConfig, - /// deadline for both parties to deposit the funds - /// TODO: rename LockupConfig to something more generic - /// to represent block/time based expiration pub deposit_deadline: Option, - pub covenant_terms: PolCovenantTerms, + pub party_a_router: String, + pub party_b_router: String, } impl InstantiateMsg { @@ -36,7 +22,7 @@ impl InstantiateMsg { Attribute::new("pool_address", self.pool_address), Attribute::new("next_contract", self.next_contract), ]; - attrs.extend(self.parties_config.get_response_attributes()); + // attrs.extend(self.parties_config.get_response_attributes()); attrs.extend(self.ragequit_config.get_response_attributes()); attrs.extend(self.lockup_config.get_response_attributes()); attrs @@ -61,12 +47,12 @@ pub enum ContractState { /// of this contract that indicates an active LP position. /// TODO: think about whether this is a fair assumption to make. Active, - /// one of the parties have initiated ragequit. - /// party with an active position is free to exit at any time. - Ragequit, - /// covenant has reached its expiration date. - Expired, - /// underlying funds have been withdrawn. + // /// one of the parties have initiated ragequit. + // /// party with an active position is free to exit at any time. + // Ragequit, + // /// covenant has reached its expiration date. + // Expired, + // /// underlying funds have been withdrawn. Complete, } @@ -81,96 +67,102 @@ pub enum QueryMsg { RagequitConfig {}, #[returns(LockupConfig)] LockupConfig {}, - #[returns(PartiesConfig)] - PartiesConfig {}, + #[returns(Addr)] + PoolAddress {}, + #[returns(Addr)] + RouterPartyA {}, + #[returns(Addr)] + RouterPartyB {}, + #[returns(LockupConfig)] + DepositDeadline {}, } -#[cw_serde] -pub struct PartiesConfig { - pub party_a: Party, - pub party_b: Party, -} +// #[cw_serde] +// pub struct PartiesConfig { +// pub party_a: Party, +// pub party_b: Party, +// } -impl PartiesConfig { - /// validates the decimal shares of parties involved - /// that must add up to 1.0 - pub fn validate_config(&self) -> Result<&PartiesConfig, ContractError> { - if self.party_a.share + self.party_b.share == Decimal::one() { - Ok(self) - } else { - Err(ContractError::InvolvedPartiesConfigError {}) - } - } +// impl PartiesConfig { +// /// validates the decimal shares of parties involved +// /// that must add up to 1.0 +// pub fn validate_config(&self) -> Result<&PartiesConfig, ContractError> { +// if self.party_a.share + self.party_b.share == Decimal::one() { +// Ok(self) +// } else { +// Err(ContractError::InvolvedPartiesConfigError {}) +// } +// } - /// validates the caller and returns an error if caller is unauthorized, - /// or the calling party if its authorized - pub fn validate_caller(&self, caller: Addr) -> Result { - let a = self.clone().party_a; - let b = self.clone().party_b; - if a.addr == caller { - Ok(a) - } else if b.addr == caller { - Ok(b) - } else { - Err(ContractError::RagequitUnauthorized {}) - } - } +// /// validates the caller and returns an error if caller is unauthorized, +// /// or the calling party if its authorized +// pub fn validate_caller(&self, caller: Addr) -> Result { +// let a = self.clone().party_a; +// let b = self.clone().party_b; +// if a.addr == caller { +// Ok(a) +// } else if b.addr == caller { +// Ok(b) +// } else { +// Err(ContractError::RagequitUnauthorized {}) +// } +// } - /// subtracts the ragequit penalty to the ragequitting party - /// and adds it to the other party - pub fn apply_ragequit_penalty( - mut self, - rq_party: Party, - penalty: Decimal - ) -> Result { - if rq_party.addr == self.party_a.addr { - self.party_a.share -= penalty; - self.party_b.share += penalty; - } else { - self.party_a.share += penalty; - self.party_b.share -= penalty; - } - Ok(self) - } +// /// subtracts the ragequit penalty to the ragequitting party +// /// and adds it to the other party +// pub fn apply_ragequit_penalty( +// mut self, +// rq_party: Party, +// penalty: Decimal +// ) -> Result { +// if rq_party.addr == self.party_a.addr { +// self.party_a.share -= penalty; +// self.party_b.share += penalty; +// } else { +// self.party_a.share += penalty; +// self.party_b.share -= penalty; +// } +// Ok(self) +// } - pub fn get_party_by_addr(self, addr: Addr) -> Result { - if self.party_a.addr == addr { - Ok(self.party_a) - } else if self.party_b.addr == addr { - Ok(self.party_b) - } else { - Err(ContractError::PartyNotFound {}) - } - } -} +// pub fn get_party_by_addr(self, addr: Addr) -> Result { +// if self.party_a.addr == addr { +// Ok(self.party_a) +// } else if self.party_b.addr == addr { +// Ok(self.party_b) +// } else { +// Err(ContractError::PartyNotFound {}) +// } +// } +// } -impl PartiesConfig { - pub fn get_response_attributes(self) -> Vec { - vec![ - Attribute::new("party_a_address", self.party_a.addr), - Attribute::new("party_a_share", self.party_a.share.to_string()), - Attribute::new("party_a_provided_denom", self.party_a.provided_denom), - Attribute::new("party_a_active_position", self.party_a.active_position.to_string()), - Attribute::new("party_b_address", self.party_b.addr), - Attribute::new("party_b_share", self.party_b.share.to_string()), - Attribute::new("party_b_provided_denom", self.party_b.provided_denom), - Attribute::new("party_b_active_position", self.party_b.active_position.to_string()), - ] - } -} +// impl PartiesConfig { +// pub fn get_response_attributes(self) -> Vec { +// vec![ +// Attribute::new("party_a_address", self.party_a.addr), +// Attribute::new("party_a_share", self.party_a.share.to_string()), +// Attribute::new("party_a_provided_denom", self.party_a.provided_denom), +// Attribute::new("party_a_active_position", self.party_a.active_position.to_string()), +// Attribute::new("party_b_address", self.party_b.addr), +// Attribute::new("party_b_share", self.party_b.share.to_string()), +// Attribute::new("party_b_provided_denom", self.party_b.provided_denom), +// Attribute::new("party_b_active_position", self.party_b.active_position.to_string()), +// ] +// } +// } -#[cw_serde] -pub struct Party { - /// authorized address of the party - pub addr: Addr, - /// decimal share of the LP position (e.g. 1/2) - pub share: Decimal, - /// denom provided by the party - pub provided_denom: String, - /// whether party is actively providing liquidity - pub active_position: bool, -} +// #[cw_serde] +// pub struct Party { +// /// authorized address of the party +// pub addr: Addr, +// /// decimal share of the LP position (e.g. 1/2) +// pub share: Decimal, +// /// denom provided by the party +// pub provided_denom: String, +// /// whether party is actively providing liquidity +// pub active_position: bool, +// } #[cw_serde] pub enum RagequitConfig { diff --git a/contracts/two-party-pol-holder/src/state.rs b/contracts/two-party-pol-holder/src/state.rs index ce433d54..737d640b 100644 --- a/contracts/two-party-pol-holder/src/state.rs +++ b/contracts/two-party-pol-holder/src/state.rs @@ -1,16 +1,21 @@ use cosmwasm_std::Addr; -use covenant_utils::{PolCovenantTerms, CovenantPartiesConfig, LockupConfig}; +use covenant_utils::LockupConfig; use cw_storage_plus::Item; use crate::msg::{ContractState, RagequitConfig}; pub const CONTRACT_STATE: Item = Item::new("contract_state"); + pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); pub const NEXT_CONTRACT: Item = Item::new("next_contract"); -pub const PARTIES_CONFIG: Item = Item::new("parties_config"); + pub const LOCKUP_CONFIG: Item = Item::new("lockup_config"); pub const RAGEQUIT_CONFIG: Item = Item::new("ragequit_config"); + pub const POOL_ADDRESS: Item = Item::new("pool_address"); + pub const DEPOSIT_DEADLINE: Item = Item::new("deposit_deadline"); -pub const COVENANT_TERMS: Item = Item::new("covenant_terms"); \ No newline at end of file + +pub const PARTY_A_ROUTER: Item = Item::new("party_a_router"); +pub const PARTY_B_ROUTER: Item = Item::new("party_b_router"); \ No newline at end of file diff --git a/contracts/two-party-pol-holder/src/suite_tests/suite.rs b/contracts/two-party-pol-holder/src/suite_tests/suite.rs index 6c9f9c08..bb310a63 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/suite.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/suite.rs @@ -1,5 +1,6 @@ -use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg}; -use cosmwasm_std::{Addr, Coin, Uint128}; +use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, RagequitConfig}; +use cosmos_sdk_proto::tendermint::types::Block; +use cosmwasm_std::{Addr, Coin, Uint128, BlockInfo, Uint64, Timestamp}; use covenant_utils::{ CovenantPartiesConfig, CovenantParty, LockupConfig, PolCovenantTerms, }; @@ -15,18 +16,21 @@ pub const DENOM_B: &str = "denom_b"; pub const PARTY_A_ADDR: &str = "party_a"; pub const PARTY_B_ADDR: &str = "party_b"; +pub const PARTY_A_ROUTER: &str = "party_a_router"; +pub const PARTY_B_ROUTER: &str = "party_b_router"; + pub const CLOCK_ADDR: &str = "clock_address"; -pub const NEXT_CONTRACT: &str = "next_contract"; +pub const NEXT_CONTRACT: &str = "contract0"; pub const INITIAL_BLOCK_HEIGHT: u64 = 12345; pub const INITIAL_BLOCK_NANOS: u64 = 1571797419879305533; +pub const POOL: &str = "some_pool"; + pub struct Suite { pub app: App, pub holder: Addr, pub mock_deposit: Addr, - pub party_a: CovenantParty, - pub party_b: CovenantParty, } pub struct SuiteBuilder { @@ -38,17 +42,14 @@ impl Default for SuiteBuilder { fn default() -> Self { Self { instantiate: InstantiateMsg { - pool_address: todo!(), - ragequit_config: todo!(), + pool_address: POOL.to_string(), + ragequit_config: RagequitConfig::Disabled, deposit_deadline: None, clock_address: CLOCK_ADDR.to_string(), next_contract: NEXT_CONTRACT.to_string(), lockup_config: LockupConfig::None, - parties_config: todo!(), - covenant_terms: PolCovenantTerms { - party_a_amount: Uint128::new(400), - party_b_amount: Uint128::new(20), - }, + party_a_router: PARTY_A_ROUTER.to_string(), + party_b_router: PARTY_B_ROUTER.to_string(), }, app: App::default(), } @@ -61,6 +62,11 @@ impl SuiteBuilder { self } + pub fn with_deposit_deadline(mut self, config: LockupConfig) -> Self { + self.instantiate.deposit_deadline = Some(config); + self + } + pub fn build(mut self) -> Suite { let mut app = self.app; let holder_code = app.store_code(two_party_pol_holder_contract()); @@ -94,8 +100,6 @@ impl SuiteBuilder { app, holder, mock_deposit, - party_a: self.instantiate.parties_config.party_a, - party_b: self.instantiate.parties_config.party_b, } } } @@ -121,6 +125,34 @@ impl Suite { .unwrap() } + pub fn query_pool(&self) -> Addr { + self.app + .wrap() + .query_wasm_smart(&self.holder, &QueryMsg::PoolAddress {}) + .unwrap() + } + + pub fn query_router_party_a(&self) -> Addr { + self.app + .wrap() + .query_wasm_smart(&self.holder, &QueryMsg::RouterPartyA {}) + .unwrap() + } + + pub fn query_router_party_b(&self) -> Addr { + self.app + .wrap() + .query_wasm_smart(&self.holder, &QueryMsg::RouterPartyB {}) + .unwrap() + } + + pub fn query_deposit_deadline(&self) -> LockupConfig { + self.app + .wrap() + .query_wasm_smart(&self.holder, &QueryMsg::DepositDeadline {}) + .unwrap() + } + pub fn query_lockup_config(&self) -> LockupConfig { self.app .wrap() @@ -154,6 +186,10 @@ impl Suite { .update_block(|mut b| b.time = b.time.plus_minutes(n)); } + pub fn get_current_block(&mut self) -> BlockInfo { + self.app.block_info() + } + pub fn fund_coin(&mut self, coin: Coin) -> AppResponse { self.app .sudo(SudoMsg::Bank(cw_multi_test::BankSudo::Mint { @@ -162,4 +198,36 @@ impl Suite { })) .unwrap() } + + pub fn get_denom_a_balance(&mut self, addr: String) -> Uint128 { + self.app.wrap().query_balance(addr, DENOM_A).unwrap().amount + } + + pub fn get_denom_b_balance(&mut self, addr: String) -> Uint128 { + self.app.wrap().query_balance(addr, DENOM_B).unwrap().amount + } + + pub fn get_party_a_coin(&mut self, amount: Uint128) -> Coin { + Coin { + denom: DENOM_A.to_string(), + amount, + } + } + + pub fn get_party_b_coin(&mut self, amount: Uint128) -> Coin { + Coin { + denom: DENOM_B.to_string(), + amount, + } + } + + } + +pub fn get_default_block_info() -> BlockInfo { + BlockInfo { + height: 12345, + time: Timestamp::from_nanos(1571797419879305533), + chain_id: "cosmos-testnet-14002".to_string(), + } +} \ No newline at end of file diff --git a/contracts/two-party-pol-holder/src/suite_tests/tests.rs b/contracts/two-party-pol-holder/src/suite_tests/tests.rs index 18e4bd1d..d24b77d4 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/tests.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/tests.rs @@ -1,7 +1,119 @@ +use astroport::router; +use cosmwasm_std::{Timestamp, Uint128}; +use covenant_utils::LockupConfig; + +use crate::{suite_tests::suite::{CLOCK_ADDR, POOL, NEXT_CONTRACT, PARTY_A_ROUTER, PARTY_B_ROUTER, Suite, get_default_block_info}, msg::ContractState}; + use super::suite::SuiteBuilder; #[test] fn test_instantiate_happy_and_query_all() { let suite = SuiteBuilder::default().build(); + let clock = suite.query_clock_address(); + let pool: cosmwasm_std::Addr = suite.query_pool(); + let next_contract = suite.query_next_contract(); + let party_a_router = suite.query_router_party_a(); + let party_b_router = suite.query_router_party_b(); + let deposit_deadline = suite.query_deposit_deadline(); + let contract_state = suite.query_contract_state(); + + assert_eq!(ContractState::Instantiated, contract_state); + assert_eq!(CLOCK_ADDR, clock); + assert_eq!(POOL, pool); + assert_eq!(NEXT_CONTRACT, next_contract.to_string()); + assert_eq!(PARTY_A_ROUTER, party_a_router.to_string()); + assert_eq!(PARTY_B_ROUTER, party_b_router.to_string()); + assert_eq!(LockupConfig::None, deposit_deadline); +} + +#[test] +#[should_panic(expected = "block height must be in the future")] +fn test_instantiate_invalid_deposit_deadline_block_based() { + SuiteBuilder::default() + .with_deposit_deadline(LockupConfig::Block(1)) + .build(); +} + +#[test] +#[should_panic(expected = "block time must be in the future")] +fn test_instantiate_invalid_deposit_deadline_time_based() { + SuiteBuilder::default() + .with_deposit_deadline(LockupConfig::Time(Timestamp::from_nanos(1))) + .build(); +} + +#[test] +fn test_instantiate_invalid_lockup_config() { + let suite = SuiteBuilder::default().build(); } + +#[test] +fn test_single_party_deposit_refund_block_based() { + let mut suite = SuiteBuilder::default() + .with_deposit_deadline(LockupConfig::Block(12545)) + .build(); + + // party A fulfills their part of covenant but B fails to + let coin = suite.get_party_a_coin(Uint128::new(500)); + suite.fund_coin(coin); + + // time passes, clock ticks.. + suite.pass_blocks(250); + suite.tick(CLOCK_ADDR).unwrap(); + + let holder_balance = suite.get_denom_a_balance(suite.holder.to_string()); + let router_a_balance = suite.get_denom_a_balance( + suite.query_router_party_a().to_string()); + let holder_state = suite.query_contract_state(); + + assert_eq!(ContractState::Complete, holder_state); + assert_eq!(Uint128::zero(), holder_balance); + assert_eq!(Uint128::new(500), router_a_balance); +} + +#[test] +fn test_single_party_deposit_refund_time_based() { + let current_timestamp = get_default_block_info(); + let mut suite = SuiteBuilder::default() + .with_deposit_deadline(LockupConfig::Time(current_timestamp.time.plus_minutes(200))) + .build(); + + // party A fulfills their part of covenant but B fails to + let coin = suite.get_party_a_coin(Uint128::new(500)); + suite.fund_coin(coin); + + // time passes, clock ticks.. + suite.pass_minutes(250); + suite.tick(CLOCK_ADDR).unwrap(); + + let holder_balance = suite.get_denom_a_balance(suite.holder.to_string()); + let router_a_balance = suite.get_denom_a_balance( + suite.query_router_party_a().to_string()); + let holder_state = suite.query_contract_state(); + + assert_eq!(ContractState::Complete, holder_state); + assert_eq!(Uint128::zero(), holder_balance); + assert_eq!(Uint128::new(500), router_a_balance); +} + + +#[test] +fn test_single_party_deposit_refund_no_deposit_deadline() { + let mut suite = SuiteBuilder::default().build(); + + // party A fulfills their part of covenant but B fails to + let coin = suite.get_party_a_coin(Uint128::new(500)); + suite.fund_coin(coin); + + // time passes, clock ticks.. + suite.pass_minutes(25000000); + suite.tick(CLOCK_ADDR).unwrap(); + + // we assert that holder still holds the tokens and did not advance the state + let holder_balance = suite.get_denom_a_balance(suite.holder.to_string()); + let holder_state = suite.query_contract_state(); + + assert_eq!(ContractState::Instantiated, holder_state); + assert_eq!(Uint128::new(500), holder_balance); +} From 644961bcc028a297075ecdb26331835c9e5fb8a4 Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 20 Sep 2023 20:08:31 +0200 Subject: [PATCH 101/586] refund logic for failed deposit --- .../two-party-pol-holder/src/contract.rs | 188 +++++++++++++----- contracts/two-party-pol-holder/src/msg.rs | 9 +- contracts/two-party-pol-holder/src/state.rs | 6 +- .../src/suite_tests/suite.rs | 12 +- .../src/suite_tests/tests.rs | 10 +- 5 files changed, 172 insertions(+), 53 deletions(-) diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index d71c0cb9..3e30eb28 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -1,11 +1,11 @@ -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, BankMsg, CosmosMsg}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use covenant_utils::LockupConfig; use cw2::set_contract_version; -use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, CONTRACT_STATE, DEPOSIT_DEADLINE, POOL_ADDRESS, PARTY_A_ROUTER, PARTY_B_ROUTER}, error::ContractError}; +use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, ContractState}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, CONTRACT_STATE, DEPOSIT_DEADLINE, POOL_ADDRESS, PARTY_A_ROUTER, PARTY_B_ROUTER, COVENANT_CONFIG}, error::ContractError}; const CONTRACT_NAME: &str = "crates.io:covenant-two-party-pol-holder"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -33,6 +33,8 @@ pub fn instantiate( RAGEQUIT_CONFIG.save(deps.storage, &msg.ragequit_config)?; PARTY_A_ROUTER.save(deps.storage, &party_a_router)?; PARTY_B_ROUTER.save(deps.storage, &party_b_router)?; + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + COVENANT_CONFIG.save(deps.storage, &msg.covenant_config)?; match &msg.deposit_deadline { Some(deadline) => { @@ -57,61 +59,155 @@ pub fn execute( info: MessageInfo, msg: ExecuteMsg, ) -> Result { - // match msg { + match msg { // ExecuteMsg::Ragequit {} => try_ragequit(deps, env, info), // ExecuteMsg::Claim {} => try_claim(deps, env, info), - // ExecuteMsg::Tick {} => try_tick(deps, env, info), - // } - Ok(Response::default()) + ExecuteMsg::Tick {} => try_tick(deps, env, info), + _ => Ok(Response::default()), + } } -// fn try_tick( -// deps: DepsMut, -// env: Env, -// info: MessageInfo, -// ) -> Result { -// let state = CONTRACT_STATE.load(deps.storage)?; -// match state { -// ContractState::Instantiated => try_deposit(deps, env, info), -// ContractState::Active => check_expiration(deps, env, info), -// ContractState::Ragequit => todo!(), -// ContractState::Expired => todo!(), -// ContractState::Complete => Ok(Response::default().add_attribute("contract_state", "complete")), -// } -// } +fn try_tick( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + let state = CONTRACT_STATE.load(deps.storage)?; + match state { + ContractState::Instantiated => try_deposit(deps, env, info), + _ => Ok(Response::default()), + // ContractState::Active => check_expiration(deps, env, info), + // ContractState::Ragequit => todo!(), + // ContractState::Expired => todo!(), + // ContractState::Complete => Ok(Response::default().add_attribute("contract_state", "complete")), + } +} -// fn try_deposit( -// deps: DepsMut, -// env: Env, -// info: MessageInfo, -// ) -> Result { +fn try_deposit( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + let config = COVENANT_CONFIG.load(deps.storage)?; + + // assert the balances + let party_a_bal = deps.querier.query_balance( + env.contract.address.to_string(), + config.party_a_contribution.denom)?; + let party_b_bal = deps.querier.query_balance( + env.contract.address.to_string(), + config.party_b_contribution.denom)?; + + let deposit_deadline = DEPOSIT_DEADLINE.load(deps.storage)?; + let party_a_fulfilled = config.party_a_contribution.amount < party_a_bal.amount; + let party_b_fulfilled = config.party_b_contribution.amount < party_b_bal.amount; + + // note: even if both parties deposit their funds in time, + // it is important to trigger this method before the expiry block + // if deposit deadline is due we complete and refund + if deposit_deadline.is_expired(env.block.clone()) { + let a_router = PARTY_A_ROUTER.load(deps.storage)?; + let b_router = PARTY_B_ROUTER.load(deps.storage)?; + + let refund_messages: Vec = match (party_a_bal.amount.is_zero(), party_b_bal.amount.is_zero()) { + // both balances empty, we complete + (true, true) => { + CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; + return Ok(Response::default() + .add_attribute("method", "try_deposit") + .add_attribute("state", "complete")) + }, + // refund party B + (true, false) => vec![CosmosMsg::Bank(BankMsg::Send { + to_address: b_router.to_string(), + amount: vec![party_b_bal], + })], + // refund party A + (false, true) => vec![CosmosMsg::Bank(BankMsg::Send { + to_address: a_router.to_string(), + amount: vec![party_a_bal], + })], + // refund both + (false, false) => vec![ + CosmosMsg::Bank(BankMsg::Send { + to_address: a_router.to_string(), + amount: vec![party_a_bal], + }), + CosmosMsg::Bank(BankMsg::Send { + to_address: b_router.to_string(), + amount: vec![party_b_bal], + }), + ], + }; + return Ok(Response::default() + .add_attribute("method", "try_deposit") + .add_attribute("action", "refund") + .add_messages(refund_messages) + ) + } else if !party_a_fulfilled || !party_b_fulfilled { + // if deposit deadline is not yet due and both parties did not fulfill we error + return Err(ContractError::InsufficientDeposits {}) + } -// let parties = PARTIES_CONFIG.load(deps.storage)?; -// let terms = COVENANT_TERMS.load(deps.storage)?; + // match ( + // config.party_a_contribution.amount < party_a_bal.amount, + // config.party_b_contribution.amount < party_b_bal.amount, + // deposit_deadline.is_expired(env.block.clone()), + // ) { + // // if deposit deadline is not yet due, we wait + // (_, _, false) => { + // return Err(ContractError::InsufficientDeposits {}) + // }, + // // neither party contributed enough, + // (true, true, true) => { -// // assert the balances -// let party_a_bal = deps.querier.query_balance(env.contract.address.to_string(), parties.party_a.ibc_denom)?; -// let party_b_bal = deps.querier.query_balance(env.contract.address.to_string(), parties.party_b.ibc_denom)?; + // }, + // (true, false, true) => { -// if terms.party_a_amount < party_a_bal.amount || terms.party_b_amount < party_b_bal.amount { -// return Err(ContractError::InsufficientDeposits {}) -// } + // }, + // (false, true, true) => { + + // }, + // (false, true, true) => { + + // }, + // } + // if config.party_a_contribution.amount < party_a_bal.amount || config.party_b_contribution.amount < party_b_bal.amount { + // let deposit_deadline = DEPOSIT_DEADLINE.load(deps.storage)?; + // if deposit_deadline.is_expired(env.block.clone()) { + // // if deposit deadline is due and some party did not deposit, + // // we refund the counterparty + + + // } else { + // return Err(ContractError::InsufficientDeposits {}) + // } + // } -// // LiquidPooler is the next contract -// let next_contract = NEXT_CONTRACT.load(deps.storage)?; -// let msg = BankMsg::Send { -// to_address: next_contract.to_string(), -// amount: vec![party_a_bal, party_b_bal], -// }; + // LiquidPooler is the next contract + let next_contract = NEXT_CONTRACT.load(deps.storage)?; + let msg = BankMsg::Send { + to_address: next_contract.to_string(), + amount: vec![party_a_bal, party_b_bal], + }; -// // advance the state to Active -// CONTRACT_STATE.save(deps.storage, &ContractState::Active)?; + // advance the state to Active + CONTRACT_STATE.save(deps.storage, &ContractState::Active)?; -// Ok(Response::default() -// .add_attribute("method", "deposit_to_next_contract") -// .add_message(msg) -// ) -// } + Ok(Response::default() + .add_attribute("method", "deposit_to_next_contract") + .add_message(msg) + ) +} + +fn try_refund( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + + Ok(Response::default()) +} // fn check_expiration( // deps: DepsMut, diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index 1982ef41..319036c0 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Decimal, Attribute}; +use cosmwasm_std::{Addr, Decimal, Attribute, Uint128, Coin}; use covenant_macros::{clocked, covenant_clock_address, covenant_next_contract}; use covenant_utils::LockupConfig; @@ -13,6 +13,7 @@ pub struct InstantiateMsg { pub deposit_deadline: Option, pub party_a_router: String, pub party_b_router: String, + pub covenant_config: TwoPartyPolCovenantConfig, } impl InstantiateMsg { @@ -29,6 +30,12 @@ impl InstantiateMsg { } } +#[cw_serde] +pub struct TwoPartyPolCovenantConfig { + pub party_a_contribution: Coin, + pub party_b_contribution: Coin, +} + #[clocked] #[cw_serde] pub enum ExecuteMsg { diff --git a/contracts/two-party-pol-holder/src/state.rs b/contracts/two-party-pol-holder/src/state.rs index 737d640b..46a86e19 100644 --- a/contracts/two-party-pol-holder/src/state.rs +++ b/contracts/two-party-pol-holder/src/state.rs @@ -2,7 +2,7 @@ use cosmwasm_std::Addr; use covenant_utils::LockupConfig; use cw_storage_plus::Item; -use crate::msg::{ContractState, RagequitConfig}; +use crate::msg::{ContractState, RagequitConfig, TwoPartyPolCovenantConfig}; pub const CONTRACT_STATE: Item = Item::new("contract_state"); @@ -18,4 +18,6 @@ pub const POOL_ADDRESS: Item = Item::new("pool_address"); pub const DEPOSIT_DEADLINE: Item = Item::new("deposit_deadline"); pub const PARTY_A_ROUTER: Item = Item::new("party_a_router"); -pub const PARTY_B_ROUTER: Item = Item::new("party_b_router"); \ No newline at end of file +pub const PARTY_B_ROUTER: Item = Item::new("party_b_router"); + +pub const COVENANT_CONFIG: Item = Item::new("covenant_config"); diff --git a/contracts/two-party-pol-holder/src/suite_tests/suite.rs b/contracts/two-party-pol-holder/src/suite_tests/suite.rs index bb310a63..45b65538 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/suite.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/suite.rs @@ -1,4 +1,4 @@ -use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, RagequitConfig}; +use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, RagequitConfig, TwoPartyPolCovenantConfig}; use cosmos_sdk_proto::tendermint::types::Block; use cosmwasm_std::{Addr, Coin, Uint128, BlockInfo, Uint64, Timestamp}; use covenant_utils::{ @@ -50,6 +50,16 @@ impl Default for SuiteBuilder { lockup_config: LockupConfig::None, party_a_router: PARTY_A_ROUTER.to_string(), party_b_router: PARTY_B_ROUTER.to_string(), + covenant_config: TwoPartyPolCovenantConfig { + party_a_contribution: Coin { + denom: DENOM_A.to_string(), + amount: Uint128::new(200), + }, + party_b_contribution: Coin { + denom: DENOM_B.to_string(), + amount: Uint128::new(100), + }, + }, }, app: App::default(), } diff --git a/contracts/two-party-pol-holder/src/suite_tests/tests.rs b/contracts/two-party-pol-holder/src/suite_tests/tests.rs index d24b77d4..91818d1a 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/tests.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/tests.rs @@ -1,8 +1,7 @@ -use astroport::router; use cosmwasm_std::{Timestamp, Uint128}; use covenant_utils::LockupConfig; -use crate::{suite_tests::suite::{CLOCK_ADDR, POOL, NEXT_CONTRACT, PARTY_A_ROUTER, PARTY_B_ROUTER, Suite, get_default_block_info}, msg::ContractState}; +use crate::{suite_tests::suite::{CLOCK_ADDR, POOL, NEXT_CONTRACT, PARTY_A_ROUTER, PARTY_B_ROUTER, get_default_block_info}, msg::ContractState, error::ContractError}; use super::suite::SuiteBuilder; @@ -61,6 +60,7 @@ fn test_single_party_deposit_refund_block_based() { // time passes, clock ticks.. suite.pass_blocks(250); suite.tick(CLOCK_ADDR).unwrap(); + suite.tick(CLOCK_ADDR).unwrap(); let holder_balance = suite.get_denom_a_balance(suite.holder.to_string()); let router_a_balance = suite.get_denom_a_balance( @@ -86,6 +86,7 @@ fn test_single_party_deposit_refund_time_based() { // time passes, clock ticks.. suite.pass_minutes(250); suite.tick(CLOCK_ADDR).unwrap(); + suite.tick(CLOCK_ADDR).unwrap(); let holder_balance = suite.get_denom_a_balance(suite.holder.to_string()); let router_a_balance = suite.get_denom_a_balance( @@ -108,7 +109,9 @@ fn test_single_party_deposit_refund_no_deposit_deadline() { // time passes, clock ticks.. suite.pass_minutes(25000000); - suite.tick(CLOCK_ADDR).unwrap(); + suite.tick(CLOCK_ADDR); + suite.tick(CLOCK_ADDR); + let resp: ContractError = suite.tick(CLOCK_ADDR).unwrap_err().downcast().unwrap(); // we assert that holder still holds the tokens and did not advance the state let holder_balance = suite.get_denom_a_balance(suite.holder.to_string()); @@ -116,4 +119,5 @@ fn test_single_party_deposit_refund_no_deposit_deadline() { assert_eq!(ContractState::Instantiated, holder_state); assert_eq!(Uint128::new(500), holder_balance); + assert_eq!(ContractError::InsufficientDeposits {}, resp); } From 6a5a4d4160498584681fd19e832a0f8f232e2eab Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 21 Sep 2023 13:54:42 +0200 Subject: [PATCH 102/586] holder expiration --- .../two-party-pol-holder/src/contract.rs | 122 ++++-------------- contracts/two-party-pol-holder/src/msg.rs | 3 +- .../src/suite_tests/mod.rs | 28 +++- .../src/suite_tests/tests.rs | 88 ++++++++++++- 4 files changed, 138 insertions(+), 103 deletions(-) diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index 3e30eb28..ef93fbba 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -1,9 +1,11 @@ -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, BankMsg, CosmosMsg}; +use astroport::pair::Cw20HookMsg; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, BankMsg, CosmosMsg, WasmMsg}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use covenant_utils::LockupConfig; use cw2::set_contract_version; +use cw20::{BalanceResponse, Cw20ExecuteMsg}; use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, ContractState}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, CONTRACT_STATE, DEPOSIT_DEADLINE, POOL_ADDRESS, PARTY_A_ROUTER, PARTY_B_ROUTER, COVENANT_CONFIG}, error::ContractError}; @@ -75,11 +77,11 @@ fn try_tick( let state = CONTRACT_STATE.load(deps.storage)?; match state { ContractState::Instantiated => try_deposit(deps, env, info), - _ => Ok(Response::default()), - // ContractState::Active => check_expiration(deps, env, info), + ContractState::Active => check_expiration(deps, env), // ContractState::Ragequit => todo!(), // ContractState::Expired => todo!(), - // ContractState::Complete => Ok(Response::default().add_attribute("contract_state", "complete")), + ContractState::Complete => Ok(Response::default().add_attribute("contract_state", "complete")), + _ => Ok(Response::default()), } } @@ -148,41 +150,6 @@ fn try_deposit( // if deposit deadline is not yet due and both parties did not fulfill we error return Err(ContractError::InsufficientDeposits {}) } - - // match ( - // config.party_a_contribution.amount < party_a_bal.amount, - // config.party_b_contribution.amount < party_b_bal.amount, - // deposit_deadline.is_expired(env.block.clone()), - // ) { - // // if deposit deadline is not yet due, we wait - // (_, _, false) => { - // return Err(ContractError::InsufficientDeposits {}) - // }, - // // neither party contributed enough, - // (true, true, true) => { - - // }, - // (true, false, true) => { - - // }, - // (false, true, true) => { - - // }, - // (false, true, true) => { - - // }, - // } - // if config.party_a_contribution.amount < party_a_bal.amount || config.party_b_contribution.amount < party_b_bal.amount { - // let deposit_deadline = DEPOSIT_DEADLINE.load(deps.storage)?; - // if deposit_deadline.is_expired(env.block.clone()) { - // // if deposit deadline is due and some party did not deposit, - // // we refund the counterparty - - - // } else { - // return Err(ContractError::InsufficientDeposits {}) - // } - // } // LiquidPooler is the next contract let next_contract = NEXT_CONTRACT.load(deps.storage)?; @@ -200,73 +167,28 @@ fn try_deposit( ) } -fn try_refund( + +fn check_expiration( deps: DepsMut, env: Env, - info: MessageInfo, ) -> Result { + let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; - Ok(Response::default()) -} - -// fn check_expiration( -// deps: DepsMut, -// env: Env, -// info: MessageInfo, -// ) -> Result { - -// let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; - -// if !lockup_config.is_expired(env.block) { -// return Ok(Response::default() -// .add_attribute("method", "check_expiration") -// .add_attribute("result", "not_due") -// ) -// } - -// let pool_address = POOL_ADDRESS.load(deps.storage)?; - -// // We query the pool to get the contract for the pool info -// // The pool info is required to fetch the address of the -// // liquidity token contract. The liquidity tokens are CW20 tokens -// let pair_info: astroport::asset::PairInfo = deps.querier.query_wasm_smart( -// pool_address.to_string(), -// &astroport::pair::QueryMsg::Pair {}, -// )?; - -// // We query our own liquidity token balance -// let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( -// pair_info.clone().liquidity_token, -// &cw20::Cw20QueryMsg::Balance { -// address: env.contract.address.to_string(), -// }, -// )?; - -// // We withdraw our liquidity constructing a CW20 send message -// // The message contains our liquidity token balance -// // The pool address and a message to call the withdraw liquidity hook of the pool contract -// let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; -// let withdraw_msg = &Cw20ExecuteMsg::Send { -// contract: pool_address.to_string(), -// amount: liquidity_token_balance.balance, -// msg: to_binary(withdraw_liquidity_hook)?, -// }; + if !lockup_config.is_expired(env.block) { + return Ok(Response::default() + .add_attribute("method", "check_expiration") + .add_attribute("result", "not_due") + ) + } -// // advance state to Expired -// CONTRACT_STATE.save(deps.storage, &ContractState::Expired)?; + // advance state to Expired to enable claims + CONTRACT_STATE.save(deps.storage, &ContractState::Expired)?; -// // We execute the message on the liquidity token contract -// // This will burn the LP tokens and withdraw liquidity into the holder -// Ok(Response::default() -// .add_attribute("method", "check_expiration") -// .add_attribute("result", "expired") -// .add_attribute("lp_token_amount", liquidity_token_balance.balance) -// .add_message(CosmosMsg::Wasm(WasmMsg::Execute { -// contract_addr: pair_info.liquidity_token.to_string(), -// msg: to_binary(withdraw_msg)?, -// funds: vec![], -// }))) -// } + Ok(Response::default() + .add_attribute("method", "check_expiration") + .add_attribute("contract_state", "expired") + ) +} // fn try_ragequit( // deps: DepsMut, diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index 319036c0..a3501634 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -36,6 +36,7 @@ pub struct TwoPartyPolCovenantConfig { pub party_b_contribution: Coin, } + #[clocked] #[cw_serde] pub enum ExecuteMsg { @@ -58,7 +59,7 @@ pub enum ContractState { // /// party with an active position is free to exit at any time. // Ragequit, // /// covenant has reached its expiration date. - // Expired, + Expired, // /// underlying funds have been withdrawn. Complete, } diff --git a/contracts/two-party-pol-holder/src/suite_tests/mod.rs b/contracts/two-party-pol-holder/src/suite_tests/mod.rs index 107dd564..a417a4d8 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/mod.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/mod.rs @@ -1,5 +1,6 @@ +use astroport::asset::PairInfo; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{to_binary, Binary, Deps, Empty, Env, StdResult}; +use cosmwasm_std::{to_binary, Binary, Deps, Empty, Env, StdResult, Addr}; use covenant_macros::covenant_deposit_address; use cw_multi_test::{Contract, ContractWrapper}; @@ -24,6 +25,7 @@ pub fn mock_deposit_contract() -> Box> { Box::new(contract) } + #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -38,3 +40,27 @@ pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::DepositAddress {} => Ok(to_binary(&"splitter")?), } } + + +pub fn mock_astro_pool_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + query_astro_pool, + ); + Box::new(contract) +} + + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query_astro_pool(_deps: Deps, _env: Env, msg: astroport::pair::QueryMsg) -> StdResult { + match msg { + astroport::pair::QueryMsg::Pair {} => Ok(to_binary(&PairInfo { + asset_infos: vec![], + contract_addr: Addr::unchecked("lp-token"), + liquidity_token: Addr::unchecked("lp-token"), + pair_type: astroport::factory::PairType::Xyk { }, + })?), + _ => Ok(to_binary(&"-")?), + } +} diff --git a/contracts/two-party-pol-holder/src/suite_tests/tests.rs b/contracts/two-party-pol-holder/src/suite_tests/tests.rs index 91818d1a..5868d750 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/tests.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/tests.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Timestamp, Uint128}; +use cosmwasm_std::{Timestamp, Uint128, Event, Attribute}; use covenant_utils::LockupConfig; use crate::{suite_tests::suite::{CLOCK_ADDR, POOL, NEXT_CONTRACT, PARTY_A_ROUTER, PARTY_B_ROUTER, get_default_block_info}, msg::ContractState, error::ContractError}; @@ -121,3 +121,89 @@ fn test_single_party_deposit_refund_no_deposit_deadline() { assert_eq!(Uint128::new(500), holder_balance); assert_eq!(ContractError::InsufficientDeposits {}, resp); } + +#[test] +fn test_holder_active_does_not_allow_claims() { + unimplemented!() +} + +#[test] +fn test_holder_active_not_expired_ticks() { + let current_timestamp = get_default_block_info(); + let mut suite = SuiteBuilder::default() + .with_deposit_deadline(LockupConfig::Time(current_timestamp.time.plus_minutes(200))) + .build(); + + // both parties fulfill their parts of the covenant + let coin_a = suite.get_party_a_coin(Uint128::new(500)); + let coin_b = suite.get_party_b_coin(Uint128::new(500)); + suite.fund_coin(coin_a); + suite.fund_coin(coin_b); + + // we tick the holder to deposit the funds and activate + suite.tick(CLOCK_ADDR).unwrap(); + + // time passes, clock ticks.. + suite.pass_minutes(50); + let resp = suite.tick(CLOCK_ADDR).unwrap(); + + let has_not_due_attribute = resp.events.into_iter() + .flat_map(|e| e.attributes) + .into_iter() + .any(|attr| attr.value == "not_due"); + let holder_balance_a = suite.get_denom_a_balance(suite.holder.to_string()); + let holder_balance_b = suite.get_denom_b_balance(suite.holder.to_string()); + let splitter_balance_a = suite.get_denom_a_balance(suite.mock_deposit.to_string()); + let splitter_balance_b = suite.get_denom_b_balance(suite.mock_deposit.to_string()); + let holder_state = suite.query_contract_state(); + + assert!(has_not_due_attribute); + assert_eq!(ContractState::Active, holder_state); + assert_eq!(Uint128::zero(), holder_balance_b); + assert_eq!(Uint128::zero(), holder_balance_a); + assert_eq!(Uint128::new(500), splitter_balance_b); + assert_eq!(Uint128::new(500), splitter_balance_a); +} + +#[test] +fn test_holder_active_expired_tick_advances_state() { + let current_timestamp = get_default_block_info(); + let mut suite = SuiteBuilder::default() + .with_lockup_config(LockupConfig::Time(current_timestamp.time.plus_minutes(200))) + .build(); + + // both parties fulfill their parts of the covenant + let coin_a = suite.get_party_a_coin(Uint128::new(500)); + let coin_b = suite.get_party_b_coin(Uint128::new(500)); + suite.fund_coin(coin_a); + suite.fund_coin(coin_b); + + // we tick the holder to deposit the funds and activate + suite.tick(CLOCK_ADDR).unwrap(); + + // time passes, clock ticks.. + suite.pass_minutes(250); + suite.tick(CLOCK_ADDR).unwrap(); + + let holder_balance_a = suite.get_denom_a_balance(suite.holder.to_string()); + let holder_balance_b = suite.get_denom_b_balance(suite.holder.to_string()); + let splitter_balance_a = suite.get_denom_a_balance(suite.mock_deposit.to_string()); + let splitter_balance_b = suite.get_denom_b_balance(suite.mock_deposit.to_string()); + let holder_state = suite.query_contract_state(); + + assert_eq!(ContractState::Expired, holder_state); + assert_eq!(Uint128::zero(), holder_balance_b); + assert_eq!(Uint128::zero(), holder_balance_a); + assert_eq!(Uint128::new(500), splitter_balance_b); + assert_eq!(Uint128::new(500), splitter_balance_a); +} + +#[test] +fn test_holder_instantiated_ragequit_fails() { + unimplemented!() +} + +#[test] +fn test_holder_active_ragequit_party_double_claim() { + unimplemented!() +} From 4d45777f0a2beb86fd39b808735e61920d643975 Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 21 Sep 2023 22:27:17 +0200 Subject: [PATCH 103/586] rq restart --- .../two-party-pol-holder/src/contract.rs | 185 +++++++++--------- contracts/two-party-pol-holder/src/error.rs | 3 + contracts/two-party-pol-holder/src/msg.rs | 10 +- .../src/suite_tests/suite.rs | 9 + .../src/suite_tests/tests.rs | 52 ++++- 5 files changed, 160 insertions(+), 99 deletions(-) diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index ef93fbba..99167a52 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -62,7 +62,7 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - // ExecuteMsg::Ragequit {} => try_ragequit(deps, env, info), + ExecuteMsg::Ragequit {} => try_ragequit(deps, env, info), // ExecuteMsg::Claim {} => try_claim(deps, env, info), ExecuteMsg::Tick {} => try_tick(deps, env, info), _ => Ok(Response::default()), @@ -78,7 +78,7 @@ fn try_tick( match state { ContractState::Instantiated => try_deposit(deps, env, info), ContractState::Active => check_expiration(deps, env), - // ContractState::Ragequit => todo!(), + // ContractState::Ragequit => try_ragequit(deps, env, info), // ContractState::Expired => todo!(), ContractState::Complete => Ok(Response::default().add_attribute("contract_state", "complete")), _ => Ok(Response::default()), @@ -190,98 +190,105 @@ fn check_expiration( ) } -// fn try_ragequit( -// deps: DepsMut, -// env: Env, -// info: MessageInfo, -// ) -> Result { -// // if lockup period had passed, just claim the tokens instead of ragequitting -// let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; -// if lockup_config.is_expired(env.block) { -// return Err(ContractError::RagequitWithLockupPassed {}) -// } +fn try_ragequit( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + + let current_state = CONTRACT_STATE.load(deps.storage)?; + let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; + + // ragequit is only possible when contract is in Active state. + // we also validate an edge case where it did expire but + // did not receive a tick yet + if current_state != ContractState::Active || lockup_config.is_expired(env.block) { + return Err(ContractError::NotActive {}) + } + + Ok(Response::default()) + + /* + // if lockup period had passed, just claim the tokens instead of ragequitting + let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; + if lockup_config.is_expired(env.block) { + return Err(ContractError::RagequitWithLockupPassed {}) + } -// // only the involved parties can initiate the ragequit -// let parties = PARTIES_CONFIG.load(deps.storage)?; -// let rq_party = parties.match_caller_party(info.sender.to_string())?; - -// let mut rq_terms = match RAGEQUIT_CONFIG.load(deps.storage)? { -// // if ragequit is not enabled for this covenant we error -// RagequitConfig::Disabled => return Err(ContractError::RagequitDisabled {}), -// RagequitConfig::Enabled(terms) => { -// if terms.active { -// return Err(ContractError::RagequitAlreadyActive {}) -// } -// terms -// }, -// }; - -// let pool_address = POOL_ADDRESS.load(deps.storage)?; - -// // We query the pool to get the contract for the pool info -// // The pool info is required to fetch the address of the -// // liquidity token contract. The liquidity tokens are CW20 tokens -// let pair_info: astroport::asset::PairInfo = deps -// .querier -// .query_wasm_smart(pool_address.to_string(), &astroport::pair::QueryMsg::Pair {})?; - -// // We query our own liquidity token balance -// let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( -// pair_info.clone().liquidity_token, -// &cw20::Cw20QueryMsg::Balance { -// address: env.contract.address.to_string(), -// }, -// )?; - -// // if no lp tokens are available, no point to ragequit -// if liquidity_token_balance.balance.is_zero() { -// return Err(ContractError::NoLpTokensAvailable {}) -// } + // only the involved parties can initiate the ragequit + let parties = PARTIES_CONFIG.load(deps.storage)?; + let rq_party = parties.match_caller_party(info.sender.to_string())?; + + let mut rq_terms = match RAGEQUIT_CONFIG.load(deps.storage)? { + // if ragequit is not enabled for this covenant we error + RagequitConfig::Disabled => return Err(ContractError::RagequitDisabled {}), + RagequitConfig::Enabled(terms) => { + if terms.active { + return Err(ContractError::RagequitAlreadyActive {}) + } + terms + }, + }; + + let pool_address = POOL_ADDRESS.load(deps.storage)?; + + // We query the pool to get the contract for the pool info + // The pool info is required to fetch the address of the + // liquidity token contract. The liquidity tokens are CW20 tokens + let pair_info: astroport::asset::PairInfo = deps + .querier + .query_wasm_smart(pool_address.to_string(), &astroport::pair::QueryMsg::Pair {})?; + + // We query our own liquidity token balance + let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( + pair_info.clone().liquidity_token, + &cw20::Cw20QueryMsg::Balance { + address: env.contract.address.to_string(), + }, + )?; + + // if no lp tokens are available, no point to ragequit + if liquidity_token_balance.balance.is_zero() { + return Err(ContractError::NoLpTokensAvailable {}) + } -// // activate the ragequit in terms -// rq_terms.active = true; + // activate the ragequit in terms + rq_terms.active = true; -// // apply the ragequit penalty -// // TODO: let parties = parties.apply_ragequit_penalty(rq_party.clone(), rq_terms.penalty)?; -// // let rq_party = parties.get_party_by_addr(rq_party.addr)?; + // apply the ragequit penalty + // TODO: let parties = parties.apply_ragequit_penalty(rq_party.clone(), rq_terms.penalty)?; + // let rq_party = parties.get_party_by_addr(rq_party.addr)?; -// // generate the withdraw_liquidity hook for the ragequitting party -// // let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; -// // let withdraw_msg = &Cw20ExecuteMsg::Send { -// // contract: pool_address.to_string(), -// // // take the ragequitting party share of the position -// // amount: liquidity_token_balance.balance.checked_mul_floor(rq_party.share) -// // .map_err(|_| ContractError::FractionMulError {})?, -// // msg: to_binary(withdraw_liquidity_hook)?, -// // }; - -// // // update the state to reflect ragequit -// // CONTRACT_STATE.save(deps.storage, &crate::msg::ContractState::Ragequit)?; - -// // TODO: need some kind of state representation of pending withdrawals -// // to distinguish allocations of ragequitting party from the non-rq party + // generate the withdraw_liquidity hook for the ragequitting party + // let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; + // let withdraw_msg = &Cw20ExecuteMsg::Send { + // contract: pool_address.to_string(), + // // take the ragequitting party share of the position + // amount: liquidity_token_balance.balance.checked_mul_floor(rq_party.share) + // .map_err(|_| ContractError::FractionMulError {})?, + // msg: to_binary(withdraw_liquidity_hook)?, + // }; + + // // update the state to reflect ragequit + // CONTRACT_STATE.save(deps.storage, &crate::msg::ContractState::Ragequit)?; + + // TODO: need some kind of state representation of pending withdrawals + // to distinguish allocations of ragequitting party from the non-rq party -// Ok(Response::default() -// .add_attribute("method", "ragequit") -// .add_attribute("caller", rq_party.addr) -// // .add_message( -// // CosmosMsg::Wasm(WasmMsg::Execute { -// // contract_addr: pair_info.liquidity_token.to_string(), -// // msg: to_binary(withdraw_msg)?, -// // funds: vec![], -// // }) -// // ) -// ) -// } - -// fn try_claim( -// deps: DepsMut, -// env: Env, -// info: MessageInfo, -// ) -> Result { - -// Ok(Response::default()) -// } + Ok(Response::default() + .add_attribute("method", "ragequit") + .add_attribute("caller", rq_party.addr) + // .add_message( + // CosmosMsg::Wasm(WasmMsg::Execute { + // contract_addr: pair_info.liquidity_token.to_string(), + // msg: to_binary(withdraw_msg)?, + // funds: vec![], + // }) + // ) + ) + */ + +} #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/two-party-pol-holder/src/error.rs b/contracts/two-party-pol-holder/src/error.rs index e8f4669f..45594691 100644 --- a/contracts/two-party-pol-holder/src/error.rs +++ b/contracts/two-party-pol-holder/src/error.rs @@ -10,6 +10,9 @@ pub enum ContractError { #[error("unauthorized")] Unauthorized {}, + #[error("covenant is not in active state")] + NotActive {}, + #[error("both parties have not deposited")] InsufficientDeposits {}, diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index a3501634..cddef381 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -55,12 +55,12 @@ pub enum ContractState { /// of this contract that indicates an active LP position. /// TODO: think about whether this is a fair assumption to make. Active, - // /// one of the parties have initiated ragequit. - // /// party with an active position is free to exit at any time. - // Ragequit, - // /// covenant has reached its expiration date. + /// one of the parties have initiated ragequit. + /// party with an active position is free to exit at any time. + Ragequit, + /// covenant has reached its expiration date. Expired, - // /// underlying funds have been withdrawn. + /// underlying funds have been withdrawn. Complete, } diff --git a/contracts/two-party-pol-holder/src/suite_tests/suite.rs b/contracts/two-party-pol-holder/src/suite_tests/suite.rs index 45b65538..92654836 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/suite.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/suite.rs @@ -124,6 +124,15 @@ impl Suite { &[], ) } + + pub fn rq(&mut self, caller: &str) -> Result { + self.app.execute_contract( + Addr::unchecked(caller), + self.holder.clone(), + &ExecuteMsg::Ragequit {}, + &[], + ) + } } // queries diff --git a/contracts/two-party-pol-holder/src/suite_tests/tests.rs b/contracts/two-party-pol-holder/src/suite_tests/tests.rs index 5868d750..1f302d96 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/tests.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/tests.rs @@ -3,7 +3,7 @@ use covenant_utils::LockupConfig; use crate::{suite_tests::suite::{CLOCK_ADDR, POOL, NEXT_CONTRACT, PARTY_A_ROUTER, PARTY_B_ROUTER, get_default_block_info}, msg::ContractState, error::ContractError}; -use super::suite::SuiteBuilder; +use super::suite::{SuiteBuilder, PARTY_A_ADDR}; #[test] fn test_instantiate_happy_and_query_all() { @@ -199,11 +199,53 @@ fn test_holder_active_expired_tick_advances_state() { } #[test] -fn test_holder_instantiated_ragequit_fails() { - unimplemented!() +fn test_holder_ragequit_not_in_active_state() { + let current_timestamp = get_default_block_info(); + let mut suite = SuiteBuilder::default() + .with_lockup_config(LockupConfig::Time(current_timestamp.time.plus_minutes(200))) + .build(); + + // both parties fulfill their parts of the covenant + let coin_a = suite.get_party_a_coin(Uint128::new(500)); + let coin_b = suite.get_party_b_coin(Uint128::new(500)); + suite.fund_coin(coin_a); + suite.fund_coin(coin_b); + + // we tick the holder to deposit the funds and activate + suite.tick(CLOCK_ADDR).unwrap(); + + suite.pass_minutes(300); + + // advance the state to expired + suite.tick(CLOCK_ADDR).unwrap(); + + let err: ContractError = suite.rq(PARTY_A_ADDR).unwrap_err().downcast().unwrap(); + let state = suite.query_contract_state(); + + assert_eq!(ContractState::Expired {}, state); + assert_eq!(ContractError::NotActive {}, err); } #[test] -fn test_holder_active_ragequit_party_double_claim() { - unimplemented!() +// #[should_panic(expected = "covenant is not in active state")] +fn test_holder_ragequit_active_but_expired() { + let current_timestamp = get_default_block_info(); + let mut suite = SuiteBuilder::default() + .with_lockup_config(LockupConfig::Time(current_timestamp.time.plus_minutes(200))) + .build(); + + // both parties fulfill their parts of the covenant + let coin_a = suite.get_party_a_coin(Uint128::new(500)); + let coin_b = suite.get_party_b_coin(Uint128::new(500)); + suite.fund_coin(coin_a); + suite.fund_coin(coin_b); + + // we tick the holder to deposit the funds and activate + suite.tick(CLOCK_ADDR).unwrap(); + + suite.pass_minutes(300); + + let err: ContractError = suite.rq(PARTY_A_ADDR).unwrap_err().downcast().unwrap(); + + assert_eq!(ContractError::NotActive {}, err); } From 7f6a3b1c36db810814d864d5c41ac8843149b7b1 Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 22 Sep 2023 15:11:12 +0200 Subject: [PATCH 104/586] draft rq --- .../two-party-pol-holder/src/contract.rs | 130 +++++++++--------- contracts/two-party-pol-holder/src/error.rs | 5 +- contracts/two-party-pol-holder/src/msg.rs | 73 +++++++++- .../src/suite_tests/suite.rs | 29 ++-- .../src/suite_tests/tests.rs | 65 ++++++++- 5 files changed, 215 insertions(+), 87 deletions(-) diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index 99167a52..c716c255 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -1,5 +1,7 @@ -use astroport::pair::Cw20HookMsg; -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, BankMsg, CosmosMsg, WasmMsg}; +use std::ops::Mul; + +use astroport::{pair::Cw20HookMsg, DecimalCheckedOps, asset::Asset}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, BankMsg, CosmosMsg, WasmMsg, Coin}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -7,7 +9,7 @@ use covenant_utils::LockupConfig; use cw2::set_contract_version; use cw20::{BalanceResponse, Cw20ExecuteMsg}; -use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, ContractState}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, CONTRACT_STATE, DEPOSIT_DEADLINE, POOL_ADDRESS, PARTY_A_ROUTER, PARTY_B_ROUTER, COVENANT_CONFIG}, error::ContractError}; +use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, ContractState, RagequitConfig, RagequitState, TwoPartyPolCovenantConfig}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, CONTRACT_STATE, DEPOSIT_DEADLINE, POOL_ADDRESS, PARTY_A_ROUTER, PARTY_B_ROUTER, COVENANT_CONFIG}, error::ContractError}; const CONTRACT_NAME: &str = "crates.io:covenant-two-party-pol-holder"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -95,14 +97,14 @@ fn try_deposit( // assert the balances let party_a_bal = deps.querier.query_balance( env.contract.address.to_string(), - config.party_a_contribution.denom)?; + config.party_a.party_contibution.denom)?; let party_b_bal = deps.querier.query_balance( env.contract.address.to_string(), - config.party_b_contribution.denom)?; + config.party_b.party_contibution.denom)?; let deposit_deadline = DEPOSIT_DEADLINE.load(deps.storage)?; - let party_a_fulfilled = config.party_a_contribution.amount < party_a_bal.amount; - let party_b_fulfilled = config.party_b_contribution.amount < party_b_bal.amount; + let party_a_fulfilled = config.party_a.party_contibution.amount < party_a_bal.amount; + let party_b_fulfilled = config.party_b.party_contibution.amount < party_b_bal.amount; // note: even if both parties deposit their funds in time, // it is important to trigger this method before the expiry block @@ -195,49 +197,44 @@ fn try_ragequit( env: Env, info: MessageInfo, ) -> Result { - + // first we error out if ragequit is disabled + let mut rq_config = match RAGEQUIT_CONFIG.load(deps.storage)? { + RagequitConfig::Disabled => return Err(ContractError::RagequitDisabled {}), + RagequitConfig::Enabled(terms) => terms, + }; let current_state = CONTRACT_STATE.load(deps.storage)?; let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; + let mut covenant_config = COVENANT_CONFIG.load(deps.storage)?; + let pool = POOL_ADDRESS.load(deps.storage)?; // ragequit is only possible when contract is in Active state. - // we also validate an edge case where it did expire but - // did not receive a tick yet - if current_state != ContractState::Active || lockup_config.is_expired(env.block) { + if current_state != ContractState::Active { return Err(ContractError::NotActive {}) } - - Ok(Response::default()) - - /* - // if lockup period had passed, just claim the tokens instead of ragequitting - let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; + // we also validate an edge case where it did expire but + // did not receive a tick yet. tick is then required to advance. if lockup_config.is_expired(env.block) { - return Err(ContractError::RagequitWithLockupPassed {}) - } - - // only the involved parties can initiate the ragequit - let parties = PARTIES_CONFIG.load(deps.storage)?; - let rq_party = parties.match_caller_party(info.sender.to_string())?; + return Err(ContractError::Expired {}) + } - let mut rq_terms = match RAGEQUIT_CONFIG.load(deps.storage)? { - // if ragequit is not enabled for this covenant we error - RagequitConfig::Disabled => return Err(ContractError::RagequitDisabled {}), - RagequitConfig::Enabled(terms) => { - if terms.active { - return Err(ContractError::RagequitAlreadyActive {}) - } - terms - }, - }; + // authorize the message sender + let (mut rq_party, mut counterparty) = covenant_config.authorize_sender(info.sender)?; - let pool_address = POOL_ADDRESS.load(deps.storage)?; + // after all validations we are ready to perform the ragequit + // 3. withdrawing the ragequitting party allocation + // 4. advancing the contract state to ragequit + + // first we apply the ragequit penalty on both parties allocations + rq_party.allocation -= rq_config.penalty; + counterparty.allocation += rq_config.penalty; + covenant_config.update_parties(rq_party.clone(), counterparty.clone()); // We query the pool to get the contract for the pool info // The pool info is required to fetch the address of the // liquidity token contract. The liquidity tokens are CW20 tokens let pair_info: astroport::asset::PairInfo = deps .querier - .query_wasm_smart(pool_address.to_string(), &astroport::pair::QueryMsg::Pair {})?; + .query_wasm_smart(pool.to_string(), &astroport::pair::QueryMsg::Pair {})?; // We query our own liquidity token balance let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( @@ -252,42 +249,41 @@ fn try_ragequit( return Err(ContractError::NoLpTokensAvailable {}) } - // activate the ragequit in terms - rq_terms.active = true; - - // apply the ragequit penalty - // TODO: let parties = parties.apply_ragequit_penalty(rq_party.clone(), rq_terms.penalty)?; - // let rq_party = parties.get_party_by_addr(rq_party.addr)?; + // we figure out the amounts of underlying tokens that rq party would receive + let rq_party_lp_token_amount = liquidity_token_balance.balance + .checked_mul_floor(rq_party.allocation) + .map_err(|_| ContractError::FractionMulError {})?; + let rq_entitled_assets: Vec = deps.querier + .query_wasm_smart( + pool.to_string(), + &astroport::pair::QueryMsg::Share { amount: rq_party_lp_token_amount }, + )?; + // reflect the ragequit in ragequit config + rq_config.state = Some(RagequitState::from_share_response(rq_entitled_assets, rq_party.clone())?); + // generate the withdraw_liquidity hook for the ragequitting party - // let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; - // let withdraw_msg = &Cw20ExecuteMsg::Send { - // contract: pool_address.to_string(), - // // take the ragequitting party share of the position - // amount: liquidity_token_balance.balance.checked_mul_floor(rq_party.share) - // .map_err(|_| ContractError::FractionMulError {})?, - // msg: to_binary(withdraw_liquidity_hook)?, - // }; - - // // update the state to reflect ragequit - // CONTRACT_STATE.save(deps.storage, &crate::msg::ContractState::Ragequit)?; - - // TODO: need some kind of state representation of pending withdrawals - // to distinguish allocations of ragequitting party from the non-rq party - + let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; + let withdraw_msg = &Cw20ExecuteMsg::Send { + contract: pool.to_string(), + amount: rq_party_lp_token_amount, + msg: to_binary(withdraw_liquidity_hook)?, + }; + + // update the states + RAGEQUIT_CONFIG.save(deps.storage, &RagequitConfig::Enabled(rq_config))?; + COVENANT_CONFIG.save(deps.storage, &covenant_config)?; + CONTRACT_STATE.save(deps.storage, &ContractState::Ragequit)?; + Ok(Response::default() .add_attribute("method", "ragequit") - .add_attribute("caller", rq_party.addr) - // .add_message( - // CosmosMsg::Wasm(WasmMsg::Execute { - // contract_addr: pair_info.liquidity_token.to_string(), - // msg: to_binary(withdraw_msg)?, - // funds: vec![], - // }) - // ) - ) - */ - + .add_attribute("caller", rq_party.party_addr) + .add_message(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: pair_info.liquidity_token.to_string(), + msg: to_binary(withdraw_msg)?, + funds: vec![], + }) + )) } diff --git a/contracts/two-party-pol-holder/src/error.rs b/contracts/two-party-pol-holder/src/error.rs index 45594691..4961a403 100644 --- a/contracts/two-party-pol-holder/src/error.rs +++ b/contracts/two-party-pol-holder/src/error.rs @@ -13,6 +13,9 @@ pub enum ContractError { #[error("covenant is not in active state")] NotActive {}, + #[error("covenant is active but expired; tick to proceed")] + Expired {}, + #[error("both parties have not deposited")] InsufficientDeposits {}, @@ -41,7 +44,7 @@ pub enum ContractError { RagequitWithLockupPassed {}, #[error("ragequit already active")] - RagequitAlreadyActive {}, + RagequitInProgress {}, #[error("no lp tokens available")] NoLpTokensAvailable {}, diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index cddef381..0ba919a8 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -1,8 +1,13 @@ +use std::ops::Deref; + +use astroport::asset::Asset; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Decimal, Attribute, Uint128, Coin}; +use cosmwasm_std::{Addr, Decimal, Attribute, Uint128, Coin, StdError}; use covenant_macros::{clocked, covenant_clock_address, covenant_next_contract}; use covenant_utils::LockupConfig; +use crate::error::ContractError; + #[cw_serde] pub struct InstantiateMsg { pub clock_address: String, @@ -32,10 +37,44 @@ impl InstantiateMsg { #[cw_serde] pub struct TwoPartyPolCovenantConfig { - pub party_a_contribution: Coin, - pub party_b_contribution: Coin, + pub party_a: TwoPartyPolCovenantParty, + pub party_b: TwoPartyPolCovenantParty, +} + +impl TwoPartyPolCovenantConfig { + pub fn update_parties(&mut self, p1: TwoPartyPolCovenantParty, p2: TwoPartyPolCovenantParty) { + if self.party_a.party_addr == p1.party_addr { + self.party_a = p1; + self.party_b = p2; + } else { + self.party_a = p2; + self.party_b = p1; + } + } } +#[cw_serde] +pub struct TwoPartyPolCovenantParty { + pub party_contibution: Coin, + pub party_addr: String, + pub allocation: Decimal, + // TODO: consider adding a boxed counterparty for convenience? +} + +impl TwoPartyPolCovenantConfig { + /// if authorized, returns (party, counterparty). otherwise errors + pub fn authorize_sender(&self, sender: Addr) -> Result<(TwoPartyPolCovenantParty, TwoPartyPolCovenantParty), ContractError> { + let party_a = self.party_a.clone(); + let party_b = self.party_b.clone(); + if party_a.party_addr == sender { + Ok((party_a, party_b)) + } else if party_b.party_addr == sender { + Ok((party_b, party_a)) + } else { + Err(ContractError::Unauthorized {}) + } + } +} #[clocked] #[cw_serde] @@ -189,7 +228,6 @@ impl RagequitConfig { RagequitConfig::Enabled(c) => vec![ Attribute::new("ragequit_config", "enabled"), Attribute::new("ragequit_penalty", c.penalty.to_string()), - Attribute::new("ragequit_active", c.active.to_string()), ], } } @@ -202,9 +240,30 @@ pub struct RagequitTerms { /// added to the counterparty that did not initiate /// the ragequit pub penalty: Decimal, - /// bool flag to indicate whether ragequit had been - /// initiated - pub active: bool, + /// optional rq state. none indicates no ragequit. + /// some holds the ragequit related config + pub state: Option, +} + +#[cw_serde] +pub struct RagequitState { + pub coins: Vec, + pub rq_party: TwoPartyPolCovenantParty, +} + +impl RagequitState { + pub fn from_share_response(assets: Vec, rq_party: TwoPartyPolCovenantParty) -> Result { + let mut rq_coins: Vec = vec![]; + for asset in assets { + let coin = asset.to_coin()?; + + } + + Ok(RagequitState { + coins: rq_coins, + rq_party, + }) + } } // / enum based configuration of the lockup period. diff --git a/contracts/two-party-pol-holder/src/suite_tests/suite.rs b/contracts/two-party-pol-holder/src/suite_tests/suite.rs index 92654836..69c41eaa 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/suite.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/suite.rs @@ -1,6 +1,6 @@ -use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, RagequitConfig, TwoPartyPolCovenantConfig}; +use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, RagequitConfig, TwoPartyPolCovenantConfig, TwoPartyPolCovenantParty}; use cosmos_sdk_proto::tendermint::types::Block; -use cosmwasm_std::{Addr, Coin, Uint128, BlockInfo, Uint64, Timestamp}; +use cosmwasm_std::{Addr, Coin, Uint128, BlockInfo, Uint64, Timestamp, Decimal}; use covenant_utils::{ CovenantPartiesConfig, CovenantParty, LockupConfig, PolCovenantTerms, }; @@ -51,13 +51,21 @@ impl Default for SuiteBuilder { party_a_router: PARTY_A_ROUTER.to_string(), party_b_router: PARTY_B_ROUTER.to_string(), covenant_config: TwoPartyPolCovenantConfig { - party_a_contribution: Coin { - denom: DENOM_A.to_string(), - amount: Uint128::new(200), + party_a: TwoPartyPolCovenantParty { + party_contibution: Coin { + denom: DENOM_A.to_string(), + amount: Uint128::new(200), + }, + party_addr: PARTY_A_ADDR.to_string(), + allocation: Decimal::from_ratio(Uint128::one(), Uint128::new(2)), }, - party_b_contribution: Coin { - denom: DENOM_B.to_string(), - amount: Uint128::new(100), + party_b: TwoPartyPolCovenantParty { + party_contibution: Coin { + denom: DENOM_A.to_string(), + amount: Uint128::new(100), + }, + party_addr: PARTY_B_ADDR.to_string(), + allocation: Decimal::from_ratio(Uint128::one(), Uint128::new(2)), }, }, }, @@ -72,6 +80,11 @@ impl SuiteBuilder { self } + pub fn with_ragequit_config(mut self, config: RagequitConfig) -> Self { + self.instantiate.ragequit_config = config; + self + } + pub fn with_deposit_deadline(mut self, config: LockupConfig) -> Self { self.instantiate.deposit_deadline = Some(config); self diff --git a/contracts/two-party-pol-holder/src/suite_tests/tests.rs b/contracts/two-party-pol-holder/src/suite_tests/tests.rs index 1f302d96..770b35ec 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/tests.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/tests.rs @@ -1,7 +1,7 @@ -use cosmwasm_std::{Timestamp, Uint128, Event, Attribute}; +use cosmwasm_std::{Timestamp, Uint128, Event, Attribute, Decimal256, Decimal}; use covenant_utils::LockupConfig; -use crate::{suite_tests::suite::{CLOCK_ADDR, POOL, NEXT_CONTRACT, PARTY_A_ROUTER, PARTY_B_ROUTER, get_default_block_info}, msg::ContractState, error::ContractError}; +use crate::{suite_tests::suite::{CLOCK_ADDR, POOL, NEXT_CONTRACT, PARTY_A_ROUTER, PARTY_B_ROUTER, get_default_block_info}, msg::{ContractState, RagequitConfig, RagequitTerms}, error::ContractError}; use super::suite::{SuiteBuilder, PARTY_A_ADDR}; @@ -198,6 +198,63 @@ fn test_holder_active_expired_tick_advances_state() { assert_eq!(Uint128::new(500), splitter_balance_a); } +#[test] +fn test_holder_ragequit_disabled() { + let mut suite = SuiteBuilder::default() + .with_ragequit_config(RagequitConfig::Disabled) + .build(); + + // both parties fulfill their parts of the covenant + let coin_a = suite.get_party_a_coin(Uint128::new(500)); + let coin_b = suite.get_party_b_coin(Uint128::new(500)); + suite.fund_coin(coin_a); + suite.fund_coin(coin_b); + + // we tick the holder to deposit the funds and activate + suite.tick(CLOCK_ADDR).unwrap(); + + suite.pass_minutes(300); + + // advance the state to expired + suite.tick(CLOCK_ADDR).unwrap(); + + let err: ContractError = suite.rq(PARTY_A_ADDR).unwrap_err().downcast().unwrap(); + let state = suite.query_contract_state(); + + assert_eq!(ContractState::Active {}, state); + assert_eq!(ContractError::NotActive {}, err); +} + +#[test] +fn test_holder_ragequit_unauthorized() { + let mut suite = SuiteBuilder::default() + .with_ragequit_config(RagequitConfig::Enabled(RagequitTerms { + penalty: Decimal::from_ratio(Uint128::one(), Uint128::new(10)), + state: None, + })) + .build(); + + // both parties fulfill their parts of the covenant + let coin_a = suite.get_party_a_coin(Uint128::new(500)); + let coin_b = suite.get_party_b_coin(Uint128::new(500)); + suite.fund_coin(coin_a); + suite.fund_coin(coin_b); + + // we tick the holder to deposit the funds and activate + suite.tick(CLOCK_ADDR).unwrap(); + + suite.pass_minutes(50); + + // advance the state to expired + suite.tick(CLOCK_ADDR).unwrap(); + + let err: ContractError = suite.rq("random_user").unwrap_err().downcast().unwrap(); + let state = suite.query_contract_state(); + + assert_eq!(ContractState::Active {}, state); + assert_eq!(ContractError::Unauthorized {}, err); +} + #[test] fn test_holder_ragequit_not_in_active_state() { let current_timestamp = get_default_block_info(); @@ -223,7 +280,7 @@ fn test_holder_ragequit_not_in_active_state() { let state = suite.query_contract_state(); assert_eq!(ContractState::Expired {}, state); - assert_eq!(ContractError::NotActive {}, err); + assert_eq!(ContractError::RagequitDisabled {}, err); } #[test] @@ -247,5 +304,5 @@ fn test_holder_ragequit_active_but_expired() { let err: ContractError = suite.rq(PARTY_A_ADDR).unwrap_err().downcast().unwrap(); - assert_eq!(ContractError::NotActive {}, err); + assert_eq!(ContractError::Expired {}, err); } From cd4cd64780dba33a4400e7956891598e8d1547c2 Mon Sep 17 00:00:00 2001 From: bekauz Date: Sun, 24 Sep 2023 22:21:59 +0200 Subject: [PATCH 105/586] holder unit tests mocking astro contracts --- .../two-party-pol-holder/src/contract.rs | 12 ++--- .../src/suite_tests/mod.rs | 53 +++++++++++++++++-- .../src/suite_tests/suite.rs | 44 ++++++++++++++- .../src/suite_tests/tests.rs | 49 +++++++++++++++++ 4 files changed, 145 insertions(+), 13 deletions(-) diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index c716c255..d8ca4a50 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -152,7 +152,7 @@ fn try_deposit( // if deposit deadline is not yet due and both parties did not fulfill we error return Err(ContractError::InsufficientDeposits {}) } - + // LiquidPooler is the next contract let next_contract = NEXT_CONTRACT.load(deps.storage)?; let msg = BankMsg::Send { @@ -219,11 +219,7 @@ fn try_ragequit( // authorize the message sender let (mut rq_party, mut counterparty) = covenant_config.authorize_sender(info.sender)?; - - // after all validations we are ready to perform the ragequit - // 3. withdrawing the ragequitting party allocation - // 4. advancing the contract state to ragequit - + // after all validations we are ready to perform the ragequit. // first we apply the ragequit penalty on both parties allocations rq_party.allocation -= rq_config.penalty; counterparty.allocation += rq_config.penalty; @@ -235,6 +231,7 @@ fn try_ragequit( let pair_info: astroport::asset::PairInfo = deps .querier .query_wasm_smart(pool.to_string(), &astroport::pair::QueryMsg::Pair {})?; + println!("pair info: {:?}", pair_info); // We query our own liquidity token balance let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( @@ -243,6 +240,7 @@ fn try_ragequit( address: env.contract.address.to_string(), }, )?; + println!("liquidity_token_balance: {:?}", liquidity_token_balance); // if no lp tokens are available, no point to ragequit if liquidity_token_balance.balance.is_zero() { @@ -258,7 +256,7 @@ fn try_ragequit( pool.to_string(), &astroport::pair::QueryMsg::Share { amount: rq_party_lp_token_amount }, )?; - + println!("entitled assets: {:?}", rq_entitled_assets); // reflect the ragequit in ragequit config rq_config.state = Some(RagequitState::from_share_response(rq_entitled_assets, rq_party.clone())?); diff --git a/contracts/two-party-pol-holder/src/suite_tests/mod.rs b/contracts/two-party-pol-holder/src/suite_tests/mod.rs index a417a4d8..79316e72 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/mod.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/mod.rs @@ -1,7 +1,8 @@ -use astroport::asset::PairInfo; +use astroport::asset::{PairInfo, Asset}; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{to_binary, Binary, Deps, Empty, Env, StdResult, Addr}; +use cosmwasm_std::{to_binary, Binary, Deps, Empty, Env, StdResult, Addr, Uint128, DepsMut, MessageInfo, Response, BankMsg, Coin}; use covenant_macros::covenant_deposit_address; +use cw20::{Cw20QueryMsg, BalanceResponse, Cw20ExecuteMsg}; use cw_multi_test::{Contract, ContractWrapper}; mod suite; @@ -29,6 +30,10 @@ pub fn mock_deposit_contract() -> Box> { #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; +use crate::error::ContractError; + +use self::suite::{DENOM_A, DENOM_B}; + #[covenant_deposit_address] #[cw_serde] #[derive(QueryResponses)] @@ -51,16 +56,56 @@ pub fn mock_astro_pool_contract() -> Box> { Box::new(contract) } +pub fn mock_astro_lp_token_contract() -> Box> { + let contract = ContractWrapper::new( + execute_lp_token, + crate::contract::instantiate, + query_astro_lp_token, + ); + Box::new(contract) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute_lp_token( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: Cw20ExecuteMsg, +) -> Result { + let msg = BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![ + Coin::new(200, DENOM_A), + Coin::new(200, DENOM_B), + ], + }; + Ok(Response::default().add_message(msg)) +} #[cfg_attr(not(feature = "library"), entry_point)] pub fn query_astro_pool(_deps: Deps, _env: Env, msg: astroport::pair::QueryMsg) -> StdResult { match msg { astroport::pair::QueryMsg::Pair {} => Ok(to_binary(&PairInfo { asset_infos: vec![], - contract_addr: Addr::unchecked("lp-token"), - liquidity_token: Addr::unchecked("lp-token"), + contract_addr: Addr::unchecked("contract0"), + liquidity_token: Addr::unchecked("contract0"), pair_type: astroport::factory::PairType::Xyk { }, })?), + astroport::pair::QueryMsg::Share { amount } => Ok(to_binary(&vec![ + Asset { info: astroport::asset::AssetInfo::NativeToken { denom: DENOM_A.to_string() }, amount: Uint128::new(200) }, + Asset { info: astroport::asset::AssetInfo::NativeToken { denom: DENOM_B.to_string() }, amount: Uint128::new(200) }, + + ])?), + _ => Ok(to_binary(&"-")?), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query_astro_lp_token(_deps: Deps, _env: Env, msg: cw20::Cw20QueryMsg) -> StdResult { + match msg { + Cw20QueryMsg::Balance { address } => Ok(to_binary(&BalanceResponse { + balance: Uint128::new(100) + })?), _ => Ok(to_binary(&"-")?), } } diff --git a/contracts/two-party-pol-holder/src/suite_tests/suite.rs b/contracts/two-party-pol-holder/src/suite_tests/suite.rs index 69c41eaa..112926be 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/suite.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/suite.rs @@ -6,7 +6,7 @@ use covenant_utils::{ }; use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; -use super::{mock_deposit_contract, two_party_pol_holder_contract}; +use super::{mock_deposit_contract, two_party_pol_holder_contract, mock_astro_pool_contract, mock_astro_lp_token_contract}; pub const ADMIN: &str = "admin"; @@ -61,7 +61,7 @@ impl Default for SuiteBuilder { }, party_b: TwoPartyPolCovenantParty { party_contibution: Coin { - denom: DENOM_A.to_string(), + denom: DENOM_B.to_string(), amount: Uint128::new(100), }, party_addr: PARTY_B_ADDR.to_string(), @@ -94,6 +94,46 @@ impl SuiteBuilder { let mut app = self.app; let holder_code = app.store_code(two_party_pol_holder_contract()); let mock_deposit_code = app.store_code(mock_deposit_contract()); + let astro_pool_mock_code = app.store_code(mock_astro_pool_contract()); + let astro_lp_token_mock_code = app.store_code(mock_astro_lp_token_contract()); + let astro_lp = app.instantiate_contract( + astro_lp_token_mock_code, + Addr::unchecked(ADMIN), + &self.instantiate, + &[], + "astro_mock_lp_code", + Some(ADMIN.to_string()), + ) + .unwrap(); + + let denom_b = Coin { + denom: DENOM_B.to_string(), + amount: Uint128::new(200), + }; + let denom_a = Coin { + denom: DENOM_A.to_string(), + amount: Uint128::new(200), + }; + app + .sudo(SudoMsg::Bank(cw_multi_test::BankSudo::Mint { + to_address: astro_lp.to_string(), + amount: vec![denom_a, denom_b], + })) + .unwrap(); + + println!("lp token: {:?}", astro_lp); + + let astro_mock = app.instantiate_contract( + astro_pool_mock_code, + Addr::unchecked(ADMIN), + &self.instantiate, + &[], + "astro_mock", + Some(ADMIN.to_string()), + ) + .unwrap(); + + self.instantiate.pool_address = astro_mock.to_string(); let mock_deposit = app .instantiate_contract( diff --git a/contracts/two-party-pol-holder/src/suite_tests/tests.rs b/contracts/two-party-pol-holder/src/suite_tests/tests.rs index 770b35ec..31fac3c1 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/tests.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/tests.rs @@ -306,3 +306,52 @@ fn test_holder_ragequit_active_but_expired() { assert_eq!(ContractError::Expired {}, err); } + +#[test] +fn test_ragequit_happy_flow() { + +} + +#[test] +fn test_ragequit_double_claim_fails() { + let current_timestamp = get_default_block_info(); + let mut suite = SuiteBuilder::default() + .with_ragequit_config(RagequitConfig::Enabled(RagequitTerms { + penalty: Decimal::from_ratio(Uint128::one(), Uint128::new(10)), + state: None, + })) + .with_lockup_config(LockupConfig::Time(current_timestamp.time.plus_minutes(200))) + .build(); + + // both parties fulfill their parts of the covenant + let coin_a = suite.get_party_a_coin(Uint128::new(500)); + let coin_b = suite.get_party_b_coin(Uint128::new(500)); + suite.fund_coin(coin_a); + suite.fund_coin(coin_b); + + // we tick the holder to deposit the funds and activate + suite.tick(CLOCK_ADDR).unwrap(); + + suite.pass_minutes(50); + + // advance the state to expired + suite.tick(CLOCK_ADDR).unwrap(); + + let holder_a_balance = suite.get_denom_a_balance(suite.holder.to_string()); + let holder_b_balance = suite.get_denom_a_balance(suite.holder.to_string()); + println!("holder_a_balance: {:?}", holder_a_balance); + println!("holder_b_balance: {:?}", holder_b_balance); + + suite.rq(PARTY_A_ADDR).unwrap(); + + let holder_a_balance = suite.get_denom_a_balance(suite.holder.to_string()); + let holder_b_balance = suite.get_denom_a_balance(suite.holder.to_string()); + println!("holder_a_balance: {:?}", holder_a_balance); + println!("holder_b_balance: {:?}", holder_b_balance); + + let state = suite.query_contract_state(); + + assert_eq!(ContractState::Ragequit {}, state); + +} + From aa257975b30d3c39e6cfc3689f73554408f33d24 Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 25 Sep 2023 17:22:14 +0200 Subject: [PATCH 106/586] counterparty claim after ragequit leads to completion --- contracts/two-party-pol-holder/README.md | 6 + .../two-party-pol-holder/src/contract.rs | 188 ++++++++++++++---- contracts/two-party-pol-holder/src/error.rs | 3 + contracts/two-party-pol-holder/src/msg.rs | 43 ++-- contracts/two-party-pol-holder/src/state.rs | 4 +- .../src/suite_tests/suite.rs | 44 ++-- .../src/suite_tests/tests.rs | 95 ++++++--- 7 files changed, 287 insertions(+), 96 deletions(-) diff --git a/contracts/two-party-pol-holder/README.md b/contracts/two-party-pol-holder/README.md index a57fbb44..7c0e64fd 100644 --- a/contracts/two-party-pol-holder/README.md +++ b/contracts/two-party-pol-holder/README.md @@ -22,6 +22,12 @@ Ragequitting party is subject to a percentage based penalty agreed upon instanti Holder then withdraws the allocation of the ragequitting party (minus the penalty) and forwards the funds to the party. Counterparty remains in an active position. +Ragequit breaks the regular covenant flow in the following way: + +- covenant is no longer subject to expiration +- splitter module no longer gets instantiated, meaning that any pre-agreed upon token distribution split is void + - both parties receive a 50/50 split of the underlying denoms + ### Updates Both parties are free to update their respective whitelisted addresses and do not need counterparty permission to do so. diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index d8ca4a50..b6b580c8 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -1,7 +1,6 @@ -use std::ops::Mul; -use astroport::{pair::Cw20HookMsg, DecimalCheckedOps, asset::Asset}; -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, BankMsg, CosmosMsg, WasmMsg, Coin}; +use astroport::{pair::Cw20HookMsg, asset::Asset}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, BankMsg, CosmosMsg, WasmMsg, Decimal, Coin}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -9,7 +8,7 @@ use covenant_utils::LockupConfig; use cw2::set_contract_version; use cw20::{BalanceResponse, Cw20ExecuteMsg}; -use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, ContractState, RagequitConfig, RagequitState, TwoPartyPolCovenantConfig}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, CONTRACT_STATE, DEPOSIT_DEADLINE, POOL_ADDRESS, PARTY_A_ROUTER, PARTY_B_ROUTER, COVENANT_CONFIG}, error::ContractError}; +use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, ContractState, RagequitConfig, RagequitState}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, CONTRACT_STATE, DEPOSIT_DEADLINE, POOL_ADDRESS, COVENANT_CONFIG}, error::ContractError}; const CONTRACT_NAME: &str = "crates.io:covenant-two-party-pol-holder"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -27,16 +26,14 @@ pub fn instantiate( let pool_addr = deps.api.addr_validate(&msg.pool_address)?; let next_contract = deps.api.addr_validate(&msg.next_contract)?; let clock_addr = deps.api.addr_validate(&msg.clock_address)?; - let party_a_router = deps.api.addr_validate(&msg.party_a_router)?; - let party_b_router = deps.api.addr_validate(&msg.party_b_router)?; + let party_a_router = deps.api.addr_validate(&msg.covenant_config.party_a.router)?; + let party_b_router = deps.api.addr_validate(&msg.covenant_config.party_b.router)?; POOL_ADDRESS.save(deps.storage, &pool_addr)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; LOCKUP_CONFIG.save(deps.storage, &msg.lockup_config)?; RAGEQUIT_CONFIG.save(deps.storage, &msg.ragequit_config)?; - PARTY_A_ROUTER.save(deps.storage, &party_a_router)?; - PARTY_B_ROUTER.save(deps.storage, &party_b_router)?; CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; COVENANT_CONFIG.save(deps.storage, &msg.covenant_config)?; @@ -65,12 +62,114 @@ pub fn execute( ) -> Result { match msg { ExecuteMsg::Ragequit {} => try_ragequit(deps, env, info), - // ExecuteMsg::Claim {} => try_claim(deps, env, info), + ExecuteMsg::Claim {} => try_claim(deps, env, info), ExecuteMsg::Tick {} => try_tick(deps, env, info), - _ => Ok(Response::default()), } } +fn try_claim( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + let contract_state = CONTRACT_STATE.load(deps.storage)?; + // claiming funds is only possible in Ragequit or Expired state + match contract_state { + ContractState::Ragequit => try_claim_ragequit(deps, env, info), + ContractState::Expired => try_claim_expired(deps, env, info), + _ => Err(ContractError::ClaimError {}), + } +} + +fn try_claim_expired( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + Ok(Response::default()) +} + + +fn try_claim_ragequit( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + let mut covenant_config = COVENANT_CONFIG.load(deps.storage)?; + let pool: cosmwasm_std::Addr = POOL_ADDRESS.load(deps.storage)?; + + let (mut claim_party, counterparty) = covenant_config.authorize_sender(info.sender)?; + + // We query the pool to get the contract for the pool info + // The pool info is required to fetch the address of the + // liquidity token contract. The liquidity tokens are CW20 tokens + let pair_info: astroport::asset::PairInfo = deps + .querier + .query_wasm_smart(pool.to_string(), &astroport::pair::QueryMsg::Pair {})?; + + // We query our own liquidity token balance + let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( + pair_info.clone().liquidity_token, + &cw20::Cw20QueryMsg::Balance { + address: env.contract.address.to_string(), + }, + )?; + + // if no lp tokens are available, no point to ragequit + if liquidity_token_balance.balance.is_zero() { + return Err(ContractError::NoLpTokensAvailable {}) + } + + // we figure out the amounts of underlying tokens that claiming party could receive + let claim_party_lp_token_amount = liquidity_token_balance.balance + .checked_mul_floor(claim_party.allocation) + .map_err(|_| ContractError::FractionMulError {})?; + let claim_party_entitled_assets: Vec = deps.querier + .query_wasm_smart( + pool.to_string(), + &astroport::pair::QueryMsg::Share { amount: claim_party_lp_token_amount }, + )?; + + // generate the withdraw_liquidity hook for the claim party + let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; + let withdraw_msg = &Cw20ExecuteMsg::Send { + contract: pool.to_string(), + amount: claim_party_lp_token_amount, + msg: to_binary(withdraw_liquidity_hook)?, + }; + + let withdraw_liquidity_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: pair_info.liquidity_token.to_string(), + msg: to_binary(withdraw_msg)?, + funds: vec![], + }); + let mut withdraw_coins: Vec = vec![]; + for asset in claim_party_entitled_assets { + let coin = asset.to_coin()?; + withdraw_coins.push(coin); + } + + let transfer_withdrawn_funds_msg = CosmosMsg::Bank(BankMsg::Send { + to_address: claim_party.clone().router, + amount: withdraw_coins, + }); + + // after building the messages we can finalize the config updates + claim_party.allocation = Decimal::zero(); + covenant_config.update_parties(claim_party.clone(), counterparty.clone()); + + COVENANT_CONFIG.save(deps.storage, &covenant_config)?; + CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; + + Ok(Response::default() + .add_attribute("method", "try_claim_ragequit") + .add_messages(vec![ + withdraw_liquidity_msg, + transfer_withdrawn_funds_msg, + ])) +} + + fn try_tick( deps: DepsMut, env: Env, @@ -80,10 +179,10 @@ fn try_tick( match state { ContractState::Instantiated => try_deposit(deps, env, info), ContractState::Active => check_expiration(deps, env), - // ContractState::Ragequit => try_ragequit(deps, env, info), - // ContractState::Expired => todo!(), - ContractState::Complete => Ok(Response::default().add_attribute("contract_state", "complete")), - _ => Ok(Response::default()), + _ => Ok(Response::default() + .add_attribute("method", "tick") + .add_attribute("contract_state", state.to_string()) + ), } } @@ -97,22 +196,19 @@ fn try_deposit( // assert the balances let party_a_bal = deps.querier.query_balance( env.contract.address.to_string(), - config.party_a.party_contibution.denom)?; + config.party_a.contribution.denom)?; let party_b_bal = deps.querier.query_balance( env.contract.address.to_string(), - config.party_b.party_contibution.denom)?; + config.party_b.contribution.denom)?; let deposit_deadline = DEPOSIT_DEADLINE.load(deps.storage)?; - let party_a_fulfilled = config.party_a.party_contibution.amount < party_a_bal.amount; - let party_b_fulfilled = config.party_b.party_contibution.amount < party_b_bal.amount; + let party_a_fulfilled = config.party_a.contribution.amount < party_a_bal.amount; + let party_b_fulfilled = config.party_b.contribution.amount < party_b_bal.amount; // note: even if both parties deposit their funds in time, // it is important to trigger this method before the expiry block // if deposit deadline is due we complete and refund if deposit_deadline.is_expired(env.block.clone()) { - let a_router = PARTY_A_ROUTER.load(deps.storage)?; - let b_router = PARTY_B_ROUTER.load(deps.storage)?; - let refund_messages: Vec = match (party_a_bal.amount.is_zero(), party_b_bal.amount.is_zero()) { // both balances empty, we complete (true, true) => { @@ -123,22 +219,22 @@ fn try_deposit( }, // refund party B (true, false) => vec![CosmosMsg::Bank(BankMsg::Send { - to_address: b_router.to_string(), + to_address: config.party_b.router.to_string(), amount: vec![party_b_bal], })], // refund party A (false, true) => vec![CosmosMsg::Bank(BankMsg::Send { - to_address: a_router.to_string(), + to_address: config.party_a.router.to_string(), amount: vec![party_a_bal], })], // refund both (false, false) => vec![ CosmosMsg::Bank(BankMsg::Send { - to_address: a_router.to_string(), + to_address: config.party_a.router.to_string(), amount: vec![party_a_bal], }), CosmosMsg::Bank(BankMsg::Send { - to_address: b_router.to_string(), + to_address: config.party_b.router.to_string(), amount: vec![party_b_bal], }), ], @@ -223,7 +319,6 @@ fn try_ragequit( // first we apply the ragequit penalty on both parties allocations rq_party.allocation -= rq_config.penalty; counterparty.allocation += rq_config.penalty; - covenant_config.update_parties(rq_party.clone(), counterparty.clone()); // We query the pool to get the contract for the pool info // The pool info is required to fetch the address of the @@ -231,7 +326,6 @@ fn try_ragequit( let pair_info: astroport::asset::PairInfo = deps .querier .query_wasm_smart(pool.to_string(), &astroport::pair::QueryMsg::Pair {})?; - println!("pair info: {:?}", pair_info); // We query our own liquidity token balance let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( @@ -240,7 +334,6 @@ fn try_ragequit( address: env.contract.address.to_string(), }, )?; - println!("liquidity_token_balance: {:?}", liquidity_token_balance); // if no lp tokens are available, no point to ragequit if liquidity_token_balance.balance.is_zero() { @@ -256,9 +349,10 @@ fn try_ragequit( pool.to_string(), &astroport::pair::QueryMsg::Share { amount: rq_party_lp_token_amount }, )?; - println!("entitled assets: {:?}", rq_entitled_assets); + // reflect the ragequit in ragequit config - rq_config.state = Some(RagequitState::from_share_response(rq_entitled_assets, rq_party.clone())?); + let rq_state = RagequitState::from_share_response(rq_entitled_assets, rq_party.clone())?; + rq_config.state = Some(rq_state.clone()); // generate the withdraw_liquidity hook for the ragequitting party let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; @@ -268,6 +362,22 @@ fn try_ragequit( msg: to_binary(withdraw_liquidity_hook)?, }; + let withdraw_liquidity_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: pair_info.liquidity_token.to_string(), + msg: to_binary(withdraw_msg)?, + funds: vec![], + }); + let transfer_withdrawn_funds_msg = CosmosMsg::Bank(BankMsg::Send { + to_address: rq_party.clone().router, + amount: rq_state.coins, + }); + + // after building the messages we can finalize the config updates. + // rq party is now entitled nothing, counterparty owns the whole position + rq_party.allocation = Decimal::zero(); + counterparty.allocation = Decimal::one(); + covenant_config.update_parties(rq_party.clone(), counterparty.clone()); + // update the states RAGEQUIT_CONFIG.save(deps.storage, &RagequitConfig::Enabled(rq_config))?; COVENANT_CONFIG.save(deps.storage, &covenant_config)?; @@ -275,13 +385,12 @@ fn try_ragequit( Ok(Response::default() .add_attribute("method", "ragequit") - .add_attribute("caller", rq_party.party_addr) - .add_message(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: pair_info.liquidity_token.to_string(), - msg: to_binary(withdraw_msg)?, - funds: vec![], - }) - )) + .add_attribute("caller", rq_party.addr) + .add_messages(vec![ + withdraw_liquidity_msg, + transfer_withdrawn_funds_msg, + ]) + ) } @@ -294,8 +403,9 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.load(deps.storage)?)?), QueryMsg::NextContract {} => Ok(to_binary(&NEXT_CONTRACT.load(deps.storage)?)?), QueryMsg::PoolAddress {} => Ok(to_binary(&POOL_ADDRESS.load(deps.storage)?)?), - QueryMsg::RouterPartyA {} => Ok(to_binary(&PARTY_A_ROUTER.load(deps.storage)?)?), - QueryMsg::RouterPartyB {} => Ok(to_binary(&PARTY_B_ROUTER.load(deps.storage)?)?), + QueryMsg::ConfigPartyA {} => Ok(to_binary(&COVENANT_CONFIG.load(deps.storage)?.party_a)?), + QueryMsg::ConfigPartyB {} => Ok(to_binary(&COVENANT_CONFIG.load(deps.storage)?.party_b)?), QueryMsg::DepositDeadline {} => Ok(to_binary(&DEPOSIT_DEADLINE.load(deps.storage)?)?), + QueryMsg::Config {} => Ok(to_binary(&COVENANT_CONFIG.load(deps.storage)?)?), } } \ No newline at end of file diff --git a/contracts/two-party-pol-holder/src/error.rs b/contracts/two-party-pol-holder/src/error.rs index 4961a403..6dbf07ed 100644 --- a/contracts/two-party-pol-holder/src/error.rs +++ b/contracts/two-party-pol-holder/src/error.rs @@ -10,6 +10,9 @@ pub enum ContractError { #[error("unauthorized")] Unauthorized {}, + #[error("contract needs to be in ragequit or expired state in order to claim")] + ClaimError {}, + #[error("covenant is not in active state")] NotActive {}, diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index 0ba919a8..a83def7f 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -1,8 +1,9 @@ -use std::ops::Deref; + +use std::fmt; use astroport::asset::Asset; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Decimal, Attribute, Uint128, Coin, StdError}; +use cosmwasm_std::{Addr, Decimal, Attribute, Coin, StdError}; use covenant_macros::{clocked, covenant_clock_address, covenant_next_contract}; use covenant_utils::LockupConfig; @@ -16,8 +17,6 @@ pub struct InstantiateMsg { pub lockup_config: LockupConfig, pub ragequit_config: RagequitConfig, pub deposit_deadline: Option, - pub party_a_router: String, - pub party_b_router: String, pub covenant_config: TwoPartyPolCovenantConfig, } @@ -43,7 +42,7 @@ pub struct TwoPartyPolCovenantConfig { impl TwoPartyPolCovenantConfig { pub fn update_parties(&mut self, p1: TwoPartyPolCovenantParty, p2: TwoPartyPolCovenantParty) { - if self.party_a.party_addr == p1.party_addr { + if self.party_a.addr == p1.addr { self.party_a = p1; self.party_b = p2; } else { @@ -55,9 +54,10 @@ impl TwoPartyPolCovenantConfig { #[cw_serde] pub struct TwoPartyPolCovenantParty { - pub party_contibution: Coin, - pub party_addr: String, + pub contribution: Coin, + pub addr: String, pub allocation: Decimal, + pub router: String, // TODO: consider adding a boxed counterparty for convenience? } @@ -66,9 +66,9 @@ impl TwoPartyPolCovenantConfig { pub fn authorize_sender(&self, sender: Addr) -> Result<(TwoPartyPolCovenantParty, TwoPartyPolCovenantParty), ContractError> { let party_a = self.party_a.clone(); let party_b = self.party_b.clone(); - if party_a.party_addr == sender { + if party_a.addr == sender { Ok((party_a, party_b)) - } else if party_b.party_addr == sender { + } else if party_b.addr == sender { Ok((party_b, party_a)) } else { Err(ContractError::Unauthorized {}) @@ -103,6 +103,19 @@ pub enum ContractState { Complete, } + +impl fmt::Display for ContractState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ContractState::Instantiated => write!(f, "instantiated"), + ContractState::Active => write!(f, "active"), + ContractState::Ragequit => write!(f, "ragequit"), + ContractState::Expired => write!(f, "expired"), + ContractState::Complete => write!(f, "complete"), + } + } +} + #[covenant_clock_address] #[covenant_next_contract] #[cw_serde] @@ -116,12 +129,14 @@ pub enum QueryMsg { LockupConfig {}, #[returns(Addr)] PoolAddress {}, - #[returns(Addr)] - RouterPartyA {}, - #[returns(Addr)] - RouterPartyB {}, + #[returns(TwoPartyPolCovenantParty)] + ConfigPartyA {}, + #[returns(TwoPartyPolCovenantParty)] + ConfigPartyB {}, #[returns(LockupConfig)] DepositDeadline {}, + #[returns(TwoPartyPolCovenantConfig)] + Config {}, } // #[cw_serde] @@ -256,7 +271,7 @@ impl RagequitState { let mut rq_coins: Vec = vec![]; for asset in assets { let coin = asset.to_coin()?; - + rq_coins.push(coin); } Ok(RagequitState { diff --git a/contracts/two-party-pol-holder/src/state.rs b/contracts/two-party-pol-holder/src/state.rs index 46a86e19..9f866f81 100644 --- a/contracts/two-party-pol-holder/src/state.rs +++ b/contracts/two-party-pol-holder/src/state.rs @@ -17,7 +17,7 @@ pub const POOL_ADDRESS: Item = Item::new("pool_address"); pub const DEPOSIT_DEADLINE: Item = Item::new("deposit_deadline"); -pub const PARTY_A_ROUTER: Item = Item::new("party_a_router"); -pub const PARTY_B_ROUTER: Item = Item::new("party_b_router"); +// pub const PARTY_A_ROUTER: Item = Item::new("party_a_router"); +// pub const PARTY_B_ROUTER: Item = Item::new("party_b_router"); pub const COVENANT_CONFIG: Item = Item::new("covenant_config"); diff --git a/contracts/two-party-pol-holder/src/suite_tests/suite.rs b/contracts/two-party-pol-holder/src/suite_tests/suite.rs index 112926be..4cf30cbf 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/suite.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/suite.rs @@ -20,12 +20,12 @@ pub const PARTY_A_ROUTER: &str = "party_a_router"; pub const PARTY_B_ROUTER: &str = "party_b_router"; pub const CLOCK_ADDR: &str = "clock_address"; -pub const NEXT_CONTRACT: &str = "contract0"; +pub const NEXT_CONTRACT: &str = "contract2"; pub const INITIAL_BLOCK_HEIGHT: u64 = 12345; pub const INITIAL_BLOCK_NANOS: u64 = 1571797419879305533; -pub const POOL: &str = "some_pool"; +pub const POOL: &str = "contract1"; pub struct Suite { pub app: App, @@ -48,23 +48,23 @@ impl Default for SuiteBuilder { clock_address: CLOCK_ADDR.to_string(), next_contract: NEXT_CONTRACT.to_string(), lockup_config: LockupConfig::None, - party_a_router: PARTY_A_ROUTER.to_string(), - party_b_router: PARTY_B_ROUTER.to_string(), covenant_config: TwoPartyPolCovenantConfig { party_a: TwoPartyPolCovenantParty { - party_contibution: Coin { + router: PARTY_A_ROUTER.to_string(), + contribution: Coin { denom: DENOM_A.to_string(), amount: Uint128::new(200), }, - party_addr: PARTY_A_ADDR.to_string(), + addr: PARTY_A_ADDR.to_string(), allocation: Decimal::from_ratio(Uint128::one(), Uint128::new(2)), }, party_b: TwoPartyPolCovenantParty { - party_contibution: Coin { + router: PARTY_B_ROUTER.to_string(), + contribution: Coin { denom: DENOM_B.to_string(), amount: Uint128::new(100), }, - party_addr: PARTY_B_ADDR.to_string(), + addr: PARTY_B_ADDR.to_string(), allocation: Decimal::from_ratio(Uint128::one(), Uint128::new(2)), }, }, @@ -108,11 +108,11 @@ impl SuiteBuilder { let denom_b = Coin { denom: DENOM_B.to_string(), - amount: Uint128::new(200), + amount: Uint128::new(500), }; let denom_a = Coin { denom: DENOM_A.to_string(), - amount: Uint128::new(200), + amount: Uint128::new(500), }; app .sudo(SudoMsg::Bank(cw_multi_test::BankSudo::Mint { @@ -186,6 +186,15 @@ impl Suite { &[], ) } + + pub fn claim(&mut self, caller: &str) -> Result { + self.app.execute_contract( + Addr::unchecked(caller), + self.holder.clone(), + &ExecuteMsg::Claim {}, + &[], + ) + } } // queries @@ -197,6 +206,13 @@ impl Suite { .unwrap() } + pub fn query_covenant_config(&self) -> TwoPartyPolCovenantConfig { + self.app + .wrap() + .query_wasm_smart(&self.holder, &QueryMsg::Config {}) + .unwrap() + } + pub fn query_pool(&self) -> Addr { self.app .wrap() @@ -204,17 +220,17 @@ impl Suite { .unwrap() } - pub fn query_router_party_a(&self) -> Addr { + pub fn query_party_a(&self) -> TwoPartyPolCovenantParty { self.app .wrap() - .query_wasm_smart(&self.holder, &QueryMsg::RouterPartyA {}) + .query_wasm_smart(&self.holder, &QueryMsg::ConfigPartyA {}) .unwrap() } - pub fn query_router_party_b(&self) -> Addr { + pub fn query_party_b(&self) -> TwoPartyPolCovenantParty { self.app .wrap() - .query_wasm_smart(&self.holder, &QueryMsg::RouterPartyB {}) + .query_wasm_smart(&self.holder, &QueryMsg::ConfigPartyB {}) .unwrap() } diff --git a/contracts/two-party-pol-holder/src/suite_tests/tests.rs b/contracts/two-party-pol-holder/src/suite_tests/tests.rs index 31fac3c1..db34078e 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/tests.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/tests.rs @@ -1,7 +1,7 @@ -use cosmwasm_std::{Timestamp, Uint128, Event, Attribute, Decimal256, Decimal}; +use cosmwasm_std::{Timestamp, Uint128, Decimal}; use covenant_utils::LockupConfig; -use crate::{suite_tests::suite::{CLOCK_ADDR, POOL, NEXT_CONTRACT, PARTY_A_ROUTER, PARTY_B_ROUTER, get_default_block_info}, msg::{ContractState, RagequitConfig, RagequitTerms}, error::ContractError}; +use crate::{suite_tests::suite::{CLOCK_ADDR, POOL, NEXT_CONTRACT, PARTY_A_ROUTER, PARTY_B_ROUTER, get_default_block_info, PARTY_B_ADDR}, msg::{ContractState, RagequitConfig, RagequitTerms, TwoPartyPolCovenantConfig}, error::ContractError}; use super::suite::{SuiteBuilder, PARTY_A_ADDR}; @@ -11,8 +11,8 @@ fn test_instantiate_happy_and_query_all() { let clock = suite.query_clock_address(); let pool: cosmwasm_std::Addr = suite.query_pool(); let next_contract = suite.query_next_contract(); - let party_a_router = suite.query_router_party_a(); - let party_b_router = suite.query_router_party_b(); + let config_party_a = suite.query_party_a(); + let config_party_b = suite.query_party_b(); let deposit_deadline = suite.query_deposit_deadline(); let contract_state = suite.query_contract_state(); @@ -20,8 +20,8 @@ fn test_instantiate_happy_and_query_all() { assert_eq!(CLOCK_ADDR, clock); assert_eq!(POOL, pool); assert_eq!(NEXT_CONTRACT, next_contract.to_string()); - assert_eq!(PARTY_A_ROUTER, party_a_router.to_string()); - assert_eq!(PARTY_B_ROUTER, party_b_router.to_string()); + assert_eq!(PARTY_A_ROUTER, config_party_a.router.to_string()); + assert_eq!(PARTY_B_ROUTER, config_party_b.router.to_string()); assert_eq!(LockupConfig::None, deposit_deadline); } @@ -64,7 +64,7 @@ fn test_single_party_deposit_refund_block_based() { let holder_balance = suite.get_denom_a_balance(suite.holder.to_string()); let router_a_balance = suite.get_denom_a_balance( - suite.query_router_party_a().to_string()); + suite.query_party_a().router.to_string()); let holder_state = suite.query_contract_state(); assert_eq!(ContractState::Complete, holder_state); @@ -90,7 +90,7 @@ fn test_single_party_deposit_refund_time_based() { let holder_balance = suite.get_denom_a_balance(suite.holder.to_string()); let router_a_balance = suite.get_denom_a_balance( - suite.query_router_party_a().to_string()); + suite.query_party_a().router.to_string()); let holder_state = suite.query_contract_state(); assert_eq!(ContractState::Complete, holder_state); @@ -222,7 +222,7 @@ fn test_holder_ragequit_disabled() { let state = suite.query_contract_state(); assert_eq!(ContractState::Active {}, state); - assert_eq!(ContractError::NotActive {}, err); + assert_eq!(ContractError::RagequitDisabled {}, err); } #[test] @@ -284,10 +284,10 @@ fn test_holder_ragequit_not_in_active_state() { } #[test] -// #[should_panic(expected = "covenant is not in active state")] fn test_holder_ragequit_active_but_expired() { let current_timestamp = get_default_block_info(); let mut suite = SuiteBuilder::default() + .with_ragequit_config(RagequitConfig::Enabled(RagequitTerms { penalty: Decimal::bps(10), state: None })) .with_lockup_config(LockupConfig::Time(current_timestamp.time.plus_minutes(200))) .build(); @@ -308,11 +308,7 @@ fn test_holder_ragequit_active_but_expired() { } #[test] -fn test_ragequit_happy_flow() { - -} - -#[test] +#[should_panic(expected = "covenant is not in active state")] fn test_ragequit_double_claim_fails() { let current_timestamp = get_default_block_info(); let mut suite = SuiteBuilder::default() @@ -332,26 +328,71 @@ fn test_ragequit_double_claim_fails() { // we tick the holder to deposit the funds and activate suite.tick(CLOCK_ADDR).unwrap(); - suite.pass_minutes(50); + // we ragequit and assert balances have reached router + suite.rq(PARTY_A_ADDR).unwrap(); - // advance the state to expired - suite.tick(CLOCK_ADDR).unwrap(); + let router_a_balance = suite.get_denom_a_balance(PARTY_A_ROUTER.to_string()); + let router_b_balance = suite.get_denom_b_balance(PARTY_A_ROUTER.to_string()); + assert_eq!(Uint128::new(200), router_a_balance); + assert_eq!(Uint128::new(200), router_b_balance); - let holder_a_balance = suite.get_denom_a_balance(suite.holder.to_string()); - let holder_b_balance = suite.get_denom_a_balance(suite.holder.to_string()); - println!("holder_a_balance: {:?}", holder_a_balance); - println!("holder_b_balance: {:?}", holder_b_balance); + let state = suite.query_contract_state(); + let config = suite.query_covenant_config(); + assert_eq!(Decimal::one(), config.party_b.allocation); + assert_eq!(Decimal::zero(), config.party_a.allocation); + assert_eq!(ContractState::Ragequit {}, state); + // we attempt to rq again and panic suite.rq(PARTY_A_ADDR).unwrap(); +} - let holder_a_balance = suite.get_denom_a_balance(suite.holder.to_string()); - let holder_b_balance = suite.get_denom_a_balance(suite.holder.to_string()); - println!("holder_a_balance: {:?}", holder_a_balance); - println!("holder_b_balance: {:?}", holder_b_balance); - let state = suite.query_contract_state(); +#[test] +fn test_ragequit_happy_flow_to_completion() { + let current_timestamp = get_default_block_info(); + let mut suite = SuiteBuilder::default() + .with_ragequit_config(RagequitConfig::Enabled(RagequitTerms { + penalty: Decimal::from_ratio(Uint128::one(), Uint128::new(10)), + state: None, + })) + .with_lockup_config(LockupConfig::Time(current_timestamp.time.plus_minutes(200))) + .build(); + + // both parties fulfill their parts of the covenant + let coin_a = suite.get_party_a_coin(Uint128::new(500)); + let coin_b = suite.get_party_b_coin(Uint128::new(500)); + suite.fund_coin(coin_a); + suite.fund_coin(coin_b); + // we tick the holder to deposit the funds and activate + suite.tick(CLOCK_ADDR).unwrap(); + + // party A ragequits; assert balances have reached router + suite.rq(PARTY_A_ADDR).unwrap(); + + let router_a_balance = suite.get_denom_a_balance(PARTY_A_ROUTER.to_string()); + let router_b_balance = suite.get_denom_b_balance(PARTY_A_ROUTER.to_string()); + assert_eq!(Uint128::new(200), router_a_balance); + assert_eq!(Uint128::new(200), router_b_balance); + + let state = suite.query_contract_state(); + let config = suite.query_covenant_config(); + assert_eq!(Decimal::one(), config.party_b.allocation); + assert_eq!(Decimal::zero(), config.party_a.allocation); assert_eq!(ContractState::Ragequit {}, state); + // party B claims + suite.claim(PARTY_B_ADDR).unwrap(); + + let router_a_balance = suite.get_denom_a_balance(PARTY_B_ROUTER.to_string()); + let router_b_balance = suite.get_denom_b_balance(PARTY_B_ROUTER.to_string()); + assert_eq!(Uint128::new(200), router_a_balance); + assert_eq!(Uint128::new(200), router_b_balance); + + let state = suite.query_contract_state(); + let config = suite.query_covenant_config(); + assert_eq!(Decimal::zero(), config.party_b.allocation); + assert_eq!(Decimal::zero(), config.party_a.allocation); + assert_eq!(ContractState::Complete {}, state); } From 2dd5a3096ab1dd17996eee8e41a51edceff9edc0 Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 25 Sep 2023 19:17:00 +0200 Subject: [PATCH 107/586] expiration claim leads to completion --- .../two-party-pol-holder/src/contract.rs | 90 ++++++++++++++++++- .../src/suite_tests/tests.rs | 51 +++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index b6b580c8..1b964b49 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -86,7 +86,85 @@ fn try_claim_expired( env: Env, info: MessageInfo, ) -> Result { - Ok(Response::default()) + let mut covenant_config = COVENANT_CONFIG.load(deps.storage)?; + let pool: cosmwasm_std::Addr = POOL_ADDRESS.load(deps.storage)?; + + let (mut claim_party, counterparty) = covenant_config.authorize_sender(info.sender)?; + + if claim_party.allocation.is_zero() && counterparty.allocation.is_zero() { + CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; + return Ok(Response::default() + .add_attribute("method", "try_claim_expired") + .add_attribute("contract_state", "complete")) + } + + // We query the pool to get the contract for the pool info + // The pool info is required to fetch the address of the + // liquidity token contract. The liquidity tokens are CW20 tokens + let pair_info: astroport::asset::PairInfo = deps + .querier + .query_wasm_smart(pool.to_string(), &astroport::pair::QueryMsg::Pair {})?; + + // We query our own liquidity token balance + let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( + pair_info.clone().liquidity_token, + &cw20::Cw20QueryMsg::Balance { + address: env.contract.address.to_string(), + }, + )?; + + // if no lp tokens are available, no point to ragequit + if liquidity_token_balance.balance.is_zero() { + return Err(ContractError::NoLpTokensAvailable {}) + } + + // we figure out the amounts of underlying tokens that claiming party could receive + let claim_party_lp_token_amount = liquidity_token_balance.balance + .checked_mul_floor(claim_party.allocation) + .map_err(|_| ContractError::FractionMulError {})?; + let claim_party_entitled_assets: Vec = deps.querier + .query_wasm_smart( + pool.to_string(), + &astroport::pair::QueryMsg::Share { amount: claim_party_lp_token_amount }, + )?; + + // generate the withdraw_liquidity hook for the claim party + let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; + let withdraw_msg = &Cw20ExecuteMsg::Send { + contract: pool.to_string(), + amount: claim_party_lp_token_amount, + msg: to_binary(withdraw_liquidity_hook)?, + }; + + let withdraw_liquidity_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: pair_info.liquidity_token.to_string(), + msg: to_binary(withdraw_msg)?, + funds: vec![], + }); + let mut withdraw_coins: Vec = vec![]; + for asset in claim_party_entitled_assets { + let coin = asset.to_coin()?; + withdraw_coins.push(coin); + } + + let transfer_withdrawn_funds_msg = CosmosMsg::Bank(BankMsg::Send { + to_address: claim_party.clone().router, + amount: withdraw_coins, + }); + + // after building the messages we can finalize the config updates + claim_party.allocation = Decimal::zero(); + covenant_config.update_parties(claim_party.clone(), counterparty.clone()); + + COVENANT_CONFIG.save(deps.storage, &covenant_config)?; + + Ok(Response::default() + .add_attribute("method", "try_claim_expired") + .add_messages(vec![ + withdraw_liquidity_msg, + transfer_withdrawn_funds_msg, + ]) + ) } @@ -179,6 +257,16 @@ fn try_tick( match state { ContractState::Instantiated => try_deposit(deps, env, info), ContractState::Active => check_expiration(deps, env), + ContractState::Expired => { + let config = COVENANT_CONFIG.load(deps.storage)?; + if config.party_a.allocation.is_zero() && config.party_b.allocation.is_zero() { + CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; + } + Ok(Response::default() + .add_attribute("method", "tick") + .add_attribute("contract_state", state.to_string()) + ) + }, _ => Ok(Response::default() .add_attribute("method", "tick") .add_attribute("contract_state", state.to_string()) diff --git a/contracts/two-party-pol-holder/src/suite_tests/tests.rs b/contracts/two-party-pol-holder/src/suite_tests/tests.rs index db34078e..de8b3aee 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/tests.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/tests.rs @@ -396,3 +396,54 @@ fn test_ragequit_happy_flow_to_completion() { assert_eq!(ContractState::Complete {}, state); } + +#[test] +fn test_expiry_happy_flow_to_completion() { + let current_timestamp = get_default_block_info(); + let mut suite = SuiteBuilder::default() + .with_lockup_config(LockupConfig::Time(current_timestamp.time.plus_minutes(200))) + .build(); + + // both parties fulfill their parts of the covenant + let coin_a = suite.get_party_a_coin(Uint128::new(500)); + let coin_b = suite.get_party_b_coin(Uint128::new(500)); + suite.fund_coin(coin_a); + suite.fund_coin(coin_b); + + // we tick the holder to deposit the funds and activate + suite.tick(CLOCK_ADDR).unwrap(); + + suite.pass_minutes(250); + + suite.tick(CLOCK_ADDR).unwrap(); + + assert_eq!(ContractState::Expired {}, suite.query_contract_state()); + assert_eq!(Uint128::new(0), suite.get_denom_a_balance(PARTY_A_ROUTER.to_string())); + assert_eq!(Uint128::new(0), suite.get_denom_b_balance(PARTY_A_ROUTER.to_string())); + assert_eq!(Uint128::new(0), suite.get_denom_a_balance(PARTY_B_ROUTER.to_string())); + assert_eq!(Uint128::new(0), suite.get_denom_b_balance(PARTY_B_ROUTER.to_string())); + + // party B claims + suite.claim(PARTY_B_ADDR).unwrap(); + + assert_eq!(Uint128::new(0), suite.get_denom_a_balance(PARTY_A_ROUTER.to_string())); + assert_eq!(Uint128::new(0), suite.get_denom_b_balance(PARTY_A_ROUTER.to_string())); + assert_eq!(Uint128::new(200), suite.get_denom_a_balance(PARTY_B_ROUTER.to_string())); + assert_eq!(Uint128::new(200), suite.get_denom_b_balance(PARTY_B_ROUTER.to_string())); + + suite.pass_minutes(5); + + // party A claims + suite.claim(PARTY_A_ADDR).unwrap(); + suite.tick(CLOCK_ADDR).unwrap(); + + let config = suite.query_covenant_config(); + assert_eq!(Decimal::zero(), config.party_b.allocation); + assert_eq!(Decimal::zero(), config.party_a.allocation); + assert_eq!(Uint128::new(200), suite.get_denom_a_balance(PARTY_A_ROUTER.to_string())); + assert_eq!(Uint128::new(200), suite.get_denom_b_balance(PARTY_A_ROUTER.to_string())); + assert_eq!(Uint128::new(200), suite.get_denom_a_balance(PARTY_B_ROUTER.to_string())); + assert_eq!(Uint128::new(200), suite.get_denom_b_balance(PARTY_B_ROUTER.to_string())); + assert_eq!(ContractState::Complete {}, suite.query_contract_state()); +} + From 7218173523eb92a1233e159da8ab105331e73766 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 26 Sep 2023 14:26:09 +0200 Subject: [PATCH 108/586] merging ragequit and expired claims --- .../two-party-pol-holder/src/contract.rs | 207 ++++++------------ contracts/two-party-pol-holder/src/msg.rs | 6 +- contracts/two-party-pol-holder/src/state.rs | 3 +- .../src/suite_tests/suite.rs | 10 +- .../src/suite_tests/tests.rs | 4 +- 5 files changed, 70 insertions(+), 160 deletions(-) diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index 1b964b49..b7d6a1f6 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -1,6 +1,6 @@ use astroport::{pair::Cw20HookMsg, asset::Asset}; -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, BankMsg, CosmosMsg, WasmMsg, Decimal, Coin}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, BankMsg, CosmosMsg, WasmMsg, Decimal, Coin, Addr, Uint128}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -8,7 +8,7 @@ use covenant_utils::LockupConfig; use cw2::set_contract_version; use cw20::{BalanceResponse, Cw20ExecuteMsg}; -use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, ContractState, RagequitConfig, RagequitState}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, CONTRACT_STATE, DEPOSIT_DEADLINE, POOL_ADDRESS, COVENANT_CONFIG}, error::ContractError}; +use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, ContractState, RagequitConfig, RagequitState, TwoPartyPolCovenantParty, TwoPartyPolCovenantConfig}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, CONTRACT_STATE, DEPOSIT_DEADLINE, POOL_ADDRESS, COVENANT_CONFIG, LP_TOKEN}, error::ContractError}; const CONTRACT_NAME: &str = "crates.io:covenant-two-party-pol-holder"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -26,8 +26,9 @@ pub fn instantiate( let pool_addr = deps.api.addr_validate(&msg.pool_address)?; let next_contract = deps.api.addr_validate(&msg.next_contract)?; let clock_addr = deps.api.addr_validate(&msg.clock_address)?; - let party_a_router = deps.api.addr_validate(&msg.covenant_config.party_a.router)?; - let party_b_router = deps.api.addr_validate(&msg.covenant_config.party_b.router)?; + deps.api.addr_validate(&msg.covenant_config.party_a.router)?; + deps.api.addr_validate(&msg.covenant_config.party_b.router)?; + POOL_ADDRESS.save(deps.storage, &pool_addr)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; @@ -71,43 +72,24 @@ fn try_claim( deps: DepsMut, env: Env, info: MessageInfo, -) -> Result { - let contract_state = CONTRACT_STATE.load(deps.storage)?; - // claiming funds is only possible in Ragequit or Expired state - match contract_state { - ContractState::Ragequit => try_claim_ragequit(deps, env, info), - ContractState::Expired => try_claim_expired(deps, env, info), - _ => Err(ContractError::ClaimError {}), - } -} - -fn try_claim_expired( - deps: DepsMut, - env: Env, - info: MessageInfo, ) -> Result { let mut covenant_config = COVENANT_CONFIG.load(deps.storage)?; - let pool: cosmwasm_std::Addr = POOL_ADDRESS.load(deps.storage)?; - - let (mut claim_party, counterparty) = covenant_config.authorize_sender(info.sender)?; + let (mut claim_party, mut counterparty) = covenant_config.authorize_sender(&info.sender)?; + let pool = POOL_ADDRESS.load(deps.storage)?; + let lp_token = LP_TOKEN.load(deps.storage)?; + let contract_state = CONTRACT_STATE.load(deps.storage)?; + // if both parties already claimed everything we complete if claim_party.allocation.is_zero() && counterparty.allocation.is_zero() { CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; return Ok(Response::default() - .add_attribute("method", "try_claim_expired") + .add_attribute("method", "try_claim") .add_attribute("contract_state", "complete")) } - // We query the pool to get the contract for the pool info - // The pool info is required to fetch the address of the - // liquidity token contract. The liquidity tokens are CW20 tokens - let pair_info: astroport::asset::PairInfo = deps - .querier - .query_wasm_smart(pool.to_string(), &astroport::pair::QueryMsg::Pair {})?; - // We query our own liquidity token balance let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( - pair_info.clone().liquidity_token, + lp_token.to_string(), &cw20::Cw20QueryMsg::Balance { address: env.contract.address.to_string(), }, @@ -122,92 +104,16 @@ fn try_claim_expired( let claim_party_lp_token_amount = liquidity_token_balance.balance .checked_mul_floor(claim_party.allocation) .map_err(|_| ContractError::FractionMulError {})?; - let claim_party_entitled_assets: Vec = deps.querier - .query_wasm_smart( - pool.to_string(), - &astroport::pair::QueryMsg::Share { amount: claim_party_lp_token_amount }, - )?; - - // generate the withdraw_liquidity hook for the claim party - let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; - let withdraw_msg = &Cw20ExecuteMsg::Send { - contract: pool.to_string(), - amount: claim_party_lp_token_amount, - msg: to_binary(withdraw_liquidity_hook)?, - }; - - let withdraw_liquidity_msg = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: pair_info.liquidity_token.to_string(), - msg: to_binary(withdraw_msg)?, - funds: vec![], - }); + let claim_party_entitled_assets: Vec = deps.querier.query_wasm_smart( + pool.to_string(), + &astroport::pair::QueryMsg::Share { amount: claim_party_lp_token_amount }, + )?; + // convert astro assets to coins let mut withdraw_coins: Vec = vec![]; for asset in claim_party_entitled_assets { - let coin = asset.to_coin()?; - withdraw_coins.push(coin); + withdraw_coins.push(asset.to_coin()?); } - let transfer_withdrawn_funds_msg = CosmosMsg::Bank(BankMsg::Send { - to_address: claim_party.clone().router, - amount: withdraw_coins, - }); - - // after building the messages we can finalize the config updates - claim_party.allocation = Decimal::zero(); - covenant_config.update_parties(claim_party.clone(), counterparty.clone()); - - COVENANT_CONFIG.save(deps.storage, &covenant_config)?; - - Ok(Response::default() - .add_attribute("method", "try_claim_expired") - .add_messages(vec![ - withdraw_liquidity_msg, - transfer_withdrawn_funds_msg, - ]) - ) -} - - -fn try_claim_ragequit( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { - let mut covenant_config = COVENANT_CONFIG.load(deps.storage)?; - let pool: cosmwasm_std::Addr = POOL_ADDRESS.load(deps.storage)?; - - let (mut claim_party, counterparty) = covenant_config.authorize_sender(info.sender)?; - - // We query the pool to get the contract for the pool info - // The pool info is required to fetch the address of the - // liquidity token contract. The liquidity tokens are CW20 tokens - let pair_info: astroport::asset::PairInfo = deps - .querier - .query_wasm_smart(pool.to_string(), &astroport::pair::QueryMsg::Pair {})?; - - // We query our own liquidity token balance - let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( - pair_info.clone().liquidity_token, - &cw20::Cw20QueryMsg::Balance { - address: env.contract.address.to_string(), - }, - )?; - - // if no lp tokens are available, no point to ragequit - if liquidity_token_balance.balance.is_zero() { - return Err(ContractError::NoLpTokensAvailable {}) - } - - // we figure out the amounts of underlying tokens that claiming party could receive - let claim_party_lp_token_amount = liquidity_token_balance.balance - .checked_mul_floor(claim_party.allocation) - .map_err(|_| ContractError::FractionMulError {})?; - let claim_party_entitled_assets: Vec = deps.querier - .query_wasm_smart( - pool.to_string(), - &astroport::pair::QueryMsg::Share { amount: claim_party_lp_token_amount }, - )?; - // generate the withdraw_liquidity hook for the claim party let withdraw_liquidity_hook = &Cw20HookMsg::WithdrawLiquidity { assets: vec![] }; let withdraw_msg = &Cw20ExecuteMsg::Send { @@ -215,39 +121,47 @@ fn try_claim_ragequit( amount: claim_party_lp_token_amount, msg: to_binary(withdraw_liquidity_hook)?, }; + + let withdraw_and_forward_msgs = vec![ + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: lp_token.to_string(), + msg: to_binary(withdraw_msg)?, + funds: vec![], + }), + CosmosMsg::Bank(BankMsg::Send { + to_address: claim_party.clone().router, + amount: withdraw_coins, + }), + ]; - let withdraw_liquidity_msg = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: pair_info.liquidity_token.to_string(), - msg: to_binary(withdraw_msg)?, - funds: vec![], - }); - let mut withdraw_coins: Vec = vec![]; - for asset in claim_party_entitled_assets { - let coin = asset.to_coin()?; - withdraw_coins.push(coin); - } + claim_party.allocation = Decimal::zero(); - let transfer_withdrawn_funds_msg = CosmosMsg::Bank(BankMsg::Send { - to_address: claim_party.clone().router, - amount: withdraw_coins, - }); + // if other party had not claimed yet, we assign full position to it + if !counterparty.allocation.is_zero() { + counterparty.allocation = Decimal::one(); + } else { + // otherwise both parties claimed everything and we can complete + CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; + } - // after building the messages we can finalize the config updates - claim_party.allocation = Decimal::zero(); covenant_config.update_parties(claim_party.clone(), counterparty.clone()); COVENANT_CONFIG.save(deps.storage, &covenant_config)?; - CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; - Ok(Response::default() - .add_attribute("method", "try_claim_ragequit") - .add_messages(vec![ - withdraw_liquidity_msg, - transfer_withdrawn_funds_msg, - ])) + // claiming funds is only possible in Ragequit or Expired state + match contract_state { + ContractState::Ragequit => Ok(Response::default() + .add_attribute("method", "try_claim_ragequit") + .add_messages(withdraw_and_forward_msgs) + ), + ContractState::Expired => Ok(Response::default() + .add_attribute("method", "try_claim_expired") + .add_messages(withdraw_and_forward_msgs) + ), + _ => Err(ContractError::ClaimError {}), + } } - fn try_tick( deps: DepsMut, env: Env, @@ -346,6 +260,14 @@ fn try_deposit( // advance the state to Active CONTRACT_STATE.save(deps.storage, &ContractState::Active)?; + // We query the pool to get the contract for the pool info + // The pool info is required to fetch the address of the + // liquidity token contract. The liquidity tokens are CW20 tokens + let pool_addr = POOL_ADDRESS.load(deps.storage)?; + let pair_info: astroport::asset::PairInfo = deps + .querier + .query_wasm_smart(pool_addr.to_string(), &astroport::pair::QueryMsg::Pair {})?; + LP_TOKEN.save(deps.storage, &pair_info.liquidity_token)?; Ok(Response::default() .add_attribute("method", "deposit_to_next_contract") @@ -402,22 +324,17 @@ fn try_ragequit( } // authorize the message sender - let (mut rq_party, mut counterparty) = covenant_config.authorize_sender(info.sender)?; + let (mut rq_party, mut counterparty) = covenant_config.authorize_sender(&info.sender)?; // after all validations we are ready to perform the ragequit. // first we apply the ragequit penalty on both parties allocations rq_party.allocation -= rq_config.penalty; counterparty.allocation += rq_config.penalty; - // We query the pool to get the contract for the pool info - // The pool info is required to fetch the address of the - // liquidity token contract. The liquidity tokens are CW20 tokens - let pair_info: astroport::asset::PairInfo = deps - .querier - .query_wasm_smart(pool.to_string(), &astroport::pair::QueryMsg::Pair {})?; + let lp_token = LP_TOKEN.load(deps.storage)?; // We query our own liquidity token balance let liquidity_token_balance: BalanceResponse = deps.querier.query_wasm_smart( - pair_info.clone().liquidity_token, + lp_token.to_string(), &cw20::Cw20QueryMsg::Balance { address: env.contract.address.to_string(), }, @@ -451,7 +368,7 @@ fn try_ragequit( }; let withdraw_liquidity_msg = CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: pair_info.liquidity_token.to_string(), + contract_addr: lp_token.to_string(), msg: to_binary(withdraw_msg)?, funds: vec![], }); diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index a83def7f..22a2e71b 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -63,12 +63,12 @@ pub struct TwoPartyPolCovenantParty { impl TwoPartyPolCovenantConfig { /// if authorized, returns (party, counterparty). otherwise errors - pub fn authorize_sender(&self, sender: Addr) -> Result<(TwoPartyPolCovenantParty, TwoPartyPolCovenantParty), ContractError> { + pub fn authorize_sender(&self, sender: &Addr) -> Result<(TwoPartyPolCovenantParty, TwoPartyPolCovenantParty), ContractError> { let party_a = self.party_a.clone(); let party_b = self.party_b.clone(); - if party_a.addr == sender { + if party_a.addr == sender.to_string() { Ok((party_a, party_b)) - } else if party_b.addr == sender { + } else if party_b.addr == sender.to_string() { Ok((party_b, party_a)) } else { Err(ContractError::Unauthorized {}) diff --git a/contracts/two-party-pol-holder/src/state.rs b/contracts/two-party-pol-holder/src/state.rs index 9f866f81..fa62c881 100644 --- a/contracts/two-party-pol-holder/src/state.rs +++ b/contracts/two-party-pol-holder/src/state.rs @@ -17,7 +17,6 @@ pub const POOL_ADDRESS: Item = Item::new("pool_address"); pub const DEPOSIT_DEADLINE: Item = Item::new("deposit_deadline"); -// pub const PARTY_A_ROUTER: Item = Item::new("party_a_router"); -// pub const PARTY_B_ROUTER: Item = Item::new("party_b_router"); +pub const LP_TOKEN: Item = Item::new("lp_token"); pub const COVENANT_CONFIG: Item = Item::new("covenant_config"); diff --git a/contracts/two-party-pol-holder/src/suite_tests/suite.rs b/contracts/two-party-pol-holder/src/suite_tests/suite.rs index 4cf30cbf..39d15ae4 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/suite.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/suite.rs @@ -1,9 +1,6 @@ use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, RagequitConfig, TwoPartyPolCovenantConfig, TwoPartyPolCovenantParty}; -use cosmos_sdk_proto::tendermint::types::Block; -use cosmwasm_std::{Addr, Coin, Uint128, BlockInfo, Uint64, Timestamp, Decimal}; -use covenant_utils::{ - CovenantPartiesConfig, CovenantParty, LockupConfig, PolCovenantTerms, -}; +use cosmwasm_std::{Addr, Coin, Uint128, BlockInfo, Timestamp, Decimal}; +use covenant_utils::LockupConfig; use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; use super::{mock_deposit_contract, two_party_pol_holder_contract, mock_astro_pool_contract, mock_astro_lp_token_contract}; @@ -22,9 +19,6 @@ pub const PARTY_B_ROUTER: &str = "party_b_router"; pub const CLOCK_ADDR: &str = "clock_address"; pub const NEXT_CONTRACT: &str = "contract2"; -pub const INITIAL_BLOCK_HEIGHT: u64 = 12345; -pub const INITIAL_BLOCK_NANOS: u64 = 1571797419879305533; - pub const POOL: &str = "contract1"; pub struct Suite { diff --git a/contracts/two-party-pol-holder/src/suite_tests/tests.rs b/contracts/two-party-pol-holder/src/suite_tests/tests.rs index de8b3aee..424abcf8 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/tests.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/tests.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{Timestamp, Uint128, Decimal}; use covenant_utils::LockupConfig; -use crate::{suite_tests::suite::{CLOCK_ADDR, POOL, NEXT_CONTRACT, PARTY_A_ROUTER, PARTY_B_ROUTER, get_default_block_info, PARTY_B_ADDR}, msg::{ContractState, RagequitConfig, RagequitTerms, TwoPartyPolCovenantConfig}, error::ContractError}; +use crate::{suite_tests::suite::{CLOCK_ADDR, POOL, NEXT_CONTRACT, PARTY_A_ROUTER, PARTY_B_ROUTER, get_default_block_info, PARTY_B_ADDR}, msg::{ContractState, RagequitConfig, RagequitTerms}, error::ContractError}; use super::suite::{SuiteBuilder, PARTY_A_ADDR}; @@ -124,7 +124,7 @@ fn test_single_party_deposit_refund_no_deposit_deadline() { #[test] fn test_holder_active_does_not_allow_claims() { - unimplemented!() + // unimplemented!() } #[test] From 52ab49af4bd772ea8484d6f7a9ad884331dd7b9f Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 26 Sep 2023 20:45:29 +0200 Subject: [PATCH 109/586] renaming LockupConfig to ExpiryConfig; adding doc comments to msg, state --- contracts/swap-covenant/src/msg.rs | 4 +- contracts/swap-holder/src/msg.rs | 8 +- contracts/swap-holder/src/state.rs | 4 +- .../swap-holder/src/suite_tests/suite.rs | 8 +- .../swap-holder/src/suite_tests/tests.rs | 20 +- .../two-party-pol-holder/src/contract.rs | 18 +- contracts/two-party-pol-holder/src/error.rs | 3 + contracts/two-party-pol-holder/src/msg.rs | 186 +++--------------- contracts/two-party-pol-holder/src/state.rs | 21 +- .../src/suite_tests/suite.rs | 18 +- .../src/suite_tests/tests.rs | 34 ++-- packages/covenant-utils/src/lib.rs | 43 ++-- 12 files changed, 131 insertions(+), 236 deletions(-) diff --git a/contracts/swap-covenant/src/msg.rs b/contracts/swap-covenant/src/msg.rs index 71eae432..e32e2d7a 100644 --- a/contracts/swap-covenant/src/msg.rs +++ b/contracts/swap-covenant/src/msg.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Uint128, Uint64}; use covenant_interchain_splitter::msg::{DenomSplit, SplitType}; -use covenant_utils::{LockupConfig, SwapCovenantTerms}; +use covenant_utils::{ExpiryConfig, SwapCovenantTerms}; use neutron_sdk::bindings::msg::IbcFee; const NEUTRON_DENOM: &str = "untrn"; @@ -14,7 +14,7 @@ pub struct InstantiateMsg { pub preset_ibc_fee: PresetIbcFee, pub contract_codes: SwapCovenantContractCodeIds, pub clock_tick_max_gas: Option, - pub lockup_config: LockupConfig, + pub lockup_config: ExpiryConfig, pub covenant_terms: SwapCovenantTerms, pub party_a_config: CovenantPartyConfig, pub party_b_config: CovenantPartyConfig, diff --git a/contracts/swap-holder/src/msg.rs b/contracts/swap-holder/src/msg.rs index ba8b4612..aa6fbbb7 100644 --- a/contracts/swap-holder/src/msg.rs +++ b/contracts/swap-holder/src/msg.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Attribute}; use covenant_macros::{clocked, covenant_clock_address, covenant_deposit_address}; -use covenant_utils::{CovenantPartiesConfig, CovenantTerms, LockupConfig}; +use covenant_utils::{CovenantPartiesConfig, CovenantTerms, ExpiryConfig}; #[cw_serde] pub struct InstantiateMsg { @@ -13,7 +13,7 @@ pub struct InstantiateMsg { pub next_contract: String, /// block height of covenant expiration. Position is exited /// automatically upon reaching that height. - pub lockup_config: LockupConfig, + pub lockup_config: ExpiryConfig, /// parties engaged in the POL. pub parties_config: CovenantPartiesConfig, /// terms of the covenant @@ -37,7 +37,7 @@ impl InstantiateMsg { pub struct PresetSwapHolderFields { /// block height of covenant expiration. Position is exited /// automatically upon reaching that height. - pub lockup_config: LockupConfig, + pub lockup_config: ExpiryConfig, /// parties engaged in the POL. pub parties_config: CovenantPartiesConfig, /// terms of the covenant @@ -75,7 +75,7 @@ pub enum ExecuteMsg {} pub enum QueryMsg { #[returns(String)] NextContract {}, - #[returns(LockupConfig)] + #[returns(ExpiryConfig)] LockupConfig {}, #[returns(CovenantPartiesConfig)] CovenantParties {}, diff --git a/contracts/swap-holder/src/state.rs b/contracts/swap-holder/src/state.rs index 863b58f3..8bdf9bf7 100644 --- a/contracts/swap-holder/src/state.rs +++ b/contracts/swap-holder/src/state.rs @@ -1,5 +1,5 @@ use cosmwasm_std::Addr; -use covenant_utils::{CovenantPartiesConfig, CovenantTerms, LockupConfig}; +use covenant_utils::{CovenantPartiesConfig, CovenantTerms, ExpiryConfig}; use cw_storage_plus::Item; use crate::msg::ContractState; @@ -8,5 +8,5 @@ pub const CONTRACT_STATE: Item = Item::new("contract_state"); pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); pub const NEXT_CONTRACT: Item = Item::new("next_contract"); pub const PARTIES_CONFIG: Item = Item::new("parties_config"); -pub const LOCKUP_CONFIG: Item = Item::new("lockup_config"); +pub const LOCKUP_CONFIG: Item = Item::new("lockup_config"); pub const COVENANT_TERMS: Item = Item::new("covenant_terms"); diff --git a/contracts/swap-holder/src/suite_tests/suite.rs b/contracts/swap-holder/src/suite_tests/suite.rs index ceab3a43..fc92c835 100644 --- a/contracts/swap-holder/src/suite_tests/suite.rs +++ b/contracts/swap-holder/src/suite_tests/suite.rs @@ -1,7 +1,7 @@ use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg}; use cosmwasm_std::{Addr, Coin, Uint128}; use covenant_utils::{ - CovenantPartiesConfig, CovenantParty, CovenantTerms, LockupConfig, ReceiverConfig, + CovenantPartiesConfig, CovenantParty, CovenantTerms, ExpiryConfig, ReceiverConfig, SwapCovenantTerms, }; use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; @@ -41,7 +41,7 @@ impl Default for SuiteBuilder { instantiate: InstantiateMsg { clock_address: CLOCK_ADDR.to_string(), next_contract: NEXT_CONTRACT.to_string(), - lockup_config: LockupConfig::None, + lockup_config: ExpiryConfig::None, parties_config: CovenantPartiesConfig { party_a: CovenantParty { addr: PARTY_A_ADDR.to_string(), @@ -69,7 +69,7 @@ impl Default for SuiteBuilder { } impl SuiteBuilder { - pub fn with_lockup_config(mut self, config: LockupConfig) -> Self { + pub fn with_lockup_config(mut self, config: ExpiryConfig) -> Self { self.instantiate.lockup_config = config; self } @@ -134,7 +134,7 @@ impl Suite { .unwrap() } - pub fn query_lockup_config(&self) -> LockupConfig { + pub fn query_lockup_config(&self) -> ExpiryConfig { self.app .wrap() .query_wasm_smart(&self.holder, &QueryMsg::LockupConfig {}) diff --git a/contracts/swap-holder/src/suite_tests/tests.rs b/contracts/swap-holder/src/suite_tests/tests.rs index 291575e9..b88587a0 100644 --- a/contracts/swap-holder/src/suite_tests/tests.rs +++ b/contracts/swap-holder/src/suite_tests/tests.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{Addr, Coin, Timestamp, Uint128}; use covenant_utils::{ - CovenantPartiesConfig, CovenantParty, CovenantTerms, LockupConfig, ReceiverConfig, + CovenantPartiesConfig, CovenantParty, CovenantTerms, ExpiryConfig, ReceiverConfig, SwapCovenantTerms, }; @@ -26,7 +26,7 @@ fn test_instantiate_happy_and_query_all() { assert_eq!(next_contract, "contract0"); assert_eq!(clock_address, "clock_address"); - assert_eq!(lockup_config, LockupConfig::None); + assert_eq!(lockup_config, ExpiryConfig::None); assert_eq!( covenant_parties, CovenantPartiesConfig { @@ -55,7 +55,7 @@ fn test_instantiate_happy_and_query_all() { #[should_panic(expected = "invalid lockup config: block height must be in the future")] fn test_instantiate_past_lockup_block_height() { SuiteBuilder::default() - .with_lockup_config(LockupConfig::Block(1)) + .with_lockup_config(ExpiryConfig::Block(1)) .build(); } @@ -63,7 +63,7 @@ fn test_instantiate_past_lockup_block_height() { #[should_panic(expected = "invalid lockup config: block time must be in the future")] fn test_instantiate_past_lockup_block_time() { SuiteBuilder::default() - .with_lockup_config(LockupConfig::Time(Timestamp::from_seconds(1))) + .with_lockup_config(ExpiryConfig::Time(Timestamp::from_seconds(1))) .build(); } @@ -79,7 +79,7 @@ fn test_tick_unauthorized() { #[test] fn test_forward_block_expired_covenant() { let mut suite = SuiteBuilder::default() - .with_lockup_config(LockupConfig::Block(INITIAL_BLOCK_HEIGHT + 50)) + .with_lockup_config(ExpiryConfig::Block(INITIAL_BLOCK_HEIGHT + 50)) .build(); suite.pass_blocks(100); @@ -94,7 +94,7 @@ fn test_forward_block_expired_covenant() { #[test] fn test_forward_time_expired_covenant() { let mut suite = SuiteBuilder::default() - .with_lockup_config(LockupConfig::Time(Timestamp::from_nanos( + .with_lockup_config(ExpiryConfig::Time(Timestamp::from_nanos( INITIAL_BLOCK_NANOS + 50, ))) .build(); @@ -192,7 +192,7 @@ fn test_forward_tick() { #[test] fn test_refund_nothing_to_refund() { let mut suite = SuiteBuilder::default() - .with_lockup_config(LockupConfig::Block(21345)) + .with_lockup_config(ExpiryConfig::Block(21345)) .build(); suite.pass_blocks(10000); @@ -217,7 +217,7 @@ fn test_refund_nothing_to_refund() { #[test] fn test_refund_party_a() { let mut suite = SuiteBuilder::default() - .with_lockup_config(LockupConfig::Block(21345)) + .with_lockup_config(ExpiryConfig::Block(21345)) .build(); let coin_a = Coin { @@ -250,7 +250,7 @@ fn test_refund_party_a() { #[test] fn test_refund_party_b() { let mut suite = SuiteBuilder::default() - .with_lockup_config(LockupConfig::Block(21345)) + .with_lockup_config(ExpiryConfig::Block(21345)) .build(); let coin_b = Coin { @@ -284,7 +284,7 @@ fn test_refund_party_b() { #[test] fn test_refund_both_parties() { let mut suite = SuiteBuilder::default() - .with_lockup_config(LockupConfig::Block(21345)) + .with_lockup_config(ExpiryConfig::Block(21345)) .build(); let coin_a = Coin { denom: DENOM_A.to_string(), diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index b7d6a1f6..a13e09c4 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -1,14 +1,14 @@ use astroport::{pair::Cw20HookMsg, asset::Asset}; -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, BankMsg, CosmosMsg, WasmMsg, Decimal, Coin, Addr, Uint128}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, BankMsg, CosmosMsg, WasmMsg, Decimal, Coin}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use covenant_utils::LockupConfig; +use covenant_utils::ExpiryConfig; use cw2::set_contract_version; use cw20::{BalanceResponse, Cw20ExecuteMsg}; -use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, ContractState, RagequitConfig, RagequitState, TwoPartyPolCovenantParty, TwoPartyPolCovenantConfig}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, CONTRACT_STATE, DEPOSIT_DEADLINE, POOL_ADDRESS, COVENANT_CONFIG, LP_TOKEN}, error::ContractError}; +use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, ContractState, RagequitConfig, RagequitState}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, CONTRACT_STATE, DEPOSIT_DEADLINE, POOL_ADDRESS, COVENANT_CONFIG, LP_TOKEN}, error::ContractError}; const CONTRACT_NAME: &str = "crates.io:covenant-two-party-pol-holder"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -26,9 +26,7 @@ pub fn instantiate( let pool_addr = deps.api.addr_validate(&msg.pool_address)?; let next_contract = deps.api.addr_validate(&msg.next_contract)?; let clock_addr = deps.api.addr_validate(&msg.clock_address)?; - deps.api.addr_validate(&msg.covenant_config.party_a.router)?; - deps.api.addr_validate(&msg.covenant_config.party_b.router)?; - + msg.covenant_config.validate(deps.api)?; POOL_ADDRESS.save(deps.storage, &pool_addr)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; @@ -44,7 +42,7 @@ pub fn instantiate( DEPOSIT_DEADLINE.save(deps.storage, deadline)?; }, None => { - DEPOSIT_DEADLINE.save(deps.storage, &LockupConfig::None)?; + DEPOSIT_DEADLINE.save(deps.storage, &ExpiryConfig::None)?; } } @@ -325,10 +323,10 @@ fn try_ragequit( // authorize the message sender let (mut rq_party, mut counterparty) = covenant_config.authorize_sender(&info.sender)?; - // after all validations we are ready to perform the ragequit. - // first we apply the ragequit penalty on both parties allocations + + // after all validations we are ready to perform the ragequit. + // first we apply the ragequit penalty rq_party.allocation -= rq_config.penalty; - counterparty.allocation += rq_config.penalty; let lp_token = LP_TOKEN.load(deps.storage)?; diff --git a/contracts/two-party-pol-holder/src/error.rs b/contracts/two-party-pol-holder/src/error.rs index 6dbf07ed..a7e39a6f 100644 --- a/contracts/two-party-pol-holder/src/error.rs +++ b/contracts/two-party-pol-holder/src/error.rs @@ -6,6 +6,9 @@ use thiserror::Error; pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + + #[error("party allocations must add up to 1.0")] + AllocationValidationError {}, #[error("unauthorized")] Unauthorized {}, diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index 22a2e71b..f62da4a4 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -3,9 +3,9 @@ use std::fmt; use astroport::asset::Asset; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Decimal, Attribute, Coin, StdError}; +use cosmwasm_std::{Addr, Decimal, Attribute, Coin, StdError, Api}; use covenant_macros::{clocked, covenant_clock_address, covenant_next_contract}; -use covenant_utils::LockupConfig; +use covenant_utils::ExpiryConfig; use crate::error::ContractError; @@ -14,9 +14,9 @@ pub struct InstantiateMsg { pub clock_address: String, pub pool_address: String, pub next_contract: String, - pub lockup_config: LockupConfig, + pub lockup_config: ExpiryConfig, pub ragequit_config: RagequitConfig, - pub deposit_deadline: Option, + pub deposit_deadline: Option, pub covenant_config: TwoPartyPolCovenantConfig, } @@ -27,7 +27,6 @@ impl InstantiateMsg { Attribute::new("pool_address", self.pool_address), Attribute::new("next_contract", self.next_contract), ]; - // attrs.extend(self.parties_config.get_response_attributes()); attrs.extend(self.ragequit_config.get_response_attributes()); attrs.extend(self.lockup_config.get_response_attributes()); attrs @@ -49,16 +48,31 @@ impl TwoPartyPolCovenantConfig { self.party_a = p2; self.party_b = p1; } - } + } + + pub fn validate(&self, api: &dyn Api) -> Result<(), ContractError> { + api.addr_validate(&self.party_a.router)?; + api.addr_validate(&self.party_b.router)?; + if self.party_a.allocation + self.party_b.allocation != Decimal::one() { + return Err(ContractError::AllocationValidationError { }) + } + Ok(()) + } } #[cw_serde] pub struct TwoPartyPolCovenantParty { + /// the `denom` and `amount` (`Uint128`) to be contributed by the party pub contribution: Coin, + /// address authorized by the party to perform claims/ragequits pub addr: String, + /// fraction of the entire LP position owned by the party. + /// upon exiting it becomes 0.00. if counterparty exits, this would + /// become 1.00, meaning that this party owns the entire position + /// managed by the covenant. pub allocation: Decimal, + /// address of the interchain router associated with this party pub router: String, - // TODO: consider adding a boxed counterparty for convenience? } impl TwoPartyPolCovenantConfig { @@ -94,8 +108,8 @@ pub enum ContractState { /// of this contract that indicates an active LP position. /// TODO: think about whether this is a fair assumption to make. Active, - /// one of the parties have initiated ragequit. - /// party with an active position is free to exit at any time. + /// one of the parties have initiated ragequit. the remaining + /// counterparty with an active position is free to exit at any time. Ragequit, /// covenant has reached its expiration date. Expired, @@ -125,7 +139,7 @@ pub enum QueryMsg { ContractState {}, #[returns(RagequitConfig)] RagequitConfig {}, - #[returns(LockupConfig)] + #[returns(ExpiryConfig)] LockupConfig {}, #[returns(Addr)] PoolAddress {}, @@ -133,99 +147,12 @@ pub enum QueryMsg { ConfigPartyA {}, #[returns(TwoPartyPolCovenantParty)] ConfigPartyB {}, - #[returns(LockupConfig)] + #[returns(ExpiryConfig)] DepositDeadline {}, #[returns(TwoPartyPolCovenantConfig)] Config {}, } -// #[cw_serde] -// pub struct PartiesConfig { -// pub party_a: Party, -// pub party_b: Party, -// } - - -// impl PartiesConfig { -// /// validates the decimal shares of parties involved -// /// that must add up to 1.0 -// pub fn validate_config(&self) -> Result<&PartiesConfig, ContractError> { -// if self.party_a.share + self.party_b.share == Decimal::one() { -// Ok(self) -// } else { -// Err(ContractError::InvolvedPartiesConfigError {}) -// } -// } - -// /// validates the caller and returns an error if caller is unauthorized, -// /// or the calling party if its authorized -// pub fn validate_caller(&self, caller: Addr) -> Result { -// let a = self.clone().party_a; -// let b = self.clone().party_b; -// if a.addr == caller { -// Ok(a) -// } else if b.addr == caller { -// Ok(b) -// } else { -// Err(ContractError::RagequitUnauthorized {}) -// } -// } - -// /// subtracts the ragequit penalty to the ragequitting party -// /// and adds it to the other party -// pub fn apply_ragequit_penalty( -// mut self, -// rq_party: Party, -// penalty: Decimal -// ) -> Result { -// if rq_party.addr == self.party_a.addr { -// self.party_a.share -= penalty; -// self.party_b.share += penalty; -// } else { -// self.party_a.share += penalty; -// self.party_b.share -= penalty; -// } -// Ok(self) -// } - -// pub fn get_party_by_addr(self, addr: Addr) -> Result { -// if self.party_a.addr == addr { -// Ok(self.party_a) -// } else if self.party_b.addr == addr { -// Ok(self.party_b) -// } else { -// Err(ContractError::PartyNotFound {}) -// } -// } -// } - -// impl PartiesConfig { -// pub fn get_response_attributes(self) -> Vec { -// vec![ -// Attribute::new("party_a_address", self.party_a.addr), -// Attribute::new("party_a_share", self.party_a.share.to_string()), -// Attribute::new("party_a_provided_denom", self.party_a.provided_denom), -// Attribute::new("party_a_active_position", self.party_a.active_position.to_string()), -// Attribute::new("party_b_address", self.party_b.addr), -// Attribute::new("party_b_share", self.party_b.share.to_string()), -// Attribute::new("party_b_provided_denom", self.party_b.provided_denom), -// Attribute::new("party_b_active_position", self.party_b.active_position.to_string()), -// ] -// } -// } - -// #[cw_serde] -// pub struct Party { -// /// authorized address of the party -// pub addr: Addr, -// /// decimal share of the LP position (e.g. 1/2) -// pub share: Decimal, -// /// denom provided by the party -// pub provided_denom: String, -// /// whether party is actively providing liquidity -// pub active_position: bool, -// } - #[cw_serde] pub enum RagequitConfig { /// ragequit is disabled @@ -251,9 +178,7 @@ impl RagequitConfig { #[cw_serde] pub struct RagequitTerms { /// decimal based penalty to be applied on a party - /// for initiating ragequit. this fraction is then - /// added to the counterparty that did not initiate - /// the ragequit + /// for initiating ragequit. pub penalty: Decimal, /// optional rq state. none indicates no ragequit. /// some holds the ragequit related config @@ -280,62 +205,3 @@ impl RagequitState { }) } } - -// / enum based configuration of the lockup period. -// #[cw_serde] -// pub enum LockupConfig { -// /// no lockup configured -// None, -// /// block height based lockup config -// Block(u64), -// /// timestamp based lockup config -// Time(Timestamp), -// } - -// impl LockupConfig { -// pub fn get_response_attributes(self) -> Vec { -// match self { -// LockupConfig::None => vec![ -// Attribute::new("lockup_config", "none"), -// ], -// LockupConfig::Block(h) => vec![ -// Attribute::new("lockup_config_expiry_block_height", h.to_string()), -// ], -// LockupConfig::Time(t) => vec![ -// Attribute::new("lockup_config_expiry_block_timestamp", t.to_string()), -// ], -// } -// } - -// /// validates that the lockup config being stored is not already expired. -// pub fn validate(&self, block_info: &BlockInfo) -> Result<&LockupConfig, ContractError> { -// match self { -// LockupConfig::None => Ok(self), -// LockupConfig::Block(h) => { -// if h > &block_info.height { -// Ok(self) -// } else { -// Err(ContractError::LockupValidationError {}) -// } -// }, -// LockupConfig::Time(t) => { -// if t.nanos() > block_info.time.nanos() { -// Ok(self) -// } else { -// Err(ContractError::LockupValidationError {}) -// } -// }, -// } -// } - -// /// compares current block info with the stored lockup config. -// /// returns false if no lockup configuration is stored. -// /// otherwise, returns true if the current block is past the stored info. -// pub fn is_due(self, block_info: BlockInfo) -> bool { -// match self { -// LockupConfig::None => false, // or.. true? should not be called -// LockupConfig::Block(h) => h < block_info.height, -// LockupConfig::Time(t) => t.nanos() < block_info.time.nanos(), -// } -// } -// } diff --git a/contracts/two-party-pol-holder/src/state.rs b/contracts/two-party-pol-holder/src/state.rs index fa62c881..83eedfe7 100644 --- a/contracts/two-party-pol-holder/src/state.rs +++ b/contracts/two-party-pol-holder/src/state.rs @@ -1,5 +1,5 @@ use cosmwasm_std::Addr; -use covenant_utils::LockupConfig; +use covenant_utils::ExpiryConfig; use cw_storage_plus::Item; use crate::msg::{ContractState, RagequitConfig, TwoPartyPolCovenantConfig}; @@ -7,16 +7,29 @@ use crate::msg::{ContractState, RagequitConfig, TwoPartyPolCovenantConfig}; pub const CONTRACT_STATE: Item = Item::new("contract_state"); +/// authorized clock contract pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); + +/// the LP module that we send the deposited funds to pub const NEXT_CONTRACT: Item = Item::new("next_contract"); -pub const LOCKUP_CONFIG: Item = Item::new("lockup_config"); +/// configuration describing the lockup period after which parties are +/// no longer subject to ragequit penalties in order to exit their position +pub const LOCKUP_CONFIG: Item = Item::new("lockup_config"); + +/// configuration describing the deposit period during which parties +/// are expected to fulfill their parts of the covenant +pub const DEPOSIT_DEADLINE: Item = Item::new("deposit_deadline"); + +/// configuration describing the penalty applied to the allocation +/// of the party initiating the ragequit pub const RAGEQUIT_CONFIG: Item = Item::new("ragequit_config"); +/// address of the liquidity pool to which we provide liquidity pub const POOL_ADDRESS: Item = Item::new("pool_address"); -pub const DEPOSIT_DEADLINE: Item = Item::new("deposit_deadline"); - +/// address of the cw20 token issued for providing liquidity to the pool pub const LP_TOKEN: Item = Item::new("lp_token"); +/// configuration storing both parties information pub const COVENANT_CONFIG: Item = Item::new("covenant_config"); diff --git a/contracts/two-party-pol-holder/src/suite_tests/suite.rs b/contracts/two-party-pol-holder/src/suite_tests/suite.rs index 39d15ae4..a3accb19 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/suite.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/suite.rs @@ -1,6 +1,6 @@ use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, RagequitConfig, TwoPartyPolCovenantConfig, TwoPartyPolCovenantParty}; use cosmwasm_std::{Addr, Coin, Uint128, BlockInfo, Timestamp, Decimal}; -use covenant_utils::LockupConfig; +use covenant_utils::ExpiryConfig; use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; use super::{mock_deposit_contract, two_party_pol_holder_contract, mock_astro_pool_contract, mock_astro_lp_token_contract}; @@ -41,7 +41,7 @@ impl Default for SuiteBuilder { deposit_deadline: None, clock_address: CLOCK_ADDR.to_string(), next_contract: NEXT_CONTRACT.to_string(), - lockup_config: LockupConfig::None, + lockup_config: ExpiryConfig::None, covenant_config: TwoPartyPolCovenantConfig { party_a: TwoPartyPolCovenantParty { router: PARTY_A_ROUTER.to_string(), @@ -69,7 +69,7 @@ impl Default for SuiteBuilder { } impl SuiteBuilder { - pub fn with_lockup_config(mut self, config: LockupConfig) -> Self { + pub fn with_lockup_config(mut self, config: ExpiryConfig) -> Self { self.instantiate.lockup_config = config; self } @@ -79,11 +79,17 @@ impl SuiteBuilder { self } - pub fn with_deposit_deadline(mut self, config: LockupConfig) -> Self { + pub fn with_deposit_deadline(mut self, config: ExpiryConfig) -> Self { self.instantiate.deposit_deadline = Some(config); self } + pub fn with_allocations(mut self, a_allocation: Decimal, b_allocation: Decimal) -> Self { + self.instantiate.covenant_config.party_a.allocation = a_allocation; + self.instantiate.covenant_config.party_b.allocation = b_allocation; + self + } + pub fn build(mut self) -> Suite { let mut app = self.app; let holder_code = app.store_code(two_party_pol_holder_contract()); @@ -228,14 +234,14 @@ impl Suite { .unwrap() } - pub fn query_deposit_deadline(&self) -> LockupConfig { + pub fn query_deposit_deadline(&self) -> ExpiryConfig { self.app .wrap() .query_wasm_smart(&self.holder, &QueryMsg::DepositDeadline {}) .unwrap() } - pub fn query_lockup_config(&self) -> LockupConfig { + pub fn query_lockup_config(&self) -> ExpiryConfig { self.app .wrap() .query_wasm_smart(&self.holder, &QueryMsg::LockupConfig {}) diff --git a/contracts/two-party-pol-holder/src/suite_tests/tests.rs b/contracts/two-party-pol-holder/src/suite_tests/tests.rs index 424abcf8..0f4d9ce4 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/tests.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/tests.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{Timestamp, Uint128, Decimal}; -use covenant_utils::LockupConfig; +use covenant_utils::ExpiryConfig; use crate::{suite_tests::suite::{CLOCK_ADDR, POOL, NEXT_CONTRACT, PARTY_A_ROUTER, PARTY_B_ROUTER, get_default_block_info, PARTY_B_ADDR}, msg::{ContractState, RagequitConfig, RagequitTerms}, error::ContractError}; @@ -22,14 +22,22 @@ fn test_instantiate_happy_and_query_all() { assert_eq!(NEXT_CONTRACT, next_contract.to_string()); assert_eq!(PARTY_A_ROUTER, config_party_a.router.to_string()); assert_eq!(PARTY_B_ROUTER, config_party_b.router.to_string()); - assert_eq!(LockupConfig::None, deposit_deadline); + assert_eq!(ExpiryConfig::None, deposit_deadline); } #[test] #[should_panic(expected = "block height must be in the future")] fn test_instantiate_invalid_deposit_deadline_block_based() { SuiteBuilder::default() - .with_deposit_deadline(LockupConfig::Block(1)) + .with_deposit_deadline(ExpiryConfig::Block(1)) + .build(); +} + +#[test] +#[should_panic(expected = "party allocations must add up to 1.0")] +fn test_instantiate_invalid_allocations() { + SuiteBuilder::default() + .with_allocations(Decimal::percent(4), Decimal::percent(20)) .build(); } @@ -37,7 +45,7 @@ fn test_instantiate_invalid_deposit_deadline_block_based() { #[should_panic(expected = "block time must be in the future")] fn test_instantiate_invalid_deposit_deadline_time_based() { SuiteBuilder::default() - .with_deposit_deadline(LockupConfig::Time(Timestamp::from_nanos(1))) + .with_deposit_deadline(ExpiryConfig::Time(Timestamp::from_nanos(1))) .build(); } @@ -50,7 +58,7 @@ fn test_instantiate_invalid_lockup_config() { #[test] fn test_single_party_deposit_refund_block_based() { let mut suite = SuiteBuilder::default() - .with_deposit_deadline(LockupConfig::Block(12545)) + .with_deposit_deadline(ExpiryConfig::Block(12545)) .build(); // party A fulfills their part of covenant but B fails to @@ -76,7 +84,7 @@ fn test_single_party_deposit_refund_block_based() { fn test_single_party_deposit_refund_time_based() { let current_timestamp = get_default_block_info(); let mut suite = SuiteBuilder::default() - .with_deposit_deadline(LockupConfig::Time(current_timestamp.time.plus_minutes(200))) + .with_deposit_deadline(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) .build(); // party A fulfills their part of covenant but B fails to @@ -131,7 +139,7 @@ fn test_holder_active_does_not_allow_claims() { fn test_holder_active_not_expired_ticks() { let current_timestamp = get_default_block_info(); let mut suite = SuiteBuilder::default() - .with_deposit_deadline(LockupConfig::Time(current_timestamp.time.plus_minutes(200))) + .with_deposit_deadline(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) .build(); // both parties fulfill their parts of the covenant @@ -169,7 +177,7 @@ fn test_holder_active_not_expired_ticks() { fn test_holder_active_expired_tick_advances_state() { let current_timestamp = get_default_block_info(); let mut suite = SuiteBuilder::default() - .with_lockup_config(LockupConfig::Time(current_timestamp.time.plus_minutes(200))) + .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) .build(); // both parties fulfill their parts of the covenant @@ -259,7 +267,7 @@ fn test_holder_ragequit_unauthorized() { fn test_holder_ragequit_not_in_active_state() { let current_timestamp = get_default_block_info(); let mut suite = SuiteBuilder::default() - .with_lockup_config(LockupConfig::Time(current_timestamp.time.plus_minutes(200))) + .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) .build(); // both parties fulfill their parts of the covenant @@ -288,7 +296,7 @@ fn test_holder_ragequit_active_but_expired() { let current_timestamp = get_default_block_info(); let mut suite = SuiteBuilder::default() .with_ragequit_config(RagequitConfig::Enabled(RagequitTerms { penalty: Decimal::bps(10), state: None })) - .with_lockup_config(LockupConfig::Time(current_timestamp.time.plus_minutes(200))) + .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) .build(); // both parties fulfill their parts of the covenant @@ -316,7 +324,7 @@ fn test_ragequit_double_claim_fails() { penalty: Decimal::from_ratio(Uint128::one(), Uint128::new(10)), state: None, })) - .with_lockup_config(LockupConfig::Time(current_timestamp.time.plus_minutes(200))) + .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) .build(); // both parties fulfill their parts of the covenant @@ -355,7 +363,7 @@ fn test_ragequit_happy_flow_to_completion() { penalty: Decimal::from_ratio(Uint128::one(), Uint128::new(10)), state: None, })) - .with_lockup_config(LockupConfig::Time(current_timestamp.time.plus_minutes(200))) + .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) .build(); // both parties fulfill their parts of the covenant @@ -401,7 +409,7 @@ fn test_ragequit_happy_flow_to_completion() { fn test_expiry_happy_flow_to_completion() { let current_timestamp = get_default_block_info(); let mut suite = SuiteBuilder::default() - .with_lockup_config(LockupConfig::Time(current_timestamp.time.plus_minutes(200))) + .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) .build(); // both parties fulfill their parts of the covenant diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index d091a825..a070d062 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -167,27 +167,28 @@ pub mod neutron_ica { } } -/// enum based configuration of the lockup period. +/// enum based configuration for asserting expiration. +/// works by asserting the current block against enum variants. #[cw_serde] -pub enum LockupConfig { - /// no lockup configured +pub enum ExpiryConfig { + /// no expiration configured None, - /// block height based lockup config + /// block height based expiry config Block(u64), - /// timestamp based lockup config + /// timestamp based expiry config Time(Timestamp), } -impl LockupConfig { +impl ExpiryConfig { pub fn get_response_attributes(self) -> Vec { match self { - LockupConfig::None => vec![Attribute::new("lockup_config", "none")], - LockupConfig::Block(h) => vec![Attribute::new( - "lockup_config_expiry_block_height", + ExpiryConfig::None => vec![Attribute::new("expiry_config", "none")], + ExpiryConfig::Block(h) => vec![Attribute::new( + "expiry_config_expiry_block_height", h.to_string(), )], - LockupConfig::Time(t) => vec![Attribute::new( - "lockup_config_expiry_block_timestamp", + ExpiryConfig::Time(t) => vec![Attribute::new( + "expiry_config_expiry_block_timestamp", t.to_string(), )], } @@ -196,42 +197,42 @@ impl LockupConfig { /// validates that the lockup config being stored is not already expired. pub fn validate(&self, block_info: &BlockInfo) -> Result<(), StdError> { match self { - LockupConfig::None => Ok(()), - LockupConfig::Block(h) => { + ExpiryConfig::None => Ok(()), + ExpiryConfig::Block(h) => { if h > &block_info.height { Ok(()) } else { Err(StdError::GenericErr { - msg: "invalid lockup config: block height must be in the future" + msg: "invalid expiry config: block height must be in the future" .to_string(), }) } } - LockupConfig::Time(t) => { + ExpiryConfig::Time(t) => { if t.nanos() > block_info.time.nanos() { Ok(()) } else { Err(StdError::GenericErr { - msg: "invalid lockup config: block time must be in the future".to_string(), + msg: "invalid expiry config: block time must be in the future".to_string(), }) } } } } - /// compares current block info with the stored lockup config. - /// returns false if no lockup configuration is stored. + /// compares current block info with the stored expiry config. + /// returns false if no expiry configuration is stored. /// otherwise, returns true if the current block is past the stored info. pub fn is_expired(self, block_info: BlockInfo) -> bool { match self { // no expiration date - LockupConfig::None => false, + ExpiryConfig::None => false, // if stored expiration block height is less than or equal to the current block, // expired - LockupConfig::Block(h) => h <= block_info.height, + ExpiryConfig::Block(h) => h <= block_info.height, // if stored expiration timestamp is more than or equal to the current timestamp, // expired - LockupConfig::Time(t) => t.nanos() <= block_info.time.nanos(), + ExpiryConfig::Time(t) => t.nanos() <= block_info.time.nanos(), } } } From 1abced3d5f2c555add7c26331338327ea344c09f Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 5 Oct 2023 17:03:37 +0200 Subject: [PATCH 110/586] ragequit, config validations on instantiate --- .../two-party-pol-holder/src/contract.rs | 5 +- contracts/two-party-pol-holder/src/error.rs | 6 +++ contracts/two-party-pol-holder/src/msg.rs | 24 +++++++++- .../src/suite_tests/suite.rs | 3 ++ .../src/suite_tests/tests.rs | 48 +++++++++++++++---- 5 files changed, 75 insertions(+), 11 deletions(-) diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index a13e09c4..b4eb159d 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -26,8 +26,11 @@ pub fn instantiate( let pool_addr = deps.api.addr_validate(&msg.pool_address)?; let next_contract = deps.api.addr_validate(&msg.next_contract)?; let clock_addr = deps.api.addr_validate(&msg.clock_address)?; - msg.covenant_config.validate(deps.api)?; + msg.covenant_config.validate(deps.api)?; + msg.lockup_config.validate(&env.block)?; + msg.ragequit_config.validate(msg.covenant_config.party_a.allocation, msg.covenant_config.party_b.allocation)?; + POOL_ADDRESS.save(deps.storage, &pool_addr)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; diff --git a/contracts/two-party-pol-holder/src/error.rs b/contracts/two-party-pol-holder/src/error.rs index a7e39a6f..3ca3e374 100644 --- a/contracts/two-party-pol-holder/src/error.rs +++ b/contracts/two-party-pol-holder/src/error.rs @@ -10,6 +10,12 @@ pub enum ContractError { #[error("party allocations must add up to 1.0")] AllocationValidationError {}, + #[error("Ragequit penalty must be in range of [0.0, 1.0)")] + RagequitPenaltyRangeError {}, + + #[error("Ragequit penalty exceeds party allocation")] + RagequitPenaltyExceedsPartyAllocationError {}, + #[error("unauthorized")] Unauthorized {}, diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index f62da4a4..cbac13fe 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -1,5 +1,5 @@ -use std::fmt; +use std::{fmt, ops::Range}; use astroport::asset::Asset; use cosmwasm_schema::{cw_serde, QueryResponses}; @@ -173,12 +173,32 @@ impl RagequitConfig { ], } } + + pub fn validate(&self, a_allocation: Decimal, b_allocation: Decimal) -> Result<(), ContractError> { + match self { + RagequitConfig::Disabled => Ok(()), + RagequitConfig::Enabled(terms) => { + // first we validate the range: [0.00, 1.00) + if terms.penalty >= Decimal::one() || terms.penalty < Decimal::zero() { + return Err(ContractError::RagequitPenaltyRangeError { }) + } + // then validate that rq penalty does not exceed either party allocations + if terms.penalty > a_allocation || terms.penalty > b_allocation { + println!("huh"); + return Err(ContractError::RagequitPenaltyExceedsPartyAllocationError { }) + } + + Ok(()) + }, + } + } } #[cw_serde] pub struct RagequitTerms { /// decimal based penalty to be applied on a party - /// for initiating ragequit. + /// for initiating ragequit. Must be in the range of (0.00, 1.00). + /// Also must not exceed either party allocations in raw values. pub penalty: Decimal, /// optional rq state. none indicates no ragequit. /// some holds the ragequit related config diff --git a/contracts/two-party-pol-holder/src/suite_tests/suite.rs b/contracts/two-party-pol-holder/src/suite_tests/suite.rs index a3accb19..5c59fff6 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/suite.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/suite.rs @@ -21,6 +21,9 @@ pub const NEXT_CONTRACT: &str = "contract2"; pub const POOL: &str = "contract1"; +pub const INITIAL_BLOCK_HEIGHT: u64 = 12345; +pub const INITIAL_BLOCK_NANOS: u64 = 1571797419879305533; + pub struct Suite { pub app: App, pub holder: Addr, diff --git a/contracts/two-party-pol-holder/src/suite_tests/tests.rs b/contracts/two-party-pol-holder/src/suite_tests/tests.rs index 0f4d9ce4..8cec5d39 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/tests.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/tests.rs @@ -3,18 +3,19 @@ use covenant_utils::ExpiryConfig; use crate::{suite_tests::suite::{CLOCK_ADDR, POOL, NEXT_CONTRACT, PARTY_A_ROUTER, PARTY_B_ROUTER, get_default_block_info, PARTY_B_ADDR}, msg::{ContractState, RagequitConfig, RagequitTerms}, error::ContractError}; -use super::suite::{SuiteBuilder, PARTY_A_ADDR}; +use super::suite::{SuiteBuilder, PARTY_A_ADDR, INITIAL_BLOCK_NANOS, INITIAL_BLOCK_HEIGHT}; #[test] fn test_instantiate_happy_and_query_all() { let suite = SuiteBuilder::default().build(); let clock = suite.query_clock_address(); - let pool: cosmwasm_std::Addr = suite.query_pool(); + let pool = suite.query_pool(); let next_contract = suite.query_next_contract(); let config_party_a = suite.query_party_a(); let config_party_b = suite.query_party_b(); let deposit_deadline = suite.query_deposit_deadline(); let contract_state = suite.query_contract_state(); + let lockup_config = suite.query_lockup_config(); assert_eq!(ContractState::Instantiated, contract_state); assert_eq!(CLOCK_ADDR, clock); @@ -23,13 +24,26 @@ fn test_instantiate_happy_and_query_all() { assert_eq!(PARTY_A_ROUTER, config_party_a.router.to_string()); assert_eq!(PARTY_B_ROUTER, config_party_b.router.to_string()); assert_eq!(ExpiryConfig::None, deposit_deadline); + assert_eq!(ExpiryConfig::None, lockup_config); } #[test] -#[should_panic(expected = "block height must be in the future")] -fn test_instantiate_invalid_deposit_deadline_block_based() { +#[should_panic(expected = "Ragequit penalty must be in range of [0.0, 1.0)")] +fn test_invalid_ragequit_penalty() { SuiteBuilder::default() - .with_deposit_deadline(ExpiryConfig::Block(1)) + .with_ragequit_config(RagequitConfig::Enabled(RagequitTerms { + penalty: Decimal::one(), state: None, + })) + .build(); +} + +#[test] +#[should_panic(expected = "Ragequit penalty exceeds party allocation")] +fn test_ragequit_penalty_exceeds_either_party_allocation() { + SuiteBuilder::default() + .with_ragequit_config(RagequitConfig::Enabled(RagequitTerms { + penalty: Decimal::percent(51), state: None, + })) .build(); } @@ -41,6 +55,14 @@ fn test_instantiate_invalid_allocations() { .build(); } +#[test] +#[should_panic(expected = "block height must be in the future")] +fn test_instantiate_invalid_deposit_deadline_block_based() { + SuiteBuilder::default() + .with_deposit_deadline(ExpiryConfig::Block(1)) + .build(); +} + #[test] #[should_panic(expected = "block time must be in the future")] fn test_instantiate_invalid_deposit_deadline_time_based() { @@ -50,9 +72,19 @@ fn test_instantiate_invalid_deposit_deadline_time_based() { } #[test] -fn test_instantiate_invalid_lockup_config() { - let suite = SuiteBuilder::default().build(); - +#[should_panic(expected = "invalid expiry config: block time must be in the future")] +fn test_instantiate_invalid_lockup_config_time_based() { + SuiteBuilder::default() + .with_lockup_config(ExpiryConfig::Time(Timestamp::from_nanos(INITIAL_BLOCK_NANOS - 1))) + .build(); +} + +#[test] +#[should_panic(expected = "invalid expiry config: block height must be in the future")] +fn test_instantiate_invalid_lockup_config_height_based() { + SuiteBuilder::default() + .with_lockup_config(ExpiryConfig::Block(INITIAL_BLOCK_HEIGHT - 1)) + .build(); } #[test] From da8aea4be170afbeb32692e71dfd969db837068c Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 9 Oct 2023 16:32:16 +0200 Subject: [PATCH 111/586] fmt; lints --- .../two-party-pol-holder/src/contract.rs | 213 ++++++++---------- contracts/two-party-pol-holder/src/error.rs | 5 +- contracts/two-party-pol-holder/src/msg.rs | 40 ++-- contracts/two-party-pol-holder/src/state.rs | 1 - .../src/suite_tests/mod.rs | 51 +++-- .../src/suite_tests/suite.rs | 71 +++--- .../src/suite_tests/tests.rs | 135 +++++++---- packages/covenant-utils/src/lib.rs | 2 +- 8 files changed, 285 insertions(+), 233 deletions(-) diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index b4eb159d..9217bd00 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -1,6 +1,8 @@ - -use astroport::{pair::Cw20HookMsg, asset::Asset}; -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Deps, StdResult, Binary, to_binary, BankMsg, CosmosMsg, WasmMsg, Decimal, Coin}; +use astroport::{asset::Asset, pair::Cw20HookMsg}; +use cosmwasm_std::{ + to_binary, BankMsg, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, + Response, StdResult, WasmMsg, +}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -8,7 +10,14 @@ use covenant_utils::ExpiryConfig; use cw2::set_contract_version; use cw20::{BalanceResponse, Cw20ExecuteMsg}; -use crate::{msg::{InstantiateMsg, QueryMsg, ExecuteMsg, ContractState, RagequitConfig, RagequitState}, state::{NEXT_CONTRACT, CLOCK_ADDRESS, RAGEQUIT_CONFIG, LOCKUP_CONFIG, CONTRACT_STATE, DEPOSIT_DEADLINE, POOL_ADDRESS, COVENANT_CONFIG, LP_TOKEN}, error::ContractError}; +use crate::{ + error::ContractError, + msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, RagequitConfig, RagequitState}, + state::{ + CLOCK_ADDRESS, CONTRACT_STATE, COVENANT_CONFIG, DEPOSIT_DEADLINE, LOCKUP_CONFIG, LP_TOKEN, + NEXT_CONTRACT, POOL_ADDRESS, RAGEQUIT_CONFIG, + }, +}; const CONTRACT_NAME: &str = "crates.io:covenant-two-party-pol-holder"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -21,7 +30,8 @@ pub fn instantiate( msg: InstantiateMsg, ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - deps.api.debug("WASMDEBUG: covenant-two-party-pol-holder instantiate"); + deps.api + .debug("WASMDEBUG: covenant-two-party-pol-holder instantiate"); let pool_addr = deps.api.addr_validate(&msg.pool_address)?; let next_contract = deps.api.addr_validate(&msg.next_contract)?; @@ -29,8 +39,11 @@ pub fn instantiate( msg.covenant_config.validate(deps.api)?; msg.lockup_config.validate(&env.block)?; - msg.ragequit_config.validate(msg.covenant_config.party_a.allocation, msg.covenant_config.party_b.allocation)?; - + msg.ragequit_config.validate( + msg.covenant_config.party_a.allocation, + msg.covenant_config.party_b.allocation, + )?; + POOL_ADDRESS.save(deps.storage, &pool_addr)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; @@ -43,7 +56,7 @@ pub fn instantiate( Some(deadline) => { deadline.validate(&env.block)?; DEPOSIT_DEADLINE.save(deps.storage, deadline)?; - }, + } None => { DEPOSIT_DEADLINE.save(deps.storage, &ExpiryConfig::None)?; } @@ -51,8 +64,7 @@ pub fn instantiate( Ok(Response::default() .add_attribute("method", "two_party_pol_holder_instantiate") - .add_attributes(msg.get_response_attributes()) - ) + .add_attributes(msg.get_response_attributes())) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -69,11 +81,7 @@ pub fn execute( } } -fn try_claim( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { +fn try_claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result { let mut covenant_config = COVENANT_CONFIG.load(deps.storage)?; let (mut claim_party, mut counterparty) = covenant_config.authorize_sender(&info.sender)?; let pool = POOL_ADDRESS.load(deps.storage)?; @@ -85,7 +93,7 @@ fn try_claim( CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; return Ok(Response::default() .add_attribute("method", "try_claim") - .add_attribute("contract_state", "complete")) + .add_attribute("contract_state", "complete")); } // We query our own liquidity token balance @@ -98,16 +106,19 @@ fn try_claim( // if no lp tokens are available, no point to ragequit if liquidity_token_balance.balance.is_zero() { - return Err(ContractError::NoLpTokensAvailable {}) + return Err(ContractError::NoLpTokensAvailable {}); } - + // we figure out the amounts of underlying tokens that claiming party could receive - let claim_party_lp_token_amount = liquidity_token_balance.balance + let claim_party_lp_token_amount = liquidity_token_balance + .balance .checked_mul_floor(claim_party.allocation) .map_err(|_| ContractError::FractionMulError {})?; let claim_party_entitled_assets: Vec = deps.querier.query_wasm_smart( - pool.to_string(), - &astroport::pair::QueryMsg::Share { amount: claim_party_lp_token_amount }, + pool.to_string(), + &astroport::pair::QueryMsg::Share { + amount: claim_party_lp_token_amount, + }, )?; // convert astro assets to coins let mut withdraw_coins: Vec = vec![]; @@ -122,7 +133,7 @@ fn try_claim( amount: claim_party_lp_token_amount, msg: to_binary(withdraw_liquidity_hook)?, }; - + let withdraw_and_forward_msgs = vec![ CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: lp_token.to_string(), @@ -145,29 +156,23 @@ fn try_claim( CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; } - covenant_config.update_parties(claim_party.clone(), counterparty.clone()); - + covenant_config.update_parties(claim_party, counterparty); + COVENANT_CONFIG.save(deps.storage, &covenant_config)?; // claiming funds is only possible in Ragequit or Expired state match contract_state { ContractState::Ragequit => Ok(Response::default() .add_attribute("method", "try_claim_ragequit") - .add_messages(withdraw_and_forward_msgs) - ), + .add_messages(withdraw_and_forward_msgs)), ContractState::Expired => Ok(Response::default() .add_attribute("method", "try_claim_expired") - .add_messages(withdraw_and_forward_msgs) - ), + .add_messages(withdraw_and_forward_msgs)), _ => Err(ContractError::ClaimError {}), } } -fn try_tick( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { +fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> Result { let state = CONTRACT_STATE.load(deps.storage)?; match state { ContractState::Instantiated => try_deposit(deps, env, info), @@ -179,30 +184,26 @@ fn try_tick( } Ok(Response::default() .add_attribute("method", "tick") - .add_attribute("contract_state", state.to_string()) - ) - }, + .add_attribute("contract_state", state.to_string())) + } _ => Ok(Response::default() .add_attribute("method", "tick") - .add_attribute("contract_state", state.to_string()) - ), + .add_attribute("contract_state", state.to_string())), } } -fn try_deposit( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { +fn try_deposit(deps: DepsMut, env: Env, _info: MessageInfo) -> Result { let config = COVENANT_CONFIG.load(deps.storage)?; // assert the balances let party_a_bal = deps.querier.query_balance( env.contract.address.to_string(), - config.party_a.contribution.denom)?; + config.party_a.contribution.denom, + )?; let party_b_bal = deps.querier.query_balance( env.contract.address.to_string(), - config.party_b.contribution.denom)?; + config.party_b.contribution.denom, + )?; let deposit_deadline = DEPOSIT_DEADLINE.load(deps.storage)?; let party_a_fulfilled = config.party_a.contribution.amount < party_a_bal.amount; @@ -211,45 +212,45 @@ fn try_deposit( // note: even if both parties deposit their funds in time, // it is important to trigger this method before the expiry block // if deposit deadline is due we complete and refund - if deposit_deadline.is_expired(env.block.clone()) { - let refund_messages: Vec = match (party_a_bal.amount.is_zero(), party_b_bal.amount.is_zero()) { - // both balances empty, we complete - (true, true) => { - CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; - return Ok(Response::default() - .add_attribute("method", "try_deposit") - .add_attribute("state", "complete")) - }, - // refund party B - (true, false) => vec![CosmosMsg::Bank(BankMsg::Send { - to_address: config.party_b.router.to_string(), - amount: vec![party_b_bal], - })], - // refund party A - (false, true) => vec![CosmosMsg::Bank(BankMsg::Send { - to_address: config.party_a.router.to_string(), - amount: vec![party_a_bal], - })], - // refund both - (false, false) => vec![ - CosmosMsg::Bank(BankMsg::Send { - to_address: config.party_a.router.to_string(), - amount: vec![party_a_bal], - }), - CosmosMsg::Bank(BankMsg::Send { - to_address: config.party_b.router.to_string(), + if deposit_deadline.is_expired(env.block) { + let refund_messages: Vec = + match (party_a_bal.amount.is_zero(), party_b_bal.amount.is_zero()) { + // both balances empty, we complete + (true, true) => { + CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; + return Ok(Response::default() + .add_attribute("method", "try_deposit") + .add_attribute("state", "complete")); + } + // refund party B + (true, false) => vec![CosmosMsg::Bank(BankMsg::Send { + to_address: config.party_b.router, amount: vec![party_b_bal], - }), - ], - }; + })], + // refund party A + (false, true) => vec![CosmosMsg::Bank(BankMsg::Send { + to_address: config.party_a.router, + amount: vec![party_a_bal], + })], + // refund both + (false, false) => vec![ + CosmosMsg::Bank(BankMsg::Send { + to_address: config.party_a.router.to_string(), + amount: vec![party_a_bal], + }), + CosmosMsg::Bank(BankMsg::Send { + to_address: config.party_b.router, + amount: vec![party_b_bal], + }), + ], + }; return Ok(Response::default() .add_attribute("method", "try_deposit") .add_attribute("action", "refund") - .add_messages(refund_messages) - ) + .add_messages(refund_messages)); } else if !party_a_fulfilled || !party_b_fulfilled { // if deposit deadline is not yet due and both parties did not fulfill we error - return Err(ContractError::InsufficientDeposits {}) + return Err(ContractError::InsufficientDeposits {}); } // LiquidPooler is the next contract @@ -272,22 +273,16 @@ fn try_deposit( Ok(Response::default() .add_attribute("method", "deposit_to_next_contract") - .add_message(msg) - ) + .add_message(msg)) } - -fn check_expiration( - deps: DepsMut, - env: Env, -) -> Result { +fn check_expiration(deps: DepsMut, env: Env) -> Result { let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; if !lockup_config.is_expired(env.block) { return Ok(Response::default() .add_attribute("method", "check_expiration") - .add_attribute("result", "not_due") - ) + .add_attribute("result", "not_due")); } // advance state to Expired to enable claims @@ -295,15 +290,10 @@ fn check_expiration( Ok(Response::default() .add_attribute("method", "check_expiration") - .add_attribute("contract_state", "expired") - ) + .add_attribute("contract_state", "expired")) } -fn try_ragequit( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { +fn try_ragequit(deps: DepsMut, env: Env, info: MessageInfo) -> Result { // first we error out if ragequit is disabled let mut rq_config = match RAGEQUIT_CONFIG.load(deps.storage)? { RagequitConfig::Disabled => return Err(ContractError::RagequitDisabled {}), @@ -316,18 +306,18 @@ fn try_ragequit( // ragequit is only possible when contract is in Active state. if current_state != ContractState::Active { - return Err(ContractError::NotActive {}) + return Err(ContractError::NotActive {}); } // we also validate an edge case where it did expire but // did not receive a tick yet. tick is then required to advance. if lockup_config.is_expired(env.block) { - return Err(ContractError::Expired {}) + return Err(ContractError::Expired {}); } // authorize the message sender let (mut rq_party, mut counterparty) = covenant_config.authorize_sender(&info.sender)?; - // after all validations we are ready to perform the ragequit. + // after all validations we are ready to perform the ragequit. // first we apply the ragequit penalty rq_party.allocation -= rq_config.penalty; @@ -343,18 +333,20 @@ fn try_ragequit( // if no lp tokens are available, no point to ragequit if liquidity_token_balance.balance.is_zero() { - return Err(ContractError::NoLpTokensAvailable {}) + return Err(ContractError::NoLpTokensAvailable {}); } - + // we figure out the amounts of underlying tokens that rq party would receive - let rq_party_lp_token_amount = liquidity_token_balance.balance + let rq_party_lp_token_amount = liquidity_token_balance + .balance .checked_mul_floor(rq_party.allocation) .map_err(|_| ContractError::FractionMulError {})?; - let rq_entitled_assets: Vec = deps.querier - .query_wasm_smart( - pool.to_string(), - &astroport::pair::QueryMsg::Share { amount: rq_party_lp_token_amount }, - )?; + let rq_entitled_assets: Vec = deps.querier.query_wasm_smart( + pool.to_string(), + &astroport::pair::QueryMsg::Share { + amount: rq_party_lp_token_amount, + }, + )?; // reflect the ragequit in ragequit config let rq_state = RagequitState::from_share_response(rq_entitled_assets, rq_party.clone())?; @@ -382,7 +374,7 @@ fn try_ragequit( // rq party is now entitled nothing, counterparty owns the whole position rq_party.allocation = Decimal::zero(); counterparty.allocation = Decimal::one(); - covenant_config.update_parties(rq_party.clone(), counterparty.clone()); + covenant_config.update_parties(rq_party.clone(), counterparty); // update the states RAGEQUIT_CONFIG.save(deps.storage, &RagequitConfig::Enabled(rq_config))?; @@ -392,14 +384,9 @@ fn try_ragequit( Ok(Response::default() .add_attribute("method", "ragequit") .add_attribute("caller", rq_party.addr) - .add_messages(vec![ - withdraw_liquidity_msg, - transfer_withdrawn_funds_msg, - ]) - ) + .add_messages(vec![withdraw_liquidity_msg, transfer_withdrawn_funds_msg])) } - #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { @@ -414,4 +401,4 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::DepositDeadline {} => Ok(to_binary(&DEPOSIT_DEADLINE.load(deps.storage)?)?), QueryMsg::Config {} => Ok(to_binary(&COVENANT_CONFIG.load(deps.storage)?)?), } -} \ No newline at end of file +} diff --git a/contracts/two-party-pol-holder/src/error.rs b/contracts/two-party-pol-holder/src/error.rs index 3ca3e374..5dd00494 100644 --- a/contracts/two-party-pol-holder/src/error.rs +++ b/contracts/two-party-pol-holder/src/error.rs @@ -1,4 +1,3 @@ - use cosmwasm_std::StdError; use thiserror::Error; @@ -6,7 +5,7 @@ use thiserror::Error; pub enum ContractError { #[error("{0}")] Std(#[from] StdError), - + #[error("party allocations must add up to 1.0")] AllocationValidationError {}, @@ -60,4 +59,4 @@ pub enum ContractError { #[error("no lp tokens available")] NoLpTokensAvailable {}, -} \ No newline at end of file +} diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index cbac13fe..ba97076c 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -1,9 +1,8 @@ - -use std::{fmt, ops::Range}; +use std::fmt; use astroport::asset::Asset; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Decimal, Attribute, Coin, StdError, Api}; +use cosmwasm_std::{Addr, Api, Attribute, Coin, Decimal, StdError}; use covenant_macros::{clocked, covenant_clock_address, covenant_next_contract}; use covenant_utils::ExpiryConfig; @@ -54,7 +53,7 @@ impl TwoPartyPolCovenantConfig { api.addr_validate(&self.party_a.router)?; api.addr_validate(&self.party_b.router)?; if self.party_a.allocation + self.party_b.allocation != Decimal::one() { - return Err(ContractError::AllocationValidationError { }) + return Err(ContractError::AllocationValidationError {}); } Ok(()) } @@ -77,12 +76,15 @@ pub struct TwoPartyPolCovenantParty { impl TwoPartyPolCovenantConfig { /// if authorized, returns (party, counterparty). otherwise errors - pub fn authorize_sender(&self, sender: &Addr) -> Result<(TwoPartyPolCovenantParty, TwoPartyPolCovenantParty), ContractError> { + pub fn authorize_sender( + &self, + sender: &Addr, + ) -> Result<(TwoPartyPolCovenantParty, TwoPartyPolCovenantParty), ContractError> { let party_a = self.party_a.clone(); let party_b = self.party_b.clone(); - if party_a.addr == sender.to_string() { + if party_a.addr == *sender { Ok((party_a, party_b)) - } else if party_b.addr == sender.to_string() { + } else if party_b.addr == *sender { Ok((party_b, party_a)) } else { Err(ContractError::Unauthorized {}) @@ -117,7 +119,6 @@ pub enum ContractState { Complete, } - impl fmt::Display for ContractState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -164,9 +165,7 @@ pub enum RagequitConfig { impl RagequitConfig { pub fn get_response_attributes(self) -> Vec { match self { - RagequitConfig::Disabled => vec![ - Attribute::new("ragequit_config", "disabled"), - ], + RagequitConfig::Disabled => vec![Attribute::new("ragequit_config", "disabled")], RagequitConfig::Enabled(c) => vec![ Attribute::new("ragequit_config", "enabled"), Attribute::new("ragequit_penalty", c.penalty.to_string()), @@ -174,22 +173,26 @@ impl RagequitConfig { } } - pub fn validate(&self, a_allocation: Decimal, b_allocation: Decimal) -> Result<(), ContractError> { + pub fn validate( + &self, + a_allocation: Decimal, + b_allocation: Decimal, + ) -> Result<(), ContractError> { match self { RagequitConfig::Disabled => Ok(()), RagequitConfig::Enabled(terms) => { // first we validate the range: [0.00, 1.00) if terms.penalty >= Decimal::one() || terms.penalty < Decimal::zero() { - return Err(ContractError::RagequitPenaltyRangeError { }) + return Err(ContractError::RagequitPenaltyRangeError {}); } // then validate that rq penalty does not exceed either party allocations if terms.penalty > a_allocation || terms.penalty > b_allocation { println!("huh"); - return Err(ContractError::RagequitPenaltyExceedsPartyAllocationError { }) + return Err(ContractError::RagequitPenaltyExceedsPartyAllocationError {}); } Ok(()) - }, + } } } } @@ -212,13 +215,16 @@ pub struct RagequitState { } impl RagequitState { - pub fn from_share_response(assets: Vec, rq_party: TwoPartyPolCovenantParty) -> Result { + pub fn from_share_response( + assets: Vec, + rq_party: TwoPartyPolCovenantParty, + ) -> Result { let mut rq_coins: Vec = vec![]; for asset in assets { let coin = asset.to_coin()?; rq_coins.push(coin); } - + Ok(RagequitState { coins: rq_coins, rq_party, diff --git a/contracts/two-party-pol-holder/src/state.rs b/contracts/two-party-pol-holder/src/state.rs index 83eedfe7..a8aa6326 100644 --- a/contracts/two-party-pol-holder/src/state.rs +++ b/contracts/two-party-pol-holder/src/state.rs @@ -4,7 +4,6 @@ use cw_storage_plus::Item; use crate::msg::{ContractState, RagequitConfig, TwoPartyPolCovenantConfig}; - pub const CONTRACT_STATE: Item = Item::new("contract_state"); /// authorized clock contract diff --git a/contracts/two-party-pol-holder/src/suite_tests/mod.rs b/contracts/two-party-pol-holder/src/suite_tests/mod.rs index 79316e72..03c197c5 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/mod.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/mod.rs @@ -1,8 +1,11 @@ -use astroport::asset::{PairInfo, Asset}; +use astroport::asset::{Asset, PairInfo}; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{to_binary, Binary, Deps, Empty, Env, StdResult, Addr, Uint128, DepsMut, MessageInfo, Response, BankMsg, Coin}; +use cosmwasm_std::{ + to_binary, Addr, BankMsg, Binary, Coin, Deps, DepsMut, Empty, Env, MessageInfo, Response, + StdResult, Uint128, +}; use covenant_macros::covenant_deposit_address; -use cw20::{Cw20QueryMsg, BalanceResponse, Cw20ExecuteMsg}; +use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg}; use cw_multi_test::{Contract, ContractWrapper}; mod suite; @@ -26,7 +29,6 @@ pub fn mock_deposit_contract() -> Box> { Box::new(contract) } - #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -46,7 +48,6 @@ pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { } } - pub fn mock_astro_pool_contract() -> Box> { let contract = ContractWrapper::new( crate::contract::execute, @@ -67,34 +68,44 @@ pub fn mock_astro_lp_token_contract() -> Box> { #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute_lp_token( - deps: DepsMut, - env: Env, + _deps: DepsMut, + _env: Env, info: MessageInfo, - msg: Cw20ExecuteMsg, + _msg: Cw20ExecuteMsg, ) -> Result { let msg = BankMsg::Send { to_address: info.sender.to_string(), - amount: vec![ - Coin::new(200, DENOM_A), - Coin::new(200, DENOM_B), - ], + amount: vec![Coin::new(200, DENOM_A), Coin::new(200, DENOM_B)], }; Ok(Response::default().add_message(msg)) } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query_astro_pool(_deps: Deps, _env: Env, msg: astroport::pair::QueryMsg) -> StdResult { +pub fn query_astro_pool( + _deps: Deps, + _env: Env, + msg: astroport::pair::QueryMsg, +) -> StdResult { match msg { astroport::pair::QueryMsg::Pair {} => Ok(to_binary(&PairInfo { asset_infos: vec![], contract_addr: Addr::unchecked("contract0"), liquidity_token: Addr::unchecked("contract0"), - pair_type: astroport::factory::PairType::Xyk { }, + pair_type: astroport::factory::PairType::Xyk {}, })?), - astroport::pair::QueryMsg::Share { amount } => Ok(to_binary(&vec![ - Asset { info: astroport::asset::AssetInfo::NativeToken { denom: DENOM_A.to_string() }, amount: Uint128::new(200) }, - Asset { info: astroport::asset::AssetInfo::NativeToken { denom: DENOM_B.to_string() }, amount: Uint128::new(200) }, - + astroport::pair::QueryMsg::Share { amount: _ } => Ok(to_binary(&vec![ + Asset { + info: astroport::asset::AssetInfo::NativeToken { + denom: DENOM_A.to_string(), + }, + amount: Uint128::new(200), + }, + Asset { + info: astroport::asset::AssetInfo::NativeToken { + denom: DENOM_B.to_string(), + }, + amount: Uint128::new(200), + }, ])?), _ => Ok(to_binary(&"-")?), } @@ -103,8 +114,8 @@ pub fn query_astro_pool(_deps: Deps, _env: Env, msg: astroport::pair::QueryMsg) #[cfg_attr(not(feature = "library"), entry_point)] pub fn query_astro_lp_token(_deps: Deps, _env: Env, msg: cw20::Cw20QueryMsg) -> StdResult { match msg { - Cw20QueryMsg::Balance { address } => Ok(to_binary(&BalanceResponse { - balance: Uint128::new(100) + Cw20QueryMsg::Balance { address: _ } => Ok(to_binary(&BalanceResponse { + balance: Uint128::new(100), })?), _ => Ok(to_binary(&"-")?), } diff --git a/contracts/two-party-pol-holder/src/suite_tests/suite.rs b/contracts/two-party-pol-holder/src/suite_tests/suite.rs index 5c59fff6..52129898 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/suite.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/suite.rs @@ -1,9 +1,15 @@ -use crate::msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, RagequitConfig, TwoPartyPolCovenantConfig, TwoPartyPolCovenantParty}; -use cosmwasm_std::{Addr, Coin, Uint128, BlockInfo, Timestamp, Decimal}; +use crate::msg::{ + ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, RagequitConfig, TwoPartyPolCovenantConfig, + TwoPartyPolCovenantParty, +}; +use cosmwasm_std::{Addr, BlockInfo, Coin, Decimal, Timestamp, Uint128}; use covenant_utils::ExpiryConfig; use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; -use super::{mock_deposit_contract, two_party_pol_holder_contract, mock_astro_pool_contract, mock_astro_lp_token_contract}; +use super::{ + mock_astro_lp_token_contract, mock_astro_pool_contract, mock_deposit_contract, + two_party_pol_holder_contract, +}; pub const ADMIN: &str = "admin"; @@ -99,42 +105,43 @@ impl SuiteBuilder { let mock_deposit_code = app.store_code(mock_deposit_contract()); let astro_pool_mock_code = app.store_code(mock_astro_pool_contract()); let astro_lp_token_mock_code = app.store_code(mock_astro_lp_token_contract()); - let astro_lp = app.instantiate_contract( - astro_lp_token_mock_code, - Addr::unchecked(ADMIN), - &self.instantiate, - &[], - "astro_mock_lp_code", - Some(ADMIN.to_string()), - ) - .unwrap(); - + let astro_lp = app + .instantiate_contract( + astro_lp_token_mock_code, + Addr::unchecked(ADMIN), + &self.instantiate, + &[], + "astro_mock_lp_code", + Some(ADMIN.to_string()), + ) + .unwrap(); + let denom_b = Coin { denom: DENOM_B.to_string(), amount: Uint128::new(500), - }; + }; let denom_a = Coin { denom: DENOM_A.to_string(), amount: Uint128::new(500), }; - app - .sudo(SudoMsg::Bank(cw_multi_test::BankSudo::Mint { - to_address: astro_lp.to_string(), - amount: vec![denom_a, denom_b], - })) - .unwrap(); + app.sudo(SudoMsg::Bank(cw_multi_test::BankSudo::Mint { + to_address: astro_lp.to_string(), + amount: vec![denom_a, denom_b], + })) + .unwrap(); println!("lp token: {:?}", astro_lp); - let astro_mock = app.instantiate_contract( - astro_pool_mock_code, - Addr::unchecked(ADMIN), - &self.instantiate, - &[], - "astro_mock", - Some(ADMIN.to_string()), - ) - .unwrap(); + let astro_mock = app + .instantiate_contract( + astro_pool_mock_code, + Addr::unchecked(ADMIN), + &self.instantiate, + &[], + "astro_mock", + Some(ADMIN.to_string()), + ) + .unwrap(); self.instantiate.pool_address = astro_mock.to_string(); @@ -311,14 +318,12 @@ impl Suite { amount, } } - - } pub fn get_default_block_info() -> BlockInfo { BlockInfo { height: 12345, time: Timestamp::from_nanos(1571797419879305533), - chain_id: "cosmos-testnet-14002".to_string(), + chain_id: "cosmos-testnet-14002".to_string(), } -} \ No newline at end of file +} diff --git a/contracts/two-party-pol-holder/src/suite_tests/tests.rs b/contracts/two-party-pol-holder/src/suite_tests/tests.rs index 8cec5d39..4416ca0e 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/tests.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/tests.rs @@ -1,9 +1,16 @@ -use cosmwasm_std::{Timestamp, Uint128, Decimal}; +use cosmwasm_std::{Decimal, Timestamp, Uint128}; use covenant_utils::ExpiryConfig; -use crate::{suite_tests::suite::{CLOCK_ADDR, POOL, NEXT_CONTRACT, PARTY_A_ROUTER, PARTY_B_ROUTER, get_default_block_info, PARTY_B_ADDR}, msg::{ContractState, RagequitConfig, RagequitTerms}, error::ContractError}; +use crate::{ + error::ContractError, + msg::{ContractState, RagequitConfig, RagequitTerms}, + suite_tests::suite::{ + get_default_block_info, CLOCK_ADDR, NEXT_CONTRACT, PARTY_A_ROUTER, PARTY_B_ADDR, + PARTY_B_ROUTER, POOL, + }, +}; -use super::suite::{SuiteBuilder, PARTY_A_ADDR, INITIAL_BLOCK_NANOS, INITIAL_BLOCK_HEIGHT}; +use super::suite::{SuiteBuilder, INITIAL_BLOCK_HEIGHT, INITIAL_BLOCK_NANOS, PARTY_A_ADDR}; #[test] fn test_instantiate_happy_and_query_all() { @@ -21,8 +28,8 @@ fn test_instantiate_happy_and_query_all() { assert_eq!(CLOCK_ADDR, clock); assert_eq!(POOL, pool); assert_eq!(NEXT_CONTRACT, next_contract.to_string()); - assert_eq!(PARTY_A_ROUTER, config_party_a.router.to_string()); - assert_eq!(PARTY_B_ROUTER, config_party_b.router.to_string()); + assert_eq!(PARTY_A_ROUTER, config_party_a.router); + assert_eq!(PARTY_B_ROUTER, config_party_b.router); assert_eq!(ExpiryConfig::None, deposit_deadline); assert_eq!(ExpiryConfig::None, lockup_config); } @@ -32,7 +39,8 @@ fn test_instantiate_happy_and_query_all() { fn test_invalid_ragequit_penalty() { SuiteBuilder::default() .with_ragequit_config(RagequitConfig::Enabled(RagequitTerms { - penalty: Decimal::one(), state: None, + penalty: Decimal::one(), + state: None, })) .build(); } @@ -42,7 +50,8 @@ fn test_invalid_ragequit_penalty() { fn test_ragequit_penalty_exceeds_either_party_allocation() { SuiteBuilder::default() .with_ragequit_config(RagequitConfig::Enabled(RagequitTerms { - penalty: Decimal::percent(51), state: None, + penalty: Decimal::percent(51), + state: None, })) .build(); } @@ -75,7 +84,9 @@ fn test_instantiate_invalid_deposit_deadline_time_based() { #[should_panic(expected = "invalid expiry config: block time must be in the future")] fn test_instantiate_invalid_lockup_config_time_based() { SuiteBuilder::default() - .with_lockup_config(ExpiryConfig::Time(Timestamp::from_nanos(INITIAL_BLOCK_NANOS - 1))) + .with_lockup_config(ExpiryConfig::Time(Timestamp::from_nanos( + INITIAL_BLOCK_NANOS - 1, + ))) .build(); } @@ -92,7 +103,7 @@ fn test_single_party_deposit_refund_block_based() { let mut suite = SuiteBuilder::default() .with_deposit_deadline(ExpiryConfig::Block(12545)) .build(); - + // party A fulfills their part of covenant but B fails to let coin = suite.get_party_a_coin(Uint128::new(500)); suite.fund_coin(coin); @@ -103,8 +114,7 @@ fn test_single_party_deposit_refund_block_based() { suite.tick(CLOCK_ADDR).unwrap(); let holder_balance = suite.get_denom_a_balance(suite.holder.to_string()); - let router_a_balance = suite.get_denom_a_balance( - suite.query_party_a().router.to_string()); + let router_a_balance = suite.get_denom_a_balance(suite.query_party_a().router); let holder_state = suite.query_contract_state(); assert_eq!(ContractState::Complete, holder_state); @@ -118,7 +128,7 @@ fn test_single_party_deposit_refund_time_based() { let mut suite = SuiteBuilder::default() .with_deposit_deadline(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) .build(); - + // party A fulfills their part of covenant but B fails to let coin = suite.get_party_a_coin(Uint128::new(500)); suite.fund_coin(coin); @@ -129,8 +139,7 @@ fn test_single_party_deposit_refund_time_based() { suite.tick(CLOCK_ADDR).unwrap(); let holder_balance = suite.get_denom_a_balance(suite.holder.to_string()); - let router_a_balance = suite.get_denom_a_balance( - suite.query_party_a().router.to_string()); + let router_a_balance = suite.get_denom_a_balance(suite.query_party_a().router); let holder_state = suite.query_contract_state(); assert_eq!(ContractState::Complete, holder_state); @@ -138,11 +147,10 @@ fn test_single_party_deposit_refund_time_based() { assert_eq!(Uint128::new(500), router_a_balance); } - #[test] fn test_single_party_deposit_refund_no_deposit_deadline() { let mut suite = SuiteBuilder::default().build(); - + // party A fulfills their part of covenant but B fails to let coin = suite.get_party_a_coin(Uint128::new(500)); suite.fund_coin(coin); @@ -173,7 +181,7 @@ fn test_holder_active_not_expired_ticks() { let mut suite = SuiteBuilder::default() .with_deposit_deadline(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) .build(); - + // both parties fulfill their parts of the covenant let coin_a = suite.get_party_a_coin(Uint128::new(500)); let coin_b = suite.get_party_b_coin(Uint128::new(500)); @@ -186,10 +194,11 @@ fn test_holder_active_not_expired_ticks() { // time passes, clock ticks.. suite.pass_minutes(50); let resp = suite.tick(CLOCK_ADDR).unwrap(); - - let has_not_due_attribute = resp.events.into_iter() - .flat_map(|e| e.attributes) + + let has_not_due_attribute = resp + .events .into_iter() + .flat_map(|e| e.attributes) .any(|attr| attr.value == "not_due"); let holder_balance_a = suite.get_denom_a_balance(suite.holder.to_string()); let holder_balance_b = suite.get_denom_b_balance(suite.holder.to_string()); @@ -211,7 +220,7 @@ fn test_holder_active_expired_tick_advances_state() { let mut suite = SuiteBuilder::default() .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) .build(); - + // both parties fulfill their parts of the covenant let coin_a = suite.get_party_a_coin(Uint128::new(500)); let coin_b = suite.get_party_b_coin(Uint128::new(500)); @@ -224,7 +233,7 @@ fn test_holder_active_expired_tick_advances_state() { // time passes, clock ticks.. suite.pass_minutes(250); suite.tick(CLOCK_ADDR).unwrap(); - + let holder_balance_a = suite.get_denom_a_balance(suite.holder.to_string()); let holder_balance_b = suite.get_denom_b_balance(suite.holder.to_string()); let splitter_balance_a = suite.get_denom_a_balance(suite.mock_deposit.to_string()); @@ -243,7 +252,7 @@ fn test_holder_ragequit_disabled() { let mut suite = SuiteBuilder::default() .with_ragequit_config(RagequitConfig::Disabled) .build(); - + // both parties fulfill their parts of the covenant let coin_a = suite.get_party_a_coin(Uint128::new(500)); let coin_b = suite.get_party_b_coin(Uint128::new(500)); @@ -273,7 +282,7 @@ fn test_holder_ragequit_unauthorized() { state: None, })) .build(); - + // both parties fulfill their parts of the covenant let coin_a = suite.get_party_a_coin(Uint128::new(500)); let coin_b = suite.get_party_b_coin(Uint128::new(500)); @@ -301,7 +310,7 @@ fn test_holder_ragequit_not_in_active_state() { let mut suite = SuiteBuilder::default() .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) .build(); - + // both parties fulfill their parts of the covenant let coin_a = suite.get_party_a_coin(Uint128::new(500)); let coin_b = suite.get_party_b_coin(Uint128::new(500)); @@ -327,10 +336,13 @@ fn test_holder_ragequit_not_in_active_state() { fn test_holder_ragequit_active_but_expired() { let current_timestamp = get_default_block_info(); let mut suite = SuiteBuilder::default() - .with_ragequit_config(RagequitConfig::Enabled(RagequitTerms { penalty: Decimal::bps(10), state: None })) + .with_ragequit_config(RagequitConfig::Enabled(RagequitTerms { + penalty: Decimal::bps(10), + state: None, + })) .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) .build(); - + // both parties fulfill their parts of the covenant let coin_a = suite.get_party_a_coin(Uint128::new(500)); let coin_b = suite.get_party_b_coin(Uint128::new(500)); @@ -358,7 +370,7 @@ fn test_ragequit_double_claim_fails() { })) .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) .build(); - + // both parties fulfill their parts of the covenant let coin_a = suite.get_party_a_coin(Uint128::new(500)); let coin_b = suite.get_party_b_coin(Uint128::new(500)); @@ -386,7 +398,6 @@ fn test_ragequit_double_claim_fails() { suite.rq(PARTY_A_ADDR).unwrap(); } - #[test] fn test_ragequit_happy_flow_to_completion() { let current_timestamp = get_default_block_info(); @@ -397,7 +408,7 @@ fn test_ragequit_happy_flow_to_completion() { })) .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) .build(); - + // both parties fulfill their parts of the covenant let coin_a = suite.get_party_a_coin(Uint128::new(500)); let coin_b = suite.get_party_b_coin(Uint128::new(500)); @@ -436,14 +447,13 @@ fn test_ragequit_happy_flow_to_completion() { assert_eq!(ContractState::Complete {}, state); } - #[test] fn test_expiry_happy_flow_to_completion() { let current_timestamp = get_default_block_info(); let mut suite = SuiteBuilder::default() .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) .build(); - + // both parties fulfill their parts of the covenant let coin_a = suite.get_party_a_coin(Uint128::new(500)); let coin_b = suite.get_party_b_coin(Uint128::new(500)); @@ -458,18 +468,42 @@ fn test_expiry_happy_flow_to_completion() { suite.tick(CLOCK_ADDR).unwrap(); assert_eq!(ContractState::Expired {}, suite.query_contract_state()); - assert_eq!(Uint128::new(0), suite.get_denom_a_balance(PARTY_A_ROUTER.to_string())); - assert_eq!(Uint128::new(0), suite.get_denom_b_balance(PARTY_A_ROUTER.to_string())); - assert_eq!(Uint128::new(0), suite.get_denom_a_balance(PARTY_B_ROUTER.to_string())); - assert_eq!(Uint128::new(0), suite.get_denom_b_balance(PARTY_B_ROUTER.to_string())); + assert_eq!( + Uint128::new(0), + suite.get_denom_a_balance(PARTY_A_ROUTER.to_string()) + ); + assert_eq!( + Uint128::new(0), + suite.get_denom_b_balance(PARTY_A_ROUTER.to_string()) + ); + assert_eq!( + Uint128::new(0), + suite.get_denom_a_balance(PARTY_B_ROUTER.to_string()) + ); + assert_eq!( + Uint128::new(0), + suite.get_denom_b_balance(PARTY_B_ROUTER.to_string()) + ); // party B claims suite.claim(PARTY_B_ADDR).unwrap(); - assert_eq!(Uint128::new(0), suite.get_denom_a_balance(PARTY_A_ROUTER.to_string())); - assert_eq!(Uint128::new(0), suite.get_denom_b_balance(PARTY_A_ROUTER.to_string())); - assert_eq!(Uint128::new(200), suite.get_denom_a_balance(PARTY_B_ROUTER.to_string())); - assert_eq!(Uint128::new(200), suite.get_denom_b_balance(PARTY_B_ROUTER.to_string())); + assert_eq!( + Uint128::new(0), + suite.get_denom_a_balance(PARTY_A_ROUTER.to_string()) + ); + assert_eq!( + Uint128::new(0), + suite.get_denom_b_balance(PARTY_A_ROUTER.to_string()) + ); + assert_eq!( + Uint128::new(200), + suite.get_denom_a_balance(PARTY_B_ROUTER.to_string()) + ); + assert_eq!( + Uint128::new(200), + suite.get_denom_b_balance(PARTY_B_ROUTER.to_string()) + ); suite.pass_minutes(5); @@ -480,10 +514,21 @@ fn test_expiry_happy_flow_to_completion() { let config = suite.query_covenant_config(); assert_eq!(Decimal::zero(), config.party_b.allocation); assert_eq!(Decimal::zero(), config.party_a.allocation); - assert_eq!(Uint128::new(200), suite.get_denom_a_balance(PARTY_A_ROUTER.to_string())); - assert_eq!(Uint128::new(200), suite.get_denom_b_balance(PARTY_A_ROUTER.to_string())); - assert_eq!(Uint128::new(200), suite.get_denom_a_balance(PARTY_B_ROUTER.to_string())); - assert_eq!(Uint128::new(200), suite.get_denom_b_balance(PARTY_B_ROUTER.to_string())); + assert_eq!( + Uint128::new(200), + suite.get_denom_a_balance(PARTY_A_ROUTER.to_string()) + ); + assert_eq!( + Uint128::new(200), + suite.get_denom_b_balance(PARTY_A_ROUTER.to_string()) + ); + assert_eq!( + Uint128::new(200), + suite.get_denom_a_balance(PARTY_B_ROUTER.to_string()) + ); + assert_eq!( + Uint128::new(200), + suite.get_denom_b_balance(PARTY_B_ROUTER.to_string()) + ); assert_eq!(ContractState::Complete {}, suite.query_contract_state()); } - diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index a070d062..f28fb59a 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -367,7 +367,7 @@ impl CovenantTerms { Attribute::new("party_b_amount", terms.party_b_amount), ]; attrs - }, + } } } } From 3b041e89a5c69e84944ca1f683b3e22c3b33be7b Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 9 Oct 2023 23:17:03 +0200 Subject: [PATCH 112/586] rebase fixes --- Cargo.lock | 1 - Cargo.toml | 2 +- .../src/suite_tests/tests.rs | 44 +++++++++++++------ contracts/interchain-splitter/README.md | 3 -- .../src/suite_test/tests.rs | 2 +- contracts/swap-covenant/Cargo.toml | 1 - .../swap-holder/src/suite_tests/tests.rs | 8 ++-- .../tests/interchaintest/tokenswap_test.go | 11 ----- 8 files changed, 37 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 232bf3b0..36cf2f47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -583,7 +583,6 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "covenant-clock", - "covenant-depositor", "covenant-holder", "covenant-ibc-forwarder", "covenant-interchain-router", diff --git a/Cargo.toml b/Cargo.toml index fa58069d..9f028d9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ panic = 'abort' rpath = false [workspace.dependencies] -covenant-depositor = { path = "contracts/depositor" } +# covenant-depositor = { path = "contracts/depositor" } covenant-lp = { path = "contracts/lper" } covenant-clock = { path = "contracts/clock" } covenant-clock-tester = { path = "contracts/clock-tester" } diff --git a/contracts/interchain-router/src/suite_tests/tests.rs b/contracts/interchain-router/src/suite_tests/tests.rs index ecb229b5..60bde759 100644 --- a/contracts/interchain-router/src/suite_tests/tests.rs +++ b/contracts/interchain-router/src/suite_tests/tests.rs @@ -3,9 +3,10 @@ use std::marker::PhantomData; use cosmwasm_std::{ coins, testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, - Attribute, Coin, CosmosMsg, Empty, IbcMsg, IbcTimeout, OwnedDeps, SubMsg, Uint64, + Attribute, CosmosMsg, Empty, OwnedDeps, SubMsg, Uint64, Uint128, coin, }; use covenant_utils::DestinationConfig; +use neutron_sdk::{bindings::msg::{NeutronMsg, IbcFee}, sudo::msg::RequestPacketTimeoutHeight}; use crate::{ contract::{execute, instantiate}, @@ -63,7 +64,7 @@ fn test_migrate_config() { } #[test] -#[should_panic(expected = "Unauthorized")] +#[should_panic(expected = "Caller is not the clock, only clock can tick contracts")] fn test_unauthorized_tick() { let mut suite = SuiteBuilder::default().build(); suite.tick("not_the_clock"); @@ -101,19 +102,36 @@ fn test_tick() { ) .unwrap(); let mock_env = mock_env(); + let msg_exp = CosmosMsg::Custom(NeutronMsg::IbcTransfer { + source_port: "transfer".to_string(), + source_channel: "channel-1".to_string(), + token: coin(100, "usdc"), + sender: "cosmos2contract".to_string(), + receiver: "receiver".to_string(), + timeout_height: RequestPacketTimeoutHeight { + revision_number: None, + revision_height: None, + }, + timeout_timestamp: mock_env.block.time + .plus_seconds(Uint64::new(10).u64()) + .nanos(), + memo: "hi".to_string(), + fee: IbcFee { + // must be empty + recv_fee: vec![], + ack_fee: vec![cosmwasm_std::Coin { + denom: "untrn".to_string(), + amount: Uint128::new(1000), + }], + timeout_fee: vec![cosmwasm_std::Coin { + denom: "untrn".to_string(), + amount: Uint128::new(1000), + }], + }, + }); let expected_messages = vec![SubMsg { id: 0, - msg: CosmosMsg::Ibc(IbcMsg::Transfer { - amount: Coin::new(100, "usdc"), - channel_id: DEFAULT_CHANNEL.to_string(), - to_address: DEFAULT_RECEIVER.to_string(), - timeout: IbcTimeout::with_timestamp( - mock_env - .block - .time - .plus_seconds(init_msg.ibc_transfer_timeout.u64()), - ), - }), + msg: msg_exp, gas_limit: None, reply_on: cosmwasm_std::ReplyOn::Never, }]; diff --git a/contracts/interchain-splitter/README.md b/contracts/interchain-splitter/README.md index 61ac0b46..c2d253ff 100644 --- a/contracts/interchain-splitter/README.md +++ b/contracts/interchain-splitter/README.md @@ -4,7 +4,6 @@ Interchain Splitter is a contract meant to facilitate a pre-agreed upon way of d Splitter should remain agnostic to any price changes that may occur during the covenant lifecycle. It should accept the tokens and distribute them according to the initial agreement. -<<<<<<< HEAD ## Split Configurations @@ -27,5 +26,3 @@ Custom split configuration should always add up to 100 or else an error is retur For cases where denoms don't really matter, a wildcard split can be provided. Then any denoms that the splitter holds that do not fall under any of other configurations will be split according to this. -======= ->>>>>>> e2dab3f (init interchain splitter) diff --git a/contracts/interchain-splitter/src/suite_test/tests.rs b/contracts/interchain-splitter/src/suite_test/tests.rs index d91e6854..ca833383 100644 --- a/contracts/interchain-splitter/src/suite_test/tests.rs +++ b/contracts/interchain-splitter/src/suite_test/tests.rs @@ -46,7 +46,7 @@ fn test_instantiate_split_misconfig() { }, Receiver { addr: PARTY_B_ADDR.to_string(), - share: Uint128::new(50), + share: Uint128::new(49), }, ], }), diff --git a/contracts/swap-covenant/Cargo.toml b/contracts/swap-covenant/Cargo.toml index 83fd5c92..9fd4b323 100644 --- a/contracts/swap-covenant/Cargo.toml +++ b/contracts/swap-covenant/Cargo.toml @@ -42,7 +42,6 @@ prost = { workspace = true } prost-types = { workspace = true } bech32 ={ workspace = true } covenant-ls = { workspace = true, features=["library"] } -covenant-depositor = { workspace = true, features=["library"] } covenant-lp = { workspace = true, features=["library"] } covenant-clock = { workspace = true, features=["library"]} covenant-holder = { workspace = true, features=["library"] } diff --git a/contracts/swap-holder/src/suite_tests/tests.rs b/contracts/swap-holder/src/suite_tests/tests.rs index b88587a0..3d7a7ee5 100644 --- a/contracts/swap-holder/src/suite_tests/tests.rs +++ b/contracts/swap-holder/src/suite_tests/tests.rs @@ -15,7 +15,7 @@ use crate::{ use super::suite::SuiteBuilder; -#[test] +#[test] fn test_instantiate_happy_and_query_all() { let suite = SuiteBuilder::default().build(); let next_contract = suite.query_next_contract(); @@ -52,7 +52,7 @@ fn test_instantiate_happy_and_query_all() { } #[test] -#[should_panic(expected = "invalid lockup config: block height must be in the future")] +#[should_panic(expected = "invalid expiry config: block height must be in the future")] fn test_instantiate_past_lockup_block_height() { SuiteBuilder::default() .with_lockup_config(ExpiryConfig::Block(1)) @@ -60,7 +60,7 @@ fn test_instantiate_past_lockup_block_height() { } #[test] -#[should_panic(expected = "invalid lockup config: block time must be in the future")] +#[should_panic(expected = "invalid expiry config: block time must be in the future")] fn test_instantiate_past_lockup_block_time() { SuiteBuilder::default() .with_lockup_config(ExpiryConfig::Time(Timestamp::from_seconds(1))) @@ -317,4 +317,4 @@ fn test_refund_both_parties() { assert_eq!(Uint128::new(300), party_a_bal.amount); assert_eq!(Uint128::new(10), party_b_bal.amount); -} +} \ No newline at end of file diff --git a/swap-covenant/tests/interchaintest/tokenswap_test.go b/swap-covenant/tests/interchaintest/tokenswap_test.go index b2927af7..ceb0d321 100644 --- a/swap-covenant/tests/interchaintest/tokenswap_test.go +++ b/swap-covenant/tests/interchaintest/tokenswap_test.go @@ -140,11 +140,7 @@ func TestTokenSwap(t *testing.T) { client, network := ibctest.DockerSetup(t) r := ibctest.NewBuiltinRelayerFactory( ibc.CosmosRly, -<<<<<<< HEAD zaptest.NewLogger(t, zaptest.Level(zap.InfoLevel)), -======= - zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel)), ->>>>>>> 2afc023 (wip: rework covenant instantiation structure) relayer.CustomDockerImage("ghcr.io/cosmos/relayer", "v2.3.1", rly.RlyDefaultUidGid), relayer.RelayerOptionExtraStartFlags{Flags: []string{"-p", "events", "-b", "100", "-d", "--log-format", "console"}}, ).Build(t, client, network) @@ -395,18 +391,11 @@ func TestTokenSwap(t *testing.T) { PartyBAmount: strconv.FormatUint(osmoContributionAmount, 10), } -<<<<<<< HEAD // timestamp := Timestamp("1981539923") block := Block(500) lockupConfig := LockupConfig{ BlockHeight: &block, -======= - timestamp := Timestamp("1981539923") - - lockupConfig := LockupConfig{ - Time: ×tamp, ->>>>>>> 2afc023 (wip: rework covenant instantiation structure) } presetIbcFee := PresetIbcFee{ AckFee: "10000", From 663004359b5f7c98f7efa954d824338215a306a0 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 10 Oct 2023 14:31:16 +0200 Subject: [PATCH 113/586] preset two party pol holder fields --- contracts/two-party-pol-holder/src/msg.rs | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index ba97076c..fe38391d 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -32,6 +32,33 @@ impl InstantiateMsg { } } +#[cw_serde] +pub struct PresetTwoPartyPolHolderFields { + pub pool_address: String, + pub lockup_config: ExpiryConfig, + pub ragequit_config: RagequitConfig, + pub deposit_deadline: Option, + pub covenant_config: TwoPartyPolCovenantConfig, +} + +impl PresetTwoPartyPolHolderFields { + pub fn to_instantiate_msg( + self, + clock_address: String, + next_contract: String, + ) -> InstantiateMsg { + InstantiateMsg { + clock_address, + pool_address: self.pool_address, + next_contract, + lockup_config: self.lockup_config, + ragequit_config: self.ragequit_config, + deposit_deadline: self.deposit_deadline, + covenant_config: self.covenant_config, + } + } +} + #[cw_serde] pub struct TwoPartyPolCovenantConfig { pub party_a: TwoPartyPolCovenantParty, From d1cbbec89e0525ac8d5e1b402625717e9acfce2b Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 10 Oct 2023 15:48:49 +0200 Subject: [PATCH 114/586] two party pol covenant init --- Cargo.lock | 32 +++++ Cargo.toml | 1 + .../two-party-pol-covenant/.cargo/config | 3 + contracts/two-party-pol-covenant/Cargo.toml | 55 ++++++++ contracts/two-party-pol-covenant/README.md | 4 + .../two-party-pol-covenant/src/contract.rs | 131 ++++++++++++++++++ contracts/two-party-pol-covenant/src/error.rs | 24 ++++ contracts/two-party-pol-covenant/src/lib.rs | 8 ++ contracts/two-party-pol-covenant/src/msg.rs | 114 +++++++++++++++ contracts/two-party-pol-covenant/src/state.rs | 19 +++ contracts/two-party-pol-holder/src/msg.rs | 28 +++- 11 files changed, 417 insertions(+), 2 deletions(-) create mode 100644 contracts/two-party-pol-covenant/.cargo/config create mode 100644 contracts/two-party-pol-covenant/Cargo.toml create mode 100644 contracts/two-party-pol-covenant/README.md create mode 100644 contracts/two-party-pol-covenant/src/contract.rs create mode 100644 contracts/two-party-pol-covenant/src/error.rs create mode 100644 contracts/two-party-pol-covenant/src/lib.rs create mode 100644 contracts/two-party-pol-covenant/src/msg.rs create mode 100644 contracts/two-party-pol-covenant/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 36cf2f47..351e7eb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -625,6 +625,38 @@ dependencies = [ "thiserror", ] +[[package]] +name = "covenant-two-party-pol" +version = "1.0.0" +dependencies = [ + "anyhow", + "astroport 2.8.0", + "base64 0.13.1", + "bech32", + "cosmos-sdk-proto 0.14.0", + "cosmwasm-schema", + "cosmwasm-std", + "covenant-clock", + "covenant-ibc-forwarder", + "covenant-interchain-router", + "covenant-lp", + "covenant-two-party-pol-holder", + "covenant-utils", + "cw-multi-test", + "cw-storage-plus 1.1.0", + "cw-utils 1.0.2", + "cw2 1.1.1", + "neutron-sdk", + "prost 0.11.9", + "prost-types", + "protobuf 3.3.0", + "schemars", + "serde", + "serde-json-wasm 0.4.1", + "sha2 0.10.8", + "thiserror", +] + [[package]] name = "covenant-two-party-pol-holder" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 9f028d9a..6535d7d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ covenant-swap-holder = { path = "contracts/swap-holder" } swap-covenant = { path = "contracts/swap-covenant" } covenant-interchain-router = { path = "contracts/interchain-router" } covenant-two-party-pol-holder = { path = "contracts/two-party-pol-holder" } +covenant-two-party-pol = { path = "contracts/two-party-pol-covenant" } # packages clock-derive = { path = "packages/clock-derive" } diff --git a/contracts/two-party-pol-covenant/.cargo/config b/contracts/two-party-pol-covenant/.cargo/config new file mode 100644 index 00000000..5f6aa466 --- /dev/null +++ b/contracts/two-party-pol-covenant/.cargo/config @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +schema = "run --bin schema" diff --git a/contracts/two-party-pol-covenant/Cargo.toml b/contracts/two-party-pol-covenant/Cargo.toml new file mode 100644 index 00000000..e99d68d0 --- /dev/null +++ b/contracts/two-party-pol-covenant/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "covenant-two-party-pol" +edition = { workspace = true } +authors = ["benskey bekauz@protonmail.com"] +description = "Two Party POL covenant" +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +exclude = [ + "contract.wasm", + "hash.txt", +] + + +[lib] +crate-type = ["cdylib", "rlib"] + + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +cw2 = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +sha2 = { workspace = true } +neutron-sdk = { workspace = true } +cosmos-sdk-proto = { workspace = true } +protobuf = { workspace = true } +schemars = { workspace = true } +serde-json-wasm = { workspace = true } +base64 = { workspace = true } +prost = { workspace = true } +prost-types = { workspace = true } +bech32 ={ workspace = true } +covenant-lp = { workspace = true, features=["library"] } +covenant-clock = { workspace = true, features=["library"]} +covenant-utils = { workspace = true } +covenant-ibc-forwarder = { workspace = true, features = ["library"] } +covenant-interchain-router = { workspace = true, features = ["library"] } +covenant-two-party-pol-holder = { workspace = true, features = ["library"] } + +[dev-dependencies] +cw-multi-test = { workspace = true } +anyhow = { workspace = true } +astroport = { workspace = true } +prost = "0.11.9" diff --git a/contracts/two-party-pol-covenant/README.md b/contracts/two-party-pol-covenant/README.md new file mode 100644 index 00000000..c8e0f548 --- /dev/null +++ b/contracts/two-party-pol-covenant/README.md @@ -0,0 +1,4 @@ +# two party POL covenant + +Contract responsible for orchestrating the flow for a two party POL. + diff --git a/contracts/two-party-pol-covenant/src/contract.rs b/contracts/two-party-pol-covenant/src/contract.rs new file mode 100644 index 00000000..0934d0a7 --- /dev/null +++ b/contracts/two-party-pol-covenant/src/contract.rs @@ -0,0 +1,131 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Decimal, Uint128, +}; + +use covenant_clock::msg::PresetClockFields; +use covenant_ibc_forwarder::msg::PresetIbcForwarderFields; +use covenant_two_party_pol_holder::msg::{PresetTwoPartyPolHolderFields, RagequitConfig, PresetPolParty}; +use cw2::set_contract_version; + +use crate::{ + error::ContractError, + msg::{InstantiateMsg, QueryMsg}, + state::{ + COVENANT_CLOCK_ADDR, + PARTY_A_IBC_FORWARDER_ADDR, PARTY_B_IBC_FORWARDER_ADDR, + PRESET_CLOCK_FIELDS, PRESET_HOLDER_FIELDS, + PRESET_PARTY_A_FORWARDER_FIELDS, PRESET_PARTY_B_FORWARDER_FIELDS, COVENANT_POL_HOLDER_ADDR, + }, +}; + +const CONTRACT_NAME: &str = "crates.io:covenant-two-party-pol"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub const CLOCK_REPLY_ID: u64 = 1u64; +pub const HOLDER_REPLY_ID: u64 = 2u64; +pub const PARTY_A_FORWARDER_REPLY_ID: u64 = 3u64; +pub const PARTY_B_FORWARDER_REPLY_ID: u64 = 4u64; +pub const LP_REPLY_ID: u64 = 5u64; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + deps.api.debug("WASMDEBUG: instantiate"); + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let preset_clock_fields = PresetClockFields { + tick_max_gas: msg.clock_tick_max_gas, + whitelist: vec![], + code_id: msg.contract_codes.clock_code, + label: format!("{}-clock", msg.label), + }; + PRESET_CLOCK_FIELDS.save(deps.storage, &preset_clock_fields)?; + + let preset_holder_fields = PresetTwoPartyPolHolderFields { + lockup_config:msg.lockup_config, + pool_address: msg.pool_address, + ragequit_config: msg.ragequit_config.unwrap_or(RagequitConfig::Disabled), + deposit_deadline: msg.deposit_deadline, + party_a: PresetPolParty { + contribution: msg.party_a_config.contribution.clone(), + addr: msg.party_a_config.addr, + allocation: Decimal::from_ratio(msg.party_a_share, Uint128::new(100)), + }, + party_b: PresetPolParty { + contribution: msg.party_b_config.contribution.clone(), + addr: msg.party_b_config.addr, + allocation: Decimal::from_ratio(msg.party_b_share, Uint128::new(100)), + }, + code_id: msg.contract_codes.holder_code, + }; + PRESET_HOLDER_FIELDS.save(deps.storage, &preset_holder_fields)?; + + + let preset_party_a_forwarder_fields = PresetIbcForwarderFields { + remote_chain_connection_id: msg.party_a_config.party_chain_connection_id, + remote_chain_channel_id: msg.party_a_config.party_to_host_chain_channel_id, + denom: msg.party_a_config.contribution.denom.to_string(), + amount: msg.party_a_config.contribution.amount, + label: format!("{}_party_a_ibc_forwarder", msg.label), + code_id: msg.contract_codes.ibc_forwarder_code, + ica_timeout: msg.timeouts.ica_timeout, + ibc_transfer_timeout: msg.timeouts.ibc_transfer_timeout, + ibc_fee: msg.preset_ibc_fee.to_ibc_fee(), + }; + let preset_party_b_forwarder_fields = PresetIbcForwarderFields { + remote_chain_connection_id: msg.party_b_config.party_chain_connection_id, + remote_chain_channel_id: msg.party_b_config.party_to_host_chain_channel_id, + denom: msg.party_b_config.contribution.denom.to_string(), + amount: msg.party_b_config.contribution.amount, + label: format!("{}_party_b_ibc_forwarder", msg.label), + code_id: msg.contract_codes.ibc_forwarder_code, + ica_timeout: msg.timeouts.ica_timeout, + ibc_transfer_timeout: msg.timeouts.ibc_transfer_timeout, + ibc_fee: msg.preset_ibc_fee.to_ibc_fee(), + }; + + PRESET_PARTY_A_FORWARDER_FIELDS.save(deps.storage, &preset_party_a_forwarder_fields)?; + PRESET_PARTY_B_FORWARDER_FIELDS.save(deps.storage, &preset_party_b_forwarder_fields)?; + + // we start the module instantiation chain with the clock + // let clock_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { + // admin: Some(env.contract.address.to_string()), + // code_id: preset_clock_fields.code_id, + // msg: to_binary(&preset_clock_fields.to_instantiate_msg())?, + // funds: vec![], + // label: preset_clock_fields.label, + // }); + + Ok(Response::default() + .add_attribute("method", "instantiate")) + // .add_submessage(SubMsg::reply_on_success( + // clock_instantiate_tx, + // CLOCK_REPLY_ID, + // ))) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::ClockAddress {} => Ok(to_binary(&COVENANT_CLOCK_ADDR.may_load(deps.storage)?)?), + QueryMsg::HolderAddress {} => Ok(to_binary( + &COVENANT_POL_HOLDER_ADDR.may_load(deps.storage)?, + )?), + QueryMsg::IbcForwarderAddress { party } => { + let resp = if party == "party_a" { + PARTY_A_IBC_FORWARDER_ADDR.may_load(deps.storage)? + } else if party == "party_b" { + PARTY_B_IBC_FORWARDER_ADDR.may_load(deps.storage)? + } else { + Some(Addr::unchecked("not found")) + }; + Ok(to_binary(&resp)?) + } + } +} diff --git a/contracts/two-party-pol-covenant/src/error.rs b/contracts/two-party-pol-covenant/src/error.rs new file mode 100644 index 00000000..177d9dc3 --- /dev/null +++ b/contracts/two-party-pol-covenant/src/error.rs @@ -0,0 +1,24 @@ +use cosmwasm_std::StdError; +use cw_utils::ParseReplyError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Unknown reply id")] + UnknownReplyId {}, + + #[error("SubMsg reply error")] + ReplyError { err: String }, + + #[error("Failed to instantiate {contract:?} contract")] + ContractInstantiationError { + contract: String, + err: ParseReplyError, + }, +} diff --git a/contracts/two-party-pol-covenant/src/lib.rs b/contracts/two-party-pol-covenant/src/lib.rs new file mode 100644 index 00000000..0faea8f4 --- /dev/null +++ b/contracts/two-party-pol-covenant/src/lib.rs @@ -0,0 +1,8 @@ +#![warn(clippy::unwrap_used, clippy::expect_used)] + +extern crate core; + +pub mod contract; +pub mod error; +pub mod msg; +pub mod state; diff --git a/contracts/two-party-pol-covenant/src/msg.rs b/contracts/two-party-pol-covenant/src/msg.rs new file mode 100644 index 00000000..723f19b3 --- /dev/null +++ b/contracts/two-party-pol-covenant/src/msg.rs @@ -0,0 +1,114 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Uint128, Uint64, Coin, Decimal}; +use covenant_two_party_pol_holder::msg::{RagequitConfig, TwoPartyPolCovenantConfig}; +use covenant_utils::{ExpiryConfig}; +use neutron_sdk::bindings::msg::IbcFee; + +const NEUTRON_DENOM: &str = "untrn"; +pub const DEFAULT_TIMEOUT: u64 = 60 * 60 * 5; // 5 hours + +#[cw_serde] +pub struct InstantiateMsg { + pub label: String, + pub timeouts: Timeouts, + pub preset_ibc_fee: PresetIbcFee, + pub contract_codes: CovenantContractCodeIds, + pub clock_tick_max_gas: Option, + pub lockup_config: ExpiryConfig, + pub party_a_config: CovenantPartyConfig, + pub party_b_config: CovenantPartyConfig, + pub pool_address: String, + pub ragequit_config: Option, + pub deposit_deadline: Option, + pub party_a_share: Uint64, + pub party_b_share: Uint64, +} + +#[cw_serde] +pub struct CovenantPartyConfig { + /// authorized address of the party + pub addr: String, + /// coin provided by the party on its native chain + pub contribution: Coin, + /// ibc denom provided by the party on neutron + pub ibc_denom: String, + /// channel id from party to host chain + pub party_to_host_chain_channel_id: String, + /// channel id from host chain to the party chain + pub host_to_party_chain_channel_id: String, + /// address of the receiver on destination chain + pub party_receiver_addr: String, + /// connection id to the party chain + pub party_chain_connection_id: String, + /// timeout in seconds + pub ibc_transfer_timeout: Uint64, +} + +#[cw_serde] +pub struct CovenantContractCodeIds { + pub ibc_forwarder_code: u64, + pub holder_code: u64, + pub clock_code: u64, + pub router_code: u64, +} + +#[cw_serde] +pub struct Timeouts { + /// ica timeout in seconds + pub ica_timeout: Uint64, + /// ibc transfer timeout in seconds + pub ibc_transfer_timeout: Uint64, +} + +impl Default for Timeouts { + fn default() -> Self { + Self { + ica_timeout: Uint64::new(DEFAULT_TIMEOUT), + ibc_transfer_timeout: Uint64::new(DEFAULT_TIMEOUT), + } + } +} + +#[cw_serde] +pub struct PresetIbcFee { + pub ack_fee: Uint128, + pub timeout_fee: Uint128, +} + +impl PresetIbcFee { + pub fn to_ibc_fee(&self) -> IbcFee { + IbcFee { + // must be empty + recv_fee: vec![], + ack_fee: vec![cosmwasm_std::Coin { + denom: NEUTRON_DENOM.to_string(), + amount: self.ack_fee, + }], + timeout_fee: vec![cosmwasm_std::Coin { + denom: NEUTRON_DENOM.to_string(), + amount: self.timeout_fee, + }], + } + } +} + +#[cw_serde] +pub enum ExecuteMsg {} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(Addr)] + ClockAddress {}, + #[returns(Addr)] + HolderAddress {}, + #[returns(Addr)] + IbcForwarderAddress { party: String }, +} + +#[cw_serde] +pub enum MigrateMsg { + MigrateContracts { + clock: Option, + }, +} diff --git a/contracts/two-party-pol-covenant/src/state.rs b/contracts/two-party-pol-covenant/src/state.rs new file mode 100644 index 00000000..b9190ca6 --- /dev/null +++ b/contracts/two-party-pol-covenant/src/state.rs @@ -0,0 +1,19 @@ +use cosmwasm_std::Addr; +use covenant_clock::msg::PresetClockFields; +use covenant_ibc_forwarder::msg::PresetIbcForwarderFields; + +use covenant_two_party_pol_holder::msg::PresetTwoPartyPolHolderFields; +use cw_storage_plus::Item; + +// fields related to the contracts known prior to their. +pub const PRESET_CLOCK_FIELDS: Item = Item::new("preset_clock_fields"); +pub const PRESET_HOLDER_FIELDS: Item = Item::new("preset_holder_fields"); +pub const PRESET_PARTY_A_FORWARDER_FIELDS: Item = + Item::new("preset_party_a_forwarder_fields"); +pub const PRESET_PARTY_B_FORWARDER_FIELDS: Item = + Item::new("preset_party_b_forwarder_fields"); + +pub const COVENANT_CLOCK_ADDR: Item = Item::new("covenant_clock_addr"); +pub const COVENANT_POL_HOLDER_ADDR: Item = Item::new("covenant_two_party_pol_holder_addr"); +pub const PARTY_A_IBC_FORWARDER_ADDR: Item = Item::new("party_a_ibc_forwarder_addr"); +pub const PARTY_B_IBC_FORWARDER_ADDR: Item = Item::new("party_b_ibc_forwarder_addr"); diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index fe38391d..c1f00f45 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -38,7 +38,16 @@ pub struct PresetTwoPartyPolHolderFields { pub lockup_config: ExpiryConfig, pub ragequit_config: RagequitConfig, pub deposit_deadline: Option, - pub covenant_config: TwoPartyPolCovenantConfig, + pub party_a: PresetPolParty, + pub party_b: PresetPolParty, + pub code_id: u64, +} + +#[cw_serde] +pub struct PresetPolParty { + pub contribution: Coin, + pub addr: String, + pub allocation: Decimal, } impl PresetTwoPartyPolHolderFields { @@ -46,6 +55,8 @@ impl PresetTwoPartyPolHolderFields { self, clock_address: String, next_contract: String, + party_a_router: String, + party_b_router: String, ) -> InstantiateMsg { InstantiateMsg { clock_address, @@ -54,7 +65,20 @@ impl PresetTwoPartyPolHolderFields { lockup_config: self.lockup_config, ragequit_config: self.ragequit_config, deposit_deadline: self.deposit_deadline, - covenant_config: self.covenant_config, + covenant_config: TwoPartyPolCovenantConfig { + party_a: TwoPartyPolCovenantParty { + contribution: self.party_a.contribution, + addr: self.party_a.addr, + allocation: self.party_a.allocation, + router: party_a_router, + }, + party_b: TwoPartyPolCovenantParty { + contribution: self.party_b.contribution, + addr: self.party_b.addr, + allocation: self.party_b.allocation, + router: party_b_router, + }, + }, } } } From 28f26876459d6e15db10db123d7e80be576643f5 Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 11 Oct 2023 15:32:08 +0200 Subject: [PATCH 115/586] storing preset router fields; instantiation chain init --- .../two-party-pol-covenant/src/contract.rs | 116 +++++++++++++++--- contracts/two-party-pol-covenant/src/state.rs | 5 + 2 files changed, 107 insertions(+), 14 deletions(-) diff --git a/contracts/two-party-pol-covenant/src/contract.rs b/contracts/two-party-pol-covenant/src/contract.rs index 0934d0a7..c9bbfcbe 100644 --- a/contracts/two-party-pol-covenant/src/contract.rs +++ b/contracts/two-party-pol-covenant/src/contract.rs @@ -1,11 +1,12 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Decimal, Uint128, + to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Decimal, Uint128, CosmosMsg, WasmMsg, SubMsg, Reply, }; use covenant_clock::msg::PresetClockFields; use covenant_ibc_forwarder::msg::PresetIbcForwarderFields; +use covenant_interchain_router::msg::PresetInterchainRouterFields; use covenant_two_party_pol_holder::msg::{PresetTwoPartyPolHolderFields, RagequitConfig, PresetPolParty}; use cw2::set_contract_version; @@ -16,7 +17,7 @@ use crate::{ COVENANT_CLOCK_ADDR, PARTY_A_IBC_FORWARDER_ADDR, PARTY_B_IBC_FORWARDER_ADDR, PRESET_CLOCK_FIELDS, PRESET_HOLDER_FIELDS, - PRESET_PARTY_A_FORWARDER_FIELDS, PRESET_PARTY_B_FORWARDER_FIELDS, COVENANT_POL_HOLDER_ADDR, + PRESET_PARTY_A_FORWARDER_FIELDS, PRESET_PARTY_B_FORWARDER_FIELDS, COVENANT_POL_HOLDER_ADDR, PRESET_PARTY_A_ROUTER_FIELDS, PRESET_PARTY_B_ROUTER_FIELDS, }, }; @@ -28,6 +29,8 @@ pub const HOLDER_REPLY_ID: u64 = 2u64; pub const PARTY_A_FORWARDER_REPLY_ID: u64 = 3u64; pub const PARTY_B_FORWARDER_REPLY_ID: u64 = 4u64; pub const LP_REPLY_ID: u64 = 5u64; +pub const PARTY_A_ROUTER_REPLY_ID: u64 = 6u64; +pub const PARTY_B_ROUTER_REPLY_ID: u64 = 7u64; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -93,21 +96,105 @@ pub fn instantiate( PRESET_PARTY_A_FORWARDER_FIELDS.save(deps.storage, &preset_party_a_forwarder_fields)?; PRESET_PARTY_B_FORWARDER_FIELDS.save(deps.storage, &preset_party_b_forwarder_fields)?; + let preset_party_a_router_fields = PresetInterchainRouterFields { + destination_chain_channel_id: msg.party_a_config.host_to_party_chain_channel_id, + destination_receiver_addr: msg.party_a_config.party_receiver_addr, + ibc_transfer_timeout: msg.party_a_config.ibc_transfer_timeout, + label: format!("{}_party_a_interchain_router", msg.label), + code_id: msg.contract_codes.router_code, + }; + let preset_party_b_router_fields = PresetInterchainRouterFields { + destination_chain_channel_id: msg.party_b_config.host_to_party_chain_channel_id, + destination_receiver_addr: msg.party_b_config.party_receiver_addr, + ibc_transfer_timeout: msg.party_b_config.ibc_transfer_timeout, + label: format!("{}_party_b_interchain_router", msg.label), + code_id: msg.contract_codes.router_code, + }; + + PRESET_PARTY_A_ROUTER_FIELDS.save(deps.storage, &preset_party_a_router_fields)?; + PRESET_PARTY_B_ROUTER_FIELDS.save(deps.storage, &preset_party_b_router_fields)?; + // we start the module instantiation chain with the clock - // let clock_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { - // admin: Some(env.contract.address.to_string()), - // code_id: preset_clock_fields.code_id, - // msg: to_binary(&preset_clock_fields.to_instantiate_msg())?, - // funds: vec![], - // label: preset_clock_fields.label, - // }); + let clock_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.to_string()), + code_id: preset_clock_fields.code_id, + msg: to_binary(&preset_clock_fields.to_instantiate_msg())?, + funds: vec![], + label: preset_clock_fields.label, + }); Ok(Response::default() - .add_attribute("method", "instantiate")) - // .add_submessage(SubMsg::reply_on_success( - // clock_instantiate_tx, - // CLOCK_REPLY_ID, - // ))) + .add_attribute("method", "instantiate") + .add_submessage(SubMsg::reply_on_success( + clock_instantiate_tx, + CLOCK_REPLY_ID, + ))) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { + match msg.id { + CLOCK_REPLY_ID => handle_clock_reply(deps, env, msg), + PARTY_A_ROUTER_REPLY_ID => handle_party_a_interchain_router_reply(deps, env, msg), + PARTY_B_ROUTER_REPLY_ID => handle_party_b_interchain_router_reply(deps, env, msg), + HOLDER_REPLY_ID => handle_holder_reply(deps, env, msg), + PARTY_A_FORWARDER_REPLY_ID => handle_party_a_ibc_forwarder_reply(deps, env, msg), + PARTY_B_FORWARDER_REPLY_ID => handle_party_b_ibc_forwarder_reply(deps, env, msg), + _ => Err(ContractError::UnknownReplyId {}), + } +} + +pub fn handle_clock_reply(deps: DepsMut, env: Env, msg: Reply) -> Result { + deps.api.debug("WASMDEBUG: clock reply"); + + Ok(Response::default()) +} + +pub fn handle_party_a_interchain_router_reply( + deps: DepsMut, + env: Env, + msg: Reply, +) -> Result { + deps.api.debug("WASMDEBUG: party A interchain router reply"); + Ok(Response::default()) + +} + +pub fn handle_party_b_interchain_router_reply( + deps: DepsMut, + env: Env, + msg: Reply, +) -> Result { + deps.api.debug("WASMDEBUG: party B interchain router reply"); + Ok(Response::default()) +} + + +pub fn handle_holder_reply( + deps: DepsMut, + env: Env, + msg: Reply, +) -> Result { + deps.api.debug("WASMDEBUG: holder reply"); + Ok(Response::default()) +} + +pub fn handle_party_a_ibc_forwarder_reply( + deps: DepsMut, + env: Env, + msg: Reply, +) -> Result { + deps.api.debug("WASMDEBUG: party A ibc forwarder reply"); + Ok(Response::default()) +} + +pub fn handle_party_b_ibc_forwarder_reply( + deps: DepsMut, + _env: Env, + msg: Reply, +) -> Result { + deps.api.debug("WASMDEBUG: party B ibc forwarder reply"); + Ok(Response::default()) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -129,3 +216,4 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { } } } + diff --git a/contracts/two-party-pol-covenant/src/state.rs b/contracts/two-party-pol-covenant/src/state.rs index b9190ca6..dbe36659 100644 --- a/contracts/two-party-pol-covenant/src/state.rs +++ b/contracts/two-party-pol-covenant/src/state.rs @@ -2,6 +2,7 @@ use cosmwasm_std::Addr; use covenant_clock::msg::PresetClockFields; use covenant_ibc_forwarder::msg::PresetIbcForwarderFields; +use covenant_interchain_router::msg::PresetInterchainRouterFields; use covenant_two_party_pol_holder::msg::PresetTwoPartyPolHolderFields; use cw_storage_plus::Item; @@ -12,6 +13,10 @@ pub const PRESET_PARTY_A_FORWARDER_FIELDS: Item = Item::new("preset_party_a_forwarder_fields"); pub const PRESET_PARTY_B_FORWARDER_FIELDS: Item = Item::new("preset_party_b_forwarder_fields"); +pub const PRESET_PARTY_A_ROUTER_FIELDS: Item = + Item::new("preset_party_a_router_fields"); +pub const PRESET_PARTY_B_ROUTER_FIELDS: Item = + Item::new("preset_party_b_router_fields"); pub const COVENANT_CLOCK_ADDR: Item = Item::new("covenant_clock_addr"); pub const COVENANT_POL_HOLDER_ADDR: Item = Item::new("covenant_two_party_pol_holder_addr"); From c28bd5e6a3bd236357bb2f1e729023ae031d32f8 Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 11 Oct 2023 22:59:16 +0200 Subject: [PATCH 116/586] interchaintests two party pol --- .../two-party-pol-covenant/src/contract.rs | 164 +- contracts/two-party-pol-covenant/src/msg.rs | 12 +- go.work | 5 +- two-party-pol-covenant/README.md | 3 + two-party-pol-covenant/justfile | 44 + two-party-pol-covenant/optimize.sh | 16 + .../tests/interchaintest/README.md | 21 + .../interchaintest/connection_helpers.go | 415 +++++ .../tests/interchaintest/genesis_helpers.go | 176 ++ .../tests/interchaintest/go.mod | 204 +++ .../tests/interchaintest/go.sum | 1490 +++++++++++++++++ .../interchaintest/two_party_pol_test.go | 504 ++++++ .../tests/interchaintest/types.go | 116 ++ 13 files changed, 3081 insertions(+), 89 deletions(-) create mode 100644 two-party-pol-covenant/README.md create mode 100644 two-party-pol-covenant/justfile create mode 100755 two-party-pol-covenant/optimize.sh create mode 100644 two-party-pol-covenant/tests/interchaintest/README.md create mode 100644 two-party-pol-covenant/tests/interchaintest/connection_helpers.go create mode 100644 two-party-pol-covenant/tests/interchaintest/genesis_helpers.go create mode 100644 two-party-pol-covenant/tests/interchaintest/go.mod create mode 100644 two-party-pol-covenant/tests/interchaintest/go.sum create mode 100644 two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go create mode 100644 two-party-pol-covenant/tests/interchaintest/types.go diff --git a/contracts/two-party-pol-covenant/src/contract.rs b/contracts/two-party-pol-covenant/src/contract.rs index c9bbfcbe..04ed3ee9 100644 --- a/contracts/two-party-pol-covenant/src/contract.rs +++ b/contracts/two-party-pol-covenant/src/contract.rs @@ -42,93 +42,93 @@ pub fn instantiate( deps.api.debug("WASMDEBUG: instantiate"); set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - let preset_clock_fields = PresetClockFields { - tick_max_gas: msg.clock_tick_max_gas, - whitelist: vec![], - code_id: msg.contract_codes.clock_code, - label: format!("{}-clock", msg.label), - }; - PRESET_CLOCK_FIELDS.save(deps.storage, &preset_clock_fields)?; - - let preset_holder_fields = PresetTwoPartyPolHolderFields { - lockup_config:msg.lockup_config, - pool_address: msg.pool_address, - ragequit_config: msg.ragequit_config.unwrap_or(RagequitConfig::Disabled), - deposit_deadline: msg.deposit_deadline, - party_a: PresetPolParty { - contribution: msg.party_a_config.contribution.clone(), - addr: msg.party_a_config.addr, - allocation: Decimal::from_ratio(msg.party_a_share, Uint128::new(100)), - }, - party_b: PresetPolParty { - contribution: msg.party_b_config.contribution.clone(), - addr: msg.party_b_config.addr, - allocation: Decimal::from_ratio(msg.party_b_share, Uint128::new(100)), - }, - code_id: msg.contract_codes.holder_code, - }; - PRESET_HOLDER_FIELDS.save(deps.storage, &preset_holder_fields)?; - - - let preset_party_a_forwarder_fields = PresetIbcForwarderFields { - remote_chain_connection_id: msg.party_a_config.party_chain_connection_id, - remote_chain_channel_id: msg.party_a_config.party_to_host_chain_channel_id, - denom: msg.party_a_config.contribution.denom.to_string(), - amount: msg.party_a_config.contribution.amount, - label: format!("{}_party_a_ibc_forwarder", msg.label), - code_id: msg.contract_codes.ibc_forwarder_code, - ica_timeout: msg.timeouts.ica_timeout, - ibc_transfer_timeout: msg.timeouts.ibc_transfer_timeout, - ibc_fee: msg.preset_ibc_fee.to_ibc_fee(), - }; - let preset_party_b_forwarder_fields = PresetIbcForwarderFields { - remote_chain_connection_id: msg.party_b_config.party_chain_connection_id, - remote_chain_channel_id: msg.party_b_config.party_to_host_chain_channel_id, - denom: msg.party_b_config.contribution.denom.to_string(), - amount: msg.party_b_config.contribution.amount, - label: format!("{}_party_b_ibc_forwarder", msg.label), - code_id: msg.contract_codes.ibc_forwarder_code, - ica_timeout: msg.timeouts.ica_timeout, - ibc_transfer_timeout: msg.timeouts.ibc_transfer_timeout, - ibc_fee: msg.preset_ibc_fee.to_ibc_fee(), - }; + // let preset_clock_fields = PresetClockFields { + // tick_max_gas: msg.clock_tick_max_gas, + // whitelist: vec![], + // code_id: msg.contract_codes.clock_code, + // label: format!("{}-clock", msg.label), + // }; + // PRESET_CLOCK_FIELDS.save(deps.storage, &preset_clock_fields)?; + + // let preset_holder_fields = PresetTwoPartyPolHolderFields { + // lockup_config:msg.lockup_config, + // pool_address: msg.pool_address, + // ragequit_config: msg.ragequit_config.unwrap_or(RagequitConfig::Disabled), + // deposit_deadline: msg.deposit_deadline, + // party_a: PresetPolParty { + // contribution: msg.party_a_config.contribution.clone(), + // addr: msg.party_a_config.addr, + // allocation: Decimal::from_ratio(msg.party_a_share, Uint128::new(100)), + // }, + // party_b: PresetPolParty { + // contribution: msg.party_b_config.contribution.clone(), + // addr: msg.party_b_config.addr, + // allocation: Decimal::from_ratio(msg.party_b_share, Uint128::new(100)), + // }, + // code_id: msg.contract_codes.holder_code, + // }; + // PRESET_HOLDER_FIELDS.save(deps.storage, &preset_holder_fields)?; + + + // let preset_party_a_forwarder_fields = PresetIbcForwarderFields { + // remote_chain_connection_id: msg.party_a_config.party_chain_connection_id, + // remote_chain_channel_id: msg.party_a_config.party_to_host_chain_channel_id, + // denom: msg.party_a_config.contribution.denom.to_string(), + // amount: msg.party_a_config.contribution.amount, + // label: format!("{}_party_a_ibc_forwarder", msg.label), + // code_id: msg.contract_codes.ibc_forwarder_code, + // ica_timeout: msg.timeouts.ica_timeout, + // ibc_transfer_timeout: msg.timeouts.ibc_transfer_timeout, + // ibc_fee: msg.preset_ibc_fee.to_ibc_fee(), + // }; + // let preset_party_b_forwarder_fields = PresetIbcForwarderFields { + // remote_chain_connection_id: msg.party_b_config.party_chain_connection_id, + // remote_chain_channel_id: msg.party_b_config.party_to_host_chain_channel_id, + // denom: msg.party_b_config.contribution.denom.to_string(), + // amount: msg.party_b_config.contribution.amount, + // label: format!("{}_party_b_ibc_forwarder", msg.label), + // code_id: msg.contract_codes.ibc_forwarder_code, + // ica_timeout: msg.timeouts.ica_timeout, + // ibc_transfer_timeout: msg.timeouts.ibc_transfer_timeout, + // ibc_fee: msg.preset_ibc_fee.to_ibc_fee(), + // }; - PRESET_PARTY_A_FORWARDER_FIELDS.save(deps.storage, &preset_party_a_forwarder_fields)?; - PRESET_PARTY_B_FORWARDER_FIELDS.save(deps.storage, &preset_party_b_forwarder_fields)?; - - let preset_party_a_router_fields = PresetInterchainRouterFields { - destination_chain_channel_id: msg.party_a_config.host_to_party_chain_channel_id, - destination_receiver_addr: msg.party_a_config.party_receiver_addr, - ibc_transfer_timeout: msg.party_a_config.ibc_transfer_timeout, - label: format!("{}_party_a_interchain_router", msg.label), - code_id: msg.contract_codes.router_code, - }; - let preset_party_b_router_fields = PresetInterchainRouterFields { - destination_chain_channel_id: msg.party_b_config.host_to_party_chain_channel_id, - destination_receiver_addr: msg.party_b_config.party_receiver_addr, - ibc_transfer_timeout: msg.party_b_config.ibc_transfer_timeout, - label: format!("{}_party_b_interchain_router", msg.label), - code_id: msg.contract_codes.router_code, - }; - - PRESET_PARTY_A_ROUTER_FIELDS.save(deps.storage, &preset_party_a_router_fields)?; - PRESET_PARTY_B_ROUTER_FIELDS.save(deps.storage, &preset_party_b_router_fields)?; + // PRESET_PARTY_A_FORWARDER_FIELDS.save(deps.storage, &preset_party_a_forwarder_fields)?; + // PRESET_PARTY_B_FORWARDER_FIELDS.save(deps.storage, &preset_party_b_forwarder_fields)?; + + // let preset_party_a_router_fields = PresetInterchainRouterFields { + // destination_chain_channel_id: msg.party_a_config.host_to_party_chain_channel_id, + // destination_receiver_addr: msg.party_a_config.party_receiver_addr, + // ibc_transfer_timeout: msg.party_a_config.ibc_transfer_timeout, + // label: format!("{}_party_a_interchain_router", msg.label), + // code_id: msg.contract_codes.router_code, + // }; + // let preset_party_b_router_fields = PresetInterchainRouterFields { + // destination_chain_channel_id: msg.party_b_config.host_to_party_chain_channel_id, + // destination_receiver_addr: msg.party_b_config.party_receiver_addr, + // ibc_transfer_timeout: msg.party_b_config.ibc_transfer_timeout, + // label: format!("{}_party_b_interchain_router", msg.label), + // code_id: msg.contract_codes.router_code, + // }; + + // PRESET_PARTY_A_ROUTER_FIELDS.save(deps.storage, &preset_party_a_router_fields)?; + // PRESET_PARTY_B_ROUTER_FIELDS.save(deps.storage, &preset_party_b_router_fields)?; // we start the module instantiation chain with the clock - let clock_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { - admin: Some(env.contract.address.to_string()), - code_id: preset_clock_fields.code_id, - msg: to_binary(&preset_clock_fields.to_instantiate_msg())?, - funds: vec![], - label: preset_clock_fields.label, - }); + // let clock_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { + // admin: Some(env.contract.address.to_string()), + // code_id: preset_clock_fields.code_id, + // msg: to_binary(&preset_clock_fields.to_instantiate_msg())?, + // funds: vec![], + // label: preset_clock_fields.label, + // }); Ok(Response::default() - .add_attribute("method", "instantiate") - .add_submessage(SubMsg::reply_on_success( - clock_instantiate_tx, - CLOCK_REPLY_ID, - ))) + .add_attribute("method", "instantiate")) + // .add_submessage(SubMsg::reply_on_success( + // clock_instantiate_tx, + // CLOCK_REPLY_ID, + // ))) } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/two-party-pol-covenant/src/msg.rs b/contracts/two-party-pol-covenant/src/msg.rs index 723f19b3..612df7b3 100644 --- a/contracts/two-party-pol-covenant/src/msg.rs +++ b/contracts/two-party-pol-covenant/src/msg.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Uint128, Uint64, Coin, Decimal}; -use covenant_two_party_pol_holder::msg::{RagequitConfig, TwoPartyPolCovenantConfig}; +use cosmwasm_std::{Addr, Uint128, Uint64, Coin}; +use covenant_two_party_pol_holder::msg::{RagequitConfig}; use covenant_utils::{ExpiryConfig}; use neutron_sdk::bindings::msg::IbcFee; @@ -15,11 +15,11 @@ pub struct InstantiateMsg { pub contract_codes: CovenantContractCodeIds, pub clock_tick_max_gas: Option, pub lockup_config: ExpiryConfig, - pub party_a_config: CovenantPartyConfig, - pub party_b_config: CovenantPartyConfig, + // pub party_a_config: CovenantPartyConfig, + // pub party_b_config: CovenantPartyConfig, pub pool_address: String, - pub ragequit_config: Option, - pub deposit_deadline: Option, + // pub ragequit_config: Option, + // pub deposit_deadline: Option, pub party_a_share: Uint64, pub party_b_share: Uint64, } diff --git a/go.work b/go.work index c8315910..7c77b88f 100644 --- a/go.work +++ b/go.work @@ -1,3 +1,6 @@ go 1.20 -use ./swap-covenant/tests/interchaintest +use ( + ./swap-covenant/tests/interchaintest + ./two-party-pol-covenant/tests/interchaintest +) diff --git a/two-party-pol-covenant/README.md b/two-party-pol-covenant/README.md new file mode 100644 index 00000000..4b366ce0 --- /dev/null +++ b/two-party-pol-covenant/README.md @@ -0,0 +1,3 @@ +# Two party POL covenant + +Trust minimized way of doing two party POL diff --git a/two-party-pol-covenant/justfile b/two-party-pol-covenant/justfile new file mode 100644 index 00000000..45d72956 --- /dev/null +++ b/two-party-pol-covenant/justfile @@ -0,0 +1,44 @@ +build: + cargo build + +gen: build gen-schema + +gen-schema: + START_DIR=$(pwd); \ + for f in ./packages/*; do \ + echo "generating schema"; \ + cd "$f"; \ + CMD="cargo run --example schema"; \ + eval ${CMD} > /dev/null; \ + rm -rf ./schema/raw; \ + cd "$START_DIR"; \ + done + +test: + cargo test + +lint: + cargo +nightly clippy --all-targets -- -D warnings && cargo +nightly fmt --all --check + +optimize: + ./optimize.sh + +simtest: optimize + if [[ $(uname -m) =~ "arm64" ]]; then \ + mv ./../artifacts/covenant_ibc_forwarder-aarch64.wasm ./../artifacts/covenant_ibc_forwarder.wasm && \ + mv ./../artifacts/covenant_interchain_router-aarch64.wasm ./../artifacts/covenant_interchain_router.wasm && \ + mv ./../artifacts/covenant_clock-aarch64.wasm ./../artifacts/covenant_clock.wasm && \ + mv ./../artifacts/covenant_two_party_pol_holder-aarch64.wasm ./../artifacts/covenant_two_party_pol_holder.wasm && \ + mv ./../artifacts/covenant_two_party_pol-aarch64.wasm ./../artifacts/covenant_two_party_pol.wasm \ + ;fi + + mkdir -p tests/interchaintest/wasms + + cp -R ./../artifacts/*.wasm tests/interchaintest/wasms + + go clean -testcache + cd tests/interchaintest/ && go test -timeout 30m -v ./... + +ictest: + go clean -testcache + cd tests/interchaintest/ && go test -timeout 20m -v ./... \ No newline at end of file diff --git a/two-party-pol-covenant/optimize.sh b/two-party-pol-covenant/optimize.sh new file mode 100755 index 00000000..0d2ed785 --- /dev/null +++ b/two-party-pol-covenant/optimize.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +cd .. +if [[ $(uname -m) =~ "arm64" ]]; then \ + docker run --rm -v "$(pwd)":/code \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/workspace-optimizer-arm64:0.12.11 + +else + docker run --rm -v "$(pwd)":/code \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + --platform linux/amd64 \ + cosmwasm/workspace-optimizer:0.12.13 +fi diff --git a/two-party-pol-covenant/tests/interchaintest/README.md b/two-party-pol-covenant/tests/interchaintest/README.md new file mode 100644 index 00000000..b5805a8a --- /dev/null +++ b/two-party-pol-covenant/tests/interchaintest/README.md @@ -0,0 +1,21 @@ +# interchaintest setup + +Prior to running the interchaintests, a modification of the stride image is needed. +We are using the [v9.2.1 tagged version](https://github.com/Stride-Labs/stride/tree/v9.2.1) image. + +In there, we alter the `utils/admins.go` as follows to allow minting tokens from our address in the tests: +```go +var Admins = map[string]bool{ +- "stride1k8c2m5cn322akk5wy8lpt87dd2f4yh9azg7jlh": true, // F5 ++ "stride1u20df3trc2c2zdhm8qvh2hdjx9ewh00sv6eyy8": true, // F5 + "stride10d07y265gmmuvt4z0w9aw880jnsr700jefnezl": true, // gov module +} +``` + +Then we use heighliner by strangelove to build a local docker image, [as described in their documentation](https://github.com/strangelove-ventures/heighliner#example-cosmos-sdk-chain-development-cycle-build-a-local-repository): +```bash +# in the stride directory +heighliner build -c stride --local +``` + +With stride image present in our local docker, we are ready to run the interchaintests. To do that, we navigate to the `stride-covenant` directory and run `just simtest`. diff --git a/two-party-pol-covenant/tests/interchaintest/connection_helpers.go b/two-party-pol-covenant/tests/interchaintest/connection_helpers.go new file mode 100644 index 00000000..f901dc5d --- /dev/null +++ b/two-party-pol-covenant/tests/interchaintest/connection_helpers.go @@ -0,0 +1,415 @@ +package ibc_test + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" + + transfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types" + "github.com/strangelove-ventures/interchaintest/v4/ibc" + "github.com/strangelove-ventures/interchaintest/v4/testreporter" + "github.com/strangelove-ventures/interchaintest/v4/testutil" + "github.com/stretchr/testify/require" +) + +type TestContext struct { + OsmoClients []*ibc.ClientOutput + GaiaClients []*ibc.ClientOutput + NeutronClients []*ibc.ClientOutput + OsmoConnections []*ibc.ConnectionOutput + GaiaConnections []*ibc.ConnectionOutput + NeutronConnections []*ibc.ConnectionOutput + NeutronTransferChannelIds map[string]string + GaiaTransferChannelIds map[string]string + OsmoTransferChannelIds map[string]string + GaiaIcsChannelIds map[string]string + NeutronIcsChannelIds map[string]string +} + +func (testCtx *TestContext) getIbcDenom(channelId string, denom string) string { + prefixedDenom := transfertypes.GetPrefixedDenom("transfer", channelId, denom) + srcDenomTrace := transfertypes.ParseDenomTrace(prefixedDenom) + return srcDenomTrace.IBCDenom() +} + +// channel trace should be an ordered list of the path denom would take, +// starting from the source chain, and ending on the destination chain. +// assumes "transfer" ports. +func (testCtx *TestContext) getMultihopIbcDenom(channelTrace []string, denom string) string { + var portChannelTrace []string + + for _, channel := range channelTrace { + portChannelTrace = append(portChannelTrace, fmt.Sprintf("%s/%s", "transfer", channel)) + } + + prefixedDenom := fmt.Sprintf("%s/%s", strings.Join(portChannelTrace, "/"), denom) + + denomTrace := transfertypes.ParseDenomTrace(prefixedDenom) + return denomTrace.IBCDenom() + +} + +func (testCtx *TestContext) getChainClients(chain string) []*ibc.ClientOutput { + switch chain { + case "neutron-2": + return testCtx.NeutronClients + case "gaia-1": + return testCtx.GaiaClients + case "osmosis-3": + return testCtx.OsmoClients + default: + return ibc.ClientOutputs{} + } +} + +func (testCtx *TestContext) setTransferChannelId(chain string, destChain string, channelId string) { + switch chain { + case "neutron-2": + testCtx.NeutronTransferChannelIds[destChain] = channelId + case "gaia-1": + testCtx.GaiaTransferChannelIds[destChain] = channelId + case "osmosis-3": + testCtx.OsmoTransferChannelIds[destChain] = channelId + default: + } +} + +func (testCtx *TestContext) setIcsChannelId(chain string, destChain string, channelId string) { + switch chain { + case "neutron-2": + testCtx.NeutronIcsChannelIds[destChain] = channelId + case "gaia-1": + testCtx.GaiaIcsChannelIds[destChain] = channelId + default: + } +} + +func (testCtx *TestContext) updateChainClients(chain string, clients []*ibc.ClientOutput) { + switch chain { + case "neutron-2": + testCtx.NeutronClients = clients + case "gaia-1": + testCtx.GaiaClients = clients + case "osmosis-3": + testCtx.OsmoClients = clients + default: + } +} + +func (testCtx *TestContext) getChainConnections(chain string) []*ibc.ConnectionOutput { + switch chain { + case "neutron-2": + return testCtx.NeutronConnections + case "gaia-1": + return testCtx.GaiaConnections + case "osmosis-3": + return testCtx.OsmoConnections + default: + println("error finding connections for chain ", chain) + return []*ibc.ConnectionOutput{} + } +} + +func (testCtx *TestContext) updateChainConnections(chain string, connections []*ibc.ConnectionOutput) { + switch chain { + case "neutron-2": + testCtx.NeutronConnections = connections + case "gaia-1": + testCtx.GaiaConnections = connections + case "osmosis-3": + testCtx.OsmoConnections = connections + default: + } +} + +func generatePath( + t *testing.T, + ctx context.Context, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + chainAId string, + chainBId string, + path string, +) { + err := r.GeneratePath(ctx, eRep, chainAId, chainBId, path) + require.NoError(t, err) +} + +func generateICSChannel( + t *testing.T, + ctx context.Context, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + icsPath string, + chainA ibc.Chain, + chainB ibc.Chain, +) { + + err := r.CreateChannel(ctx, eRep, icsPath, ibc.CreateChannelOptions{ + SourcePortName: "consumer", + DestPortName: "provider", + Order: ibc.Ordered, + Version: "1", + }) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 2, chainA, chainB) + require.NoError(t, err, "failed to wait for blocks") +} + +func createValidator( + t *testing.T, + ctx context.Context, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + chain ibc.Chain, + counterparty ibc.Chain, +) { + cmd := getCreateValidatorCmd(chain) + _, _, err := chain.Exec(ctx, cmd, nil) + require.NoError(t, err) + + // Wait a bit for the VSC packet to get relayed. + err = testutil.WaitForBlocks(ctx, 2, chain, counterparty) + require.NoError(t, err, "failed to wait for blocks") +} + +func linkPath( + t *testing.T, + ctx context.Context, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + chainA ibc.Chain, + chainB ibc.Chain, + path string, +) { + err := r.LinkPath(ctx, eRep, path, ibc.DefaultChannelOpts(), ibc.DefaultClientOpts()) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 2, chainA, chainB) + require.NoError(t, err, "failed to wait for blocks") +} + +func generateClient( + t *testing.T, + ctx context.Context, + testCtx *TestContext, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + path string, + chainA ibc.Chain, + chainB ibc.Chain, +) (string, string) { + chainAClients := testCtx.getChainClients(chainA.Config().Name) + chainBClients := testCtx.getChainClients(chainB.Config().Name) + + err := r.CreateClients(ctx, eRep, path, ibc.CreateClientOptions{TrustingPeriod: "330h"}) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 2, chainA, chainB) + require.NoError(t, err, "failed to wait for blocks") + + newChainAClients, _ := r.GetClients(ctx, eRep, chainA.Config().ChainID) + newChainBClients, _ := r.GetClients(ctx, eRep, chainB.Config().ChainID) + var newClientA, newClientB string + + aClientDiff := clientDifference(chainAClients, newChainAClients) + bClientDiff := clientDifference(chainBClients, newChainBClients) + + if len(aClientDiff) > 0 { + newClientA = aClientDiff[0] + } else { + newClientA = "" + } + + if len(bClientDiff) > 0 { + newClientB = bClientDiff[0] + } else { + newClientB = "" + } + + testCtx.updateChainClients(chainA.Config().Name, newChainAClients) + testCtx.updateChainClients(chainB.Config().Name, newChainBClients) + + return newClientA, newClientB +} + +func generateConnections( + t *testing.T, + ctx context.Context, + testCtx *TestContext, + r ibc.Relayer, + eRep *testreporter.RelayerExecReporter, + path string, + chainA ibc.Chain, + chainB ibc.Chain, +) (string, string) { + chainAConns := testCtx.getChainConnections(chainA.Config().Name) + chainBConns := testCtx.getChainConnections(chainB.Config().Name) + + err := r.CreateConnections(ctx, eRep, path) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 2, chainA, chainB) + require.NoError(t, err, "failed to wait for blocks") + + newChainAConns, _ := r.GetConnections(ctx, eRep, chainA.Config().ChainID) + newChainBConns, _ := r.GetConnections(ctx, eRep, chainB.Config().ChainID) + + newChainAConnection := connectionDifference(chainAConns, newChainAConns) + newChainBConnection := connectionDifference(chainBConns, newChainBConns) + + require.NotEqual(t, 0, len(newChainAConnection), "more than one connection generated", strings.Join(newChainAConnection, " ")) + require.NotEqual(t, 0, len(newChainBConnection), "more than one connection generated", strings.Join(newChainBConnection, " ")) + + testCtx.updateChainConnections(chainA.Config().Name, newChainAConns) + testCtx.updateChainConnections(chainB.Config().Name, newChainBConns) + + return newChainAConnection[0], newChainBConnection[0] +} + +func connectionDifference(a, b []*ibc.ConnectionOutput) (diff []string) { + m := make(map[string]bool) + + // we first mark all existing connections + for _, item := range a { + m[item.ID] = true + } + + // and append all new ones + for _, item := range b { + if _, ok := m[item.ID]; !ok { + diff = append(diff, item.ID) + } + } + return +} + +func clientDifference(a, b []*ibc.ClientOutput) (diff []string) { + m := make(map[string]bool) + + // we first mark all existing clients + for _, item := range a { + m[item.ClientID] = true + } + + // and append all new ones + for _, item := range b { + if _, ok := m[item.ClientID]; !ok { + diff = append(diff, item.ClientID) + } + } + return +} + +func printChannels(channels []ibc.ChannelOutput, chain string) { + for _, channel := range channels { + print("\n\n", chain, " channels after create channel :", channel.ChannelID, " to ", channel.Counterparty.ChannelID, "\n") + } +} + +func printConnections(connections ibc.ConnectionOutputs) { + for _, connection := range connections { + print(connection.ID, "\n") + } +} + +func channelDifference(oldChannels, newChannels []ibc.ChannelOutput) (diff []string) { + m := make(map[string]bool) + // we first mark all existing channels + for _, channel := range newChannels { + m[channel.ChannelID] = true + } + + // then find the new ones + for _, channel := range oldChannels { + if _, ok := m[channel.ChannelID]; !ok { + diff = append(diff, channel.ChannelID) + } + } + + return +} + +func getPairwiseConnectionIds( + aconns ibc.ConnectionOutputs, + bconns ibc.ConnectionOutputs, +) ([]string, []string, error) { + abconnids := make([]string, 0) + baconnids := make([]string, 0) + found := false + for _, a := range aconns { + for _, b := range bconns { + if a.ClientID == b.Counterparty.ClientId && + b.ClientID == a.Counterparty.ClientId && + a.ID == b.Counterparty.ConnectionId && + b.ID == a.Counterparty.ConnectionId { + found = true + abconnids = append(abconnids, a.ID) + baconnids = append(baconnids, b.ID) + } + } + } + if found { + return abconnids, baconnids, nil + } else { + return abconnids, baconnids, errors.New("no connection found") + } +} + +// returns transfer channel ids +func getPairwiseTransferChannelIds( + testCtx *TestContext, + achans []ibc.ChannelOutput, + bchans []ibc.ChannelOutput, + aToBConnId string, + bToAConnId string, + chainA string, + chainB string, +) (string, string) { + + for _, a := range achans { + for _, b := range bchans { + if a.ChannelID == b.Counterparty.ChannelID && + b.ChannelID == a.Counterparty.ChannelID && + a.PortID == "transfer" && + b.PortID == "transfer" && + a.Ordering == "ORDER_UNORDERED" && + b.Ordering == "ORDER_UNORDERED" && + a.ConnectionHops[0] == aToBConnId && + b.ConnectionHops[0] == bToAConnId { + testCtx.setTransferChannelId(chainA, chainB, a.ChannelID) + testCtx.setTransferChannelId(chainB, chainA, b.ChannelID) + return a.ChannelID, b.ChannelID + } + } + } + panic("failed to match pairwise transfer channels") +} + +// returns ccv channel ids +func getPairwiseCCVChannelIds( + testCtx *TestContext, + achans []ibc.ChannelOutput, + bchans []ibc.ChannelOutput, + aToBConnId string, + bToAConnId string, + chainA string, + chainB string, +) (string, string) { + for _, a := range achans { + for _, b := range bchans { + if a.ChannelID == b.Counterparty.ChannelID && + b.ChannelID == a.Counterparty.ChannelID && + a.PortID == "provider" && + b.PortID == "consumer" && + a.Ordering == "ORDER_ORDERED" && + b.Ordering == "ORDER_ORDERED" && + a.ConnectionHops[0] == aToBConnId && + b.ConnectionHops[0] == bToAConnId { + testCtx.setIcsChannelId(chainA, chainB, a.ChannelID) + testCtx.setIcsChannelId(chainB, chainA, b.ChannelID) + return a.ChannelID, b.ChannelID + } + } + } + panic("failed to match pairwise ICS channels") +} diff --git a/two-party-pol-covenant/tests/interchaintest/genesis_helpers.go b/two-party-pol-covenant/tests/interchaintest/genesis_helpers.go new file mode 100644 index 00000000..db10752a --- /dev/null +++ b/two-party-pol-covenant/tests/interchaintest/genesis_helpers.go @@ -0,0 +1,176 @@ +package ibc_test + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/icza/dyno" + "github.com/strangelove-ventures/interchaintest/v4/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v4/ibc" + "github.com/strangelove-ventures/interchaintest/v4/testreporter" +) + +// Sets custom fields for the Neutron genesis file that interchaintest isn't aware of by default. +// +// soft_opt_out_threshold - the bottom `soft_opt_out_threshold` +// percentage of validators may opt out of running a Neutron +// node [^1]. +// +// reward_denoms - the reward denominations allowed to be sent to the +// provider (atom) from the consumer (neutron) [^2]. +// +// provider_reward_denoms - the reward denominations allowed to be +// sent to the consumer by the provider [^2]. +// +// [^1]: https://docs.neutron.org/neutron/consumer-chain-launch#relevant-parameters +// [^2]: https://github.com/cosmos/interchain-security/blob/54e9852d3c89a2513cd0170a56c6eec894fc878d/proto/interchain_security/ccv/consumer/v1/consumer.proto#L61-L66 +func setupNeutronGenesis( + soft_opt_out_threshold string, + reward_denoms []string, + provider_reward_denoms []string, + allowed_messages []string) func(ibc.ChainConfig, []byte) ([]byte, error) { + return func(chainConfig ibc.ChainConfig, genbz []byte) ([]byte, error) { + g := make(map[string]interface{}) + if err := json.Unmarshal(genbz, &g); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis file: %w", err) + } + + if err := dyno.Set(g, soft_opt_out_threshold, "app_state", "ccvconsumer", "params", "soft_opt_out_threshold"); err != nil { + return nil, fmt.Errorf("failed to set soft_opt_out_threshold in genesis json: %w", err) + } + + if err := dyno.Set(g, reward_denoms, "app_state", "ccvconsumer", "params", "reward_denoms"); err != nil { + return nil, fmt.Errorf("failed to set reward_denoms in genesis json: %w", err) + } + + if err := dyno.Set(g, provider_reward_denoms, "app_state", "ccvconsumer", "params", "provider_reward_denoms"); err != nil { + return nil, fmt.Errorf("failed to set provider_reward_denoms in genesis json: %w", err) + } + + if err := dyno.Set(g, allowed_messages, "app_state", "interchainaccounts", "host_genesis_state", "params", "allow_messages"); err != nil { + return nil, fmt.Errorf("failed to set allow_messages for interchainaccount host in genesis json: %w", err) + } + + out, err := json.Marshal(g) + + if err != nil { + return nil, fmt.Errorf("failed to marshal genesis bytes to json: %w", err) + } + return out, nil + } +} + +// Sets custom fields for the Gaia genesis file that interchaintest isn't aware of by default. +// +// allowed_messages - explicitly allowed messages to be accepted by the the interchainaccounts section +func setupGaiaGenesis(allowed_messages []string) func(ibc.ChainConfig, []byte) ([]byte, error) { + return func(chainConfig ibc.ChainConfig, genbz []byte) ([]byte, error) { + g := make(map[string]interface{}) + if err := json.Unmarshal(genbz, &g); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis file: %w", err) + } + + if err := dyno.Set(g, allowed_messages, "app_state", "interchainaccounts", "host_genesis_state", "params", "allow_messages"); err != nil { + return nil, fmt.Errorf("failed to set allow_messages for interchainaccount host in genesis json: %w", err) + } + + out, err := json.Marshal(g) + if err != nil { + return nil, fmt.Errorf("failed to marshal genesis bytes to json: %w", err) + } + return out, nil + } +} + +func getDefaultInterchainGenesisMessages() []string { + return []string{ + "/cosmos.bank.v1beta1.MsgSend", + "/cosmos.bank.v1beta1.MsgMultiSend", + "/cosmos.staking.v1beta1.MsgDelegate", + "/cosmos.staking.v1beta1.MsgUndelegate", + "/cosmos.staking.v1beta1.MsgBeginRedelegate", + "/cosmos.staking.v1beta1.MsgRedeemTokensforShares", + "/cosmos.staking.v1beta1.MsgTokenizeShares", + "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", + "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", + "/ibc.applications.transfer.v1.MsgTransfer", + } +} + +func setupOsmoGenesis(allowed_messages []string) func(ibc.ChainConfig, []byte) ([]byte, error) { + return func(chainConfig ibc.ChainConfig, genbz []byte) ([]byte, error) { + g := make(map[string]interface{}) + if err := json.Unmarshal(genbz, &g); err != nil { + return nil, fmt.Errorf("failed to unmarshal genesis file: %w", err) + } + + missingFields := map[string]interface{}{ + "active_channels": []interface{}{}, + "interchain_accounts": []interface{}{}, + "port": "icahost", + "params": map[string]interface{}{ + "host_enabled": true, + "allow_messages": []interface{}{}, + }, + } + if g["app_state"].(map[string]interface{})["interchainaccounts"] == nil { + g["app_state"].(map[string]interface{})["interchainaccounts"] = make(map[string]interface{}) + } + + if g["app_state"].(map[string]interface{})["interchainaccounts"].(map[string]interface{})["host_genesis_state"] == nil { + g["app_state"].(map[string]interface{})["interchainaccounts"].(map[string]interface{})["host_genesis_state"] = make(map[string]interface{}) + } + + if err := dyno.Set(g, missingFields, "app_state", "interchainaccounts", "host_genesis_state"); err != nil { + return nil, fmt.Errorf("failed to set interchainaccounts for app_state in genesis json: %w. \ngenesis json: %s", err, g) + } + + if err := dyno.Set(g, allowed_messages, "app_state", "interchainaccounts", "host_genesis_state", "params", "allow_messages"); err != nil { + return nil, fmt.Errorf("failed to set allow_messages for interchainaccount host in genesis json: %w. \ngenesis json: %s", err, g) + } + + out, err := json.Marshal(g) + if err != nil { + return nil, fmt.Errorf("failed to marshal genesis bytes to json: %w", err) + } + return out, nil + } +} + +func getCreateValidatorCmd(chain ibc.Chain) []string { + // Before receiving a validator set change (VSC) packet, + // consumer chains disallow bank transfers. To trigger a VSC + // packet, this creates a validator (from a random public key) + // that will never do anything, triggering a VSC + // packet. Eventually this validator will become jailed, + // triggering another one. + cmd := []string{"gaiad", "tx", "staking", "create-validator", + "--amount", "1000000uatom", + "--pubkey", `{"@type":"/cosmos.crypto.ed25519.PubKey","key":"qwrYHaJ7sNHfYBR1nzDr851+wT4ed6p8BbwTeVhaHoA="}`, + "--moniker", "a", + "--commission-rate", "0.1", + "--commission-max-rate", "0.2", + "--commission-max-change-rate", "0.01", + "--min-self-delegation", "1000000", + "--node", chain.GetRPCAddress(), + "--home", chain.HomeDir(), + "--chain-id", chain.Config().ChainID, + "--from", "faucet", + "--fees", "20000uatom", + "--keyring-backend", keyring.BackendTest, + "-y", + } + + return cmd +} + +func getChannelMap(r ibc.Relayer, ctx context.Context, eRep *testreporter.RelayerExecReporter, + cosmosStride *cosmos.CosmosChain, cosmosNeutron *cosmos.CosmosChain, cosmosAtom *cosmos.CosmosChain) map[string]string { + channelMap := map[string]string{ + "hi": "Dog", + } + + return channelMap +} diff --git a/two-party-pol-covenant/tests/interchaintest/go.mod b/two-party-pol-covenant/tests/interchaintest/go.mod new file mode 100644 index 00000000..a8a48b4d --- /dev/null +++ b/two-party-pol-covenant/tests/interchaintest/go.mod @@ -0,0 +1,204 @@ +module github.com/timewave-computer/covenants/two-party-pol + +go 1.20 + +require ( + github.com/cosmos/cosmos-sdk v0.45.16 + github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0 + github.com/strangelove-ventures/interchaintest/v4 v4.0.0-20230316161044-8d8c01f96b4a + github.com/stretchr/testify v1.8.4 + go.uber.org/zap v1.24.0 +) + +require ( + cosmossdk.io/api v0.2.6 // indirect + cosmossdk.io/core v0.5.1 // indirect + cosmossdk.io/depinject v1.0.0-alpha.3 // indirect + filippo.io/edwards25519 v1.0.0-rc.1 // indirect + github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/99designs/keyring v1.2.1 // indirect + github.com/BurntSushi/toml v1.2.1 // indirect + github.com/ChainSafe/go-schnorrkel v1.0.0 // indirect + github.com/ChainSafe/go-schnorrkel/1 v0.0.0-00010101000000-000000000000 // indirect + github.com/DataDog/zstd v1.5.0 // indirect + github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect + github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/StirlingMarketingGroup/go-namecase v1.0.0 // indirect + github.com/armon/go-metrics v0.4.1 // indirect + github.com/avast/retry-go/v4 v4.0.4 // indirect + github.com/benbjohnson/clock v1.3.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect + github.com/btcsuite/btcd v0.23.0 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/centrifuge/go-substrate-rpc-client/v4 v4.0.4 // indirect + github.com/cespare/xxhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cockroachdb/errors v1.9.1 // indirect + github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/cockroachdb/pebble v0.0.0-20220817183557-09c6e030a677 // indirect + github.com/cockroachdb/redact v1.1.3 // indirect + github.com/confio/ics23/go v0.9.0 // indirect + github.com/cosmos/btcutil v1.0.4 // indirect + github.com/cosmos/cosmos-db v0.0.0-20221226095112-f3c38ecb5e32 // indirect + github.com/cosmos/cosmos-proto v1.0.0-beta.1 // indirect + github.com/cosmos/go-bip39 v1.0.0 // indirect + github.com/cosmos/gorocksdb v1.2.0 // indirect + github.com/cosmos/iavl v0.19.5 // indirect + github.com/cosmos/ibc-go/v4 v4.4.2 // indirect + github.com/cosmos/interchain-security v1.0.0 // indirect + github.com/cosmos/ledger-cosmos-go v0.12.2 // indirect + github.com/danieljoos/wincred v1.1.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deckarep/golang-set v1.8.0 // indirect + github.com/decred/base58 v1.0.3 // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/dgraph-io/badger/v2 v2.2007.4 // indirect + github.com/dgraph-io/ristretto v0.1.0 // indirect + github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect + github.com/docker/distribution v2.8.1+incompatible // indirect + github.com/docker/docker v20.10.19+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac // indirect + github.com/dvsekhvalnov/jose2go v1.5.0 // indirect + github.com/ethereum/go-ethereum v1.10.17 // indirect + github.com/felixge/httpsnoop v1.0.2 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/getsentry/sentry-go v0.17.0 // indirect + github.com/go-kit/kit v0.12.0 // indirect + github.com/go-kit/log v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.5.1 // indirect + github.com/go-stack/stack v1.8.1 // indirect + github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/gogo/gateway v1.1.0 // indirect + github.com/gogo/protobuf v1.3.3 // indirect + github.com/golang/glog v1.0.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/handlers v1.5.1 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect + github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/gtank/merlin v0.1.1 // indirect + github.com/gtank/ristretto255 v0.1.2 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/ipfs/go-cid v0.0.7 // indirect + github.com/jmhodges/levigo v1.0.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/klauspost/compress v1.15.11 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/libp2p/go-libp2p-core v0.15.1 // indirect + github.com/libp2p/go-openssl v0.0.7 // indirect + github.com/linxGnu/grocksdb v1.7.10 // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect + github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect + github.com/minio/sha256-simd v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/mtibben/percent v0.2.1 // indirect + github.com/multiformats/go-base32 v0.0.3 // indirect + github.com/multiformats/go-base36 v0.1.0 // indirect + github.com/multiformats/go-multiaddr v0.4.1 // indirect + github.com/multiformats/go-multibase v0.0.3 // indirect + github.com/multiformats/go-multicodec v0.4.1 // indirect + github.com/multiformats/go-multihash v0.1.0 // indirect + github.com/multiformats/go-varint v0.0.6 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc2 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect + github.com/pierrec/xxHash v0.1.5 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.37.0 // indirect + github.com/prometheus/procfs v0.8.0 // indirect + github.com/rakyll/statik v0.1.7 // indirect + github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/regen-network/cosmos-proto v0.3.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/rs/cors v1.8.2 // indirect + github.com/sasha-s/go-deadlock v0.3.1 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/spf13/afero v1.9.2 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cobra v1.6.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.14.0 // indirect + github.com/subosito/gotenv v1.4.1 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect + github.com/tendermint/go-amino v0.16.0 // indirect + github.com/tendermint/tendermint v0.34.27 // indirect + github.com/tendermint/tm-db v0.6.7 // indirect + github.com/tidwall/btree v1.5.0 // indirect + github.com/vedhavyas/go-subkey v1.0.3 // indirect + github.com/zondax/hid v0.9.1 // indirect + github.com/zondax/ledger-go v0.14.1 // indirect + go.etcd.io/bbolt v1.3.6 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/multierr v1.8.0 // indirect + golang.org/x/crypto v0.5.0 // indirect + golang.org/x/exp v0.0.0-20221019170559-20944726eadf // indirect + golang.org/x/mod v0.7.0 // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/term v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + golang.org/x/tools v0.4.0 // indirect + google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa // indirect + google.golang.org/grpc v1.52.3 // indirect + google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/blake3 v1.1.6 // indirect + lukechampine.com/uint128 v1.1.1 // indirect + modernc.org/cc/v3 v3.36.0 // indirect + modernc.org/ccgo/v3 v3.16.6 // indirect + modernc.org/libc v1.16.7 // indirect + modernc.org/mathutil v1.4.1 // indirect + modernc.org/memory v1.1.1 // indirect + modernc.org/opt v0.1.1 // indirect + modernc.org/sqlite v1.17.3 // indirect + modernc.org/strutil v1.1.1 // indirect + modernc.org/token v1.0.0 // indirect +) + +// replace block copied from interchaintest v3-ics branch: +// +replace ( + github.com/ChainSafe/go-schnorrkel => github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d + github.com/ChainSafe/go-schnorrkel/1 => github.com/ChainSafe/go-schnorrkel v1.0.0 + github.com/btcsuite/btcd => github.com/btcsuite/btcd v0.22.2 //indirect + github.com/cosmos/cosmos-sdk => github.com/cosmos/cosmos-sdk v0.45.16-ics + github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 + github.com/tendermint/tendermint => github.com/cometbft/cometbft v0.34.27 + // github.com/tidwall/btree => github.com/tidwall/btree v1.5.0 + github.com/vedhavyas/go-subkey => github.com/strangelove-ventures/go-subkey v1.0.7 +) diff --git a/two-party-pol-covenant/tests/interchaintest/go.sum b/two-party-pol-covenant/tests/interchaintest/go.sum new file mode 100644 index 00000000..932fcd61 --- /dev/null +++ b/two-party-pol-covenant/tests/interchaintest/go.sum @@ -0,0 +1,1490 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigtable v1.2.0/go.mod h1:JcVAOl45lrTmQfLj7T6TxyMzIN/3FGGcFm+2xVAli2o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE= +cosmossdk.io/api v0.2.6 h1:AoNwaLLapcLsphhMK6+o0kZl+D6MMUaHVqSdwinASGU= +cosmossdk.io/api v0.2.6/go.mod h1:u/d+GAxil0nWpl1XnQL8nkziQDIWuBDhv8VnDm/s6dI= +cosmossdk.io/core v0.5.1 h1:vQVtFrIYOQJDV3f7rw4pjjVqc1id4+mE0L9hHP66pyI= +cosmossdk.io/core v0.5.1/go.mod h1:KZtwHCLjcFuo0nmDc24Xy6CRNEL9Vl/MeimQ2aC7NLE= +cosmossdk.io/depinject v1.0.0-alpha.3 h1:6evFIgj//Y3w09bqOUOzEpFj5tsxBqdc5CfkO7z+zfw= +cosmossdk.io/depinject v1.0.0-alpha.3/go.mod h1:eRbcdQ7MRpIPEM5YUJh8k97nxHpYbc3sMUnEtt8HPWU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= +filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo87o= +github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= +github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0/go.mod h1:tPaiy8S5bQ+S5sOiDlINkp7+Ef339+Nz5L5XO+cnOHo= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d h1:nalkkPQcITbvhmL4+C4cKA87NW0tfm3Kl9VXRoPywFg= +github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d/go.mod h1:URdX5+vg25ts3aCh8H5IFZybJYKWhJHYMTnf+ULtoC4= +github.com/ChainSafe/go-schnorrkel v1.0.0 h1:3aDA67lAykLaG1y3AOjs88dMxC88PgUuHRrLeDnvGIM= +github.com/ChainSafe/go-schnorrkel v1.0.0/go.mod h1:dpzHYVxLZcp8pjlV+O+UR8K0Hp/z7vcchBSbMBEhCw4= +github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= +github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w= +github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/DataDog/zstd v1.5.0 h1:+K/VEwIAaPcHiMtQvpLD4lqW7f0Gk3xdYZmI1hD+CXo= +github.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= +github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/StirlingMarketingGroup/go-namecase v1.0.0 h1:2CzaNtCzc4iNHirR+5ru9OzGg8rQp860gqLBFqRI02Y= +github.com/StirlingMarketingGroup/go-namecase v1.0.0/go.mod h1:ZsoSKcafcAzuBx+sndbxHu/RjDcDTrEdT4UvhniHfio= +github.com/VictoriaMetrics/fastcache v1.6.0/go.mod h1:0qHz5QP0GMX4pfmMA/zt5RgfNuXJrTP0zS7DqpHGGTw= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/alecthomas/participle/v2 v2.0.0-alpha7 h1:cK4vjj0VSgb3lN1nuKA5F7dw+1s1pWBe5bx7nNCnN+c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/arrow/go/arrow v0.0.0-20191024131854-af6fa24be0db/go.mod h1:VTxUBvSJ3s3eHAg65PNgrsn5BtqCRPdmyXh6rAfdxN0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/avast/retry-go/v4 v4.0.4 h1:38hLf0DsRXh+hOF6HbTni0+5QGTNdw9zbaMD7KAO830= +github.com/avast/retry-go/v4 v4.0.4/go.mod h1:HqmLvS2VLdStPCGDFjSuZ9pzlTqVRldCI4w2dO4m1Ms= +github.com/aws/aws-sdk-go-v2 v1.2.0/go.mod h1:zEQs02YRBw1DjK0PoJv3ygDYOFTre1ejlJWl8FwAuQo= +github.com/aws/aws-sdk-go-v2/config v1.1.1/go.mod h1:0XsVy9lBI/BCXm+2Tuvt39YmdHwS5unDQmxZOYe8F5Y= +github.com/aws/aws-sdk-go-v2/credentials v1.1.1/go.mod h1:mM2iIjwl7LULWtS6JCACyInboHirisUUdkBPoTHMOUo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.2/go.mod h1:3hGg3PpiEjHnrkrlasTfxFqUsZ2GCk/fMUn4CbKgSkM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.2/go.mod h1:45MfaXZ0cNbeuT0KQ1XJylq8A6+OpVV2E5kvY/Kq+u8= +github.com/aws/aws-sdk-go-v2/service/route53 v1.1.1/go.mod h1:rLiOUrPLW/Er5kRcQ7NkwbjlijluLsrIbu/iyl35RO4= +github.com/aws/aws-sdk-go-v2/service/sso v1.1.1/go.mod h1:SuZJxklHxLAXgLTc1iFXbEWkXs7QRTQpCLGaKIprQW0= +github.com/aws/aws-sdk-go-v2/service/sts v1.1.1/go.mod h1:Wi0EBZwiz/K44YliU0EKxqTCJGUfYTWXrrBwkq736bM= +github.com/aws/smithy-go v1.1.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw= +github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/btcsuite/btcd v0.22.2 h1:vBZ+lGGd1XubpOWO67ITJpAEsICWhA0YzqkcpkgNBfo= +github.com/btcsuite/btcd v0.22.2/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y= +github.com/btcsuite/btcd/btcec/v2 v2.1.2/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcutil v1.1.2 h1:XLMbX8JQEiwMcYft2EGi8zPUkoa0abKIU6/BJSRsjzQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/centrifuge/go-substrate-rpc-client/v4 v4.0.4 h1:G2kCJurlIkguX0oxxI9sPPENuQqMVhIhV9RVkh/dpDg= +github.com/centrifuge/go-substrate-rpc-client/v4 v4.0.4/go.mod h1:5g1oM4Zu3BOaLpsKQ+O8PAv2kNuq+kPcA1VzFbsSqxE= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cloudflare-go v0.14.0/go.mod h1:EnwdgGMaFOruiPZRFSgn+TsQ3hQ7C/YWzIGLeu5c304= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cockroachdb/apd/v3 v3.1.0 h1:MK3Ow7LH0W8zkd5GMKA1PvS9qG3bWFI95WaVNfyZJ/w= +github.com/cockroachdb/datadriven v1.0.0/go.mod h1:5Ib8Meh+jk1RlHIXej6Pzevx/NLlNvQB9pmSBZErGA4= +github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/cockroachdb/errors v1.6.1/go.mod h1:tm6FTP5G81vwJ5lC0SizQo374JNCOPrHyXGitRJoDqM= +github.com/cockroachdb/errors v1.8.1/go.mod h1:qGwQn6JmZ+oMjuLwjWzUNqblqk0xl4CVV3SQbGwK7Ac= +github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8= +github.com/cockroachdb/errors v1.9.1/go.mod h1:2sxOtL2WIc096WSZqZ5h8fa17rdDq9HZOZLBCor4mBk= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v0.0.0-20220817183557-09c6e030a677 h1:qbb/AE938DFhOajUYh9+OXELpSF9KZw2ZivtmW6eX1Q= +github.com/cockroachdb/pebble v0.0.0-20220817183557-09c6e030a677/go.mod h1:890yq1fUb9b6dGNwssgeUO5vQV9qfXnCPxAJhBQfXw0= +github.com/cockroachdb/redact v1.0.8/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ= +github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2/go.mod h1:8BT+cPK6xvFOcRlk0R8eg+OTkcqI6baNH4xAkpiYVvQ= +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= +github.com/coinbase/rosetta-sdk-go v0.7.9 h1:lqllBjMnazTjIqYrOGv8h8jxjg9+hJazIGZr9ZvoCcA= +github.com/cometbft/cometbft v0.34.27 h1:ri6BvmwjWR0gurYjywcBqRe4bbwc3QVs9KRcCzgh/J0= +github.com/cometbft/cometbft v0.34.27/go.mod h1:BcCbhKv7ieM0KEddnYXvQZR+pZykTKReJJYf7YC7qhw= +github.com/cometbft/cometbft-db v0.7.0 h1:uBjbrBx4QzU0zOEnU8KxoDl18dMNgDh+zZRUE0ucsbo= +github.com/confio/ics23/go v0.9.0 h1:cWs+wdbS2KRPZezoaaj+qBleXgUk5WOQFMP3CQFGTr4= +github.com/confio/ics23/go v0.9.0/go.mod h1:4LPZ2NYqnYIVRklaozjNR1FScgDJ2s5Xrp+e/mYVRak= +github.com/consensys/bavard v0.1.8-0.20210406032232-f3452dc9b572/go.mod h1:Bpd0/3mZuaj6Sj+PqrmIquiOKy397AKGThQPaGzNXAQ= +github.com/consensys/gnark-crypto v0.4.1-0.20210426202927-39ac3d4b3f1f/go.mod h1:815PAHg3wvysy0SyIqanF8gZ0Y1wjk/hrDHD/iT88+Q= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cosmos/btcutil v1.0.4 h1:n7C2ngKXo7UC9gNyMNLbzqz7Asuf+7Qv4gnX/rOdQ44= +github.com/cosmos/btcutil v1.0.4/go.mod h1:Ffqc8Hn6TJUdDgHBwIZLtrLQC1KdJ9jGJl/TvgUaxbU= +github.com/cosmos/cosmos-db v0.0.0-20221226095112-f3c38ecb5e32 h1:zlCp9n3uwQieELltZWHRmwPmPaZ8+XoL2Sj+A2YJlr8= +github.com/cosmos/cosmos-db v0.0.0-20221226095112-f3c38ecb5e32/go.mod h1:kwMlEC4wWvB48zAShGKVqboJL6w4zCLesaNQ3YLU2BQ= +github.com/cosmos/cosmos-proto v1.0.0-beta.1 h1:iDL5qh++NoXxG8hSy93FdYJut4XfgbShIocllGaXx/0= +github.com/cosmos/cosmos-proto v1.0.0-beta.1/go.mod h1:8k2GNZghi5sDRFw/scPL8gMSowT1vDA+5ouxL8GjaUE= +github.com/cosmos/cosmos-sdk v0.45.16-ics h1:KsPigLNmdyyQMktAsJzW42eBFsq1uajhQF7rlnHDUgM= +github.com/cosmos/cosmos-sdk v0.45.16-ics/go.mod h1:bScuNwWAP0TZJpUf+SHXRU3xGoUPp+X9nAzfeIXts40= +github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= +github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= +github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= +github.com/cosmos/gorocksdb v1.2.0 h1:d0l3jJG8M4hBouIZq0mDUHZ+zjOx044J3nGRskwTb4Y= +github.com/cosmos/gorocksdb v1.2.0/go.mod h1:aaKvKItm514hKfNJpUJXnnOWeBnk2GL4+Qw9NHizILw= +github.com/cosmos/iavl v0.19.5 h1:rGA3hOrgNxgRM5wYcSCxgQBap7fW82WZgY78V9po/iY= +github.com/cosmos/iavl v0.19.5/go.mod h1:X9PKD3J0iFxdmgNLa7b2LYWdsGd90ToV5cAONApkEPw= +github.com/cosmos/ibc-go/v4 v4.4.2 h1:PG4Yy0/bw6Hvmha3RZbc53KYzaCwuB07Ot4GLyzcBvo= +github.com/cosmos/ibc-go/v4 v4.4.2/go.mod h1:j/kD2JCIaV5ozvJvaEkWhLxM2zva7/KTM++EtKFYcB8= +github.com/cosmos/interchain-security v1.0.0 h1:xNQjjigqH3mzEKSGQhAhKy8I0TA8XR2z5rRTxRBKK3o= +github.com/cosmos/interchain-security v1.0.0/go.mod h1:J9SbXUJT1GSe+mZy+MDCxtuAfbhwCKBEJRYnfjXsE8Q= +github.com/cosmos/ledger-cosmos-go v0.12.2 h1:/XYaBlE2BJxtvpkHiBm97gFGSGmYGKunKyF3nNqAXZA= +github.com/cosmos/ledger-cosmos-go v0.12.2/go.mod h1:ZcqYgnfNJ6lAXe4HPtWgarNEY+B74i+2/8MhZw4ziiI= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creachadair/taskgroup v0.3.2 h1:zlfutDS+5XG40AOxcHDSThxKzns8Tnr9jnr6VqkYlkM= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cucumber/common/gherkin/go/v22 v22.0.0 h1:4K8NqptbvdOrjL9DEea6HFjSpbdT9+Q5kgLpmmsHYl0= +github.com/cucumber/common/messages/go/v17 v17.1.1 h1:RNqopvIFyLWnKv0LfATh34SWBhXeoFTJnSrgm9cT/Ts= +github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= +github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= +github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= +github.com/decred/base58 v1.0.3 h1:KGZuh8d1WEMIrK0leQRM47W85KqCAdl2N+uagbctdDI= +github.com/decred/base58 v1.0.3/go.mod h1:pXP9cXCfM2sFLb2viz2FNIdeMWmZDBKG3ZBYbiSM78E= +github.com/decred/dcrd/chaincfg/chainhash v1.0.2 h1:rt5Vlq/jM3ZawwiacWjPa+smINyLRN07EO0cNBV6DGU= +github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0 h1:3GIJYXQDAKpLEFriGFN8SbSffak10UXHGdIcFaMPykY= +github.com/decred/dcrd/dcrec/secp256k1/v2 v2.0.0/go.mod h1:3s92l0paYkZoIHuj4X93Teg/HB7eGM9x/zokGw+u4mY= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= +github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= +github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= +github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= +github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-bitstream v0.0.0-20180413035011-3522498ce2c8/go.mod h1:VMaSuZ+SZcx/wljOQKvp5srsbCiKDEb6K2wC4+PiBmQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.19+incompatible h1:lzEmjivyNHFHMNAFLXORMBXyGIhw/UP4DvJwvyKYq64= +github.com/docker/docker v20.10.19+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dop251/goja v0.0.0-20211011172007-d99e4b8cbf48/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac h1:opbrjaN/L8gg6Xh5D04Tem+8xVcz6ajZlGCs49mQgyg= +github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= +github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/ethereum/go-ethereum v1.10.17 h1:XEcumY+qSr1cZQaWsQs5Kck3FHB0V2RiMHPdTBJ+oT8= +github.com/ethereum/go-ethereum v1.10.17/go.mod h1:Lt5WzjM07XlXc95YzrhosmR4J9Ahd6X2wyEV2SvGhk0= +github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= +github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= +github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= +github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= +github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= +github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= +github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c= +github.com/getsentry/sentry-go v0.17.0 h1:UustVWnOoDFHBS7IJUB2QK/nB5pap748ZEp0swnQJak= +github.com/getsentry/sentry-go v0.17.0/go.mod h1:B82dxtBvxG0KaPD8/hfSV+VcHD+Lg/xUS4JuQn1P4cM= +github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= +github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc= +github.com/gogo/gateway v1.1.0 h1:u0SuhL9+Il+UbjM9VIE3ntfRujKbvVpFvNB4HbjeVQ0= +github.com/gogo/gateway v1.1.0/go.mod h1:S7rR8FRQyG3QFESeSv4l2WnsyzlCLG0CzBbUUo/mbic= +github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= +github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa h1:Q75Upo5UN4JbPFURXZ8nLKYUvF85dyFRop/vQ0Rv+64= +github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= +github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= +github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= +github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 h1:aSVUgRRRtOrZOC1fYmY9gV0e9z/Iu+xNVSASWjsuyGU= +github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3/go.mod h1:5PC6ZNPde8bBqU/ewGZig35+UIZtw9Ytxez8/q5ZyFE= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huin/goupnp v1.0.3-0.20220313090229-ca81a64b4204/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= +github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= +github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= +github.com/hydrogen18/memlistener v0.0.0-20200120041712-dcc25e7acd91/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0 h1:nHoRIX8iXob3Y2kdt9KsjyIb7iApSvb3vgsd93xb5Ow= +github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0/go.mod h1:c1tRKs5Tx7E2+uHGSyyncziFjvGpgv4H2HrqXeUQ/Uk= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/flux v0.65.1/go.mod h1:J754/zds0vvpfwuq7Gc2wRdVwEodfpCFM7mYlOw2LqY= +github.com/influxdata/influxdb v1.8.3/go.mod h1:JugdFhsvvI8gadxOI6noqNeeBHvWNTbfYGtiAn+2jhI= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxql v1.1.1-0.20200828144457-65d3ef77d385/go.mod h1:gHp9y86a/pxhjJ+zMjNXiQAA197Xk9wLxaz+fGG+kWk= +github.com/influxdata/line-protocol v0.0.0-20180522152040-32c6aa80de5e/go.mod h1:4kt73NQhadE3daL3WhR5EJ/J2ocX0PZzwxQ0gXJ7oFE= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/influxdata/promql/v2 v2.12.0/go.mod h1:fxOPu+DY0bqCTCECchSRtWfc+0X19ybifQhZoQNF5D8= +github.com/influxdata/roaring v0.4.13-0.20180809181101-fc520f41fab6/go.mod h1:bSgUQ7q5ZLSO+bKBGqJiCBGAl+9DxyW63zLTujjUlOE= +github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0= +github.com/influxdata/usage-client v0.0.0-20160829180054-6d3895376368/go.mod h1:Wbbw6tYNvwa5dlB6304Sd+82Z3f7PmVZHVKU637d4po= +github.com/ipfs/go-cid v0.0.7 h1:ysQJVJA3fNDF1qigJbsSQOdjhVLsOEoPdh0+R97k3jY= +github.com/ipfs/go-cid v0.0.7/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= +github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= +github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= +github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI= +github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= +github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= +github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e/go.mod h1:G1CVv03EnqU1wYL2dFwXxW2An0az9JTl/ZsqXQeBlkU= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= +github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K8mysFmDaM/h+o= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= +github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/karalabe/usb v0.0.2/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= +github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4CqbAk= +github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= +github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U= +github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= +github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw= +github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= +github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0= +github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= +github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= +github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= +github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= +github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-libp2p-core v0.15.1 h1:0RY+Mi/ARK9DgG1g9xVQLb8dDaaU8tCePMtGALEfBnM= +github.com/libp2p/go-libp2p-core v0.15.1/go.mod h1:agSaboYM4hzB1cWekgVReqV5M4g5M+2eNNejV+1EEhs= +github.com/libp2p/go-openssl v0.0.7 h1:eCAzdLejcNVBzP/iZM9vqHnQm+XyCEbSSIheIPRGNsw= +github.com/libp2p/go-openssl v0.0.7/go.mod h1:unDrJpgy3oFr+rqXsarWifmJuNnJR4chtO1HmaZjggc= +github.com/linxGnu/grocksdb v1.7.10 h1:dz7RY7GnFUA+GJO6jodyxgkUeGMEkPp3ikt9hAcNGEw= +github.com/linxGnu/grocksdb v1.7.10/go.mod h1:0hTf+iA+GOr0jDX4CgIYyJZxqOH9XlBh6KVj8+zmF34= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= +github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= +github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg= +github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ= +github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= +github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= +github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 h1:QRUSJEgZn2Snx0EmT/QLXibWjSUDjKWvXIT19NBVp94= +github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= +github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI= +github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA= +github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4= +github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= +github.com/multiformats/go-multiaddr v0.4.1 h1:Pq37uLx3hsyNlTDir7FZyU8+cFCTqd5y1KiM2IzOutI= +github.com/multiformats/go-multiaddr v0.4.1/go.mod h1:3afI9HfVW8csiF8UZqtpYRiDyew8pRX7qLIGHu9FLuM= +github.com/multiformats/go-multibase v0.0.3 h1:l/B6bJDQjvQ5G52jw4QGSYeOTZoAwIO77RblWplfIqk= +github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc= +github.com/multiformats/go-multicodec v0.4.1 h1:BSJbf+zpghcZMZrwTYBGwy0CPcVZGWiC72Cp8bBd4R4= +github.com/multiformats/go-multicodec v0.4.1/go.mod h1:1Hj/eHRaVWSXiSNNfcEPcwZleTmdNP81xlxDLnWU9GQ= +github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= +github.com/multiformats/go-multihash v0.0.14/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= +github.com/multiformats/go-multihash v0.1.0 h1:CgAgwqk3//SVEw3T+6DqI4mWMyRuDwZtOWcJT0q9+EA= +github.com/multiformats/go-multihash v0.1.0/go.mod h1:RJlXsxt6vHGaia+S8We0ErjhojtKzPP2AH4+kYM7k84= +github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY= +github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= +github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= +github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.0.3-0.20180606204148-bd9c31933947/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/otiai10/copy v1.6.0 h1:IinKAryFFuPONZ7cm6T6E2QX/vcJwSnlaA5lfoaXIiQ= +github.com/oxyno-zeta/gomock-extra-matcher v1.1.0 h1:Yyk5ov0ZPKBXtVEeIWtc4J2XVrHuNoIK+0F2BUJgtsc= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/peterh/liner v1.0.1-0.20180619022028-8c1271fcf47f/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= +github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/xxHash v0.1.5 h1:n/jBpwTHiER4xYvK3/CdPVnLDPchj8eTJFFLUb4QHBo= +github.com/pierrec/xxHash v0.1.5/go.mod h1:w2waW5Zoa/Wc4Yqe0wgrIYAGKqRMf7czn2HNKXmuL+I= +github.com/pierrre/gotestcover v0.0.0-20160517101806-924dca7d15f0/go.mod h1:4xpMLz7RBWyB+ElzHu8Llua96TRCB3YwX+l5EP1wmHk= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pkg/term v0.0.0-20180730021639-bffc007b7fd5/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= +github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/regen-network/cosmos-proto v0.3.1 h1:rV7iM4SSFAagvy8RiyhiACbWEGotmqzywPxOvwMdxcg= +github.com/regen-network/cosmos-proto v0.3.1/go.mod h1:jO0sVX6a1B36nmE8C9xBFXpNwWejXC7QqCOnH3O0+YM= +github.com/regen-network/gocuke v0.6.2 h1:pHviZ0kKAq2U2hN2q3smKNxct6hS0mGByFMHGnWA97M= +github.com/regen-network/protobuf v1.3.3-alpha.regen.1 h1:OHEc+q5iIAXpqiqFKeLpu5NwTIkVXUs48vFMwzqpqY4= +github.com/regen-network/protobuf v1.3.3-alpha.regen.1/go.mod h1:2DjTFR1HhMQhiWC5sZ4OhQ3+NtdbZ6oBDKQwq5Ou+FI= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc= +github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= +github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= +github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= +github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= +github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/segmentio/kafka-go v0.1.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= +github.com/segmentio/kafka-go v0.2.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= +github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= +github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU= +github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= +github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= +github.com/strangelove-ventures/go-subkey v1.0.7 h1:cOP/Lajg3uxV/tvspu0m6+0Cu+DJgygkEAbx/s+f35I= +github.com/strangelove-ventures/go-subkey v1.0.7/go.mod h1:E34izOIEm+sZ1YmYawYRquqBQWeZBjVB4pF7bMuhc1c= +github.com/strangelove-ventures/interchaintest/v4 v4.0.0-20230316161044-8d8c01f96b4a h1:ReDdhlzY19zH7Ql6aPUsxnO7H5H/HT5PcfXtkn2OWPc= +github.com/strangelove-ventures/interchaintest/v4 v4.0.0-20230316161044-8d8c01f96b4a/go.mod h1:1iRRVEhAIGtYMl7/UVEwGx7O1tN4x8C+SzS/MBpi+JY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= +github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= +github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok= +github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= +github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= +github.com/tendermint/tm-db v0.6.7 h1:fE00Cbl0jayAoqlExN6oyQJ7fR/ZtoVOmvPJ//+shu8= +github.com/tendermint/tm-db v0.6.7/go.mod h1:byQDzFkZV1syXr/ReXS808NxA2xvyuuVgXOJ/088L6I= +github.com/tidwall/btree v1.5.0 h1:iV0yVY/frd7r6qGBXfEYs7DH0gTDgrKTrDjS7xt/IyQ= +github.com/tidwall/btree v1.5.0/go.mod h1:LGm8L/DZjPLmeWGjv5kFrY8dL4uVhMmzmmLYmsObdKE= +github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4= +github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= +github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA= +github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zondax/hid v0.9.1 h1:gQe66rtmyZ8VeGFcOpbuH3r7erYtNEAezCAYu8LdkJo= +github.com/zondax/hid v0.9.1/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= +github.com/zondax/ledger-go v0.14.1 h1:Pip65OOl4iJ84WTpA4BKChvOufMhhbxED3BaihoZN4c= +github.com/zondax/ledger-go v0.14.1/go.mod h1:fZ3Dqg6qcdXWSOJFKMG8GCTnD7slO/RL2feOQv8K320= +go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190909091759-094676da4a83/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= +golang.org/x/exp v0.0.0-20221019170559-20944726eadf h1:nFVjjKDgNY37+ZSYCJmtYf7tOlfQswHqplG2eosjOMg= +golang.org/x/exp v0.0.0-20221019170559-20944726eadf/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210909193231-528a39cd75f3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211107104306-e0b2ad06fe42/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= +gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200108215221-bd8f9a0ef82f/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200324203455-a04cca1dde73/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa h1:qQPhfbPO23fwm/9lQr91L1u62Zo6cm+zI+slZT+uf+o= +google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.52.3 h1:pf7sOysg4LdgBqduXveGKrcEwbStiK2rtfghdzlUYDQ= +google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8 h1:KR8+MyP7/qOlV+8Af01LtjL04bu7on42eVsxT4EyBQk= +google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= +gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c= +lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.36.0 h1:0kmRkTmqNidmu3c7BNDSdVHCxXCkWLmWmCIVX4LUboo= +modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= +modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= +modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.6 h1:3l18poV+iUemQ98O3X5OMr97LOqlzis+ytivU4NqGhA= +modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= +modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= +modernc.org/libc v1.16.7 h1:qzQtHhsZNpVPpeCu+aMIQldXeV1P0vRhSqCL0nOIJOA= +modernc.org/libc v1.16.7/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU= +modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.17.3 h1:iE+coC5g17LtByDYDWKpR6m2Z9022YrSh3bumwOnIrI= +modernc.org/sqlite v1.17.3/go.mod h1:10hPVYar9C0kfXuTWGz8s0XtB8uAGymUy51ZzStYe3k= +modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao= +modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= +modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM= +modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +pgregory.net/rapid v0.5.3 h1:163N50IHFqr1phZens4FQOdPgfJscR7a562mjQqeo4M= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go new file mode 100644 index 00000000..abb1bad0 --- /dev/null +++ b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go @@ -0,0 +1,504 @@ +package ibc_test + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + ibctest "github.com/strangelove-ventures/interchaintest/v4" + "github.com/strangelove-ventures/interchaintest/v4/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v4/ibc" + "github.com/strangelove-ventures/interchaintest/v4/relayer" + "github.com/strangelove-ventures/interchaintest/v4/relayer/rly" + "github.com/strangelove-ventures/interchaintest/v4/testreporter" + "github.com/strangelove-ventures/interchaintest/v4/testutil" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zaptest" +) + +const gaiaNeutronICSPath = "gn-ics-path" +const gaiaNeutronIBCPath = "gn-ibc-path" +const gaiaOsmosisIBCPath = "go-ibc-path" +const neutronOsmosisIBCPath = "no-ibc-path" +const nativeAtomDenom = "uatom" +const nativeOsmoDenom = "uosmo" +const nativeNtrnDenom = "untrn" + +var covenantAddress string +var clockAddress string +var partyARouterAddress, partyBRouterAddress string +var partyAIbcForwarderAddress, partyBIbcForwarderAddress string +var partyADepositAddress, partyBDepositAddress string +var holderAddress string +var neutronAtomIbcDenom, neutronOsmoIbcDenom, osmoNeutronAtomIbcDenom, gaiaNeutronOsmoIbcDenom string +var atomNeutronICSConnectionId, neutronAtomICSConnectionId string +var neutronOsmosisIBCConnId, osmosisNeutronIBCConnId string +var atomNeutronIBCConnId, neutronAtomIBCConnId string +var gaiaOsmosisIBCConnId, osmosisGaiaIBCConnId string + +// PARTY_A +const osmoContributionAmount uint64 = 100_000_000_000 // in uosmo + +// PARTY_B +const atomContributionAmount uint64 = 5_000_000_000 // in uatom + +// sets up and tests a two party pol between hub and osmo facilitated by neutron +func TestTwoPartyPol(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + + ctx := context.Background() + + // Modify the the timeout_commit in the config.toml node files + // to reduce the block commit times. This speeds up the tests + // by about 35% + configFileOverrides := make(map[string]any) + configTomlOverrides := make(testutil.Toml) + consensus := make(testutil.Toml) + consensus["timeout_commit"] = "1s" + configTomlOverrides["consensus"] = consensus + configFileOverrides["config/config.toml"] = configTomlOverrides + + // Chain Factory + cf := ibctest.NewBuiltinChainFactory(zaptest.NewLogger(t, zaptest.Level(zap.WarnLevel)), []*ibctest.ChainSpec{ + {Name: "gaia", Version: "v9.1.0", ChainConfig: ibc.ChainConfig{ + GasAdjustment: 1.3, + GasPrices: "0.0atom", + ModifyGenesis: setupGaiaGenesis(getDefaultInterchainGenesisMessages()), + ConfigFileOverrides: configFileOverrides, + }}, + { + ChainConfig: ibc.ChainConfig{ + Type: "cosmos", + Name: "neutron", + ChainID: "neutron-2", + Images: []ibc.DockerImage{ + { + Repository: "ghcr.io/strangelove-ventures/heighliner/neutron", + Version: "v1.0.2", + UidGid: "1025:1025", + }, + }, + Bin: "neutrond", + Bech32Prefix: "neutron", + Denom: nativeNtrnDenom, + GasPrices: "0.0untrn,0.0uatom", + GasAdjustment: 1.3, + TrustingPeriod: "1197504s", + NoHostMount: false, + ModifyGenesis: setupNeutronGenesis( + "0.05", + []string{nativeNtrnDenom}, + []string{nativeAtomDenom}, + getDefaultInterchainGenesisMessages(), + ), + ConfigFileOverrides: configFileOverrides, + }, + }, + { + Name: "osmosis", + Version: "v14.0.0", + ChainConfig: ibc.ChainConfig{ + Type: "cosmos", + Bin: "osmosisd", + Bech32Prefix: "osmo", + Denom: nativeOsmoDenom, + ModifyGenesis: setupOsmoGenesis( + append(getDefaultInterchainGenesisMessages(), "/ibc.applications.interchain_accounts.v1.InterchainAccount"), + ), + GasPrices: "0.0uosmo", + GasAdjustment: 1.3, + Images: []ibc.DockerImage{ + { + Repository: "ghcr.io/strangelove-ventures/heighliner/osmosis", + Version: "v14.0.0", + UidGid: "1025:1025", + }, + }, + TrustingPeriod: "336h", + NoHostMount: false, + ConfigFileOverrides: configFileOverrides, + }, + }, + }) + + chains, err := cf.Chains(t.Name()) + require.NoError(t, err) + + // We have three chains + atom, neutron, osmosis := chains[0], chains[1], chains[2] + cosmosAtom, cosmosNeutron, cosmosOsmosis := atom.(*cosmos.CosmosChain), neutron.(*cosmos.CosmosChain), osmosis.(*cosmos.CosmosChain) + + // Relayer Factory + client, network := ibctest.DockerSetup(t) + r := ibctest.NewBuiltinRelayerFactory( + ibc.CosmosRly, + zaptest.NewLogger(t, zaptest.Level(zap.InfoLevel)), + relayer.CustomDockerImage("ghcr.io/cosmos/relayer", "v2.3.1", rly.RlyDefaultUidGid), + relayer.RelayerOptionExtraStartFlags{Flags: []string{"-p", "events", "-b", "100", "-d", "--log-format", "console"}}, + ).Build(t, client, network) + + // Prep Interchain + ic := ibctest.NewInterchain(). + AddChain(cosmosAtom). + AddChain(cosmosNeutron). + AddChain(cosmosOsmosis). + AddRelayer(r, "relayer"). + AddProviderConsumerLink(ibctest.ProviderConsumerLink{ + Provider: cosmosAtom, + Consumer: cosmosNeutron, + Relayer: r, + Path: gaiaNeutronICSPath, + }). + AddLink(ibctest.InterchainLink{ + Chain1: cosmosAtom, + Chain2: cosmosNeutron, + Relayer: r, + Path: gaiaNeutronIBCPath, + }). + AddLink(ibctest.InterchainLink{ + Chain1: cosmosNeutron, + Chain2: cosmosOsmosis, + Relayer: r, + Path: neutronOsmosisIBCPath, + }). + AddLink(ibctest.InterchainLink{ + Chain1: cosmosAtom, + Chain2: cosmosOsmosis, + Relayer: r, + Path: gaiaOsmosisIBCPath, + }) + + // Log location + f, err := ibctest.CreateLogFile(fmt.Sprintf("%d.json", time.Now().Unix())) + require.NoError(t, err) + // Reporter/logs + rep := testreporter.NewReporter(f) + eRep := rep.RelayerExecReporter(t) + + // Build interchain + require.NoError( + t, + ic.Build(ctx, eRep, ibctest.InterchainBuildOptions{ + TestName: t.Name(), + Client: client, + NetworkID: network, + BlockDatabaseFile: ibctest.DefaultBlockDatabaseFilepath(), + SkipPathCreation: true, + }), + "failed to build interchain") + + err = testutil.WaitForBlocks(ctx, 10, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + + testCtx := &TestContext{ + OsmoClients: []*ibc.ClientOutput{}, + GaiaClients: []*ibc.ClientOutput{}, + NeutronClients: []*ibc.ClientOutput{}, + OsmoConnections: []*ibc.ConnectionOutput{}, + GaiaConnections: []*ibc.ConnectionOutput{}, + NeutronConnections: []*ibc.ConnectionOutput{}, + NeutronTransferChannelIds: make(map[string]string), + GaiaTransferChannelIds: make(map[string]string), + OsmoTransferChannelIds: make(map[string]string), + GaiaIcsChannelIds: make(map[string]string), + NeutronIcsChannelIds: make(map[string]string), + } + + t.Run("generate IBC paths", func(t *testing.T) { + generatePath(t, ctx, r, eRep, cosmosAtom.Config().ChainID, cosmosNeutron.Config().ChainID, gaiaNeutronIBCPath) + generatePath(t, ctx, r, eRep, cosmosAtom.Config().ChainID, cosmosOsmosis.Config().ChainID, gaiaOsmosisIBCPath) + generatePath(t, ctx, r, eRep, cosmosNeutron.Config().ChainID, cosmosOsmosis.Config().ChainID, neutronOsmosisIBCPath) + generatePath(t, ctx, r, eRep, cosmosNeutron.Config().ChainID, cosmosAtom.Config().ChainID, gaiaNeutronICSPath) + }) + + t.Run("setup neutron-gaia ICS", func(t *testing.T) { + generateClient(t, ctx, testCtx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) + neutronClients := testCtx.getChainClients(cosmosNeutron.Config().Name) + atomClients := testCtx.getChainClients(cosmosAtom.Config().Name) + + err = r.UpdatePath(ctx, eRep, gaiaNeutronICSPath, ibc.PathUpdateOptions{ + SrcClientID: &neutronClients[0].ClientID, + DstClientID: &atomClients[0].ClientID, + }) + require.NoError(t, err) + + atomNeutronICSConnectionId, neutronAtomICSConnectionId = generateConnections(t, ctx, testCtx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) + + generateICSChannel(t, ctx, r, eRep, gaiaNeutronICSPath, cosmosAtom, cosmosNeutron) + + createValidator(t, ctx, r, eRep, atom, neutron) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + }) + + t.Run("setup IBC interchain clients, connections, and links", func(t *testing.T) { + generateClient(t, ctx, testCtx, r, eRep, neutronOsmosisIBCPath, cosmosNeutron, cosmosOsmosis) + neutronOsmosisIBCConnId, osmosisNeutronIBCConnId = generateConnections(t, ctx, testCtx, r, eRep, neutronOsmosisIBCPath, cosmosNeutron, cosmosOsmosis) + linkPath(t, ctx, r, eRep, cosmosNeutron, cosmosOsmosis, neutronOsmosisIBCPath) + + generateClient(t, ctx, testCtx, r, eRep, gaiaOsmosisIBCPath, cosmosAtom, cosmosOsmosis) + gaiaOsmosisIBCConnId, osmosisGaiaIBCConnId = generateConnections(t, ctx, testCtx, r, eRep, gaiaOsmosisIBCPath, cosmosAtom, cosmosOsmosis) + linkPath(t, ctx, r, eRep, cosmosAtom, cosmosOsmosis, gaiaOsmosisIBCPath) + + generateClient(t, ctx, testCtx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) + atomNeutronIBCConnId, neutronAtomIBCConnId = generateConnections(t, ctx, testCtx, r, eRep, gaiaNeutronIBCPath, cosmosAtom, cosmosNeutron) + linkPath(t, ctx, r, eRep, cosmosAtom, cosmosNeutron, gaiaNeutronIBCPath) + }) + + // Start the relayer and clean it up when the test ends. + err = r.StartRelayer(ctx, eRep, gaiaNeutronICSPath, gaiaNeutronIBCPath, gaiaOsmosisIBCPath, neutronOsmosisIBCPath) + require.NoError(t, err, "failed to start relayer with given paths") + t.Cleanup(func() { + err = r.StopRelayer(ctx, eRep) + if err != nil { + t.Logf("failed to stop relayer: %s", err) + } + }) + + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + + // Once the VSC packet has been relayed, x/bank transfers are + // enabled on Neutron and we can fund its account. + // The funds for this are sent from a "faucet" account created + // by interchaintest in the genesis file. + users := ibctest.GetAndFundTestUsers(t, ctx, "default", int64(500_000_000_000), atom, neutron, osmosis) + gaiaUser, neutronUser, osmoUser := users[0], users[1], users[2] + _, _, _ = gaiaUser, neutronUser, osmoUser + + err = testutil.WaitForBlocks(ctx, 10, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + + t.Run("determine ibc channels", func(t *testing.T) { + neutronChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosNeutron.Config().ChainID) + gaiaChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosAtom.Config().ChainID) + osmoChannelInfo, _ := r.GetChannels(ctx, eRep, cosmosOsmosis.Config().ChainID) + + // Find all pairwise channels + getPairwiseTransferChannelIds(testCtx, osmoChannelInfo, neutronChannelInfo, osmosisNeutronIBCConnId, neutronOsmosisIBCConnId, osmosis.Config().Name, neutron.Config().Name) + getPairwiseTransferChannelIds(testCtx, osmoChannelInfo, gaiaChannelInfo, osmosisGaiaIBCConnId, gaiaOsmosisIBCConnId, osmosis.Config().Name, cosmosAtom.Config().Name) + getPairwiseTransferChannelIds(testCtx, gaiaChannelInfo, neutronChannelInfo, atomNeutronIBCConnId, neutronAtomIBCConnId, cosmosAtom.Config().Name, neutron.Config().Name) + getPairwiseCCVChannelIds(testCtx, gaiaChannelInfo, neutronChannelInfo, atomNeutronICSConnectionId, neutronAtomICSConnectionId, cosmosAtom.Config().Name, cosmosNeutron.Config().Name) + }) + + t.Run("determine ibc denoms", func(t *testing.T) { + // We can determine the ibc denoms of: + // 1. ATOM on Neutron + neutronAtomIbcDenom = testCtx.getIbcDenom( + testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], + nativeAtomDenom, + ) + // 2. Osmo on neutron + neutronOsmoIbcDenom = testCtx.getIbcDenom( + testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], + nativeOsmoDenom, + ) + // 3. hub atom => neutron => osmosis + osmoNeutronAtomIbcDenom = testCtx.getMultihopIbcDenom( + []string{ + testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name], + testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], + }, + nativeAtomDenom, + ) + // 4. osmosis osmo => neutron => hub + gaiaNeutronOsmoIbcDenom = testCtx.getMultihopIbcDenom( + []string{ + testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], + testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], + }, + nativeOsmoDenom, + ) + }) + + t.Run("two party pol covenant setup", func(t *testing.T) { + // Wasm code that we need to store on Neutron + const covenantContractPath = "wasms/covenant_two_party_pol.wasm" + const clockContractPath = "wasms/covenant_clock.wasm" + const routerContractPath = "wasms/covenant_interchain_router.wasm" + const ibcForwarderContractPath = "wasms/covenant_ibc_forwarder.wasm" + const holderContractPath = "wasms/covenant_two_party_pol_holder.wasm" + + // After storing on Neutron, we will receive a code id + // We parse all the subcontracts into uint64 + // The will be required when we instantiate the covenant. + var clockCodeId uint64 + var routerCodeId uint64 + var ibcForwarderCodeId uint64 + var holderCodeId uint64 + var covenantCodeIdStr string + var covenantCodeId uint64 + _ = covenantCodeId + + t.Run("deploy covenant contracts", func(t *testing.T) { + // store covenant and get code id + covenantCodeIdStr, err = cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, covenantContractPath) + require.NoError(t, err, "failed to store stride covenant contract") + covenantCodeId, err = strconv.ParseUint(covenantCodeIdStr, 10, 64) + require.NoError(t, err, "failed to parse codeId into uint64") + + // store clock and get code id + clockCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, clockContractPath) + require.NoError(t, err, "failed to store clock contract") + clockCodeId, err = strconv.ParseUint(clockCodeIdStr, 10, 64) + require.NoError(t, err, "failed to parse codeId into uint64") + + // store router and get code id + routerCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, routerContractPath) + require.NoError(t, err, "failed to store router contract") + routerCodeId, err = strconv.ParseUint(routerCodeIdStr, 10, 64) + require.NoError(t, err, "failed to parse codeId into uint64") + + // store forwarder and get code id + ibcForwarderCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, ibcForwarderContractPath) + require.NoError(t, err, "failed to store ibc forwarder contract") + ibcForwarderCodeId, err = strconv.ParseUint(ibcForwarderCodeIdStr, 10, 64) + require.NoError(t, err, "failed to parse codeId into uint64") + + // store clock and get code id + holderCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, holderContractPath) + require.NoError(t, err, "failed to store two party pol holder contract") + holderCodeId, err = strconv.ParseUint(holderCodeIdStr, 10, 64) + require.NoError(t, err, "failed to parse codeId into uint64") + + require.NoError(t, testutil.WaitForBlocks(ctx, 5, cosmosNeutron, cosmosAtom, cosmosOsmosis)) + }) + + t.Run("instantiate covenant", func(t *testing.T) { + timeouts := Timeouts{ + IcaTimeout: "100", // sec + IbcTransferTimeout: "100", // sec + } + + block := Block(500) + + lockupConfig := ExpiryConfig{ + BlockHeight: &block, + } + // depositDeadline := ExpiryConfig{ + // BlockHeight: &block, + // } + presetIbcFee := PresetIbcFee{ + AckFee: "10000", + TimeoutFee: "10000", + } + + tickMaxGas := "2000" + poolAddress := "todo" + + // atomCoin := Coin{ + // Denom: "uatom", + // Amount: "10000", + // } + + // osmoCoin := Coin{ + // Denom: "uosmo", + // Amount: "100000", + // } + + // partyAConfig := CovenantPartyConfig{ + // Addr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + // Contribution: atomCoin, + // IbcDenom: neutronAtomIbcDenom, + // PartyToHostChainChannelId: testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], + // HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], + // PartyReceiverAddr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + // PartyChainConnectionId: neutronAtomIBCConnId, + // IbcTransferTimeout: timeouts.IbcTransferTimeout, + // } + // partyBConfig := CovenantPartyConfig{ + // Addr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + // Contribution: osmoCoin, + // IbcDenom: neutronOsmoIbcDenom, + // PartyToHostChainChannelId: testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name], + // HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], + // PartyReceiverAddr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + // PartyChainConnectionId: neutronOsmosisIBCConnId, + // IbcTransferTimeout: timeouts.IbcTransferTimeout, + // } + codeIds := ContractCodeIds{ + IbcForwarderCode: ibcForwarderCodeId, + InterchainRouterCode: routerCodeId, + ClockCode: clockCodeId, + HolderCode: holderCodeId, + } + + // ragequitTerms := RagequitTerms{ + // Penalty: "0.1", + // } + + // ragequitConfig := RagequitConfig{ + // Enabled: &ragequitTerms, + // } + + covenantMsg := CovenantInstantiateMsg{ + Label: "two-party-pol-covenant", + Timeouts: timeouts, + PresetIbcFee: presetIbcFee, + ContractCodeIds: codeIds, + TickMaxGas: &tickMaxGas, + LockupConfig: lockupConfig, + // PartyAConfig: partyAConfig, + // PartyBConfig: partyBConfig, + PoolAddress: poolAddress, + // RagequitConfig: &ragequitConfig, + // DepositDeadline: &depositDeadline, + PartyAShare: "50", + PartyBShare: "50", + } + str, err := json.Marshal(covenantMsg) + require.NoError(t, err, "Failed to marshall CovenantInstantiateMsg") + instantiateMsg := string(str) + + println("instantiation message: ", instantiateMsg) + cmd := []string{"neutrond", "tx", "wasm", "instantiate", covenantCodeIdStr, + instantiateMsg, + "--label", "two-party-pol-covenant", + "--no-admin", + "--from", neutronUser.KeyName, + "--output", "json", + "--home", neutron.HomeDir(), + "--node", neutron.GetRPCAddress(), + "--chain-id", neutron.Config().ChainID, + "--gas", "90009000", + "--keyring-backend", keyring.BackendTest, + "-y", + } + + _, _, err = neutron.Exec(ctx, cmd, nil) + require.NoError(t, err) + require.NoError(t, testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis)) + + queryCmd := []string{"neutrond", "query", "wasm", + "list-contract-by-code", covenantCodeIdStr, + "--output", "json", + "--home", neutron.HomeDir(), + "--node", neutron.GetRPCAddress(), + "--chain-id", neutron.Config().ChainID, + } + + queryResp, _, err := neutron.Exec(ctx, queryCmd, nil) + require.NoError(t, err, "failed to query") + + type QueryContractResponse struct { + Contracts []string `json:"contracts"` + Pagination any `json:"pagination"` + } + + contactsRes := QueryContractResponse{} + require.NoError(t, json.Unmarshal(queryResp, &contactsRes), "failed to unmarshal contract response") + + covenantAddress = contactsRes.Contracts[len(contactsRes.Contracts)-1] + + println("covenant address: ", covenantAddress) + }) + + }) +} diff --git a/two-party-pol-covenant/tests/interchaintest/types.go b/two-party-pol-covenant/tests/interchaintest/types.go new file mode 100644 index 00000000..31d5f427 --- /dev/null +++ b/two-party-pol-covenant/tests/interchaintest/types.go @@ -0,0 +1,116 @@ +package ibc_test + +////////////////////////////////////////////// +///// Covenant contracts +////////////////////////////////////////////// + +// ----- Covenant Instantiation ------ +type CovenantInstantiateMsg struct { + Label string `json:"label"` + Timeouts Timeouts `json:"timeouts"` + PresetIbcFee PresetIbcFee `json:"preset_ibc_fee"` + ContractCodeIds ContractCodeIds `json:"contract_codes"` + TickMaxGas *string `json:"clock_tick_max_gas,omitempty"` + LockupConfig ExpiryConfig `json:"lockup_config"` + // PartyAConfig CovenantPartyConfig `json:"party_a_config"` + // PartyBConfig CovenantPartyConfig `json:"party_b_config"` + PoolAddress string `json:"pool_address"` + RagequitConfig *RagequitConfig `json:"ragequit_config,omitempty"` + DepositDeadline *ExpiryConfig `json:"expiry_config,omitempty"` + PartyAShare string `json:"party_a_share"` + PartyBShare string `json:"party_b_share"` +} + +type ContractCodeIds struct { + IbcForwarderCode uint64 `json:"ibc_forwarder_code"` + InterchainRouterCode uint64 `json:"router_code"` + ClockCode uint64 `json:"clock_code"` + HolderCode uint64 `json:"holder_code"` +} + +type Timeouts struct { + IcaTimeout string `json:"ica_timeout"` + IbcTransferTimeout string `json:"ibc_transfer_timeout"` +} + +type PresetIbcFee struct { + AckFee string `json:"ack_fee"` + TimeoutFee string `json:"timeout_fee"` +} + +type Timestamp string +type Block uint64 + +type ExpiryConfig struct { + None string `json:"none,omitempty"` + BlockHeight *Block `json:"block,omitempty"` + Time *Timestamp `json:"time,omitempty"` +} + +type RagequitConfig struct { + Disabled bool `json:"disabled,omitempty"` + Enabled *RagequitTerms `json:"enabled,omitempty"` +} + +type RagequitTerms struct { + Penalty string `json:"penalty,omitempty"` + State *RagequitState `json:"state,omitempty"` +} + +type RagequitState struct { + Coins []Coin `json:"coins"` + RqParty CovenantParty `json:"rq_party"` +} + +type CovenantParty struct { + Contribution Coin `json:"contribution"` + Addr string `json:"addr"` + Allocation string `json:"allocation"` + Router string `json:"router"` +} + +type CovenantPartyConfig struct { + Addr string `json:"addr"` + Contribution Coin `json:"contribution"` + IbcDenom string `json:"ibc_denom"` + PartyToHostChainChannelId string `json:"party_to_host_chain_channel_id"` + HostToPartyChainChannelId string `json:"host_to_party_chain_channel_id"` + PartyReceiverAddr string `json:"party_receiver_addr"` + PartyChainConnectionId string `json:"party_chain_connection_id"` + IbcTransferTimeout string `json:"ibc_transfer_timeout"` +} + +type Coin struct { + Denom string `json:"denom"` + Amount string `json:"amount"` +} + +// ----- Covenant Queries ------ +type ClockAddress struct{} +type ClockAddressQuery struct { + ClockAddress ClockAddress `json:"clock_address"` +} + +type HolderAddress struct{} +type HolderAddressQuery struct { + HolderAddress HolderAddress `json:"holder_address"` +} + +type CovenantParties struct{} +type CovenantPartiesQuery struct { + CovenantParties CovenantParties `json:"covenant_parties"` +} + +type Party struct { + Party string `json:"party"` +} +type InterchainRouterQuery struct { + Party Party `json:"interchain_router_address"` +} +type IbcForwarderQuery struct { + Party Party `json:"ibc_forwarder_address"` +} + +type CovenantAddressQueryResponse struct { + Data string `json:"data"` +} From 11a0a67b5a289c6f904345519391693de844a423 Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 12 Oct 2023 14:32:50 +0200 Subject: [PATCH 117/586] interchaintest fix instantiate msg --- .../two-party-pol-covenant/src/contract.rs | 167 +++++++++--------- contracts/two-party-pol-covenant/src/msg.rs | 12 +- .../interchaintest/two_party_pol_test.go | 92 +++++----- .../tests/interchaintest/types.go | 26 +-- 4 files changed, 148 insertions(+), 149 deletions(-) diff --git a/contracts/two-party-pol-covenant/src/contract.rs b/contracts/two-party-pol-covenant/src/contract.rs index 04ed3ee9..cccb1541 100644 --- a/contracts/two-party-pol-covenant/src/contract.rs +++ b/contracts/two-party-pol-covenant/src/contract.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Decimal, Uint128, CosmosMsg, WasmMsg, SubMsg, Reply, + to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Decimal, Uint128, CosmosMsg, WasmMsg, SubMsg, Reply, coin, }; use covenant_clock::msg::PresetClockFields; @@ -41,94 +41,93 @@ pub fn instantiate( ) -> Result { deps.api.debug("WASMDEBUG: instantiate"); set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - // let preset_clock_fields = PresetClockFields { - // tick_max_gas: msg.clock_tick_max_gas, - // whitelist: vec![], - // code_id: msg.contract_codes.clock_code, - // label: format!("{}-clock", msg.label), - // }; - // PRESET_CLOCK_FIELDS.save(deps.storage, &preset_clock_fields)?; - - // let preset_holder_fields = PresetTwoPartyPolHolderFields { - // lockup_config:msg.lockup_config, - // pool_address: msg.pool_address, - // ragequit_config: msg.ragequit_config.unwrap_or(RagequitConfig::Disabled), - // deposit_deadline: msg.deposit_deadline, - // party_a: PresetPolParty { - // contribution: msg.party_a_config.contribution.clone(), - // addr: msg.party_a_config.addr, - // allocation: Decimal::from_ratio(msg.party_a_share, Uint128::new(100)), - // }, - // party_b: PresetPolParty { - // contribution: msg.party_b_config.contribution.clone(), - // addr: msg.party_b_config.addr, - // allocation: Decimal::from_ratio(msg.party_b_share, Uint128::new(100)), - // }, - // code_id: msg.contract_codes.holder_code, - // }; - // PRESET_HOLDER_FIELDS.save(deps.storage, &preset_holder_fields)?; - - - // let preset_party_a_forwarder_fields = PresetIbcForwarderFields { - // remote_chain_connection_id: msg.party_a_config.party_chain_connection_id, - // remote_chain_channel_id: msg.party_a_config.party_to_host_chain_channel_id, - // denom: msg.party_a_config.contribution.denom.to_string(), - // amount: msg.party_a_config.contribution.amount, - // label: format!("{}_party_a_ibc_forwarder", msg.label), - // code_id: msg.contract_codes.ibc_forwarder_code, - // ica_timeout: msg.timeouts.ica_timeout, - // ibc_transfer_timeout: msg.timeouts.ibc_transfer_timeout, - // ibc_fee: msg.preset_ibc_fee.to_ibc_fee(), - // }; - // let preset_party_b_forwarder_fields = PresetIbcForwarderFields { - // remote_chain_connection_id: msg.party_b_config.party_chain_connection_id, - // remote_chain_channel_id: msg.party_b_config.party_to_host_chain_channel_id, - // denom: msg.party_b_config.contribution.denom.to_string(), - // amount: msg.party_b_config.contribution.amount, - // label: format!("{}_party_b_ibc_forwarder", msg.label), - // code_id: msg.contract_codes.ibc_forwarder_code, - // ica_timeout: msg.timeouts.ica_timeout, - // ibc_transfer_timeout: msg.timeouts.ibc_transfer_timeout, - // ibc_fee: msg.preset_ibc_fee.to_ibc_fee(), - // }; + let preset_clock_fields = PresetClockFields { + tick_max_gas: msg.clock_tick_max_gas, + whitelist: vec![], + code_id: msg.contract_codes.clock_code, + label: format!("{}-clock", msg.label), + }; + PRESET_CLOCK_FIELDS.save(deps.storage, &preset_clock_fields)?; + + let preset_holder_fields = PresetTwoPartyPolHolderFields { + lockup_config:msg.lockup_config, + pool_address: msg.pool_address, + ragequit_config: msg.ragequit_config.unwrap_or(RagequitConfig::Disabled), + deposit_deadline: msg.deposit_deadline, + party_a: PresetPolParty { + contribution: msg.party_a_config.contribution.clone(), + addr: msg.party_a_config.addr, + allocation: Decimal::from_ratio(msg.party_a_share, Uint128::new(100)), + }, + party_b: PresetPolParty { + contribution: msg.party_b_config.contribution.clone(), + addr: msg.party_b_config.addr, + allocation: Decimal::from_ratio(msg.party_b_share, Uint128::new(100)), + }, + code_id: msg.contract_codes.holder_code, + }; + PRESET_HOLDER_FIELDS.save(deps.storage, &preset_holder_fields)?; + + + let preset_party_a_forwarder_fields = PresetIbcForwarderFields { + remote_chain_connection_id: msg.party_a_config.party_chain_connection_id, + remote_chain_channel_id: msg.party_a_config.party_to_host_chain_channel_id, + denom: msg.party_a_config.contribution.denom.to_string(), + amount: msg.party_a_config.contribution.amount, + label: format!("{}_party_a_ibc_forwarder", msg.label), + code_id: msg.contract_codes.ibc_forwarder_code, + ica_timeout: msg.timeouts.ica_timeout, + ibc_transfer_timeout: msg.timeouts.ibc_transfer_timeout, + ibc_fee: msg.preset_ibc_fee.to_ibc_fee(), + }; + let preset_party_b_forwarder_fields = PresetIbcForwarderFields { + remote_chain_connection_id: msg.party_b_config.party_chain_connection_id, + remote_chain_channel_id: msg.party_b_config.party_to_host_chain_channel_id, + denom: msg.party_b_config.contribution.denom.to_string(), + amount: msg.party_b_config.contribution.amount, + label: format!("{}_party_b_ibc_forwarder", msg.label), + code_id: msg.contract_codes.ibc_forwarder_code, + ica_timeout: msg.timeouts.ica_timeout, + ibc_transfer_timeout: msg.timeouts.ibc_transfer_timeout, + ibc_fee: msg.preset_ibc_fee.to_ibc_fee(), + }; - // PRESET_PARTY_A_FORWARDER_FIELDS.save(deps.storage, &preset_party_a_forwarder_fields)?; - // PRESET_PARTY_B_FORWARDER_FIELDS.save(deps.storage, &preset_party_b_forwarder_fields)?; - - // let preset_party_a_router_fields = PresetInterchainRouterFields { - // destination_chain_channel_id: msg.party_a_config.host_to_party_chain_channel_id, - // destination_receiver_addr: msg.party_a_config.party_receiver_addr, - // ibc_transfer_timeout: msg.party_a_config.ibc_transfer_timeout, - // label: format!("{}_party_a_interchain_router", msg.label), - // code_id: msg.contract_codes.router_code, - // }; - // let preset_party_b_router_fields = PresetInterchainRouterFields { - // destination_chain_channel_id: msg.party_b_config.host_to_party_chain_channel_id, - // destination_receiver_addr: msg.party_b_config.party_receiver_addr, - // ibc_transfer_timeout: msg.party_b_config.ibc_transfer_timeout, - // label: format!("{}_party_b_interchain_router", msg.label), - // code_id: msg.contract_codes.router_code, - // }; - - // PRESET_PARTY_A_ROUTER_FIELDS.save(deps.storage, &preset_party_a_router_fields)?; - // PRESET_PARTY_B_ROUTER_FIELDS.save(deps.storage, &preset_party_b_router_fields)?; + PRESET_PARTY_A_FORWARDER_FIELDS.save(deps.storage, &preset_party_a_forwarder_fields)?; + PRESET_PARTY_B_FORWARDER_FIELDS.save(deps.storage, &preset_party_b_forwarder_fields)?; + + let preset_party_a_router_fields = PresetInterchainRouterFields { + destination_chain_channel_id: msg.party_a_config.host_to_party_chain_channel_id, + destination_receiver_addr: msg.party_a_config.party_receiver_addr, + ibc_transfer_timeout: msg.party_a_config.ibc_transfer_timeout, + label: format!("{}_party_a_interchain_router", msg.label), + code_id: msg.contract_codes.router_code, + }; + let preset_party_b_router_fields = PresetInterchainRouterFields { + destination_chain_channel_id: msg.party_b_config.host_to_party_chain_channel_id, + destination_receiver_addr: msg.party_b_config.party_receiver_addr, + ibc_transfer_timeout: msg.party_b_config.ibc_transfer_timeout, + label: format!("{}_party_b_interchain_router", msg.label), + code_id: msg.contract_codes.router_code, + }; + + PRESET_PARTY_A_ROUTER_FIELDS.save(deps.storage, &preset_party_a_router_fields)?; + PRESET_PARTY_B_ROUTER_FIELDS.save(deps.storage, &preset_party_b_router_fields)?; // we start the module instantiation chain with the clock - // let clock_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { - // admin: Some(env.contract.address.to_string()), - // code_id: preset_clock_fields.code_id, - // msg: to_binary(&preset_clock_fields.to_instantiate_msg())?, - // funds: vec![], - // label: preset_clock_fields.label, - // }); + let clock_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.to_string()), + code_id: preset_clock_fields.code_id, + msg: to_binary(&preset_clock_fields.to_instantiate_msg())?, + funds: vec![], + label: preset_clock_fields.label, + }); Ok(Response::default() - .add_attribute("method", "instantiate")) - // .add_submessage(SubMsg::reply_on_success( - // clock_instantiate_tx, - // CLOCK_REPLY_ID, - // ))) + .add_attribute("method", "instantiate") + .add_submessage(SubMsg::reply_on_success( + clock_instantiate_tx, + CLOCK_REPLY_ID, + ))) } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/two-party-pol-covenant/src/msg.rs b/contracts/two-party-pol-covenant/src/msg.rs index 612df7b3..bb17e8ef 100644 --- a/contracts/two-party-pol-covenant/src/msg.rs +++ b/contracts/two-party-pol-covenant/src/msg.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Uint128, Uint64, Coin}; -use covenant_two_party_pol_holder::msg::{RagequitConfig}; -use covenant_utils::{ExpiryConfig}; +use covenant_two_party_pol_holder::msg::RagequitConfig; +use covenant_utils::ExpiryConfig; use neutron_sdk::bindings::msg::IbcFee; const NEUTRON_DENOM: &str = "untrn"; @@ -15,11 +15,11 @@ pub struct InstantiateMsg { pub contract_codes: CovenantContractCodeIds, pub clock_tick_max_gas: Option, pub lockup_config: ExpiryConfig, - // pub party_a_config: CovenantPartyConfig, - // pub party_b_config: CovenantPartyConfig, + pub party_a_config: CovenantPartyConfig, + pub party_b_config: CovenantPartyConfig, pub pool_address: String, - // pub ragequit_config: Option, - // pub deposit_deadline: Option, + pub ragequit_config: Option, + pub deposit_deadline: Option, pub party_a_share: Uint64, pub party_b_share: Uint64, } diff --git a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go index abb1bad0..fea4cf23 100644 --- a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go +++ b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go @@ -382,9 +382,9 @@ func TestTwoPartyPol(t *testing.T) { lockupConfig := ExpiryConfig{ BlockHeight: &block, } - // depositDeadline := ExpiryConfig{ - // BlockHeight: &block, - // } + depositDeadline := ExpiryConfig{ + BlockHeight: &block, + } presetIbcFee := PresetIbcFee{ AckFee: "10000", TimeoutFee: "10000", @@ -393,36 +393,36 @@ func TestTwoPartyPol(t *testing.T) { tickMaxGas := "2000" poolAddress := "todo" - // atomCoin := Coin{ - // Denom: "uatom", - // Amount: "10000", - // } - - // osmoCoin := Coin{ - // Denom: "uosmo", - // Amount: "100000", - // } - - // partyAConfig := CovenantPartyConfig{ - // Addr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), - // Contribution: atomCoin, - // IbcDenom: neutronAtomIbcDenom, - // PartyToHostChainChannelId: testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], - // HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], - // PartyReceiverAddr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), - // PartyChainConnectionId: neutronAtomIBCConnId, - // IbcTransferTimeout: timeouts.IbcTransferTimeout, - // } - // partyBConfig := CovenantPartyConfig{ - // Addr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), - // Contribution: osmoCoin, - // IbcDenom: neutronOsmoIbcDenom, - // PartyToHostChainChannelId: testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name], - // HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], - // PartyReceiverAddr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), - // PartyChainConnectionId: neutronOsmosisIBCConnId, - // IbcTransferTimeout: timeouts.IbcTransferTimeout, - // } + atomCoin := Coin{ + Denom: "uatom", + Amount: "10000", + } + + osmoCoin := Coin{ + Denom: "uosmo", + Amount: "100000", + } + + partyAConfig := CovenantPartyConfig{ + Addr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + Contribution: atomCoin, + IbcDenom: neutronAtomIbcDenom, + PartyToHostChainChannelId: testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], + HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], + PartyReceiverAddr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + PartyChainConnectionId: neutronAtomIBCConnId, + IbcTransferTimeout: timeouts.IbcTransferTimeout, + } + partyBConfig := CovenantPartyConfig{ + Addr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + Contribution: osmoCoin, + IbcDenom: neutronOsmoIbcDenom, + PartyToHostChainChannelId: testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name], + HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], + PartyReceiverAddr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + PartyChainConnectionId: neutronOsmosisIBCConnId, + IbcTransferTimeout: timeouts.IbcTransferTimeout, + } codeIds := ContractCodeIds{ IbcForwarderCode: ibcForwarderCodeId, InterchainRouterCode: routerCodeId, @@ -430,13 +430,13 @@ func TestTwoPartyPol(t *testing.T) { HolderCode: holderCodeId, } - // ragequitTerms := RagequitTerms{ - // Penalty: "0.1", - // } + ragequitTerms := RagequitTerms{ + Penalty: "0.1", + } - // ragequitConfig := RagequitConfig{ - // Enabled: &ragequitTerms, - // } + ragequitConfig := RagequitConfig{ + Enabled: &ragequitTerms, + } covenantMsg := CovenantInstantiateMsg{ Label: "two-party-pol-covenant", @@ -445,13 +445,13 @@ func TestTwoPartyPol(t *testing.T) { ContractCodeIds: codeIds, TickMaxGas: &tickMaxGas, LockupConfig: lockupConfig, - // PartyAConfig: partyAConfig, - // PartyBConfig: partyBConfig, - PoolAddress: poolAddress, - // RagequitConfig: &ragequitConfig, - // DepositDeadline: &depositDeadline, - PartyAShare: "50", - PartyBShare: "50", + PartyAConfig: partyAConfig, + PartyBConfig: partyBConfig, + PoolAddress: poolAddress, + RagequitConfig: &ragequitConfig, + DepositDeadline: &depositDeadline, + PartyAShare: "50", + PartyBShare: "50", } str, err := json.Marshal(covenantMsg) require.NoError(t, err, "Failed to marshall CovenantInstantiateMsg") diff --git a/two-party-pol-covenant/tests/interchaintest/types.go b/two-party-pol-covenant/tests/interchaintest/types.go index 31d5f427..8562d3f3 100644 --- a/two-party-pol-covenant/tests/interchaintest/types.go +++ b/two-party-pol-covenant/tests/interchaintest/types.go @@ -6,19 +6,19 @@ package ibc_test // ----- Covenant Instantiation ------ type CovenantInstantiateMsg struct { - Label string `json:"label"` - Timeouts Timeouts `json:"timeouts"` - PresetIbcFee PresetIbcFee `json:"preset_ibc_fee"` - ContractCodeIds ContractCodeIds `json:"contract_codes"` - TickMaxGas *string `json:"clock_tick_max_gas,omitempty"` - LockupConfig ExpiryConfig `json:"lockup_config"` - // PartyAConfig CovenantPartyConfig `json:"party_a_config"` - // PartyBConfig CovenantPartyConfig `json:"party_b_config"` - PoolAddress string `json:"pool_address"` - RagequitConfig *RagequitConfig `json:"ragequit_config,omitempty"` - DepositDeadline *ExpiryConfig `json:"expiry_config,omitempty"` - PartyAShare string `json:"party_a_share"` - PartyBShare string `json:"party_b_share"` + Label string `json:"label"` + Timeouts Timeouts `json:"timeouts"` + PresetIbcFee PresetIbcFee `json:"preset_ibc_fee"` + ContractCodeIds ContractCodeIds `json:"contract_codes"` + TickMaxGas *string `json:"clock_tick_max_gas,omitempty"` + LockupConfig ExpiryConfig `json:"lockup_config"` + PartyAConfig CovenantPartyConfig `json:"party_a_config"` + PartyBConfig CovenantPartyConfig `json:"party_b_config"` + PoolAddress string `json:"pool_address"` + RagequitConfig *RagequitConfig `json:"ragequit_config,omitempty"` + DepositDeadline *ExpiryConfig `json:"deposit_deadline,omitempty"` + PartyAShare string `json:"party_a_share"` + PartyBShare string `json:"party_b_share"` } type ContractCodeIds struct { From acce143e5fe897945a87d972d968653f20ab27cf Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 12 Oct 2023 17:46:25 +0200 Subject: [PATCH 118/586] instantiation chain --- .../two-party-pol-covenant/src/contract.rs | 249 +++++++++++++++++- contracts/two-party-pol-covenant/src/msg.rs | 3 + contracts/two-party-pol-covenant/src/state.rs | 2 + .../two-party-pol-holder/src/contract.rs | 2 +- contracts/two-party-pol-holder/src/msg.rs | 3 +- 5 files changed, 243 insertions(+), 16 deletions(-) diff --git a/contracts/two-party-pol-covenant/src/contract.rs b/contracts/two-party-pol-covenant/src/contract.rs index cccb1541..626ba266 100644 --- a/contracts/two-party-pol-covenant/src/contract.rs +++ b/contracts/two-party-pol-covenant/src/contract.rs @@ -1,14 +1,16 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Decimal, Uint128, CosmosMsg, WasmMsg, SubMsg, Reply, coin, + to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Decimal, Uint128, CosmosMsg, WasmMsg, SubMsg, Reply, }; use covenant_clock::msg::PresetClockFields; use covenant_ibc_forwarder::msg::PresetIbcForwarderFields; use covenant_interchain_router::msg::PresetInterchainRouterFields; -use covenant_two_party_pol_holder::msg::{PresetTwoPartyPolHolderFields, RagequitConfig, PresetPolParty}; +use covenant_lp::state::HOLDER_ADDRESS; +use covenant_two_party_pol_holder::{msg::{PresetTwoPartyPolHolderFields, RagequitConfig, PresetPolParty}, state::POOL_ADDRESS}; use cw2::set_contract_version; +use cw_utils::parse_reply_instantiate_data; use crate::{ error::ContractError, @@ -17,7 +19,7 @@ use crate::{ COVENANT_CLOCK_ADDR, PARTY_A_IBC_FORWARDER_ADDR, PARTY_B_IBC_FORWARDER_ADDR, PRESET_CLOCK_FIELDS, PRESET_HOLDER_FIELDS, - PRESET_PARTY_A_FORWARDER_FIELDS, PRESET_PARTY_B_FORWARDER_FIELDS, COVENANT_POL_HOLDER_ADDR, PRESET_PARTY_A_ROUTER_FIELDS, PRESET_PARTY_B_ROUTER_FIELDS, + PRESET_PARTY_A_FORWARDER_FIELDS, PRESET_PARTY_B_FORWARDER_FIELDS, COVENANT_POL_HOLDER_ADDR, PRESET_PARTY_A_ROUTER_FIELDS, PRESET_PARTY_B_ROUTER_FIELDS, PARTY_A_ROUTER_ADDR, PARTY_B_ROUTER_ADDR, }, }; @@ -65,6 +67,7 @@ pub fn instantiate( allocation: Decimal::from_ratio(msg.party_b_share, Uint128::new(100)), }, code_id: msg.contract_codes.holder_code, + label: format!("{}-holder", msg.label), }; PRESET_HOLDER_FIELDS.save(deps.storage, &preset_holder_fields)?; @@ -127,7 +130,8 @@ pub fn instantiate( .add_submessage(SubMsg::reply_on_success( clock_instantiate_tx, CLOCK_REPLY_ID, - ))) + )) + ) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -140,14 +144,47 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result handle_party_a_ibc_forwarder_reply(deps, env, msg), PARTY_B_FORWARDER_REPLY_ID => handle_party_b_ibc_forwarder_reply(deps, env, msg), _ => Err(ContractError::UnknownReplyId {}), + // _ => Ok(Response::default()) } } pub fn handle_clock_reply(deps: DepsMut, env: Env, msg: Reply) -> Result { deps.api.debug("WASMDEBUG: clock reply"); - Ok(Response::default()) -} + let parsed_data = parse_reply_instantiate_data(msg); + match parsed_data { + Ok(response) => { + // validate and store the clock address + let clock_addr = deps.api.addr_validate(&response.contract_address)?; + COVENANT_CLOCK_ADDR.save(deps.storage, &clock_addr)?; + + let party_a_router_preset_fields = PRESET_PARTY_A_ROUTER_FIELDS.load(deps.storage)?; + + let party_a_router_instantiate_tx: CosmosMsg = CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.to_string()), + code_id: party_a_router_preset_fields.code_id, + msg: to_binary( + &party_a_router_preset_fields.to_instantiate_msg(clock_addr.to_string()), + )?, + funds: vec![], + label: party_a_router_preset_fields.label, + }); + + Ok(Response::default() + .add_attribute("method", "handle_clock_reply") + .add_attribute("clock_addr", clock_addr) + .add_attribute("router_code_id", party_a_router_preset_fields.code_id.to_string()) + .add_attribute("party_a_addr", party_a_router_preset_fields.destination_receiver_addr) + .add_submessage(SubMsg::reply_always( + party_a_router_instantiate_tx, + PARTY_A_ROUTER_REPLY_ID, + ))) + } + Err(err) => Err(ContractError::ContractInstantiationError { + contract: "clock".to_string(), + err, + }), + }} pub fn handle_party_a_interchain_router_reply( deps: DepsMut, @@ -155,8 +192,41 @@ pub fn handle_party_a_interchain_router_reply( msg: Reply, ) -> Result { deps.api.debug("WASMDEBUG: party A interchain router reply"); - Ok(Response::default()) + let parsed_data = parse_reply_instantiate_data(msg); + match parsed_data { + Ok(response) => { + // validate and store the instantiated router address + let router_addr = deps.api.addr_validate(&response.contract_address)?; + PARTY_A_ROUTER_ADDR.save(deps.storage, &router_addr)?; + + // load the fields relevant to router instantiation + let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; + let party_b_router_preset_fields = PRESET_PARTY_B_ROUTER_FIELDS.load(deps.storage)?; + + let party_b_router_instantiate_tx: CosmosMsg = CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.to_string()), + code_id: party_b_router_preset_fields.code_id, + msg: to_binary( + &party_b_router_preset_fields.to_instantiate_msg(clock_addr.to_string()), + )?, + funds: vec![], + label: party_b_router_preset_fields.label, + }); + + Ok(Response::default() + .add_attribute("method", "handle_party_a_interchain_router_reply") + .add_attribute("party_a_interchain_router_addr", router_addr) + .add_submessage(SubMsg::reply_always( + party_b_router_instantiate_tx, + PARTY_B_ROUTER_REPLY_ID, + ))) + } + Err(err) => Err(ContractError::ContractInstantiationError { + contract: "party a router".to_string(), + err, + }), + } } pub fn handle_party_b_interchain_router_reply( @@ -165,8 +235,47 @@ pub fn handle_party_b_interchain_router_reply( msg: Reply, ) -> Result { deps.api.debug("WASMDEBUG: party B interchain router reply"); - Ok(Response::default()) -} + + let parsed_data = parse_reply_instantiate_data(msg); + match parsed_data { + Ok(response) => { + // validate and store the instantiated router address + let router_addr = deps.api.addr_validate(&response.contract_address)?; + PARTY_B_ROUTER_ADDR.save(deps.storage, &router_addr)?; + + let preset_holder_fields = PRESET_HOLDER_FIELDS.load(deps.storage)?; + let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; + let party_a_router = PARTY_A_ROUTER_ADDR.load(deps.storage)?; + + let lper_addr = router_addr.clone(); // TODO: replace with actual lper + let instantiate_msg = preset_holder_fields.clone().to_instantiate_msg( + clock_addr.to_string(), + lper_addr.to_string(), + party_a_router.to_string(), + router_addr.to_string(), + ); + + let holder_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.to_string()), + code_id: preset_holder_fields.code_id, + msg: to_binary(&instantiate_msg)?, + funds: vec![], + label: preset_holder_fields.label, + }); + + Ok(Response::default() + .add_attribute("method", "handle_party_b_interchain_router_reply") + .add_attribute("party_b_interchain_router_addr", router_addr) + .add_submessage(SubMsg::reply_always( + holder_instantiate_tx, + HOLDER_REPLY_ID, + ))) + } + Err(err) => Err(ContractError::ContractInstantiationError { + contract: "party b router".to_string(), + err, + }), + }} pub fn handle_holder_reply( @@ -175,7 +284,40 @@ pub fn handle_holder_reply( msg: Reply, ) -> Result { deps.api.debug("WASMDEBUG: holder reply"); - Ok(Response::default()) + let parsed_data = parse_reply_instantiate_data(msg); + match parsed_data { + Ok(response) => { + // validate and store the instantiated holder address + let holder_addr = deps.api.addr_validate(&response.contract_address)?; + HOLDER_ADDRESS.save(deps.storage, &holder_addr)?; + + // load the fields relevant to router instantiation + let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; + let preset_party_a_ibc_forwarder = PRESET_PARTY_A_ROUTER_FIELDS.load(deps.storage)?; + + let party_a_ibc_forwarder_inst_tx: CosmosMsg = CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.to_string()), + code_id: preset_party_a_ibc_forwarder.code_id, + msg: to_binary( + &preset_party_a_ibc_forwarder.to_instantiate_msg(clock_addr.to_string()), + )?, + funds: vec![], + label: preset_party_a_ibc_forwarder.label, + }); + + Ok(Response::default() + .add_attribute("method", "handle_holder_reply") + .add_attribute("holder_addr", holder_addr) + .add_submessage(SubMsg::reply_always( + party_a_ibc_forwarder_inst_tx, + PARTY_A_FORWARDER_REPLY_ID, + ))) + } + Err(err) => Err(ContractError::ContractInstantiationError { + contract: "holder".to_string(), + err, + }) + } } pub fn handle_party_a_ibc_forwarder_reply( @@ -184,8 +326,40 @@ pub fn handle_party_a_ibc_forwarder_reply( msg: Reply, ) -> Result { deps.api.debug("WASMDEBUG: party A ibc forwarder reply"); - Ok(Response::default()) -} + let parsed_data = parse_reply_instantiate_data(msg); + match parsed_data { + Ok(response) => { + // validate and store the instantiated forwarder address + let forwarder_addr = deps.api.addr_validate(&response.contract_address)?; + PARTY_A_IBC_FORWARDER_ADDR.save(deps.storage, &forwarder_addr)?; + + // load the fields relevant to router instantiation + let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; + let preset_party_b_ibc_forwarder = PRESET_PARTY_B_ROUTER_FIELDS.load(deps.storage)?; + + let party_b_ibc_forwarder_inst_tx: CosmosMsg = CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.to_string()), + code_id: preset_party_b_ibc_forwarder.code_id, + msg: to_binary( + &preset_party_b_ibc_forwarder.to_instantiate_msg(clock_addr.to_string()), + )?, + funds: vec![], + label: preset_party_b_ibc_forwarder.label, + }); + + Ok(Response::default() + .add_attribute("method", "handle_party_a_ibc_forwarder_reply") + .add_attribute("PARTY_A_IBC_FORWARDER_ADDR", forwarder_addr) + .add_submessage(SubMsg::reply_always( + party_b_ibc_forwarder_inst_tx, + PARTY_B_FORWARDER_REPLY_ID, + ))) + } + Err(err) => Err(ContractError::ContractInstantiationError { + contract: "PARTY_A_IBC_FORWARDER_ADDR".to_string(), + err, + }) + }} pub fn handle_party_b_ibc_forwarder_reply( deps: DepsMut, @@ -193,8 +367,45 @@ pub fn handle_party_b_ibc_forwarder_reply( msg: Reply, ) -> Result { deps.api.debug("WASMDEBUG: party B ibc forwarder reply"); - Ok(Response::default()) -} + let parsed_data = parse_reply_instantiate_data(msg); + match parsed_data { + Ok(response) => { + // validate and store the party b ibc forwarder address + let party_b_ibc_forwarder_addr = deps.api.addr_validate(&response.contract_address)?; + PARTY_B_IBC_FORWARDER_ADDR.save(deps.storage, &party_b_ibc_forwarder_addr)?; + + let party_a_forwarder = PARTY_A_IBC_FORWARDER_ADDR.load(deps.storage)?; + let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; + let preset_clock_fields = PRESET_CLOCK_FIELDS.load(deps.storage)?; + let holder = HOLDER_ADDRESS.load(deps.storage)?; + let party_a_router = PARTY_A_ROUTER_ADDR.load(deps.storage)?; + let party_b_router = PARTY_B_ROUTER_ADDR.load(deps.storage)?; + + let update_clock_whitelist_msg = WasmMsg::Migrate { + contract_addr: clock_addr.to_string(), + new_code_id: preset_clock_fields.code_id, + msg: to_binary(&covenant_clock::msg::MigrateMsg::ManageWhitelist { + add: Some(vec![ + party_a_forwarder.to_string(), + party_b_ibc_forwarder_addr.to_string(), + holder.to_string(), + party_a_router.to_string(), + party_b_router.to_string(), + ]), + remove: None, + })?, + }; + + Ok(Response::default() + .add_attribute("method", "handle_party_b_ibc_forwarder_reply") + .add_attribute("party_b_ibc_forwarder_addr", party_b_ibc_forwarder_addr) + .add_message(update_clock_whitelist_msg)) + } + Err(err) => Err(ContractError::ContractInstantiationError { + contract: "party_b ibc forwarder".to_string(), + err, + }), + }} #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { @@ -212,6 +423,16 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { Some(Addr::unchecked("not found")) }; Ok(to_binary(&resp)?) + }, + QueryMsg::RouterAddress { party } => { + let resp = if party == "party_a" { + PARTY_A_ROUTER_ADDR.may_load(deps.storage)? + } else if party == "party_b" { + PARTY_B_ROUTER_ADDR.may_load(deps.storage)? + } else { + Some(Addr::unchecked("not found")) + }; + Ok(to_binary(&resp)?) } } } diff --git a/contracts/two-party-pol-covenant/src/msg.rs b/contracts/two-party-pol-covenant/src/msg.rs index bb17e8ef..344b389d 100644 --- a/contracts/two-party-pol-covenant/src/msg.rs +++ b/contracts/two-party-pol-covenant/src/msg.rs @@ -22,6 +22,7 @@ pub struct InstantiateMsg { pub deposit_deadline: Option, pub party_a_share: Uint64, pub party_b_share: Uint64, + // TODO: lper instantiation fields } #[cw_serde] @@ -104,6 +105,8 @@ pub enum QueryMsg { HolderAddress {}, #[returns(Addr)] IbcForwarderAddress { party: String }, + #[returns(Addr)] + RouterAddress { party: String }, } #[cw_serde] diff --git a/contracts/two-party-pol-covenant/src/state.rs b/contracts/two-party-pol-covenant/src/state.rs index dbe36659..3f630d80 100644 --- a/contracts/two-party-pol-covenant/src/state.rs +++ b/contracts/two-party-pol-covenant/src/state.rs @@ -22,3 +22,5 @@ pub const COVENANT_CLOCK_ADDR: Item = Item::new("covenant_clock_addr"); pub const COVENANT_POL_HOLDER_ADDR: Item = Item::new("covenant_two_party_pol_holder_addr"); pub const PARTY_A_IBC_FORWARDER_ADDR: Item = Item::new("party_a_ibc_forwarder_addr"); pub const PARTY_B_IBC_FORWARDER_ADDR: Item = Item::new("party_b_ibc_forwarder_addr"); +pub const PARTY_A_ROUTER_ADDR: Item = Item::new("party_a_router_addr"); +pub const PARTY_B_ROUTER_ADDR: Item = Item::new("party_b_router_addr"); diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index 9217bd00..b4646698 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -258,7 +258,7 @@ fn try_deposit(deps: DepsMut, env: Env, _info: MessageInfo) -> Result InstantiateMsg { InstantiateMsg { clock_address, - pool_address: self.pool_address, + pool_address: next_contract.to_string(), // todo: replace with actual pool next_contract, lockup_config: self.lockup_config, ragequit_config: self.ragequit_config, From ffea9fa11f6e3c83aadba0cf5eb6de994e8f03e0 Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 13 Oct 2023 14:19:14 +0200 Subject: [PATCH 119/586] interchaintest instantiation chain addr queries; adding migrate templates to forwarder, holder --- contracts/ibc-forwarder/src/contract.rs | 9 +++- contracts/ibc-forwarder/src/msg.rs | 3 ++ .../two-party-pol-covenant/src/contract.rs | 31 +++++------ contracts/two-party-pol-covenant/src/msg.rs | 2 +- .../two-party-pol-holder/src/contract.rs | 8 ++- contracts/two-party-pol-holder/src/msg.rs | 5 ++ .../interchaintest/two_party_pol_test.go | 53 +++++++++++++++++++ 7 files changed, 93 insertions(+), 18 deletions(-) diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index 3c9e0aea..a8d4ec25 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -19,7 +19,7 @@ use neutron_sdk::{ }; use crate::{ - msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg}, + msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, MigrateMsg}, state::{ CLOCK_ADDRESS, CONTRACT_STATE, INTERCHAIN_ACCOUNTS, NEXT_CONTRACT, REMOTE_CHAIN_INFO, REPLY_ID_STORAGE, SUDO_PAYLOAD, TRANSFER_AMOUNT, @@ -403,3 +403,10 @@ pub fn save_sudo_payload( ) -> StdResult<()> { SUDO_PAYLOAD.save(store, (channel_id, seq_id), &to_vec(&payload)?) } + + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { + deps.api.debug("WASMDEBUG: migrate"); + unimplemented!(); +} \ No newline at end of file diff --git a/contracts/ibc-forwarder/src/msg.rs b/contracts/ibc-forwarder/src/msg.rs index e0da20ce..613ec528 100644 --- a/contracts/ibc-forwarder/src/msg.rs +++ b/contracts/ibc-forwarder/src/msg.rs @@ -96,6 +96,9 @@ impl InstantiateMsg { #[cw_serde] pub enum ExecuteMsg {} +#[cw_serde] +pub enum MigrateMsg {} + #[covenant_deposit_address] #[covenant_remote_chain] #[covenant_clock_address] diff --git a/contracts/two-party-pol-covenant/src/contract.rs b/contracts/two-party-pol-covenant/src/contract.rs index 626ba266..a45586c6 100644 --- a/contracts/two-party-pol-covenant/src/contract.rs +++ b/contracts/two-party-pol-covenant/src/contract.rs @@ -7,14 +7,13 @@ use cosmwasm_std::{ use covenant_clock::msg::PresetClockFields; use covenant_ibc_forwarder::msg::PresetIbcForwarderFields; use covenant_interchain_router::msg::PresetInterchainRouterFields; -use covenant_lp::state::HOLDER_ADDRESS; -use covenant_two_party_pol_holder::{msg::{PresetTwoPartyPolHolderFields, RagequitConfig, PresetPolParty}, state::POOL_ADDRESS}; +use covenant_two_party_pol_holder::msg::{PresetTwoPartyPolHolderFields, RagequitConfig, PresetPolParty}; use cw2::set_contract_version; use cw_utils::parse_reply_instantiate_data; use crate::{ error::ContractError, - msg::{InstantiateMsg, QueryMsg}, + msg::{InstantiateMsg, QueryMsg, MigrateMsg}, state::{ COVENANT_CLOCK_ADDR, PARTY_A_IBC_FORWARDER_ADDR, PARTY_B_IBC_FORWARDER_ADDR, @@ -43,14 +42,13 @@ pub fn instantiate( ) -> Result { deps.api.debug("WASMDEBUG: instantiate"); set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + let preset_clock_fields = PresetClockFields { tick_max_gas: msg.clock_tick_max_gas, whitelist: vec![], code_id: msg.contract_codes.clock_code, label: format!("{}-clock", msg.label), }; - PRESET_CLOCK_FIELDS.save(deps.storage, &preset_clock_fields)?; - let preset_holder_fields = PresetTwoPartyPolHolderFields { lockup_config:msg.lockup_config, pool_address: msg.pool_address, @@ -69,9 +67,6 @@ pub fn instantiate( code_id: msg.contract_codes.holder_code, label: format!("{}-holder", msg.label), }; - PRESET_HOLDER_FIELDS.save(deps.storage, &preset_holder_fields)?; - - let preset_party_a_forwarder_fields = PresetIbcForwarderFields { remote_chain_connection_id: msg.party_a_config.party_chain_connection_id, remote_chain_channel_id: msg.party_a_config.party_to_host_chain_channel_id, @@ -94,9 +89,6 @@ pub fn instantiate( ibc_transfer_timeout: msg.timeouts.ibc_transfer_timeout, ibc_fee: msg.preset_ibc_fee.to_ibc_fee(), }; - - PRESET_PARTY_A_FORWARDER_FIELDS.save(deps.storage, &preset_party_a_forwarder_fields)?; - PRESET_PARTY_B_FORWARDER_FIELDS.save(deps.storage, &preset_party_b_forwarder_fields)?; let preset_party_a_router_fields = PresetInterchainRouterFields { destination_chain_channel_id: msg.party_a_config.host_to_party_chain_channel_id, @@ -113,6 +105,10 @@ pub fn instantiate( code_id: msg.contract_codes.router_code, }; + PRESET_CLOCK_FIELDS.save(deps.storage, &preset_clock_fields)?; + PRESET_HOLDER_FIELDS.save(deps.storage, &preset_holder_fields)?; + PRESET_PARTY_A_FORWARDER_FIELDS.save(deps.storage, &preset_party_a_forwarder_fields)?; + PRESET_PARTY_B_FORWARDER_FIELDS.save(deps.storage, &preset_party_b_forwarder_fields)?; PRESET_PARTY_A_ROUTER_FIELDS.save(deps.storage, &preset_party_a_router_fields)?; PRESET_PARTY_B_ROUTER_FIELDS.save(deps.storage, &preset_party_b_router_fields)?; @@ -144,7 +140,6 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result handle_party_a_ibc_forwarder_reply(deps, env, msg), PARTY_B_FORWARDER_REPLY_ID => handle_party_b_ibc_forwarder_reply(deps, env, msg), _ => Err(ContractError::UnknownReplyId {}), - // _ => Ok(Response::default()) } } @@ -289,7 +284,7 @@ pub fn handle_holder_reply( Ok(response) => { // validate and store the instantiated holder address let holder_addr = deps.api.addr_validate(&response.contract_address)?; - HOLDER_ADDRESS.save(deps.storage, &holder_addr)?; + COVENANT_POL_HOLDER_ADDR.save(deps.storage, &holder_addr)?; // load the fields relevant to router instantiation let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; @@ -377,7 +372,7 @@ pub fn handle_party_b_ibc_forwarder_reply( let party_a_forwarder = PARTY_A_IBC_FORWARDER_ADDR.load(deps.storage)?; let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; let preset_clock_fields = PRESET_CLOCK_FIELDS.load(deps.storage)?; - let holder = HOLDER_ADDRESS.load(deps.storage)?; + let holder = COVENANT_POL_HOLDER_ADDR.load(deps.storage)?; let party_a_router = PARTY_A_ROUTER_ADDR.load(deps.storage)?; let party_b_router = PARTY_B_ROUTER_ADDR.load(deps.storage)?; @@ -424,7 +419,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { }; Ok(to_binary(&resp)?) }, - QueryMsg::RouterAddress { party } => { + QueryMsg::InterchainRouterAddress { party } => { let resp = if party == "party_a" { PARTY_A_ROUTER_ADDR.may_load(deps.storage)? } else if party == "party_b" { @@ -437,3 +432,9 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { } } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { + deps.api.debug("WASMDEBUG: migrate"); + unimplemented!(); +} \ No newline at end of file diff --git a/contracts/two-party-pol-covenant/src/msg.rs b/contracts/two-party-pol-covenant/src/msg.rs index 344b389d..0c1d9cb5 100644 --- a/contracts/two-party-pol-covenant/src/msg.rs +++ b/contracts/two-party-pol-covenant/src/msg.rs @@ -106,7 +106,7 @@ pub enum QueryMsg { #[returns(Addr)] IbcForwarderAddress { party: String }, #[returns(Addr)] - RouterAddress { party: String }, + InterchainRouterAddress { party: String }, } #[cw_serde] diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index b4646698..d57550d6 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -12,7 +12,7 @@ use cw20::{BalanceResponse, Cw20ExecuteMsg}; use crate::{ error::ContractError, - msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, RagequitConfig, RagequitState}, + msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, RagequitConfig, RagequitState, MigrateMsg}, state::{ CLOCK_ADDRESS, CONTRACT_STATE, COVENANT_CONFIG, DEPOSIT_DEADLINE, LOCKUP_CONFIG, LP_TOKEN, NEXT_CONTRACT, POOL_ADDRESS, RAGEQUIT_CONFIG, @@ -402,3 +402,9 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::Config {} => Ok(to_binary(&COVENANT_CONFIG.load(deps.storage)?)?), } } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { + deps.api.debug("WASMDEBUG: migrate"); + unimplemented!(); +} \ No newline at end of file diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index 7260a820..5d895f9e 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -153,6 +153,11 @@ pub enum ExecuteMsg { Claim {}, } +#[cw_serde] +pub enum MigrateMsg { +} + + #[cw_serde] pub enum ContractState { /// contract is instantiated and awaiting for deposits from diff --git a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go index fea4cf23..14ba22be 100644 --- a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go +++ b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go @@ -500,5 +500,58 @@ func TestTwoPartyPol(t *testing.T) { println("covenant address: ", covenantAddress) }) + t.Run("query covenant contracts", func(t *testing.T) { + routerQueryPartyA := InterchainRouterQuery{ + Party: Party{ + Party: "party_a", + }, + } + routerQueryPartyB := InterchainRouterQuery{ + Party: Party{ + Party: "party_b", + }, + } + forwarderQueryPartyA := IbcForwarderQuery{ + Party: Party{ + Party: "party_a", + }, + } + forwarderQueryPartyB := IbcForwarderQuery{ + Party: Party{ + Party: "party_b", + }, + } + var response CovenantAddressQueryResponse + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, ClockAddressQuery{}, &response) + require.NoError(t, err, "failed to query instantiated clock address") + clockAddress = response.Data + println("clock addr: ", clockAddress) + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, HolderAddressQuery{}, &response) + require.NoError(t, err, "failed to query instantiated holder address") + holderAddress = response.Data + println("holder addr: ", holderAddress) + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, routerQueryPartyA, &response) + require.NoError(t, err, "failed to query instantiated party a router address") + partyARouterAddress = response.Data + println("partyARouterAddress: ", partyARouterAddress) + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, routerQueryPartyB, &response) + require.NoError(t, err, "failed to query instantiated party b router address") + partyBRouterAddress = response.Data + println("partyBRouterAddress: ", partyBRouterAddress) + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, forwarderQueryPartyA, &response) + require.NoError(t, err, "failed to query instantiated party a forwarder address") + partyAIbcForwarderAddress = response.Data + println("partyAIbcForwarderAddress: ", partyAIbcForwarderAddress) + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, forwarderQueryPartyB, &response) + require.NoError(t, err, "failed to query instantiated party b forwarder address") + partyBIbcForwarderAddress = response.Data + println("partyBIbcForwarderAddress: ", partyBIbcForwarderAddress) + }) }) } From ea3774e714229c67abecb45bedf683e0c7d28a59 Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 13 Oct 2023 17:16:55 +0200 Subject: [PATCH 120/586] ictests until ica creation --- .../two-party-pol-covenant/src/contract.rs | 9 +- .../interchaintest/two_party_pol_test.go | 195 ++++++++++++++++++ 2 files changed, 200 insertions(+), 4 deletions(-) diff --git a/contracts/two-party-pol-covenant/src/contract.rs b/contracts/two-party-pol-covenant/src/contract.rs index a45586c6..58e194ea 100644 --- a/contracts/two-party-pol-covenant/src/contract.rs +++ b/contracts/two-party-pol-covenant/src/contract.rs @@ -288,13 +288,13 @@ pub fn handle_holder_reply( // load the fields relevant to router instantiation let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; - let preset_party_a_ibc_forwarder = PRESET_PARTY_A_ROUTER_FIELDS.load(deps.storage)?; + let preset_party_a_ibc_forwarder = PRESET_PARTY_A_FORWARDER_FIELDS.load(deps.storage)?; let party_a_ibc_forwarder_inst_tx: CosmosMsg = CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(env.contract.address.to_string()), code_id: preset_party_a_ibc_forwarder.code_id, msg: to_binary( - &preset_party_a_ibc_forwarder.to_instantiate_msg(clock_addr.to_string()), + &preset_party_a_ibc_forwarder.to_instantiate_msg(clock_addr.to_string(), holder_addr.to_string()), )?, funds: vec![], label: preset_party_a_ibc_forwarder.label, @@ -327,16 +327,17 @@ pub fn handle_party_a_ibc_forwarder_reply( // validate and store the instantiated forwarder address let forwarder_addr = deps.api.addr_validate(&response.contract_address)?; PARTY_A_IBC_FORWARDER_ADDR.save(deps.storage, &forwarder_addr)?; + let holder = COVENANT_POL_HOLDER_ADDR.load(deps.storage)?; // load the fields relevant to router instantiation let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; - let preset_party_b_ibc_forwarder = PRESET_PARTY_B_ROUTER_FIELDS.load(deps.storage)?; + let preset_party_b_ibc_forwarder = PRESET_PARTY_B_FORWARDER_FIELDS.load(deps.storage)?; let party_b_ibc_forwarder_inst_tx: CosmosMsg = CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(env.contract.address.to_string()), code_id: preset_party_b_ibc_forwarder.code_id, msg: to_binary( - &preset_party_b_ibc_forwarder.to_instantiate_msg(clock_addr.to_string()), + &preset_party_b_ibc_forwarder.to_instantiate_msg(clock_addr.to_string(), holder.to_string()), )?, funds: vec![], label: preset_party_b_ibc_forwarder.label, diff --git a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go index 14ba22be..3a6c3ff3 100644 --- a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go +++ b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go @@ -553,5 +553,200 @@ func TestTwoPartyPol(t *testing.T) { partyBIbcForwarderAddress = response.Data println("partyBIbcForwarderAddress: ", partyBIbcForwarderAddress) }) + + t.Run("fund contracts with neutron", func(t *testing.T) { + err := neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: partyAIbcForwarderAddress, + Amount: 5000001, + Denom: nativeNtrnDenom, + }) + + require.NoError(t, err, "failed to send funds from neutron user to partyAIbcForwarder contract") + + err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: partyBIbcForwarderAddress, + Amount: 5000001, + Denom: nativeNtrnDenom, + }) + require.NoError(t, err, "failed to send funds from neutron user to partyBIbcForwarder contract") + + err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: clockAddress, + Amount: 5000001, + Denom: nativeNtrnDenom, + }) + require.NoError(t, err, "failed to send funds from neutron user to clock contract") + err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: partyARouterAddress, + Amount: 15000001, + Denom: nativeNtrnDenom, + }) + require.NoError(t, err, "failed to send funds from neutron user to party a router") + err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: partyBRouterAddress, + Amount: 15000001, + Denom: nativeNtrnDenom, + }) + require.NoError(t, err, "failed to send funds from neutron user to party b router") + + err = testutil.WaitForBlocks(ctx, 2, atom, neutron) + require.NoError(t, err, "failed to wait for blocks") + + bal, err := neutron.GetBalance(ctx, partyAIbcForwarderAddress, nativeNtrnDenom) + require.NoError(t, err) + require.Equal(t, int64(5000001), bal) + bal, err = neutron.GetBalance(ctx, partyBIbcForwarderAddress, nativeNtrnDenom) + require.NoError(t, err) + require.Equal(t, int64(5000001), bal) + bal, err = neutron.GetBalance(ctx, clockAddress, nativeNtrnDenom) + require.NoError(t, err) + require.Equal(t, int64(5000001), bal) + bal, err = neutron.GetBalance(ctx, partyARouterAddress, nativeNtrnDenom) + require.NoError(t, err) + require.Equal(t, int64(15000001), bal) + bal, err = neutron.GetBalance(ctx, partyBRouterAddress, nativeNtrnDenom) + require.NoError(t, err) + require.Equal(t, int64(15000001), bal) + }) + + t.Run("two party POL", func(t *testing.T) { + + tickClock := func() { + println("\ntick") + cmd := []string{"neutrond", "tx", "wasm", "execute", clockAddress, + `{"tick":{}}`, + "--from", neutronUser.KeyName, + "--gas-prices", "0.0untrn", + "--gas-adjustment", `1.8`, + "--output", "json", + "--home", "/var/cosmos-chain/neutron-2", + "--node", neutron.GetRPCAddress(), + "--home", neutron.HomeDir(), + "--chain-id", neutron.Config().ChainID, + "--from", neutronUser.KeyName, + "--gas", "auto", + "--keyring-backend", keyring.BackendTest, + "-y", + } + + _, _, err := cosmosNeutron.Exec(ctx, cmd, nil) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + } + + t.Run("tick until forwarders create ICA", func(t *testing.T) { + for { + tickClock() + var response CovenantAddressQueryResponse + type ContractState struct{} + type ContractStateQuery struct { + ContractState ContractState `json:"contract_state"` + } + contractStateQuery := ContractStateQuery{ + ContractState: ContractState{}, + } + + require.NoError(t, + cosmosNeutron.QueryContract(ctx, partyAIbcForwarderAddress, contractStateQuery, &response), + "failed to query forwarder A state") + forwarderAState := response.Data + + require.NoError(t, + cosmosNeutron.QueryContract(ctx, partyBIbcForwarderAddress, contractStateQuery, &response), + "failed to query forwarder B state") + + forwarderBState := response.Data + + if forwarderAState == forwarderBState && forwarderBState == "ica_created" { + type DepositAddress struct{} + type DepositAddressQuery struct { + DepositAddress DepositAddress `json:"deposit_address"` + } + depositAddressQuery := DepositAddressQuery{ + DepositAddress: DepositAddress{}, + } + + err := cosmosNeutron.QueryContract(ctx, partyAIbcForwarderAddress, depositAddressQuery, &response) + require.NoError(t, err, "failed to query party a forwarder deposit address") + partyADepositAddress = response.Data + + err = cosmosNeutron.QueryContract(ctx, partyBIbcForwarderAddress, depositAddressQuery, &response) + require.NoError(t, err, "failed to query party b forwarder deposit address") + partyBDepositAddress = response.Data + println("both parties icas created") + break + } + } + }) + + t.Run("fund the forwarders with sufficient funds", func(t *testing.T) { + err := cosmosOsmosis.SendFunds(ctx, osmoUser.KeyName, ibc.WalletAmount{ + Address: partyBDepositAddress, + Denom: nativeOsmoDenom, + Amount: int64(osmoContributionAmount + 1000), + }) + require.NoError(t, err, "failed to fund osmo forwarder") + err = cosmosAtom.SendFunds(ctx, gaiaUser.KeyName, ibc.WalletAmount{ + Address: partyADepositAddress, + Denom: nativeAtomDenom, + Amount: int64(atomContributionAmount + 1000), + }) + require.NoError(t, err, "failed to fund gaia forwarder") + + err = testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + + bal, err := cosmosAtom.GetBalance(ctx, partyADepositAddress, nativeAtomDenom) + require.NoError(t, err, "failed to query bal") + require.Equal(t, int64(atomContributionAmount+1000), bal) + bal, err = cosmosOsmosis.GetBalance(ctx, partyBDepositAddress, nativeOsmoDenom) + require.NoError(t, err, "failed to query bal") + require.Equal(t, int64(osmoContributionAmount+1000), bal) + }) + + t.Run("tick until forwarders forward the funds to holder", func(t *testing.T) { + for { + holderOsmoBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronOsmoIbcDenom) + require.NoError(t, err, "failed to query holder osmo bal") + holderAtomBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronAtomIbcDenom) + require.NoError(t, err, "failed to query holder atom bal") + + var response CovenantAddressQueryResponse + type ContractState struct{} + type ContractStateQuery struct { + ContractState ContractState `json:"contract_state"` + } + contractStateQuery := ContractStateQuery{ + ContractState: ContractState{}, + } + + require.NoError(t, + cosmosNeutron.QueryContract(ctx, holderAddress, contractStateQuery, &response), + "failed to query forwarder A state") + holderState := response.Data + + if holderAtomBal != 0 && holderOsmoBal != 0 || holderState == "complete" { + println("holder atom bal: ", holderAtomBal) + println("holder osmo bal: ", holderOsmoBal) + break + } else { + tickClock() + } + } + }) + + t.Run("tick until holder sends the funds to LPer", func(t *testing.T) { + // TODO + }) + + t.Run("tick until holder receives LP tokens", func(t *testing.T) { + // TODO + }) + + t.Run("tick until routers route the funds after POL expires", func(t *testing.T) { + // TODO + }) + }) }) } From 6d4ec535cb49022946e7e96c648935d538322ecd Mon Sep 17 00:00:00 2001 From: bekauz Date: Sun, 15 Oct 2023 06:00:02 +0200 Subject: [PATCH 121/586] migrations two party pol --- contracts/ibc-forwarder/src/contract.rs | 33 +++++++- contracts/ibc-forwarder/src/msg.rs | 13 ++- .../two-party-pol-covenant/src/contract.rs | 83 ++++++++++++++++++- contracts/two-party-pol-covenant/src/msg.rs | 5 ++ .../two-party-pol-holder/src/contract.rs | 66 ++++++++++++++- contracts/two-party-pol-holder/src/msg.rs | 15 +++- packages/covenant-utils/src/lib.rs | 2 +- 7 files changed, 209 insertions(+), 8 deletions(-) diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index a8d4ec25..1f215a6a 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -408,5 +408,36 @@ pub fn save_sudo_payload( #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { deps.api.debug("WASMDEBUG: migrate"); - unimplemented!(); + match msg { + MigrateMsg::UpdateConfig { + clock_addr, + next_contract, + remote_chain_info, + } => { + let mut resp = Response::default().add_attribute("method", "update_config"); + + if let Some(addr) = clock_addr { + let clock_address = deps.api.addr_validate(&addr)?; + CLOCK_ADDRESS.save(deps.storage, &clock_address)?; + resp = resp.add_attribute("clock_addr", addr); + } + + if let Some(addr) = next_contract { + let next_contract_addr = deps.api.addr_validate(&addr)?; + NEXT_CONTRACT.save(deps.storage, &next_contract_addr)?; + resp = resp.add_attribute("next_contract", addr); + } + + if let Some(rci) = remote_chain_info { + let validated_rci = rci.validate()?; + REMOTE_CHAIN_INFO.save(deps.storage, &validated_rci)?; + resp = resp.add_attributes(validated_rci.get_response_attributes()); + } + + Ok(resp) + }, + MigrateMsg::UpdateCodeId { data } => { + unimplemented!() + }, + } } \ No newline at end of file diff --git a/contracts/ibc-forwarder/src/msg.rs b/contracts/ibc-forwarder/src/msg.rs index 613ec528..099656f5 100644 --- a/contracts/ibc-forwarder/src/msg.rs +++ b/contracts/ibc-forwarder/src/msg.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Attribute, Uint128, Uint64}; +use cosmwasm_std::{Addr, Attribute, Uint128, Uint64, Binary}; use covenant_macros::{ clocked, covenant_clock_address, covenant_deposit_address, covenant_ica_address, covenant_remote_chain, @@ -97,7 +97,16 @@ impl InstantiateMsg { pub enum ExecuteMsg {} #[cw_serde] -pub enum MigrateMsg {} +pub enum MigrateMsg { + UpdateConfig { + clock_addr: Option, + next_contract: Option, + remote_chain_info: Option, + }, + UpdateCodeId { + data: Option, + }, +} #[covenant_deposit_address] #[covenant_remote_chain] diff --git a/contracts/two-party-pol-covenant/src/contract.rs b/contracts/two-party-pol-covenant/src/contract.rs index 58e194ea..6ec5eb8a 100644 --- a/contracts/two-party-pol-covenant/src/contract.rs +++ b/contracts/two-party-pol-covenant/src/contract.rs @@ -437,5 +437,86 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { deps.api.debug("WASMDEBUG: migrate"); - unimplemented!(); + match msg { + MigrateMsg::MigrateContracts { + clock, + holder, + party_a_router, + party_b_router, + party_a_forwarder, + party_b_forwarder, + } => { + let mut migrate_msgs = vec![]; + let mut resp = Response::default().add_attribute("method", "migrate_contracts"); + + if let Some(clock) = clock { + let msg = to_binary(&clock)?; + let clock_fields = PRESET_CLOCK_FIELDS.load(deps.storage)?; + resp = resp.add_attribute("clock_migrate", msg.to_base64()); + migrate_msgs.push(WasmMsg::Migrate { + contract_addr: COVENANT_CLOCK_ADDR.load(deps.storage)?.to_string(), + new_code_id: clock_fields.code_id, + msg, + }); + } + + if let Some(router) = party_a_router { + let msg: Binary = to_binary(&router)?; + let router_fields = PRESET_PARTY_A_ROUTER_FIELDS.load(deps.storage)?; + resp = resp.add_attribute("party_a_router_migrate", msg.to_base64()); + migrate_msgs.push(WasmMsg::Migrate { + contract_addr: PARTY_A_ROUTER_ADDR.load(deps.storage)?.to_string(), + new_code_id: router_fields.code_id, + msg, + }); + } + + if let Some(router) = party_b_router { + let msg: Binary = to_binary(&router)?; + let router_fields = PRESET_PARTY_B_ROUTER_FIELDS.load(deps.storage)?; + resp = resp.add_attribute("party_b_router_migrate", msg.to_base64()); + migrate_msgs.push(WasmMsg::Migrate { + contract_addr: PARTY_B_ROUTER_ADDR.load(deps.storage)?.to_string(), + new_code_id: router_fields.code_id, + msg, + }); + } + + if let Some(forwarder) = party_a_forwarder { + let msg: Binary = to_binary(&forwarder)?; + let forwarder_fields = PRESET_PARTY_A_FORWARDER_FIELDS.load(deps.storage)?; + resp = resp.add_attribute("party_a_forwarder_migrate", msg.to_base64()); + migrate_msgs.push(WasmMsg::Migrate { + contract_addr: PARTY_A_IBC_FORWARDER_ADDR.load(deps.storage)?.to_string(), + new_code_id: forwarder_fields.code_id, + msg, + }); + } + + if let Some(forwarder) = party_b_forwarder { + let msg: Binary = to_binary(&forwarder)?; + let forwarder_fields = PRESET_PARTY_B_FORWARDER_FIELDS.load(deps.storage)?; + resp = resp.add_attribute("party_b_forwarder_migrate", msg.to_base64()); + migrate_msgs.push(WasmMsg::Migrate { + contract_addr: PARTY_B_IBC_FORWARDER_ADDR.load(deps.storage)?.to_string(), + new_code_id: forwarder_fields.code_id, + msg, + }); + } + + if let Some(holder) = holder { + let msg: Binary = to_binary(&holder)?; + let holder_fields = PRESET_HOLDER_FIELDS.load(deps.storage)?; + resp = resp.add_attribute("holder_migrate", msg.to_base64()); + migrate_msgs.push(WasmMsg::Migrate { + contract_addr: COVENANT_POL_HOLDER_ADDR.load(deps.storage)?.to_string(), + new_code_id: holder_fields.code_id, + msg, + }); + } + + + Ok(resp.add_messages(migrate_msgs)) + }, + } } \ No newline at end of file diff --git a/contracts/two-party-pol-covenant/src/msg.rs b/contracts/two-party-pol-covenant/src/msg.rs index 0c1d9cb5..a805fd97 100644 --- a/contracts/two-party-pol-covenant/src/msg.rs +++ b/contracts/two-party-pol-covenant/src/msg.rs @@ -113,5 +113,10 @@ pub enum QueryMsg { pub enum MigrateMsg { MigrateContracts { clock: Option, + holder: Option, + party_a_router: Option, + party_b_router: Option, + party_a_forwarder: Option, + party_b_forwarder: Option, }, } diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index d57550d6..1ac775b8 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -404,7 +404,69 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult { +pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> StdResult { deps.api.debug("WASMDEBUG: migrate"); - unimplemented!(); + match msg { + MigrateMsg::UpdateConfig { + clock_addr, + next_contract, + lockup_config, + deposit_deadline, + pool_address, + ragequit_config, + lp_token, + covenant_config, + } => { + let mut resp = Response::default().add_attribute("method", "update_config"); + + if let Some(addr) = clock_addr { + let clock_address = deps.api.addr_validate(&addr)?; + CLOCK_ADDRESS.save(deps.storage, &clock_address)?; + resp = resp.add_attribute("clock_addr", addr); + } + + if let Some(addr) = next_contract { + let next_contract_addr = deps.api.addr_validate(&addr)?; + NEXT_CONTRACT.save(deps.storage, &next_contract_addr)?; + resp = resp.add_attribute("next_contract", addr); + } + + if let Some(expiry_config) = lockup_config { + expiry_config.validate(&env.block)?; + LOCKUP_CONFIG.save(deps.storage, &expiry_config)?; + resp = resp.add_attributes(expiry_config.get_response_attributes()); + } + + if let Some(expiry_config) = deposit_deadline { + expiry_config.validate(&env.block)?; + DEPOSIT_DEADLINE.save(deps.storage, &expiry_config)?; + resp = resp.add_attributes(expiry_config.get_response_attributes()); + } + + if let Some(addr) = pool_address { + let pool_addr = deps.api.addr_validate(&addr)?; + POOL_ADDRESS.save(deps.storage, &pool_addr)?; + resp = resp.add_attribute("pool_addr", pool_addr); + } + + if let Some(addr) = lp_token { + let lp_addr = deps.api.addr_validate(&addr)?; + LP_TOKEN.save(deps.storage, &lp_addr)?; + resp = resp.add_attribute("lp_token", lp_addr); + } + + if let Some(config) = ragequit_config { + RAGEQUIT_CONFIG.save(deps.storage, &config)?; + resp = resp.add_attributes(config.get_response_attributes()); + } + + if let Some(config) = covenant_config { + COVENANT_CONFIG.save(deps.storage, &config)?; + resp = resp.add_attribute("todo", "todo"); + } + + Ok(resp) + }, + MigrateMsg::UpdateCodeId { data } => todo!(), + } } \ No newline at end of file diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index 5d895f9e..9752c31b 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -2,7 +2,7 @@ use std::fmt; use astroport::asset::Asset; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Api, Attribute, Coin, Decimal, StdError}; +use cosmwasm_std::{Addr, Api, Attribute, Coin, Decimal, StdError, Binary}; use covenant_macros::{clocked, covenant_clock_address, covenant_next_contract}; use covenant_utils::ExpiryConfig; @@ -155,6 +155,19 @@ pub enum ExecuteMsg { #[cw_serde] pub enum MigrateMsg { + UpdateConfig { + clock_addr: Option, + next_contract: Option, + lockup_config: Option, + deposit_deadline: Option, + pool_address: Option, + ragequit_config: Option, + lp_token: Option, + covenant_config: Option, + }, + UpdateCodeId { + data: Option, + }, } diff --git a/packages/covenant-utils/src/lib.rs b/packages/covenant-utils/src/lib.rs index f28fb59a..1430072e 100644 --- a/packages/covenant-utils/src/lib.rs +++ b/packages/covenant-utils/src/lib.rs @@ -180,7 +180,7 @@ pub enum ExpiryConfig { } impl ExpiryConfig { - pub fn get_response_attributes(self) -> Vec { + pub fn get_response_attributes(&self) -> Vec { match self { ExpiryConfig::None => vec![Attribute::new("expiry_config", "none")], ExpiryConfig::Block(h) => vec![Attribute::new( From 952027676605cf8402f6ffc36d72b3f07e9354df Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 16 Oct 2023 22:53:07 +0200 Subject: [PATCH 122/586] ictest funding deposit ICAs --- contracts/ibc-forwarder/src/contract.rs | 1 - .../interchaintest/two_party_pol_test.go | 48 ++++++++++--------- .../tests/interchaintest/types.go | 2 +- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index 1f215a6a..d92b9015 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -96,7 +96,6 @@ fn try_tick(deps: ExecuteDeps, env: Env, info: MessageInfo) -> NeutronResult NeutronResult> { let remote_chain_info = REMOTE_CHAIN_INFO.load(deps.storage)?; - let register_msg = NeutronMsg::register_interchain_account( remote_chain_info.connection_id, INTERCHAIN_ACCOUNT_ID.to_string(), diff --git a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go index 3a6c3ff3..4bc00350 100644 --- a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go +++ b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go @@ -390,17 +390,17 @@ func TestTwoPartyPol(t *testing.T) { TimeoutFee: "10000", } - tickMaxGas := "2000" + // tickMaxGas := "2000" poolAddress := "todo" atomCoin := Coin{ Denom: "uatom", - Amount: "10000", + Amount: "5000000000", } osmoCoin := Coin{ Denom: "uosmo", - Amount: "100000", + Amount: "100000000000", } partyAConfig := CovenantPartyConfig{ @@ -443,7 +443,6 @@ func TestTwoPartyPol(t *testing.T) { Timeouts: timeouts, PresetIbcFee: presetIbcFee, ContractCodeIds: codeIds, - TickMaxGas: &tickMaxGas, LockupConfig: lockupConfig, PartyAConfig: partyAConfig, PartyBConfig: partyBConfig, @@ -467,7 +466,7 @@ func TestTwoPartyPol(t *testing.T) { "--home", neutron.HomeDir(), "--node", neutron.GetRPCAddress(), "--chain-id", neutron.Config().ChainID, - "--gas", "90009000", + "--gas", "9000900", "--keyring-backend", keyring.BackendTest, "-y", } @@ -557,7 +556,7 @@ func TestTwoPartyPol(t *testing.T) { t.Run("fund contracts with neutron", func(t *testing.T) { err := neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ Address: partyAIbcForwarderAddress, - Amount: 5000001, + Amount: 50000001, Denom: nativeNtrnDenom, }) @@ -565,48 +564,48 @@ func TestTwoPartyPol(t *testing.T) { err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ Address: partyBIbcForwarderAddress, - Amount: 5000001, + Amount: 50000001, Denom: nativeNtrnDenom, }) require.NoError(t, err, "failed to send funds from neutron user to partyBIbcForwarder contract") err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ Address: clockAddress, - Amount: 5000001, + Amount: 50000001, Denom: nativeNtrnDenom, }) require.NoError(t, err, "failed to send funds from neutron user to clock contract") err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ Address: partyARouterAddress, - Amount: 15000001, + Amount: 150000001, Denom: nativeNtrnDenom, }) require.NoError(t, err, "failed to send funds from neutron user to party a router") err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ Address: partyBRouterAddress, - Amount: 15000001, + Amount: 150000001, Denom: nativeNtrnDenom, }) require.NoError(t, err, "failed to send funds from neutron user to party b router") - err = testutil.WaitForBlocks(ctx, 2, atom, neutron) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, osmosis) require.NoError(t, err, "failed to wait for blocks") bal, err := neutron.GetBalance(ctx, partyAIbcForwarderAddress, nativeNtrnDenom) require.NoError(t, err) - require.Equal(t, int64(5000001), bal) + require.Equal(t, int64(50000001), bal) bal, err = neutron.GetBalance(ctx, partyBIbcForwarderAddress, nativeNtrnDenom) require.NoError(t, err) - require.Equal(t, int64(5000001), bal) + require.Equal(t, int64(50000001), bal) bal, err = neutron.GetBalance(ctx, clockAddress, nativeNtrnDenom) require.NoError(t, err) - require.Equal(t, int64(5000001), bal) + require.Equal(t, int64(50000001), bal) bal, err = neutron.GetBalance(ctx, partyARouterAddress, nativeNtrnDenom) require.NoError(t, err) - require.Equal(t, int64(15000001), bal) + require.Equal(t, int64(150000001), bal) bal, err = neutron.GetBalance(ctx, partyBRouterAddress, nativeNtrnDenom) require.NoError(t, err) - require.Equal(t, int64(15000001), bal) + require.Equal(t, int64(150000001), bal) }) t.Run("two party POL", func(t *testing.T) { @@ -636,6 +635,7 @@ func TestTwoPartyPol(t *testing.T) { } t.Run("tick until forwarders create ICA", func(t *testing.T) { + require.NoError(t, testutil.WaitForBlocks(ctx, 15, atom, neutron, osmosis), "failed to wait for blocks") for { tickClock() var response CovenantAddressQueryResponse @@ -655,10 +655,13 @@ func TestTwoPartyPol(t *testing.T) { require.NoError(t, cosmosNeutron.QueryContract(ctx, partyBIbcForwarderAddress, contractStateQuery, &response), "failed to query forwarder B state") - forwarderBState := response.Data if forwarderAState == forwarderBState && forwarderBState == "ica_created" { + require.NoError(t, testutil.WaitForBlocks(ctx, 15, atom, neutron, osmosis), "failed to wait for blocks") + + var depositAddressResponse CovenantAddressQueryResponse + type DepositAddress struct{} type DepositAddressQuery struct { DepositAddress DepositAddress `json:"deposit_address"` @@ -667,20 +670,21 @@ func TestTwoPartyPol(t *testing.T) { DepositAddress: DepositAddress{}, } - err := cosmosNeutron.QueryContract(ctx, partyAIbcForwarderAddress, depositAddressQuery, &response) + err := cosmosNeutron.QueryContract(ctx, partyAIbcForwarderAddress, depositAddressQuery, &depositAddressResponse) require.NoError(t, err, "failed to query party a forwarder deposit address") - partyADepositAddress = response.Data + partyADepositAddress = depositAddressResponse.Data - err = cosmosNeutron.QueryContract(ctx, partyBIbcForwarderAddress, depositAddressQuery, &response) + err = cosmosNeutron.QueryContract(ctx, partyBIbcForwarderAddress, depositAddressQuery, &depositAddressResponse) require.NoError(t, err, "failed to query party b forwarder deposit address") - partyBDepositAddress = response.Data - println("both parties icas created") + partyBDepositAddress = depositAddressResponse.Data + println("both parties icas created: ", partyADepositAddress, " , ", partyBDepositAddress) break } } }) t.Run("fund the forwarders with sufficient funds", func(t *testing.T) { + err := cosmosOsmosis.SendFunds(ctx, osmoUser.KeyName, ibc.WalletAmount{ Address: partyBDepositAddress, Denom: nativeOsmoDenom, diff --git a/two-party-pol-covenant/tests/interchaintest/types.go b/two-party-pol-covenant/tests/interchaintest/types.go index 8562d3f3..dde25ea3 100644 --- a/two-party-pol-covenant/tests/interchaintest/types.go +++ b/two-party-pol-covenant/tests/interchaintest/types.go @@ -10,7 +10,7 @@ type CovenantInstantiateMsg struct { Timeouts Timeouts `json:"timeouts"` PresetIbcFee PresetIbcFee `json:"preset_ibc_fee"` ContractCodeIds ContractCodeIds `json:"contract_codes"` - TickMaxGas *string `json:"clock_tick_max_gas,omitempty"` + TickMaxGas string `json:"clock_tick_max_gas,omitempty"` LockupConfig ExpiryConfig `json:"lockup_config"` PartyAConfig CovenantPartyConfig `json:"party_a_config"` PartyBConfig CovenantPartyConfig `json:"party_b_config"` From d000c06a4b9ba9ebd8a4338f0b97f6166a5d3554 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 17 Oct 2023 12:47:35 +0200 Subject: [PATCH 123/586] adding deposit address query to holder --- contracts/two-party-pol-holder/src/contract.rs | 3 ++- contracts/two-party-pol-holder/src/msg.rs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index 1ac775b8..926b7d08 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -388,7 +388,7 @@ fn try_ragequit(deps: DepsMut, env: Env, info: MessageInfo) -> Result StdResult { +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.load(deps.storage)?)?), QueryMsg::RagequitConfig {} => Ok(to_binary(&RAGEQUIT_CONFIG.load(deps.storage)?)?), @@ -400,6 +400,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::ConfigPartyB {} => Ok(to_binary(&COVENANT_CONFIG.load(deps.storage)?.party_b)?), QueryMsg::DepositDeadline {} => Ok(to_binary(&DEPOSIT_DEADLINE.load(deps.storage)?)?), QueryMsg::Config {} => Ok(to_binary(&COVENANT_CONFIG.load(deps.storage)?)?), + QueryMsg::DepositAddress {} => Ok(to_binary(&env.contract.address)?), } } diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index 9752c31b..52a65374 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -3,7 +3,7 @@ use std::fmt; use astroport::asset::Asset; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Api, Attribute, Coin, Decimal, StdError, Binary}; -use covenant_macros::{clocked, covenant_clock_address, covenant_next_contract}; +use covenant_macros::{clocked, covenant_clock_address, covenant_next_contract, covenant_deposit_address}; use covenant_utils::ExpiryConfig; use crate::error::ContractError; @@ -203,6 +203,7 @@ impl fmt::Display for ContractState { #[covenant_clock_address] #[covenant_next_contract] +#[covenant_deposit_address] #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { From 448b4f3b4d28316ec9bbeca3dfa9e4f6f7486c3b Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 18 Oct 2023 17:02:36 +0200 Subject: [PATCH 124/586] generalized astroport liquid poolor --- Cargo.lock | 34 ++ .../astroport-liquid-pooler/.cargo/config | 3 + contracts/astroport-liquid-pooler/Cargo.toml | 61 +++ .../examples/schema.rs | 15 + .../astroport-liquid-pooler/src/contract.rs | 446 ++++++++++++++++++ .../astroport-liquid-pooler/src/error.rs | 39 ++ contracts/astroport-liquid-pooler/src/lib.rs | 8 + contracts/astroport-liquid-pooler/src/msg.rs | 187 ++++++++ .../astroport-liquid-pooler/src/state.rs | 22 + 9 files changed, 815 insertions(+) create mode 100644 contracts/astroport-liquid-pooler/.cargo/config create mode 100644 contracts/astroport-liquid-pooler/Cargo.toml create mode 100644 contracts/astroport-liquid-pooler/examples/schema.rs create mode 100644 contracts/astroport-liquid-pooler/src/contract.rs create mode 100644 contracts/astroport-liquid-pooler/src/error.rs create mode 100644 contracts/astroport-liquid-pooler/src/lib.rs create mode 100644 contracts/astroport-liquid-pooler/src/msg.rs create mode 100644 contracts/astroport-liquid-pooler/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 351e7eb2..d8412089 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -326,6 +326,40 @@ dependencies = [ "serde", ] +[[package]] +name = "covenant-astroport-liquid-pooler" +version = "1.0.0" +dependencies = [ + "astroport 2.8.0", + "astroport-factory", + "astroport-native-coin-registry", + "astroport-pair-stable", + "astroport-token", + "astroport-whitelist", + "base64 0.13.1", + "bech32", + "cosmos-sdk-proto 0.14.0", + "cosmwasm-schema", + "cosmwasm-std", + "covenant-clock", + "covenant-macros", + "cw-multi-test", + "cw-storage-plus 1.1.0", + "cw-utils 1.0.2", + "cw1-whitelist 1.1.1", + "cw2 1.1.1", + "cw20 0.15.1", + "neutron-sdk", + "prost 0.11.9", + "prost-types", + "protobuf 3.3.0", + "schemars", + "serde", + "serde-json-wasm 0.4.1", + "sha2 0.10.8", + "thiserror", +] + [[package]] name = "covenant-clock" version = "1.0.0" diff --git a/contracts/astroport-liquid-pooler/.cargo/config b/contracts/astroport-liquid-pooler/.cargo/config new file mode 100644 index 00000000..6a6f2852 --- /dev/null +++ b/contracts/astroport-liquid-pooler/.cargo/config @@ -0,0 +1,3 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +schema = "run --bin schema" \ No newline at end of file diff --git a/contracts/astroport-liquid-pooler/Cargo.toml b/contracts/astroport-liquid-pooler/Cargo.toml new file mode 100644 index 00000000..a60a78d8 --- /dev/null +++ b/contracts/astroport-liquid-pooler/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "covenant-astroport-liquid-pooler" +authors = ["benskey bekauz@protonmail.com"] +description = "Astroport LP contract for covenants" +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } +edition = { workspace = true } + +exclude = [ + "contract.wasm", + "hash.txt", +] + + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +covenant-macros = { workspace = true } +covenant-clock = { workspace = true, features=["library"] } + +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +cw2 = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +# the sha2 version here is the same as the one used by +# cosmwasm-std. when bumping cosmwasm-std, this should also be +# updated. to find cosmwasm_std's sha function: +# ```cargo tree --package cosmwasm-std``` +sha2 = { workspace = true } +neutron-sdk = { workspace = true } +cosmos-sdk-proto = { workspace = true } +protobuf = { workspace = true } +schemars = { workspace = true } +serde-json-wasm = { workspace = true } +base64 = { workspace = true } +prost = { workspace = true } +prost-types = { workspace = true } +bech32 = { workspace = true } +astroport = "2.8.0" +cw20 = { version = "0.15" } + +# dev-dependencies +[dev-dependencies] +cw-multi-test = { workspace = true } +astroport-token = {git = "https://github.com/astroport-fi/astroport-core.git"} +astroport-whitelist = {git = "https://github.com/astroport-fi/astroport-core.git"} +astroport-factory = {git = "https://github.com/astroport-fi/astroport-core.git"} +astroport-native-coin-registry = {git = "https://github.com/astroport-fi/astroport-core.git"} +astroport-pair-stable = {git = "https://github.com/astroport-fi/astroport-core.git"} +cw1-whitelist = "1.1.0" diff --git a/contracts/astroport-liquid-pooler/examples/schema.rs b/contracts/astroport-liquid-pooler/examples/schema.rs new file mode 100644 index 00000000..8a393186 --- /dev/null +++ b/contracts/astroport-liquid-pooler/examples/schema.rs @@ -0,0 +1,15 @@ +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; +use covenant_astroport_liquid_pooler::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use std::env::current_dir; +use std::fs::create_dir_all; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(InstantiateMsg), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); + export_schema(&schema_for!(ExecuteMsg), &out_dir); +} diff --git a/contracts/astroport-liquid-pooler/src/contract.rs b/contracts/astroport-liquid-pooler/src/contract.rs new file mode 100644 index 00000000..22389a9f --- /dev/null +++ b/contracts/astroport-liquid-pooler/src/contract.rs @@ -0,0 +1,446 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_binary, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Reply, Response, + StdError, StdResult, SubMsg, Uint128, WasmMsg, +}; +use covenant_clock::helpers::verify_clock; +use cw2::set_contract_version; + +use astroport::{ + asset::Asset, + pair::{ExecuteMsg::ProvideLiquidity, PoolResponse}, + DecimalCheckedOps, +}; + +use crate::{ + error::ContractError, + msg::{ + ContractState, ExecuteMsg, InstantiateMsg, LpConfig, MigrateMsg, ProvidedLiquidityInfo, + QueryMsg, + }, + state::{ASSETS, HOLDER_ADDRESS, LP_CONFIG, PROVIDED_LIQUIDITY_INFO}, +}; + +use neutron_sdk::NeutronResult; + +use crate::state::{CLOCK_ADDRESS, CONTRACT_STATE}; + +const CONTRACT_NAME: &str = "crates.io:covenant-astroport-liquid-pooler"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// type QueryDeps<'a> = Deps<'a, NeutronQuery>; +// type ExecuteDeps<'a> = DepsMut<'a, NeutronQuery>; +const DOUBLE_SIDED_REPLY_ID: u64 = 321u64; +const SINGLE_SIDED_REPLY_ID: u64 = 322u64; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + deps.api.debug("WASMDEBUG: lp instantiate"); + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // validate the contract addresses + let clock_addr = deps.api.addr_validate(&msg.clock_address)?; + let pool_addr = deps.api.addr_validate(&msg.pool_address)?; + let holder_addr = deps.api.addr_validate(&msg.holder_address)?; + + // contract starts at Instantiated state + CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; + + // store the relevant module addresses + CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; + HOLDER_ADDRESS.save(deps.storage, &holder_addr)?; + + ASSETS.save(deps.storage, &msg.assets)?; + + let lp_config = LpConfig { + pool_address: pool_addr, + single_side_lp_limits: msg.single_side_lp_limits, + autostake: msg.autostake, + slippage_tolerance: msg.slippage_tolerance, + }; + LP_CONFIG.save(deps.storage, &lp_config)?; + + // we begin with no liquidity provided + PROVIDED_LIQUIDITY_INFO.save( + deps.storage, + &ProvidedLiquidityInfo { + provided_amount_a: Uint128::zero(), + provided_amount_b: Uint128::zero(), + }, + )?; + + Ok(Response::default() + .add_attribute("method", "lp_instantiate") + .add_attribute("clock_addr", clock_addr) + .add_attribute("holder_addr", holder_addr) + .add_attribute("asset_a_denom", msg.assets.asset_a_denom) + .add_attribute("asset_b_denom", msg.assets.asset_b_denom) + .add_attributes(lp_config.to_response_attributes())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Tick {} => try_tick(deps, env, info), + } +} + +/// attempts to advance the state machine. performs `info.sender` validation. +fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + // Verify caller is the clock + verify_clock(&info.sender, &CLOCK_ADDRESS.load(deps.storage)?)?; + + let current_state = CONTRACT_STATE.load(deps.storage)?; + match current_state { + ContractState::Instantiated => try_lp(deps, env), + } +} + +/// method which attempts to provision liquidity to the pool. +/// if both desired asset balances are non-zero, double sided liquidity +/// is provided. +/// otherwise, single-sided liquidity provision is attempted. +fn try_lp(mut deps: DepsMut, env: Env) -> Result { + let asset_data = ASSETS.load(deps.storage)?; + + // first we query our own balances and filter out any unexpected denoms + let bal_coins = deps + .querier + .query_all_balances(env.contract.address.to_string())?; + let (coin_a, coin_b) = get_relevant_balances( + bal_coins, + asset_data.asset_a_denom, + asset_data.asset_b_denom, + ); + + // depending on available balances we attempt a different action: + match (coin_a.amount.is_zero(), coin_b.amount.is_zero()) { + // one balance is non-zero, we attempt single-side + (true, false) | (false, true) => { + let single_sided_submsg = + try_get_single_side_lp_submsg(deps.branch(), coin_a, coin_b)?; + if let Some(msg) = single_sided_submsg { + return Ok(Response::default() + .add_submessage(msg) + .add_attribute("method", "single_side_lp")); + } + } + // both balances are non-zero, we attempt double-side + (false, false) => { + let double_sided_submsg = + try_get_double_side_lp_submsg(deps.branch(), coin_a, coin_b)?; + + if let Some(msg) = double_sided_submsg { + return Ok(Response::default() + .add_submessage(msg) + .add_attribute("method", "double_side_lp")); + } + } + // both balances zero, no liquidity can be provisioned + _ => (), + } + + // if no message could be constructed, we keep waiting for funds + Ok(Response::default() + .add_attribute("method", "try_lp") + .add_attribute("status", "not enough funds")) +} + +/// attempts to get a double sided ProvideLiquidity submessage. +/// amounts here do not matter. as long as we have non-zero balances of both +/// a and b tokens, the maximum amount of liquidity is provided to maintain +/// the existing pool ratio. +fn try_get_double_side_lp_submsg( + deps: DepsMut, + token_a: Coin, + token_b: Coin, +) -> Result, ContractError> { + let lp_config = LP_CONFIG.load(deps.storage)?; + let asset_data = ASSETS.load(deps.storage)?; + let holder_address = HOLDER_ADDRESS.load(deps.storage)?; + + // we now query the pool to know the balances + let pool_response: PoolResponse = deps + .querier + .query_wasm_smart(&lp_config.pool_address, &astroport::pair::QueryMsg::Pool {})?; + let (pool_token_a_bal, pool_token_b_bal) = get_pool_asset_amounts( + pool_response.assets, + &asset_data.asset_a_denom.as_str(), + &asset_data.asset_b_denom.as_str(), + )?; + + // we derive the ratio of token a to token b + // using this ratio we know how many of token a we should provide for every one b token + // by multiplying available b token amount by this ratio. + let a_to_b_ratio = Decimal::from_ratio(pool_token_a_bal, pool_token_b_bal); + + // we thus find the required token amount to enter into the position using all available b tokens: + let required_token_a_amount = a_to_b_ratio.checked_mul_uint128(token_b.amount)?; + + // depending on available balances we determine the highest amount + // of liquidity we can provide: + let (asset_a_double_sided, asset_b_double_sided) = + if token_a.amount >= required_token_a_amount { + // if we are able to satisfy the required amount, we do that: + // provide all b tokens along with required amount of a tokens + asset_data.to_tuple(required_token_a_amount, token_b.amount) + } else { + // otherwise, our token a amount is insufficient to provide double + // sided liquidity using all of our b tokens. + // this means that we should provide all of our available a tokens, + // and as many b tokens as needed to satisfy the existing ratio + asset_data.to_tuple( + token_a.amount, + Decimal::from_ratio( + pool_token_b_bal, + pool_token_a_bal + ).checked_mul_uint128(token_a.amount)?) + }; + + let (a_coin, b_coin) = ( + asset_a_double_sided.to_coin()?, + asset_b_double_sided.to_coin()?, + ); + + // craft a ProvideLiquidity message with the determined assets + let double_sided_liq_msg = ProvideLiquidity { + assets: vec![asset_a_double_sided, asset_b_double_sided], + slippage_tolerance: lp_config.slippage_tolerance, + auto_stake: lp_config.autostake, + receiver: Some(holder_address.to_string()), + }; + + // update the provided amounts and leftover assets + PROVIDED_LIQUIDITY_INFO.update( + deps.storage, + |mut info: ProvidedLiquidityInfo| -> StdResult<_> { + info.provided_amount_b = info.provided_amount_b.checked_add(b_coin.amount)?; + info.provided_amount_a = info + .provided_amount_a + .checked_add(a_coin.amount)?; + Ok(info) + }, + )?; + + Ok(Some(SubMsg::reply_on_success( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: lp_config.pool_address.to_string(), + msg: to_binary(&double_sided_liq_msg)?, + funds: vec![a_coin, b_coin], + }), + DOUBLE_SIDED_REPLY_ID, + ))) +} + +/// attempts to build a single sided `ProvideLiquidity` message. +/// pool ratio does not get validated here. as long as the single +/// side asset amount being provided is within our predefined +/// single-side liquidity limits, we provide it. +fn try_get_single_side_lp_submsg( + deps: DepsMut, + coin_a: Coin, + coin_b: Coin, +) -> Result, ContractError> { + let asset_data = ASSETS.load(deps.storage)?; + let holder_address = HOLDER_ADDRESS.load(deps.storage)?; + let lp_config = LP_CONFIG.load(deps.storage)?; + + let assets = asset_data.to_asset_vec(coin_a.amount, coin_b.amount); + + // given one non-zero asset, we build the ProvideLiquidity message + let single_sided_liq_msg = ProvideLiquidity { + assets, + slippage_tolerance: lp_config.slippage_tolerance, + auto_stake: lp_config.autostake, + receiver: Some(holder_address.to_string()), + }; + + // now we try to submit the message for either B or A token single side liquidity + if coin_a.amount.is_zero() + && coin_b.amount <= lp_config.single_side_lp_limits.asset_b_limit + { + // update the provided liquidity info + PROVIDED_LIQUIDITY_INFO.update(deps.storage, |mut info| -> StdResult<_> { + info.provided_amount_b = info.provided_amount_b.checked_add(coin_b.amount)?; + Ok(info) + })?; + + // if available ls token amount is within single side limits we build a single side msg + let submsg = SubMsg::reply_on_success( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: lp_config.pool_address.to_string(), + msg: to_binary(&single_sided_liq_msg)?, + funds: vec![coin_b], + }), + SINGLE_SIDED_REPLY_ID, + ); + + return Ok(Some(submsg)); + } else if coin_b.amount.is_zero() + && coin_a.amount <= lp_config.single_side_lp_limits.asset_a_limit + { + // update the provided liquidity info + PROVIDED_LIQUIDITY_INFO.update(deps.storage, |mut info| -> StdResult<_> { + info.provided_amount_a = + info.provided_amount_a.checked_add(coin_a.amount)?; + Ok(info) + })?; + + // if available A token amount is within single side limits we build a single side msg + let submsg = SubMsg::reply_on_success( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: lp_config.pool_address.to_string(), + msg: to_binary(&single_sided_liq_msg)?, + funds: vec![coin_a], + }), + SINGLE_SIDED_REPLY_ID, + ); + + return Ok(Some(submsg)); + } + + // if neither a nor b token single side lp message was built, we just go back and wait + Ok(None) +} + +/// filters out a vector of `Coin`s to retrieve ones with relevant denoms +fn get_relevant_balances(coins: Vec, a_denom: String, b_denom: String) -> (Coin, Coin) { + let (mut token_a, mut token_b) = (Coin::default(), Coin::default()); + + for c in coins { + if c.denom == a_denom { + // found token_a balance + token_a = c; + } else if c.denom == b_denom { + // found token_b balance + token_b = c; + } + } + (token_a, token_b) +} + +/// filters out irrelevant balances and returns a and b token amounts +fn get_pool_asset_amounts( + assets: Vec, + a_denom: &str, + b_denom: &str, +) -> Result<(Uint128, Uint128), StdError> { + let (mut a_bal, mut b_bal) = (Uint128::zero(), Uint128::zero()); + + for asset in assets { + let coin = asset.to_coin()?; + if coin.denom == b_denom { + // found b balance + b_bal = coin.amount; + } else if coin.denom == a_denom { + // found a token balance + a_bal = coin.amount; + } + } + + Ok((a_bal, b_bal)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), + QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.may_load(deps.storage)?)?), + QueryMsg::HolderAddress {} => Ok(to_binary(&HOLDER_ADDRESS.may_load(deps.storage)?)?), + QueryMsg::Assets {} => Ok(to_binary(&ASSETS.may_load(deps.storage)?)?), + QueryMsg::LpConfig {} => Ok(to_binary(&LP_CONFIG.may_load(deps.storage)?)?), + // the deposit address for LP module is the contract itself + QueryMsg::DepositAddress {} => Ok(to_binary(&Some(&env.contract.address.to_string()))?), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> NeutronResult { + deps.api.debug("WASMDEBUG: migrate"); + + match msg { + MigrateMsg::UpdateConfig { + clock_addr, + holder_address, + assets, + lp_config, + } => { + let mut response = Response::default().add_attribute("method", "update_config"); + + if let Some(clock_addr) = clock_addr { + CLOCK_ADDRESS.save(deps.storage, &deps.api.addr_validate(&clock_addr)?)?; + response = response.add_attribute("clock_addr", clock_addr); + } + + if let Some(holder_address) = holder_address { + HOLDER_ADDRESS.save(deps.storage, &deps.api.addr_validate(&holder_address)?)?; + response = response.add_attribute("holder_address", holder_address); + } + + if let Some(denoms) = assets { + ASSETS.save(deps.storage, &denoms)?; + response = response.add_attribute("ls_denom", denoms.asset_b_denom.to_string()); + response = + response.add_attribute("native_denom", denoms.asset_a_denom.to_string()); + } + + if let Some(config) = lp_config { + // validate the address before storing it + deps.api.addr_validate(config.pool_address.as_str())?; + LP_CONFIG.save(deps.storage, &config)?; + response = response.add_attributes(config.to_response_attributes()); + } + + Ok(response) + } + MigrateMsg::UpdateCodeId { data: _ } => { + // This is a migrate message to update code id, + // Data is optional base64 that we can parse to any data we would like in the future + // let data: SomeStruct = from_binary(&data)?; + Ok(Response::default()) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { + deps.api.debug("WASMDEBUG: reply"); + match msg.id { + DOUBLE_SIDED_REPLY_ID => handle_double_sided_reply_id(deps, _env, msg), + SINGLE_SIDED_REPLY_ID => handle_single_sided_reply_id(deps, _env, msg), + _ => Err(ContractError::from(StdError::GenericErr { + msg: "err".to_string(), + })), + } +} + +fn handle_double_sided_reply_id( + _deps: DepsMut, + _env: Env, + msg: Reply, +) -> Result { + Ok(Response::default() + .add_attribute("method", "handle_double_sided_reply_id") + .add_attribute("reply_id", msg.id.to_string())) +} + +fn handle_single_sided_reply_id( + _deps: DepsMut, + _env: Env, + msg: Reply, +) -> Result { + Ok(Response::default() + .add_attribute("method", "handle_single_sided_reply_id") + .add_attribute("reply_id", msg.id.to_string())) +} diff --git a/contracts/astroport-liquid-pooler/src/error.rs b/contracts/astroport-liquid-pooler/src/error.rs new file mode 100644 index 00000000..40ae9aec --- /dev/null +++ b/contracts/astroport-liquid-pooler/src/error.rs @@ -0,0 +1,39 @@ +use cosmwasm_std::{OverflowError, StdError}; +use neutron_sdk::NeutronError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + NeutronError(#[from] NeutronError), + + #[error("{0}")] + OverflowError(#[from] OverflowError), + + #[error("Not clock")] + ClockVerificationError {}, + + #[error("Single side LP limit exceeded")] + SingleSideLpLimitError {}, + + #[error("Non zero balances for single side liquidity")] + SingleSideLpNonZeroBalanceError {}, + + #[error("Zero balance for double side liquidity")] + DoubleSideLpZeroBalanceError {}, + + #[error("Insufficient funds for double sided LP")] + DoubleSideLpLimitError {}, + + #[error("Incomplete pool assets")] + IncompletePoolAssets {}, + + #[error("Pool validation error")] + PoolValidationError {}, + + #[error("Price range error")] + PriceRangeError {}, +} diff --git a/contracts/astroport-liquid-pooler/src/lib.rs b/contracts/astroport-liquid-pooler/src/lib.rs new file mode 100644 index 00000000..0faea8f4 --- /dev/null +++ b/contracts/astroport-liquid-pooler/src/lib.rs @@ -0,0 +1,8 @@ +#![warn(clippy::unwrap_used, clippy::expect_used)] + +extern crate core; + +pub mod contract; +pub mod error; +pub mod msg; +pub mod state; diff --git a/contracts/astroport-liquid-pooler/src/msg.rs b/contracts/astroport-liquid-pooler/src/msg.rs new file mode 100644 index 00000000..77e2430e --- /dev/null +++ b/contracts/astroport-liquid-pooler/src/msg.rs @@ -0,0 +1,187 @@ +use astroport::asset::{Asset, AssetInfo}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Attribute, Binary, Decimal, Uint128}; +use covenant_macros::{clocked, covenant_clock_address, covenant_deposit_address}; + +#[cw_serde] +pub struct InstantiateMsg { + pub pool_address: String, + pub clock_address: String, + pub holder_address: String, + pub slippage_tolerance: Option, + pub autostake: Option, + pub assets: AssetData, + pub single_side_lp_limits: SingleSideLpLimits, +} + +#[cw_serde] +pub struct LpConfig { + /// address of the liquidity pool we plan to enter + pub pool_address: Addr, + /// amounts of both tokens we consider ok to single-side lp + pub single_side_lp_limits: SingleSideLpLimits, + /// boolean flag for enabling autostaking of LP tokens upon liquidity provisioning + pub autostake: Option, + /// slippage tolerance parameter for liquidity provisioning + pub slippage_tolerance: Option, +} + +impl LpConfig { + pub fn to_response_attributes(self) -> Vec { + let autostake = match self.autostake { + Some(val) => val.to_string(), + None => "None".to_string(), + }; + let slippage_tolerance = match self.slippage_tolerance { + Some(val) => val.to_string(), + None => "None".to_string(), + }; + vec![ + Attribute::new("pool_address", self.pool_address.to_string()), + Attribute::new( + "single_side_asset_a_limit", + self.single_side_lp_limits.asset_a_limit.to_string(), + ), + Attribute::new( + "single_side_asset_b_limit", + self.single_side_lp_limits.asset_b_limit.to_string(), + ), + Attribute::new("autostake", autostake), + Attribute::new("slippage_tolerance", slippage_tolerance), + ] + } +} + +/// holds the both asset denoms relevant for providing liquidity +#[cw_serde] +pub struct AssetData { + pub asset_a_denom: String, + pub asset_b_denom: String, +} + +impl AssetData { + pub fn to_asset_vec(&self, a_bal: Uint128, b_bal: Uint128) -> Vec { + vec![ + Asset { + info: AssetInfo::NativeToken { denom: self.asset_a_denom.to_string() }, + amount: a_bal, + }, + Asset { + info: AssetInfo::NativeToken { denom: self.asset_b_denom.to_string() }, + amount: b_bal, + }, + ] + } + + /// returns tuple of (asset_A, asset_B) + pub fn to_tuple(&self, a_bal: Uint128, b_bal: Uint128) -> (Asset, Asset) { + ( + Asset { + info: AssetInfo::NativeToken { denom: self.asset_a_denom.to_string() }, + amount: a_bal, + }, + Asset { + info: AssetInfo::NativeToken { denom: self.asset_b_denom.to_string() }, + amount: b_bal, + }, + ) + } +} + +/// single side lp limits define the highest amount (in `Uint128`) that +/// we consider acceptable to provide single-sided. +/// if asset balance exceeds these limits, double-sided liquidity should be provided. +#[cw_serde] +pub struct SingleSideLpLimits { + pub asset_a_limit: Uint128, + pub asset_b_limit: Uint128, +} + +/// Defines fields relevant to LP module that are known prior to covenant +/// being instantiated. Use `to_instantiate_msg` implemented method to obtain +/// the `InstantiateMsg` by providing the non-deterministic fields. +#[cw_serde] +pub struct PresetLpFields { + /// slippage tolerance for providing liquidity + pub slippage_tolerance: Option, + /// determines whether provided liquidity is automatically staked + pub autostake: Option, + /// denominations of both assets + pub assets: AssetData, + /// limits (in `Uint128`) for single side liquidity provision. + /// Defaults to 100 if none are provided. + pub single_side_lp_limits: Option, + /// lp contract code + pub lp_code: u64, + /// label for contract to be instantiated with + pub label: String, + /// address of the target liquidity pool + pub pool_address: String, +} + +impl PresetLpFields { + /// builds an `InstantiateMsg` by taking in any fields not known on instantiation. + pub fn to_instantiate_msg( + self, + clock_address: String, + holder_address: String, + ) -> InstantiateMsg { + InstantiateMsg { + pool_address: self.pool_address, + clock_address, + holder_address, + slippage_tolerance: self.slippage_tolerance, + autostake: self.autostake, + assets: self.assets, + single_side_lp_limits: self.single_side_lp_limits.unwrap_or(SingleSideLpLimits { + asset_a_limit: Uint128::new(100), + asset_b_limit: Uint128::new(100), + }), + } + } +} + +#[clocked] +#[cw_serde] +pub enum ExecuteMsg {} + +#[covenant_clock_address] +#[covenant_deposit_address] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(ContractState)] + ContractState {}, + #[returns(Addr)] + HolderAddress {}, + #[returns(Vec)] + Assets {}, + #[returns(LpConfig)] + LpConfig {}, +} + +#[cw_serde] +pub enum MigrateMsg { + UpdateConfig { + clock_addr: Option, + holder_address: Option, + assets: Option, + lp_config: Option, + }, + UpdateCodeId { + data: Option, + }, +} + +/// keeps track of provided asset liquidities in `Uint128`. +#[cw_serde] +pub struct ProvidedLiquidityInfo { + pub provided_amount_a: Uint128, + pub provided_amount_b: Uint128, +} + +/// state of the LP state machine +#[cw_serde] +pub enum ContractState { + Instantiated, +} diff --git a/contracts/astroport-liquid-pooler/src/state.rs b/contracts/astroport-liquid-pooler/src/state.rs new file mode 100644 index 00000000..afcd9d31 --- /dev/null +++ b/contracts/astroport-liquid-pooler/src/state.rs @@ -0,0 +1,22 @@ +use cosmwasm_std::Addr; +use cw_storage_plus::Item; + +use crate::msg::{AssetData, ContractState, LpConfig, ProvidedLiquidityInfo}; + +/// contract state tracks the state machine progress +pub const CONTRACT_STATE: Item = Item::new("contract_state"); + +/// asset denom information +pub const ASSETS: Item = Item::new("assets"); + +/// clock module address to verify the incoming ticks sender +pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); +/// holder module address to verify withdrawal requests +pub const HOLDER_ADDRESS: Item = Item::new("holder_address"); + +/// keeps track of both token amounts we provided to the pool +pub const PROVIDED_LIQUIDITY_INFO: Item = + Item::new("provided_liquidity_info"); + +/// configuration relevant to entering into an LP position +pub const LP_CONFIG: Item = Item::new("lp_config"); From e2cc83ce1fc78c6820f10a76a6e61998f5ec1ad4 Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 19 Oct 2023 21:06:17 +0200 Subject: [PATCH 125/586] adding astroport liquid pooler to the covenant flow & interchaintests --- Cargo.lock | 1 + Cargo.toml | 1 + .../astroport-liquid-pooler/src/contract.rs | 8 +- contracts/astroport-liquid-pooler/src/msg.rs | 29 +++++ contracts/two-party-pol-covenant/Cargo.toml | 1 + .../two-party-pol-covenant/src/contract.rs | 108 ++++++++++++++++-- contracts/two-party-pol-covenant/src/msg.rs | 3 + contracts/two-party-pol-covenant/src/state.rs | 3 + two-party-pol-covenant/justfile | 1 + .../interchaintest/two_party_pol_test.go | 22 +++- .../tests/interchaintest/types.go | 6 +- 11 files changed, 165 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8412089..389ac4ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -670,6 +670,7 @@ dependencies = [ "cosmos-sdk-proto 0.14.0", "cosmwasm-schema", "cosmwasm-std", + "covenant-astroport-liquid-pooler", "covenant-clock", "covenant-ibc-forwarder", "covenant-interchain-router", diff --git a/Cargo.toml b/Cargo.toml index 6535d7d9..716de8fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ swap-covenant = { path = "contracts/swap-covenant" } covenant-interchain-router = { path = "contracts/interchain-router" } covenant-two-party-pol-holder = { path = "contracts/two-party-pol-holder" } covenant-two-party-pol = { path = "contracts/two-party-pol-covenant" } +covenant-astroport-liquid-pooler = { path = "contracts/astroport-liquid-pooler" } # packages clock-derive = { path = "packages/clock-derive" } diff --git a/contracts/astroport-liquid-pooler/src/contract.rs b/contracts/astroport-liquid-pooler/src/contract.rs index 22389a9f..b719435a 100644 --- a/contracts/astroport-liquid-pooler/src/contract.rs +++ b/contracts/astroport-liquid-pooler/src/contract.rs @@ -2,7 +2,7 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ to_binary, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Reply, Response, - StdError, StdResult, SubMsg, Uint128, WasmMsg, + StdError, StdResult, SubMsg, Uint128, WasmMsg, Addr, }; use covenant_clock::helpers::verify_clock; use cw2::set_contract_version; @@ -37,7 +37,7 @@ const SINGLE_SIDED_REPLY_ID: u64 = 322u64; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, - _env: Env, + env: Env, _info: MessageInfo, msg: InstantiateMsg, ) -> Result { @@ -47,8 +47,10 @@ pub fn instantiate( // validate the contract addresses let clock_addr = deps.api.addr_validate(&msg.clock_address)?; let pool_addr = deps.api.addr_validate(&msg.pool_address)?; - let holder_addr = deps.api.addr_validate(&msg.holder_address)?; + // TODO: instantiate2 / remove this field from instantiateMsg + let holder_addr = env.contract.address; + // contract starts at Instantiated state CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; diff --git a/contracts/astroport-liquid-pooler/src/msg.rs b/contracts/astroport-liquid-pooler/src/msg.rs index 77e2430e..f785c9a2 100644 --- a/contracts/astroport-liquid-pooler/src/msg.rs +++ b/contracts/astroport-liquid-pooler/src/msg.rs @@ -14,6 +14,35 @@ pub struct InstantiateMsg { pub single_side_lp_limits: SingleSideLpLimits, } +#[cw_serde] +pub struct PresetAstroLiquidPoolerFields { + pub slippage_tolerance: Option, + pub autostake: Option, + pub assets: AssetData, + pub single_side_lp_limits: SingleSideLpLimits, + pub label: String, + pub code_id: u64, +} + +impl PresetAstroLiquidPoolerFields { + pub fn to_instantiate_msg( + &self, + pool_address: String, + clock_address: String, + holder_address: String, + ) -> InstantiateMsg { + InstantiateMsg { + pool_address, + clock_address, + holder_address, + slippage_tolerance: self.slippage_tolerance, + autostake: self.autostake.clone(), + assets: self.assets.clone(), + single_side_lp_limits: self.single_side_lp_limits.clone(), + } + } +} + #[cw_serde] pub struct LpConfig { /// address of the liquidity pool we plan to enter diff --git a/contracts/two-party-pol-covenant/Cargo.toml b/contracts/two-party-pol-covenant/Cargo.toml index e99d68d0..42cad9b6 100644 --- a/contracts/two-party-pol-covenant/Cargo.toml +++ b/contracts/two-party-pol-covenant/Cargo.toml @@ -47,6 +47,7 @@ covenant-utils = { workspace = true } covenant-ibc-forwarder = { workspace = true, features = ["library"] } covenant-interchain-router = { workspace = true, features = ["library"] } covenant-two-party-pol-holder = { workspace = true, features = ["library"] } +covenant-astroport-liquid-pooler = { workspace = true, features = ["library"] } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/two-party-pol-covenant/src/contract.rs b/contracts/two-party-pol-covenant/src/contract.rs index 6ec5eb8a..b88e59c5 100644 --- a/contracts/two-party-pol-covenant/src/contract.rs +++ b/contracts/two-party-pol-covenant/src/contract.rs @@ -4,6 +4,7 @@ use cosmwasm_std::{ to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Decimal, Uint128, CosmosMsg, WasmMsg, SubMsg, Reply, }; +use covenant_astroport_liquid_pooler::msg::{PresetAstroLiquidPoolerFields, SingleSideLpLimits, AssetData}; use covenant_clock::msg::PresetClockFields; use covenant_ibc_forwarder::msg::PresetIbcForwarderFields; use covenant_interchain_router::msg::PresetInterchainRouterFields; @@ -18,7 +19,7 @@ use crate::{ COVENANT_CLOCK_ADDR, PARTY_A_IBC_FORWARDER_ADDR, PARTY_B_IBC_FORWARDER_ADDR, PRESET_CLOCK_FIELDS, PRESET_HOLDER_FIELDS, - PRESET_PARTY_A_FORWARDER_FIELDS, PRESET_PARTY_B_FORWARDER_FIELDS, COVENANT_POL_HOLDER_ADDR, PRESET_PARTY_A_ROUTER_FIELDS, PRESET_PARTY_B_ROUTER_FIELDS, PARTY_A_ROUTER_ADDR, PARTY_B_ROUTER_ADDR, + PRESET_PARTY_A_FORWARDER_FIELDS, PRESET_PARTY_B_FORWARDER_FIELDS, COVENANT_POL_HOLDER_ADDR, PRESET_PARTY_A_ROUTER_FIELDS, PRESET_PARTY_B_ROUTER_FIELDS, PARTY_A_ROUTER_ADDR, PARTY_B_ROUTER_ADDR, PRESET_LIQUID_POOLER_FIELDS, LIQUID_POOLER_ADDR, }, }; @@ -105,12 +106,28 @@ pub fn instantiate( code_id: msg.contract_codes.router_code, }; + let preset_liquid_pooler_fields = PresetAstroLiquidPoolerFields { + slippage_tolerance: None, + autostake: None, + assets: AssetData { + asset_a_denom: msg.party_a_config.ibc_denom, + asset_b_denom: msg.party_b_config.ibc_denom, + }, + single_side_lp_limits: SingleSideLpLimits { + asset_a_limit: Uint128::one(), + asset_b_limit: Uint128::one(), + }, + label: format!("{}_liquid_pooler", msg.label), + code_id: msg.contract_codes.liquid_pooler_code, + }; + PRESET_CLOCK_FIELDS.save(deps.storage, &preset_clock_fields)?; PRESET_HOLDER_FIELDS.save(deps.storage, &preset_holder_fields)?; PRESET_PARTY_A_FORWARDER_FIELDS.save(deps.storage, &preset_party_a_forwarder_fields)?; PRESET_PARTY_B_FORWARDER_FIELDS.save(deps.storage, &preset_party_b_forwarder_fields)?; PRESET_PARTY_A_ROUTER_FIELDS.save(deps.storage, &preset_party_a_router_fields)?; PRESET_PARTY_B_ROUTER_FIELDS.save(deps.storage, &preset_party_b_router_fields)?; + PRESET_LIQUID_POOLER_FIELDS.save(deps.storage, &preset_liquid_pooler_fields)?; // we start the module instantiation chain with the clock let clock_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { @@ -139,6 +156,7 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result handle_holder_reply(deps, env, msg), PARTY_A_FORWARDER_REPLY_ID => handle_party_a_ibc_forwarder_reply(deps, env, msg), PARTY_B_FORWARDER_REPLY_ID => handle_party_b_ibc_forwarder_reply(deps, env, msg), + LP_REPLY_ID => handle_liquid_pooler_reply_id(deps, env, msg), _ => Err(ContractError::UnknownReplyId {}), } } @@ -238,16 +256,65 @@ pub fn handle_party_b_interchain_router_reply( let router_addr = deps.api.addr_validate(&response.contract_address)?; PARTY_B_ROUTER_ADDR.save(deps.storage, &router_addr)?; + let clock_address = COVENANT_CLOCK_ADDR.load(deps.storage)?.to_string(); + let pool_address = PRESET_HOLDER_FIELDS.load(deps.storage)?.pool_address.to_string(); + let holder_address = "replace".to_string(); + let preset_liquid_pooler_fields = PRESET_LIQUID_POOLER_FIELDS.load(deps.storage)?; + + let instantiate_msg = preset_liquid_pooler_fields.to_instantiate_msg( + pool_address, + clock_address, + holder_address + ); + + let liquid_pooler_inst_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.to_string()), + code_id: preset_liquid_pooler_fields.code_id, + msg: to_binary(&instantiate_msg)?, + funds: vec![], + label: preset_liquid_pooler_fields.label, + }); + + Ok(Response::default() + .add_attribute("method", "handle_party_b_interchain_router_reply") + .add_attribute("party_b_interchain_router_addr", router_addr) + .add_submessage(SubMsg::reply_always( + liquid_pooler_inst_tx, + LP_REPLY_ID, + ))) + } + Err(err) => Err(ContractError::ContractInstantiationError { + contract: "party b router".to_string(), + err, + }), + } +} + + +pub fn handle_liquid_pooler_reply_id( + deps: DepsMut, + env: Env, + msg: Reply, +) -> Result { + deps.api.debug("WASMDEBUG: liquid pooler reply"); + + let parsed_data = parse_reply_instantiate_data(msg); + match parsed_data { + Ok(response) => { + // validate and store the instantiated liquid pooler address + let liquid_pooler = deps.api.addr_validate(&response.contract_address)?; + LIQUID_POOLER_ADDR.save(deps.storage, &liquid_pooler)?; + + let party_b_router = PARTY_B_ROUTER_ADDR.load(deps.storage)?; let preset_holder_fields = PRESET_HOLDER_FIELDS.load(deps.storage)?; let clock_addr = COVENANT_CLOCK_ADDR.load(deps.storage)?; let party_a_router = PARTY_A_ROUTER_ADDR.load(deps.storage)?; - let lper_addr = router_addr.clone(); // TODO: replace with actual lper let instantiate_msg = preset_holder_fields.clone().to_instantiate_msg( clock_addr.to_string(), - lper_addr.to_string(), + liquid_pooler.to_string(), party_a_router.to_string(), - router_addr.to_string(), + party_b_router.to_string(), ); let holder_instantiate_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { @@ -259,20 +326,21 @@ pub fn handle_party_b_interchain_router_reply( }); Ok(Response::default() - .add_attribute("method", "handle_party_b_interchain_router_reply") - .add_attribute("party_b_interchain_router_addr", router_addr) + .add_attribute("method", "handle_liquid_pooler_reply") + .add_attribute("liquid_pooler_addr", liquid_pooler) .add_submessage(SubMsg::reply_always( holder_instantiate_tx, HOLDER_REPLY_ID, ))) } Err(err) => Err(ContractError::ContractInstantiationError { - contract: "party b router".to_string(), + contract: "liquid pooler".to_string(), err, }), - }} - + } +} + pub fn handle_holder_reply( deps: DepsMut, env: Env, @@ -376,6 +444,20 @@ pub fn handle_party_b_ibc_forwarder_reply( let holder = COVENANT_POL_HOLDER_ADDR.load(deps.storage)?; let party_a_router = PARTY_A_ROUTER_ADDR.load(deps.storage)?; let party_b_router = PARTY_B_ROUTER_ADDR.load(deps.storage)?; + let liquid_pooler = LIQUID_POOLER_ADDR.load(deps.storage)?; + + let lp_fields = PRESET_LIQUID_POOLER_FIELDS.load(deps.storage)?; + + let update_liquid_pooler_holder_addr = WasmMsg::Migrate { + contract_addr: liquid_pooler.to_string(), + new_code_id: lp_fields.code_id, + msg: to_binary(&covenant_astroport_liquid_pooler::msg::MigrateMsg::UpdateConfig { + clock_addr: None, + holder_address: Some(holder.to_string()), + assets: None, + lp_config: None, + })?, + }; let update_clock_whitelist_msg = WasmMsg::Migrate { contract_addr: clock_addr.to_string(), @@ -387,6 +469,7 @@ pub fn handle_party_b_ibc_forwarder_reply( holder.to_string(), party_a_router.to_string(), party_b_router.to_string(), + liquid_pooler.to_string(), ]), remove: None, })?, @@ -395,7 +478,9 @@ pub fn handle_party_b_ibc_forwarder_reply( Ok(Response::default() .add_attribute("method", "handle_party_b_ibc_forwarder_reply") .add_attribute("party_b_ibc_forwarder_addr", party_b_ibc_forwarder_addr) - .add_message(update_clock_whitelist_msg)) + .add_message(update_clock_whitelist_msg) + .add_message(update_liquid_pooler_holder_addr) + ) } Err(err) => Err(ContractError::ContractInstantiationError { contract: "party_b ibc forwarder".to_string(), @@ -429,7 +514,8 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { Some(Addr::unchecked("not found")) }; Ok(to_binary(&resp)?) - } + }, + QueryMsg::LiquidPoolerAddress {} => Ok(to_binary(&LIQUID_POOLER_ADDR.may_load(deps.storage)?)?), } } diff --git a/contracts/two-party-pol-covenant/src/msg.rs b/contracts/two-party-pol-covenant/src/msg.rs index a805fd97..4aaa435d 100644 --- a/contracts/two-party-pol-covenant/src/msg.rs +++ b/contracts/two-party-pol-covenant/src/msg.rs @@ -51,6 +51,7 @@ pub struct CovenantContractCodeIds { pub holder_code: u64, pub clock_code: u64, pub router_code: u64, + pub liquid_pooler_code: u64, } #[cw_serde] @@ -107,6 +108,8 @@ pub enum QueryMsg { IbcForwarderAddress { party: String }, #[returns(Addr)] InterchainRouterAddress { party: String }, + #[returns(Addr)] + LiquidPoolerAddress {}, } #[cw_serde] diff --git a/contracts/two-party-pol-covenant/src/state.rs b/contracts/two-party-pol-covenant/src/state.rs index 3f630d80..2f6ceec1 100644 --- a/contracts/two-party-pol-covenant/src/state.rs +++ b/contracts/two-party-pol-covenant/src/state.rs @@ -1,4 +1,5 @@ use cosmwasm_std::Addr; +use covenant_astroport_liquid_pooler::msg::PresetAstroLiquidPoolerFields; use covenant_clock::msg::PresetClockFields; use covenant_ibc_forwarder::msg::PresetIbcForwarderFields; @@ -17,6 +18,7 @@ pub const PRESET_PARTY_A_ROUTER_FIELDS: Item = Item::new("preset_party_a_router_fields"); pub const PRESET_PARTY_B_ROUTER_FIELDS: Item = Item::new("preset_party_b_router_fields"); +pub const PRESET_LIQUID_POOLER_FIELDS: Item = Item::new("preset_lp_fields"); pub const COVENANT_CLOCK_ADDR: Item = Item::new("covenant_clock_addr"); pub const COVENANT_POL_HOLDER_ADDR: Item = Item::new("covenant_two_party_pol_holder_addr"); @@ -24,3 +26,4 @@ pub const PARTY_A_IBC_FORWARDER_ADDR: Item = Item::new("party_a_ibc_forwar pub const PARTY_B_IBC_FORWARDER_ADDR: Item = Item::new("party_b_ibc_forwarder_addr"); pub const PARTY_A_ROUTER_ADDR: Item = Item::new("party_a_router_addr"); pub const PARTY_B_ROUTER_ADDR: Item = Item::new("party_b_router_addr"); +pub const LIQUID_POOLER_ADDR: Item = Item::new("liquid_pooler_addr"); diff --git a/two-party-pol-covenant/justfile b/two-party-pol-covenant/justfile index 45d72956..d34ecbb8 100644 --- a/two-party-pol-covenant/justfile +++ b/two-party-pol-covenant/justfile @@ -29,6 +29,7 @@ simtest: optimize mv ./../artifacts/covenant_interchain_router-aarch64.wasm ./../artifacts/covenant_interchain_router.wasm && \ mv ./../artifacts/covenant_clock-aarch64.wasm ./../artifacts/covenant_clock.wasm && \ mv ./../artifacts/covenant_two_party_pol_holder-aarch64.wasm ./../artifacts/covenant_two_party_pol_holder.wasm && \ + mv ./../artifacts/covenant_astroport_liquid_pooler-aarch64.wasm ./../artifacts/covenant_liquid_pooler.wasm && \ mv ./../artifacts/covenant_two_party_pol-aarch64.wasm ./../artifacts/covenant_two_party_pol.wasm \ ;fi diff --git a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go index 4bc00350..8fc2a01c 100644 --- a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go +++ b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go @@ -32,6 +32,7 @@ const nativeNtrnDenom = "untrn" var covenantAddress string var clockAddress string var partyARouterAddress, partyBRouterAddress string +var liquidPoolerAddress string var partyAIbcForwarderAddress, partyBIbcForwarderAddress string var partyADepositAddress, partyBDepositAddress string var holderAddress string @@ -325,6 +326,7 @@ func TestTwoPartyPol(t *testing.T) { const routerContractPath = "wasms/covenant_interchain_router.wasm" const ibcForwarderContractPath = "wasms/covenant_ibc_forwarder.wasm" const holderContractPath = "wasms/covenant_two_party_pol_holder.wasm" + const liquidPoolerPath = "wasms/covenant_liquid_pooler.wasm" // After storing on Neutron, we will receive a code id // We parse all the subcontracts into uint64 @@ -333,6 +335,7 @@ func TestTwoPartyPol(t *testing.T) { var routerCodeId uint64 var ibcForwarderCodeId uint64 var holderCodeId uint64 + var lperCodeId uint64 var covenantCodeIdStr string var covenantCodeId uint64 _ = covenantCodeId @@ -362,6 +365,12 @@ func TestTwoPartyPol(t *testing.T) { ibcForwarderCodeId, err = strconv.ParseUint(ibcForwarderCodeIdStr, 10, 64) require.NoError(t, err, "failed to parse codeId into uint64") + // store lper, get code + lperCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, liquidPoolerPath) + require.NoError(t, err, "failed to store liquid pooler contract") + lperCodeId, err = strconv.ParseUint(lperCodeIdStr, 10, 64) + require.NoError(t, err, "failed to parse codeId into uint64") + // store clock and get code id holderCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, holderContractPath) require.NoError(t, err, "failed to store two party pol holder contract") @@ -390,8 +399,8 @@ func TestTwoPartyPol(t *testing.T) { TimeoutFee: "10000", } - // tickMaxGas := "2000" - poolAddress := "todo" + // todo: no bueno, make it real here + poolAddress := neutronUser.Bech32Address(cosmosNeutron.Config().Bech32Prefix) atomCoin := Coin{ Denom: "uatom", @@ -428,6 +437,7 @@ func TestTwoPartyPol(t *testing.T) { InterchainRouterCode: routerCodeId, ClockCode: clockCodeId, HolderCode: holderCodeId, + LiquidPoolerCode: lperCodeId, } ragequitTerms := RagequitTerms{ @@ -466,7 +476,7 @@ func TestTwoPartyPol(t *testing.T) { "--home", neutron.HomeDir(), "--node", neutron.GetRPCAddress(), "--chain-id", neutron.Config().ChainID, - "--gas", "9000900", + "--gas", "90009000", "--keyring-backend", keyring.BackendTest, "-y", } @@ -520,6 +530,7 @@ func TestTwoPartyPol(t *testing.T) { Party: "party_b", }, } + var response CovenantAddressQueryResponse err = cosmosNeutron.QueryContract(ctx, covenantAddress, ClockAddressQuery{}, &response) @@ -532,6 +543,11 @@ func TestTwoPartyPol(t *testing.T) { holderAddress = response.Data println("holder addr: ", holderAddress) + err = cosmosNeutron.QueryContract(ctx, covenantAddress, LiquidPoolerQuery{}, &response) + require.NoError(t, err, "failed to query instantiated liquid pooler address") + liquidPoolerAddress = response.Data + println("liquid pooler addr: ", liquidPoolerAddress) + err = cosmosNeutron.QueryContract(ctx, covenantAddress, routerQueryPartyA, &response) require.NoError(t, err, "failed to query instantiated party a router address") partyARouterAddress = response.Data diff --git a/two-party-pol-covenant/tests/interchaintest/types.go b/two-party-pol-covenant/tests/interchaintest/types.go index dde25ea3..dc543056 100644 --- a/two-party-pol-covenant/tests/interchaintest/types.go +++ b/two-party-pol-covenant/tests/interchaintest/types.go @@ -26,6 +26,7 @@ type ContractCodeIds struct { InterchainRouterCode uint64 `json:"router_code"` ClockCode uint64 `json:"clock_code"` HolderCode uint64 `json:"holder_code"` + LiquidPoolerCode uint64 `json:"liquid_pooler_code"` } type Timeouts struct { @@ -110,7 +111,10 @@ type InterchainRouterQuery struct { type IbcForwarderQuery struct { Party Party `json:"ibc_forwarder_address"` } - +type LiquidPoolerAddress struct{} +type LiquidPoolerQuery struct { + LiquidPoolerAddress LiquidPoolerAddress `json:"liquid_pooler_address"` +} type CovenantAddressQueryResponse struct { Data string `json:"data"` } From ef511f3211b42678e04941ff45d70ea02ce9a0de Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 23 Oct 2023 17:47:04 +0200 Subject: [PATCH 126/586] expected price range validation --- .../astroport-liquid-pooler/src/contract.rs | 63 ++++++++------- contracts/astroport-liquid-pooler/src/msg.rs | 81 +++++++++---------- .../two-party-pol-covenant/src/contract.rs | 2 + contracts/two-party-pol-covenant/src/msg.rs | 5 +- .../interchaintest/two_party_pol_test.go | 26 +++--- .../tests/interchaintest/types.go | 28 ++++--- 6 files changed, 107 insertions(+), 98 deletions(-) diff --git a/contracts/astroport-liquid-pooler/src/contract.rs b/contracts/astroport-liquid-pooler/src/contract.rs index b719435a..c8c780d6 100644 --- a/contracts/astroport-liquid-pooler/src/contract.rs +++ b/contracts/astroport-liquid-pooler/src/contract.rs @@ -1,8 +1,10 @@ +use std::ops::Sub; + #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ to_binary, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Reply, Response, - StdError, StdResult, SubMsg, Uint128, WasmMsg, Addr, + StdError, StdResult, SubMsg, Uint128, WasmMsg, }; use covenant_clock::helpers::verify_clock; use cw2::set_contract_version; @@ -17,7 +19,7 @@ use crate::{ error::ContractError, msg::{ ContractState, ExecuteMsg, InstantiateMsg, LpConfig, MigrateMsg, ProvidedLiquidityInfo, - QueryMsg, + QueryMsg, DecimalRange, AssetData, }, state::{ASSETS, HOLDER_ADDRESS, LP_CONFIG, PROVIDED_LIQUIDITY_INFO}, }; @@ -60,11 +62,17 @@ pub fn instantiate( ASSETS.save(deps.storage, &msg.assets)?; + let decimal_range = DecimalRange::try_from( + msg.expected_pool_ratio, + msg.acceptable_pool_ratio_delta, + )?; + let lp_config = LpConfig { pool_address: pool_addr, single_side_lp_limits: msg.single_side_lp_limits, autostake: msg.autostake, slippage_tolerance: msg.slippage_tolerance, + expected_pool_ratio_range: decimal_range, }; LP_CONFIG.save(deps.storage, &lp_config)?; @@ -115,6 +123,19 @@ fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> Result Result { let asset_data = ASSETS.load(deps.storage)?; + let lp_config = LP_CONFIG.load(deps.storage)?; + + let pool_response: PoolResponse = deps + .querier + .query_wasm_smart(&lp_config.pool_address, &astroport::pair::QueryMsg::Pool {})?; + let (pool_token_a_bal, pool_token_b_bal) = get_pool_asset_amounts( + pool_response.assets, + &asset_data.asset_a_denom.as_str(), + &asset_data.asset_b_denom.as_str(), + )?; + let a_to_b_ratio = Decimal::from_ratio(pool_token_a_bal, pool_token_b_bal); + // validate the current pool ratio against our expectations + lp_config.expected_pool_ratio_range.is_within_range(a_to_b_ratio)?; // first we query our own balances and filter out any unexpected denoms let bal_coins = deps @@ -122,8 +143,8 @@ fn try_lp(mut deps: DepsMut, env: Env) -> Result { .query_all_balances(env.contract.address.to_string())?; let (coin_a, coin_b) = get_relevant_balances( bal_coins, - asset_data.asset_a_denom, - asset_data.asset_b_denom, + asset_data.asset_a_denom.as_str(), + asset_data.asset_b_denom.as_str(), ); // depending on available balances we attempt a different action: @@ -131,7 +152,7 @@ fn try_lp(mut deps: DepsMut, env: Env) -> Result { // one balance is non-zero, we attempt single-side (true, false) | (false, true) => { let single_sided_submsg = - try_get_single_side_lp_submsg(deps.branch(), coin_a, coin_b)?; + try_get_single_side_lp_submsg(deps.branch(), coin_a, coin_b, lp_config, asset_data)?; if let Some(msg) = single_sided_submsg { return Ok(Response::default() .add_submessage(msg) @@ -141,7 +162,7 @@ fn try_lp(mut deps: DepsMut, env: Env) -> Result { // both balances are non-zero, we attempt double-side (false, false) => { let double_sided_submsg = - try_get_double_side_lp_submsg(deps.branch(), coin_a, coin_b)?; + try_get_double_side_lp_submsg(deps.branch(), coin_a, coin_b, a_to_b_ratio, pool_token_a_bal, pool_token_b_bal, lp_config, asset_data)?; if let Some(msg) = double_sided_submsg { return Ok(Response::default() @@ -167,28 +188,16 @@ fn try_get_double_side_lp_submsg( deps: DepsMut, token_a: Coin, token_b: Coin, + pool_token_ratio: Decimal, + pool_token_a_bal: Uint128, + pool_token_b_bal: Uint128, + lp_config: LpConfig, + asset_data: AssetData, ) -> Result, ContractError> { - let lp_config = LP_CONFIG.load(deps.storage)?; - let asset_data = ASSETS.load(deps.storage)?; let holder_address = HOLDER_ADDRESS.load(deps.storage)?; - // we now query the pool to know the balances - let pool_response: PoolResponse = deps - .querier - .query_wasm_smart(&lp_config.pool_address, &astroport::pair::QueryMsg::Pool {})?; - let (pool_token_a_bal, pool_token_b_bal) = get_pool_asset_amounts( - pool_response.assets, - &asset_data.asset_a_denom.as_str(), - &asset_data.asset_b_denom.as_str(), - )?; - - // we derive the ratio of token a to token b - // using this ratio we know how many of token a we should provide for every one b token - // by multiplying available b token amount by this ratio. - let a_to_b_ratio = Decimal::from_ratio(pool_token_a_bal, pool_token_b_bal); - // we thus find the required token amount to enter into the position using all available b tokens: - let required_token_a_amount = a_to_b_ratio.checked_mul_uint128(token_b.amount)?; + let required_token_a_amount = pool_token_ratio.checked_mul_uint128(token_b.amount)?; // depending on available balances we determine the highest amount // of liquidity we can provide: @@ -253,10 +262,10 @@ fn try_get_single_side_lp_submsg( deps: DepsMut, coin_a: Coin, coin_b: Coin, + lp_config: LpConfig, + asset_data: AssetData, ) -> Result, ContractError> { - let asset_data = ASSETS.load(deps.storage)?; let holder_address = HOLDER_ADDRESS.load(deps.storage)?; - let lp_config = LP_CONFIG.load(deps.storage)?; let assets = asset_data.to_asset_vec(coin_a.amount, coin_b.amount); @@ -317,7 +326,7 @@ fn try_get_single_side_lp_submsg( } /// filters out a vector of `Coin`s to retrieve ones with relevant denoms -fn get_relevant_balances(coins: Vec, a_denom: String, b_denom: String) -> (Coin, Coin) { +fn get_relevant_balances(coins: Vec, a_denom: &str, b_denom: &str) -> (Coin, Coin) { let (mut token_a, mut token_b) = (Coin::default(), Coin::default()); for c in coins { diff --git a/contracts/astroport-liquid-pooler/src/msg.rs b/contracts/astroport-liquid-pooler/src/msg.rs index f785c9a2..d2b4284b 100644 --- a/contracts/astroport-liquid-pooler/src/msg.rs +++ b/contracts/astroport-liquid-pooler/src/msg.rs @@ -3,6 +3,8 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Attribute, Binary, Decimal, Uint128}; use covenant_macros::{clocked, covenant_clock_address, covenant_deposit_address}; +use crate::error::ContractError; + #[cw_serde] pub struct InstantiateMsg { pub pool_address: String, @@ -12,6 +14,8 @@ pub struct InstantiateMsg { pub autostake: Option, pub assets: AssetData, pub single_side_lp_limits: SingleSideLpLimits, + pub expected_pool_ratio: Decimal, + pub acceptable_pool_ratio_delta: Decimal, } #[cw_serde] @@ -22,6 +26,8 @@ pub struct PresetAstroLiquidPoolerFields { pub single_side_lp_limits: SingleSideLpLimits, pub label: String, pub code_id: u64, + pub expected_pool_ratio: Decimal, + pub acceptable_pool_ratio_delta: Decimal, } impl PresetAstroLiquidPoolerFields { @@ -39,6 +45,35 @@ impl PresetAstroLiquidPoolerFields { autostake: self.autostake.clone(), assets: self.assets.clone(), single_side_lp_limits: self.single_side_lp_limits.clone(), + expected_pool_ratio: self.expected_pool_ratio, + acceptable_pool_ratio_delta: self.acceptable_pool_ratio_delta, + } + } +} + +#[cw_serde] +pub struct DecimalRange { + min: Decimal, + max: Decimal, +} + +impl DecimalRange { + pub fn new(min: Decimal, max: Decimal) -> Self { + DecimalRange { min, max } + } + + pub fn try_from(mid: Decimal, delta: Decimal) -> Result { + Ok(DecimalRange { + min: mid.checked_sub(delta)?, + max: mid.checked_add(delta)?, + }) + } + + pub fn is_within_range(&self, value: Decimal) -> Result<(), ContractError> { + if value >= self.min && value <= self.max { + Ok(()) + } else { + Err(ContractError::PriceRangeError { }) } } } @@ -53,6 +88,8 @@ pub struct LpConfig { pub autostake: Option, /// slippage tolerance parameter for liquidity provisioning pub slippage_tolerance: Option, + /// expected price range + pub expected_pool_ratio_range: DecimalRange, } impl LpConfig { @@ -126,50 +163,6 @@ pub struct SingleSideLpLimits { pub asset_b_limit: Uint128, } -/// Defines fields relevant to LP module that are known prior to covenant -/// being instantiated. Use `to_instantiate_msg` implemented method to obtain -/// the `InstantiateMsg` by providing the non-deterministic fields. -#[cw_serde] -pub struct PresetLpFields { - /// slippage tolerance for providing liquidity - pub slippage_tolerance: Option, - /// determines whether provided liquidity is automatically staked - pub autostake: Option, - /// denominations of both assets - pub assets: AssetData, - /// limits (in `Uint128`) for single side liquidity provision. - /// Defaults to 100 if none are provided. - pub single_side_lp_limits: Option, - /// lp contract code - pub lp_code: u64, - /// label for contract to be instantiated with - pub label: String, - /// address of the target liquidity pool - pub pool_address: String, -} - -impl PresetLpFields { - /// builds an `InstantiateMsg` by taking in any fields not known on instantiation. - pub fn to_instantiate_msg( - self, - clock_address: String, - holder_address: String, - ) -> InstantiateMsg { - InstantiateMsg { - pool_address: self.pool_address, - clock_address, - holder_address, - slippage_tolerance: self.slippage_tolerance, - autostake: self.autostake, - assets: self.assets, - single_side_lp_limits: self.single_side_lp_limits.unwrap_or(SingleSideLpLimits { - asset_a_limit: Uint128::new(100), - asset_b_limit: Uint128::new(100), - }), - } - } -} - #[clocked] #[cw_serde] pub enum ExecuteMsg {} diff --git a/contracts/two-party-pol-covenant/src/contract.rs b/contracts/two-party-pol-covenant/src/contract.rs index b88e59c5..faf0ff65 100644 --- a/contracts/two-party-pol-covenant/src/contract.rs +++ b/contracts/two-party-pol-covenant/src/contract.rs @@ -119,6 +119,8 @@ pub fn instantiate( }, label: format!("{}_liquid_pooler", msg.label), code_id: msg.contract_codes.liquid_pooler_code, + expected_pool_ratio: msg.expected_pool_ratio, + acceptable_pool_ratio_delta: msg.acceptable_pool_ratio_delta, }; PRESET_CLOCK_FIELDS.save(deps.storage, &preset_clock_fields)?; diff --git a/contracts/two-party-pol-covenant/src/msg.rs b/contracts/two-party-pol-covenant/src/msg.rs index 4aaa435d..0237d707 100644 --- a/contracts/two-party-pol-covenant/src/msg.rs +++ b/contracts/two-party-pol-covenant/src/msg.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Uint128, Uint64, Coin}; +use cosmwasm_std::{Addr, Uint128, Uint64, Coin, Decimal}; use covenant_two_party_pol_holder::msg::RagequitConfig; use covenant_utils::ExpiryConfig; use neutron_sdk::bindings::msg::IbcFee; @@ -22,7 +22,8 @@ pub struct InstantiateMsg { pub deposit_deadline: Option, pub party_a_share: Uint64, pub party_b_share: Uint64, - // TODO: lper instantiation fields + pub expected_pool_ratio: Decimal, + pub acceptable_pool_ratio_delta: Decimal, } #[cw_serde] diff --git a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go index 8fc2a01c..92c1dad4 100644 --- a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go +++ b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go @@ -449,18 +449,20 @@ func TestTwoPartyPol(t *testing.T) { } covenantMsg := CovenantInstantiateMsg{ - Label: "two-party-pol-covenant", - Timeouts: timeouts, - PresetIbcFee: presetIbcFee, - ContractCodeIds: codeIds, - LockupConfig: lockupConfig, - PartyAConfig: partyAConfig, - PartyBConfig: partyBConfig, - PoolAddress: poolAddress, - RagequitConfig: &ragequitConfig, - DepositDeadline: &depositDeadline, - PartyAShare: "50", - PartyBShare: "50", + Label: "two-party-pol-covenant", + Timeouts: timeouts, + PresetIbcFee: presetIbcFee, + ContractCodeIds: codeIds, + LockupConfig: lockupConfig, + PartyAConfig: partyAConfig, + PartyBConfig: partyBConfig, + PoolAddress: poolAddress, + RagequitConfig: &ragequitConfig, + DepositDeadline: &depositDeadline, + PartyAShare: "50", + PartyBShare: "50", + ExpectedPoolRatio: "0.5", + AcceptablePoolRatioDelta: "0.5", } str, err := json.Marshal(covenantMsg) require.NoError(t, err, "Failed to marshall CovenantInstantiateMsg") diff --git a/two-party-pol-covenant/tests/interchaintest/types.go b/two-party-pol-covenant/tests/interchaintest/types.go index dc543056..141b8244 100644 --- a/two-party-pol-covenant/tests/interchaintest/types.go +++ b/two-party-pol-covenant/tests/interchaintest/types.go @@ -6,19 +6,21 @@ package ibc_test // ----- Covenant Instantiation ------ type CovenantInstantiateMsg struct { - Label string `json:"label"` - Timeouts Timeouts `json:"timeouts"` - PresetIbcFee PresetIbcFee `json:"preset_ibc_fee"` - ContractCodeIds ContractCodeIds `json:"contract_codes"` - TickMaxGas string `json:"clock_tick_max_gas,omitempty"` - LockupConfig ExpiryConfig `json:"lockup_config"` - PartyAConfig CovenantPartyConfig `json:"party_a_config"` - PartyBConfig CovenantPartyConfig `json:"party_b_config"` - PoolAddress string `json:"pool_address"` - RagequitConfig *RagequitConfig `json:"ragequit_config,omitempty"` - DepositDeadline *ExpiryConfig `json:"deposit_deadline,omitempty"` - PartyAShare string `json:"party_a_share"` - PartyBShare string `json:"party_b_share"` + Label string `json:"label"` + Timeouts Timeouts `json:"timeouts"` + PresetIbcFee PresetIbcFee `json:"preset_ibc_fee"` + ContractCodeIds ContractCodeIds `json:"contract_codes"` + TickMaxGas string `json:"clock_tick_max_gas,omitempty"` + LockupConfig ExpiryConfig `json:"lockup_config"` + PartyAConfig CovenantPartyConfig `json:"party_a_config"` + PartyBConfig CovenantPartyConfig `json:"party_b_config"` + PoolAddress string `json:"pool_address"` + RagequitConfig *RagequitConfig `json:"ragequit_config,omitempty"` + DepositDeadline *ExpiryConfig `json:"deposit_deadline,omitempty"` + PartyAShare string `json:"party_a_share"` + PartyBShare string `json:"party_b_share"` + ExpectedPoolRatio string `json:"expected_pool_ratio"` + AcceptablePoolRatioDelta string `json:"acceptable_pool_ratio_delta"` } type ContractCodeIds struct { From 2ae19dd838846e8230f1a785e8b7f6c77668a494 Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 23 Oct 2023 20:35:00 +0200 Subject: [PATCH 127/586] deploying & instantiating astro pool on ictest --- .../astroport-liquid-pooler/src/contract.rs | 2 - .../interchaintest/two_party_pol_test.go | 332 +++++++++++++++++- .../tests/interchaintest/types.go | 172 +++++++++ 3 files changed, 499 insertions(+), 7 deletions(-) diff --git a/contracts/astroport-liquid-pooler/src/contract.rs b/contracts/astroport-liquid-pooler/src/contract.rs index c8c780d6..6d5a9a05 100644 --- a/contracts/astroport-liquid-pooler/src/contract.rs +++ b/contracts/astroport-liquid-pooler/src/contract.rs @@ -1,5 +1,3 @@ -use std::ops::Sub; - #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ diff --git a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go index 92c1dad4..94865460 100644 --- a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go +++ b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "strconv" + "strings" "testing" "time" @@ -41,6 +42,12 @@ var atomNeutronICSConnectionId, neutronAtomICSConnectionId string var neutronOsmosisIBCConnId, osmosisNeutronIBCConnId string var atomNeutronIBCConnId, neutronAtomIBCConnId string var gaiaOsmosisIBCConnId, osmosisGaiaIBCConnId string +var tokenAddress string +var whitelistAddress string +var factoryAddress string +var coinRegistryAddress string +var stableswapAddress string +var liquidityTokenAddress string // PARTY_A const osmoContributionAmount uint64 = 100_000_000_000 // in uosmo @@ -343,7 +350,7 @@ func TestTwoPartyPol(t *testing.T) { t.Run("deploy covenant contracts", func(t *testing.T) { // store covenant and get code id covenantCodeIdStr, err = cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, covenantContractPath) - require.NoError(t, err, "failed to store stride covenant contract") + require.NoError(t, err, "failed to store two party pol covenant contract") covenantCodeId, err = strconv.ParseUint(covenantCodeIdStr, 10, 64) require.NoError(t, err, "failed to parse codeId into uint64") @@ -380,6 +387,324 @@ func TestTwoPartyPol(t *testing.T) { require.NoError(t, testutil.WaitForBlocks(ctx, 5, cosmosNeutron, cosmosAtom, cosmosOsmosis)) }) + t.Run("deploy astroport contracts", func(t *testing.T) { + + stablePairCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, "wasms/astroport_pair_stable.wasm") + require.NoError(t, err, "failed to store astroport stableswap contract") + stablePairCodeId, err := strconv.ParseUint(stablePairCodeIdStr, 10, 64) + require.NoError(t, err, "failed to parse codeId into uint64") + + factoryCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, "wasms/astroport_factory.wasm") + require.NoError(t, err, "failed to store astroport factory contract") + + whitelistCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, "wasms/astroport_whitelist.wasm") + require.NoError(t, err, "failed to store astroport whitelist contract") + whitelistCodeId, err := strconv.ParseUint(whitelistCodeIdStr, 10, 64) + require.NoError(t, err, "failed to parse codeId into uint64") + + tokenCodeIdStr, err := cosmosNeutron.StoreContract(ctx, neutronUser.KeyName, "wasms/astroport_token.wasm") + require.NoError(t, err, "failed to store astroport token contract") + tokenCodeId, err := strconv.ParseUint(tokenCodeIdStr, 10, 64) + require.NoError(t, err, "failed to parse codeId into uint64") + + t.Run("astroport token", func(t *testing.T) { + + msg := NativeTokenInstantiateMsg{ + Name: "atomosmolp", + Symbol: "atomosmo", + Decimals: 5, + InitialBalances: []Cw20Coin{}, + Mint: nil, + Marketing: nil, + } + + str, err := json.Marshal(msg) + require.NoError(t, err, "Failed to marshall NativeTokenInstantiateMsg") + + tokenAddress, err = cosmosNeutron.InstantiateContract(ctx, neutronUser.KeyName, tokenCodeIdStr, string(str), true) + require.NoError(t, err, "Failed to instantiate atom Token") + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + }) + + t.Run("whitelist", func(t *testing.T) { + + admins := []string{neutronUser.Bech32Address(neutron.Config().Bech32Prefix)} + + msg := WhitelistInstantiateMsg{ + Admins: admins, + Mutable: false, + } + + str, err := json.Marshal(msg) + require.NoError(t, err, "Failed to marshall WhitelistInstantiateMsg") + + whitelistAddress, err = cosmosNeutron.InstantiateContract( + ctx, neutronUser.KeyName, whitelistCodeIdStr, string(str), true) + require.NoError(t, err, "Failed to instantiate Whitelist") + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + }) + + t.Run("native coins registry", func(t *testing.T) { + coinRegistryCodeId, err := cosmosNeutron.StoreContract( + ctx, neutronUser.KeyName, "wasms/astroport_native_coin_registry.wasm") + require.NoError(t, err, "failed to store astroport native coin registry contract") + + msg := NativeCoinRegistryInstantiateMsg{ + Owner: neutronUser.Bech32Address(neutron.Config().Bech32Prefix), + } + str, err := json.Marshal(msg) + require.NoError(t, err, "Failed to marshall NativeCoinRegistryInstantiateMsg") + + nativeCoinRegistryAddress, err := cosmosNeutron.InstantiateContract( + ctx, neutronUser.KeyName, coinRegistryCodeId, string(str), true) + require.NoError(t, err, "Failed to instantiate NativeCoinRegistry") + coinRegistryAddress = nativeCoinRegistryAddress + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + }) + + t.Run("add coins to registry", func(t *testing.T) { + // Add ibc native tokens for uosmo and uatom to the native coin registry + // each of these tokens has a precision of 6 + addMessage := `{"add":{"native_coins":[["` + neutronAtomIbcDenom + `",6],["` + neutronOsmoIbcDenom + `",6]]}}` + addCmd := []string{"neutrond", "tx", "wasm", "execute", + coinRegistryAddress, + addMessage, + "--from", neutronUser.KeyName, + "--gas-prices", "0.0untrn", + "--gas-adjustment", `1.5`, + "--output", "json", + "--home", "/var/cosmos-chain/neutron-2", + "--node", neutron.GetRPCAddress(), + "--home", neutron.HomeDir(), + "--chain-id", neutron.Config().ChainID, + "--from", neutronUser.KeyName, + "--gas", "auto", + "--keyring-backend", keyring.BackendTest, + "-y", + } + _, _, err = cosmosNeutron.Exec(ctx, addCmd, nil) + require.NoError(t, err, err) + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + }) + + t.Run("factory", func(t *testing.T) { + pairConfigs := []PairConfig{ + PairConfig{ + CodeId: stablePairCodeId, + PairType: PairType{ + Stable: struct{}{}, + }, + TotalFeeBps: 0, + MakerFeeBps: 0, + IsDisabled: false, + IsGeneratorDisabled: true, + }, + } + + msg := FactoryInstantiateMsg{ + PairConfigs: pairConfigs, + TokenCodeId: tokenCodeId, + FeeAddress: nil, + GeneratorAddress: nil, + Owner: neutronUser.Bech32Address(neutron.Config().Bech32Prefix), + WhitelistCodeId: whitelistCodeId, + CoinRegistryAddress: coinRegistryAddress, + } + + str, err := json.Marshal(msg) + require.NoError(t, err, "Failed to marshall FactoryInstantiateMsg") + + factoryAddr, err := cosmosNeutron.InstantiateContract( + ctx, neutronUser.KeyName, factoryCodeIdStr, string(str), true) + require.NoError(t, err, "Failed to instantiate Factory") + factoryAddress = factoryAddr + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + }) + + t.Run("create pair on factory", func(t *testing.T) { + + initParams := StablePoolParams{ + Amp: 3, + } + binaryData, err := json.Marshal(initParams) + require.NoError(t, err, "error encoding stable pool params to binary") + + osmoNativeToken := NativeToken{ + Denom: neutronOsmoIbcDenom, + } + atomNativeToken := NativeToken{ + Denom: neutronAtomIbcDenom, + } + assetInfos := []AssetInfo{ + { + NativeToken: &atomNativeToken, + }, + { + NativeToken: &osmoNativeToken, + }, + } + + initPairMsg := CreatePair{ + PairType: PairType{ + Stable: struct{}{}, + }, + AssetInfos: assetInfos, + InitParams: binaryData, + } + + createPairMsg := CreatePairMsg{ + CreatePair: initPairMsg, + } + + str, err := json.Marshal(createPairMsg) + require.NoError(t, err, "Failed to marshall CreatePair message") + + createCmd := []string{"neutrond", "tx", "wasm", "execute", + factoryAddress, + string(str), + "--from", neutronUser.KeyName, + "--gas-prices", "0.0untrn", + "--gas-adjustment", `1.5`, + "--output", "json", + "--home", "/var/cosmos-chain/neutron-2", + "--node", neutron.GetRPCAddress(), + "--home", neutron.HomeDir(), + "--chain-id", neutron.Config().ChainID, + "--from", neutronUser.KeyName, + "--gas", "auto", + "--keyring-backend", keyring.BackendTest, + "-y", + } + + _, _, err = cosmosNeutron.Exec(ctx, createCmd, nil) + require.NoError(t, err, err) + err = testutil.WaitForBlocks(ctx, 30, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + }) + }) + + t.Run("add liquidity to the atom-osmo stableswap pool", func(t *testing.T) { + osmoNativeToken := NativeToken{ + Denom: neutronOsmoIbcDenom, + } + atomNativeToken := NativeToken{ + Denom: neutronAtomIbcDenom, + } + assetInfos := []AssetInfo{ + { + NativeToken: &atomNativeToken, + }, + { + NativeToken: &osmoNativeToken, + }, + } + pair := Pair{ + AssetInfos: assetInfos, + } + pairQueryMsg := PairQuery{ + Pair: pair, + } + queryJson, _ := json.Marshal(pairQueryMsg) + + queryCmd := []string{"neutrond", "query", "wasm", "contract-state", "smart", + factoryAddress, string(queryJson), + } + + print("\n factory query cmd: ", string(strings.Join(queryCmd, " ")), "\n") + + factoryQueryRespBytes, _, _ := neutron.Exec(ctx, queryCmd, nil) + print(string(factoryQueryRespBytes)) + + var response FactoryPairResponse + err = cosmosNeutron.QueryContract(ctx, factoryAddress, pairQueryMsg, &response) + stableswapAddress = response.Data.ContractAddr + print("\n stableswap address: ", stableswapAddress, "\n") + liquidityTokenAddress = response.Data.LiquidityToken + print("\n liquidity token: ", liquidityTokenAddress, "\n") + + require.NoError(t, err, "failed to query pair info") + jsonResp, _ := json.Marshal(response) + print("\npair info: ", string(jsonResp), "\n") + + // set up the pool with 1:10 ratio of atom/osmo + transferAtom := ibc.WalletAmount{ + Address: neutronUser.Bech32Address(neutron.Config().Bech32Prefix), + Denom: atom.Config().Denom, + Amount: int64(100_000_000_00), + } + _, err := atom.SendIBCTransfer(ctx, testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], gaiaUser.KeyName, transferAtom, ibc.TransferOptions{}) + require.NoError(t, err) + + transferOsmo := ibc.WalletAmount{ + Address: neutronUser.Bech32Address(neutron.Config().Bech32Prefix), + Denom: osmosis.Config().Denom, + Amount: int64(100_000_000_000), + } + + _, err = osmosis.SendIBCTransfer(ctx, testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name], osmoUser.KeyName, transferOsmo, ibc.TransferOptions{}) + require.NoError(t, err) + + testutil.WaitForBlocks(ctx, 10, atom, neutron, osmosis) + + // join pool + assets := []AstroportAsset{ + AstroportAsset{ + Info: AssetInfo{ + NativeToken: &NativeToken{ + Denom: neutronAtomIbcDenom, + }, + }, + Amount: "10000000000", + }, + AstroportAsset{ + Info: AssetInfo{ + NativeToken: &NativeToken{ + Denom: neutronOsmoIbcDenom, + }, + }, + Amount: "100000000000", + }, + } + + msg := ProvideLiqudityMsg{ + ProvideLiquidity: ProvideLiquidityStruct{ + Assets: assets, + SlippageTolerance: "0.01", + AutoStake: false, + Receiver: neutronUser.Bech32Address(neutron.Config().Bech32Prefix), + }, + } + + str, err := json.Marshal(msg) + require.NoError(t, err, "Failed to marshall provide liquidity msg") + amountStr := "10000000000" + neutronAtomIbcDenom + "," + "100000000000" + neutronOsmoIbcDenom + + cmd := []string{"neutrond", "tx", "wasm", "execute", stableswapAddress, + string(str), + "--from", neutronUser.KeyName, + "--amount", amountStr, + "--output", "json", + "--home", "/var/cosmos-chain/neutron-2", + "--node", neutron.GetRPCAddress(), + "--chain-id", neutron.Config().ChainID, + "--gas", "900000", + "--keyring-backend", keyring.BackendTest, + "-y", + } + resp, _, err := cosmosNeutron.Exec(ctx, cmd, nil) + require.NoError(t, err) + jsonResp, _ = json.Marshal(resp) + print("\nprovide liquidity response: ", string(jsonResp), "\n") + + testutil.WaitForBlocks(ctx, 10, atom, neutron, osmosis) + + }) + t.Run("instantiate covenant", func(t *testing.T) { timeouts := Timeouts{ IcaTimeout: "100", // sec @@ -399,9 +724,6 @@ func TestTwoPartyPol(t *testing.T) { TimeoutFee: "10000", } - // todo: no bueno, make it real here - poolAddress := neutronUser.Bech32Address(cosmosNeutron.Config().Bech32Prefix) - atomCoin := Coin{ Denom: "uatom", Amount: "5000000000", @@ -456,7 +778,7 @@ func TestTwoPartyPol(t *testing.T) { LockupConfig: lockupConfig, PartyAConfig: partyAConfig, PartyBConfig: partyBConfig, - PoolAddress: poolAddress, + PoolAddress: stableswapAddress, RagequitConfig: &ragequitConfig, DepositDeadline: &depositDeadline, PartyAShare: "50", diff --git a/two-party-pol-covenant/tests/interchaintest/types.go b/two-party-pol-covenant/tests/interchaintest/types.go index 141b8244..2e712a2b 100644 --- a/two-party-pol-covenant/tests/interchaintest/types.go +++ b/two-party-pol-covenant/tests/interchaintest/types.go @@ -120,3 +120,175 @@ type LiquidPoolerQuery struct { type CovenantAddressQueryResponse struct { Data string `json:"data"` } + +// astroport stableswap +type StableswapInstantiateMsg struct { + TokenCodeId uint64 `json:"token_code_id"` + FactoryAddr string `json:"factory_addr"` + AssetInfos []AssetInfo `json:"asset_infos"` + InitParams []byte `json:"init_params"` +} + +type AssetInfo struct { + Token *Token `json:"token,omitempty"` + NativeToken *NativeToken `json:"native_token,omitempty"` +} + +type StablePoolParams struct { + Amp uint64 `json:"amp"` + Owner *string `json:"owner"` +} + +type Token struct { + ContractAddr string `json:"contract_addr"` +} + +type NativeToken struct { + Denom string `json:"denom"` +} + +type CwCoin struct { + Denom string `json:"denom"` + Amount uint64 `json:"amount"` +} + +// astroport factory +type FactoryInstantiateMsg struct { + PairConfigs []PairConfig `json:"pair_configs"` + TokenCodeId uint64 `json:"token_code_id"` + FeeAddress *string `json:"fee_address"` + GeneratorAddress *string `json:"generator_address"` + Owner string `json:"owner"` + WhitelistCodeId uint64 `json:"whitelist_code_id"` + CoinRegistryAddress string `json:"coin_registry_address"` +} + +type PairConfig struct { + CodeId uint64 `json:"code_id"` + PairType PairType `json:"pair_type"` + TotalFeeBps uint64 `json:"total_fee_bps"` + MakerFeeBps uint64 `json:"maker_fee_bps"` + IsDisabled bool `json:"is_disabled"` + IsGeneratorDisabled bool `json:"is_generator_disabled"` +} + +type PairType struct { + // Xyk struct{} `json:"xyk,omitempty"` + Stable struct{} `json:"stable,omitempty"` + // Custom struct{} `json:"custom,omitempty"` +} + +// astroport native coin registry + +type NativeCoinRegistryInstantiateMsg struct { + Owner string `json:"owner"` +} + +type AddExecuteMsg struct { + Add Add `json:"add"` +} + +type Add struct { + NativeCoins []NativeCoin `json:"native_coins"` +} + +type NativeCoin struct { + Name string `json:"name"` + Value uint8 `json:"value"` +} + +// Add { native_coins: Vec<(String, u8)> }, + +// astroport native token +type NativeTokenInstantiateMsg struct { + Name string `json:"name"` + Symbol string `json:"symbol"` + Decimals uint8 `json:"decimals"` + InitialBalances []Cw20Coin `json:"initial_balances"` + Mint *MinterResponse `json:"mint"` + Marketing *InstantiateMarketingInfo `json:"marketing"` +} + +type Cw20Coin struct { + Address string `json:"address"` + Amount uint64 `json:"amount"` +} + +type MinterResponse struct { + Minter string `json:"minter"` + Cap *uint64 `json:"cap,omitempty"` +} + +type InstantiateMarketingInfo struct { + Project string `json:"project"` + Description string `json:"description"` + Marketing string `json:"marketing"` + Logo Logo `json:"logo"` +} + +type Logo struct { + Url string `json:"url"` +} + +// astroport whitelist +type WhitelistInstantiateMsg struct { + Admins []string `json:"admins"` + Mutable bool `json:"mutable"` +} + +type ProvideLiqudityMsg struct { + ProvideLiquidity ProvideLiquidityStruct `json:"provide_liquidity"` +} + +type ProvideLiquidityStruct struct { + Assets []AstroportAsset `json:"assets"` + SlippageTolerance string `json:"slippage_tolerance"` + AutoStake bool `json:"auto_stake"` + Receiver string `json:"receiver"` +} + +// factory + +type FactoryPairResponse struct { + Data PairInfo `json:"data"` +} + +type LpPositionQueryResponse struct { + Data string `json:"data"` +} + +type AstroportAsset struct { + Info AssetInfo `json:"info"` + Amount string `json:"amount"` +} + +type LpPositionQuery struct{} + +type PairInfo struct { + LiquidityToken string `json:"liquidity_token"` + ContractAddr string `json:"contract_addr"` + PairType PairType `json:"pair_type"` + AssetInfos []AssetInfo `json:"asset_infos"` +} + +type LPPositionQuery struct { + LpPosition LpPositionQuery `json:"lp_position"` +} + +type Pair struct { + AssetInfos []AssetInfo `json:"asset_infos"` +} + +type PairQuery struct { + Pair Pair `json:"pair"` +} + +type CreatePair struct { + PairType PairType `json:"pair_type"` + AssetInfos []AssetInfo `json:"asset_infos"` + InitParams []byte `json:"init_params"` +} + +type CreatePairMsg struct { + CreatePair CreatePair `json:"create_pair"` +} From 91bd7f06bb0edbdc8df05ec9e6948059383f6453 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 24 Oct 2023 23:48:08 +0200 Subject: [PATCH 128/586] deploying initial liquidity on ictest; removing hardcoded holder addresses --- .../astroport-liquid-pooler/src/contract.rs | 4 +- contracts/astroport-liquid-pooler/src/msg.rs | 2 + .../two-party-pol-covenant/src/contract.rs | 21 +- .../two-party-pol-holder/src/contract.rs | 21 +- two-party-pol-covenant/justfile | 1 + .../interchaintest/two_party_pol_test.go | 179 +++++++++++++----- .../tests/interchaintest/types.go | 24 +++ 7 files changed, 190 insertions(+), 62 deletions(-) diff --git a/contracts/astroport-liquid-pooler/src/contract.rs b/contracts/astroport-liquid-pooler/src/contract.rs index 6d5a9a05..d6ca70f7 100644 --- a/contracts/astroport-liquid-pooler/src/contract.rs +++ b/contracts/astroport-liquid-pooler/src/contract.rs @@ -57,7 +57,6 @@ pub fn instantiate( // store the relevant module addresses CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; HOLDER_ADDRESS.save(deps.storage, &holder_addr)?; - ASSETS.save(deps.storage, &msg.assets)?; let decimal_range = DecimalRange::try_from( @@ -133,7 +132,7 @@ fn try_lp(mut deps: DepsMut, env: Env) -> Result { )?; let a_to_b_ratio = Decimal::from_ratio(pool_token_a_bal, pool_token_b_bal); // validate the current pool ratio against our expectations - lp_config.expected_pool_ratio_range.is_within_range(a_to_b_ratio)?; + // lp_config.expected_pool_ratio_range.is_within_range(a_to_b_ratio)?; // first we query our own balances and filter out any unexpected denoms let bal_coins = deps @@ -371,6 +370,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::LpConfig {} => Ok(to_binary(&LP_CONFIG.may_load(deps.storage)?)?), // the deposit address for LP module is the contract itself QueryMsg::DepositAddress {} => Ok(to_binary(&Some(&env.contract.address.to_string()))?), + QueryMsg::ProvidedLiquidityInfo {} => Ok(to_binary(&PROVIDED_LIQUIDITY_INFO.load(deps.storage)?)?), } } diff --git a/contracts/astroport-liquid-pooler/src/msg.rs b/contracts/astroport-liquid-pooler/src/msg.rs index d2b4284b..4721e940 100644 --- a/contracts/astroport-liquid-pooler/src/msg.rs +++ b/contracts/astroport-liquid-pooler/src/msg.rs @@ -180,6 +180,8 @@ pub enum QueryMsg { Assets {}, #[returns(LpConfig)] LpConfig {}, + #[returns(ProvidedLiquidityInfo)] + ProvidedLiquidityInfo {}, } #[cw_serde] diff --git a/contracts/two-party-pol-covenant/src/contract.rs b/contracts/two-party-pol-covenant/src/contract.rs index faf0ff65..ae75dd45 100644 --- a/contracts/two-party-pol-covenant/src/contract.rs +++ b/contracts/two-party-pol-covenant/src/contract.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Decimal, Uint128, CosmosMsg, WasmMsg, SubMsg, Reply, + to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Decimal, Uint128, CosmosMsg, WasmMsg, SubMsg, Reply, Coin, }; use covenant_astroport_liquid_pooler::msg::{PresetAstroLiquidPoolerFields, SingleSideLpLimits, AssetData}; @@ -56,12 +56,18 @@ pub fn instantiate( ragequit_config: msg.ragequit_config.unwrap_or(RagequitConfig::Disabled), deposit_deadline: msg.deposit_deadline, party_a: PresetPolParty { - contribution: msg.party_a_config.contribution.clone(), + contribution: Coin { + denom: msg.party_a_config.ibc_denom.to_string(), + amount: msg.party_a_config.contribution.amount, + }, addr: msg.party_a_config.addr, allocation: Decimal::from_ratio(msg.party_a_share, Uint128::new(100)), }, party_b: PresetPolParty { - contribution: msg.party_b_config.contribution.clone(), + contribution: Coin { + denom: msg.party_b_config.ibc_denom.to_string(), + amount: msg.party_b_config.contribution.amount, + }, addr: msg.party_b_config.addr, allocation: Decimal::from_ratio(msg.party_b_share, Uint128::new(100)), }, @@ -114,8 +120,8 @@ pub fn instantiate( asset_b_denom: msg.party_b_config.ibc_denom, }, single_side_lp_limits: SingleSideLpLimits { - asset_a_limit: Uint128::one(), - asset_b_limit: Uint128::one(), + asset_a_limit: Uint128::new(10000), + asset_b_limit: Uint128::new(100000), }, label: format!("{}_liquid_pooler", msg.label), code_id: msg.contract_codes.liquid_pooler_code, @@ -407,7 +413,10 @@ pub fn handle_party_a_ibc_forwarder_reply( admin: Some(env.contract.address.to_string()), code_id: preset_party_b_ibc_forwarder.code_id, msg: to_binary( - &preset_party_b_ibc_forwarder.to_instantiate_msg(clock_addr.to_string(), holder.to_string()), + &preset_party_b_ibc_forwarder.to_instantiate_msg( + clock_addr.to_string(), + holder.to_string(), + ), )?, funds: vec![], label: preset_party_b_ibc_forwarder.label, diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index 926b7d08..4bb5ba09 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -1,4 +1,5 @@ use astroport::{asset::Asset, pair::Cw20HookMsg}; +use cosmos_sdk_proto::cosmos::bank::v1beta1::{MsgMultiSend, Output}; use cosmwasm_std::{ to_binary, BankMsg, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Response, StdResult, WasmMsg, @@ -206,8 +207,8 @@ fn try_deposit(deps: DepsMut, env: Env, _info: MessageInfo) -> Result Result Date: Wed, 25 Oct 2023 13:49:33 +0200 Subject: [PATCH 129/586] ictest holder receives lp tokens --- .../astroport-liquid-pooler/src/contract.rs | 18 +++---- .../astroport-liquid-pooler/src/error.rs | 3 ++ .../interchaintest/two_party_pol_test.go | 54 ++++--------------- 3 files changed, 23 insertions(+), 52 deletions(-) diff --git a/contracts/astroport-liquid-pooler/src/contract.rs b/contracts/astroport-liquid-pooler/src/contract.rs index d6ca70f7..d69dbd35 100644 --- a/contracts/astroport-liquid-pooler/src/contract.rs +++ b/contracts/astroport-liquid-pooler/src/contract.rs @@ -29,8 +29,6 @@ use crate::state::{CLOCK_ADDRESS, CONTRACT_STATE}; const CONTRACT_NAME: &str = "crates.io:covenant-astroport-liquid-pooler"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -// type QueryDeps<'a> = Deps<'a, NeutronQuery>; -// type ExecuteDeps<'a> = DepsMut<'a, NeutronQuery>; const DOUBLE_SIDED_REPLY_ID: u64 = 321u64; const SINGLE_SIDED_REPLY_ID: u64 = 322u64; @@ -48,15 +46,11 @@ pub fn instantiate( let clock_addr = deps.api.addr_validate(&msg.clock_address)?; let pool_addr = deps.api.addr_validate(&msg.pool_address)?; - // TODO: instantiate2 / remove this field from instantiateMsg - let holder_addr = env.contract.address; - // contract starts at Instantiated state CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; // store the relevant module addresses CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; - HOLDER_ADDRESS.save(deps.storage, &holder_addr)?; ASSETS.save(deps.storage, &msg.assets)?; let decimal_range = DecimalRange::try_from( @@ -85,7 +79,6 @@ pub fn instantiate( Ok(Response::default() .add_attribute("method", "lp_instantiate") .add_attribute("clock_addr", clock_addr) - .add_attribute("holder_addr", holder_addr) .add_attribute("asset_a_denom", msg.assets.asset_a_denom) .add_attribute("asset_b_denom", msg.assets.asset_b_denom) .add_attributes(lp_config.to_response_attributes())) @@ -191,7 +184,11 @@ fn try_get_double_side_lp_submsg( lp_config: LpConfig, asset_data: AssetData, ) -> Result, ContractError> { - let holder_address = HOLDER_ADDRESS.load(deps.storage)?; + let holder_address = match HOLDER_ADDRESS.may_load(deps.storage)? { + Some(addr) => addr, + None => return Err(ContractError::MissingHolderError {}), + }; + // we thus find the required token amount to enter into the position using all available b tokens: let required_token_a_amount = pool_token_ratio.checked_mul_uint128(token_b.amount)?; @@ -262,7 +259,10 @@ fn try_get_single_side_lp_submsg( lp_config: LpConfig, asset_data: AssetData, ) -> Result, ContractError> { - let holder_address = HOLDER_ADDRESS.load(deps.storage)?; + let holder_address = match HOLDER_ADDRESS.may_load(deps.storage)? { + Some(addr) => addr, + None => return Err(ContractError::MissingHolderError {}), + }; let assets = asset_data.to_asset_vec(coin_a.amount, coin_b.amount); diff --git a/contracts/astroport-liquid-pooler/src/error.rs b/contracts/astroport-liquid-pooler/src/error.rs index 40ae9aec..0d754f96 100644 --- a/contracts/astroport-liquid-pooler/src/error.rs +++ b/contracts/astroport-liquid-pooler/src/error.rs @@ -36,4 +36,7 @@ pub enum ContractError { #[error("Price range error")] PriceRangeError {}, + + #[error("Unknown holder address. Migrate update to set it.")] + MissingHolderError {}, } diff --git a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go index ce78f582..56200bbe 100644 --- a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go +++ b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go @@ -1071,13 +1071,13 @@ func TestTwoPartyPol(t *testing.T) { err := cosmosOsmosis.SendFunds(ctx, osmoUser.KeyName, ibc.WalletAmount{ Address: partyBDepositAddress, Denom: nativeOsmoDenom, - Amount: int64(osmoContributionAmount + 1000), + Amount: int64(osmoContributionAmount + 1), }) require.NoError(t, err, "failed to fund osmo forwarder") err = cosmosAtom.SendFunds(ctx, gaiaUser.KeyName, ibc.WalletAmount{ Address: partyADepositAddress, Denom: nativeAtomDenom, - Amount: int64(atomContributionAmount + 1000), + Amount: int64(atomContributionAmount + 1), }) require.NoError(t, err, "failed to fund gaia forwarder") @@ -1086,10 +1086,10 @@ func TestTwoPartyPol(t *testing.T) { bal, err := cosmosAtom.GetBalance(ctx, partyADepositAddress, nativeAtomDenom) require.NoError(t, err, "failed to query bal") - require.Equal(t, int64(atomContributionAmount+1000), bal) + require.Equal(t, int64(atomContributionAmount+1), bal) bal, err = cosmosOsmosis.GetBalance(ctx, partyBDepositAddress, nativeOsmoDenom) require.NoError(t, err, "failed to query bal") - require.Equal(t, int64(osmoContributionAmount+1000), bal) + require.Equal(t, int64(osmoContributionAmount+1), bal) }) t.Run("tick until forwarders forward the funds to holder", func(t *testing.T) { @@ -1098,24 +1098,11 @@ func TestTwoPartyPol(t *testing.T) { require.NoError(t, err, "failed to query holder osmo bal") holderAtomBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronAtomIbcDenom) require.NoError(t, err, "failed to query holder atom bal") + println("holder atom bal: ", holderAtomBal) + println("holder osmo bal: ", holderOsmoBal) - var response CovenantAddressQueryResponse - type ContractState struct{} - type ContractStateQuery struct { - ContractState ContractState `json:"contract_state"` - } - contractStateQuery := ContractStateQuery{ - ContractState: ContractState{}, - } - - require.NoError(t, - cosmosNeutron.QueryContract(ctx, holderAddress, contractStateQuery, &response), - "failed to query holder state") - holderState := response.Data - - if holderAtomBal != 0 && holderOsmoBal != 0 || holderState == "complete" { - println("holder atom bal: ", holderAtomBal) - println("holder osmo bal: ", holderOsmoBal) + if holderAtomBal == int64(atomContributionAmount) && holderOsmoBal == int64(osmoContributionAmount) { + println("\nholder received atom & osmo\n") break } else { tickClock() @@ -1129,32 +1116,13 @@ func TestTwoPartyPol(t *testing.T) { require.NoError(t, err, "failed to query liquidPooler osmo bal") liquidPoolerAtomBal, err := cosmosNeutron.GetBalance(ctx, liquidPoolerAddress, neutronAtomIbcDenom) require.NoError(t, err, "failed to query liquidPooler atom bal") + holderLpTokenBal := queryLpTokenBalance(liquidityTokenAddress, holderAddress) - var response CovenantAddressQueryResponse - type ContractState struct{} - type ContractStateQuery struct { - ContractState ContractState `json:"contract_state"` - } - contractStateQuery := ContractStateQuery{ - ContractState: ContractState{}, - } - - require.NoError(t, - cosmosNeutron.QueryContract(ctx, holderAddress, contractStateQuery, &response), - "failed to query forwarder A state") - holderState := response.Data - println("holder state: ", holderState) println("liquid pooler atom bal: ", liquidPoolerAtomBal) println("liquid pooler osmo bal: ", liquidPoolerOsmoBal) - - holderLpTokenBal := queryLpTokenBalance(liquidityTokenAddress, holderAddress) println("holder lp token balance: ", holderLpTokenBal) - holderLpBal, err := strconv.ParseUint(holderLpTokenBal, 10, 64) - if err != nil { - panic(err) - } - if liquidPoolerOsmoBal != 0 && liquidPoolerAtomBal != 0 || holderLpBal != 0 { + if liquidPoolerOsmoBal == int64(osmoContributionAmount) && liquidPoolerAtomBal == int64(atomContributionAmount) { break } else { tickClock() @@ -1180,7 +1148,7 @@ func TestTwoPartyPol(t *testing.T) { }) t.Run("tick until routers route the funds after POL expires", func(t *testing.T) { - // TODO + }) }) }) From 229a29a8a784cad7429ceaec9078c507a372179c Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 25 Oct 2023 14:22:34 +0200 Subject: [PATCH 130/586] re-enabling expected pool ratio checks --- contracts/astroport-liquid-pooler/src/contract.rs | 4 ++-- contracts/astroport-liquid-pooler/src/msg.rs | 3 --- contracts/two-party-pol-covenant/src/contract.rs | 2 -- .../tests/interchaintest/two_party_pol_test.go | 11 ++++++++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/contracts/astroport-liquid-pooler/src/contract.rs b/contracts/astroport-liquid-pooler/src/contract.rs index d69dbd35..00698f3c 100644 --- a/contracts/astroport-liquid-pooler/src/contract.rs +++ b/contracts/astroport-liquid-pooler/src/contract.rs @@ -35,7 +35,7 @@ const SINGLE_SIDED_REPLY_ID: u64 = 322u64; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, - env: Env, + _env: Env, _info: MessageInfo, msg: InstantiateMsg, ) -> Result { @@ -125,7 +125,7 @@ fn try_lp(mut deps: DepsMut, env: Env) -> Result { )?; let a_to_b_ratio = Decimal::from_ratio(pool_token_a_bal, pool_token_b_bal); // validate the current pool ratio against our expectations - // lp_config.expected_pool_ratio_range.is_within_range(a_to_b_ratio)?; + lp_config.expected_pool_ratio_range.is_within_range(a_to_b_ratio)?; // first we query our own balances and filter out any unexpected denoms let bal_coins = deps diff --git a/contracts/astroport-liquid-pooler/src/msg.rs b/contracts/astroport-liquid-pooler/src/msg.rs index 4721e940..ac36cbff 100644 --- a/contracts/astroport-liquid-pooler/src/msg.rs +++ b/contracts/astroport-liquid-pooler/src/msg.rs @@ -9,7 +9,6 @@ use crate::error::ContractError; pub struct InstantiateMsg { pub pool_address: String, pub clock_address: String, - pub holder_address: String, pub slippage_tolerance: Option, pub autostake: Option, pub assets: AssetData, @@ -35,12 +34,10 @@ impl PresetAstroLiquidPoolerFields { &self, pool_address: String, clock_address: String, - holder_address: String, ) -> InstantiateMsg { InstantiateMsg { pool_address, clock_address, - holder_address, slippage_tolerance: self.slippage_tolerance, autostake: self.autostake.clone(), assets: self.assets.clone(), diff --git a/contracts/two-party-pol-covenant/src/contract.rs b/contracts/two-party-pol-covenant/src/contract.rs index ae75dd45..da0a5b9c 100644 --- a/contracts/two-party-pol-covenant/src/contract.rs +++ b/contracts/two-party-pol-covenant/src/contract.rs @@ -266,13 +266,11 @@ pub fn handle_party_b_interchain_router_reply( let clock_address = COVENANT_CLOCK_ADDR.load(deps.storage)?.to_string(); let pool_address = PRESET_HOLDER_FIELDS.load(deps.storage)?.pool_address.to_string(); - let holder_address = "replace".to_string(); let preset_liquid_pooler_fields = PRESET_LIQUID_POOLER_FIELDS.load(deps.storage)?; let instantiate_msg = preset_liquid_pooler_fields.to_instantiate_msg( pool_address, clock_address, - holder_address ); let liquid_pooler_inst_tx = CosmosMsg::Wasm(WasmMsg::Instantiate { diff --git a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go index 56200bbe..b0709582 100644 --- a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go +++ b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go @@ -1098,11 +1098,16 @@ func TestTwoPartyPol(t *testing.T) { require.NoError(t, err, "failed to query holder osmo bal") holderAtomBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronAtomIbcDenom) require.NoError(t, err, "failed to query holder atom bal") + liquidPoolerOsmoBal, err := cosmosNeutron.GetBalance(ctx, liquidPoolerAddress, neutronOsmoIbcDenom) + require.NoError(t, err, "failed to query liquidPooler osmo bal") + liquidPoolerAtomBal, err := cosmosNeutron.GetBalance(ctx, liquidPoolerAddress, neutronAtomIbcDenom) + require.NoError(t, err, "failed to query liquidPooler atom bal") println("holder atom bal: ", holderAtomBal) println("holder osmo bal: ", holderOsmoBal) - if holderAtomBal == int64(atomContributionAmount) && holderOsmoBal == int64(osmoContributionAmount) { - println("\nholder received atom & osmo\n") + if holderAtomBal == int64(atomContributionAmount) && holderOsmoBal == int64(osmoContributionAmount) || + liquidPoolerAtomBal == int64(atomContributionAmount) && liquidPoolerOsmoBal == int64(osmoContributionAmount) { + println("\nholder/liquidpooler received atom & osmo\n") break } else { tickClock() @@ -1148,7 +1153,7 @@ func TestTwoPartyPol(t *testing.T) { }) t.Run("tick until routers route the funds after POL expires", func(t *testing.T) { - + // todo }) }) }) From c733190355dfff87769ef11bc9f004ac71ea13c9 Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 26 Oct 2023 13:23:04 +0200 Subject: [PATCH 131/586] adding pool pair type checks prior to LPing --- contracts/astroport-liquid-pooler/src/contract.rs | 13 +++++++++++-- contracts/astroport-liquid-pooler/src/error.rs | 3 +++ contracts/astroport-liquid-pooler/src/msg.rs | 7 ++++++- contracts/two-party-pol-covenant/Cargo.toml | 1 + contracts/two-party-pol-covenant/src/contract.rs | 1 + contracts/two-party-pol-covenant/src/msg.rs | 2 ++ .../tests/interchaintest/two_party_pol_test.go | 4 ++++ .../tests/interchaintest/types.go | 1 + 8 files changed, 29 insertions(+), 3 deletions(-) diff --git a/contracts/astroport-liquid-pooler/src/contract.rs b/contracts/astroport-liquid-pooler/src/contract.rs index 00698f3c..eae285d8 100644 --- a/contracts/astroport-liquid-pooler/src/contract.rs +++ b/contracts/astroport-liquid-pooler/src/contract.rs @@ -8,9 +8,9 @@ use covenant_clock::helpers::verify_clock; use cw2::set_contract_version; use astroport::{ - asset::Asset, + asset::{Asset, PairInfo}, pair::{ExecuteMsg::ProvideLiquidity, PoolResponse}, - DecimalCheckedOps, + DecimalCheckedOps, factory::PairsResponse, }; use crate::{ @@ -64,6 +64,7 @@ pub fn instantiate( autostake: msg.autostake, slippage_tolerance: msg.slippage_tolerance, expected_pool_ratio_range: decimal_range, + pair_type: msg.pair_type, }; LP_CONFIG.save(deps.storage, &lp_config)?; @@ -115,6 +116,14 @@ fn try_lp(mut deps: DepsMut, env: Env) -> Result { let asset_data = ASSETS.load(deps.storage)?; let lp_config = LP_CONFIG.load(deps.storage)?; + // validate that the pool did not migrate to a new pair type + let pool_response: PairInfo = deps + .querier + .query_wasm_smart(&lp_config.pool_address, &astroport::pair::QueryMsg::Pair {})?; + if pool_response.pair_type != lp_config.pair_type { + return Err(ContractError::PairTypeMismatch {}) + } + let pool_response: PoolResponse = deps .querier .query_wasm_smart(&lp_config.pool_address, &astroport::pair::QueryMsg::Pool {})?; diff --git a/contracts/astroport-liquid-pooler/src/error.rs b/contracts/astroport-liquid-pooler/src/error.rs index 0d754f96..e053f83c 100644 --- a/contracts/astroport-liquid-pooler/src/error.rs +++ b/contracts/astroport-liquid-pooler/src/error.rs @@ -39,4 +39,7 @@ pub enum ContractError { #[error("Unknown holder address. Migrate update to set it.")] MissingHolderError {}, + + #[error("Pair type mismatch")] + PairTypeMismatch {}, } diff --git a/contracts/astroport-liquid-pooler/src/msg.rs b/contracts/astroport-liquid-pooler/src/msg.rs index ac36cbff..4ca79a5e 100644 --- a/contracts/astroport-liquid-pooler/src/msg.rs +++ b/contracts/astroport-liquid-pooler/src/msg.rs @@ -1,4 +1,4 @@ -use astroport::asset::{Asset, AssetInfo}; +use astroport::{asset::{Asset, AssetInfo}, factory::PairType}; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Attribute, Binary, Decimal, Uint128}; use covenant_macros::{clocked, covenant_clock_address, covenant_deposit_address}; @@ -15,6 +15,7 @@ pub struct InstantiateMsg { pub single_side_lp_limits: SingleSideLpLimits, pub expected_pool_ratio: Decimal, pub acceptable_pool_ratio_delta: Decimal, + pub pair_type: PairType, } #[cw_serde] @@ -27,6 +28,7 @@ pub struct PresetAstroLiquidPoolerFields { pub code_id: u64, pub expected_pool_ratio: Decimal, pub acceptable_pool_ratio_delta: Decimal, + pub pair_type: PairType, } impl PresetAstroLiquidPoolerFields { @@ -44,6 +46,7 @@ impl PresetAstroLiquidPoolerFields { single_side_lp_limits: self.single_side_lp_limits.clone(), expected_pool_ratio: self.expected_pool_ratio, acceptable_pool_ratio_delta: self.acceptable_pool_ratio_delta, + pair_type: self.pair_type.clone(), } } } @@ -87,6 +90,8 @@ pub struct LpConfig { pub slippage_tolerance: Option, /// expected price range pub expected_pool_ratio_range: DecimalRange, + /// pair type specified in the covenant + pub pair_type: PairType, } impl LpConfig { diff --git a/contracts/two-party-pol-covenant/Cargo.toml b/contracts/two-party-pol-covenant/Cargo.toml index 42cad9b6..54f76d4b 100644 --- a/contracts/two-party-pol-covenant/Cargo.toml +++ b/contracts/two-party-pol-covenant/Cargo.toml @@ -48,6 +48,7 @@ covenant-ibc-forwarder = { workspace = true, features = ["library"] } covenant-interchain-router = { workspace = true, features = ["library"] } covenant-two-party-pol-holder = { workspace = true, features = ["library"] } covenant-astroport-liquid-pooler = { workspace = true, features = ["library"] } +astroport = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/two-party-pol-covenant/src/contract.rs b/contracts/two-party-pol-covenant/src/contract.rs index da0a5b9c..59453090 100644 --- a/contracts/two-party-pol-covenant/src/contract.rs +++ b/contracts/two-party-pol-covenant/src/contract.rs @@ -127,6 +127,7 @@ pub fn instantiate( code_id: msg.contract_codes.liquid_pooler_code, expected_pool_ratio: msg.expected_pool_ratio, acceptable_pool_ratio_delta: msg.acceptable_pool_ratio_delta, + pair_type: msg.pool_pair_type, }; PRESET_CLOCK_FIELDS.save(deps.storage, &preset_clock_fields)?; diff --git a/contracts/two-party-pol-covenant/src/msg.rs b/contracts/two-party-pol-covenant/src/msg.rs index 0237d707..66b0b0e0 100644 --- a/contracts/two-party-pol-covenant/src/msg.rs +++ b/contracts/two-party-pol-covenant/src/msg.rs @@ -1,3 +1,4 @@ +use astroport::factory::PairType; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Uint128, Uint64, Coin, Decimal}; use covenant_two_party_pol_holder::msg::RagequitConfig; @@ -24,6 +25,7 @@ pub struct InstantiateMsg { pub party_b_share: Uint64, pub expected_pool_ratio: Decimal, pub acceptable_pool_ratio_delta: Decimal, + pub pool_pair_type: PairType, } #[cw_serde] diff --git a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go index b0709582..31a41756 100644 --- a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go +++ b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go @@ -799,6 +799,9 @@ func TestTwoPartyPol(t *testing.T) { } poolAddress := stableswapAddress + pairType := PairType{ + Stable: struct{}{}, + } covenantMsg := CovenantInstantiateMsg{ Label: "two-party-pol-covenant", @@ -815,6 +818,7 @@ func TestTwoPartyPol(t *testing.T) { PartyBShare: "50", ExpectedPoolRatio: "0.1", AcceptablePoolRatioDelta: "0.09", + PairType: pairType, } str, err := json.Marshal(covenantMsg) require.NoError(t, err, "Failed to marshall CovenantInstantiateMsg") diff --git a/two-party-pol-covenant/tests/interchaintest/types.go b/two-party-pol-covenant/tests/interchaintest/types.go index e872eef1..51cea225 100644 --- a/two-party-pol-covenant/tests/interchaintest/types.go +++ b/two-party-pol-covenant/tests/interchaintest/types.go @@ -21,6 +21,7 @@ type CovenantInstantiateMsg struct { PartyBShare string `json:"party_b_share"` ExpectedPoolRatio string `json:"expected_pool_ratio"` AcceptablePoolRatioDelta string `json:"acceptable_pool_ratio_delta"` + PairType PairType `json:"pool_pair_type"` } type ContractCodeIds struct { From 493139a4db8037514191506e2f4a45fe19af9d08 Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 27 Oct 2023 17:33:34 +0200 Subject: [PATCH 132/586] unit tests --- Cargo.lock | 1 + contracts/astroport-liquid-pooler/Cargo.toml | 1 + .../astroport-liquid-pooler/src/contract.rs | 6 +- contracts/astroport-liquid-pooler/src/lib.rs | 4 + .../src/suite_test/mod.rs | 2 + .../src/suite_test/suite.rs | 664 ++++++++++++++++++ .../src/suite_test/tests.rs | 199 ++++++ 7 files changed, 874 insertions(+), 3 deletions(-) create mode 100644 contracts/astroport-liquid-pooler/src/suite_test/mod.rs create mode 100644 contracts/astroport-liquid-pooler/src/suite_test/suite.rs create mode 100644 contracts/astroport-liquid-pooler/src/suite_test/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 389ac4ea..2e200299 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -343,6 +343,7 @@ dependencies = [ "cosmwasm-std", "covenant-clock", "covenant-macros", + "covenant-two-party-pol-holder", "cw-multi-test", "cw-storage-plus 1.1.0", "cw-utils 1.0.2", diff --git a/contracts/astroport-liquid-pooler/Cargo.toml b/contracts/astroport-liquid-pooler/Cargo.toml index a60a78d8..f7d7c577 100644 --- a/contracts/astroport-liquid-pooler/Cargo.toml +++ b/contracts/astroport-liquid-pooler/Cargo.toml @@ -59,3 +59,4 @@ astroport-factory = {git = "https://github.com/astroport-fi/astroport-core.git" astroport-native-coin-registry = {git = "https://github.com/astroport-fi/astroport-core.git"} astroport-pair-stable = {git = "https://github.com/astroport-fi/astroport-core.git"} cw1-whitelist = "1.1.0" +covenant-two-party-pol-holder = { workspace = true } diff --git a/contracts/astroport-liquid-pooler/src/contract.rs b/contracts/astroport-liquid-pooler/src/contract.rs index eae285d8..9d2bfd05 100644 --- a/contracts/astroport-liquid-pooler/src/contract.rs +++ b/contracts/astroport-liquid-pooler/src/contract.rs @@ -10,7 +10,7 @@ use cw2::set_contract_version; use astroport::{ asset::{Asset, PairInfo}, pair::{ExecuteMsg::ProvideLiquidity, PoolResponse}, - DecimalCheckedOps, factory::PairsResponse, + DecimalCheckedOps, }; use crate::{ @@ -127,6 +127,7 @@ fn try_lp(mut deps: DepsMut, env: Env) -> Result { let pool_response: PoolResponse = deps .querier .query_wasm_smart(&lp_config.pool_address, &astroport::pair::QueryMsg::Pool {})?; + let (pool_token_a_bal, pool_token_b_bal) = get_pool_asset_amounts( pool_response.assets, &asset_data.asset_a_denom.as_str(), @@ -148,7 +149,7 @@ fn try_lp(mut deps: DepsMut, env: Env) -> Result { // depending on available balances we attempt a different action: match (coin_a.amount.is_zero(), coin_b.amount.is_zero()) { - // one balance is non-zero, we attempt single-side + // exactly one balance is non-zero, we attempt single-side (true, false) | (false, true) => { let single_sided_submsg = try_get_single_side_lp_submsg(deps.branch(), coin_a, coin_b, lp_config, asset_data)?; @@ -162,7 +163,6 @@ fn try_lp(mut deps: DepsMut, env: Env) -> Result { (false, false) => { let double_sided_submsg = try_get_double_side_lp_submsg(deps.branch(), coin_a, coin_b, a_to_b_ratio, pool_token_a_bal, pool_token_b_bal, lp_config, asset_data)?; - if let Some(msg) = double_sided_submsg { return Ok(Response::default() .add_submessage(msg) diff --git a/contracts/astroport-liquid-pooler/src/lib.rs b/contracts/astroport-liquid-pooler/src/lib.rs index 0faea8f4..3b47dbd4 100644 --- a/contracts/astroport-liquid-pooler/src/lib.rs +++ b/contracts/astroport-liquid-pooler/src/lib.rs @@ -6,3 +6,7 @@ pub mod contract; pub mod error; pub mod msg; pub mod state; + +#[allow(clippy::unwrap_used)] +#[cfg(test)] +mod suite_test; diff --git a/contracts/astroport-liquid-pooler/src/suite_test/mod.rs b/contracts/astroport-liquid-pooler/src/suite_test/mod.rs new file mode 100644 index 00000000..7b881830 --- /dev/null +++ b/contracts/astroport-liquid-pooler/src/suite_test/mod.rs @@ -0,0 +1,2 @@ +mod suite; +mod tests; diff --git a/contracts/astroport-liquid-pooler/src/suite_test/suite.rs b/contracts/astroport-liquid-pooler/src/suite_test/suite.rs new file mode 100644 index 00000000..b6a59d49 --- /dev/null +++ b/contracts/astroport-liquid-pooler/src/suite_test/suite.rs @@ -0,0 +1,664 @@ +use astroport::{ + asset::{Asset, AssetInfo, PairInfo}, + factory::{PairConfig, PairType}, + pair::{Cw20HookMsg, PoolResponse, SimulationResponse, StablePoolParams}, +}; + +use cosmwasm_std::{ + testing::MockApi, to_binary, Addr, Coin, Decimal, Empty, MemoryStorage, QueryRequest, Uint128, + Uint64, WasmQuery, +}; +use cw20::Cw20ExecuteMsg; +use cw_multi_test::{ + App, AppResponse, BankKeeper, BankSudo, Contract, ContractWrapper, Executor, FailingModule, + SudoMsg, WasmKeeper, +}; +use neutron_sdk::bindings::{msg::NeutronMsg, query::NeutronQuery}; + +use crate::msg::{AssetData, InstantiateMsg, LpConfig, QueryMsg, SingleSideLpLimits, MigrateMsg}; +use astroport::factory::InstantiateMsg as FactoryInstantiateMsg; +use astroport::native_coin_registry::InstantiateMsg as NativeCoinRegistryInstantiateMsg; +use astroport::pair::InstantiateMsg as PairInstantiateMsg; +use astroport::token::InstantiateMsg as TokenInstantiateMsg; +use cw1_whitelist::msg::InstantiateMsg as WhitelistInstantiateMsg; + +pub const CREATOR_ADDR: &str = "creator"; +pub const TOKEN_A_DENOM: &str = "uatom"; +pub const TOKEN_B_DENOM: &str = "untrn"; + +fn astro_token() -> Box> { + Box::new( + ContractWrapper::new( + astroport_token::contract::execute, + astroport_token::contract::instantiate, + astroport_token::contract::query, + ) + .with_migrate(astroport_token::contract::migrate), + ) +} + +fn astro_whitelist() -> Box> { + Box::new(ContractWrapper::new( + astroport_whitelist::contract::instantiate, + astroport_whitelist::contract::instantiate, + astroport_whitelist::contract::query, + )) +} + +fn astro_factory() -> Box> { + Box::new( + ContractWrapper::new( + astroport_factory::contract::execute, + astroport_factory::contract::instantiate, + astroport_factory::contract::query, + ) + .with_migrate(astroport_factory::contract::migrate) + .with_reply(astroport_factory::contract::reply), + ) +} + +fn astro_pair_stable() -> Box> { + Box::new( + ContractWrapper::new( + astroport_pair_stable::contract::execute, + astroport_pair_stable::contract::instantiate, + astroport_pair_stable::contract::query, + ) + .with_reply(astroport_pair_stable::contract::reply) + .with_migrate(astroport_pair_stable::contract::migrate), + ) +} + +fn astro_coin_registry() -> Box> { + let registry_contract = ContractWrapper::new( + astroport_native_coin_registry::contract::execute, + astroport_native_coin_registry::contract::instantiate, + astroport_native_coin_registry::contract::query, + ) + .with_migrate(astroport_native_coin_registry::contract::migrate); + + Box::new(registry_contract) +} + +fn lper_contract() -> Box> { + let lp_contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ) + .with_reply(crate::contract::reply) + .with_migrate(crate::contract::migrate); + + Box::new(lp_contract) +} + +fn clock_contract() -> Box> { + Box::new( + ContractWrapper::new( + covenant_clock::contract::execute, + covenant_clock::contract::instantiate, + covenant_clock::contract::query, + ) + .with_reply(covenant_clock::contract::reply) + .with_migrate(covenant_clock::contract::migrate), + ) +} + +#[allow(unused)] +pub type BaseApp = App< + BankKeeper, + MockApi, + MemoryStorage, + FailingModule, + WasmKeeper, +>; +#[allow(unused)] +pub(crate) struct Suite { + pub app: App, + pub admin: Addr, + pub lp_token: Addr, + // (token_code, contract_address) + pub token: u64, + pub whitelist: (u64, String), + pub factory: (u64, String), + pub stable_pair: (u64, String), + pub coin_registry: (u64, String), + pub liquid_pooler: (u64, String), + pub clock_addr: String, + pub holder_addr: String, +} + +pub(crate) struct SuiteBuilder { + pub lp_instantiate: InstantiateMsg, + pub token_instantiate: TokenInstantiateMsg, + pub whitelist_instantiate: WhitelistInstantiateMsg, + pub factory_instantiate: FactoryInstantiateMsg, + pub stablepair_instantiate: PairInstantiateMsg, + pub registry_instantiate: NativeCoinRegistryInstantiateMsg, + pub clock_instantiate: covenant_clock::msg::InstantiateMsg, +} + +impl Default for SuiteBuilder { + fn default() -> Self { + Self { + lp_instantiate: InstantiateMsg { + clock_address: "clock-addr".to_string(), + pool_address: "lp-addr".to_string(), + slippage_tolerance: Some(Decimal::one()), + autostake: Some(false), + assets: AssetData { + asset_a_denom: "uatom".to_string(), + asset_b_denom: "untrn".to_string(), + }, + single_side_lp_limits: SingleSideLpLimits { + asset_a_limit: Uint128::new(100), + asset_b_limit: Uint128::new(1000), + }, + expected_pool_ratio: Decimal::from_ratio(Uint128::new(1), Uint128::new(10)), + acceptable_pool_ratio_delta: Decimal::from_ratio(Uint128::one(), Uint128::new(100)), + pair_type: PairType::Stable {}, + }, + token_instantiate: TokenInstantiateMsg { + name: "nativetoken".to_string(), + symbol: "ntk".to_string(), + decimals: 20, + initial_balances: vec![], + mint: None, + marketing: None, + }, + whitelist_instantiate: WhitelistInstantiateMsg { + admins: vec![CREATOR_ADDR.to_string()], + mutable: false, + }, + factory_instantiate: FactoryInstantiateMsg { + pair_configs: vec![PairConfig { + code_id: u64::MAX, + pair_type: astroport::factory::PairType::Stable {}, + total_fee_bps: 0, + maker_fee_bps: 0, + is_disabled: false, + is_generator_disabled: true, + }], + token_code_id: u64::MAX, + fee_address: None, + generator_address: None, + owner: CREATOR_ADDR.to_string(), + whitelist_code_id: u64::MAX, + coin_registry_address: "TODO".to_string(), + }, + stablepair_instantiate: PairInstantiateMsg { + asset_infos: vec![ + astroport::asset::AssetInfo::NativeToken { + denom: TOKEN_A_DENOM.to_string(), + }, + astroport::asset::AssetInfo::NativeToken { + denom: TOKEN_B_DENOM.to_string(), + }, + ], + token_code_id: u64::MAX, + factory_addr: "TODO".to_string(), + init_params: Some( + to_binary(&StablePoolParams { + amp: 1000, + owner: Some(CREATOR_ADDR.to_string()), + }) + .unwrap(), + ), + }, + registry_instantiate: NativeCoinRegistryInstantiateMsg { + owner: CREATOR_ADDR.to_string(), + }, + clock_instantiate: covenant_clock::msg::InstantiateMsg { + tick_max_gas: Some(Uint64::new(50000)), + // this is the lper, if any instantiate flow changes, this needs to be updated + whitelist: vec!["contract9".to_string()], + }, + } + } +} + +#[allow(unused)] +impl SuiteBuilder { + fn with_slippage_tolerance(mut self, decimal: Decimal) -> Self { + self.lp_instantiate.slippage_tolerance = Some(decimal); + self + } + + fn with_autostake(mut self, autosake: Option) -> Self { + self.lp_instantiate.autostake = autosake; + self + } + + fn with_assets(mut self, assets: AssetData) -> Self { + self.lp_instantiate.assets = assets; + self + } + + fn with_token_instantiate_msg(mut self, msg: TokenInstantiateMsg) -> Self { + self.token_instantiate = msg; + self + } + + pub fn with_expected_pair_type(mut self, pair_type: PairType) -> Self { + self.lp_instantiate.pair_type = pair_type; + self + } + + pub fn build(mut self) -> Suite { + // let mut app = BasicAppBuilder::::new_custom().build(|_,_,_| {}); + + let mut app = App::default(); + + let token_code = app.store_code(astro_token()); + let stablepair_code = app.store_code(astro_pair_stable()); + let whitelist_code = app.store_code(astro_whitelist()); + let coin_registry_code = app.store_code(astro_coin_registry()); + let factory_code = app.store_code(astro_factory()); + let lper_code = app.store_code(lper_contract()); + let clock_code = app.store_code(clock_contract()); + + let clock_address = app + .instantiate_contract( + clock_code, + Addr::unchecked(CREATOR_ADDR), + &self.clock_instantiate, + &[], + "clock", + None, + ) + .unwrap(); + + self.lp_instantiate.clock_address = clock_address.to_string(); + self.factory_instantiate.token_code_id = token_code; + self.stablepair_instantiate.token_code_id = token_code; + self.factory_instantiate.whitelist_code_id = whitelist_code; + self.factory_instantiate.pair_configs[0].code_id = stablepair_code; + + let whitelist_addr = app + .instantiate_contract( + whitelist_code, + Addr::unchecked(CREATOR_ADDR), + &self.whitelist_instantiate, + &[], + "whitelist", + None, + ) + .unwrap(); + + app.update_block(|b: &mut cosmwasm_std::BlockInfo| b.height += 5); + + let coin_registry_addr = app + .instantiate_contract( + coin_registry_code, + Addr::unchecked(CREATOR_ADDR), + &self.registry_instantiate, + &[], + "native coin registry", + None, + ) + .unwrap(); + app.update_block(|b| b.height += 5); + + // add coins to registry + app.execute_contract( + Addr::unchecked(CREATOR_ADDR), + coin_registry_addr.clone(), + &astroport::native_coin_registry::ExecuteMsg::Add { + native_coins: vec![ + (TOKEN_A_DENOM.to_string(), 6), + (TOKEN_B_DENOM.to_string(), 6), + ], + }, + &[], + ) + .unwrap(); + app.update_block(|b| b.height += 5); + + self.factory_instantiate.coin_registry_address = coin_registry_addr.to_string(); + + let factory_addr = app + .instantiate_contract( + factory_code, + Addr::unchecked(CREATOR_ADDR), + &self.factory_instantiate, + &[], + "factory", + None, + ) + .unwrap(); + app.update_block(|b| b.height += 5); + + let init_pair_msg = astroport::factory::ExecuteMsg::CreatePair { + pair_type: PairType::Stable {}, + asset_infos: vec![ + AssetInfo::NativeToken { + denom: TOKEN_A_DENOM.to_string(), + }, + AssetInfo::NativeToken { + denom: TOKEN_B_DENOM.to_string(), + }, + ], + init_params: Some( + to_binary(&StablePoolParams { + owner: Some(CREATOR_ADDR.to_string()), + amp: 10, + }) + .unwrap(), + ), + }; + app.update_block(|b| b.height += 5); + + let pair_msg = app + .execute_contract( + Addr::unchecked(CREATOR_ADDR), + factory_addr.clone(), + &init_pair_msg, + &[], + ) + .unwrap(); + app.update_block(|b| b.height += 5); + + let pair_info: PairInfo = app + .wrap() + .query_wasm_smart( + &factory_addr, + &astroport::factory::QueryMsg::Pair { + asset_infos: vec![ + AssetInfo::NativeToken { + denom: TOKEN_A_DENOM.to_string(), + }, + AssetInfo::NativeToken { + denom: TOKEN_B_DENOM.to_string(), + }, + ], + }, + ) + .unwrap(); + + self.stablepair_instantiate.factory_addr = factory_addr.to_string(); + app.update_block(|b| b.height += 5); + + let stable_pair_addr = app + .instantiate_contract( + stablepair_code, + Addr::unchecked(CREATOR_ADDR), + &self.stablepair_instantiate, + &[], + "stableswap", + None, + ) + .unwrap(); + + app.update_block(|b| b.height += 5); + + self.lp_instantiate.pool_address = stable_pair_addr.to_string(); + + let lper_address = app + .instantiate_contract( + lper_code, + Addr::unchecked(CREATOR_ADDR), + &self.lp_instantiate, + &[], + "lper contract", + Some(CREATOR_ADDR.to_string()), + ) + .unwrap(); + app.update_block(|b| b.height += 5); + + app.migrate_contract( + Addr::unchecked(CREATOR_ADDR), + lper_address.clone(), + &MigrateMsg::UpdateConfig { + clock_addr: None, + holder_address: Some(CREATOR_ADDR.to_string()), + assets: None, + lp_config: None, + }, + lper_code, + ).unwrap(); + + Suite { + app, + admin: Addr::unchecked(CREATOR_ADDR), + lp_token: pair_info.liquidity_token, + token: token_code, + whitelist: (whitelist_code, whitelist_addr.to_string()), + factory: (factory_code, factory_addr.to_string()), + stable_pair: (stablepair_code, stable_pair_addr.to_string()), + coin_registry: (coin_registry_code, coin_registry_addr.to_string()), + liquid_pooler: (lper_code, lper_address.to_string()), + clock_addr: clock_address.to_string(), + holder_addr: CREATOR_ADDR.to_string(), + } + } +} + +// queries +#[allow(unused)] +impl Suite { + pub fn query_clock_address(&self) -> String { + self.app + .wrap() + .query_wasm_smart(&self.liquid_pooler.1, &QueryMsg::ClockAddress {}) + .unwrap() + } + + pub fn query_lp_position(&self) -> String { + let lp_config: LpConfig = self + .app + .wrap() + .query_wasm_smart(&self.liquid_pooler.1, &QueryMsg::LpConfig {}) + .unwrap(); + lp_config.pool_address.to_string() + } + + pub fn query_contract_state(&self) -> String { + self.app + .wrap() + .query_wasm_smart(&self.liquid_pooler.1, &QueryMsg::ContractState {}) + .unwrap() + } + + pub fn query_holder_address(&self) -> String { + self.app + .wrap() + .query_wasm_smart(&self.liquid_pooler.1, &QueryMsg::HolderAddress {}) + .unwrap() + } + + pub fn query_assets(&self) -> Vec { + self.app + .wrap() + .query_wasm_smart(&self.liquid_pooler.1, &QueryMsg::Assets {}) + .unwrap() + } + + pub fn query_addr_balances(&self, addr: Addr) -> Vec { + self.app.wrap().query_all_balances(addr).unwrap() + } + + pub fn query_pool_info(&self) -> PoolResponse { + self.app + .wrap() + .query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: self.stable_pair.clone().1, + msg: to_binary(&astroport::pair::QueryMsg::Pool {}).unwrap(), + })) + .unwrap() + } + + pub fn query_pool_share(&self) -> Vec { + self.app + .wrap() + .query_wasm_smart( + Addr::unchecked(self.stable_pair.clone().1), + &astroport::pair::QueryMsg::Share { + amount: Uint128::one(), + }, + ) + .unwrap() + } + + pub fn query_simulation(&self, addr: String) -> SimulationResponse { + let query = astroport::pair::QueryMsg::Simulation { + offer_asset: Asset { + info: AssetInfo::NativeToken { + denom: TOKEN_A_DENOM.to_string(), + }, + amount: Uint128::one(), + }, + // ask_asset_info: None, + ask_asset_info: Some(AssetInfo::NativeToken { + denom: TOKEN_B_DENOM.to_string(), + }), + }; + println!("\nquerying simulation: {query:?}\n"); + + self.app.wrap().query_wasm_smart(addr, &query).unwrap() + } + + pub fn query_contract_config(&self, addr: String) -> String { + let bytes = self + .app + .wrap() + .query_wasm_raw(addr, b"config") + .transpose() + .unwrap() + .unwrap(); + match std::str::from_utf8(&bytes) { + Ok(v) => v.to_string(), + Err(e) => panic!("Invalid UTF-8 sequence: {e}"), + } + } + + pub fn query_cw20_bal(&self, token: String, addr: String) -> cw20::BalanceResponse { + self.app + .wrap() + .query_wasm_smart(token, &cw20::Cw20QueryMsg::Balance { address: addr }) + .unwrap() + } + + pub fn query_liquidity_token_addr(&self) -> astroport::asset::PairInfo { + self.app + .wrap() + .query_wasm_smart( + self.stable_pair.1.to_string(), + &astroport::pair::QueryMsg::Pair {}, + ) + .unwrap() + } +} + +// assertion helpers +impl Suite {} + +impl Suite { + // tick LPer + pub fn tick(&mut self) -> AppResponse { + self.app + .execute_contract( + Addr::unchecked(self.clock_addr.to_string()), + Addr::unchecked(self.liquid_pooler.1.to_string()), + &crate::msg::ExecuteMsg::Tick {}, + &[], + ) + .unwrap() + } + + // mint coins + pub fn mint_coins_to_addr(&mut self, address: String, denom: String, amount: Uint128) { + self.app + .sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: address, + amount: vec![Coin { amount, denom }], + })) + .unwrap(); + } + + // pass time + pub fn pass_blocks(&mut self, num: u64) { + self.app.update_block(|b| b.height += num) + } + + // withdraw liquidity from pool + #[allow(unused)] + pub fn withdraw_liquidity( + &mut self, + sender: Addr, + amount: u128, + assets: Vec, + ) -> AppResponse { + self.app + .execute_contract( + sender, + Addr::unchecked("contract6".to_string()), + &Cw20ExecuteMsg::Send { + contract: self.stable_pair.1.to_string(), + amount: Uint128::from(amount), + msg: to_binary(&Cw20HookMsg::WithdrawLiquidity { assets }).unwrap(), + }, + &[], + ) + .unwrap() + } + + pub fn provide_manual_liquidity( + &mut self, + from: String, + token_a_amt: Uint128, + token_b_amt: Uint128, + ) -> AppResponse { + let _stable_pair_addr = self.stable_pair.1.to_string(); + + let balances = vec![ + Coin { + denom: TOKEN_A_DENOM.to_string(), + amount: token_a_amt, + }, + Coin { + denom: TOKEN_B_DENOM.to_string(), + amount: token_b_amt, + }, + ]; + + let assets = vec![ + Asset { + info: AssetInfo::NativeToken { + denom: TOKEN_A_DENOM.to_string(), + }, + amount: token_a_amt, + }, + Asset { + info: AssetInfo::NativeToken { + denom: TOKEN_B_DENOM.to_string(), + }, + amount: token_b_amt, + }, + ]; + + self.mint_coins_to_addr( + from.clone(), + TOKEN_A_DENOM.to_string(), + token_a_amt, + ); + self.mint_coins_to_addr(from.clone(), TOKEN_B_DENOM.to_string(), token_b_amt); + + let provide_liquidity_msg = astroport::pair::ExecuteMsg::ProvideLiquidity { + assets, + slippage_tolerance: None, + auto_stake: Some(false), + receiver: Some(from.clone()), + }; + + self.pass_blocks(10); + + self.app + .execute_contract( + Addr::unchecked(from), + Addr::unchecked(self.stable_pair.1.to_string()), + &provide_liquidity_msg, + &balances, + ) + .unwrap() + } +} diff --git a/contracts/astroport-liquid-pooler/src/suite_test/tests.rs b/contracts/astroport-liquid-pooler/src/suite_test/tests.rs new file mode 100644 index 00000000..79650958 --- /dev/null +++ b/contracts/astroport-liquid-pooler/src/suite_test/tests.rs @@ -0,0 +1,199 @@ +use cosmwasm_std::{Addr, Uint128, Coin}; + +use crate::suite_test::suite::{TOKEN_A_DENOM, TOKEN_B_DENOM}; + +use super::suite::SuiteBuilder; + +#[test] +fn test_double_sided_lp() { + let mut suite = SuiteBuilder::default().build(); + + // fund pool with balanced amounts of underlying tokens + let token_a_amt = Uint128::new(1000); + let token_b_amt = Uint128::new(10000); + suite.provide_manual_liquidity("alice".to_string(), token_a_amt, token_b_amt); + + // fund LP contract with some tokens to provide liquidity with + suite.mint_coins_to_addr( + suite.liquid_pooler.1.to_string(), + TOKEN_A_DENOM.to_string(), + token_a_amt, + ); + suite.mint_coins_to_addr( + suite.liquid_pooler.1.to_string(), + TOKEN_B_DENOM.to_string(), + token_b_amt, + ); + + let liquid_pooler_balances = + suite.query_addr_balances(Addr::unchecked(suite.liquid_pooler.1.to_string())); + assert_eq!(liquid_pooler_balances[0].amount, token_a_amt); + assert_eq!(liquid_pooler_balances[1].amount, token_b_amt); + + let liquidity_token_addr = suite.query_liquidity_token_addr().liquidity_token.to_string(); + + let holder_balances = suite.query_cw20_bal( + liquidity_token_addr.clone(), + suite.holder_addr.to_string()); + assert_eq!(Uint128::zero(), holder_balances.balance); + + suite.pass_blocks(10); + suite.tick(); + + let liquid_pooler_balances = + suite.query_addr_balances(Addr::unchecked(suite.liquid_pooler.1.to_string())); + assert_eq!(0usize, liquid_pooler_balances.len()); + + let holder_balances = suite.query_cw20_bal( + liquidity_token_addr.to_string(), + suite.holder_addr.to_string(), + ); + assert_ne!(Uint128::zero(), holder_balances.balance); +} + +#[test] +fn test_double_and_single_sided_lp() { + let mut suite = SuiteBuilder::default().build(); + + // fund pool with balanced amounts of underlying tokens at 1:10 ratio + + suite.provide_manual_liquidity("alice".to_string(), Uint128::new(10000), Uint128::new(100000)); + + // fund LP contract with some tokens to provide liquidity with + let token_a_amt = Uint128::new(1000); + let token_b_amt = Uint128::new(9000); + suite.mint_coins_to_addr( + suite.liquid_pooler.1.to_string(), + TOKEN_A_DENOM.to_string(), + token_a_amt, + ); + suite.mint_coins_to_addr( + suite.liquid_pooler.1.to_string(), + TOKEN_B_DENOM.to_string(), + token_b_amt, + ); + let liquid_pooler_balances = + suite.query_addr_balances(Addr::unchecked(suite.liquid_pooler.1.to_string())); + assert_eq!(liquid_pooler_balances[0].amount, token_a_amt); + assert_eq!(liquid_pooler_balances[1].amount, token_b_amt); + + let liquidity_token_addr = suite.query_liquidity_token_addr().liquidity_token.to_string(); + + let holder_balances = suite.query_cw20_bal( + liquidity_token_addr.clone(), + suite.holder_addr.to_string()); + assert_eq!(Uint128::zero(), holder_balances.balance); + + suite.pass_blocks(10); + suite.tick(); + + let liquid_pooler_balances = + suite.query_addr_balances(Addr::unchecked(suite.liquid_pooler.1.to_string())); + + // assert there are 100 uatoms remaining because of missmatched pool/provision ratio + assert_eq!( + Coin { denom: TOKEN_A_DENOM.to_string(), amount: Uint128::new(100) }, + liquid_pooler_balances[0], + ); + let holder_balance = suite.query_cw20_bal( + liquidity_token_addr.to_string(), + suite.holder_addr.to_string(), + ).balance; + assert_ne!(Uint128::zero(), holder_balance); + + // tick again + suite.pass_blocks(10); + suite.tick(); + + let liquid_pooler_balances = + suite.query_addr_balances(Addr::unchecked(suite.liquid_pooler.1.to_string())); + assert_eq!(0, liquid_pooler_balances.len()); + + let new_holder_balance = suite.query_cw20_bal( + liquidity_token_addr.to_string(), + suite.holder_addr.to_string(), + ).balance; + assert_ne!(holder_balance, new_holder_balance); +} + +#[test] +#[should_panic(expected = "Pair type mismatch")] +fn test_migrated_pool_type_lp() { + let mut suite = SuiteBuilder::default() + .with_expected_pair_type(astroport::factory::PairType::Xyk {}) + .build(); + + // fund pool with balanced amounts of underlying tokens + let token_a_amt = Uint128::new(1000); + let token_b_amt = Uint128::new(10000); + suite.provide_manual_liquidity("alice".to_string(), token_a_amt, token_b_amt); + + // fund LP contract with some tokens to provide liquidity with + suite.mint_coins_to_addr( + suite.liquid_pooler.1.to_string(), + TOKEN_A_DENOM.to_string(), + token_a_amt, + ); + suite.mint_coins_to_addr( + suite.liquid_pooler.1.to_string(), + TOKEN_B_DENOM.to_string(), + token_b_amt, + ); + + suite.tick(); +} + +#[test] +#[should_panic(expected = "Price range error")] +fn test_lp_not_within_price_range_denom_a_dominant() { + let mut suite = SuiteBuilder::default() + .build(); + + // fund pool with 10:1 ratio of token a to b. + // Liquid Pooler is configured to expect 1:10 ratio. + // pool balances: [10000, 1000] + suite.provide_manual_liquidity("alice".to_string(), Uint128::new(10000), Uint128::new(1000)); + + let token_a_amt = Uint128::new(1000); + let token_b_amt = Uint128::new(10000); + // fund LP contract with some tokens to provide liquidity with + suite.mint_coins_to_addr( + suite.liquid_pooler.1.to_string(), + TOKEN_A_DENOM.to_string(), + token_a_amt, + ); + suite.mint_coins_to_addr( + suite.liquid_pooler.1.to_string(), + TOKEN_B_DENOM.to_string(), + token_b_amt, + ); + suite.tick(); +} + +#[test] +#[should_panic(expected = "Price range error")] +fn test_lp_not_within_price_range_denom_b_dominant() { + let mut suite = SuiteBuilder::default() + .build(); + + // fund pool with 1:20 ratio of token a to b. + // Liquid Pooler is configured to expect 1:10 ratio. + // pool balances: [1000, 20000] + suite.provide_manual_liquidity("alice".to_string(), Uint128::new(1000), Uint128::new(20000)); + + let token_a_amt = Uint128::new(1000); + let token_b_amt = Uint128::new(10000); + // fund LP contract with some tokens to provide liquidity with + suite.mint_coins_to_addr( + suite.liquid_pooler.1.to_string(), + TOKEN_A_DENOM.to_string(), + token_a_amt, + ); + suite.mint_coins_to_addr( + suite.liquid_pooler.1.to_string(), + TOKEN_B_DENOM.to_string(), + token_b_amt, + ); + suite.tick(); + +} From 48e692207390eeea2e9c7cd1a1f93f66fa38f878 Mon Sep 17 00:00:00 2001 From: bekauz Date: Sat, 28 Oct 2023 22:30:27 +0200 Subject: [PATCH 133/586] routers receive funds on ictest --- Cargo.lock | 1 - contracts/two-party-pol-covenant/Cargo.toml | 1 - .../two-party-pol-covenant/src/contract.rs | 6 +- contracts/two-party-pol-covenant/src/msg.rs | 6 +- .../two-party-pol-holder/src/contract.rs | 42 ++--- contracts/two-party-pol-holder/src/msg.rs | 25 +-- contracts/two-party-pol-holder/src/state.rs | 2 +- .../src/suite_tests/suite.rs | 6 +- .../interchaintest/two_party_pol_test.go | 152 ++++++++++++++++-- .../tests/interchaintest/types.go | 3 +- 10 files changed, 191 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e200299..ec895149 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -675,7 +675,6 @@ dependencies = [ "covenant-clock", "covenant-ibc-forwarder", "covenant-interchain-router", - "covenant-lp", "covenant-two-party-pol-holder", "covenant-utils", "cw-multi-test", diff --git a/contracts/two-party-pol-covenant/Cargo.toml b/contracts/two-party-pol-covenant/Cargo.toml index 54f76d4b..ee618906 100644 --- a/contracts/two-party-pol-covenant/Cargo.toml +++ b/contracts/two-party-pol-covenant/Cargo.toml @@ -41,7 +41,6 @@ base64 = { workspace = true } prost = { workspace = true } prost-types = { workspace = true } bech32 ={ workspace = true } -covenant-lp = { workspace = true, features=["library"] } covenant-clock = { workspace = true, features=["library"]} covenant-utils = { workspace = true } covenant-ibc-forwarder = { workspace = true, features = ["library"] } diff --git a/contracts/two-party-pol-covenant/src/contract.rs b/contracts/two-party-pol-covenant/src/contract.rs index 59453090..2fbb2723 100644 --- a/contracts/two-party-pol-covenant/src/contract.rs +++ b/contracts/two-party-pol-covenant/src/contract.rs @@ -60,7 +60,8 @@ pub fn instantiate( denom: msg.party_a_config.ibc_denom.to_string(), amount: msg.party_a_config.contribution.amount, }, - addr: msg.party_a_config.addr, + controller_addr: msg.party_a_config.controller_addr, + host_addr: msg.party_a_config.host_addr, allocation: Decimal::from_ratio(msg.party_a_share, Uint128::new(100)), }, party_b: PresetPolParty { @@ -68,7 +69,8 @@ pub fn instantiate( denom: msg.party_b_config.ibc_denom.to_string(), amount: msg.party_b_config.contribution.amount, }, - addr: msg.party_b_config.addr, + controller_addr: msg.party_b_config.controller_addr, + host_addr: msg.party_b_config.host_addr, allocation: Decimal::from_ratio(msg.party_b_share, Uint128::new(100)), }, code_id: msg.contract_codes.holder_code, diff --git a/contracts/two-party-pol-covenant/src/msg.rs b/contracts/two-party-pol-covenant/src/msg.rs index 66b0b0e0..9c5b08c1 100644 --- a/contracts/two-party-pol-covenant/src/msg.rs +++ b/contracts/two-party-pol-covenant/src/msg.rs @@ -30,8 +30,10 @@ pub struct InstantiateMsg { #[cw_serde] pub struct CovenantPartyConfig { - /// authorized address of the party - pub addr: String, + /// authorized address of the party on the controller chain + pub controller_addr: String, + /// authorized address of the party on the host chain + pub host_addr: String, /// coin provided by the party on its native chain pub contribution: Coin, /// ibc denom provided by the party on neutron diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index 4bb5ba09..a55ec100 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -1,5 +1,4 @@ -use astroport::{asset::Asset, pair::Cw20HookMsg}; -use cosmos_sdk_proto::cosmos::bank::v1beta1::{MsgMultiSend, Output}; +use astroport::{asset::{Asset, PairInfo}, pair::Cw20HookMsg}; use cosmwasm_std::{ to_binary, BankMsg, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Response, StdResult, WasmMsg, @@ -33,7 +32,14 @@ pub fn instantiate( set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; deps.api .debug("WASMDEBUG: covenant-two-party-pol-holder instantiate"); - + // We query the pool to get the contract for the pool info + // The pool info is required to fetch the address of the + // liquidity token contract. The liquidity tokens are CW20 tokens + let pair_info: PairInfo = deps.querier.query_wasm_smart( + msg.pool_address.to_string(), + &astroport::pair::QueryMsg::Pair {} + )?; + let liquidity_token = pair_info.liquidity_token.to_string(); let pool_addr = deps.api.addr_validate(&msg.pool_address)?; let next_contract = deps.api.addr_validate(&msg.next_contract)?; let clock_addr = deps.api.addr_validate(&msg.clock_address)?; @@ -44,7 +50,7 @@ pub fn instantiate( msg.covenant_config.party_a.allocation, msg.covenant_config.party_b.allocation, )?; - + POOL_ADDRESS.save(deps.storage, &pool_addr)?; NEXT_CONTRACT.save(deps.storage, &next_contract)?; CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; @@ -52,6 +58,7 @@ pub fn instantiate( RAGEQUIT_CONFIG.save(deps.storage, &msg.ragequit_config)?; CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; COVENANT_CONFIG.save(deps.storage, &msg.covenant_config)?; + LP_TOKEN.save(deps.storage, &liquidity_token)?; match &msg.deposit_deadline { Some(deadline) => { @@ -84,7 +91,10 @@ pub fn execute( fn try_claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result { let mut covenant_config = COVENANT_CONFIG.load(deps.storage)?; - let (mut claim_party, mut counterparty) = covenant_config.authorize_sender(&info.sender)?; + let ( + mut claim_party, + mut counterparty + ) = covenant_config.authorize_sender(info.sender.to_string())?; let pool = POOL_ADDRESS.load(deps.storage)?; let lp_token = LP_TOKEN.load(deps.storage)?; let contract_state = CONTRACT_STATE.load(deps.storage)?; @@ -263,14 +273,6 @@ fn try_deposit(deps: DepsMut, env: Env, _info: MessageInfo) -> Result Result Result StdResult } if let Some(addr) = lp_token { - let lp_addr = deps.api.addr_validate(&addr)?; - LP_TOKEN.save(deps.storage, &lp_addr)?; - resp = resp.add_attribute("lp_token", lp_addr); + // let lp_addr = deps.api.addr_validate(&addr)?; + LP_TOKEN.save(deps.storage, &addr)?; + resp = resp.add_attribute("lp_token", addr); } if let Some(config) = ragequit_config { diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index 52a65374..e0b86d60 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -47,7 +47,8 @@ pub struct PresetTwoPartyPolHolderFields { #[cw_serde] pub struct PresetPolParty { pub contribution: Coin, - pub addr: String, + pub host_addr: String, + pub controller_addr: String, pub allocation: Decimal, } @@ -61,7 +62,7 @@ impl PresetTwoPartyPolHolderFields { ) -> InstantiateMsg { InstantiateMsg { clock_address, - pool_address: next_contract.to_string(), // todo: replace with actual pool + pool_address: self.pool_address, next_contract, lockup_config: self.lockup_config, ragequit_config: self.ragequit_config, @@ -69,15 +70,17 @@ impl PresetTwoPartyPolHolderFields { covenant_config: TwoPartyPolCovenantConfig { party_a: TwoPartyPolCovenantParty { contribution: self.party_a.contribution, - addr: self.party_a.addr, allocation: self.party_a.allocation, router: party_a_router, + host_addr: self.party_a.host_addr, + controller_addr: self.party_a.controller_addr, }, party_b: TwoPartyPolCovenantParty { contribution: self.party_b.contribution, - addr: self.party_b.addr, allocation: self.party_b.allocation, router: party_b_router, + host_addr: self.party_b.host_addr, + controller_addr: self.party_b.controller_addr, }, }, } @@ -92,7 +95,7 @@ pub struct TwoPartyPolCovenantConfig { impl TwoPartyPolCovenantConfig { pub fn update_parties(&mut self, p1: TwoPartyPolCovenantParty, p2: TwoPartyPolCovenantParty) { - if self.party_a.addr == p1.addr { + if self.party_a.controller_addr == p1.controller_addr { self.party_a = p1; self.party_b = p2; } else { @@ -115,8 +118,10 @@ impl TwoPartyPolCovenantConfig { pub struct TwoPartyPolCovenantParty { /// the `denom` and `amount` (`Uint128`) to be contributed by the party pub contribution: Coin, - /// address authorized by the party to perform claims/ragequits - pub addr: String, + /// neutron address authorized by the party to perform claims/ragequits + pub host_addr: String, + /// address of the party on the controller chain (final receiver) + pub controller_addr: String, /// fraction of the entire LP position owned by the party. /// upon exiting it becomes 0.00. if counterparty exits, this would /// become 1.00, meaning that this party owns the entire position @@ -130,13 +135,13 @@ impl TwoPartyPolCovenantConfig { /// if authorized, returns (party, counterparty). otherwise errors pub fn authorize_sender( &self, - sender: &Addr, + sender: String, ) -> Result<(TwoPartyPolCovenantParty, TwoPartyPolCovenantParty), ContractError> { let party_a = self.party_a.clone(); let party_b = self.party_b.clone(); - if party_a.addr == *sender { + if party_a.host_addr == sender { Ok((party_a, party_b)) - } else if party_b.addr == *sender { + } else if party_b.host_addr == sender { Ok((party_b, party_a)) } else { Err(ContractError::Unauthorized {}) diff --git a/contracts/two-party-pol-holder/src/state.rs b/contracts/two-party-pol-holder/src/state.rs index a8aa6326..1b4d2e23 100644 --- a/contracts/two-party-pol-holder/src/state.rs +++ b/contracts/two-party-pol-holder/src/state.rs @@ -28,7 +28,7 @@ pub const RAGEQUIT_CONFIG: Item = Item::new("ragequit_config"); pub const POOL_ADDRESS: Item = Item::new("pool_address"); /// address of the cw20 token issued for providing liquidity to the pool -pub const LP_TOKEN: Item = Item::new("lp_token"); +pub const LP_TOKEN: Item = Item::new("lp_token"); /// configuration storing both parties information pub const COVENANT_CONFIG: Item = Item::new("covenant_config"); diff --git a/contracts/two-party-pol-holder/src/suite_tests/suite.rs b/contracts/two-party-pol-holder/src/suite_tests/suite.rs index 52129898..32291087 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/suite.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/suite.rs @@ -58,8 +58,9 @@ impl Default for SuiteBuilder { denom: DENOM_A.to_string(), amount: Uint128::new(200), }, - addr: PARTY_A_ADDR.to_string(), allocation: Decimal::from_ratio(Uint128::one(), Uint128::new(2)), + host_addr: PARTY_A_ADDR.to_string(), + controller_addr: PARTY_A_ADDR.to_string(), }, party_b: TwoPartyPolCovenantParty { router: PARTY_B_ROUTER.to_string(), @@ -67,7 +68,8 @@ impl Default for SuiteBuilder { denom: DENOM_B.to_string(), amount: Uint128::new(100), }, - addr: PARTY_B_ADDR.to_string(), + host_addr: PARTY_B_ADDR.to_string(), + controller_addr: PARTY_B_ADDR.to_string(), allocation: Decimal::from_ratio(Uint128::one(), Uint128::new(2)), }, }, diff --git a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go index 31a41756..c2549bca 100644 --- a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go +++ b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go @@ -280,6 +280,8 @@ func TestTwoPartyPol(t *testing.T) { users := ibctest.GetAndFundTestUsers(t, ctx, "default", int64(500_000_000_000), atom, neutron, osmosis) gaiaUser, neutronUser, osmoUser := users[0], users[1], users[2] _, _, _ = gaiaUser, neutronUser, osmoUser + hubNeutronAccount := ibctest.GetAndFundTestUsers(t, ctx, "default", int64(500_000_000_000), neutron)[0] + osmoNeutronAccount := ibctest.GetAndFundTestUsers(t, ctx, "default", int64(500_000_000_000), neutron)[0] err = testutil.WaitForBlocks(ctx, 10, atom, neutron, osmosis) require.NoError(t, err, "failed to wait for blocks") @@ -739,7 +741,7 @@ func TestTwoPartyPol(t *testing.T) { } depositBlock := Block(500) - lockupBlock := Block(1500) + lockupBlock := Block(500) lockupConfig := ExpiryConfig{ BlockHeight: &lockupBlock, @@ -763,7 +765,8 @@ func TestTwoPartyPol(t *testing.T) { } partyAConfig := CovenantPartyConfig{ - Addr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + ControllerAddr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + HostAddr: hubNeutronAccount.Bech32Address(cosmosNeutron.Config().Bech32Prefix), Contribution: atomCoin, IbcDenom: neutronAtomIbcDenom, PartyToHostChainChannelId: testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], @@ -773,7 +776,8 @@ func TestTwoPartyPol(t *testing.T) { IbcTransferTimeout: timeouts.IbcTransferTimeout, } partyBConfig := CovenantPartyConfig{ - Addr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + ControllerAddr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + HostAddr: osmoNeutronAccount.Bech32Address(cosmosNeutron.Config().Bech32Prefix), Contribution: osmoCoin, IbcDenom: neutronOsmoIbcDenom, PartyToHostChainChannelId: testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name], @@ -999,16 +1003,15 @@ func TestTwoPartyPol(t *testing.T) { println("\ntick") cmd := []string{"neutrond", "tx", "wasm", "execute", clockAddress, `{"tick":{}}`, - "--from", neutronUser.KeyName, "--gas-prices", "0.0untrn", - "--gas-adjustment", `1.8`, + "--gas-adjustment", `1.5`, "--output", "json", "--home", "/var/cosmos-chain/neutron-2", "--node", neutron.GetRPCAddress(), "--home", neutron.HomeDir(), "--chain-id", neutron.Config().ChainID, "--from", neutronUser.KeyName, - "--gas", "auto", + "--gas", "1500000", "--keyring-backend", keyring.BackendTest, "-y", } @@ -1016,7 +1019,6 @@ func TestTwoPartyPol(t *testing.T) { resp, _, err := cosmosNeutron.Exec(ctx, cmd, nil) require.NoError(t, err) println("tick response: ", string(resp), "\n") - err = testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis) require.NoError(t, err, "failed to wait for blocks") } @@ -1102,15 +1104,29 @@ func TestTwoPartyPol(t *testing.T) { require.NoError(t, err, "failed to query holder osmo bal") holderAtomBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronAtomIbcDenom) require.NoError(t, err, "failed to query holder atom bal") - liquidPoolerOsmoBal, err := cosmosNeutron.GetBalance(ctx, liquidPoolerAddress, neutronOsmoIbcDenom) - require.NoError(t, err, "failed to query liquidPooler osmo bal") - liquidPoolerAtomBal, err := cosmosNeutron.GetBalance(ctx, liquidPoolerAddress, neutronAtomIbcDenom) - require.NoError(t, err, "failed to query liquidPooler atom bal") + // liquidPoolerOsmoBal, err := cosmosNeutron.GetBalance(ctx, liquidPoolerAddress, neutronOsmoIbcDenom) + // require.NoError(t, err, "failed to query liquidPooler osmo bal") + // liquidPoolerAtomBal, err := cosmosNeutron.GetBalance(ctx, liquidPoolerAddress, neutronAtomIbcDenom) + // require.NoError(t, err, "failed to query liquidPooler atom bal") println("holder atom bal: ", holderAtomBal) println("holder osmo bal: ", holderOsmoBal) - if holderAtomBal == int64(atomContributionAmount) && holderOsmoBal == int64(osmoContributionAmount) || - liquidPoolerAtomBal == int64(atomContributionAmount) && liquidPoolerOsmoBal == int64(osmoContributionAmount) { + var response CovenantAddressQueryResponse + type ContractState struct{} + type ContractStateQuery struct { + ContractState ContractState `json:"contract_state"` + } + contractStateQuery := ContractStateQuery{ + ContractState: ContractState{}, + } + + require.NoError(t, + cosmosNeutron.QueryContract(ctx, holderAddress, contractStateQuery, &response), + "failed to query holder state") + holderState := response.Data + println("holder state: ", holderState) + + if holderAtomBal == int64(atomContributionAmount) && holderOsmoBal == int64(osmoContributionAmount) || holderState == "active" { println("\nholder/liquidpooler received atom & osmo\n") break } else { @@ -1156,8 +1172,114 @@ func TestTwoPartyPol(t *testing.T) { } }) - t.Run("tick until routers route the funds after POL expires", func(t *testing.T) { - // todo + t.Run("tick until POL expires", func(t *testing.T) { + for { + neutronHeight, err := cosmosNeutron.Height(ctx) + require.NoError(t, err) + + if neutronHeight >= 500 { + println("neutron height: ", neutronHeight) + break + } else { + tickClock() + + } + } + }) + + t.Run("party A claims and router routes the funds", func(t *testing.T) { + + cmd := []string{"neutrond", "tx", "wasm", "execute", holderAddress, + `{"claim":{}}`, + "--from", hubNeutronAccount.GetKeyName(), + "--gas-prices", "0.0untrn", + "--gas-adjustment", `1.5`, + "--output", "json", + "--node", neutron.GetRPCAddress(), + "--home", neutron.HomeDir(), + "--chain-id", neutron.Config().ChainID, + "--gas", "42069420", + "--keyring-backend", keyring.BackendTest, + "-y", + } + + println("hub claim msg: ", strings.Join(cmd, " ")) + resp, _, _ := cosmosNeutron.Exec(ctx, cmd, nil) + + println("claim response: ", string(resp), "\n") + + for { + routerAtomBalA, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronAtomIbcDenom) + require.NoError(t, err) + + routerAtomBalB, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronAtomIbcDenom) + require.NoError(t, err) + + routerOsmoBalA, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronOsmoIbcDenom) + require.NoError(t, err) + + routerOsmoBalB, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronOsmoIbcDenom) + require.NoError(t, err) + + println("routerAtomBalA: ", routerAtomBalA) + println("routerAtomBalB: ", routerAtomBalB) + println("routerOsmoBalA: ", routerOsmoBalA) + println("routerOsmoBalB: ", routerOsmoBalB) + + if routerAtomBalA != 0 && routerOsmoBalA != 0 { + break + } else { + tickClock() + + } + } + }) + + t.Run("party B claims and router routes the funds", func(t *testing.T) { + + cmd := []string{"neutrond", "tx", "wasm", "execute", holderAddress, + `{"claim":{}}`, + "--from", osmoNeutronAccount.GetKeyName(), + "--gas-prices", "0.0untrn", + "--gas-adjustment", `1.8`, + "--output", "json", + "--node", neutron.GetRPCAddress(), + "--home", neutron.HomeDir(), + "--chain-id", neutron.Config().ChainID, + "--gas", "42069420", + "--keyring-backend", keyring.BackendTest, + "-y", + } + + println("osmo claim msg: ", strings.Join(cmd, " ")) + resp, _, _ := cosmosNeutron.Exec(ctx, cmd, nil) + println("claim response: ", string(resp), "\n") + + for { + routerAtomBalA, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronAtomIbcDenom) + require.NoError(t, err) + + routerAtomBalB, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronAtomIbcDenom) + require.NoError(t, err) + + routerOsmoBalA, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronOsmoIbcDenom) + require.NoError(t, err) + + routerOsmoBalB, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronOsmoIbcDenom) + require.NoError(t, err) + + println("routerAtomBalA: ", routerAtomBalA) + println("routerAtomBalB: ", routerAtomBalB) + println("routerOsmoBalA: ", routerOsmoBalA) + println("routerOsmoBalB: ", routerOsmoBalB) + + if routerAtomBalB != 0 || routerOsmoBalB != 0 { + break + } else { + tickClock() + + } + } }) }) }) diff --git a/two-party-pol-covenant/tests/interchaintest/types.go b/two-party-pol-covenant/tests/interchaintest/types.go index 51cea225..8d47b750 100644 --- a/two-party-pol-covenant/tests/interchaintest/types.go +++ b/two-party-pol-covenant/tests/interchaintest/types.go @@ -74,7 +74,8 @@ type CovenantParty struct { } type CovenantPartyConfig struct { - Addr string `json:"addr"` + ControllerAddr string `json:"controller_addr"` + HostAddr string `json:"host_addr"` Contribution Coin `json:"contribution"` IbcDenom string `json:"ibc_denom"` PartyToHostChainChannelId string `json:"party_to_host_chain_channel_id"` From 296807ac42ffa5c65c2304218b7968e74faeb9cd Mon Sep 17 00:00:00 2001 From: bekauz Date: Sun, 29 Oct 2023 14:26:40 +0100 Subject: [PATCH 134/586] ictest routers send funds to destination --- contracts/interchain-router/src/contract.rs | 20 +++--- .../interchaintest/two_party_pol_test.go | 68 ++++++++++++------- 2 files changed, 56 insertions(+), 32 deletions(-) diff --git a/contracts/interchain-router/src/contract.rs b/contracts/interchain-router/src/contract.rs index 5d851da0..a534e2f7 100644 --- a/contracts/interchain-router/src/contract.rs +++ b/contracts/interchain-router/src/contract.rs @@ -78,15 +78,17 @@ fn try_route_balances(deps: ExecuteDeps, env: Env) -> NeutronResult = if balances.is_empty() { - return Ok(Response::default() - .add_attribute("method", "try_route_balances") - .add_attribute("balances", "[]")); - } else { - balances - .iter() - .map(|c| Attribute::new(c.denom.to_string(), c.amount)) - .collect() + let balance_attributes: Vec = match balances.len() { + 0 => return Ok(Response::default() + .add_attribute("method", "try_route_balances") + .add_attribute("balances", "[]")), + 1 => return Ok(Response::default() + .add_attribute("method", "try_route_balances") + .add_attribute("balances", balances[0].to_string())), + _ => balances + .iter() + .map(|c| Attribute::new(c.denom.to_string(), c.amount)) + .collect(), }; // get ibc transfer messages for each denom diff --git a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go index c2549bca..8fd8520f 100644 --- a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go +++ b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go @@ -1187,7 +1187,7 @@ func TestTwoPartyPol(t *testing.T) { } }) - t.Run("party A claims and router routes the funds", func(t *testing.T) { + t.Run("party A claims and router receives the funds", func(t *testing.T) { cmd := []string{"neutrond", "tx", "wasm", "execute", holderAddress, `{"claim":{}}`, @@ -1204,38 +1204,31 @@ func TestTwoPartyPol(t *testing.T) { } println("hub claim msg: ", strings.Join(cmd, " ")) - resp, _, _ := cosmosNeutron.Exec(ctx, cmd, nil) + _, _, err := cosmosNeutron.Exec(ctx, cmd, nil) + require.NoError(t, err, "party A claim failed") - println("claim response: ", string(resp), "\n") + err = testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") for { routerAtomBalA, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronAtomIbcDenom) require.NoError(t, err) - routerAtomBalB, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronAtomIbcDenom) - require.NoError(t, err) - routerOsmoBalA, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronOsmoIbcDenom) require.NoError(t, err) - routerOsmoBalB, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronOsmoIbcDenom) - require.NoError(t, err) - println("routerAtomBalA: ", routerAtomBalA) - println("routerAtomBalB: ", routerAtomBalB) println("routerOsmoBalA: ", routerOsmoBalA) - println("routerOsmoBalB: ", routerOsmoBalB) if routerAtomBalA != 0 && routerOsmoBalA != 0 { break } else { tickClock() - } } }) - t.Run("party B claims and router routes the funds", func(t *testing.T) { + t.Run("party B claims and router receives the funds", func(t *testing.T) { cmd := []string{"neutrond", "tx", "wasm", "execute", holderAddress, `{"claim":{}}`, @@ -1252,32 +1245,61 @@ func TestTwoPartyPol(t *testing.T) { } println("osmo claim msg: ", strings.Join(cmd, " ")) - resp, _, _ := cosmosNeutron.Exec(ctx, cmd, nil) - println("claim response: ", string(resp), "\n") + _, _, err := cosmosNeutron.Exec(ctx, cmd, nil) + require.NoError(t, err, "party B claim failed") - for { - routerAtomBalA, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronAtomIbcDenom) - require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + for { routerAtomBalB, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronAtomIbcDenom) require.NoError(t, err) - routerOsmoBalA, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronOsmoIbcDenom) - require.NoError(t, err) - routerOsmoBalB, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronOsmoIbcDenom) require.NoError(t, err) - println("routerAtomBalA: ", routerAtomBalA) println("routerAtomBalB: ", routerAtomBalB) - println("routerOsmoBalA: ", routerOsmoBalA) println("routerOsmoBalB: ", routerOsmoBalB) if routerAtomBalB != 0 || routerOsmoBalB != 0 { break } else { tickClock() + } + } + }) + t.Run("tick routers until both parties receive their funds", func(t *testing.T) { + for { + osmoBalPartyA, err := cosmosAtom.GetBalance( + ctx, gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), gaiaNeutronOsmoIbcDenom, + ) + require.NoError(t, err) + + osmoBalPartyB, err := cosmosOsmosis.GetBalance( + ctx, osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), cosmosOsmosis.Config().Denom, + ) + require.NoError(t, err) + + atomBalPartyA, err := cosmosAtom.GetBalance( + ctx, gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), cosmosAtom.Config().Denom, + ) + require.NoError(t, err) + + atomBalPartyB, err := cosmosOsmosis.GetBalance( + ctx, osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), osmoNeutronAtomIbcDenom, + ) + require.NoError(t, err) + + println("party A osmo bal: ", osmoBalPartyA) + println("party A atom bal: ", atomBalPartyA) + println("party B osmo bal: ", osmoBalPartyB) + println("party B atom bal: ", atomBalPartyB) + + if osmoBalPartyA != 0 && atomBalPartyA != 0 && osmoBalPartyB != 0 && atomBalPartyB != 0 { + break + } else { + tickClock() } } }) From d1e5fc0020e172a4fb537c0bc4afb417e2e695be Mon Sep 17 00:00:00 2001 From: bekauz Date: Sun, 29 Oct 2023 16:55:55 +0100 Subject: [PATCH 135/586] querying liquidity token addr on demand; DepositDeadline no longer optional --- contracts/two-party-pol-covenant/src/msg.rs | 2 +- contracts/two-party-pol-holder/README.md | 1 - .../two-party-pol-holder/src/contract.rs | 180 +++++++++--------- contracts/two-party-pol-holder/src/msg.rs | 5 +- contracts/two-party-pol-holder/src/state.rs | 9 +- .../src/suite_tests/suite.rs | 4 +- .../interchaintest/two_party_pol_test.go | 10 +- 7 files changed, 101 insertions(+), 110 deletions(-) diff --git a/contracts/two-party-pol-covenant/src/msg.rs b/contracts/two-party-pol-covenant/src/msg.rs index 9c5b08c1..58a86409 100644 --- a/contracts/two-party-pol-covenant/src/msg.rs +++ b/contracts/two-party-pol-covenant/src/msg.rs @@ -20,7 +20,7 @@ pub struct InstantiateMsg { pub party_b_config: CovenantPartyConfig, pub pool_address: String, pub ragequit_config: Option, - pub deposit_deadline: Option, + pub deposit_deadline: ExpiryConfig, pub party_a_share: Uint64, pub party_b_share: Uint64, pub expected_pool_ratio: Decimal, diff --git a/contracts/two-party-pol-holder/README.md b/contracts/two-party-pol-holder/README.md index 7c0e64fd..4ab12e54 100644 --- a/contracts/two-party-pol-holder/README.md +++ b/contracts/two-party-pol-holder/README.md @@ -59,4 +59,3 @@ Any ticks received while holder is `Active` will trigger a check for expiration. If covenant is expired, holder state is advanced to `Expired`. Both parties are free to submit `Claim` messages to the holder. - diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index a55ec100..fe0290ca 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -1,7 +1,7 @@ use astroport::{asset::{Asset, PairInfo}, pair::Cw20HookMsg}; use cosmwasm_std::{ to_binary, BankMsg, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, - Response, StdResult, WasmMsg, + Response, StdResult, WasmMsg, Uint128, QuerierWrapper, }; #[cfg(not(feature = "library"))] @@ -14,7 +14,7 @@ use crate::{ error::ContractError, msg::{ContractState, ExecuteMsg, InstantiateMsg, QueryMsg, RagequitConfig, RagequitState, MigrateMsg}, state::{ - CLOCK_ADDRESS, CONTRACT_STATE, COVENANT_CONFIG, DEPOSIT_DEADLINE, LOCKUP_CONFIG, LP_TOKEN, + CLOCK_ADDRESS, CONTRACT_STATE, COVENANT_CONFIG, DEPOSIT_DEADLINE, LOCKUP_CONFIG, NEXT_CONTRACT, POOL_ADDRESS, RAGEQUIT_CONFIG, }, }; @@ -32,18 +32,12 @@ pub fn instantiate( set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; deps.api .debug("WASMDEBUG: covenant-two-party-pol-holder instantiate"); - // We query the pool to get the contract for the pool info - // The pool info is required to fetch the address of the - // liquidity token contract. The liquidity tokens are CW20 tokens - let pair_info: PairInfo = deps.querier.query_wasm_smart( - msg.pool_address.to_string(), - &astroport::pair::QueryMsg::Pair {} - )?; - let liquidity_token = pair_info.liquidity_token.to_string(); + let pool_addr = deps.api.addr_validate(&msg.pool_address)?; let next_contract = deps.api.addr_validate(&msg.next_contract)?; let clock_addr = deps.api.addr_validate(&msg.clock_address)?; + msg.deposit_deadline.validate(&env.block)?; msg.covenant_config.validate(deps.api)?; msg.lockup_config.validate(&env.block)?; msg.ragequit_config.validate( @@ -58,17 +52,7 @@ pub fn instantiate( RAGEQUIT_CONFIG.save(deps.storage, &msg.ragequit_config)?; CONTRACT_STATE.save(deps.storage, &ContractState::Instantiated)?; COVENANT_CONFIG.save(deps.storage, &msg.covenant_config)?; - LP_TOKEN.save(deps.storage, &liquidity_token)?; - - match &msg.deposit_deadline { - Some(deadline) => { - deadline.validate(&env.block)?; - DEPOSIT_DEADLINE.save(deps.storage, deadline)?; - } - None => { - DEPOSIT_DEADLINE.save(deps.storage, &ExpiryConfig::None)?; - } - } + DEPOSIT_DEADLINE.save(deps.storage, &msg.deposit_deadline)?; Ok(Response::default() .add_attribute("method", "two_party_pol_holder_instantiate") @@ -89,6 +73,28 @@ pub fn execute( } } +/// queries the liquidity token balance of given address +fn query_liquidity_token_balance(querier: QuerierWrapper, liquidity_token: &str, contract_addr: String) -> Result { + let liquidity_token_balance: BalanceResponse = querier.query_wasm_smart( + liquidity_token, + &cw20::Cw20QueryMsg::Balance { + address: contract_addr, + }, + )?; + Ok(liquidity_token_balance.balance) +} + +/// queries the cw20 liquidity token address corresponding to a given pool +fn query_liquidity_token_address(querier: QuerierWrapper, pool: String) -> Result { + let pair_info: PairInfo = querier.query_wasm_smart( + pool, + &astroport::pair::QueryMsg::Pair {} + )?; + Ok(pair_info.liquidity_token.to_string()) +} + +// TODO: figure out best UX to implement a way to claim partial positions +// - Option ? None -> claim entire position, Some(%) -> claim the % of your entitlement fn try_claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result { let mut covenant_config = COVENANT_CONFIG.load(deps.storage)?; let ( @@ -96,7 +102,6 @@ fn try_claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result Result = deps.querier.query_wasm_smart( @@ -145,6 +144,8 @@ fn try_claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result Result Result check_expiration(deps, env), ContractState::Expired => { let config = COVENANT_CONFIG.load(deps.storage)?; - if config.party_a.allocation.is_zero() && config.party_b.allocation.is_zero() { + let state = if config.party_a.allocation.is_zero() && config.party_b.allocation.is_zero() { CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; - } + ContractState::Complete + } else { + state + }; Ok(Response::default() .add_attribute("method", "tick") .add_attribute("contract_state", state.to_string())) @@ -205,6 +209,7 @@ fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> Result Result { let config = COVENANT_CONFIG.load(deps.storage)?; + let deposit_deadline = DEPOSIT_DEADLINE.load(deps.storage)?; // assert the balances let party_a_bal = deps.querier.query_balance( @@ -216,7 +221,6 @@ fn try_deposit(deps: DepsMut, env: Env, _info: MessageInfo) -> Result Result = - match (party_a_bal.amount.is_zero(), party_b_bal.amount.is_zero()) { - // both balances empty, we complete - (true, true) => { - CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; - return Ok(Response::default() - .add_attribute("method", "try_deposit") - .add_attribute("state", "complete")); - } - // refund party B - (true, false) => vec![CosmosMsg::Bank(BankMsg::Send { + let refund_messages: Vec = match (party_a_bal.amount.is_zero(), party_b_bal.amount.is_zero()) { + // both balances empty, we complete + (true, true) => { + CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; + return Ok(Response::default() + .add_attribute("method", "try_deposit") + .add_attribute("state", "complete")); + } + // refund party B + (true, false) => vec![CosmosMsg::Bank(BankMsg::Send { + to_address: config.party_b.router, + amount: vec![party_b_bal], + })], + // refund party A + (false, true) => vec![CosmosMsg::Bank(BankMsg::Send { + to_address: config.party_a.router, + amount: vec![party_a_bal], + })], + // refund both + (false, false) => vec![ + CosmosMsg::Bank(BankMsg::Send { + to_address: config.party_a.router.to_string(), + amount: vec![party_a_bal], + }), + CosmosMsg::Bank(BankMsg::Send { to_address: config.party_b.router, amount: vec![party_b_bal], - })], - // refund party A - (false, true) => vec![CosmosMsg::Bank(BankMsg::Send { - to_address: config.party_a.router, - amount: vec![party_a_bal], - })], - // refund both - (false, false) => vec![ - CosmosMsg::Bank(BankMsg::Send { - to_address: config.party_a.router.to_string(), - amount: vec![party_a_bal], - }), - CosmosMsg::Bank(BankMsg::Send { - to_address: config.party_b.router, - amount: vec![party_b_bal], - }), - ], - }; + }), + ], + }; return Ok(Response::default() .add_attribute("method", "try_deposit") .add_attribute("action", "refund") .add_messages(refund_messages)); - } else if !party_a_fulfilled || !party_b_fulfilled { + } + + if !party_a_fulfilled || !party_b_fulfilled { // if deposit deadline is not yet due and both parties did not fulfill we error return Err(ContractError::InsufficientDeposits {}); } @@ -327,24 +332,18 @@ fn try_ragequit(deps: DepsMut, env: Env, info: MessageInfo) -> Result = deps.querier.query_wasm_smart( @@ -366,18 +365,22 @@ fn try_ragequit(deps: DepsMut, env: Env, info: MessageInfo) -> Result Result StdResult deposit_deadline, pool_address, ragequit_config, - lp_token, covenant_config, } => { let mut resp = Response::default().add_attribute("method", "update_config"); @@ -457,12 +459,6 @@ pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> StdResult resp = resp.add_attribute("pool_addr", pool_addr); } - if let Some(addr) = lp_token { - // let lp_addr = deps.api.addr_validate(&addr)?; - LP_TOKEN.save(deps.storage, &addr)?; - resp = resp.add_attribute("lp_token", addr); - } - if let Some(config) = ragequit_config { RAGEQUIT_CONFIG.save(deps.storage, &config)?; resp = resp.add_attributes(config.get_response_attributes()); diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index e0b86d60..ab027646 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -15,7 +15,7 @@ pub struct InstantiateMsg { pub next_contract: String, pub lockup_config: ExpiryConfig, pub ragequit_config: RagequitConfig, - pub deposit_deadline: Option, + pub deposit_deadline: ExpiryConfig, pub covenant_config: TwoPartyPolCovenantConfig, } @@ -37,7 +37,7 @@ pub struct PresetTwoPartyPolHolderFields { pub pool_address: String, pub lockup_config: ExpiryConfig, pub ragequit_config: RagequitConfig, - pub deposit_deadline: Option, + pub deposit_deadline: ExpiryConfig, pub party_a: PresetPolParty, pub party_b: PresetPolParty, pub code_id: u64, @@ -167,7 +167,6 @@ pub enum MigrateMsg { deposit_deadline: Option, pool_address: Option, ragequit_config: Option, - lp_token: Option, covenant_config: Option, }, UpdateCodeId { diff --git a/contracts/two-party-pol-holder/src/state.rs b/contracts/two-party-pol-holder/src/state.rs index 1b4d2e23..84c49541 100644 --- a/contracts/two-party-pol-holder/src/state.rs +++ b/contracts/two-party-pol-holder/src/state.rs @@ -12,6 +12,9 @@ pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); /// the LP module that we send the deposited funds to pub const NEXT_CONTRACT: Item = Item::new("next_contract"); +/// address of the liquidity pool to which we provide liquidity +pub const POOL_ADDRESS: Item = Item::new("pool_address"); + /// configuration describing the lockup period after which parties are /// no longer subject to ragequit penalties in order to exit their position pub const LOCKUP_CONFIG: Item = Item::new("lockup_config"); @@ -24,11 +27,5 @@ pub const DEPOSIT_DEADLINE: Item = Item::new("deposit_deadline"); /// of the party initiating the ragequit pub const RAGEQUIT_CONFIG: Item = Item::new("ragequit_config"); -/// address of the liquidity pool to which we provide liquidity -pub const POOL_ADDRESS: Item = Item::new("pool_address"); - -/// address of the cw20 token issued for providing liquidity to the pool -pub const LP_TOKEN: Item = Item::new("lp_token"); - /// configuration storing both parties information pub const COVENANT_CONFIG: Item = Item::new("covenant_config"); diff --git a/contracts/two-party-pol-holder/src/suite_tests/suite.rs b/contracts/two-party-pol-holder/src/suite_tests/suite.rs index 32291087..ae5693ac 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/suite.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/suite.rs @@ -47,7 +47,7 @@ impl Default for SuiteBuilder { instantiate: InstantiateMsg { pool_address: POOL.to_string(), ragequit_config: RagequitConfig::Disabled, - deposit_deadline: None, + deposit_deadline: ExpiryConfig::None, clock_address: CLOCK_ADDR.to_string(), next_contract: NEXT_CONTRACT.to_string(), lockup_config: ExpiryConfig::None, @@ -91,7 +91,7 @@ impl SuiteBuilder { } pub fn with_deposit_deadline(mut self, config: ExpiryConfig) -> Self { - self.instantiate.deposit_deadline = Some(config); + self.instantiate.deposit_deadline = config; self } diff --git a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go index 8fd8520f..563e8ce5 100644 --- a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go +++ b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go @@ -1189,6 +1189,9 @@ func TestTwoPartyPol(t *testing.T) { t.Run("party A claims and router receives the funds", func(t *testing.T) { + err = testutil.WaitForBlocks(ctx, 15, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") + cmd := []string{"neutrond", "tx", "wasm", "execute", holderAddress, `{"claim":{}}`, "--from", hubNeutronAccount.GetKeyName(), @@ -1207,9 +1210,6 @@ func TestTwoPartyPol(t *testing.T) { _, _, err := cosmosNeutron.Exec(ctx, cmd, nil) require.NoError(t, err, "party A claim failed") - err = testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis) - require.NoError(t, err, "failed to wait for blocks") - for { routerAtomBalA, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronAtomIbcDenom) require.NoError(t, err) @@ -1298,9 +1298,9 @@ func TestTwoPartyPol(t *testing.T) { if osmoBalPartyA != 0 && atomBalPartyA != 0 && osmoBalPartyB != 0 && atomBalPartyB != 0 { break - } else { - tickClock() } + + tickClock() } }) }) From 0bbaac7deafd86aa8190df88d31b93bc012baa9b Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 30 Oct 2023 16:33:17 +0100 Subject: [PATCH 136/586] uninventing the wheel by using cw_utils Expiration instead of ExpiryConfig --- Cargo.lock | 1 + Cargo.toml | 2 +- contracts/two-party-pol-covenant/src/msg.rs | 6 +- contracts/two-party-pol-holder/Cargo.toml | 2 +- .../two-party-pol-holder/src/contract.rs | 57 +++++++++++-------- contracts/two-party-pol-holder/src/error.rs | 5 +- contracts/two-party-pol-holder/src/msg.rs | 16 +++--- contracts/two-party-pol-holder/src/state.rs | 6 +- .../src/suite_tests/suite.rs | 9 +-- .../src/suite_tests/tests.rs | 27 ++++----- .../interchaintest/two_party_pol_test.go | 26 ++++----- .../tests/interchaintest/types.go | 12 ++-- 12 files changed, 91 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec895149..9e084823 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -706,6 +706,7 @@ dependencies = [ "covenant-utils", "cw-multi-test", "cw-storage-plus 1.1.0", + "cw-utils 1.0.2", "cw2 1.1.1", "cw20 0.15.1", "neutron-sdk", diff --git a/Cargo.toml b/Cargo.toml index 716de8fe..8e759e76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ bech32 = "0.9.0" cosmwasm-schema = "1.2.1" cosmwasm-std = { version = "1.2.4", features = ["ibc3"] } cw-storage-plus = "1.0.1" -cw-utils = "1.0.1" +cw-utils = "1.0.2" cw2 = "1.0.1" serde = { version = "1.0.145", default-features = false, features = ["derive"] } thiserror = "1.0.31" diff --git a/contracts/two-party-pol-covenant/src/msg.rs b/contracts/two-party-pol-covenant/src/msg.rs index 58a86409..74942301 100644 --- a/contracts/two-party-pol-covenant/src/msg.rs +++ b/contracts/two-party-pol-covenant/src/msg.rs @@ -2,7 +2,7 @@ use astroport::factory::PairType; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Uint128, Uint64, Coin, Decimal}; use covenant_two_party_pol_holder::msg::RagequitConfig; -use covenant_utils::ExpiryConfig; +use cw_utils::Expiration; use neutron_sdk::bindings::msg::IbcFee; const NEUTRON_DENOM: &str = "untrn"; @@ -15,12 +15,12 @@ pub struct InstantiateMsg { pub preset_ibc_fee: PresetIbcFee, pub contract_codes: CovenantContractCodeIds, pub clock_tick_max_gas: Option, - pub lockup_config: ExpiryConfig, + pub lockup_config: Expiration, pub party_a_config: CovenantPartyConfig, pub party_b_config: CovenantPartyConfig, pub pool_address: String, pub ragequit_config: Option, - pub deposit_deadline: ExpiryConfig, + pub deposit_deadline: Expiration, pub party_a_share: Uint64, pub party_b_share: Uint64, pub expected_pool_ratio: Decimal, diff --git a/contracts/two-party-pol-holder/Cargo.toml b/contracts/two-party-pol-holder/Cargo.toml index 0fd81010..900cad97 100644 --- a/contracts/two-party-pol-holder/Cargo.toml +++ b/contracts/two-party-pol-holder/Cargo.toml @@ -33,7 +33,7 @@ cosmos-sdk-proto = { workspace = true } protobuf = { workspace = true } astroport = "2.8.0" cw20 = { version = "0.15" } - +cw-utils = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index fe0290ca..a5947c92 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -1,12 +1,12 @@ use astroport::{asset::{Asset, PairInfo}, pair::Cw20HookMsg}; use cosmwasm_std::{ to_binary, BankMsg, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, - Response, StdResult, WasmMsg, Uint128, QuerierWrapper, + Response, StdResult, WasmMsg, Uint128, QuerierWrapper, StdError, }; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use covenant_utils::ExpiryConfig; + use cw2::set_contract_version; use cw20::{BalanceResponse, Cw20ExecuteMsg}; @@ -37,9 +37,14 @@ pub fn instantiate( let next_contract = deps.api.addr_validate(&msg.next_contract)?; let clock_addr = deps.api.addr_validate(&msg.clock_address)?; - msg.deposit_deadline.validate(&env.block)?; + if msg.deposit_deadline.is_expired(&env.block) { + return Err(ContractError::DepositDeadlineValidationError {}) + } + if msg.lockup_config.is_expired(&env.block) { + return Err(ContractError::LockupValidationError {}) + } + msg.covenant_config.validate(deps.api)?; - msg.lockup_config.validate(&env.block)?; msg.ragequit_config.validate( msg.covenant_config.party_a.allocation, msg.covenant_config.party_b.allocation, @@ -97,13 +102,19 @@ fn query_liquidity_token_address(querier: QuerierWrapper, pool: String) -> Resul // - Option ? None -> claim entire position, Some(%) -> claim the % of your entitlement fn try_claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result { let mut covenant_config = COVENANT_CONFIG.load(deps.storage)?; - let ( - mut claim_party, - mut counterparty - ) = covenant_config.authorize_sender(info.sender.to_string())?; + let (mut claim_party, mut counterparty) = + covenant_config.authorize_sender(info.sender.to_string())?; let pool = POOL_ADDRESS.load(deps.storage)?; let contract_state = CONTRACT_STATE.load(deps.storage)?; + // we exit early if contract is not in ragequit or expired state + // otherwise claim process is the same + let response: Response = match contract_state { + ContractState::Ragequit => Response::default().add_attribute("method", "try_claim_ragequit"), + ContractState::Expired => Response::default().add_attribute("method", "try_claim_expired"), + _ => return Err(ContractError::ClaimError {}), + }; + // if both parties already claimed everything we complete if claim_party.allocation.is_zero() && counterparty.allocation.is_zero() { CONTRACT_STATE.save(deps.storage, &ContractState::Complete)?; @@ -172,16 +183,7 @@ fn try_claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result Ok(Response::default() - .add_attribute("method", "try_claim_ragequit") - .add_messages(withdraw_and_forward_msgs)), - ContractState::Expired => Ok(Response::default() - .add_attribute("method", "try_claim_expired") - .add_messages(withdraw_and_forward_msgs)), - _ => Err(ContractError::ClaimError {}), - } + Ok(response.add_messages(withdraw_and_forward_msgs)) } fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> Result { @@ -201,6 +203,7 @@ fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> Result Ok(Response::default() .add_attribute("method", "tick") .add_attribute("contract_state", state.to_string())), @@ -227,7 +230,7 @@ fn try_deposit(deps: DepsMut, env: Env, _info: MessageInfo) -> Result = match (party_a_bal.amount.is_zero(), party_b_bal.amount.is_zero()) { // both balances empty, we complete (true, true) => { @@ -287,7 +290,7 @@ fn try_deposit(deps: DepsMut, env: Env, _info: MessageInfo) -> Result Result { let lockup_config = LOCKUP_CONFIG.load(deps.storage)?; - if !lockup_config.is_expired(env.block) { + if !lockup_config.is_expired(&env.block) { return Ok(Response::default() .add_attribute("method", "check_expiration") .add_attribute("result", "not_due")); @@ -318,7 +321,7 @@ fn try_ragequit(deps: DepsMut, env: Env, info: MessageInfo) -> Result StdResult } if let Some(expiry_config) = lockup_config { - expiry_config.validate(&env.block)?; + if expiry_config.is_expired(&env.block) { + return Err(StdError::generic_err("lockup config is already past")) + } LOCKUP_CONFIG.save(deps.storage, &expiry_config)?; - resp = resp.add_attributes(expiry_config.get_response_attributes()); + resp = resp.add_attribute("lockup_config", expiry_config.to_string()); } if let Some(expiry_config) = deposit_deadline { - expiry_config.validate(&env.block)?; + if expiry_config.is_expired(&env.block) { + return Err(StdError::generic_err("deposit deadline is already past")) + } DEPOSIT_DEADLINE.save(deps.storage, &expiry_config)?; - resp = resp.add_attributes(expiry_config.get_response_attributes()); + resp = resp.add_attribute("deposit_deadline", expiry_config.to_string()); } if let Some(addr) = pool_address { diff --git a/contracts/two-party-pol-holder/src/error.rs b/contracts/two-party-pol-holder/src/error.rs index 5dd00494..fefcdb8a 100644 --- a/contracts/two-party-pol-holder/src/error.rs +++ b/contracts/two-party-pol-holder/src/error.rs @@ -36,9 +36,12 @@ pub enum ContractError { #[error("expiry block is already past")] InvalidExpiryBlockHeight {}, - #[error("lockup validation failed")] + #[error("lockup deadline is already past")] LockupValidationError {}, + #[error("deposit deadline is already past")] + DepositDeadlineValidationError {}, + #[error("shares of covenant parties must add up to 1.0")] InvolvedPartiesConfigError {}, diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index ab027646..0f4481dd 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -5,6 +5,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Api, Attribute, Coin, Decimal, StdError, Binary}; use covenant_macros::{clocked, covenant_clock_address, covenant_next_contract, covenant_deposit_address}; use covenant_utils::ExpiryConfig; +use cw_utils::Expiration; use crate::error::ContractError; @@ -13,9 +14,9 @@ pub struct InstantiateMsg { pub clock_address: String, pub pool_address: String, pub next_contract: String, - pub lockup_config: ExpiryConfig, + pub lockup_config: Expiration, pub ragequit_config: RagequitConfig, - pub deposit_deadline: ExpiryConfig, + pub deposit_deadline: Expiration, pub covenant_config: TwoPartyPolCovenantConfig, } @@ -25,9 +26,10 @@ impl InstantiateMsg { Attribute::new("clock_addr", self.clock_address), Attribute::new("pool_address", self.pool_address), Attribute::new("next_contract", self.next_contract), + Attribute::new("lockup_config", self.lockup_config.to_string()), + Attribute::new("deposit_deadline", self.deposit_deadline.to_string()), ]; attrs.extend(self.ragequit_config.get_response_attributes()); - attrs.extend(self.lockup_config.get_response_attributes()); attrs } } @@ -35,9 +37,9 @@ impl InstantiateMsg { #[cw_serde] pub struct PresetTwoPartyPolHolderFields { pub pool_address: String, - pub lockup_config: ExpiryConfig, + pub lockup_config: Expiration, pub ragequit_config: RagequitConfig, - pub deposit_deadline: ExpiryConfig, + pub deposit_deadline: Expiration, pub party_a: PresetPolParty, pub party_b: PresetPolParty, pub code_id: u64, @@ -163,8 +165,8 @@ pub enum MigrateMsg { UpdateConfig { clock_addr: Option, next_contract: Option, - lockup_config: Option, - deposit_deadline: Option, + lockup_config: Option, + deposit_deadline: Option, pool_address: Option, ragequit_config: Option, covenant_config: Option, diff --git a/contracts/two-party-pol-holder/src/state.rs b/contracts/two-party-pol-holder/src/state.rs index 84c49541..4fc34a22 100644 --- a/contracts/two-party-pol-holder/src/state.rs +++ b/contracts/two-party-pol-holder/src/state.rs @@ -1,6 +1,6 @@ use cosmwasm_std::Addr; -use covenant_utils::ExpiryConfig; use cw_storage_plus::Item; +use cw_utils::Expiration; use crate::msg::{ContractState, RagequitConfig, TwoPartyPolCovenantConfig}; @@ -17,11 +17,11 @@ pub const POOL_ADDRESS: Item = Item::new("pool_address"); /// configuration describing the lockup period after which parties are /// no longer subject to ragequit penalties in order to exit their position -pub const LOCKUP_CONFIG: Item = Item::new("lockup_config"); +pub const LOCKUP_CONFIG: Item = Item::new("lockup_config"); /// configuration describing the deposit period during which parties /// are expected to fulfill their parts of the covenant -pub const DEPOSIT_DEADLINE: Item = Item::new("deposit_deadline"); +pub const DEPOSIT_DEADLINE: Item = Item::new("deposit_deadline"); /// configuration describing the penalty applied to the allocation /// of the party initiating the ragequit diff --git a/contracts/two-party-pol-holder/src/suite_tests/suite.rs b/contracts/two-party-pol-holder/src/suite_tests/suite.rs index ae5693ac..5fb2472d 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/suite.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/suite.rs @@ -5,6 +5,7 @@ use crate::msg::{ use cosmwasm_std::{Addr, BlockInfo, Coin, Decimal, Timestamp, Uint128}; use covenant_utils::ExpiryConfig; use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; +use cw_utils::Expiration; use super::{ mock_astro_lp_token_contract, mock_astro_pool_contract, mock_deposit_contract, @@ -47,10 +48,10 @@ impl Default for SuiteBuilder { instantiate: InstantiateMsg { pool_address: POOL.to_string(), ragequit_config: RagequitConfig::Disabled, - deposit_deadline: ExpiryConfig::None, + deposit_deadline: Expiration::Never {}, clock_address: CLOCK_ADDR.to_string(), next_contract: NEXT_CONTRACT.to_string(), - lockup_config: ExpiryConfig::None, + lockup_config: Expiration::Never {}, covenant_config: TwoPartyPolCovenantConfig { party_a: TwoPartyPolCovenantParty { router: PARTY_A_ROUTER.to_string(), @@ -80,7 +81,7 @@ impl Default for SuiteBuilder { } impl SuiteBuilder { - pub fn with_lockup_config(mut self, config: ExpiryConfig) -> Self { + pub fn with_lockup_config(mut self, config: Expiration) -> Self { self.instantiate.lockup_config = config; self } @@ -90,7 +91,7 @@ impl SuiteBuilder { self } - pub fn with_deposit_deadline(mut self, config: ExpiryConfig) -> Self { + pub fn with_deposit_deadline(mut self, config: Expiration) -> Self { self.instantiate.deposit_deadline = config; self } diff --git a/contracts/two-party-pol-holder/src/suite_tests/tests.rs b/contracts/two-party-pol-holder/src/suite_tests/tests.rs index 4416ca0e..d5104865 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/tests.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/tests.rs @@ -1,5 +1,6 @@ use cosmwasm_std::{Decimal, Timestamp, Uint128}; use covenant_utils::ExpiryConfig; +use cw_utils::Expiration; use crate::{ error::ContractError, @@ -68,7 +69,7 @@ fn test_instantiate_invalid_allocations() { #[should_panic(expected = "block height must be in the future")] fn test_instantiate_invalid_deposit_deadline_block_based() { SuiteBuilder::default() - .with_deposit_deadline(ExpiryConfig::Block(1)) + .with_deposit_deadline(Expiration::AtHeight(1)) .build(); } @@ -76,7 +77,7 @@ fn test_instantiate_invalid_deposit_deadline_block_based() { #[should_panic(expected = "block time must be in the future")] fn test_instantiate_invalid_deposit_deadline_time_based() { SuiteBuilder::default() - .with_deposit_deadline(ExpiryConfig::Time(Timestamp::from_nanos(1))) + .with_deposit_deadline(Expiration::AtTime(Timestamp::from_nanos(1))) .build(); } @@ -84,7 +85,7 @@ fn test_instantiate_invalid_deposit_deadline_time_based() { #[should_panic(expected = "invalid expiry config: block time must be in the future")] fn test_instantiate_invalid_lockup_config_time_based() { SuiteBuilder::default() - .with_lockup_config(ExpiryConfig::Time(Timestamp::from_nanos( + .with_lockup_config(Expiration::AtTime(Timestamp::from_nanos( INITIAL_BLOCK_NANOS - 1, ))) .build(); @@ -94,14 +95,14 @@ fn test_instantiate_invalid_lockup_config_time_based() { #[should_panic(expected = "invalid expiry config: block height must be in the future")] fn test_instantiate_invalid_lockup_config_height_based() { SuiteBuilder::default() - .with_lockup_config(ExpiryConfig::Block(INITIAL_BLOCK_HEIGHT - 1)) + .with_lockup_config(Expiration::AtHeight(INITIAL_BLOCK_HEIGHT - 1)) .build(); } #[test] fn test_single_party_deposit_refund_block_based() { let mut suite = SuiteBuilder::default() - .with_deposit_deadline(ExpiryConfig::Block(12545)) + .with_deposit_deadline(Expiration::AtHeight(12545)) .build(); // party A fulfills their part of covenant but B fails to @@ -126,7 +127,7 @@ fn test_single_party_deposit_refund_block_based() { fn test_single_party_deposit_refund_time_based() { let current_timestamp = get_default_block_info(); let mut suite = SuiteBuilder::default() - .with_deposit_deadline(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) + .with_deposit_deadline(Expiration::AtTime(current_timestamp.time.plus_minutes(200))) .build(); // party A fulfills their part of covenant but B fails to @@ -179,7 +180,7 @@ fn test_holder_active_does_not_allow_claims() { fn test_holder_active_not_expired_ticks() { let current_timestamp = get_default_block_info(); let mut suite = SuiteBuilder::default() - .with_deposit_deadline(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) + .with_deposit_deadline(Expiration::AtTime(current_timestamp.time.plus_minutes(200))) .build(); // both parties fulfill their parts of the covenant @@ -218,7 +219,7 @@ fn test_holder_active_not_expired_ticks() { fn test_holder_active_expired_tick_advances_state() { let current_timestamp = get_default_block_info(); let mut suite = SuiteBuilder::default() - .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) + .with_lockup_config(Expiration::AtTime(current_timestamp.time.plus_minutes(200))) .build(); // both parties fulfill their parts of the covenant @@ -308,7 +309,7 @@ fn test_holder_ragequit_unauthorized() { fn test_holder_ragequit_not_in_active_state() { let current_timestamp = get_default_block_info(); let mut suite = SuiteBuilder::default() - .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) + .with_lockup_config(Expiration::AtTime(current_timestamp.time.plus_minutes(200))) .build(); // both parties fulfill their parts of the covenant @@ -340,7 +341,7 @@ fn test_holder_ragequit_active_but_expired() { penalty: Decimal::bps(10), state: None, })) - .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) + .with_lockup_config(Expiration::AtTime(current_timestamp.time.plus_minutes(200))) .build(); // both parties fulfill their parts of the covenant @@ -368,7 +369,7 @@ fn test_ragequit_double_claim_fails() { penalty: Decimal::from_ratio(Uint128::one(), Uint128::new(10)), state: None, })) - .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) + .with_lockup_config(Expiration::AtTime(current_timestamp.time.plus_minutes(200))) .build(); // both parties fulfill their parts of the covenant @@ -406,7 +407,7 @@ fn test_ragequit_happy_flow_to_completion() { penalty: Decimal::from_ratio(Uint128::one(), Uint128::new(10)), state: None, })) - .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) + .with_lockup_config(Expiration::AtTime(current_timestamp.time.plus_minutes(200))) .build(); // both parties fulfill their parts of the covenant @@ -451,7 +452,7 @@ fn test_ragequit_happy_flow_to_completion() { fn test_expiry_happy_flow_to_completion() { let current_timestamp = get_default_block_info(); let mut suite = SuiteBuilder::default() - .with_lockup_config(ExpiryConfig::Time(current_timestamp.time.plus_minutes(200))) + .with_lockup_config(Expiration::AtTime(current_timestamp.time.plus_minutes(200))) .build(); // both parties fulfill their parts of the covenant diff --git a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go index 563e8ce5..b5796471 100644 --- a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go +++ b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go @@ -743,11 +743,11 @@ func TestTwoPartyPol(t *testing.T) { depositBlock := Block(500) lockupBlock := Block(500) - lockupConfig := ExpiryConfig{ - BlockHeight: &lockupBlock, + lockupConfig := Expiration{ + AtHeight: &lockupBlock, } - depositDeadline := ExpiryConfig{ - BlockHeight: &depositBlock, + depositDeadline := Expiration{ + AtHeight: &depositBlock, } presetIbcFee := PresetIbcFee{ AckFee: "10000", @@ -817,7 +817,7 @@ func TestTwoPartyPol(t *testing.T) { PartyBConfig: partyBConfig, PoolAddress: poolAddress, RagequitConfig: &ragequitConfig, - DepositDeadline: &depositDeadline, + DepositDeadline: depositDeadline, PartyAShare: "50", PartyBShare: "50", ExpectedPoolRatio: "0.1", @@ -1172,26 +1172,22 @@ func TestTwoPartyPol(t *testing.T) { } }) - t.Run("tick until POL expires", func(t *testing.T) { + t.Run("tick until holder expires", func(t *testing.T) { for { neutronHeight, err := cosmosNeutron.Height(ctx) require.NoError(t, err) - if neutronHeight >= 500 { + if neutronHeight >= 515 { println("neutron height: ", neutronHeight) break } else { tickClock() - } } }) t.Run("party A claims and router receives the funds", func(t *testing.T) { - err = testutil.WaitForBlocks(ctx, 15, atom, neutron, osmosis) - require.NoError(t, err, "failed to wait for blocks") - cmd := []string{"neutrond", "tx", "wasm", "execute", holderAddress, `{"claim":{}}`, "--from", hubNeutronAccount.GetKeyName(), @@ -1205,10 +1201,7 @@ func TestTwoPartyPol(t *testing.T) { "--keyring-backend", keyring.BackendTest, "-y", } - println("hub claim msg: ", strings.Join(cmd, " ")) - _, _, err := cosmosNeutron.Exec(ctx, cmd, nil) - require.NoError(t, err, "party A claim failed") for { routerAtomBalA, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronAtomIbcDenom) @@ -1224,6 +1217,11 @@ func TestTwoPartyPol(t *testing.T) { break } else { tickClock() + _, _, err = cosmosNeutron.Exec(ctx, cmd, nil) + require.NoError(t, err, "party A claim failed") + + err = testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") } } }) diff --git a/two-party-pol-covenant/tests/interchaintest/types.go b/two-party-pol-covenant/tests/interchaintest/types.go index 8d47b750..2e3693c4 100644 --- a/two-party-pol-covenant/tests/interchaintest/types.go +++ b/two-party-pol-covenant/tests/interchaintest/types.go @@ -11,12 +11,12 @@ type CovenantInstantiateMsg struct { PresetIbcFee PresetIbcFee `json:"preset_ibc_fee"` ContractCodeIds ContractCodeIds `json:"contract_codes"` TickMaxGas string `json:"clock_tick_max_gas,omitempty"` - LockupConfig ExpiryConfig `json:"lockup_config"` + LockupConfig Expiration `json:"lockup_config"` PartyAConfig CovenantPartyConfig `json:"party_a_config"` PartyBConfig CovenantPartyConfig `json:"party_b_config"` PoolAddress string `json:"pool_address"` RagequitConfig *RagequitConfig `json:"ragequit_config,omitempty"` - DepositDeadline *ExpiryConfig `json:"deposit_deadline,omitempty"` + DepositDeadline Expiration `json:"deposit_deadline"` PartyAShare string `json:"party_a_share"` PartyBShare string `json:"party_b_share"` ExpectedPoolRatio string `json:"expected_pool_ratio"` @@ -45,10 +45,10 @@ type PresetIbcFee struct { type Timestamp string type Block uint64 -type ExpiryConfig struct { - None string `json:"none,omitempty"` - BlockHeight *Block `json:"block,omitempty"` - Time *Timestamp `json:"time,omitempty"` +type Expiration struct { + Never string `json:"none,omitempty"` + AtHeight *Block `json:"at_height,omitempty"` + AtTime *Timestamp `json:"at_time,omitempty"` } type RagequitConfig struct { From ce32f871f1ef469dec152c0ce32b775de2600a76 Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 30 Oct 2023 16:53:12 +0100 Subject: [PATCH 137/586] cleanup; update readme --- Cargo.toml | 1 + contracts/two-party-pol-holder/Cargo.toml | 4 ++-- contracts/two-party-pol-holder/README.md | 10 +++------- contracts/two-party-pol-holder/src/msg.rs | 5 ++--- .../two-party-pol-holder/src/suite_tests/suite.rs | 5 ++--- .../two-party-pol-holder/src/suite_tests/tests.rs | 5 ++--- 6 files changed, 12 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8e759e76..ab812c35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ cw2 = "1.0.1" serde = { version = "1.0.145", default-features = false, features = ["derive"] } thiserror = "1.0.31" schemars = "0.8.10" +cw20 = { version = "0.15" } # dev-dependencies cw-multi-test = "0.16.2" diff --git a/contracts/two-party-pol-holder/Cargo.toml b/contracts/two-party-pol-holder/Cargo.toml index 900cad97..549a6f0c 100644 --- a/contracts/two-party-pol-holder/Cargo.toml +++ b/contracts/two-party-pol-holder/Cargo.toml @@ -31,8 +31,8 @@ serde = { workspace = true } neutron-sdk = { workspace = true } cosmos-sdk-proto = { workspace = true } protobuf = { workspace = true } -astroport = "2.8.0" -cw20 = { version = "0.15" } +astroport = { workspace = true } +cw20 = { workspace = true } cw-utils = { workspace = true } [dev-dependencies] diff --git a/contracts/two-party-pol-holder/README.md b/contracts/two-party-pol-holder/README.md index 4ab12e54..78704063 100644 --- a/contracts/two-party-pol-holder/README.md +++ b/contracts/two-party-pol-holder/README.md @@ -10,9 +10,9 @@ Multiple parties are going to be participating, so the holder should store a lis A `Lock` duration should be stored to keep track of the covenant duration. -After the `Lock` period expires, holder should withdraw the liquidity and forward the underlying funds to the configured splitter module that will deal with the distribution. - -Splitter should be instantiated on demand, when the lock expires. +After the `Lock` period expires, both parties are allowed to submit `Claim` messages. +A successful claim results in the claiming party's liquidity portion being withdrawn from the +pool, and forwarding the underlying assets to the respective router module. ### Ragequit @@ -28,10 +28,6 @@ Ragequit breaks the regular covenant flow in the following way: - splitter module no longer gets instantiated, meaning that any pre-agreed upon token distribution split is void - both parties receive a 50/50 split of the underlying denoms -### Updates - -Both parties are free to update their respective whitelisted addresses and do not need counterparty permission to do so. - ### Deposit funds to Liquid Pooler Both parties should deposit their funds to holder. After holder asserts the expected balances, it forwards diff --git a/contracts/two-party-pol-holder/src/msg.rs b/contracts/two-party-pol-holder/src/msg.rs index 0f4481dd..a2e4f749 100644 --- a/contracts/two-party-pol-holder/src/msg.rs +++ b/contracts/two-party-pol-holder/src/msg.rs @@ -4,7 +4,6 @@ use astroport::asset::Asset; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Api, Attribute, Coin, Decimal, StdError, Binary}; use covenant_macros::{clocked, covenant_clock_address, covenant_next_contract, covenant_deposit_address}; -use covenant_utils::ExpiryConfig; use cw_utils::Expiration; use crate::error::ContractError; @@ -217,7 +216,7 @@ pub enum QueryMsg { ContractState {}, #[returns(RagequitConfig)] RagequitConfig {}, - #[returns(ExpiryConfig)] + #[returns(Expiration)] LockupConfig {}, #[returns(Addr)] PoolAddress {}, @@ -225,7 +224,7 @@ pub enum QueryMsg { ConfigPartyA {}, #[returns(TwoPartyPolCovenantParty)] ConfigPartyB {}, - #[returns(ExpiryConfig)] + #[returns(Expiration)] DepositDeadline {}, #[returns(TwoPartyPolCovenantConfig)] Config {}, diff --git a/contracts/two-party-pol-holder/src/suite_tests/suite.rs b/contracts/two-party-pol-holder/src/suite_tests/suite.rs index 5fb2472d..2f442a69 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/suite.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/suite.rs @@ -3,7 +3,6 @@ use crate::msg::{ TwoPartyPolCovenantParty, }; use cosmwasm_std::{Addr, BlockInfo, Coin, Decimal, Timestamp, Uint128}; -use covenant_utils::ExpiryConfig; use cw_multi_test::{App, AppResponse, Executor, SudoMsg}; use cw_utils::Expiration; @@ -247,14 +246,14 @@ impl Suite { .unwrap() } - pub fn query_deposit_deadline(&self) -> ExpiryConfig { + pub fn query_deposit_deadline(&self) -> Expiration { self.app .wrap() .query_wasm_smart(&self.holder, &QueryMsg::DepositDeadline {}) .unwrap() } - pub fn query_lockup_config(&self) -> ExpiryConfig { + pub fn query_lockup_config(&self) -> Expiration { self.app .wrap() .query_wasm_smart(&self.holder, &QueryMsg::LockupConfig {}) diff --git a/contracts/two-party-pol-holder/src/suite_tests/tests.rs b/contracts/two-party-pol-holder/src/suite_tests/tests.rs index d5104865..c38531a4 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/tests.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/tests.rs @@ -1,5 +1,4 @@ use cosmwasm_std::{Decimal, Timestamp, Uint128}; -use covenant_utils::ExpiryConfig; use cw_utils::Expiration; use crate::{ @@ -31,8 +30,8 @@ fn test_instantiate_happy_and_query_all() { assert_eq!(NEXT_CONTRACT, next_contract.to_string()); assert_eq!(PARTY_A_ROUTER, config_party_a.router); assert_eq!(PARTY_B_ROUTER, config_party_b.router); - assert_eq!(ExpiryConfig::None, deposit_deadline); - assert_eq!(ExpiryConfig::None, lockup_config); + assert_eq!(Expiration::Never { }, deposit_deadline); + assert_eq!(Expiration::Never { }, lockup_config); } #[test] From bb84fe06033bc6b2476839ba400e985a7a0bd06f Mon Sep 17 00:00:00 2001 From: bekauz Date: Mon, 30 Oct 2023 22:44:41 +0100 Subject: [PATCH 138/586] liquid pooler cleanup --- contracts/astroport-liquid-pooler/Cargo.toml | 2 +- contracts/astroport-liquid-pooler/README.md | 29 +++++ .../astroport-liquid-pooler/src/contract.rs | 104 ++++++++---------- contracts/astroport-liquid-pooler/src/msg.rs | 17 +-- .../astroport-liquid-pooler/src/state.rs | 5 +- .../src/suite_test/suite.rs | 14 --- .../two-party-pol-covenant/src/contract.rs | 2 - 7 files changed, 80 insertions(+), 93 deletions(-) create mode 100644 contracts/astroport-liquid-pooler/README.md diff --git a/contracts/astroport-liquid-pooler/Cargo.toml b/contracts/astroport-liquid-pooler/Cargo.toml index f7d7c577..b26796a0 100644 --- a/contracts/astroport-liquid-pooler/Cargo.toml +++ b/contracts/astroport-liquid-pooler/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "covenant-astroport-liquid-pooler" authors = ["benskey bekauz@protonmail.com"] -description = "Astroport LP contract for covenants" +description = "Astroport liquid pooler contract for covenants" license = { workspace = true } repository = { workspace = true } version = { workspace = true } diff --git a/contracts/astroport-liquid-pooler/README.md b/contracts/astroport-liquid-pooler/README.md new file mode 100644 index 00000000..f87d7176 --- /dev/null +++ b/contracts/astroport-liquid-pooler/README.md @@ -0,0 +1,29 @@ +# astroport liquid pooler + +Contract responsible for providing liquidity to a specified pool. + +## Instantiation + +The following parameters are expected to instantiate the liquid pooler: + +`pool_address` - address of the liquidity pool we wish to interact with + +`clock_address` - address of the authorized clock contract to receive ticks from + +`slippage_tolerance` - optional parameter to specify the acceptable slippage tolerance for providing liquidity + +`assets` - TODO + +`single_side_lp_limits` - TODO + +`expected_pool_ratio` - the price at which we expect to provide liquidity at + +`acceptable_pool_ratio_delta` - the acceptable deviation from the expected price above + +`pair_type` - the expected pair type of the pool we wish to enter. used for validation of cases where pool migrates. + +## Flow + +After instantiation, liquid pooler continuously attempts to provide liquidity to the specified pool. +If possible, double sided liquidity is provided. If it is not, liquid pooler attempts to provide single-sided liquidity. +If neither are possible, nothing happens until the next tick is received, at which point it retries. diff --git a/contracts/astroport-liquid-pooler/src/contract.rs b/contracts/astroport-liquid-pooler/src/contract.rs index 9d2bfd05..8270e152 100644 --- a/contracts/astroport-liquid-pooler/src/contract.rs +++ b/contracts/astroport-liquid-pooler/src/contract.rs @@ -2,7 +2,7 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ to_binary, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Reply, Response, - StdError, StdResult, SubMsg, Uint128, WasmMsg, + StdError, StdResult, SubMsg, Uint128, WasmMsg, QuerierWrapper, }; use covenant_clock::helpers::verify_clock; use cw2::set_contract_version; @@ -10,16 +10,16 @@ use cw2::set_contract_version; use astroport::{ asset::{Asset, PairInfo}, pair::{ExecuteMsg::ProvideLiquidity, PoolResponse}, - DecimalCheckedOps, + DecimalCheckedOps, factory::PairType, }; use crate::{ error::ContractError, msg::{ ContractState, ExecuteMsg, InstantiateMsg, LpConfig, MigrateMsg, ProvidedLiquidityInfo, - QueryMsg, DecimalRange, AssetData, + QueryMsg, DecimalRange, }, - state::{ASSETS, HOLDER_ADDRESS, LP_CONFIG, PROVIDED_LIQUIDITY_INFO}, + state::{HOLDER_ADDRESS, LP_CONFIG, PROVIDED_LIQUIDITY_INFO}, }; use neutron_sdk::NeutronResult; @@ -51,7 +51,6 @@ pub fn instantiate( // store the relevant module addresses CLOCK_ADDRESS.save(deps.storage, &clock_addr)?; - ASSETS.save(deps.storage, &msg.assets)?; let decimal_range = DecimalRange::try_from( msg.expected_pool_ratio, @@ -61,10 +60,10 @@ pub fn instantiate( let lp_config = LpConfig { pool_address: pool_addr, single_side_lp_limits: msg.single_side_lp_limits, - autostake: msg.autostake, slippage_tolerance: msg.slippage_tolerance, expected_pool_ratio_range: decimal_range, pair_type: msg.pair_type, + asset_data: msg.assets, }; LP_CONFIG.save(deps.storage, &lp_config)?; @@ -80,8 +79,6 @@ pub fn instantiate( Ok(Response::default() .add_attribute("method", "lp_instantiate") .add_attribute("clock_addr", clock_addr) - .add_attribute("asset_a_denom", msg.assets.asset_a_denom) - .add_attribute("asset_b_denom", msg.assets.asset_b_denom) .add_attributes(lp_config.to_response_attributes())) } @@ -108,51 +105,54 @@ fn try_tick(deps: DepsMut, env: Env, info: MessageInfo) -> Result Result<(), ContractError> { + let pool_response: PairInfo = querier.query_wasm_smart( + &pool, &astroport::pair::QueryMsg::Pair {})?; + if &pool_response.pair_type != pair_type { + return Err(ContractError::PairTypeMismatch {}) + } + Ok(()) +} + /// method which attempts to provision liquidity to the pool. /// if both desired asset balances are non-zero, double sided liquidity /// is provided. /// otherwise, single-sided liquidity provision is attempted. fn try_lp(mut deps: DepsMut, env: Env) -> Result { - let asset_data = ASSETS.load(deps.storage)?; let lp_config = LP_CONFIG.load(deps.storage)?; // validate that the pool did not migrate to a new pair type - let pool_response: PairInfo = deps - .querier - .query_wasm_smart(&lp_config.pool_address, &astroport::pair::QueryMsg::Pair {})?; - if pool_response.pair_type != lp_config.pair_type { - return Err(ContractError::PairTypeMismatch {}) - } + validate_pair_type(deps.querier, lp_config.pool_address.to_string(), &lp_config.pair_type)?; - let pool_response: PoolResponse = deps - .querier + let pool_response: PoolResponse = deps.querier .query_wasm_smart(&lp_config.pool_address, &astroport::pair::QueryMsg::Pool {})?; let (pool_token_a_bal, pool_token_b_bal) = get_pool_asset_amounts( pool_response.assets, - &asset_data.asset_a_denom.as_str(), - &asset_data.asset_b_denom.as_str(), + &lp_config.asset_data.asset_a_denom.as_str(), + &lp_config.asset_data.asset_b_denom.as_str(), )?; let a_to_b_ratio = Decimal::from_ratio(pool_token_a_bal, pool_token_b_bal); // validate the current pool ratio against our expectations lp_config.expected_pool_ratio_range.is_within_range(a_to_b_ratio)?; // first we query our own balances and filter out any unexpected denoms - let bal_coins = deps - .querier - .query_all_balances(env.contract.address.to_string())?; + let bal_coins = deps.querier.query_all_balances(env.contract.address.to_string())?; let (coin_a, coin_b) = get_relevant_balances( bal_coins, - asset_data.asset_a_denom.as_str(), - asset_data.asset_b_denom.as_str(), + lp_config.asset_data.asset_a_denom.as_str(), + lp_config.asset_data.asset_b_denom.as_str(), ); // depending on available balances we attempt a different action: match (coin_a.amount.is_zero(), coin_b.amount.is_zero()) { // exactly one balance is non-zero, we attempt single-side (true, false) | (false, true) => { - let single_sided_submsg = - try_get_single_side_lp_submsg(deps.branch(), coin_a, coin_b, lp_config, asset_data)?; + let single_sided_submsg = try_get_single_side_lp_submsg( + deps.branch(), + coin_a, + coin_b, + lp_config)?; if let Some(msg) = single_sided_submsg { return Ok(Response::default() .add_submessage(msg) @@ -161,8 +161,14 @@ fn try_lp(mut deps: DepsMut, env: Env) -> Result { } // both balances are non-zero, we attempt double-side (false, false) => { - let double_sided_submsg = - try_get_double_side_lp_submsg(deps.branch(), coin_a, coin_b, a_to_b_ratio, pool_token_a_bal, pool_token_b_bal, lp_config, asset_data)?; + let double_sided_submsg = try_get_double_side_lp_submsg( + deps.branch(), + coin_a, + coin_b, + a_to_b_ratio, + pool_token_a_bal, + pool_token_b_bal, + lp_config)?; if let Some(msg) = double_sided_submsg { return Ok(Response::default() .add_submessage(msg) @@ -191,14 +197,12 @@ fn try_get_double_side_lp_submsg( pool_token_a_bal: Uint128, pool_token_b_bal: Uint128, lp_config: LpConfig, - asset_data: AssetData, ) -> Result, ContractError> { let holder_address = match HOLDER_ADDRESS.may_load(deps.storage)? { Some(addr) => addr, None => return Err(ContractError::MissingHolderError {}), }; - // we thus find the required token amount to enter into the position using all available b tokens: let required_token_a_amount = pool_token_ratio.checked_mul_uint128(token_b.amount)?; @@ -208,30 +212,26 @@ fn try_get_double_side_lp_submsg( if token_a.amount >= required_token_a_amount { // if we are able to satisfy the required amount, we do that: // provide all b tokens along with required amount of a tokens - asset_data.to_tuple(required_token_a_amount, token_b.amount) + lp_config.asset_data.to_tuple(required_token_a_amount, token_b.amount) } else { // otherwise, our token a amount is insufficient to provide double // sided liquidity using all of our b tokens. // this means that we should provide all of our available a tokens, // and as many b tokens as needed to satisfy the existing ratio - asset_data.to_tuple( + let ratio = Decimal::from_ratio(pool_token_b_bal, pool_token_a_bal); + lp_config.asset_data.to_tuple( token_a.amount, - Decimal::from_ratio( - pool_token_b_bal, - pool_token_a_bal - ).checked_mul_uint128(token_a.amount)?) + ratio.checked_mul_uint128(token_a.amount)?) }; - let (a_coin, b_coin) = ( - asset_a_double_sided.to_coin()?, - asset_b_double_sided.to_coin()?, - ); + let a_coin = asset_a_double_sided.to_coin()?; + let b_coin = asset_b_double_sided.to_coin()?; // craft a ProvideLiquidity message with the determined assets let double_sided_liq_msg = ProvideLiquidity { assets: vec![asset_a_double_sided, asset_b_double_sided], slippage_tolerance: lp_config.slippage_tolerance, - auto_stake: lp_config.autostake, + auto_stake: Some(false), receiver: Some(holder_address.to_string()), }; @@ -266,27 +266,24 @@ fn try_get_single_side_lp_submsg( coin_a: Coin, coin_b: Coin, lp_config: LpConfig, - asset_data: AssetData, ) -> Result, ContractError> { let holder_address = match HOLDER_ADDRESS.may_load(deps.storage)? { Some(addr) => addr, None => return Err(ContractError::MissingHolderError {}), }; - let assets = asset_data.to_asset_vec(coin_a.amount, coin_b.amount); + let assets = lp_config.asset_data.to_asset_vec(coin_a.amount, coin_b.amount); // given one non-zero asset, we build the ProvideLiquidity message let single_sided_liq_msg = ProvideLiquidity { assets, slippage_tolerance: lp_config.slippage_tolerance, - auto_stake: lp_config.autostake, + auto_stake: Some(false), receiver: Some(holder_address.to_string()), }; // now we try to submit the message for either B or A token single side liquidity - if coin_a.amount.is_zero() - && coin_b.amount <= lp_config.single_side_lp_limits.asset_b_limit - { + if coin_a.amount.is_zero() && coin_b.amount <= lp_config.single_side_lp_limits.asset_b_limit { // update the provided liquidity info PROVIDED_LIQUIDITY_INFO.update(deps.storage, |mut info| -> StdResult<_> { info.provided_amount_b = info.provided_amount_b.checked_add(coin_b.amount)?; @@ -304,9 +301,7 @@ fn try_get_single_side_lp_submsg( ); return Ok(Some(submsg)); - } else if coin_b.amount.is_zero() - && coin_a.amount <= lp_config.single_side_lp_limits.asset_a_limit - { + } else if coin_b.amount.is_zero() && coin_a.amount <= lp_config.single_side_lp_limits.asset_a_limit { // update the provided liquidity info PROVIDED_LIQUIDITY_INFO.update(deps.storage, |mut info| -> StdResult<_> { info.provided_amount_a = @@ -375,7 +370,6 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::ClockAddress {} => Ok(to_binary(&CLOCK_ADDRESS.may_load(deps.storage)?)?), QueryMsg::ContractState {} => Ok(to_binary(&CONTRACT_STATE.may_load(deps.storage)?)?), QueryMsg::HolderAddress {} => Ok(to_binary(&HOLDER_ADDRESS.may_load(deps.storage)?)?), - QueryMsg::Assets {} => Ok(to_binary(&ASSETS.may_load(deps.storage)?)?), QueryMsg::LpConfig {} => Ok(to_binary(&LP_CONFIG.may_load(deps.storage)?)?), // the deposit address for LP module is the contract itself QueryMsg::DepositAddress {} => Ok(to_binary(&Some(&env.contract.address.to_string()))?), @@ -391,7 +385,6 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> NeutronResult { let mut response = Response::default().add_attribute("method", "update_config"); @@ -406,13 +399,6 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> NeutronResult, - pub autostake: Option, pub assets: AssetData, pub single_side_lp_limits: SingleSideLpLimits, pub expected_pool_ratio: Decimal, @@ -21,7 +20,6 @@ pub struct InstantiateMsg { #[cw_serde] pub struct PresetAstroLiquidPoolerFields { pub slippage_tolerance: Option, - pub autostake: Option, pub assets: AssetData, pub single_side_lp_limits: SingleSideLpLimits, pub label: String, @@ -41,7 +39,6 @@ impl PresetAstroLiquidPoolerFields { pool_address, clock_address, slippage_tolerance: self.slippage_tolerance, - autostake: self.autostake.clone(), assets: self.assets.clone(), single_side_lp_limits: self.single_side_lp_limits.clone(), expected_pool_ratio: self.expected_pool_ratio, @@ -82,10 +79,10 @@ impl DecimalRange { pub struct LpConfig { /// address of the liquidity pool we plan to enter pub pool_address: Addr, + /// denoms of both parties + pub asset_data: AssetData, /// amounts of both tokens we consider ok to single-side lp pub single_side_lp_limits: SingleSideLpLimits, - /// boolean flag for enabling autostaking of LP tokens upon liquidity provisioning - pub autostake: Option, /// slippage tolerance parameter for liquidity provisioning pub slippage_tolerance: Option, /// expected price range @@ -96,10 +93,6 @@ pub struct LpConfig { impl LpConfig { pub fn to_response_attributes(self) -> Vec { - let autostake = match self.autostake { - Some(val) => val.to_string(), - None => "None".to_string(), - }; let slippage_tolerance = match self.slippage_tolerance { Some(val) => val.to_string(), None => "None".to_string(), @@ -114,8 +107,9 @@ impl LpConfig { "single_side_asset_b_limit", self.single_side_lp_limits.asset_b_limit.to_string(), ), - Attribute::new("autostake", autostake), Attribute::new("slippage_tolerance", slippage_tolerance), + Attribute::new("party_a_denom", self.asset_data.asset_a_denom), + Attribute::new("party_b_denom", self.asset_data.asset_b_denom), ] } } @@ -178,8 +172,6 @@ pub enum QueryMsg { ContractState {}, #[returns(Addr)] HolderAddress {}, - #[returns(Vec)] - Assets {}, #[returns(LpConfig)] LpConfig {}, #[returns(ProvidedLiquidityInfo)] @@ -191,7 +183,6 @@ pub enum MigrateMsg { UpdateConfig { clock_addr: Option, holder_address: Option, - assets: Option, lp_config: Option, }, UpdateCodeId { diff --git a/contracts/astroport-liquid-pooler/src/state.rs b/contracts/astroport-liquid-pooler/src/state.rs index afcd9d31..0952fde5 100644 --- a/contracts/astroport-liquid-pooler/src/state.rs +++ b/contracts/astroport-liquid-pooler/src/state.rs @@ -1,14 +1,11 @@ use cosmwasm_std::Addr; use cw_storage_plus::Item; -use crate::msg::{AssetData, ContractState, LpConfig, ProvidedLiquidityInfo}; +use crate::msg::{ContractState, LpConfig, ProvidedLiquidityInfo}; /// contract state tracks the state machine progress pub const CONTRACT_STATE: Item = Item::new("contract_state"); -/// asset denom information -pub const ASSETS: Item = Item::new("assets"); - /// clock module address to verify the incoming ticks sender pub const CLOCK_ADDRESS: Item = Item::new("clock_address"); /// holder module address to verify withdrawal requests diff --git a/contracts/astroport-liquid-pooler/src/suite_test/suite.rs b/contracts/astroport-liquid-pooler/src/suite_test/suite.rs index b6a59d49..2555232a 100644 --- a/contracts/astroport-liquid-pooler/src/suite_test/suite.rs +++ b/contracts/astroport-liquid-pooler/src/suite_test/suite.rs @@ -145,7 +145,6 @@ impl Default for SuiteBuilder { clock_address: "clock-addr".to_string(), pool_address: "lp-addr".to_string(), slippage_tolerance: Some(Decimal::one()), - autostake: Some(false), assets: AssetData { asset_a_denom: "uatom".to_string(), asset_b_denom: "untrn".to_string(), @@ -224,11 +223,6 @@ impl SuiteBuilder { self } - fn with_autostake(mut self, autosake: Option) -> Self { - self.lp_instantiate.autostake = autosake; - self - } - fn with_assets(mut self, assets: AssetData) -> Self { self.lp_instantiate.assets = assets; self @@ -411,7 +405,6 @@ impl SuiteBuilder { &MigrateMsg::UpdateConfig { clock_addr: None, holder_address: Some(CREATOR_ADDR.to_string()), - assets: None, lp_config: None, }, lper_code, @@ -466,13 +459,6 @@ impl Suite { .unwrap() } - pub fn query_assets(&self) -> Vec { - self.app - .wrap() - .query_wasm_smart(&self.liquid_pooler.1, &QueryMsg::Assets {}) - .unwrap() - } - pub fn query_addr_balances(&self, addr: Addr) -> Vec { self.app.wrap().query_all_balances(addr).unwrap() } diff --git a/contracts/two-party-pol-covenant/src/contract.rs b/contracts/two-party-pol-covenant/src/contract.rs index 2fbb2723..104f8899 100644 --- a/contracts/two-party-pol-covenant/src/contract.rs +++ b/contracts/two-party-pol-covenant/src/contract.rs @@ -116,7 +116,6 @@ pub fn instantiate( let preset_liquid_pooler_fields = PresetAstroLiquidPoolerFields { slippage_tolerance: None, - autostake: None, assets: AssetData { asset_a_denom: msg.party_a_config.ibc_denom, asset_b_denom: msg.party_b_config.ibc_denom, @@ -466,7 +465,6 @@ pub fn handle_party_b_ibc_forwarder_reply( msg: to_binary(&covenant_astroport_liquid_pooler::msg::MigrateMsg::UpdateConfig { clock_addr: None, holder_address: Some(holder.to_string()), - assets: None, lp_config: None, })?, }; From bc9b49cb2bcc22d39c2487958985d3f6dcaa9ec4 Mon Sep 17 00:00:00 2001 From: bekauz Date: Tue, 31 Oct 2023 22:20:37 +0100 Subject: [PATCH 139/586] ragequit interchaintest --- two-party-pol-covenant/justfile | 2 +- .../interchaintest/two_party_pol_test.go | 1095 +++++++++++++---- 2 files changed, 833 insertions(+), 264 deletions(-) diff --git a/two-party-pol-covenant/justfile b/two-party-pol-covenant/justfile index 967520c4..877524b4 100644 --- a/two-party-pol-covenant/justfile +++ b/two-party-pol-covenant/justfile @@ -43,4 +43,4 @@ simtest: optimize ictest: go clean -testcache - cd tests/interchaintest/ && go test -timeout 20m -v ./... \ No newline at end of file + cd tests/interchaintest/ && go test -timeout 30m -v ./... \ No newline at end of file diff --git a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go index b5796471..d566da0e 100644 --- a/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go +++ b/two-party-pol-covenant/tests/interchaintest/two_party_pol_test.go @@ -734,294 +734,863 @@ func TestTwoPartyPol(t *testing.T) { println("neutronUser lp token bal: ", neutronUserLPTokenBal) }) - t.Run("instantiate covenant", func(t *testing.T) { - timeouts := Timeouts{ - IcaTimeout: "100", // sec - IbcTransferTimeout: "100", // sec - } + // t.Run("two party POL happy path", func(t *testing.T) { + + // tickClock := func() { + // println("\ntick") + // cmd := []string{"neutrond", "tx", "wasm", "execute", clockAddress, + // `{"tick":{}}`, + // "--gas-prices", "0.0untrn", + // "--gas-adjustment", `1.5`, + // "--output", "json", + // "--home", "/var/cosmos-chain/neutron-2", + // "--node", neutron.GetRPCAddress(), + // "--home", neutron.HomeDir(), + // "--chain-id", neutron.Config().ChainID, + // "--from", neutronUser.KeyName, + // "--gas", "1500000", + // "--keyring-backend", keyring.BackendTest, + // "-y", + // } + + // resp, _, err := cosmosNeutron.Exec(ctx, cmd, nil) + // require.NoError(t, err) + // println("tick response: ", string(resp), "\n") + // err = testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis) + // require.NoError(t, err, "failed to wait for blocks") + // } + + // t.Run("instantiate covenant", func(t *testing.T) { + // timeouts := Timeouts{ + // IcaTimeout: "100", // sec + // IbcTransferTimeout: "100", // sec + // } + + // depositBlock := Block(500) + // lockupBlock := Block(500) + + // lockupConfig := Expiration{ + // AtHeight: &lockupBlock, + // } + // depositDeadline := Expiration{ + // AtHeight: &depositBlock, + // } + // presetIbcFee := PresetIbcFee{ + // AckFee: "10000", + // TimeoutFee: "10000", + // } + + // atomCoin := Coin{ + // Denom: cosmosAtom.Config().Denom, + // Amount: strconv.FormatUint(atomContributionAmount, 10), + // } + + // osmoCoin := Coin{ + // Denom: cosmosOsmosis.Config().Denom, + // Amount: strconv.FormatUint(osmoContributionAmount, 10), + // } + + // partyAConfig := CovenantPartyConfig{ + // ControllerAddr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + // HostAddr: hubNeutronAccount.Bech32Address(cosmosNeutron.Config().Bech32Prefix), + // Contribution: atomCoin, + // IbcDenom: neutronAtomIbcDenom, + // PartyToHostChainChannelId: testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], + // HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], + // PartyReceiverAddr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + // PartyChainConnectionId: neutronAtomIBCConnId, + // IbcTransferTimeout: timeouts.IbcTransferTimeout, + // } + // partyBConfig := CovenantPartyConfig{ + // ControllerAddr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + // HostAddr: osmoNeutronAccount.Bech32Address(cosmosNeutron.Config().Bech32Prefix), + // Contribution: osmoCoin, + // IbcDenom: neutronOsmoIbcDenom, + // PartyToHostChainChannelId: testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name], + // HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], + // PartyReceiverAddr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + // PartyChainConnectionId: neutronOsmosisIBCConnId, + // IbcTransferTimeout: timeouts.IbcTransferTimeout, + // } + // codeIds := ContractCodeIds{ + // IbcForwarderCode: ibcForwarderCodeId, + // InterchainRouterCode: routerCodeId, + // ClockCode: clockCodeId, + // HolderCode: holderCodeId, + // LiquidPoolerCode: lperCodeId, + // } + + // ragequitTerms := RagequitTerms{ + // Penalty: "0.1", + // } + + // ragequitConfig := RagequitConfig{ + // Enabled: &ragequitTerms, + // } + + // poolAddress := stableswapAddress + // pairType := PairType{ + // Stable: struct{}{}, + // } + + // covenantMsg := CovenantInstantiateMsg{ + // Label: "two-party-pol-covenant", + // Timeouts: timeouts, + // PresetIbcFee: presetIbcFee, + // ContractCodeIds: codeIds, + // LockupConfig: lockupConfig, + // PartyAConfig: partyAConfig, + // PartyBConfig: partyBConfig, + // PoolAddress: poolAddress, + // RagequitConfig: &ragequitConfig, + // DepositDeadline: depositDeadline, + // PartyAShare: "50", + // PartyBShare: "50", + // ExpectedPoolRatio: "0.1", + // AcceptablePoolRatioDelta: "0.09", + // PairType: pairType, + // } + // str, err := json.Marshal(covenantMsg) + // require.NoError(t, err, "Failed to marshall CovenantInstantiateMsg") + // instantiateMsg := string(str) + + // println("instantiation message: ", instantiateMsg) + // cmd := []string{"neutrond", "tx", "wasm", "instantiate", covenantCodeIdStr, + // instantiateMsg, + // "--label", "two-party-pol-covenant", + // "--no-admin", + // "--from", neutronUser.KeyName, + // "--output", "json", + // "--home", neutron.HomeDir(), + // "--node", neutron.GetRPCAddress(), + // "--chain-id", neutron.Config().ChainID, + // "--gas", "90009000", + // "--keyring-backend", keyring.BackendTest, + // "-y", + // } + + // _, _, err = neutron.Exec(ctx, cmd, nil) + // require.NoError(t, err) + // require.NoError(t, testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis)) + + // queryCmd := []string{"neutrond", "query", "wasm", + // "list-contract-by-code", covenantCodeIdStr, + // "--output", "json", + // "--home", neutron.HomeDir(), + // "--node", neutron.GetRPCAddress(), + // "--chain-id", neutron.Config().ChainID, + // } + + // queryResp, _, err := neutron.Exec(ctx, queryCmd, nil) + // require.NoError(t, err, "failed to query") + + // type QueryContractResponse struct { + // Contracts []string `json:"contracts"` + // Pagination any `json:"pagination"` + // } + + // contactsRes := QueryContractResponse{} + // require.NoError(t, json.Unmarshal(queryResp, &contactsRes), "failed to unmarshal contract response") + + // covenantAddress = contactsRes.Contracts[len(contactsRes.Contracts)-1] + + // println("covenant address: ", covenantAddress) + // }) + + // t.Run("query covenant contracts", func(t *testing.T) { + // routerQueryPartyA := InterchainRouterQuery{ + // Party: Party{ + // Party: "party_a", + // }, + // } + // routerQueryPartyB := InterchainRouterQuery{ + // Party: Party{ + // Party: "party_b", + // }, + // } + // forwarderQueryPartyA := IbcForwarderQuery{ + // Party: Party{ + // Party: "party_a", + // }, + // } + // forwarderQueryPartyB := IbcForwarderQuery{ + // Party: Party{ + // Party: "party_b", + // }, + // } + + // var response CovenantAddressQueryResponse + + // err = cosmosNeutron.QueryContract(ctx, covenantAddress, ClockAddressQuery{}, &response) + // require.NoError(t, err, "failed to query instantiated clock address") + // clockAddress = response.Data + // println("clock addr: ", clockAddress) + + // err = cosmosNeutron.QueryContract(ctx, covenantAddress, HolderAddressQuery{}, &response) + // require.NoError(t, err, "failed to query instantiated holder address") + // holderAddress = response.Data + // println("holder addr: ", holderAddress) + + // err = cosmosNeutron.QueryContract(ctx, covenantAddress, LiquidPoolerQuery{}, &response) + // require.NoError(t, err, "failed to query instantiated liquid pooler address") + // liquidPoolerAddress = response.Data + // println("liquid pooler addr: ", liquidPoolerAddress) + + // err = cosmosNeutron.QueryContract(ctx, covenantAddress, routerQueryPartyA, &response) + // require.NoError(t, err, "failed to query instantiated party a router address") + // partyARouterAddress = response.Data + // println("partyARouterAddress: ", partyARouterAddress) + + // err = cosmosNeutron.QueryContract(ctx, covenantAddress, routerQueryPartyB, &response) + // require.NoError(t, err, "failed to query instantiated party b router address") + // partyBRouterAddress = response.Data + // println("partyBRouterAddress: ", partyBRouterAddress) + + // err = cosmosNeutron.QueryContract(ctx, covenantAddress, forwarderQueryPartyA, &response) + // require.NoError(t, err, "failed to query instantiated party a forwarder address") + // partyAIbcForwarderAddress = response.Data + // println("partyAIbcForwarderAddress: ", partyAIbcForwarderAddress) + + // err = cosmosNeutron.QueryContract(ctx, covenantAddress, forwarderQueryPartyB, &response) + // require.NoError(t, err, "failed to query instantiated party b forwarder address") + // partyBIbcForwarderAddress = response.Data + // println("partyBIbcForwarderAddress: ", partyBIbcForwarderAddress) + // }) + + // t.Run("fund contracts with neutron", func(t *testing.T) { + // err := neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + // Address: partyAIbcForwarderAddress, + // Amount: 5000000001, + // Denom: nativeNtrnDenom, + // }) + // require.NoError(t, err, "failed to send funds from neutron user to partyAIbcForwarder contract") + + // err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + // Address: partyBIbcForwarderAddress, + // Amount: 5000000001, + // Denom: nativeNtrnDenom, + // }) + // require.NoError(t, err, "failed to send funds from neutron user to partyBIbcForwarder contract") + + // err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + // Address: clockAddress, + // Amount: 5000000001, + // Denom: nativeNtrnDenom, + // }) + // require.NoError(t, err, "failed to send funds from neutron user to clock contract") + // err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + // Address: partyARouterAddress, + // Amount: 5000000001, + // Denom: nativeNtrnDenom, + // }) + // require.NoError(t, err, "failed to send funds from neutron user to party a router") + // err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + // Address: partyBRouterAddress, + // Amount: 5000000001, + // Denom: nativeNtrnDenom, + // }) + // require.NoError(t, err, "failed to send funds from neutron user to party b router") + // err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + // Address: holderAddress, + // Amount: 5000000001, + // Denom: nativeNtrnDenom, + // }) + // require.NoError(t, err, "failed to send funds from neutron user to holder") + // err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + // Address: liquidPoolerAddress, + // Amount: 5000000001, + // Denom: nativeNtrnDenom, + // }) + // require.NoError(t, err, "failed to send funds from neutron user to holder") + + // err = testutil.WaitForBlocks(ctx, 2, atom, neutron, osmosis) + // require.NoError(t, err, "failed to wait for blocks") + + // bal, err := neutron.GetBalance(ctx, partyAIbcForwarderAddress, nativeNtrnDenom) + // require.NoError(t, err) + // require.Equal(t, int64(5000000001), bal) + // bal, err = neutron.GetBalance(ctx, partyBIbcForwarderAddress, nativeNtrnDenom) + // require.NoError(t, err) + // require.Equal(t, int64(5000000001), bal) + // bal, err = neutron.GetBalance(ctx, clockAddress, nativeNtrnDenom) + // require.NoError(t, err) + // require.Equal(t, int64(5000000001), bal) + // bal, err = neutron.GetBalance(ctx, partyARouterAddress, nativeNtrnDenom) + // require.NoError(t, err) + // require.Equal(t, int64(5000000001), bal) + // bal, err = neutron.GetBalance(ctx, partyBRouterAddress, nativeNtrnDenom) + // require.NoError(t, err) + // require.Equal(t, int64(5000000001), bal) + // }) + + // t.Run("tick until forwarders create ICA", func(t *testing.T) { + // require.NoError(t, testutil.WaitForBlocks(ctx, 15, atom, neutron, osmosis), "failed to wait for blocks") + // for { + // tickClock() + // var response CovenantAddressQueryResponse + // type ContractState struct{} + // type ContractStateQuery struct { + // ContractState ContractState `json:"contract_state"` + // } + // contractStateQuery := ContractStateQuery{ + // ContractState: ContractState{}, + // } + + // require.NoError(t, + // cosmosNeutron.QueryContract(ctx, partyAIbcForwarderAddress, contractStateQuery, &response), + // "failed to query forwarder A state") + // forwarderAState := response.Data + + // require.NoError(t, + // cosmosNeutron.QueryContract(ctx, partyBIbcForwarderAddress, contractStateQuery, &response), + // "failed to query forwarder B state") + // forwarderBState := response.Data + + // if forwarderAState == forwarderBState && forwarderBState == "ica_created" { + // require.NoError(t, testutil.WaitForBlocks(ctx, 15, atom, neutron, osmosis), "failed to wait for blocks") + + // var depositAddressResponse CovenantAddressQueryResponse + + // type DepositAddress struct{} + // type DepositAddressQuery struct { + // DepositAddress DepositAddress `json:"deposit_address"` + // } + // depositAddressQuery := DepositAddressQuery{ + // DepositAddress: DepositAddress{}, + // } + + // err := cosmosNeutron.QueryContract(ctx, partyAIbcForwarderAddress, depositAddressQuery, &depositAddressResponse) + // require.NoError(t, err, "failed to query party a forwarder deposit address") + // partyADepositAddress = depositAddressResponse.Data + + // err = cosmosNeutron.QueryContract(ctx, partyBIbcForwarderAddress, depositAddressQuery, &depositAddressResponse) + // require.NoError(t, err, "failed to query party b forwarder deposit address") + // partyBDepositAddress = depositAddressResponse.Data + // println("both parties icas created: ", partyADepositAddress, " , ", partyBDepositAddress) + // break + // } + // } + // }) + + // t.Run("fund the forwarders with sufficient funds", func(t *testing.T) { + + // err := cosmosOsmosis.SendFunds(ctx, osmoUser.KeyName, ibc.WalletAmount{ + // Address: partyBDepositAddress, + // Denom: nativeOsmoDenom, + // Amount: int64(osmoContributionAmount + 1), + // }) + // require.NoError(t, err, "failed to fund osmo forwarder") + // err = cosmosAtom.SendFunds(ctx, gaiaUser.KeyName, ibc.WalletAmount{ + // Address: partyADepositAddress, + // Denom: nativeAtomDenom, + // Amount: int64(atomContributionAmount + 1), + // }) + // require.NoError(t, err, "failed to fund gaia forwarder") + + // err = testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis) + // require.NoError(t, err, "failed to wait for blocks") + + // bal, err := cosmosAtom.GetBalance(ctx, partyADepositAddress, nativeAtomDenom) + // require.NoError(t, err, "failed to query bal") + // require.Equal(t, int64(atomContributionAmount+1), bal) + // bal, err = cosmosOsmosis.GetBalance(ctx, partyBDepositAddress, nativeOsmoDenom) + // require.NoError(t, err, "failed to query bal") + // require.Equal(t, int64(osmoContributionAmount+1), bal) + // }) + + // t.Run("tick until forwarders forward the funds to holder", func(t *testing.T) { + // for { + // holderOsmoBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronOsmoIbcDenom) + // require.NoError(t, err, "failed to query holder osmo bal") + // holderAtomBal, err := cosmosNeutron.GetBalance(ctx, holderAddress, neutronAtomIbcDenom) + // require.NoError(t, err, "failed to query holder atom bal") + // // liquidPoolerOsmoBal, err := cosmosNeutron.GetBalance(ctx, liquidPoolerAddress, neutronOsmoIbcDenom) + // // require.NoError(t, err, "failed to query liquidPooler osmo bal") + // // liquidPoolerAtomBal, err := cosmosNeutron.GetBalance(ctx, liquidPoolerAddress, neutronAtomIbcDenom) + // // require.NoError(t, err, "failed to query liquidPooler atom bal") + // println("holder atom bal: ", holderAtomBal) + // println("holder osmo bal: ", holderOsmoBal) + + // var response CovenantAddressQueryResponse + // type ContractState struct{} + // type ContractStateQuery struct { + // ContractState ContractState `json:"contract_state"` + // } + // contractStateQuery := ContractStateQuery{ + // ContractState: ContractState{}, + // } + + // require.NoError(t, + // cosmosNeutron.QueryContract(ctx, holderAddress, contractStateQuery, &response), + // "failed to query holder state") + // holderState := response.Data + // println("holder state: ", holderState) + + // if holderAtomBal == int64(atomContributionAmount) && holderOsmoBal == int64(osmoContributionAmount) || holderState == "active" { + // println("\nholder/liquidpooler received atom & osmo\n") + // break + // } else { + // tickClock() + // } + // } + // }) + + // t.Run("tick until holder sends the funds to LPer", func(t *testing.T) { + // for { + // liquidPoolerOsmoBal, err := cosmosNeutron.GetBalance(ctx, liquidPoolerAddress, neutronOsmoIbcDenom) + // require.NoError(t, err, "failed to query liquidPooler osmo bal") + // liquidPoolerAtomBal, err := cosmosNeutron.GetBalance(ctx, liquidPoolerAddress, neutronAtomIbcDenom) + // require.NoError(t, err, "failed to query liquidPooler atom bal") + // holderLpTokenBal := queryLpTokenBalance(liquidityTokenAddress, holderAddress) + + // println("liquid pooler atom bal: ", liquidPoolerAtomBal) + // println("liquid pooler osmo bal: ", liquidPoolerOsmoBal) + // println("holder lp token balance: ", holderLpTokenBal) + + // if liquidPoolerOsmoBal == int64(osmoContributionAmount) && liquidPoolerAtomBal == int64(atomContributionAmount) { + // break + // } else { + // tickClock() + // } + // } + // }) + + // t.Run("tick until holder receives LP tokens", func(t *testing.T) { + // for { + // holderLpTokenBal := queryLpTokenBalance(liquidityTokenAddress, holderAddress) + // println("holder lp token balance: ", holderLpTokenBal) + // holderLpBal, err := strconv.ParseUint(holderLpTokenBal, 10, 64) + // if err != nil { + // panic(err) + // } + + // if holderLpBal == 0 { + // tickClock() + // } else { + // break + // } + // } + // }) + + // t.Run("tick until holder expires", func(t *testing.T) { + // for { + // neutronHeight, err := cosmosNeutron.Height(ctx) + // require.NoError(t, err) + + // if neutronHeight >= 515 { + // println("neutron height: ", neutronHeight) + // break + // } else { + // tickClock() + // } + // } + // }) + + // t.Run("party A claims and router receives the funds", func(t *testing.T) { + + // cmd := []string{"neutrond", "tx", "wasm", "execute", holderAddress, + // `{"claim":{}}`, + // "--from", hubNeutronAccount.GetKeyName(), + // "--gas-prices", "0.0untrn", + // "--gas-adjustment", `1.5`, + // "--output", "json", + // "--node", neutron.GetRPCAddress(), + // "--home", neutron.HomeDir(), + // "--chain-id", neutron.Config().ChainID, + // "--gas", "42069420", + // "--keyring-backend", keyring.BackendTest, + // "-y", + // } + // println("hub claim msg: ", strings.Join(cmd, " ")) + + // for { + // routerAtomBalA, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronAtomIbcDenom) + // require.NoError(t, err) + + // routerOsmoBalA, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronOsmoIbcDenom) + // require.NoError(t, err) + + // println("routerAtomBalA: ", routerAtomBalA) + // println("routerOsmoBalA: ", routerOsmoBalA) + + // if routerAtomBalA != 0 && routerOsmoBalA != 0 { + // break + // } else { + // tickClock() + // _, _, err = cosmosNeutron.Exec(ctx, cmd, nil) + // require.NoError(t, err, "party A claim failed") + + // err = testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis) + // require.NoError(t, err, "failed to wait for blocks") + // } + // } + // }) + + // t.Run("party B claims and router receives the funds", func(t *testing.T) { + + // cmd := []string{"neutrond", "tx", "wasm", "execute", holderAddress, + // `{"claim":{}}`, + // "--from", osmoNeutronAccount.GetKeyName(), + // "--gas-prices", "0.0untrn", + // "--gas-adjustment", `1.8`, + // "--output", "json", + // "--node", neutron.GetRPCAddress(), + // "--home", neutron.HomeDir(), + // "--chain-id", neutron.Config().ChainID, + // "--gas", "42069420", + // "--keyring-backend", keyring.BackendTest, + // "-y", + // } + + // println("osmo claim msg: ", strings.Join(cmd, " ")) + // _, _, err := cosmosNeutron.Exec(ctx, cmd, nil) + // require.NoError(t, err, "party B claim failed") + + // err = testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis) + // require.NoError(t, err, "failed to wait for blocks") + + // for { + // routerAtomBalB, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronAtomIbcDenom) + // require.NoError(t, err) + + // routerOsmoBalB, err := cosmosNeutron.GetBalance(ctx, partyBRouterAddress, neutronOsmoIbcDenom) + // require.NoError(t, err) + + // println("routerAtomBalB: ", routerAtomBalB) + // println("routerOsmoBalB: ", routerOsmoBalB) + + // if routerAtomBalB != 0 || routerOsmoBalB != 0 { + // break + // } else { + // tickClock() + // } + // } + // }) + + // t.Run("tick routers until both parties receive their funds", func(t *testing.T) { + // for { + // osmoBalPartyA, err := cosmosAtom.GetBalance( + // ctx, gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), gaiaNeutronOsmoIbcDenom, + // ) + // require.NoError(t, err) + + // osmoBalPartyB, err := cosmosOsmosis.GetBalance( + // ctx, osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), cosmosOsmosis.Config().Denom, + // ) + // require.NoError(t, err) + + // atomBalPartyA, err := cosmosAtom.GetBalance( + // ctx, gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), cosmosAtom.Config().Denom, + // ) + // require.NoError(t, err) + + // atomBalPartyB, err := cosmosOsmosis.GetBalance( + // ctx, osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), osmoNeutronAtomIbcDenom, + // ) + // require.NoError(t, err) + + // println("party A osmo bal: ", osmoBalPartyA) + // println("party A atom bal: ", atomBalPartyA) + // println("party B osmo bal: ", osmoBalPartyB) + // println("party B atom bal: ", atomBalPartyB) + + // if osmoBalPartyA != 0 && atomBalPartyA != 0 && osmoBalPartyB != 0 && atomBalPartyB != 0 { + // break + // } + + // tickClock() + // } + // }) + // }) + + t.Run("two party POL ragequit path", func(t *testing.T) { - depositBlock := Block(500) - lockupBlock := Block(500) + tickClock := func() { + println("\ntick") + cmd := []string{"neutrond", "tx", "wasm", "execute", clockAddress, + `{"tick":{}}`, + "--gas-prices", "0.0untrn", + "--gas-adjustment", `1.5`, + "--output", "json", + "--home", "/var/cosmos-chain/neutron-2", + "--node", neutron.GetRPCAddress(), + "--home", neutron.HomeDir(), + "--chain-id", neutron.Config().ChainID, + "--from", neutronUser.KeyName, + "--gas", "1500000", + "--keyring-backend", keyring.BackendTest, + "-y", + } - lockupConfig := Expiration{ - AtHeight: &lockupBlock, - } - depositDeadline := Expiration{ - AtHeight: &depositBlock, - } - presetIbcFee := PresetIbcFee{ - AckFee: "10000", - TimeoutFee: "10000", + resp, _, err := cosmosNeutron.Exec(ctx, cmd, nil) + require.NoError(t, err) + println("tick response: ", string(resp), "\n") + err = testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") } - atomCoin := Coin{ - Denom: cosmosAtom.Config().Denom, - Amount: strconv.FormatUint(atomContributionAmount, 10), - } + t.Run("instantiate covenant", func(t *testing.T) { + timeouts := Timeouts{ + IcaTimeout: "100", // sec + IbcTransferTimeout: "100", // sec + } - osmoCoin := Coin{ - Denom: cosmosOsmosis.Config().Denom, - Amount: strconv.FormatUint(osmoContributionAmount, 10), - } + depositBlock := Block(500) + lockupBlock := Block(1000) - partyAConfig := CovenantPartyConfig{ - ControllerAddr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), - HostAddr: hubNeutronAccount.Bech32Address(cosmosNeutron.Config().Bech32Prefix), - Contribution: atomCoin, - IbcDenom: neutronAtomIbcDenom, - PartyToHostChainChannelId: testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], - HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], - PartyReceiverAddr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), - PartyChainConnectionId: neutronAtomIBCConnId, - IbcTransferTimeout: timeouts.IbcTransferTimeout, - } - partyBConfig := CovenantPartyConfig{ - ControllerAddr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), - HostAddr: osmoNeutronAccount.Bech32Address(cosmosNeutron.Config().Bech32Prefix), - Contribution: osmoCoin, - IbcDenom: neutronOsmoIbcDenom, - PartyToHostChainChannelId: testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name], - HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], - PartyReceiverAddr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), - PartyChainConnectionId: neutronOsmosisIBCConnId, - IbcTransferTimeout: timeouts.IbcTransferTimeout, - } - codeIds := ContractCodeIds{ - IbcForwarderCode: ibcForwarderCodeId, - InterchainRouterCode: routerCodeId, - ClockCode: clockCodeId, - HolderCode: holderCodeId, - LiquidPoolerCode: lperCodeId, - } + lockupConfig := Expiration{ + AtHeight: &lockupBlock, + } + depositDeadline := Expiration{ + AtHeight: &depositBlock, + } + presetIbcFee := PresetIbcFee{ + AckFee: "10000", + TimeoutFee: "10000", + } - ragequitTerms := RagequitTerms{ - Penalty: "0.1", - } + atomCoin := Coin{ + Denom: cosmosAtom.Config().Denom, + Amount: strconv.FormatUint(atomContributionAmount, 10), + } - ragequitConfig := RagequitConfig{ - Enabled: &ragequitTerms, - } + osmoCoin := Coin{ + Denom: cosmosOsmosis.Config().Denom, + Amount: strconv.FormatUint(osmoContributionAmount, 10), + } - poolAddress := stableswapAddress - pairType := PairType{ - Stable: struct{}{}, - } + partyAConfig := CovenantPartyConfig{ + ControllerAddr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + HostAddr: hubNeutronAccount.Bech32Address(cosmosNeutron.Config().Bech32Prefix), + Contribution: atomCoin, + IbcDenom: neutronAtomIbcDenom, + PartyToHostChainChannelId: testCtx.GaiaTransferChannelIds[cosmosNeutron.Config().Name], + HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosAtom.Config().Name], + PartyReceiverAddr: gaiaUser.Bech32Address(cosmosAtom.Config().Bech32Prefix), + PartyChainConnectionId: neutronAtomIBCConnId, + IbcTransferTimeout: timeouts.IbcTransferTimeout, + } + partyBConfig := CovenantPartyConfig{ + ControllerAddr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + HostAddr: osmoNeutronAccount.Bech32Address(cosmosNeutron.Config().Bech32Prefix), + Contribution: osmoCoin, + IbcDenom: neutronOsmoIbcDenom, + PartyToHostChainChannelId: testCtx.OsmoTransferChannelIds[cosmosNeutron.Config().Name], + HostToPartyChainChannelId: testCtx.NeutronTransferChannelIds[cosmosOsmosis.Config().Name], + PartyReceiverAddr: osmoUser.Bech32Address(cosmosOsmosis.Config().Bech32Prefix), + PartyChainConnectionId: neutronOsmosisIBCConnId, + IbcTransferTimeout: timeouts.IbcTransferTimeout, + } + codeIds := ContractCodeIds{ + IbcForwarderCode: ibcForwarderCodeId, + InterchainRouterCode: routerCodeId, + ClockCode: clockCodeId, + HolderCode: holderCodeId, + LiquidPoolerCode: lperCodeId, + } - covenantMsg := CovenantInstantiateMsg{ - Label: "two-party-pol-covenant", - Timeouts: timeouts, - PresetIbcFee: presetIbcFee, - ContractCodeIds: codeIds, - LockupConfig: lockupConfig, - PartyAConfig: partyAConfig, - PartyBConfig: partyBConfig, - PoolAddress: poolAddress, - RagequitConfig: &ragequitConfig, - DepositDeadline: depositDeadline, - PartyAShare: "50", - PartyBShare: "50", - ExpectedPoolRatio: "0.1", - AcceptablePoolRatioDelta: "0.09", - PairType: pairType, - } - str, err := json.Marshal(covenantMsg) - require.NoError(t, err, "Failed to marshall CovenantInstantiateMsg") - instantiateMsg := string(str) - - println("instantiation message: ", instantiateMsg) - cmd := []string{"neutrond", "tx", "wasm", "instantiate", covenantCodeIdStr, - instantiateMsg, - "--label", "two-party-pol-covenant", - "--no-admin", - "--from", neutronUser.KeyName, - "--output", "json", - "--home", neutron.HomeDir(), - "--node", neutron.GetRPCAddress(), - "--chain-id", neutron.Config().ChainID, - "--gas", "90009000", - "--keyring-backend", keyring.BackendTest, - "-y", - } + ragequitTerms := RagequitTerms{ + Penalty: "0.1", + } - _, _, err = neutron.Exec(ctx, cmd, nil) - require.NoError(t, err) - require.NoError(t, testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis)) + ragequitConfig := RagequitConfig{ + Enabled: &ragequitTerms, + } - queryCmd := []string{"neutrond", "query", "wasm", - "list-contract-by-code", covenantCodeIdStr, - "--output", "json", - "--home", neutron.HomeDir(), - "--node", neutron.GetRPCAddress(), - "--chain-id", neutron.Config().ChainID, - } + poolAddress := stableswapAddress + pairType := PairType{ + Stable: struct{}{}, + } - queryResp, _, err := neutron.Exec(ctx, queryCmd, nil) - require.NoError(t, err, "failed to query") + covenantMsg := CovenantInstantiateMsg{ + Label: "two-party-pol-covenant", + Timeouts: timeouts, + PresetIbcFee: presetIbcFee, + ContractCodeIds: codeIds, + LockupConfig: lockupConfig, + PartyAConfig: partyAConfig, + PartyBConfig: partyBConfig, + PoolAddress: poolAddress, + RagequitConfig: &ragequitConfig, + DepositDeadline: depositDeadline, + PartyAShare: "50", + PartyBShare: "50", + ExpectedPoolRatio: "0.1", + AcceptablePoolRatioDelta: "0.09", + PairType: pairType, + } + str, err := json.Marshal(covenantMsg) + require.NoError(t, err, "Failed to marshall CovenantInstantiateMsg") + instantiateMsg := string(str) + + println("instantiation message: ", instantiateMsg) + cmd := []string{"neutrond", "tx", "wasm", "instantiate", covenantCodeIdStr, + instantiateMsg, + "--label", "two-party-pol-covenant", + "--no-admin", + "--from", neutronUser.KeyName, + "--output", "json", + "--home", neutron.HomeDir(), + "--node", neutron.GetRPCAddress(), + "--chain-id", neutron.Config().ChainID, + "--gas", "90009000", + "--keyring-backend", keyring.BackendTest, + "-y", + } - type QueryContractResponse struct { - Contracts []string `json:"contracts"` - Pagination any `json:"pagination"` - } + _, _, err = neutron.Exec(ctx, cmd, nil) + require.NoError(t, err) + require.NoError(t, testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis)) - contactsRes := QueryContractResponse{} - require.NoError(t, json.Unmarshal(queryResp, &contactsRes), "failed to unmarshal contract response") + queryCmd := []string{"neutrond", "query", "wasm", + "list-contract-by-code", covenantCodeIdStr, + "--output", "json", + "--home", neutron.HomeDir(), + "--node", neutron.GetRPCAddress(), + "--chain-id", neutron.Config().ChainID, + } - covenantAddress = contactsRes.Contracts[len(contactsRes.Contracts)-1] + queryResp, _, err := neutron.Exec(ctx, queryCmd, nil) + require.NoError(t, err, "failed to query") - println("covenant address: ", covenantAddress) - }) + type QueryContractResponse struct { + Contracts []string `json:"contracts"` + Pagination any `json:"pagination"` + } - t.Run("query covenant contracts", func(t *testing.T) { - routerQueryPartyA := InterchainRouterQuery{ - Party: Party{ - Party: "party_a", - }, - } - routerQueryPartyB := InterchainRouterQuery{ - Party: Party{ - Party: "party_b", - }, - } - forwarderQueryPartyA := IbcForwarderQuery{ - Party: Party{ - Party: "party_a", - }, - } - forwarderQueryPartyB := IbcForwarderQuery{ - Party: Party{ - Party: "party_b", - }, - } + contactsRes := QueryContractResponse{} + require.NoError(t, json.Unmarshal(queryResp, &contactsRes), "failed to unmarshal contract response") - var response CovenantAddressQueryResponse - - err = cosmosNeutron.QueryContract(ctx, covenantAddress, ClockAddressQuery{}, &response) - require.NoError(t, err, "failed to query instantiated clock address") - clockAddress = response.Data - println("clock addr: ", clockAddress) - - err = cosmosNeutron.QueryContract(ctx, covenantAddress, HolderAddressQuery{}, &response) - require.NoError(t, err, "failed to query instantiated holder address") - holderAddress = response.Data - println("holder addr: ", holderAddress) - - err = cosmosNeutron.QueryContract(ctx, covenantAddress, LiquidPoolerQuery{}, &response) - require.NoError(t, err, "failed to query instantiated liquid pooler address") - liquidPoolerAddress = response.Data - println("liquid pooler addr: ", liquidPoolerAddress) - - err = cosmosNeutron.QueryContract(ctx, covenantAddress, routerQueryPartyA, &response) - require.NoError(t, err, "failed to query instantiated party a router address") - partyARouterAddress = response.Data - println("partyARouterAddress: ", partyARouterAddress) - - err = cosmosNeutron.QueryContract(ctx, covenantAddress, routerQueryPartyB, &response) - require.NoError(t, err, "failed to query instantiated party b router address") - partyBRouterAddress = response.Data - println("partyBRouterAddress: ", partyBRouterAddress) - - err = cosmosNeutron.QueryContract(ctx, covenantAddress, forwarderQueryPartyA, &response) - require.NoError(t, err, "failed to query instantiated party a forwarder address") - partyAIbcForwarderAddress = response.Data - println("partyAIbcForwarderAddress: ", partyAIbcForwarderAddress) - - err = cosmosNeutron.QueryContract(ctx, covenantAddress, forwarderQueryPartyB, &response) - require.NoError(t, err, "failed to query instantiated party b forwarder address") - partyBIbcForwarderAddress = response.Data - println("partyBIbcForwarderAddress: ", partyBIbcForwarderAddress) - }) + covenantAddress = contactsRes.Contracts[len(contactsRes.Contracts)-1] - t.Run("fund contracts with neutron", func(t *testing.T) { - err := neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ - Address: partyAIbcForwarderAddress, - Amount: 5000000001, - Denom: nativeNtrnDenom, + println("covenant address: ", covenantAddress) }) - require.NoError(t, err, "failed to send funds from neutron user to partyAIbcForwarder contract") - err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ - Address: partyBIbcForwarderAddress, - Amount: 5000000001, - Denom: nativeNtrnDenom, - }) - require.NoError(t, err, "failed to send funds from neutron user to partyBIbcForwarder contract") + t.Run("query covenant contracts", func(t *testing.T) { + routerQueryPartyA := InterchainRouterQuery{ + Party: Party{ + Party: "party_a", + }, + } + routerQueryPartyB := InterchainRouterQuery{ + Party: Party{ + Party: "party_b", + }, + } + forwarderQueryPartyA := IbcForwarderQuery{ + Party: Party{ + Party: "party_a", + }, + } + forwarderQueryPartyB := IbcForwarderQuery{ + Party: Party{ + Party: "party_b", + }, + } - err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ - Address: clockAddress, - Amount: 5000000001, - Denom: nativeNtrnDenom, - }) - require.NoError(t, err, "failed to send funds from neutron user to clock contract") - err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ - Address: partyARouterAddress, - Amount: 5000000001, - Denom: nativeNtrnDenom, - }) - require.NoError(t, err, "failed to send funds from neutron user to party a router") - err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ - Address: partyBRouterAddress, - Amount: 5000000001, - Denom: nativeNtrnDenom, - }) - require.NoError(t, err, "failed to send funds from neutron user to party b router") - err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ - Address: holderAddress, - Amount: 5000000001, - Denom: nativeNtrnDenom, + var response CovenantAddressQueryResponse + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, ClockAddressQuery{}, &response) + require.NoError(t, err, "failed to query instantiated clock address") + clockAddress = response.Data + println("clock addr: ", clockAddress) + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, HolderAddressQuery{}, &response) + require.NoError(t, err, "failed to query instantiated holder address") + holderAddress = response.Data + println("holder addr: ", holderAddress) + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, LiquidPoolerQuery{}, &response) + require.NoError(t, err, "failed to query instantiated liquid pooler address") + liquidPoolerAddress = response.Data + println("liquid pooler addr: ", liquidPoolerAddress) + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, routerQueryPartyA, &response) + require.NoError(t, err, "failed to query instantiated party a router address") + partyARouterAddress = response.Data + println("partyARouterAddress: ", partyARouterAddress) + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, routerQueryPartyB, &response) + require.NoError(t, err, "failed to query instantiated party b router address") + partyBRouterAddress = response.Data + println("partyBRouterAddress: ", partyBRouterAddress) + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, forwarderQueryPartyA, &response) + require.NoError(t, err, "failed to query instantiated party a forwarder address") + partyAIbcForwarderAddress = response.Data + println("partyAIbcForwarderAddress: ", partyAIbcForwarderAddress) + + err = cosmosNeutron.QueryContract(ctx, covenantAddress, forwarderQueryPartyB, &response) + require.NoError(t, err, "failed to query instantiated party b forwarder address") + partyBIbcForwarderAddress = response.Data + println("partyBIbcForwarderAddress: ", partyBIbcForwarderAddress) }) - require.NoError(t, err, "failed to send funds from neutron user to holder") - err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ - Address: liquidPoolerAddress, - Amount: 5000000001, - Denom: nativeNtrnDenom, - }) - require.NoError(t, err, "failed to send funds from neutron user to holder") - err = testutil.WaitForBlocks(ctx, 2, atom, neutron, osmosis) - require.NoError(t, err, "failed to wait for blocks") + t.Run("fund contracts with neutron", func(t *testing.T) { + err := neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: partyAIbcForwarderAddress, + Amount: 5000000001, + Denom: nativeNtrnDenom, + }) + require.NoError(t, err, "failed to send funds from neutron user to partyAIbcForwarder contract") - bal, err := neutron.GetBalance(ctx, partyAIbcForwarderAddress, nativeNtrnDenom) - require.NoError(t, err) - require.Equal(t, int64(5000000001), bal) - bal, err = neutron.GetBalance(ctx, partyBIbcForwarderAddress, nativeNtrnDenom) - require.NoError(t, err) - require.Equal(t, int64(5000000001), bal) - bal, err = neutron.GetBalance(ctx, clockAddress, nativeNtrnDenom) - require.NoError(t, err) - require.Equal(t, int64(5000000001), bal) - bal, err = neutron.GetBalance(ctx, partyARouterAddress, nativeNtrnDenom) - require.NoError(t, err) - require.Equal(t, int64(5000000001), bal) - bal, err = neutron.GetBalance(ctx, partyBRouterAddress, nativeNtrnDenom) - require.NoError(t, err) - require.Equal(t, int64(5000000001), bal) - }) + err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: partyBIbcForwarderAddress, + Amount: 5000000001, + Denom: nativeNtrnDenom, + }) + require.NoError(t, err, "failed to send funds from neutron user to partyBIbcForwarder contract") - t.Run("two party POL", func(t *testing.T) { + err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: clockAddress, + Amount: 5000000001, + Denom: nativeNtrnDenom, + }) + require.NoError(t, err, "failed to send funds from neutron user to clock contract") + err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: partyARouterAddress, + Amount: 5000000001, + Denom: nativeNtrnDenom, + }) + require.NoError(t, err, "failed to send funds from neutron user to party a router") + err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: partyBRouterAddress, + Amount: 5000000001, + Denom: nativeNtrnDenom, + }) + require.NoError(t, err, "failed to send funds from neutron user to party b router") + err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: holderAddress, + Amount: 5000000001, + Denom: nativeNtrnDenom, + }) + require.NoError(t, err, "failed to send funds from neutron user to holder") + err = neutron.SendFunds(ctx, neutronUser.KeyName, ibc.WalletAmount{ + Address: liquidPoolerAddress, + Amount: 5000000001, + Denom: nativeNtrnDenom, + }) + require.NoError(t, err, "failed to send funds from neutron user to holder") - tickClock := func() { - println("\ntick") - cmd := []string{"neutrond", "tx", "wasm", "execute", clockAddress, - `{"tick":{}}`, - "--gas-prices", "0.0untrn", - "--gas-adjustment", `1.5`, - "--output", "json", - "--home", "/var/cosmos-chain/neutron-2", - "--node", neutron.GetRPCAddress(), - "--home", neutron.HomeDir(), - "--chain-id", neutron.Config().ChainID, - "--from", neutronUser.KeyName, - "--gas", "1500000", - "--keyring-backend", keyring.BackendTest, - "-y", - } + err = testutil.WaitForBlocks(ctx, 2, atom, neutron, osmosis) + require.NoError(t, err, "failed to wait for blocks") - resp, _, err := cosmosNeutron.Exec(ctx, cmd, nil) + bal, err := neutron.GetBalance(ctx, partyAIbcForwarderAddress, nativeNtrnDenom) require.NoError(t, err) - println("tick response: ", string(resp), "\n") - err = testutil.WaitForBlocks(ctx, 5, atom, neutron, osmosis) - require.NoError(t, err, "failed to wait for blocks") - } + require.Equal(t, int64(5000000001), bal) + bal, err = neutron.GetBalance(ctx, partyBIbcForwarderAddress, nativeNtrnDenom) + require.NoError(t, err) + require.Equal(t, int64(5000000001), bal) + bal, err = neutron.GetBalance(ctx, clockAddress, nativeNtrnDenom) + require.NoError(t, err) + require.Equal(t, int64(5000000001), bal) + bal, err = neutron.GetBalance(ctx, partyARouterAddress, nativeNtrnDenom) + require.NoError(t, err) + require.Equal(t, int64(5000000001), bal) + bal, err = neutron.GetBalance(ctx, partyBRouterAddress, nativeNtrnDenom) + require.NoError(t, err) + require.Equal(t, int64(5000000001), bal) + }) t.Run("tick until forwarders create ICA", func(t *testing.T) { require.NoError(t, testutil.WaitForBlocks(ctx, 15, atom, neutron, osmosis), "failed to wait for blocks") @@ -1172,12 +1741,12 @@ func TestTwoPartyPol(t *testing.T) { } }) - t.Run("tick until holder expires", func(t *testing.T) { + t.Run("tick a bit", func(t *testing.T) { for { neutronHeight, err := cosmosNeutron.Height(ctx) require.NoError(t, err) - if neutronHeight >= 515 { + if neutronHeight >= 500 { println("neutron height: ", neutronHeight) break } else { @@ -1186,10 +1755,10 @@ func TestTwoPartyPol(t *testing.T) { } }) - t.Run("party A claims and router receives the funds", func(t *testing.T) { + t.Run("party A ragequits", func(t *testing.T) { cmd := []string{"neutrond", "tx", "wasm", "execute", holderAddress, - `{"claim":{}}`, + `{"ragequit":{}}`, "--from", hubNeutronAccount.GetKeyName(), "--gas-prices", "0.0untrn", "--gas-adjustment", `1.5`, @@ -1201,7 +1770,7 @@ func TestTwoPartyPol(t *testing.T) { "--keyring-backend", keyring.BackendTest, "-y", } - println("hub claim msg: ", strings.Join(cmd, " ")) + println("hub ragequit msg: ", strings.Join(cmd, " ")) for { routerAtomBalA, err := cosmosNeutron.GetBalance(ctx, partyARouterAddress, neutronAtomIbcDenom) From 21ef4b229de7404d0e3abf658d5246b84df185cd Mon Sep 17 00:00:00 2001 From: bekauz Date: Wed, 1 Nov 2023 17:35:19 +0100 Subject: [PATCH 140/586] holder readme deposit deadline --- contracts/two-party-pol-holder/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/two-party-pol-holder/README.md b/contracts/two-party-pol-holder/README.md index 78704063..ea5bb7f6 100644 --- a/contracts/two-party-pol-holder/README.md +++ b/contracts/two-party-pol-holder/README.md @@ -33,7 +33,9 @@ Ragequit breaks the regular covenant flow in the following way: Both parties should deposit their funds to holder. After holder asserts the expected balances, it forwards the funds to the Liquid Pooler which then in turn enters into a position. -If party A delivers their part of the covenant deposit agreement but party B fails, party A is refunded. +Deposit stage is subject to a deposit deadline (`Expiration`). +Once the deposit deadline expires, refunds are issued to parties that delivered their parts of the covenant. +This can happen if any of the counterparties do not deliver the funds before the deadline expires, as holder attempts to send all expected funds in a combined `BankSend`. ## Flow From 456e6717258f610c664ad973e4adfb62ddaf7a9d Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 2 Nov 2023 18:26:08 +0100 Subject: [PATCH 141/586] basic & interchaintest workflow init go version env var cd and test in same step v3 checkout installing just --- .github/workflows/basic.yml | 75 ++++++++++++++++++++++++++++ .github/workflows/interchaintest.yml | 33 ++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 .github/workflows/basic.yml create mode 100644 .github/workflows/interchaintest.yml diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml new file mode 100644 index 00000000..8f0f288f --- /dev/null +++ b/.github/workflows/basic.yml @@ -0,0 +1,75 @@ +# Based on https://github.com/actions-rs/example/blob/master/.github/workflows/quickstart.yml +on: [push, pull_request] + +name: Basic + +env: + RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + +jobs: + unit-test: + name: Test Suite + runs-on: self-hosted-ubuntu-22.04 + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Install latest stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: 1.66.0 + target: wasm32-unknown-unknown + override: true + + - name: Run tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --locked + + lints: + name: Lints + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: 1.66.0 + override: true + components: rustfmt, clippy + + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --all-targets -- -D warnings + + schema: + name: Schema + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: 1.66.0 + override: true + components: rustfmt, clippy + + - name: Gen schemas + run: ./scripts/schema.sh \ No newline at end of file diff --git a/.github/workflows/interchaintest.yml b/.github/workflows/interchaintest.yml new file mode 100644 index 00000000..8a3429ec --- /dev/null +++ b/.github/workflows/interchaintest.yml @@ -0,0 +1,33 @@ +name: interchaintest + +on: + pull_request: + push: + + +env: + GO_VERSION: 1.21 +jobs: + events: + runs-on: self-hosted-ubuntu-22.04 + steps: + - name: checkout repository + uses: actions/checkout@v3 + + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + + - name: install just + run: | + sudo apt-get update -qy + sudo apt-get install -y just + + - name: change directory to swap-covenant + run: cd swap-covenant/ + + - name: interchaintest + run: | + cd swap-covenant + just simtest From 36e4a09aadf456d2756ca99dbf4160843725dd03 Mon Sep 17 00:00:00 2001 From: bekauz Date: Thu, 2 Nov 2023 19:01:17 +0100 Subject: [PATCH 142/586] just extraction test root justfile workspace-optimizer --- .github/workflows/basic.yml | 78 +++---- .github/workflows/interchaintest.yml | 82 ++++--- Cargo.lock | 219 +++++++++++------- contracts/ibc-forwarder/src/contract.rs | 2 +- .../src/suite_tests/tests.rs | 2 +- contracts/interchain-splitter/src/contract.rs | 2 +- contracts/native-splitter/src/contract.rs | 6 +- .../two-party-pol-holder/src/contract.rs | 2 +- justfile | 27 +++ 9 files changed, 262 insertions(+), 158 deletions(-) create mode 100644 justfile diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index 8f0f288f..f0062ae5 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -13,7 +13,7 @@ jobs: runs-on: self-hosted-ubuntu-22.04 steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install latest stable toolchain uses: actions-rs/toolchain@v1 @@ -29,47 +29,47 @@ jobs: command: test args: --locked - lints: - name: Lints - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 + # lints: + # name: Lints + # runs-on: ubuntu-latest + # steps: + # - name: Checkout sources + # uses: actions/checkout@v3 - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: 1.66.0 - override: true - components: rustfmt, clippy + # - name: Install stable toolchain + # uses: actions-rs/toolchain@v1 + # with: + # profile: minimal + # toolchain: 1.66.0 + # override: true + # components: rustfmt, clippy - - name: Run cargo fmt - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + # - name: Run cargo fmt + # uses: actions-rs/cargo@v1 + # with: + # command: fmt + # args: --all -- --check - - name: Run cargo clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --all-targets -- -D warnings + # - name: Run cargo clippy + # uses: actions-rs/cargo@v1 + # with: + # command: clippy + # args: --all-targets -- -D warnings - schema: - name: Schema - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v2 + # schema: + # name: Schema + # runs-on: ubuntu-latest + # steps: + # - name: Checkout sources + # uses: actions/checkout@v2 - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: 1.66.0 - override: true - components: rustfmt, clippy + # - name: Install stable toolchain + # uses: actions-rs/toolchain@v1 + # with: + # profile: minimal + # toolchain: 1.66.0 + # override: true + # components: rustfmt, clippy - - name: Gen schemas - run: ./scripts/schema.sh \ No newline at end of file + # - name: Gen schemas + # run: ./scripts/schema.sh \ No newline at end of file diff --git a/.github/workflows/interchaintest.yml b/.github/workflows/interchaintest.yml index 8a3429ec..b765926b 100644 --- a/.github/workflows/interchaintest.yml +++ b/.github/workflows/interchaintest.yml @@ -1,33 +1,61 @@ name: interchaintest on: - pull_request: - push: - + pull_request: + push: +permissions: + contents: write + env: - GO_VERSION: 1.21 + GO_VERSION: 1.21 + jobs: - events: - runs-on: self-hosted-ubuntu-22.04 - steps: - - name: checkout repository - uses: actions/checkout@v3 - - - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@v4 - with: - go-version: ${{ env.GO_VERSION }} - - - name: install just - run: | - sudo apt-get update -qy - sudo apt-get install -y just - - - name: change directory to swap-covenant - run: cd swap-covenant/ - - - name: interchaintest - run: | - cd swap-covenant - just simtest + release: + runs-on: self-hosted-ubuntu-22.04 + container: cosmwasm/workspace-optimizer:0.14.0 + steps: + - name: Install Node.js + run: | + apk add --no-cache nodejs + + - name: checkout sources + uses: actions/checkout@v3 + + # - run: apk add --no-cache tar + + - name: set up cargo cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: compile contracts + timeout-minutes: 30 + run: optimize_workspace.sh . + + - name: Upload contracts + uses: actions/upload-artifact@v3 + with: + name: contracts + path: artifacts/ + +# events: +# runs-on: self-hosted-ubuntu-22.04 +# steps: +# - name: checkout repository +# uses: actions/checkout@v3 + +# - name: Set up Go ${{ env.GO_VERSION }} +# uses: actions/setup-go@v4 +# with: +# go-version: ${{ env.GO_VERSION }} + +# - name: setup just +# uses: extractions/setup-just@v1 + +# - name: interchaintest +# run: just swap-covenant \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9e084823..b292ace6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "ahash" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" dependencies = [ "getrandom", "once_cell", @@ -21,23 +21,23 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "astroport" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcea351626899d205aab091c891fc878fc9b3c930585fd3ef6222de028d8a7a" +checksum = "78b863a982595743e181f89540d7aaeda35c60b6b5cac9c36c9be30cf11a5ece" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", "cw-utils 0.15.1", "cw20 0.15.1", - "itertools", + "itertools 0.10.5", "uint", ] [[package]] name = "astroport" -version = "3.6.0" -source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" +version = "3.6.1" +source = "git+https://github.com/astroport-fi/astroport-core.git#3b7c0c5681f6b287e61cadc8045431e11d2fbae0" dependencies = [ "astroport-circular-buffer", "cosmwasm-schema", @@ -46,14 +46,14 @@ dependencies = [ "cw-utils 1.0.2", "cw20 0.15.1", "cw3", - "itertools", + "itertools 0.10.5", "uint", ] [[package]] name = "astroport-circular-buffer" version = "0.1.0" -source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" +source = "git+https://github.com/astroport-fi/astroport-core.git#3b7c0c5681f6b287e61cadc8045431e11d2fbae0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -64,15 +64,15 @@ dependencies = [ [[package]] name = "astroport-factory" version = "1.6.0" -source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" +source = "git+https://github.com/astroport-fi/astroport-core.git#3b7c0c5681f6b287e61cadc8045431e11d2fbae0" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.1", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", "cw-utils 1.0.2", "cw2 0.15.1", - "itertools", + "itertools 0.10.5", "protobuf 2.28.0", "thiserror", ] @@ -80,9 +80,9 @@ dependencies = [ [[package]] name = "astroport-native-coin-registry" version = "1.0.1" -source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" +source = "git+https://github.com/astroport-fi/astroport-core.git#3b7c0c5681f6b287e61cadc8045431e11d2fbae0" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.1", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", @@ -94,9 +94,9 @@ dependencies = [ [[package]] name = "astroport-pair-stable" version = "3.3.0" -source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" +source = "git+https://github.com/astroport-fi/astroport-core.git#3b7c0c5681f6b287e61cadc8045431e11d2fbae0" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.1", "astroport-circular-buffer", "cosmwasm-schema", "cosmwasm-std", @@ -104,16 +104,16 @@ dependencies = [ "cw-utils 1.0.2", "cw2 0.15.1", "cw20 0.15.1", - "itertools", + "itertools 0.10.5", "thiserror", ] [[package]] name = "astroport-token" version = "1.1.1" -source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" +source = "git+https://github.com/astroport-fi/astroport-core.git#3b7c0c5681f6b287e61cadc8045431e11d2fbae0" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.1", "cosmwasm-schema", "cosmwasm-std", "cw2 0.15.1", @@ -125,9 +125,9 @@ dependencies = [ [[package]] name = "astroport-whitelist" version = "1.0.1" -source = "git+https://github.com/astroport-fi/astroport-core.git#98550a04b98a593762908eb7d668cd9f2503f9c5" +source = "git+https://github.com/astroport-fi/astroport-core.git#3b7c0c5681f6b287e61cadc8045431e11d2fbae0" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.1", "cosmwasm-schema", "cosmwasm-std", "cw1-whitelist 0.15.1", @@ -161,9 +161,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.4" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64ct" @@ -235,28 +235,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20b42021d8488665b1a0d9748f1f81df7235362d194f44481e2e61bf376b77b4" dependencies = [ "prost 0.11.9", - "prost-types", + "prost-types 0.11.9", "tendermint-proto 0.23.9", ] [[package]] name = "cosmos-sdk-proto" -version = "0.16.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4776e787b24d9568dd61d3237eeb4eb321d622fb881b858c7b82806420e87d4" +checksum = "32560304ab4c365791fd307282f76637213d8083c1a98490c35159cd67852237" dependencies = [ - "prost 0.11.9", - "prost-types", - "tendermint-proto 0.27.0", + "prost 0.12.1", + "prost-types 0.12.1", + "tendermint-proto 0.34.0", ] [[package]] name = "cosmwasm-crypto" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6fb22494cf7d23d0c348740e06e5c742070b2991fd41db77bba0bcfbae1a723" +checksum = "d8bb3c77c3b7ce472056968c745eb501c440fbc07be5004eba02782c35bfbbe3" dependencies = [ "digest 0.10.7", + "ecdsa 0.16.8", "ed25519-zebra", "k256 0.13.1", "rand_core 0.6.4", @@ -265,18 +266,18 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e199424486ea97d6b211db6387fd72e26b4a439d40cc23140b2d8305728055b" +checksum = "fea73e9162e6efde00018d55ed0061e93a108b5d6ec4548b4f8ce3c706249687" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef683a9c1c4eabd6d31515719d0d2cc66952c4c87f7eb192bfc90384517dc34" +checksum = "0df41ea55f2946b6b43579659eec048cc2f66e8c8e2e3652fc5e5e476f673856" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -287,9 +288,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9567025acbb4c0c008178393eb53b3ac3c2e492c25949d3bf415b9cbe80772d8" +checksum = "43609e92ce1b9368aa951b334dd354a2d0dd4d484931a5f83ae10e12a26c8ba9" dependencies = [ "proc-macro2", "quote", @@ -298,11 +299,12 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d89d680fb60439b7c5947b15f9c84b961b88d1f8a3b20c4bd178a3f87db8bae" +checksum = "04d6864742e3a7662d024b51a94ea81c9af21db6faea2f9a6d2232bb97c6e53e" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", + "bech32", "bnum", "cosmwasm-crypto", "cosmwasm-derive", @@ -313,14 +315,15 @@ dependencies = [ "serde", "serde-json-wasm 0.5.1", "sha2 0.10.8", + "static_assertions", "thiserror", ] [[package]] name = "cosmwasm-storage" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a1c574d30feffe4b8121e61e839c231a5ce21901221d2fb4d5c945968a4f00" +checksum = "bd2b4ae72a03e8f56c85df59d172d51d2d7dc9cec6e2bc811e3fb60c588032a4" dependencies = [ "cosmwasm-std", "serde", @@ -330,7 +333,7 @@ dependencies = [ name = "covenant-astroport-liquid-pooler" version = "1.0.0" dependencies = [ - "astroport 2.8.0", + "astroport 2.9.0", "astroport-factory", "astroport-native-coin-registry", "astroport-pair-stable", @@ -352,7 +355,7 @@ dependencies = [ "cw20 0.15.1", "neutron-sdk", "prost 0.11.9", - "prost-types", + "prost-types 0.11.9", "protobuf 3.3.0", "schemars", "serde", @@ -410,7 +413,7 @@ dependencies = [ "cw2 1.1.1", "neutron-sdk", "prost 0.11.9", - "prost-types", + "prost-types 0.11.9", "protobuf 3.3.0", "schemars", "serde", @@ -424,7 +427,7 @@ name = "covenant-holder" version = "1.0.0" dependencies = [ "anyhow", - "astroport 2.8.0", + "astroport 2.9.0", "astroport-factory", "astroport-native-coin-registry", "astroport-pair-stable", @@ -460,7 +463,7 @@ dependencies = [ "cw2 1.1.1", "neutron-sdk", "prost 0.11.9", - "prost-types", + "prost-types 0.11.9", "protobuf 3.3.0", "schemars", "serde", @@ -489,7 +492,7 @@ dependencies = [ "cw2 1.1.1", "neutron-sdk", "prost 0.11.9", - "prost-types", + "prost-types 0.11.9", "protobuf 3.3.0", "schemars", "serde", @@ -524,7 +527,7 @@ dependencies = [ name = "covenant-lp" version = "1.0.0" dependencies = [ - "astroport 2.8.0", + "astroport 2.9.0", "astroport-factory", "astroport-native-coin-registry", "astroport-pair-stable", @@ -546,7 +549,7 @@ dependencies = [ "cw20 0.15.1", "neutron-sdk", "prost 0.11.9", - "prost-types", + "prost-types 0.11.9", "protobuf 3.3.0", "schemars", "serde", @@ -611,7 +614,7 @@ name = "covenant-swap" version = "1.0.0" dependencies = [ "anyhow", - "astroport 2.8.0", + "astroport 2.9.0", "base64 0.13.1", "bech32", "cosmos-sdk-proto 0.14.0", @@ -632,7 +635,7 @@ dependencies = [ "cw2 1.1.1", "neutron-sdk", "prost 0.11.9", - "prost-types", + "prost-types 0.11.9", "protobuf 3.3.0", "schemars", "serde", @@ -665,7 +668,7 @@ name = "covenant-two-party-pol" version = "1.0.0" dependencies = [ "anyhow", - "astroport 2.8.0", + "astroport 2.9.0", "base64 0.13.1", "bech32", "cosmos-sdk-proto 0.14.0", @@ -683,7 +686,7 @@ dependencies = [ "cw2 1.1.1", "neutron-sdk", "prost 0.11.9", - "prost-types", + "prost-types 0.11.9", "protobuf 3.3.0", "schemars", "serde", @@ -697,7 +700,7 @@ name = "covenant-two-party-pol-holder" version = "1.0.0" dependencies = [ "anyhow", - "astroport 2.8.0", + "astroport 2.9.0", "cosmos-sdk-proto 0.14.0", "cosmwasm-schema", "cosmwasm-std", @@ -730,9 +733,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] @@ -811,7 +814,7 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw-utils 1.0.2", "derivative", - "itertools", + "itertools 0.10.5", "k256 0.11.6", "prost 0.9.0", "schemars", @@ -1277,6 +1280,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1317,21 +1329,17 @@ checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "neutron-sdk" -version = "0.6.1" -source = "git+https://github.com/neutron-org/neutron-sdk#74fea05e407e5ff7cdfc195c3a76d2cce6a47d20" +version = "0.7.0" +source = "git+https://github.com/neutron-org/neutron-sdk#1d65cda874c13669c81acd92f2978dd30cdaaea8" dependencies = [ - "base64 0.21.4", "bech32", - "cosmos-sdk-proto 0.16.0", + "cosmos-sdk-proto 0.20.0", "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 1.1.0", - "prost 0.11.9", "protobuf 3.3.0", "schemars", "serde", - "serde-json-wasm 0.5.1", - "serde_json", + "serde-json-wasm 1.0.0", "thiserror", ] @@ -1431,6 +1439,16 @@ dependencies = [ "prost-derive 0.11.9", ] +[[package]] +name = "prost" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fdd22f3b9c31b53c060df4a0613a1c7f062d4115a2b984dd15b1858f7e340d" +dependencies = [ + "bytes", + "prost-derive 0.12.1", +] + [[package]] name = "prost-derive" version = "0.9.0" @@ -1438,7 +1456,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.109", @@ -1451,12 +1469,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "prost-derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32" +dependencies = [ + "anyhow", + "itertools 0.11.0", + "proc-macro2", + "quote", + "syn 2.0.38", +] + [[package]] name = "prost-types" version = "0.11.9" @@ -1466,6 +1497,15 @@ dependencies = [ "prost 0.11.9", ] +[[package]] +name = "prost-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e081b29f63d83a4bc75cfc9f3fe424f9156cf92d8a4f0c9407cce9a1b67327cf" +dependencies = [ + "prost 0.12.1", +] + [[package]] name = "protobuf" version = "2.28.0" @@ -1601,15 +1641,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.188" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" dependencies = [ "serde_derive", ] @@ -1632,6 +1672,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde-json-wasm" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c37d03f3b0f6b5f77c11af1e7c772de1c9af83e50bef7bb6069601900ba67b" +dependencies = [ + "serde", +] + [[package]] name = "serde_bytes" version = "0.11.12" @@ -1643,9 +1692,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", @@ -1665,9 +1714,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -1813,7 +1862,7 @@ dependencies = [ "num-derive", "num-traits", "prost 0.11.9", - "prost-types", + "prost-types 0.11.9", "serde", "serde_bytes", "subtle-encoding", @@ -1822,16 +1871,16 @@ dependencies = [ [[package]] name = "tendermint-proto" -version = "0.27.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5895470f28c530f8ae8c4071bf8190304ce00bd131d25e81730453124a3375c" +checksum = "2cc728a4f9e891d71adf66af6ecaece146f9c7a11312288a3107b3e1d6979aaf" dependencies = [ "bytes", "flex-error", "num-derive", "num-traits", - "prost 0.11.9", - "prost-types", + "prost 0.12.1", + "prost-types 0.12.1", "serde", "serde_bytes", "subtle-encoding", @@ -1840,18 +1889,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.49" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.49" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", diff --git a/contracts/ibc-forwarder/src/contract.rs b/contracts/ibc-forwarder/src/contract.rs index d92b9015..bd3c0da8 100644 --- a/contracts/ibc-forwarder/src/contract.rs +++ b/contracts/ibc-forwarder/src/contract.rs @@ -435,7 +435,7 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> StdResult Ok(resp) }, - MigrateMsg::UpdateCodeId { data } => { + MigrateMsg::UpdateCodeId { data: _ } => { unimplemented!() }, } diff --git a/contracts/interchain-router/src/suite_tests/tests.rs b/contracts/interchain-router/src/suite_tests/tests.rs index 60bde759..f846ab12 100644 --- a/contracts/interchain-router/src/suite_tests/tests.rs +++ b/contracts/interchain-router/src/suite_tests/tests.rs @@ -84,7 +84,7 @@ fn test_tick() { deps.querier = querier; let info = mock_info(CLOCK_ADDR, &[]); - let init_msg = SuiteBuilder::default().instantiate; + let _init_msg = SuiteBuilder::default().instantiate; instantiate( deps.as_mut(), diff --git a/contracts/interchain-splitter/src/contract.rs b/contracts/interchain-splitter/src/contract.rs index 9438c709..c1f5519c 100644 --- a/contracts/interchain-splitter/src/contract.rs +++ b/contracts/interchain-splitter/src/contract.rs @@ -1,4 +1,4 @@ -use cosmwasm_schema::cw_serde; + #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ diff --git a/contracts/native-splitter/src/contract.rs b/contracts/native-splitter/src/contract.rs index e6ef095d..c00ba781 100644 --- a/contracts/native-splitter/src/contract.rs +++ b/contracts/native-splitter/src/contract.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -use cosmos_sdk_proto::cosmos::bank::v1beta1::{MsgMultiSend, Input, Output, MsgMultiSendResponse}; +use cosmos_sdk_proto::cosmos::bank::v1beta1::{MsgMultiSend, Input, Output}; use cosmos_sdk_proto::cosmos::base::v1beta1::Coin; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -13,7 +13,7 @@ use covenant_utils::neutron_ica::{self, OpenAckVersion, RemoteChainInfo, SudoPay use cw2::set_contract_version; use neutron_sdk::bindings::msg::MsgSubmitTxResponse; use neutron_sdk::interchain_txs::helpers::{ - decode_acknowledgement_response, decode_message_response, get_port_id, + decode_acknowledgement_response, get_port_id, decode_message_response, }; use neutron_sdk::sudo::msg::{RequestPacket, SudoMsg}; use neutron_sdk::NeutronError; @@ -363,7 +363,7 @@ fn sudo_response(deps: DepsMut, request: RequestPacket, data: Binary) -> StdResu item_types.push(item_type.to_string()); match item_type { "/cosmos.bank.v1beta1.MsgMultiSend" => { - let _out: MsgMultiSendResponse = decode_message_response(&item.data)?; + decode_message_response(&item.data)?; // TODO: look into if this successful decoding is enough to assume multi // send was successful CONTRACT_STATE.save(deps.storage, &ContractState::Completed)?; diff --git a/contracts/two-party-pol-holder/src/contract.rs b/contracts/two-party-pol-holder/src/contract.rs index a5947c92..b0d1afa1 100644 --- a/contracts/two-party-pol-holder/src/contract.rs +++ b/contracts/two-party-pol-holder/src/contract.rs @@ -478,6 +478,6 @@ pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> StdResult Ok(resp) }, - MigrateMsg::UpdateCodeId { data } => todo!(), + MigrateMsg::UpdateCodeId { data: _ } => todo!(), } } \ No newline at end of file diff --git a/justfile b/justfile new file mode 100644 index 00000000..2b315aba --- /dev/null +++ b/justfile @@ -0,0 +1,27 @@ +build: + cargo build + +test: + cargo test + +lint: + cargo +nightly clippy --all-targets -- -D warnings + +workspace-optimize: + #!/bin/bash + docker run --rm -v "$(pwd)":/code \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + --platform linux/amd64 \ + cosmwasm/workspace-optimizer:0.12.13 + + +swap-covenant: + cd swap-covenant/ + + mkdir -p tests/interchaintest/wasms + + cp -R ./../artifacts/*.wasm tests/interchaintest/wasms + + go clean -testcache + cd tests/interchaintest/ && go test -timeout 30m -v ./... From 126160c76b399c73cb16dd33a3ca98b297907f8a Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 3 Nov 2023 16:12:25 +0100 Subject: [PATCH 143/586] cleanup root cargo test ubuntu-latest cleanup unused deps removing cargo comments --- .github/workflows/interchaintest.yml | 34 +++-- Cargo.lock | 138 ------------------- Cargo.toml | 36 ++++- contracts/astroport-liquid-pooler/Cargo.toml | 22 ++- contracts/clock/Cargo.toml | 2 +- contracts/ibc-forwarder/Cargo.toml | 5 +- contracts/interchain-router/Cargo.toml | 4 - contracts/interchain-splitter/Cargo.toml | 5 - contracts/lper/Cargo.toml | 1 - contracts/swap-covenant/Cargo.toml | 6 +- contracts/swap-holder/Cargo.toml | 2 - contracts/two-party-pol-covenant/Cargo.toml | 2 +- contracts/two-party-pol-holder/Cargo.toml | 6 - packages/covenant-macros/Cargo.toml | 8 +- packages/cw-fifo/Cargo.toml | 1 - 15 files changed, 67 insertions(+), 205 deletions(-) diff --git a/.github/workflows/interchaintest.yml b/.github/workflows/interchaintest.yml index b765926b..8394c61a 100644 --- a/.github/workflows/interchaintest.yml +++ b/.github/workflows/interchaintest.yml @@ -1,47 +1,55 @@ name: interchaintest +permissions: + contents: write + on: pull_request: push: -permissions: - contents: write - env: GO_VERSION: 1.21 jobs: - release: - runs-on: self-hosted-ubuntu-22.04 - container: cosmwasm/workspace-optimizer:0.14.0 + compile-contracts: + name: compile wasm contract with workspace-optimizer + runs-on: ubuntu-latest + container: + image: cosmwasm/workspace-optimizer:0.14.0 steps: - - name: Install Node.js + - name: install node run: | apk add --no-cache nodejs - name: checkout sources uses: actions/checkout@v3 - # - run: apk add --no-cache tar + - name: install tar for cache + run: apk add --no-cache tar - name: set up cargo cache uses: actions/cache@v3 with: path: | - ~/.cargo/registry - ~/.cargo/git - target + ~/.cargo/registry/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: compile contracts - timeout-minutes: 30 + timeout-minutes: 40 run: optimize_workspace.sh . - - name: Upload contracts + - name: upload contracts uses: actions/upload-artifact@v3 with: name: contracts path: artifacts/ + + - name: test ls + run: ls artifacts/ # events: # runs-on: self-hosted-ubuntu-22.04 diff --git a/Cargo.lock b/Cargo.lock index b292ace6..85db06f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,9 +339,7 @@ dependencies = [ "astroport-pair-stable", "astroport-token", "astroport-whitelist", - "base64 0.13.1", "bech32", - "cosmos-sdk-proto 0.14.0", "cosmwasm-schema", "cosmwasm-std", "covenant-clock", @@ -349,17 +347,13 @@ dependencies = [ "covenant-two-party-pol-holder", "cw-multi-test", "cw-storage-plus 1.1.0", - "cw-utils 1.0.2", "cw1-whitelist 1.1.1", "cw2 1.1.1", "cw20 0.15.1", "neutron-sdk", - "prost 0.11.9", - "prost-types 0.11.9", "protobuf 3.3.0", "schemars", "serde", - "serde-json-wasm 0.4.1", "sha2 0.10.8", "thiserror", ] @@ -393,73 +387,20 @@ dependencies = [ "thiserror", ] -[[package]] -name = "covenant-depositor" -version = "1.0.0" -dependencies = [ - "anyhow", - "base64 0.13.1", - "bech32", - "cosmos-sdk-proto 0.14.0", - "cosmwasm-schema", - "cosmwasm-std", - "covenant-clock", - "covenant-ls", - "covenant-macros", - "covenant-utils", - "cw-multi-test", - "cw-storage-plus 1.1.0", - "cw-utils 1.0.2", - "cw2 1.1.1", - "neutron-sdk", - "prost 0.11.9", - "prost-types 0.11.9", - "protobuf 3.3.0", - "schemars", - "serde", - "serde-json-wasm 0.4.1", - "sha2 0.10.8", - "thiserror", -] - -[[package]] -name = "covenant-holder" -version = "1.0.0" -dependencies = [ - "anyhow", - "astroport 2.9.0", - "astroport-factory", - "astroport-native-coin-registry", - "astroport-pair-stable", - "astroport-token", - "astroport-whitelist", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test", - "cw-storage-plus 1.1.0", - "cw2 1.1.1", - "cw20 0.15.1", - "serde", - "thiserror", -] - [[package]] name = "covenant-ibc-forwarder" version = "1.0.0" dependencies = [ "anyhow", - "base64 0.13.1", "bech32", "cosmos-sdk-proto 0.14.0", "cosmwasm-schema", "cosmwasm-std", "covenant-clock", - "covenant-ls", "covenant-macros", "covenant-utils", "cw-multi-test", "cw-storage-plus 1.1.0", - "cw-utils 1.0.2", "cw2 1.1.1", "neutron-sdk", "prost 0.11.9", @@ -477,18 +418,15 @@ name = "covenant-interchain-router" version = "1.0.0" dependencies = [ "anyhow", - "base64 0.13.1", "bech32", "cosmos-sdk-proto 0.14.0", "cosmwasm-schema", "cosmwasm-std", "covenant-clock", - "covenant-ls", "covenant-macros", "covenant-utils", "cw-multi-test", "cw-storage-plus 1.1.0", - "cw-utils 1.0.2", "cw2 1.1.1", "neutron-sdk", "prost 0.11.9", @@ -496,7 +434,6 @@ dependencies = [ "protobuf 3.3.0", "schemars", "serde", - "serde-json-wasm 0.4.1", "sha2 0.10.8", "thiserror", ] @@ -506,72 +443,12 @@ name = "covenant-interchain-splitter" version = "1.0.0" dependencies = [ "anyhow", - "cosmos-sdk-proto 0.14.0", "cosmwasm-schema", "cosmwasm-std", - "covenant-clock", "covenant-macros", - "covenant-utils", "cw-multi-test", "cw-storage-plus 1.1.0", "cw2 1.1.1", - "neutron-sdk", - "protobuf 3.3.0", - "schemars", - "serde", - "serde-json-wasm 0.4.1", - "thiserror", -] - -[[package]] -name = "covenant-lp" -version = "1.0.0" -dependencies = [ - "astroport 2.9.0", - "astroport-factory", - "astroport-native-coin-registry", - "astroport-pair-stable", - "astroport-token", - "astroport-whitelist", - "base64 0.13.1", - "bech32", - "cosmos-sdk-proto 0.14.0", - "cosmwasm-schema", - "cosmwasm-std", - "covenant-clock", - "covenant-holder", - "covenant-macros", - "cw-multi-test", - "cw-storage-plus 1.1.0", - "cw-utils 1.0.2", - "cw1-whitelist 1.1.1", - "cw2 1.1.1", - "cw20 0.15.1", - "neutron-sdk", - "prost 0.11.9", - "prost-types 0.11.9", - "protobuf 3.3.0", - "schemars", - "serde", - "serde-json-wasm 0.4.1", - "sha2 0.10.8", - "thiserror", -] - -[[package]] -name = "covenant-ls" -version = "1.0.0" -dependencies = [ - "cosmos-sdk-proto 0.14.0", - "cosmwasm-schema", - "cosmwasm-std", - "covenant-clock", - "covenant-macros", - "covenant-utils", - "cw-storage-plus 1.1.0", - "cw2 1.1.1", - "neutron-sdk", - "protobuf 3.3.0", "schemars", "serde", "serde-json-wasm 0.4.1", @@ -582,8 +459,6 @@ dependencies = [ name = "covenant-macros" version = "0.0.1" dependencies = [ - "cosmwasm-schema", - "covenant-utils", "proc-macro2", "quote", "syn 1.0.109", @@ -615,18 +490,14 @@ version = "1.0.0" dependencies = [ "anyhow", "astroport 2.9.0", - "base64 0.13.1", "bech32", "cosmos-sdk-proto 0.14.0", "cosmwasm-schema", "cosmwasm-std", "covenant-clock", - "covenant-holder", "covenant-ibc-forwarder", "covenant-interchain-router", "covenant-interchain-splitter", - "covenant-lp", - "covenant-ls", "covenant-swap-holder", "covenant-utils", "cw-multi-test", @@ -652,13 +523,11 @@ dependencies = [ "cosmos-sdk-proto 0.14.0", "cosmwasm-schema", "cosmwasm-std", - "covenant-clock", "covenant-macros", "covenant-utils", "cw-multi-test", "cw-storage-plus 1.1.0", "cw2 1.1.1", - "cw20 0.15.1", "serde", "thiserror", ] @@ -701,22 +570,16 @@ version = "1.0.0" dependencies = [ "anyhow", "astroport 2.9.0", - "cosmos-sdk-proto 0.14.0", "cosmwasm-schema", "cosmwasm-std", - "covenant-clock", "covenant-macros", - "covenant-utils", "cw-multi-test", "cw-storage-plus 1.1.0", "cw-utils 1.0.2", "cw2 1.1.1", "cw20 0.15.1", - "neutron-sdk", - "protobuf 3.3.0", "schemars", "serde", - "serde-json-wasm 0.4.1", "thiserror", ] @@ -797,7 +660,6 @@ dependencies = [ name = "cw-fifo" version = "0.0.1" dependencies = [ - "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", "serde", diff --git a/Cargo.toml b/Cargo.toml index ab812c35..70b958bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,25 @@ [workspace] members = [ "packages/*", - "contracts/*", + "contracts/astroport-liquid-pooler", + "contracts/clock", + "contracts/clock-tester", + "contracts/ibc-forwarder", + "contracts/interchain-router", + "contracts/interchain-splitter", + "contracts/native-splitter", + "contracts/swap-covenant", + "contracts/swap-holder", + "contracts/two-party-pol-holder", + "contracts/two-party-pol-covenant", +] +exclude = [ + "contracts/covenant", + "contracts/depositor", + "contracts/ls", + "contracts/holder", + "contracts/lper", ] -exclude = ["contracts/covenant"] [workspace.package] edition = "2021" @@ -26,11 +42,11 @@ rpath = false [workspace.dependencies] # covenant-depositor = { path = "contracts/depositor" } -covenant-lp = { path = "contracts/lper" } +# covenant-lp = { path = "contracts/lper" } covenant-clock = { path = "contracts/clock" } covenant-clock-tester = { path = "contracts/clock-tester" } -covenant-ls = { path = "contracts/ls" } -covenant-holder = { path = "contracts/holder" } +# covenant-ls = { path = "contracts/ls" } +# covenant-holder = { path = "contracts/holder" } covenant-ibc-forwarder = { path = "contracts/ibc-forwarder" } covenant-native-splitter = { path = "contracts/native-splitter" } covenant-interchain-splitter = { path = "contracts/interchain-splitter" } @@ -69,8 +85,16 @@ serde = { version = "1.0.145", default-features = false, features = ["derive thiserror = "1.0.31" schemars = "0.8.10" cw20 = { version = "0.15" } +proc-macro2 = "1" +quote = "1" +syn = "1" # dev-dependencies cw-multi-test = "0.16.2" anyhow = { version = "1.0.51" } - +astroport-token = {git = "https://github.com/astroport-fi/astroport-core.git"} +astroport-whitelist = {git = "https://github.com/astroport-fi/astroport-core.git"} +astroport-factory = {git = "https://github.com/astroport-fi/astroport-core.git"} +astroport-native-coin-registry = {git = "https://github.com/astroport-fi/astroport-core.git"} +astroport-pair-stable = {git = "https://github.com/astroport-fi/astroport-core.git"} +cw1-whitelist = "1.1.0" \ No newline at end of file diff --git a/contracts/astroport-liquid-pooler/Cargo.toml b/contracts/astroport-liquid-pooler/Cargo.toml index b26796a0..092febe6 100644 --- a/contracts/astroport-liquid-pooler/Cargo.toml +++ b/contracts/astroport-liquid-pooler/Cargo.toml @@ -29,7 +29,6 @@ covenant-clock = { workspace = true, features=["library"] } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw-storage-plus = { workspace = true } -cw-utils = { workspace = true } cw2 = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } @@ -39,24 +38,19 @@ thiserror = { workspace = true } # ```cargo tree --package cosmwasm-std``` sha2 = { workspace = true } neutron-sdk = { workspace = true } -cosmos-sdk-proto = { workspace = true } protobuf = { workspace = true } schemars = { workspace = true } -serde-json-wasm = { workspace = true } -base64 = { workspace = true } -prost = { workspace = true } -prost-types = { workspace = true } bech32 = { workspace = true } -astroport = "2.8.0" -cw20 = { version = "0.15" } +astroport = { workspace = true } +cw20 = { workspace = true } # dev-dependencies [dev-dependencies] cw-multi-test = { workspace = true } -astroport-token = {git = "https://github.com/astroport-fi/astroport-core.git"} -astroport-whitelist = {git = "https://github.com/astroport-fi/astroport-core.git"} -astroport-factory = {git = "https://github.com/astroport-fi/astroport-core.git"} -astroport-native-coin-registry = {git = "https://github.com/astroport-fi/astroport-core.git"} -astroport-pair-stable = {git = "https://github.com/astroport-fi/astroport-core.git"} -cw1-whitelist = "1.1.0" +astroport-token = { workspace = true } +astroport-whitelist = { workspace = true } +astroport-factory = { workspace = true } +astroport-native-coin-registry = { workspace = true } +astroport-pair-stable = { workspace = true } +cw1-whitelist = { workspace = true } covenant-two-party-pol-holder = { workspace = true } diff --git a/contracts/clock/Cargo.toml b/contracts/clock/Cargo.toml index 30d71fe6..0508549a 100644 --- a/contracts/clock/Cargo.toml +++ b/contracts/clock/Cargo.toml @@ -20,7 +20,6 @@ library = [] cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } covenant-macros = { workspace = true } -covenant-clock-tester = { workspace = true, features=["library"] } cw-fifo = { workspace = true } cw-storage-plus = { workspace = true } cw2 = { workspace = true } @@ -31,3 +30,4 @@ neutron-sdk = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } anyhow = { workspace = true } +covenant-clock-tester = { workspace = true, features=["library"] } diff --git a/contracts/ibc-forwarder/Cargo.toml b/contracts/ibc-forwarder/Cargo.toml index 480411cd..487874ce 100644 --- a/contracts/ibc-forwarder/Cargo.toml +++ b/contracts/ibc-forwarder/Cargo.toml @@ -23,12 +23,11 @@ library = [] [dependencies] covenant-macros = { workspace = true} -covenant-ls = { workspace = true, features=["library"] } covenant-clock = { workspace = true, features=["library"]} cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw-storage-plus = { workspace = true } -cw-utils = { workspace = true } +# cw-utils = { workspace = true } cw2 = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } @@ -38,7 +37,7 @@ cosmos-sdk-proto = { workspace = true } protobuf = { workspace = true } schemars = { workspace = true } serde-json-wasm = { workspace = true } -base64 = { workspace = true } +# base64 = { workspace = true } prost = { workspace = true } prost-types = { workspace = true } bech32 = { workspace = true } diff --git a/contracts/interchain-router/Cargo.toml b/contracts/interchain-router/Cargo.toml index 5d500f15..868a5ac7 100644 --- a/contracts/interchain-router/Cargo.toml +++ b/contracts/interchain-router/Cargo.toml @@ -23,12 +23,10 @@ library = [] [dependencies] covenant-macros = { workspace = true} -covenant-ls = { workspace = true, features=["library"] } covenant-clock = { workspace = true, features=["library"]} cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw-storage-plus = { workspace = true } -cw-utils = { workspace = true } cw2 = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } @@ -37,8 +35,6 @@ neutron-sdk = { workspace = true } cosmos-sdk-proto = { workspace = true } protobuf = { workspace = true } schemars = { workspace = true } -serde-json-wasm = { workspace = true } -base64 = { workspace = true } prost = { workspace = true } prost-types = { workspace = true } bech32 = { workspace = true } diff --git a/contracts/interchain-splitter/Cargo.toml b/contracts/interchain-splitter/Cargo.toml index 079cb116..cf1da40f 100644 --- a/contracts/interchain-splitter/Cargo.toml +++ b/contracts/interchain-splitter/Cargo.toml @@ -23,8 +23,6 @@ library = [] [dependencies] covenant-macros = { workspace = true } -covenant-clock = { workspace = true, features=["library"] } -covenant-utils = { workspace = true } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw-storage-plus = { workspace = true } @@ -33,9 +31,6 @@ thiserror = { workspace = true } schemars = { workspace = true } serde-json-wasm = { workspace = true } serde = { workspace = true } -neutron-sdk = { workspace = true } -cosmos-sdk-proto = { workspace = true } -protobuf = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/lper/Cargo.toml b/contracts/lper/Cargo.toml index 42dbf1b9..f4041622 100644 --- a/contracts/lper/Cargo.toml +++ b/contracts/lper/Cargo.toml @@ -59,4 +59,3 @@ astroport-factory = {git = "https://github.com/astroport-fi/astroport-core.git" astroport-native-coin-registry = {git = "https://github.com/astroport-fi/astroport-core.git"} astroport-pair-stable = {git = "https://github.com/astroport-fi/astroport-core.git"} cw1-whitelist = "1.1.0" -covenant-holder = { workspace = true } \ No newline at end of file diff --git a/contracts/swap-covenant/Cargo.toml b/contracts/swap-covenant/Cargo.toml index 9fd4b323..7c88fd8c 100644 --- a/contracts/swap-covenant/Cargo.toml +++ b/contracts/swap-covenant/Cargo.toml @@ -37,14 +37,10 @@ cosmos-sdk-proto = { workspace = true } protobuf = { workspace = true } schemars = { workspace = true } serde-json-wasm = { workspace = true } -base64 = { workspace = true } prost = { workspace = true } prost-types = { workspace = true } bech32 ={ workspace = true } -covenant-ls = { workspace = true, features=["library"] } -covenant-lp = { workspace = true, features=["library"] } covenant-clock = { workspace = true, features=["library"]} -covenant-holder = { workspace = true, features=["library"] } covenant-swap-holder = { workspace = true, features = ["library"] } covenant-utils = { workspace = true } covenant-interchain-splitter = { workspace = true, features = ["library"] } @@ -55,4 +51,4 @@ covenant-interchain-router = { workspace = true, features = ["library"] } cw-multi-test = { workspace = true } anyhow = { workspace = true } astroport = { workspace = true } -prost = "0.11.9" +prost = { workspace = true } diff --git a/contracts/swap-holder/Cargo.toml b/contracts/swap-holder/Cargo.toml index c5404d12..dbcea520 100644 --- a/contracts/swap-holder/Cargo.toml +++ b/contracts/swap-holder/Cargo.toml @@ -23,9 +23,7 @@ cw-storage-plus = { workspace = true } cw2 = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } -cw20 = { version = "0.15" } covenant-macros = { workspace = true } -covenant-clock = { workspace = true, features=["library"] } covenant-utils = { workspace = true } cosmos-sdk-proto = { workspace = true } diff --git a/contracts/two-party-pol-covenant/Cargo.toml b/contracts/two-party-pol-covenant/Cargo.toml index ee618906..61cbc0bf 100644 --- a/contracts/two-party-pol-covenant/Cargo.toml +++ b/contracts/two-party-pol-covenant/Cargo.toml @@ -53,4 +53,4 @@ astroport = { workspace = true } cw-multi-test = { workspace = true } anyhow = { workspace = true } astroport = { workspace = true } -prost = "0.11.9" +prost = { workspace = true } diff --git a/contracts/two-party-pol-holder/Cargo.toml b/contracts/two-party-pol-holder/Cargo.toml index 549a6f0c..32ddf267 100644 --- a/contracts/two-party-pol-holder/Cargo.toml +++ b/contracts/two-party-pol-holder/Cargo.toml @@ -18,19 +18,13 @@ library = [] [dependencies] covenant-macros = { workspace = true } -covenant-clock = { workspace = true, features=["library"] } -covenant-utils = { workspace = true } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw-storage-plus = { workspace = true } cw2 = { workspace = true } thiserror = { workspace = true } schemars = { workspace = true } -serde-json-wasm = { workspace = true } serde = { workspace = true } -neutron-sdk = { workspace = true } -cosmos-sdk-proto = { workspace = true } -protobuf = { workspace = true } astroport = { workspace = true } cw20 = { workspace = true } cw-utils = { workspace = true } diff --git a/packages/covenant-macros/Cargo.toml b/packages/covenant-macros/Cargo.toml index f464ca5c..fc6dc2b7 100644 --- a/packages/covenant-macros/Cargo.toml +++ b/packages/covenant-macros/Cargo.toml @@ -11,8 +11,6 @@ license = "BSD-3" proc-macro = true [dependencies] -proc-macro2 = "1" -quote = "1" -syn = "1" -covenant-utils = { workspace = true } -cosmwasm-schema = { workspace = true } +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } diff --git a/packages/cw-fifo/Cargo.toml b/packages/cw-fifo/Cargo.toml index e0d14126..e2f5339e 100644 --- a/packages/cw-fifo/Cargo.toml +++ b/packages/cw-fifo/Cargo.toml @@ -7,7 +7,6 @@ description = "A CosmWasm FIFO queue with nice runtime and no size limits." license = { workspace = true } [dependencies] -cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw-storage-plus = { workspace = true } serde = { workspace = true } From 3999c8a4bc56cfd158c0db405e9f65909975bdec Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 3 Nov 2023 16:53:27 +0100 Subject: [PATCH 144/586] test swap covenant contracts dependency on tests try to setup docker on self hosted runner v2 buildx docker debugging ci bump astroport ver --- .github/workflows/basic.yml | 52 ++++++++++++++-------------- .github/workflows/interchaintest.yml | 42 ++++++++++++++-------- Cargo.toml | 2 +- contracts/ibc-forwarder/Cargo.toml | 2 -- justfile | 13 +++++++ 5 files changed, 68 insertions(+), 43 deletions(-) diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index f0062ae5..3060b98c 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -10,10 +10,10 @@ env: jobs: unit-test: name: Test Suite - runs-on: self-hosted-ubuntu-22.04 + runs-on: ubuntu-latest steps: - - name: Checkout sources - uses: actions/checkout@v4 + - name: checkout sources + uses: actions/checkout@v3 - name: Install latest stable toolchain uses: actions-rs/toolchain@v1 @@ -29,32 +29,32 @@ jobs: command: test args: --locked - # lints: - # name: Lints - # runs-on: ubuntu-latest - # steps: - # - name: Checkout sources - # uses: actions/checkout@v3 + lints: + name: Lints + runs-on: ubuntu-latest + steps: + - name: checkout sources + uses: actions/checkout@v3 - # - name: Install stable toolchain - # uses: actions-rs/toolchain@v1 - # with: - # profile: minimal - # toolchain: 1.66.0 - # override: true - # components: rustfmt, clippy + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: 1.66.0 + override: true + components: rustfmt, clippy - # - name: Run cargo fmt - # uses: actions-rs/cargo@v1 - # with: - # command: fmt - # args: --all -- --check + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check - # - name: Run cargo clippy - # uses: actions-rs/cargo@v1 - # with: - # command: clippy - # args: --all-targets -- -D warnings + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --all-targets -- -D warnings # schema: # name: Schema diff --git a/.github/workflows/interchaintest.yml b/.github/workflows/interchaintest.yml index 8394c61a..0ba689a4 100644 --- a/.github/workflows/interchaintest.yml +++ b/.github/workflows/interchaintest.yml @@ -23,7 +23,7 @@ jobs: - name: checkout sources uses: actions/checkout@v3 - + - name: install tar for cache run: apk add --no-cache tar @@ -42,6 +42,11 @@ jobs: timeout-minutes: 40 run: optimize_workspace.sh . + - name: test ls + run: ls + - name: test pwd + run: pwd + - name: upload contracts uses: actions/upload-artifact@v3 with: @@ -51,19 +56,28 @@ jobs: - name: test ls run: ls artifacts/ -# events: -# runs-on: self-hosted-ubuntu-22.04 -# steps: -# - name: checkout repository -# uses: actions/checkout@v3 + events: + needs: compile-contracts + runs-on: ubuntu-latest + steps: + - name: checkout repository + uses: actions/checkout@v3 -# - name: Set up Go ${{ env.GO_VERSION }} -# uses: actions/setup-go@v4 -# with: -# go-version: ${{ env.GO_VERSION }} + - name: test ls + run: ls + - name: test pwd + run: pwd + - name: test ../ls + run: ls ../ + - name: test ../../ls + run: ls ../../ + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} -# - name: setup just -# uses: extractions/setup-just@v1 + - name: setup just + uses: extractions/setup-just@v1 -# - name: interchaintest -# run: just swap-covenant \ No newline at end of file + - name: interchaintest + run: just swap-covenant \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 70b958bd..6bf4fff9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,7 +73,7 @@ protobuf = { version = "3.2.0", features = ["with-bytes"] } serde-json-wasm = { version = "0.4.1" } base64 = "0.13.0" prost = "0.11" -astroport = "2.8.0" +astroport = "2.9.0" prost-types = "0.11" bech32 = "0.9.0" cosmwasm-schema = "1.2.1" diff --git a/contracts/ibc-forwarder/Cargo.toml b/contracts/ibc-forwarder/Cargo.toml index 487874ce..11cb47c7 100644 --- a/contracts/ibc-forwarder/Cargo.toml +++ b/contracts/ibc-forwarder/Cargo.toml @@ -27,7 +27,6 @@ covenant-clock = { workspace = true, features=["library"]} cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw-storage-plus = { workspace = true } -# cw-utils = { workspace = true } cw2 = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } @@ -37,7 +36,6 @@ cosmos-sdk-proto = { workspace = true } protobuf = { workspace = true } schemars = { workspace = true } serde-json-wasm = { workspace = true } -# base64 = { workspace = true } prost = { workspace = true } prost-types = { workspace = true } bech32 = { workspace = true } diff --git a/justfile b/justfile index 2b315aba..57d95959 100644 --- a/justfile +++ b/justfile @@ -17,6 +17,9 @@ workspace-optimize: swap-covenant: + ls + pwd + cd swap-covenant/ mkdir -p tests/interchaintest/wasms @@ -25,3 +28,13 @@ swap-covenant: go clean -testcache cd tests/interchaintest/ && go test -timeout 30m -v ./... + +two-party-pol: + cd two-party-pol-covenant/ + + mkdir -p tests/interchaintest/wasms + + cp -R ./../artifacts/*.wasm tests/interchaintest/wasms + + go clean -testcache + cd tests/interchaintest/ && go test -timeout 30m -v ./... \ No newline at end of file From 23cbf284fc89ab97345fa7745fefc944e78d82a0 Mon Sep 17 00:00:00 2001 From: bekauz Date: Fri, 3 Nov 2023 18:37:55 +0100 Subject: [PATCH 145/586] downloading artifacts in events debug artifacts copying over wasms to test folder fix unit tests apk add node debug wasm folder directory weirdness cd & test --- .github/workflows/interchaintest.yml | 29 +++++++++++-------- contracts/interchain-router/src/contract.rs | 5 +--- .../src/suite_tests/tests.rs | 4 ++- .../src/suite_tests/tests.rs | 8 ++--- justfile | 16 ++++------ 5 files changed, 30 insertions(+), 32 deletions(-) diff --git a/.github/workflows/interchaintest.yml b/.github/workflows/interchaintest.yml index 0ba689a4..0b6ebe5a 100644 --- a/.github/workflows/interchaintest.yml +++ b/.github/workflows/interchaintest.yml @@ -17,13 +17,21 @@ jobs: container: image: cosmwasm/workspace-optimizer:0.14.0 steps: - - name: install node - run: | - apk add --no-cache nodejs - + # - uses: actions/setup-node@v3 + # with: + # node-version: 16 + # cache: ${{ hashFiles('**/package-lock.json') && 'npm' || null }} + - name: checkout sources uses: actions/checkout@v3 + - name: install nodejs + run: | + apk update + apk add nodejs npm + apk update + node -v + - name: install tar for cache run: apk add --no-cache tar @@ -63,14 +71,11 @@ jobs: - name: checkout repository uses: actions/checkout@v3 - - name: test ls - run: ls - - name: test pwd - run: pwd - - name: test ../ls - run: ls ../ - - name: test ../../ls - run: ls ../../ + - uses: actions/download-artifact@v3 + with: + name: contracts + path: artifacts/ + - name: Set up Go ${{ env.GO_VERSION }} uses: actions/setup-go@v4 with: diff --git a/contracts/interchain-router/src/contract.rs b/contracts/interchain-router/src/contract.rs index a534e2f7..cf54047f 100644 --- a/contracts/interchain-router/src/contract.rs +++ b/contracts/interchain-router/src/contract.rs @@ -75,16 +75,13 @@ fn try_route_balances(deps: ExecuteDeps, env: Env) -> NeutronResult = match balances.len() { 0 => return Ok(Response::default() .add_attribute("method", "try_route_balances") .add_attribute("balances", "[]")), - 1 => return Ok(Response::default() - .add_attribute("method", "try_route_balances") - .add_attribute("balances", balances[0].to_string())), + 1 => vec![Attribute::new(balances[0].denom.to_string(), balances[0].amount)], _ => balances .iter() .map(|c| Attribute::new(c.denom.to_string(), c.amount)) diff --git a/contracts/interchain-router/src/suite_tests/tests.rs b/contracts/interchain-router/src/suite_tests/tests.rs index f846ab12..dcc2ee1c 100644 --- a/contracts/interchain-router/src/suite_tests/tests.rs +++ b/contracts/interchain-router/src/suite_tests/tests.rs @@ -72,7 +72,9 @@ fn test_unauthorized_tick() { #[test] fn test_tick() { - let querier: MockQuerier = MockQuerier::new(&[("cosmos2contract", &coins(100, "usdc"))]); + let usdc_coin = coin(100, "usdc"); + let coins = vec![usdc_coin]; + let querier: MockQuerier = MockQuerier::new(&[("cosmos2contract", &coins)]); let mut deps = OwnedDeps { storage: MockStorage::default(), diff --git a/contracts/two-party-pol-holder/src/suite_tests/tests.rs b/contracts/two-party-pol-holder/src/suite_tests/tests.rs index c38531a4..cac3eeca 100644 --- a/contracts/two-party-pol-holder/src/suite_tests/tests.rs +++ b/contracts/two-party-pol-holder/src/suite_tests/tests.rs @@ -65,7 +65,7 @@ fn test_instantiate_invalid_allocations() { } #[test] -#[should_panic(expected = "block height must be in the future")] +#[should_panic(expected = "deposit deadline is already past")] fn test_instantiate_invalid_deposit_deadline_block_based() { SuiteBuilder::default() .with_deposit_deadline(Expiration::AtHeight(1)) @@ -73,7 +73,7 @@ fn test_instantiate_invalid_deposit_deadline_block_based() { } #[test] -#[should_panic(expected = "block time must be in the future")] +#[should_panic(expected = "deposit deadline is already past")] fn test_instantiate_invalid_deposit_deadline_time_based() { SuiteBuilder::default() .with_deposit_deadline(Expiration::AtTime(Timestamp::from_nanos(1))) @@ -81,7 +81,7 @@ fn test_instantiate_invalid_deposit_deadline_time_based() { } #[test] -#[should_panic(expected = "invalid expiry config: block time must be in the future")] +#[should_panic(expected = "lockup deadline is already past")] fn test_instantiate_invalid_lockup_config_time_based() { SuiteBuilder::default() .with_lockup_config(Expiration::AtTime(Timestamp::from_nanos( @@ -91,7 +91,7 @@ fn test_instantiate_invalid_lockup_config_time_based() { } #[test] -#[should_panic(expected = "invalid expiry config: block height must be in the future")] +#[should_panic(expected = "lockup deadline is already past")] fn test_instantiate_invalid_lockup_config_height_based() { SuiteBuilder::default() .with_lockup_config(Expiration::AtHeight(INITIAL_BLOCK_HEIGHT - 1)) diff --git a/justfile b/justfile index 57d95959..91265316 100644 --- a/justfile +++ b/justfile @@ -17,17 +17,12 @@ workspace-optimize: swap-covenant: - ls - pwd - - cd swap-covenant/ - - mkdir -p tests/interchaintest/wasms - - cp -R ./../artifacts/*.wasm tests/interchaintest/wasms + ls artifacts/ - go clean -testcache - cd tests/interchaintest/ && go test -timeout 30m -v ./... + mkdir -p swap-covenant/tests/interchaintest/wasms + cp -R artifacts/*.wasm swap-covenant/tests/interchaintest/wasms + cd swap-covenant/tests/interchaintest && go test --timeout 30m + two-party-pol: cd two-party-pol-covenant/ @@ -36,5 +31,4 @@ two-party-pol: cp -R ./../artifacts/*.wasm tests/interchaintest/wasms - go clean -testcache cd tests/interchaintest/ && go test -timeout 30m -v ./... \ No newline at end of file From a8270ff3f1760bcd4315a22a2fc93e1484be764c Mon Sep 17 00:00:00 2001 From: bekauz Date: Sat, 4 Nov 2023 04:06:44 +0100 Subject: [PATCH 146/586] enable two party pol test action cleanup mv-contracts step split jobs run ictests without justfile --- .github/workflows/interchaintest.yml | 41 ++++++++++++++++++++++------ justfile | 18 +++++------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/.github/workflows/interchaintest.yml b/.github/workflows/interchaintest.yml index 0b6ebe5a..044c1a61 100644 --- a/.github/workflows/interchaintest.yml +++ b/.github/workflows/interchaintest.yml @@ -17,11 +17,6 @@ jobs: container: image: cosmwasm/workspace-optimizer:0.14.0 steps: - # - uses: actions/setup-node@v3 - # with: - # node-version: 16 - # cache: ${{ hashFiles('**/package-lock.json') && 'npm' || null }} - - name: checkout sources uses: actions/checkout@v3 @@ -64,7 +59,7 @@ jobs: - name: test ls run: ls artifacts/ - events: + swap-covenant: needs: compile-contracts runs-on: ubuntu-latest steps: @@ -83,6 +78,36 @@ jobs: - name: setup just uses: extractions/setup-just@v1 + + - name: move wasms to test directories + run: just mv-contracts - - name: interchaintest - run: just swap-covenant \ No newline at end of file + - name: swap covenant + run: cd swap-covenant/tests/interchaintest && go test --timeout 30m + + + two-party-pol-covenant: + needs: compile-contracts + runs-on: ubuntu-latest + steps: + - name: checkout repository + uses: actions/checkout@v3 + + - uses: actions/download-artifact@v3 + with: + name: contracts + path: artifacts/ + + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + + - name: setup just + uses: extractions/setup-just@v1 + + - name: move wasms to test directories + run: just mv-contracts + + - name: two party POL covenant + run: cd two-party-pol-covenant/tests/interchaintest && go test --timeout 30m \ No newline at end of file diff --git a/justfile b/justfile index 91265316..c77af00d 100644 --- a/justfile +++ b/justfile @@ -15,20 +15,16 @@ workspace-optimize: --platform linux/amd64 \ cosmwasm/workspace-optimizer:0.12.13 - -swap-covenant: +mv-contracts: ls artifacts/ mkdir -p swap-covenant/tests/interchaintest/wasms cp -R artifacts/*.wasm swap-covenant/tests/interchaintest/wasms - cd swap-covenant/tests/interchaintest && go test --timeout 30m - + mkdir -p two-party-pol-covenant/tests/interchaintest/wasms + cp -R artifacts/*.wasm two-party-pol-covenant/tests/interchaintest/wasms -two-party-pol: - cd two-party-pol-covenant/ - - mkdir -p tests/interchaintest/wasms - - cp -R ./../artifacts/*.wasm tests/interchaintest/wasms +swap-covenant: + cd swap-covenant/tests/interchaintest && go test --timeout 30m - cd tests/interchaintest/ && go test -timeout 30m -v ./... \ No newline at end of file +two-party-pol-covenant: + cd two-party-pol-covenant/tests/interchaintest && go test --timeout 30m From 1ee07ce14b2dacbde9cab0b733e34a8692dcb707 Mon Sep 17 00:00:00 2001 From: bekauz Date: Sat, 4 Nov 2023 13:28:31 +0100 Subject: [PATCH 147/586] two party pol contracts load fix test copying over astroport contracts unignore astroport wasms self hosted runner apk add docker docker step docker action docker action in separate step --- .github/workflows/interchaintest.yml | 34 ++++++++++-------- .gitignore | 3 +- justfile | 11 ++++-- .../astroport/astroport_factory.wasm | Bin 0 -> 346585 bytes .../astroport_native_coin_registry.wasm | Bin 0 -> 195979 bytes .../astroport/astroport_pair.wasm | Bin 0 -> 444951 bytes .../astroport/astroport_pair_stable.wasm | Bin 0 -> 492543 bytes .../astroport/astroport_token.wasm | Bin 0 -> 296861 bytes .../astroport/astroport_whitelist.wasm | Bin 0 -> 205372 bytes .../interchaintest/two_party_pol_test.go | 2 +- 10 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 two-party-pol-covenant/astroport/astroport_factory.wasm create mode 100644 two-party-pol-covenant/astroport/astroport_native_coin_registry.wasm create mode 100644 two-party-pol-covenant/astroport/astroport_pair.wasm create mode 100644 two-party-pol-covenant/astroport/astroport_pair_stable.wasm create mode 100644 two-party-pol-covenant/astroport/astroport_token.wasm create mode 100644 two-party-pol-covenant/astroport/astroport_whitelist.wasm diff --git a/.github/workflows/interchaintest.yml b/.github/workflows/interchaintest.yml index 044c1a61..d26ea18f 100644 --- a/.github/workflows/interchaintest.yml +++ b/.github/workflows/interchaintest.yml @@ -11,13 +11,21 @@ env: GO_VERSION: 1.21 jobs: + docker: + name: install docker + runs-on: self-hosted-ubuntu-22.04 + steps: + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v2 + compile-contracts: name: compile wasm contract with workspace-optimizer - runs-on: ubuntu-latest + # needs: docker + runs-on: self-hosted-ubuntu-22.04 container: image: cosmwasm/workspace-optimizer:0.14.0 steps: - - name: checkout sources + - name: checkout sources uses: actions/checkout@v3 - name: install nodejs @@ -40,7 +48,7 @@ jobs: ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - + - name: compile contracts timeout-minutes: 40 run: optimize_workspace.sh . @@ -58,10 +66,10 @@ jobs: - name: test ls run: ls artifacts/ - + swap-covenant: needs: compile-contracts - runs-on: ubuntu-latest + runs-on: self-hosted-ubuntu-22.04 steps: - name: checkout repository uses: actions/checkout@v3 @@ -79,16 +87,13 @@ jobs: - name: setup just uses: extractions/setup-just@v1 - - name: move wasms to test directories - run: just mv-contracts - - name: swap covenant - run: cd swap-covenant/tests/interchaintest && go test --timeout 30m + run: just swap-covenant + - two-party-pol-covenant: needs: compile-contracts - runs-on: ubuntu-latest + runs-on: self-hosted-ubuntu-22.04 steps: - name: checkout repository uses: actions/checkout@v3 @@ -106,8 +111,9 @@ jobs: - name: setup just uses: extractions/setup-just@v1 - - name: move wasms to test directories - run: just mv-contracts + # - name: move wasms to test directories + # run: just mv-contracts - name: two party POL covenant - run: cd two-party-pol-covenant/tests/interchaintest && go test --timeout 30m \ No newline at end of file + run: just two-party-pol-covenant + # run: cd two-party-pol-covenant/tests/interchaintest && go test --timeout 30m diff --git a/.gitignore b/.gitignore index a4e8210c..03a27e3f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,8 @@ target/ .cargo-ok # Build artifacts -*.wasm +# *.wasm +**/wasms hash.txt contracts.txt artifacts/ diff --git a/justfile b/justfile index c77af00d..9fb34d41 100644 --- a/justfile +++ b/justfile @@ -24,7 +24,14 @@ mv-contracts: cp -R artifacts/*.wasm two-party-pol-covenant/tests/interchaintest/wasms swap-covenant: - cd swap-covenant/tests/interchaintest && go test --timeout 30m + mkdir -p swap-covenant/tests/interchaintest/wasms + cp -R artifacts/*.wasm swap-covenant/tests/interchaintest/wasms + ls swap-covenant/tests/interchaintest/wasms/ + cd swap-covenant/tests/interchaintest && go test --timeout 30m two-party-pol-covenant: - cd two-party-pol-covenant/tests/interchaintest && go test --timeout 30m + mkdir -p two-party-pol-covenant/tests/interchaintest/wasms + cp -R artifacts/*.wasm two-party-pol-covenant/tests/interchaintest/wasms + cp -R two-party-pol-covenant/astroport/*.wasm two-party-pol-covenant/tests/interchaintest/wasms + ls two-party-pol-covenant/tests/interchaintest/wasms/ + cd two-party-pol-covenant/tests/interchaintest && go test --timeout 30m diff --git a/two-party-pol-covenant/astroport/astroport_factory.wasm b/two-party-pol-covenant/astroport/astroport_factory.wasm new file mode 100644 index 0000000000000000000000000000000000000000..322158724198a9b27f9dcf2af3dfabf629f8287c GIT binary patch literal 346585 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 +n!p|9taHQQM^IUl6xf3Rq$6=58u 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 z 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 literal 0 HcmV?d00001 diff --git a/two-party-pol-covenant/astroport/astroport_native_coin_registry.wasm b/two-party-pol-covenant/astroport/astroport_native_coin_registry.wasm new file mode 100644 index 0000000000000000000000000000000000000000..b3e79d9ce4d310e15f66ff76b3fe2dbffc106454 GIT binary patch literal 195979 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 jV#uiAlR%ZqD_n}o(VPCWxZ=i32C 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 s6Y6?0r-tSuAg{rwNw9EhaE{#Udb{v@qgU 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

3By(cOZwcbtq{ke30_v(R|s7656N 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 y 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 literal 0 HcmV?d00001 diff --git a/two-party-pol-covenant/astroport/astroport_pair_stable.wasm b/two-party-pol-covenant/astroport/astroport_pair_stable.wasm new file mode 100644 index 0000000000000000000000000000000000000000..47caf760714a475b63f07fc0cfd511e89def20e0 GIT binary patch literal 492543 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 z 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