use std::{error::Error, fs::File, io::Read, path::PathBuf}; use anyhow::anyhow; use cosmos_sdk_proto::{ cosmos::{ auth::v1beta1::{ query_client::QueryClient as AuthQueryClient, BaseAccount as RawBaseAccount, QueryAccountRequest, }, tx::v1beta1::{ service_client::ServiceClient, BroadcastMode, BroadcastTxRequest, BroadcastTxResponse, }, }, cosmwasm::wasm::v1::{ query_client::QueryClient as WasmdQueryClient, QuerySmartContractStateRequest, }, traits::Message, Any, }; use cosmrs::{ auth::BaseAccount, cosmwasm::MsgExecuteContract, crypto::{secp256k1::SigningKey, PublicKey}, tendermint::chain::Id as TmChainId, tx, tx::{Fee, Msg, SignDoc, SignerInfo}, AccountId, Coin, }; use reqwest::Url; use serde::de::DeserializeOwned; use crate::CwClient; #[derive(Clone, Debug)] pub struct GrpcClient { sk_file: PathBuf, url: Url, } impl GrpcClient { pub fn new(sk_file: PathBuf, url: Url) -> Self { Self { sk_file, url } } } #[async_trait::async_trait] impl CwClient for GrpcClient { type Address = AccountId; type Query = serde_json::Value; type RawQuery = String; type ChainId = TmChainId; type Error = anyhow::Error; async fn query_smart( &self, contract: &Self::Address, query: Self::Query, ) -> Result { let mut client = WasmdQueryClient::connect(self.url.to_string()).await?; let raw_query_request = QuerySmartContractStateRequest { address: contract.to_string(), query_data: query.to_string().into_bytes(), }; let raw_query_response = client.smart_contract_state(raw_query_request).await?; let raw_value = raw_query_response.into_inner().data; serde_json::from_slice(&raw_value) .map_err(|e| anyhow!("failed to deserialize JSON reponse: {}", e)) } fn query_raw( &self, _contract: &Self::Address, _query: Self::RawQuery, ) -> Result { unimplemented!() } fn query_tx(&self, _txhash: &str) -> Result { unimplemented!() } async fn tx_execute( &self, contract: &Self::Address, chain_id: &TmChainId, gas: u64, _sender: &str, msg: M, _fees: &str, ) -> Result { let secret = { let mut secret_hex = String::new(); let mut sk_file = File::open(self.sk_file.clone())?; sk_file.read_to_string(&mut secret_hex)?; let secret = hex::decode(secret_hex)?; SigningKey::from_slice(&secret) .map_err(|e| anyhow!("failed to read secret key: {}", e))? }; let tm_pubkey = secret.public_key(); let sender = tm_pubkey .account_id("neutron") .map_err(|e| anyhow!("failed to create AccountId from pubkey: {}", e))?; let msgs = vec![MsgExecuteContract { sender: sender.clone(), contract: contract.clone(), msg: msg.to_string().into_bytes(), funds: vec![], } .to_any() .unwrap()]; let account = account_info(self.url.to_string(), sender.to_string()) .await .map_err(|e| anyhow!("error querying account info: {}", e))?; let amount = Coin { amount: 11000u128, denom: "untrn".parse().expect("hardcoded denom"), }; let tx_bytes = tx_bytes( &secret, amount, gas, tm_pubkey, msgs, account.sequence, account.account_number, chain_id, ) .map_err(|e| anyhow!("failed to create msg/tx: {}", e))?; let response = send_tx(self.url.to_string(), tx_bytes) .await .map_err(|e| anyhow!("failed to send tx: {}", e))?; println!("{response:?}"); Ok(response .tx_response .map(|tx_response| tx_response.txhash) .unwrap_or_default()) } fn deploy( &self, _chain_id: &TmChainId, _sender: &str, _wasm_path: M, ) -> Result { unimplemented!() } fn init( &self, _chain_id: &TmChainId, _sender: &str, _code_id: u64, _init_msg: M, _label: &str, ) -> Result { unimplemented!() } fn trusted_height_hash(&self) -> Result<(u64, String), Self::Error> { unimplemented!() } } pub async fn account_info( node: impl ToString, address: impl ToString, ) -> Result> { let mut client = AuthQueryClient::connect(node.to_string()).await?; let request = tonic::Request::new(QueryAccountRequest { address: address.to_string(), }); let response = client.account(request).await?; let response = RawBaseAccount::decode(response.into_inner().account.unwrap().value.as_slice())?; let account = BaseAccount::try_from(response)?; Ok(account) } #[allow(clippy::too_many_arguments)] pub fn tx_bytes( secret: &SigningKey, amount: Coin, gas: u64, tm_pubkey: PublicKey, msgs: Vec, sequence_number: u64, account_number: u64, chain_id: &TmChainId, ) -> Result, Box> { let tx_body = tx::Body::new(msgs, "", 0u16); let signer_info = SignerInfo::single_direct(Some(tm_pubkey), sequence_number); let auth_info = signer_info.auth_info(Fee::from_amount_and_gas(amount, gas)); let sign_doc = SignDoc::new(&tx_body, &auth_info, chain_id, account_number)?; let tx_signed = sign_doc.sign(secret)?; Ok(tx_signed.to_bytes()?) } pub async fn send_tx( node: impl ToString, tx_bytes: Vec, ) -> Result> { let mut client = ServiceClient::connect(node.to_string()).await?; let request = tonic::Request::new(BroadcastTxRequest { tx_bytes, mode: BroadcastMode::Sync.into(), }); let tx_response = client.broadcast_tx(request).await?; Ok(tx_response.into_inner()) } #[cfg(test)] mod tests { use std::error::Error; use serde_json::json; use transfers_contract::msg::{execute::Request, QueryMsg::GetRequests}; use crate::{CwClient, GrpcClient}; #[tokio::test] #[ignore] async fn test_query() -> Result<(), Box> { let sk_file = "../data/admin.sk".parse().unwrap(); let url = "https://grpc-falcron.pion-1.ntrn.tech:80".parse().unwrap(); let contract = "neutron15ruzx9wvrupt9cffzsp6868uad2svhfym2nsgxm2skpeqr3qrd4q4uwk83" .parse() .unwrap(); let cw_client = GrpcClient::new(sk_file, url); let resp: Vec = cw_client .query_smart(&contract, json!(GetRequests {})) .await?; println!("{resp:?}"); Ok(()) } #[tokio::test] #[ignore] async fn test_execute() -> Result<(), Box> { let sk_file = "data/admin.sk".parse().unwrap(); let url = "https://grpc-falcron.pion-1.ntrn.tech:80".parse().unwrap(); let contract = "neutron15ruzx9wvrupt9cffzsp6868uad2svhfym2nsgxm2skpeqr3qrd4q4uwk83" .parse() .unwrap(); let chain_id = "pion-1".parse().unwrap(); let cw_client = GrpcClient::new(sk_file, url); let tx_hash = cw_client .tx_execute( &contract, &chain_id, 2000000, "/* unused since we're getting the account from the sk */", json!([]), "11000untrn", ) .await?; println!("{}", tx_hash); Ok(()) } }