From 2eb43b7b6238a9dcf90d4615b58a9cacd8fb574a Mon Sep 17 00:00:00 2001 From: Shoaib Ahmed Date: Thu, 18 Sep 2025 19:02:14 +0400 Subject: [PATCH 01/13] Allow request handlers to send multiple messages --- crates/cli/src/handler/handshake.rs | 5 +++-- crates/enclave/core/src/chain_client.rs | 4 ++-- crates/enclave/core/src/chain_client/default.rs | 6 +++--- crates/enclave/core/src/host.rs | 3 ++- crates/utils/cw-client/src/cli.rs | 8 ++++++-- crates/utils/cw-client/src/grpc.rs | 8 ++++---- crates/utils/cw-client/src/lib.rs | 4 ++-- examples/pingpong/enclave/src/bin/send_message.rs | 4 ++-- examples/pingpong/enclave/src/main.rs | 5 +++-- examples/pingpong/enclave/src/request.rs | 6 +++++- examples/transfers/enclave/src/main.rs | 7 ++++--- examples/transfers/enclave/src/request.rs | 6 +++++- 12 files changed, 41 insertions(+), 25 deletions(-) diff --git a/crates/cli/src/handler/handshake.rs b/crates/cli/src/handler/handshake.rs index 63abc7b6..b75d616b 100644 --- a/crates/cli/src/handler/handshake.rs +++ b/crates/cli/src/handler/handshake.rs @@ -1,3 +1,4 @@ +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 +56,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 +106,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..461b7cb7 100644 --- a/crates/enclave/core/src/chain_client.rs +++ b/crates/enclave/core/src/chain_client.rs @@ -70,10 +70,10 @@ pub trait ChainClient: Send + Sync + 'static { /// /// 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, + msgs: impl Iterator + Send + Sync, config: Self::TxConfig, ) -> Result; diff --git a/crates/enclave/core/src/chain_client/default.rs b/crates/enclave/core/src/chain_client/default.rs index cd5c15cb..ebe7ece5 100644 --- a/crates/enclave/core/src/chain_client/default.rs +++ b/crates/enclave/core/src/chain_client/default.rs @@ -129,10 +129,10 @@ impl ChainClient for DefaultChainClient { Ok(proof_output) } - async fn send_tx( + async fn send_tx( &self, contract: &Self::Contract, - tx: T, + msgs: impl Iterator + Send + Sync, config: Self::TxConfig, ) -> Result { debug!( @@ -145,7 +145,7 @@ impl ChainClient for DefaultChainClient { &self.chain_id, config.gas, "", - json!(tx), + msgs.map(|m| json!(m)), &config.amount, ) .await diff --git a/crates/enclave/core/src/host.rs b/crates/enclave/core/src/host.rs index 09bc153c..8608cf01 100644 --- a/crates/enclave/core/src/host.rs +++ b/crates/enclave/core/src/host.rs @@ -156,7 +156,8 @@ 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, diff --git a/crates/utils/cw-client/src/cli.rs b/crates/utils/cw-client/src/cli.rs index e2d2dc68..46c6c584 100644 --- a/crates/utils/cw-client/src/cli.rs +++ b/crates/utils/cw-client/src/cli.rs @@ -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()]) diff --git a/crates/utils/cw-client/src/grpc.rs b/crates/utils/cw-client/src/grpc.rs index 00706552..91e901e4 100644 --- a/crates/utils/cw-client/src/grpc.rs +++ b/crates/utils/cw-client/src/grpc.rs @@ -93,13 +93,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(); @@ -107,14 +107,14 @@ impl CwClient for GrpcClient { .account_id("neutron") .map_err(|e| anyhow!("failed to create AccountId from pubkey: {}", e))?; - let msgs = vec![MsgExecuteContract { + let msgs = msgs.map(|msg| MsgExecuteContract { sender: sender.clone(), contract: contract.clone(), msg: msg.to_string().into_bytes(), funds: vec![], } .to_any() - .unwrap()]; + .unwrap()).collect(); let account = account_info(self.url.to_string(), sender.to_string()) .await diff --git a/crates/utils/cw-client/src/lib.rs b/crates/utils/cw-client/src/lib.rs index 69d4ee1c..2ccc8e43 100644 --- a/crates/utils/cw-client/src/lib.rs +++ b/crates/utils/cw-client/src/lib.rs @@ -30,13 +30,13 @@ 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; 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..5cf3ada0 100644 --- a/examples/pingpong/enclave/src/main.rs +++ b/examples/pingpong/enclave/src/main.rs @@ -31,7 +31,7 @@ use quartz_common::{ use crate::{ event::EnclaveEvent, - request::{EnclaveRequest, EnclaveResponse}, + request::{EnclaveMsg, EnclaveRequest, EnclaveResponse}, }; #[tokio::main(flavor = "current_thread")] @@ -105,7 +105,8 @@ async fn main() -> Result<(), Box> { } fn gas_fn(response: &EnclaveResponse) -> DefaultTxConfig { - if matches!(response, EnclaveResponse::Ping(_)) { + let response = response.clone().collect::>(); + if matches!(response.first(), Some(EnclaveMsg::Ping(_))) { DefaultTxConfig { gas: 2000000, amount: "11000untrn".to_string(), 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..19ebc2c2 100644 --- a/examples/transfers/enclave/src/main.rs +++ b/examples/transfers/enclave/src/main.rs @@ -32,7 +32,7 @@ use quartz_common::{ use crate::{ event::EnclaveEvent, - request::{EnclaveRequest, EnclaveResponse}, + request::{EnclaveMsg, EnclaveRequest, EnclaveResponse}, state::{AppCtx, AppEnclave}, }; @@ -110,9 +110,10 @@ async fn main() -> Result<(), Box> { } fn gas_fn(response: &EnclaveResponse) -> DefaultTxConfig { + let response = response.clone().collect::>(); if matches!( - response, - EnclaveResponse::Update(_) | EnclaveResponse::QueryResponse(_) + response.first(), + Some(EnclaveMsg::Update(_)) | Some(EnclaveMsg::QueryResponse(_)) ) { DefaultTxConfig { gas: 2000000, 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()) } } From 27bebdecaa9dc592547be127c5ee41ce6d08bd73 Mon Sep 17 00:00:00 2001 From: Shoaib Ahmed Date: Thu, 18 Sep 2025 19:30:57 +0400 Subject: [PATCH 02/13] fmt --- crates/cli/src/handler/handshake.rs | 1 + crates/enclave/core/src/chain_client.rs | 2 +- .../enclave/core/src/chain_client/default.rs | 2 +- crates/utils/cw-client/src/cli.rs | 2 +- crates/utils/cw-client/src/grpc.rs | 22 +++++++++++-------- crates/utils/cw-client/src/lib.rs | 2 +- 6 files changed, 18 insertions(+), 13 deletions(-) diff --git a/crates/cli/src/handler/handshake.rs b/crates/cli/src/handler/handshake.rs index b75d616b..5ad9b7b4 100644 --- a/crates/cli/src/handler/handshake.rs +++ b/crates/cli/src/handler/handshake.rs @@ -1,4 +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}; diff --git a/crates/enclave/core/src/chain_client.rs b/crates/enclave/core/src/chain_client.rs index 461b7cb7..c15619f2 100644 --- a/crates/enclave/core/src/chain_client.rs +++ b/crates/enclave/core/src/chain_client.rs @@ -73,7 +73,7 @@ pub trait ChainClient: Send + Sync + 'static { async fn send_tx( &self, contract: &Self::Contract, - msgs: impl Iterator + Send + Sync, + msgs: impl Iterator + Send + Sync, config: Self::TxConfig, ) -> Result; diff --git a/crates/enclave/core/src/chain_client/default.rs b/crates/enclave/core/src/chain_client/default.rs index ebe7ece5..9b09f272 100644 --- a/crates/enclave/core/src/chain_client/default.rs +++ b/crates/enclave/core/src/chain_client/default.rs @@ -132,7 +132,7 @@ impl ChainClient for DefaultChainClient { async fn send_tx( &self, contract: &Self::Contract, - msgs: impl Iterator + Send + Sync, + msgs: impl Iterator + Send + Sync, config: Self::TxConfig, ) -> Result { debug!( diff --git a/crates/utils/cw-client/src/cli.rs b/crates/utils/cw-client/src/cli.rs index 46c6c584..20065fdb 100644 --- a/crates/utils/cw-client/src/cli.rs +++ b/crates/utils/cw-client/src/cli.rs @@ -151,7 +151,7 @@ impl CwClient for CliClient { chain_id: &Id, gas: u64, sender: &str, - msgs: impl Iterator + Send + Sync, + msgs: impl Iterator + Send + Sync, pay_amount: &str, ) -> Result { let gas_amount = match gas { diff --git a/crates/utils/cw-client/src/grpc.rs b/crates/utils/cw-client/src/grpc.rs index 91e901e4..06cb43a0 100644 --- a/crates/utils/cw-client/src/grpc.rs +++ b/crates/utils/cw-client/src/grpc.rs @@ -99,7 +99,7 @@ impl CwClient for GrpcClient { chain_id: &TmChainId, gas: u64, _sender: &str, - msgs: impl Iterator + Send + Sync, + msgs: impl Iterator + Send + Sync, pay_amount: &str, ) -> Result { let tm_pubkey = self.sk.public_key(); @@ -107,14 +107,18 @@ impl CwClient for GrpcClient { .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 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 diff --git a/crates/utils/cw-client/src/lib.rs b/crates/utils/cw-client/src/lib.rs index 2ccc8e43..2911e0f7 100644 --- a/crates/utils/cw-client/src/lib.rs +++ b/crates/utils/cw-client/src/lib.rs @@ -36,7 +36,7 @@ pub trait CwClient { chain_id: &Id, gas: u64, sender: &str, - msgs: impl Iterator + Send + Sync, + msgs: impl Iterator + Send + Sync, pay_amount: &str, ) -> Result; From e24972607eccde1c20e91364627d4fa77ba4b092 Mon Sep 17 00:00:00 2001 From: Shoaib Ahmed Date: Fri, 19 Sep 2025 15:28:25 +0400 Subject: [PATCH 03/13] Add CwClient::tx_simulate and impl for GrpcClient --- crates/utils/cw-client/src/cli.rs | 14 +++++++- crates/utils/cw-client/src/grpc.rs | 54 ++++++++++++++++++++++++++++++ crates/utils/cw-client/src/lib.rs | 11 ++++++ 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/crates/utils/cw-client/src/cli.rs b/crates/utils/cw-client/src/cli.rs index 20065fdb..883831c9 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; @@ -187,6 +187,18 @@ 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 { + unimplemented!() + } + 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 cf32dac3..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}, @@ -147,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 2911e0f7..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; @@ -40,6 +41,16 @@ pub trait CwClient { 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, From 309e7ab9ab6537b9d4a15420860dfb1dc4342778 Mon Sep 17 00:00:00 2001 From: Shoaib Ahmed Date: Fri, 19 Sep 2025 15:33:22 +0400 Subject: [PATCH 04/13] Add ChainClient::simulate_tx --- crates/enclave/core/src/chain_client.rs | 22 +++++++++++++++++ .../enclave/core/src/chain_client/default.rs | 24 ++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/crates/enclave/core/src/chain_client.rs b/crates/enclave/core/src/chain_client.rs index c15619f2..2af7d09c 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. @@ -77,6 +80,25 @@ pub trait ChainClient: Send + Sync + 'static { config: Self::TxConfig, ) -> 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 9b09f272..41ce2d47 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}; @@ -151,6 +151,28 @@ impl ChainClient for DefaultChainClient { .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 + } + async fn wait_for_blocks(&self, blocks: u8) -> Result<(), Self::Error> { debug!("Waiting for {} blocks", blocks); let (client, driver) = WebSocketClient::new(self.ws_url.to_string().as_str()).await?; From 3fd1b9b2481e217e86fc7a36c9adb5df141c0b0d Mon Sep 17 00:00:00 2001 From: Shoaib Ahmed Date: Fri, 19 Sep 2025 15:34:52 +0400 Subject: [PATCH 05/13] Make all DefaultChainClient fields public --- crates/enclave/core/src/chain_client/default.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/enclave/core/src/chain_client/default.rs b/crates/enclave/core/src/chain_client/default.rs index 41ce2d47..b660f228 100644 --- a/crates/enclave/core/src/chain_client/default.rs +++ b/crates/enclave/core/src/chain_client/default.rs @@ -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 { From 32ad47988e19cb132729985d49c09d95a3fcec7e Mon Sep 17 00:00:00 2001 From: Shoaib Ahmed Date: Fri, 19 Sep 2025 15:35:53 +0400 Subject: [PATCH 06/13] Remove assoc type TxConfig and use concrete type --- crates/enclave/core/src/chain_client.rs | 6 ++---- crates/enclave/core/src/chain_client/default.rs | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/enclave/core/src/chain_client.rs b/crates/enclave/core/src/chain_client.rs index 2af7d09c..999a7e95 100644 --- a/crates/enclave/core/src/chain_client.rs +++ b/crates/enclave/core/src/chain_client.rs @@ -22,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; @@ -66,7 +64,7 @@ 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 @@ -77,7 +75,7 @@ pub trait ChainClient: Send + Sync + 'static { &self, contract: &Self::Contract, msgs: impl Iterator + Send + Sync, - config: Self::TxConfig, + config: DefaultTxConfig, ) -> Result; /// Simulates a transaction returning the gas_info. diff --git a/crates/enclave/core/src/chain_client/default.rs b/crates/enclave/core/src/chain_client/default.rs index b660f228..80cec23a 100644 --- a/crates/enclave/core/src/chain_client/default.rs +++ b/crates/enclave/core/src/chain_client/default.rs @@ -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( @@ -133,7 +132,7 @@ impl ChainClient for DefaultChainClient { &self, contract: &Self::Contract, msgs: impl Iterator + Send + Sync, - config: Self::TxConfig, + config: DefaultTxConfig, ) -> Result { debug!( "Sending transaction to contract {contract} with gas {}", From 553701468b1ba7ebbff9ac655aa43c79b7b0385a Mon Sep 17 00:00:00 2001 From: Shoaib Ahmed Date: Fri, 19 Sep 2025 15:36:09 +0400 Subject: [PATCH 07/13] Define GasProvider --- crates/enclave/core/src/host.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/enclave/core/src/host.rs b/crates/enclave/core/src/host.rs index 8608cf01..26345b35 100644 --- a/crates/enclave/core/src/host.rs +++ b/crates/enclave/core/src/host.rs @@ -316,3 +316,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; +} From fa593d8446f58ef139bca8398ba6e56632c744b3 Mon Sep 17 00:00:00 2001 From: Shoaib Ahmed Date: Fri, 19 Sep 2025 15:36:48 +0400 Subject: [PATCH 08/13] Use GasProvider instead of gas_fn in host --- crates/enclave/core/src/host.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/enclave/core/src/host.rs b/crates/enclave/core/src/host.rs index 26345b35..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, @@ -160,7 +163,7 @@ where <>::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; @@ -281,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}"), From 8b0e67e3295984bef92a76e8de49b21cd550aeac Mon Sep 17 00:00:00 2001 From: Shoaib Ahmed Date: Fri, 19 Sep 2025 17:02:05 +0400 Subject: [PATCH 09/13] Update examples to use new API --- examples/pingpong/enclave/src/main.rs | 38 +++++++++++++++++------- examples/transfers/enclave/src/main.rs | 41 +++++++++++++++++--------- 2 files changed, 54 insertions(+), 25 deletions(-) diff --git a/examples/pingpong/enclave/src/main.rs b/examples/pingpong/enclave/src/main.rs index 5cf3ada0..c21e4c52 100644 --- a/examples/pingpong/enclave/src/main.rs +++ b/examples/pingpong/enclave/src/main.rs @@ -19,19 +19,23 @@ 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, }, }; use crate::{ event::EnclaveEvent, - request::{EnclaveMsg, EnclaveRequest, EnclaveResponse}, + request::{EnclaveRequest, EnclaveResponse}, }; #[tokio::main(flavor = "current_thread")] @@ -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,14 +108,26 @@ async fn main() -> Result<(), Box> { Ok(()) } -fn gas_fn(response: &EnclaveResponse) -> DefaultTxConfig { - let response = response.clone().collect::>(); - if matches!(response.first(), Some(EnclaveMsg::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 { + gas: gas_info.gas_used, + amount: "11000untrn".to_string(), + }) } } diff --git a/examples/transfers/enclave/src/main.rs b/examples/transfers/enclave/src/main.rs index 19ebc2c2..6f868470 100644 --- a/examples/transfers/enclave/src/main.rs +++ b/examples/transfers/enclave/src/main.rs @@ -20,19 +20,23 @@ 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, }, }; use crate::{ event::EnclaveEvent, - request::{EnclaveMsg, EnclaveRequest, EnclaveResponse}, + request::{EnclaveRequest, EnclaveResponse}, state::{AppCtx, AppEnclave}, }; @@ -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,17 +113,26 @@ async fn main() -> Result<(), Box> { Ok(()) } -fn gas_fn(response: &EnclaveResponse) -> DefaultTxConfig { - let response = response.clone().collect::>(); - if matches!( - response.first(), - Some(EnclaveMsg::Update(_)) | Some(EnclaveMsg::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 { + gas: gas_info.gas_used, + amount: "11000untrn".to_string(), + }) } } From fcaa4f9135999ee4dbfc7e4ebe9b6a58e9b8e427 Mon Sep 17 00:00:00 2001 From: Shoaib Ahmed Date: Thu, 2 Oct 2025 20:54:41 +0400 Subject: [PATCH 10/13] Impl tx_simulate for CliClient --- crates/utils/cw-client/src/cli.rs | 45 ++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/crates/utils/cw-client/src/cli.rs b/crates/utils/cw-client/src/cli.rs index 883831c9..84df27ee 100644 --- a/crates/utils/cw-client/src/cli.rs +++ b/crates/utils/cw-client/src/cli.rs @@ -189,14 +189,45 @@ impl CwClient for CliClient { async fn tx_simulate( &self, - _contract: &Self::Address, - _chain_id: &Id, - _gas: u64, - _sender: &str, - _msgs: impl Iterator + Send + Sync, - _pay_amount: &str, + contract: &Self::Address, + chain_id: &Id, + gas: u64, + sender: &str, + msgs: impl Iterator + Send + Sync, + pay_amount: &str, ) -> std::result::Result { - unimplemented!() + 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( From d249d26dd91e1f1eef0672d01c0e563433d9b0ca Mon Sep 17 00:00:00 2001 From: Shoaib Ahmed Date: Thu, 2 Oct 2025 21:05:04 +0400 Subject: [PATCH 11/13] Impl helpers for gas adjustments --- .../enclave/core/src/chain_client/default.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/enclave/core/src/chain_client/default.rs b/crates/enclave/core/src/chain_client/default.rs index 80cec23a..478fa4de 100644 --- a/crates/enclave/core/src/chain_client/default.rs +++ b/crates/enclave/core/src/chain_client/default.rs @@ -206,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: u64, denom: &str) -> Self { + let gas = scale_gas(gas_used, multiplier); + let amount_num = 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 + } +} From 07e329f79afb108ed7bb20547b9b6011fed75fcc Mon Sep 17 00:00:00 2001 From: Shoaib Ahmed Date: Thu, 2 Oct 2025 21:13:55 +0400 Subject: [PATCH 12/13] Fix gas calc --- crates/enclave/core/src/chain_client/default.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/enclave/core/src/chain_client/default.rs b/crates/enclave/core/src/chain_client/default.rs index 478fa4de..bdceaebb 100644 --- a/crates/enclave/core/src/chain_client/default.rs +++ b/crates/enclave/core/src/chain_client/default.rs @@ -208,9 +208,9 @@ pub struct DefaultTxConfig { } impl DefaultTxConfig { - pub fn new(gas_used: u64, multiplier: f64, base_gas_price: u64, denom: &str) -> Self { + 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 = gas * base_gas_price; + let amount_num = scale_gas(gas, base_gas_price); Self { gas, amount: format!("{amount_num}{denom}"), From df011af1292836896e2f4b87bd70bdfec350336b Mon Sep 17 00:00:00 2001 From: Shoaib Ahmed Date: Thu, 2 Oct 2025 21:14:10 +0400 Subject: [PATCH 13/13] Use helpers for gas calc --- examples/pingpong/enclave/src/main.rs | 10 ++++++---- examples/transfers/enclave/src/main.rs | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/examples/pingpong/enclave/src/main.rs b/examples/pingpong/enclave/src/main.rs index c21e4c52..adea9b06 100644 --- a/examples/pingpong/enclave/src/main.rs +++ b/examples/pingpong/enclave/src/main.rs @@ -125,9 +125,11 @@ impl GasProvider for GasSimulator { let gas_info = chain_client .simulate_tx(contract, tx.as_slice().iter(), default_config) .await?; - Ok(DefaultTxConfig { - gas: gas_info.gas_used, - amount: "11000untrn".to_string(), - }) + Ok(DefaultTxConfig::new( + gas_info.gas_used, + 1.3, + 0.0053, + "untrn", + )) } } diff --git a/examples/transfers/enclave/src/main.rs b/examples/transfers/enclave/src/main.rs index 6f868470..123bd28d 100644 --- a/examples/transfers/enclave/src/main.rs +++ b/examples/transfers/enclave/src/main.rs @@ -130,9 +130,11 @@ impl GasProvider for GasSimulator { let gas_info = chain_client .simulate_tx(contract, tx.as_slice().iter(), default_config) .await?; - Ok(DefaultTxConfig { - gas: gas_info.gas_used, - amount: "11000untrn".to_string(), - }) + Ok(DefaultTxConfig::new( + gas_info.gas_used, + 1.3, + 0.0053, + "untrn", + )) } }