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