cosm_script/
sender.rs

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        // NETWORK_MNEMONIC_GROUP
35        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        // use deployment mnemonic if specified, else use default network mnemonic
40        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            // Cloning is encouraged: https://docs.rs/tonic/latest/tonic/transport/struct.Channel.html
58            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        // log::debug!("{:?}", self.pub_addr_str());
137        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}