1use crate::cosmos_modules::{self, auth::BaseAccount};
2use cosmrs::{
3 bank::MsgSend,
4 crypto::secp256k1::SigningKey,
5 tendermint::chain::Id,
6 tx::{self, Fee, Msg, Raw, SignDoc, SignerInfo},
7 AccountId, Any, Coin,
8};
9use prost::Message;
10use secp256k1::{All, Context, Secp256k1, Signing};
11
12use std::{convert::TryFrom, env, rc::Rc, str::FromStr, time::Duration};
13use tokio::time::sleep;
14use tonic::transport::Channel;
15
16use crate::{
17 error::CosmScriptError, keys::private::PrivateKey, CosmTxResponse, Deployment, Network,
18};
19
20const GAS_LIMIT: u64 = 1_000_000;
21const GAS_BUFFER: f64 = 1.2;
22
23pub type Wallet<'a> = &'a Rc<Sender<All>>;
24
25pub struct Sender<C: Signing + Context> {
26 pub private_key: SigningKey,
27 pub secp: Secp256k1<C>,
28 network: Network,
29 channel: Channel,
30}
31
32impl<C: Signing + Context> Sender<C> {
33 pub fn new(config: Deployment, secp: Secp256k1<C>) -> Result<Sender<C>, CosmScriptError> {
34 let mut composite_name = config.network.kind.mnemonic_name().to_string();
36 composite_name.push('_');
37 composite_name.push_str(&config.name.to_ascii_uppercase());
38
39 let p_key: PrivateKey = if let Some(mnemonic) = env::var_os(&composite_name) {
41 PrivateKey::from_words(
42 &secp,
43 mnemonic.to_str().unwrap(),
44 0,
45 0,
46 config.network.chain.coin_type,
47 )?
48 } else {
49 log::debug!("{}", config.network.kind.mnemonic_name());
50 let mnemonic = env::var(config.network.kind.mnemonic_name())?;
51 PrivateKey::from_words(&secp, &mnemonic, 0, 0, config.network.chain.coin_type)?
52 };
53
54 let cosmos_private_key = SigningKey::from_bytes(&p_key.raw_key()).unwrap();
55
56 Ok(Sender {
57 channel: config.network.grpc_channel.clone(),
59 network: config.network,
60 private_key: cosmos_private_key,
61 secp,
62 })
63 }
64 pub fn pub_addr(&self) -> Result<AccountId, CosmScriptError> {
65 Ok(self
66 .private_key
67 .public_key()
68 .account_id(&self.network.chain.pub_addr_prefix)?)
69 }
70
71 pub fn pub_addr_str(&self) -> Result<String, CosmScriptError> {
72 Ok(self
73 .private_key
74 .public_key()
75 .account_id(&self.network.chain.pub_addr_prefix)?
76 .to_string())
77 }
78
79 pub async fn bank_send(
80 &self,
81 recipient: &str,
82 coins: Vec<Coin>,
83 ) -> Result<CosmTxResponse, CosmScriptError> {
84 let msg_send = MsgSend {
85 from_address: self.pub_addr()?,
86 to_address: AccountId::from_str(recipient)?,
87 amount: coins,
88 };
89
90 self.commit_tx(vec![msg_send], Some("sending tokens")).await
91 }
92
93 pub async fn commit_tx<T: Msg>(
94 &self,
95 msgs: Vec<T>,
96 memo: Option<&str>,
97 ) -> Result<CosmTxResponse, CosmScriptError> {
98 let timeout_height = 900124u32;
99 let msgs: Result<Vec<Any>, _> = msgs.into_iter().map(Msg::into_any).collect();
100 let msgs = msgs?;
101 let gas_denom = self.network.gas_denom.clone();
102 let amount = Coin {
103 amount: 0u8.into(),
104 denom: gas_denom.clone(),
105 };
106 let fee = Fee::from_amount_and_gas(amount, GAS_LIMIT);
107
108 let BaseAccount {
109 account_number,
110 sequence,
111 ..
112 } = self.base_account().await?;
113
114 let tx_body = tx::Body::new(msgs, memo.unwrap_or_default(), timeout_height);
115 let auth_info =
116 SignerInfo::single_direct(Some(self.private_key.public_key()), sequence).auth_info(fee);
117 let sign_doc = SignDoc::new(
118 &tx_body,
119 &auth_info,
120 &Id::try_from(self.network.id.clone())?,
121 account_number,
122 )?;
123 let tx_raw = sign_doc.sign(&self.private_key)?;
124
125 let sim_gas_used = self.simulate_tx(tx_raw.to_bytes()?).await?;
126
127 log::debug!("{:?}", sim_gas_used);
128
129 let gas_expected = sim_gas_used as f64 * GAS_BUFFER;
130 let amount_to_pay = gas_expected * self.network.gas_price;
131 let amount = Coin {
132 amount: (amount_to_pay as u64).into(),
133 denom: gas_denom,
134 };
135 let fee = Fee::from_amount_and_gas(amount, gas_expected as u64);
136 let auth_info =
138 SignerInfo::single_direct(Some(self.private_key.public_key()), sequence).auth_info(fee);
139 let sign_doc = SignDoc::new(
140 &tx_body,
141 &auth_info,
142 &Id::try_from(self.network.id.clone())?,
143 account_number,
144 )?;
145 let tx_raw = sign_doc.sign(&self.private_key)?;
146
147 self.broadcast(tx_raw).await
148 }
149
150 pub async fn base_account(&self) -> Result<BaseAccount, CosmScriptError> {
151 let addr = self.pub_addr().unwrap().to_string();
152
153 let mut client =
154 cosmos_sdk_proto::cosmos::auth::v1beta1::query_client::QueryClient::new(self.channel());
155
156 let resp = client
157 .account(cosmos_sdk_proto::cosmos::auth::v1beta1::QueryAccountRequest { address: addr })
158 .await?
159 .into_inner();
160
161 let acc: BaseAccount = BaseAccount::decode(resp.account.unwrap().value.as_ref()).unwrap();
162 Ok(acc)
163 }
164
165 pub async fn simulate_tx(&self, tx_bytes: Vec<u8>) -> Result<u64, CosmScriptError> {
166 let _addr = self.pub_addr().unwrap().to_string();
167
168 let mut client = cosmos_modules::tx::service_client::ServiceClient::new(self.channel());
169 #[allow(deprecated)]
170 let resp = client
171 .simulate(cosmos_modules::tx::SimulateRequest { tx: None, tx_bytes })
172 .await?
173 .into_inner();
174
175 let gas_used = resp.gas_info.unwrap().gas_used;
176 Ok(gas_used)
177 }
178
179 pub fn channel(&self) -> Channel {
180 self.channel.clone()
181 }
182
183 async fn broadcast(&self, tx: Raw) -> Result<CosmTxResponse, CosmScriptError> {
184 let mut client = cosmos_modules::tx::service_client::ServiceClient::new(self.channel());
185
186 let commit = client
187 .broadcast_tx(cosmos_modules::tx::BroadcastTxRequest {
188 tx_bytes: tx.to_bytes()?,
189 mode: cosmos_modules::tx::BroadcastMode::Sync.into(),
190 })
191 .await?;
192 log::debug!("{:?}", commit);
193
194 find_by_hash(&mut client, commit.into_inner().tx_response.unwrap().txhash).await
195 }
196}
197
198async fn find_by_hash(
199 client: &mut cosmos_modules::tx::service_client::ServiceClient<Channel>,
200 hash: String,
201) -> Result<CosmTxResponse, CosmScriptError> {
202 let attempts = 10;
203 let request = cosmos_modules::tx::GetTxRequest { hash };
204 for _ in 0..attempts {
205 if let Ok(tx) = client.get_tx(request.clone()).await {
206 let resp = tx.into_inner().tx_response.unwrap();
207
208 log::debug!("{:?}", resp);
209 return Ok(resp.into());
210 }
211 sleep(Duration::from_secs(5)).await;
212 }
213 panic!("couldn't find transaction after {} attempts!", attempts);
214}