cw_client/
grpc.rs

1use std::error::Error;
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, Coin,
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 = Coin {
123            amount: 11000u128,
124            denom: "untrn".parse().expect("hardcoded denom"),
125        };
126        let tx_bytes = tx_bytes(
127            &self.sk,
128            amount,
129            gas,
130            tm_pubkey,
131            msgs,
132            account.sequence,
133            account.account_number,
134            chain_id,
135        )
136        .map_err(|e| anyhow!("failed to create msg/tx: {}", e))?;
137
138        let response = send_tx(self.url.to_string(), tx_bytes)
139            .await
140            .map_err(|e| anyhow!("failed to send tx: {}", e))?;
141        println!("{response:?}");
142        Ok(response
143            .tx_response
144            .map(|tx_response| tx_response.txhash)
145            .unwrap_or_default())
146    }
147
148    fn deploy<M: ToString>(
149        &self,
150        _chain_id: &TmChainId,
151        _sender: &str,
152        _wasm_path: M,
153    ) -> Result<String, Self::Error> {
154        unimplemented!()
155    }
156
157    fn init<M: ToString>(
158        &self,
159        _chain_id: &TmChainId,
160        _sender: &str,
161        _code_id: u64,
162        _init_msg: M,
163        _label: &str,
164    ) -> Result<String, Self::Error> {
165        unimplemented!()
166    }
167
168    fn trusted_height_hash(&self) -> Result<(u64, String), Self::Error> {
169        unimplemented!()
170    }
171}
172
173pub async fn account_info(
174    node: impl ToString,
175    address: impl ToString,
176) -> Result<BaseAccount, Box<dyn Error>> {
177    let mut client = AuthQueryClient::connect(node.to_string()).await?;
178    let request = tonic::Request::new(QueryAccountRequest {
179        address: address.to_string(),
180    });
181    let response = client.account(request).await?;
182    let response = RawBaseAccount::decode(response.into_inner().account.unwrap().value.as_slice())?;
183    let account = BaseAccount::try_from(response)?;
184    Ok(account)
185}
186
187#[allow(clippy::too_many_arguments)]
188pub fn tx_bytes(
189    secret: &SigningKey,
190    amount: Coin,
191    gas: u64,
192    tm_pubkey: PublicKey,
193    msgs: Vec<Any>,
194    sequence_number: u64,
195    account_number: u64,
196    chain_id: &TmChainId,
197) -> Result<Vec<u8>, Box<dyn Error>> {
198    let tx_body = tx::Body::new(msgs, "", 0u16);
199    let signer_info = SignerInfo::single_direct(Some(tm_pubkey), sequence_number);
200    let auth_info = signer_info.auth_info(Fee::from_amount_and_gas(amount, gas));
201    let sign_doc = SignDoc::new(&tx_body, &auth_info, chain_id, account_number)?;
202    let tx_signed = sign_doc.sign(secret)?;
203    Ok(tx_signed.to_bytes()?)
204}
205
206pub async fn send_tx(
207    node: impl ToString,
208    tx_bytes: Vec<u8>,
209) -> Result<BroadcastTxResponse, Box<dyn Error>> {
210    let mut client = ServiceClient::connect(node.to_string()).await?;
211    let request = tonic::Request::new(BroadcastTxRequest {
212        tx_bytes,
213        mode: BroadcastMode::Sync.into(),
214    });
215    let tx_response = client.broadcast_tx(request).await?;
216    Ok(tx_response.into_inner())
217}