cw_client/
grpc.rs

1use std::{error::Error, str::FromStr};
2
3use anyhow::anyhow;
4use cosmos_sdk_proto::{
5    cosmos::{
6        auth::v1beta1::{
7            query_client::QueryClient as AuthQueryClient, BaseAccount as RawBaseAccount,
8            QueryAccountRequest,
9        },
10        tx::v1beta1::{
11            service_client::ServiceClient, BroadcastMode, BroadcastTxRequest, BroadcastTxResponse,
12            SimulateRequest, SimulateResponse,
13        },
14    },
15    cosmwasm::wasm::v1::{
16        query_client::QueryClient as WasmdQueryClient, QueryRawContractStateRequest,
17        QuerySmartContractStateRequest,
18    },
19    traits::Message,
20    Any,
21};
22use cosmrs::{
23    abci::GasInfo,
24    auth::BaseAccount,
25    cosmwasm::MsgExecuteContract,
26    crypto::{secp256k1::SigningKey, PublicKey},
27    tendermint::chain::Id as TmChainId,
28    tx,
29    tx::{Fee, Msg, SignDoc, SignerInfo},
30    AccountId, Amount, Coin, Denom,
31};
32use reqwest::Url;
33use serde::de::DeserializeOwned;
34
35use crate::CwClient;
36
37pub struct GrpcClient {
38    sk: SigningKey,
39    url: Url,
40}
41
42impl GrpcClient {
43    pub fn new(sk: SigningKey, url: Url) -> Self {
44        Self { sk, url }
45    }
46}
47
48#[async_trait::async_trait]
49impl CwClient for GrpcClient {
50    type Address = AccountId;
51    type Query = serde_json::Value;
52    type RawQuery = String;
53    type ChainId = TmChainId;
54    type Error = anyhow::Error;
55
56    async fn query_smart<R: DeserializeOwned + Send>(
57        &self,
58        contract: &Self::Address,
59        query: Self::Query,
60    ) -> Result<R, Self::Error> {
61        let mut client = WasmdQueryClient::connect(self.url.to_string()).await?;
62
63        let raw_query_request = QuerySmartContractStateRequest {
64            address: contract.to_string(),
65            query_data: query.to_string().into_bytes(),
66        };
67
68        let raw_query_response = client.smart_contract_state(raw_query_request).await?;
69
70        let raw_value = raw_query_response.into_inner().data;
71        serde_json::from_slice(&raw_value)
72            .map_err(|e| anyhow!("failed to deserialize JSON reponse: {}", e))
73    }
74
75    async fn query_raw<R: DeserializeOwned + Default>(
76        &self,
77        contract: &Self::Address,
78        query: Self::RawQuery,
79    ) -> Result<R, Self::Error> {
80        let mut client = WasmdQueryClient::connect(self.url.to_string()).await?;
81
82        let raw_query_request = QueryRawContractStateRequest {
83            address: contract.to_string(),
84            query_data: query.to_string().into_bytes(),
85        };
86
87        let raw_query_response = client.raw_contract_state(raw_query_request).await?;
88
89        let raw_value = raw_query_response.into_inner().data;
90        serde_json::from_slice(&raw_value)
91            .map_err(|e| anyhow!("failed to deserialize JSON reponse: {}", e))
92    }
93
94    fn query_tx<R: DeserializeOwned + Default>(&self, _txhash: &str) -> Result<R, Self::Error> {
95        unimplemented!()
96    }
97
98    async fn tx_execute<M: ToString>(
99        &self,
100        contract: &Self::Address,
101        chain_id: &TmChainId,
102        gas: u64,
103        _sender: &str,
104        msgs: impl Iterator<Item = M> + Send + Sync,
105        pay_amount: &str,
106    ) -> Result<String, Self::Error> {
107        let tm_pubkey = self.sk.public_key();
108        let sender = tm_pubkey
109            .account_id("neutron")
110            .map_err(|e| anyhow!("failed to create AccountId from pubkey: {}", e))?;
111
112        let msgs = msgs
113            .map(|msg| {
114                MsgExecuteContract {
115                    sender: sender.clone(),
116                    contract: contract.clone(),
117                    msg: msg.to_string().into_bytes(),
118                    funds: vec![],
119                }
120                .to_any()
121                .unwrap()
122            })
123            .collect();
124
125        let account = account_info(self.url.to_string(), sender.to_string())
126            .await
127            .map_err(|e| anyhow!("error querying account info: {}", e))?;
128        let amount = parse_coin(pay_amount)?;
129        let tx_bytes = tx_bytes(
130            &self.sk,
131            amount,
132            gas,
133            tm_pubkey,
134            msgs,
135            account.sequence,
136            account.account_number,
137            chain_id,
138        )
139        .map_err(|e| anyhow!("failed to create msg/tx: {}", e))?;
140
141        let response = send_tx(self.url.to_string(), tx_bytes)
142            .await
143            .map_err(|e| anyhow!("failed to send tx: {}", e))?;
144        println!("{response:?}");
145        Ok(response
146            .tx_response
147            .map(|tx_response| tx_response.txhash)
148            .unwrap_or_default())
149    }
150
151    async fn tx_simulate<M: ToString>(
152        &self,
153        contract: &Self::Address,
154        chain_id: &TmChainId,
155        gas: u64,
156        _sender: &str,
157        msgs: impl Iterator<Item = M> + Send + Sync,
158        pay_amount: &str,
159    ) -> Result<GasInfo, Self::Error> {
160        let tm_pubkey = self.sk.public_key();
161        let sender = tm_pubkey
162            .account_id("neutron")
163            .map_err(|e| anyhow!("failed to create AccountId from pubkey: {}", e))?;
164
165        let msgs = msgs
166            .map(|msg| {
167                MsgExecuteContract {
168                    sender: sender.clone(),
169                    contract: contract.clone(),
170                    msg: msg.to_string().into_bytes(),
171                    funds: vec![],
172                }
173                .to_any()
174                .unwrap()
175            })
176            .collect();
177
178        let account = account_info(self.url.to_string(), sender.to_string())
179            .await
180            .map_err(|e| anyhow!("error querying account info: {}", e))?;
181        let amount = parse_coin(pay_amount)?;
182        let tx_bytes = tx_bytes(
183            &self.sk,
184            amount,
185            gas,
186            tm_pubkey,
187            msgs,
188            account.sequence,
189            account.account_number,
190            chain_id,
191        )
192        .map_err(|e| anyhow!("failed to create msg/tx: {}", e))?;
193
194        let response = simulate_tx(self.url.to_string(), tx_bytes)
195            .await
196            .map_err(|e| anyhow!("failed to simulate tx: {}", e))?;
197        response
198            .gas_info
199            .expect("missing gas info from tx_simulate response")
200            .try_into()
201            .map_err(|e| anyhow!("failed to simulate tx: {}", e))
202    }
203
204    fn deploy<M: ToString>(
205        &self,
206        _chain_id: &TmChainId,
207        _sender: &str,
208        _wasm_path: M,
209    ) -> Result<String, Self::Error> {
210        unimplemented!()
211    }
212
213    fn init<M: ToString>(
214        &self,
215        _chain_id: &TmChainId,
216        _sender: &str,
217        _admin: Option<&str>,
218        _code_id: u64,
219        _init_msg: M,
220        _label: &str,
221    ) -> Result<String, Self::Error> {
222        unimplemented!()
223    }
224
225    fn trusted_height_hash(&self) -> Result<(u64, String), Self::Error> {
226        unimplemented!()
227    }
228}
229
230pub async fn account_info(
231    node: impl ToString,
232    address: impl ToString,
233) -> Result<BaseAccount, Box<dyn Error>> {
234    let mut client = AuthQueryClient::connect(node.to_string()).await?;
235    let request = tonic::Request::new(QueryAccountRequest {
236        address: address.to_string(),
237    });
238    let response = client.account(request).await?;
239    let response = RawBaseAccount::decode(response.into_inner().account.unwrap().value.as_slice())?;
240    let account = BaseAccount::try_from(response)?;
241    Ok(account)
242}
243
244#[allow(clippy::too_many_arguments)]
245pub fn tx_bytes(
246    secret: &SigningKey,
247    amount: Coin,
248    gas: u64,
249    tm_pubkey: PublicKey,
250    msgs: Vec<Any>,
251    sequence_number: u64,
252    account_number: u64,
253    chain_id: &TmChainId,
254) -> Result<Vec<u8>, Box<dyn Error>> {
255    let tx_body = tx::Body::new(msgs, "", 0u16);
256    let signer_info = SignerInfo::single_direct(Some(tm_pubkey), sequence_number);
257    let auth_info = signer_info.auth_info(Fee::from_amount_and_gas(amount, gas));
258    let sign_doc = SignDoc::new(&tx_body, &auth_info, chain_id, account_number)?;
259    let tx_signed = sign_doc.sign(secret)?;
260    Ok(tx_signed.to_bytes()?)
261}
262
263pub async fn send_tx(
264    node: impl ToString,
265    tx_bytes: Vec<u8>,
266) -> Result<BroadcastTxResponse, Box<dyn Error>> {
267    let mut client = ServiceClient::connect(node.to_string()).await?;
268    let request = tonic::Request::new(BroadcastTxRequest {
269        tx_bytes,
270        mode: BroadcastMode::Sync.into(),
271    });
272    let tx_response = client.broadcast_tx(request).await?;
273    Ok(tx_response.into_inner())
274}
275
276pub async fn simulate_tx(
277    node: impl ToString,
278    tx_bytes: Vec<u8>,
279) -> Result<SimulateResponse, Box<dyn Error>> {
280    let mut client = ServiceClient::connect(node.to_string()).await?;
281    #[allow(deprecated)] // must provide the Tx as None
282    let request = tonic::Request::new(SimulateRequest { tx: None, tx_bytes });
283    let simulate_response = client.simulate(request).await?;
284    Ok(simulate_response.into_inner())
285}
286
287pub fn parse_coin(input: &str) -> anyhow::Result<Coin> {
288    let split_at = input
289        .find(|c: char| !c.is_ascii_digit())
290        .ok_or(anyhow!("invalid coin format: missing denomination"))?;
291    let (amt_str, denom_str) = input.split_at(split_at);
292
293    let amount: Amount = amt_str.parse()?;
294    let denom: Denom = Denom::from_str(denom_str).map_err(|e| anyhow!("invalid denom: {e}"))?;
295
296    Ok(Coin { denom, amount })
297}
298
299#[cfg(test)]
300mod tests {
301    use std::str::FromStr;
302
303    use super::*;
304
305    #[test]
306    fn parse_valid_basic() {
307        let coin = parse_coin("11000untrn").unwrap();
308        assert_eq!(coin.amount, 11_000);
309        assert_eq!(coin.denom, Denom::from_str("untrn").unwrap());
310    }
311
312    #[test]
313    fn parse_leading_zeros() {
314        let coin = parse_coin("000123abc").unwrap();
315        assert_eq!(coin.amount, 123);
316        assert_eq!(coin.denom, Denom::from_str("abc").unwrap());
317    }
318
319    #[test]
320    fn parse_zero_amount() {
321        let coin = parse_coin("0xyz").unwrap();
322        assert_eq!(coin.amount, 0);
323        assert_eq!(coin.denom, Denom::from_str("xyz").unwrap());
324    }
325
326    #[test]
327    fn parse_denom_with_digits() {
328        let coin = parse_coin("10token123").unwrap();
329        assert_eq!(coin.amount, 10);
330        assert_eq!(coin.denom, Denom::from_str("token123").unwrap());
331    }
332
333    #[test]
334    fn parse_max_u128_amount() {
335        // u128::MAX = 340282366920938463463374607431768211455
336        let s = "340282366920938463463374607431768211455max";
337        let coin = parse_coin(s).unwrap();
338        assert_eq!(coin.amount, u128::MAX);
339        assert_eq!(coin.denom, Denom::from_str("max").unwrap());
340    }
341
342    #[test]
343    fn error_missing_denom() {
344        assert!(parse_coin("123").is_err());
345    }
346
347    #[test]
348    fn error_missing_amount() {
349        assert!(parse_coin("abc").is_err());
350    }
351
352    #[test]
353    fn error_overflow_amount() {
354        // one more than u128::MAX
355        let s = "340282366920938463463374607431768211456overflow";
356        assert!(parse_coin(s).is_err());
357    }
358
359    #[test]
360    fn error_negative_amount() {
361        // '-' is non-digit at pos 0 → empty amount → parse error
362        assert!(parse_coin("-100untrn").is_err());
363    }
364}