diff --git a/crates/cli/src/handler/handshake.rs b/crates/cli/src/handler/handshake.rs index 63abc7b6..5ad9b7b4 100644 --- a/crates/cli/src/handler/handshake.rs +++ b/crates/cli/src/handler/handshake.rs @@ -1,3 +1,5 @@ +use std::iter; + use async_trait::async_trait; use color_eyre::{eyre::eyre, owo_colors::OwoColorize, Report, Result}; use cw_client::{CliClient, CwClient}; @@ -55,7 +57,7 @@ async fn handshake(args: HandshakeRequest, config: Config) -> Result { &config.chain_id, 2000000, &config.tx_sender, - json!(res), + iter::once(json!(res)), "0untrn" ) .await @@ -105,7 +107,7 @@ async fn handshake(args: HandshakeRequest, config: Config) -> Result { &config.chain_id, 2000000, &config.tx_sender, - json!(res), + iter::once(json!(res)), "0untrn" ) .await diff --git a/crates/enclave/core/src/chain_client.rs b/crates/enclave/core/src/chain_client.rs index dcfab3dd..999a7e95 100644 --- a/crates/enclave/core/src/chain_client.rs +++ b/crates/enclave/core/src/chain_client.rs @@ -1,7 +1,10 @@ use std::fmt::Display; +use cosmrs::abci::GasInfo; use serde::{de::DeserializeOwned, Serialize}; +use crate::chain_client::default::DefaultTxConfig; + pub mod default; /// Abstraction over a blockchain client. @@ -19,8 +22,6 @@ pub trait ChainClient: Send + Sync + 'static { type Proof: Serialize + Send + Sync + 'static; /// The type used to represent query messages. type Query: Send + Sync + 'static; - /// The configuration type for transactions (e.g. gas fees, parameters). - type TxConfig: Send + Sync + 'static; /// The output type returned after sending a transaction. type TxOutput: Send + Sync + 'static; @@ -63,20 +64,39 @@ pub trait ChainClient: Send + Sync + 'static { /// # Parameters /// /// - `contract`: A reference to the contract identifier. - /// - `tx`: The transaction payload, which must be serializable. + /// - `msgs`: The transaction messages, which must be serializable. /// - `config`: The transaction configuration (e.g., gas, fees). /// /// # Returns /// /// A `Result` containing the transaction output of type `Self::TxOutput` on success, /// or an error of type `Self::Error` if the transaction fails. - async fn send_tx( + async fn send_tx( &self, contract: &Self::Contract, - tx: T, - config: Self::TxConfig, + msgs: impl Iterator + Send + Sync, + config: DefaultTxConfig, ) -> Result; + /// Simulates a transaction returning the gas_info. + /// + /// # Parameters + /// + /// - `contract`: A reference to the contract identifier. + /// - `msgs`: The transaction messages, which must be serializable. + /// - `config`: The transaction configuration (e.g., gas, fees). + /// + /// # Returns + /// + /// A `Result` containing the `GasInfo` on success, + /// or an error of type `Self::Error` if the transaction fails. + async fn simulate_tx( + &self, + contract: &Self::Contract, + msgs: impl Iterator + Send + Sync, + config: DefaultTxConfig, + ) -> Result; + /// Waits for a specified number of blocks to be produced on the blockchain. /// /// # Parameters diff --git a/crates/enclave/core/src/chain_client/default.rs b/crates/enclave/core/src/chain_client/default.rs index cd5c15cb..bdceaebb 100644 --- a/crates/enclave/core/src/chain_client/default.rs +++ b/crates/enclave/core/src/chain_client/default.rs @@ -1,5 +1,5 @@ use anyhow::anyhow; -use cosmrs::{crypto::secp256k1::SigningKey, AccountId}; +use cosmrs::{abci::GasInfo, crypto::secp256k1::SigningKey, AccountId}; use cw_client::{CwClient, GrpcClient}; use futures_util::StreamExt; use log::{debug, error, info, trace}; @@ -21,12 +21,12 @@ use crate::chain_client::ChainClient; /// - websocket for waiting for blocks /// - tendermint HTTP RPC for generating light client proofs pub struct DefaultChainClient { - chain_id: TmChainId, - grpc_client: GrpcClient, - node_url: Url, - ws_url: Url, - trusted_height: Height, - trusted_hash: Hash, + pub chain_id: TmChainId, + pub grpc_client: GrpcClient, + pub node_url: Url, + pub ws_url: Url, + pub trusted_height: Height, + pub trusted_hash: Hash, } impl DefaultChainClient { @@ -74,7 +74,6 @@ impl ChainClient for DefaultChainClient { type Error = anyhow::Error; type Proof = ProofOutput; type Query = Query; - type TxConfig = DefaultTxConfig; type TxOutput = String; async fn query_contract( @@ -129,11 +128,11 @@ impl ChainClient for DefaultChainClient { Ok(proof_output) } - async fn send_tx( + async fn send_tx( &self, contract: &Self::Contract, - tx: T, - config: Self::TxConfig, + msgs: impl Iterator + Send + Sync, + config: DefaultTxConfig, ) -> Result { debug!( "Sending transaction to contract {contract} with gas {}", @@ -145,7 +144,29 @@ impl ChainClient for DefaultChainClient { &self.chain_id, config.gas, "", - json!(tx), + msgs.map(|m| json!(m)), + &config.amount, + ) + .await + } + + async fn simulate_tx( + &self, + contract: &Self::Contract, + msgs: impl Iterator + Send + Sync, + config: DefaultTxConfig, + ) -> Result { + debug!( + "Simulating a transaction to contract {contract} with gas {}", + config.gas + ); + self.grpc_client + .tx_simulate( + contract, + &self.chain_id, + config.gas, + "", + msgs.map(|m| json!(m)), &config.amount, ) .await @@ -185,3 +206,26 @@ pub struct DefaultTxConfig { pub gas: u64, pub amount: String, } + +impl DefaultTxConfig { + pub fn new(gas_used: u64, multiplier: f64, base_gas_price: f64, denom: &str) -> Self { + let gas = scale_gas(gas_used, multiplier); + let amount_num = scale_gas(gas, base_gas_price); + Self { + gas, + amount: format!("{amount_num}{denom}"), + } + } +} + +pub fn scale_gas(gas: u64, multiplier: f64) -> u64 { + if !multiplier.is_finite() || multiplier < 0.0 { + return gas; + } + let prod = (gas as f64) * multiplier; + if prod >= (u64::MAX as f64) { + u64::MAX + } else { + prod.ceil() as u64 + } +} diff --git a/crates/enclave/core/src/host.rs b/crates/enclave/core/src/host.rs index 09bc153c..c344ca1e 100644 --- a/crates/enclave/core/src/host.rs +++ b/crates/enclave/core/src/host.rs @@ -23,7 +23,10 @@ use tonic_health::server::health_reporter; use crate::{ backup_restore::Backup, - chain_client::{default::DefaultChainClient, ChainClient}, + chain_client::{ + default::{DefaultChainClient, DefaultTxConfig}, + ChainClient, + }, event::QuartzEvent, handler::Handler, store::Store, @@ -156,10 +159,11 @@ where C: ChainClient, ::TxOutput: Display, R: Handler + Debug, - >::Response: Serialize + Send + Sync + 'static, + >::Response: Iterator + Send + Sync, + <>::Response as Iterator>::Item: Serialize + Send + Sync + 'static, EV: Handler, EV: TryFrom, - GF: Fn(&>::Response) -> ::TxConfig + Send + Sync + 'static, + GF: GasProvider<>::Response, C> + Send + Sync + 'static, { type ChainClient = C; type Enclave = E; @@ -280,10 +284,13 @@ where }; // submit response to the chain - let tx_config = (self.gas_fn)(&response); + let gas_info = self + .gas_fn + .gas_for_tx(&response, &self.chain_client, &contract) + .await?; let output = self .chain_client - .send_tx(&contract, response, tx_config) + .send_tx(&contract, response, gas_info) .await; match output { Ok(o) => info!("tx output: {o}"), @@ -315,3 +322,13 @@ fn busy_wait_iters(mut iters: u64) { iters -= 1; } } + +#[async_trait::async_trait] +pub trait GasProvider { + async fn gas_for_tx( + &self, + tx: &Tx, + chain_client: &CC, + contract: &AccountId, + ) -> Result; +} diff --git a/crates/utils/cw-client/src/cli.rs b/crates/utils/cw-client/src/cli.rs index e2d2dc68..84df27ee 100644 --- a/crates/utils/cw-client/src/cli.rs +++ b/crates/utils/cw-client/src/cli.rs @@ -1,7 +1,7 @@ use std::process::Command; use color_eyre::{eyre::eyre, Help, Report, Result}; -use cosmrs::{tendermint::chain::Id, AccountId}; +use cosmrs::{abci::GasInfo, tendermint::chain::Id, AccountId}; use reqwest::Url; use serde::de::DeserializeOwned; @@ -145,13 +145,13 @@ impl CwClient for CliClient { Ok(query_result) } - async fn tx_execute( + async fn tx_execute( &self, contract: &Self::Address, chain_id: &Id, gas: u64, sender: &str, - msg: M, + msgs: impl Iterator + Send + Sync, pay_amount: &str, ) -> Result { let gas_amount = match gas { @@ -159,6 +159,10 @@ impl CwClient for CliClient { _ => &gas.to_string(), }; + // only support one message for now + let msgs = msgs.collect::>(); + let msg = msgs.first().ok_or(eyre!("No messages provided"))?; + let mut command = self.new_command()?; let command = command .args(["--node", self.url.as_str()]) @@ -183,6 +187,49 @@ impl CwClient for CliClient { Ok((String::from_utf8(output.stdout)?).to_string()) } + async fn tx_simulate( + &self, + contract: &Self::Address, + chain_id: &Id, + gas: u64, + sender: &str, + msgs: impl Iterator + Send + Sync, + pay_amount: &str, + ) -> std::result::Result { + let gas_amount = match gas { + 0 => "auto", + _ => &gas.to_string(), + }; + + // only support one message for now + let msgs = msgs.collect::>(); + let msg = msgs.first().ok_or(eyre!("No messages provided"))?; + + let mut command = self.new_command()?; + let command = command + .args(["--node", self.url.as_str()]) + .args(["--chain-id", chain_id.as_ref()]) + .args(["tx", "wasm"]) + .args(["execute", contract.as_ref(), &msg.to_string()]) + .args(["--amount", pay_amount]) + .args(["--gas", gas_amount]) + .args(["--gas-adjustment", "1.3"]) + .args(["--gas-prices", "0.025untrn"]) + .args(["--from", sender]) + .args(["--output", "json"]) + .arg("--dry-run") + .arg("-y"); + + let output = command.output()?; + + if !output.status.success() { + return Err(eyre!("{:?}", output)); + } + + let gas_info: GasInfo = serde_json::from_slice(&output.stdout)?; + Ok(gas_info) + } + fn deploy( &self, chain_id: &Id, diff --git a/crates/utils/cw-client/src/grpc.rs b/crates/utils/cw-client/src/grpc.rs index ba3cc291..44c130b4 100644 --- a/crates/utils/cw-client/src/grpc.rs +++ b/crates/utils/cw-client/src/grpc.rs @@ -20,6 +20,7 @@ use cosmos_sdk_proto::{ Any, }; use cosmrs::{ + abci::GasInfo, auth::BaseAccount, cosmwasm::MsgExecuteContract, crypto::{secp256k1::SigningKey, PublicKey}, @@ -94,13 +95,13 @@ impl CwClient for GrpcClient { unimplemented!() } - async fn tx_execute( + async fn tx_execute( &self, contract: &Self::Address, chain_id: &TmChainId, gas: u64, _sender: &str, - msg: M, + msgs: impl Iterator + Send + Sync, pay_amount: &str, ) -> Result { let tm_pubkey = self.sk.public_key(); @@ -108,14 +109,18 @@ impl CwClient for GrpcClient { .account_id("neutron") .map_err(|e| anyhow!("failed to create AccountId from pubkey: {}", e))?; - let msgs = vec![MsgExecuteContract { - sender: sender.clone(), - contract: contract.clone(), - msg: msg.to_string().into_bytes(), - funds: vec![], - } - .to_any() - .unwrap()]; + let msgs = msgs + .map(|msg| { + MsgExecuteContract { + sender: sender.clone(), + contract: contract.clone(), + msg: msg.to_string().into_bytes(), + funds: vec![], + } + .to_any() + .unwrap() + }) + .collect(); let account = account_info(self.url.to_string(), sender.to_string()) .await @@ -143,6 +148,59 @@ impl CwClient for GrpcClient { .unwrap_or_default()) } + async fn tx_simulate( + &self, + contract: &Self::Address, + chain_id: &TmChainId, + gas: u64, + _sender: &str, + msgs: impl Iterator + Send + Sync, + pay_amount: &str, + ) -> Result { + let tm_pubkey = self.sk.public_key(); + let sender = tm_pubkey + .account_id("neutron") + .map_err(|e| anyhow!("failed to create AccountId from pubkey: {}", e))?; + + let msgs = msgs + .map(|msg| { + MsgExecuteContract { + sender: sender.clone(), + contract: contract.clone(), + msg: msg.to_string().into_bytes(), + funds: vec![], + } + .to_any() + .unwrap() + }) + .collect(); + + let account = account_info(self.url.to_string(), sender.to_string()) + .await + .map_err(|e| anyhow!("error querying account info: {}", e))?; + let amount = parse_coin(pay_amount)?; + let tx_bytes = tx_bytes( + &self.sk, + amount, + gas, + tm_pubkey, + msgs, + account.sequence, + account.account_number, + chain_id, + ) + .map_err(|e| anyhow!("failed to create msg/tx: {}", e))?; + + let response = simulate_tx(self.url.to_string(), tx_bytes) + .await + .map_err(|e| anyhow!("failed to simulate tx: {}", e))?; + response + .gas_info + .expect("missing gas info from tx_simulate response") + .try_into() + .map_err(|e| anyhow!("failed to simulate tx: {}", e)) + } + fn deploy( &self, _chain_id: &TmChainId, diff --git a/crates/utils/cw-client/src/lib.rs b/crates/utils/cw-client/src/lib.rs index 69d4ee1c..9c3998a5 100644 --- a/crates/utils/cw-client/src/lib.rs +++ b/crates/utils/cw-client/src/lib.rs @@ -1,4 +1,5 @@ pub use cli::CliClient; +pub use cosmrs::abci::GasInfo; use cosmrs::tendermint::chain::Id; pub use grpc::GrpcClient; use hex::ToHex; @@ -30,16 +31,26 @@ pub trait CwClient { fn query_tx(&self, txhash: &str) -> Result; - async fn tx_execute( + async fn tx_execute( &self, contract: &Self::Address, chain_id: &Id, gas: u64, sender: &str, - msg: M, + msgs: impl Iterator + Send + Sync, pay_amount: &str, ) -> Result; + async fn tx_simulate( + &self, + contract: &Self::Address, + chain_id: &Id, + gas: u64, + sender: &str, + msgs: impl Iterator + Send + Sync, + pay_amount: &str, + ) -> Result; + fn deploy( &self, chain_id: &Id, diff --git a/examples/pingpong/enclave/src/bin/send_message.rs b/examples/pingpong/enclave/src/bin/send_message.rs index 03c405f4..70c47412 100644 --- a/examples/pingpong/enclave/src/bin/send_message.rs +++ b/examples/pingpong/enclave/src/bin/send_message.rs @@ -1,4 +1,4 @@ -use std::str::FromStr; +use std::{iter, str::FromStr}; use cosmrs::{tendermint::chain::Id as ChainId, AccountId}; use cosmwasm_std::HexBinary; @@ -47,7 +47,7 @@ async fn main() { chain_id, 2000000, "admin", - json!(pong_msg), + iter::once(json!(pong_msg)), "11000untrn", ) .await; diff --git a/examples/pingpong/enclave/src/main.rs b/examples/pingpong/enclave/src/main.rs index 5da4dcf0..adea9b06 100644 --- a/examples/pingpong/enclave/src/main.rs +++ b/examples/pingpong/enclave/src/main.rs @@ -19,12 +19,16 @@ pub mod request; use clap::Parser; use cli::Cli; +use cosmrs::AccountId; use quartz_common::{ contract::state::{Config, LightClientOpts}, enclave::{ attestor::{self, Attestor}, - chain_client::default::{DefaultChainClient, DefaultTxConfig}, - host::{DefaultHost, Host}, + chain_client::{ + default::{DefaultChainClient, DefaultTxConfig}, + ChainClient, + }, + host::{DefaultHost, GasProvider, Host}, DefaultSharedEnclave, }, }; @@ -94,7 +98,7 @@ async fn main() -> Result<(), Box> { let host = DefaultHost::::new( enclave, chain_client, - gas_fn, + GasSimulator, args.backup_path, notifier_rx, ); @@ -104,13 +108,28 @@ async fn main() -> Result<(), Box> { Ok(()) } -fn gas_fn(response: &EnclaveResponse) -> DefaultTxConfig { - if matches!(response, EnclaveResponse::Ping(_)) { - DefaultTxConfig { +struct GasSimulator; + +#[async_trait::async_trait] +impl GasProvider for GasSimulator { + async fn gas_for_tx( + &self, + tx: &EnclaveResponse, + chain_client: &DefaultChainClient, + contract: &AccountId, + ) -> Result { + let default_config = DefaultTxConfig { gas: 2000000, amount: "11000untrn".to_string(), - } - } else { - unreachable!() + }; + let gas_info = chain_client + .simulate_tx(contract, tx.as_slice().iter(), default_config) + .await?; + Ok(DefaultTxConfig::new( + gas_info.gas_used, + 1.3, + 0.0053, + "untrn", + )) } } diff --git a/examples/pingpong/enclave/src/request.rs b/examples/pingpong/enclave/src/request.rs index b4f7fb08..7ec03a94 100644 --- a/examples/pingpong/enclave/src/request.rs +++ b/examples/pingpong/enclave/src/request.rs @@ -1,3 +1,5 @@ +use std::vec::IntoIter; + use ecies::{decrypt, encrypt}; use ping_pong_contract::{ msg::{execute, execute::Ping, AttestedMsg, ExecuteMsg}, @@ -17,7 +19,8 @@ use tonic::Status; use crate::proto::PingRequest; -pub type EnclaveResponse = ExecuteMsg<::RawAttestation>; +pub type EnclaveMsg = ExecuteMsg<::RawAttestation>; +pub type EnclaveResponse = IntoIter; #[derive(Clone, Debug)] pub enum EnclaveRequest { @@ -52,6 +55,7 @@ impl Handler> for EnclaveRequest { .map(|msg| attested_msg(msg, attestor))? .map(ExecuteMsg::Pong), } + .map(|msg| vec![msg].into_iter()) } } diff --git a/examples/transfers/enclave/src/main.rs b/examples/transfers/enclave/src/main.rs index 4edc4f01..123bd28d 100644 --- a/examples/transfers/enclave/src/main.rs +++ b/examples/transfers/enclave/src/main.rs @@ -20,12 +20,16 @@ pub mod state; use clap::Parser; use cli::Cli; +use cosmrs::AccountId; use quartz_common::{ contract::state::{Config, LightClientOpts}, enclave::{ attestor::{self, Attestor}, - chain_client::default::{DefaultChainClient, DefaultTxConfig}, - host::{DefaultHost, Host}, + chain_client::{ + default::{DefaultChainClient, DefaultTxConfig}, + ChainClient, + }, + host::{DefaultHost, GasProvider, Host}, DefaultSharedEnclave, }, }; @@ -99,7 +103,7 @@ async fn main() -> Result<(), Box> { let host = DefaultHost::::new( enclave, chain_client, - gas_fn, + GasSimulator, args.backup_path, notifier_rx, ); @@ -109,16 +113,28 @@ async fn main() -> Result<(), Box> { Ok(()) } -fn gas_fn(response: &EnclaveResponse) -> DefaultTxConfig { - if matches!( - response, - EnclaveResponse::Update(_) | EnclaveResponse::QueryResponse(_) - ) { - DefaultTxConfig { +struct GasSimulator; + +#[async_trait::async_trait] +impl GasProvider for GasSimulator { + async fn gas_for_tx( + &self, + tx: &EnclaveResponse, + chain_client: &DefaultChainClient, + contract: &AccountId, + ) -> Result { + let default_config = DefaultTxConfig { gas: 2000000, amount: "11000untrn".to_string(), - } - } else { - unreachable!() + }; + let gas_info = chain_client + .simulate_tx(contract, tx.as_slice().iter(), default_config) + .await?; + Ok(DefaultTxConfig::new( + gas_info.gas_used, + 1.3, + 0.0053, + "untrn", + )) } } diff --git a/examples/transfers/enclave/src/request.rs b/examples/transfers/enclave/src/request.rs index 166fbcc9..c9723de4 100644 --- a/examples/transfers/enclave/src/request.rs +++ b/examples/transfers/enclave/src/request.rs @@ -1,3 +1,5 @@ +use std::vec::IntoIter; + use cosmwasm_std::HexBinary; use ecies::{decrypt, encrypt}; use k256::ecdsa::{SigningKey, VerifyingKey}; @@ -20,7 +22,8 @@ use crate::{ pub mod query; pub mod update; -pub type EnclaveResponse = ExecuteMsg<::RawAttestation>; +pub type EnclaveMsg = ExecuteMsg<::RawAttestation>; +pub type EnclaveResponse = IntoIter; #[derive(Clone, Debug)] pub enum EnclaveRequest { @@ -61,6 +64,7 @@ impl Handler for EnclaveRequest { .map(|msg| attested_msg(msg, attestor))? .map(ExecuteMsg::QueryResponse), } + .map(|msg| vec![msg].into_iter()) } }