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)] 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 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 let s = "340282366920938463463374607431768211456overflow";
356 assert!(parse_coin(s).is_err());
357 }
358
359 #[test]
360 fn error_negative_amount() {
361 assert!(parse_coin("-100untrn").is_err());
363 }
364}