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 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 let s = "340282366920938463463374607431768211456overflow";
286 assert!(parse_coin(s).is_err());
287 }
288
289 #[test]
290 fn error_negative_amount() {
291 assert!(parse_coin("-100untrn").is_err());
293 }
294}