layer_climb_core/
transaction.rs

1use crate::prelude::*;
2use crate::querier::tx::AnyTxResponse;
3use crate::signing::middleware::{SigningMiddlewareMapBody, SigningMiddlewareMapResp};
4use std::sync::{
5    atomic::{AtomicBool, AtomicU64},
6    Arc,
7};
8
9use layer_climb_signer::TxSigner;
10
11pub struct TxBuilder<'a> {
12    pub querier: &'a QueryClient,
13    pub signer: &'a dyn TxSigner,
14
15    /// Must be set if not providing a `sequence` or `account_number`
16    pub sender: Option<Address>,
17
18    /// how many blocks until a tx is considered invalid
19    /// if not set, the default is 10 blocks
20    pub tx_timeout_blocks: Option<u64>,
21    /// for manually overriding the sequence number, e.g. parallel transactions (multiple *messages* in a tx do not need this)
22    pub sequence_strategy: Option<SequenceStrategy>,
23
24    /// The account number of the sender. If not set, it will be derived from the sender's account
25    pub account_number: Option<u64>,
26
27    pub memo: Option<String>,
28
29    /// The gas coin to use. Gas price (in gas_coin.denom) = gas_coin.amount * gas_units
30    /// If not set, it will be derived from querier.chain_config (without hitting the network)
31    pub gas_coin: Option<layer_climb_proto::Coin>,
32
33    /// The maximum gas units. Gas price (in gas_coin.denom) = gas_coin.amount * gas_units
34    /// If not set, it will be derived from running an on-chain simulation multiplied by `gas_multiplier`
35    pub gas_units_or_simulate: Option<u64>,
36
37    /// A multiplier to use for simulated gas units.
38    /// If not set, a default of 1.5 will be used.
39    pub gas_simulate_multiplier: Option<f32>,
40
41    /// The broadcast mode to use. If not set, the default is `Sync`
42    pub broadcast_mode: Option<layer_climb_proto::tx::BroadcastMode>,
43
44    /// Whether broadcasting should poll for the tx landing on chain before returning
45    /// default is true
46    pub broadcast_poll: bool,
47
48    /// The duration to sleep between polling for the tx landing on chain
49    /// If not set, the default is 1 second
50    pub broadcast_poll_sleep_duration: Option<std::time::Duration>,
51
52    /// The duration to wait before giving up on polling for the tx landing on chain
53    /// If not set, the default is 30 seconds
54    pub broadcast_poll_timeout_duration: Option<std::time::Duration>,
55
56    /// Middleware to run before the tx is broadcast
57    pub middleware_map_body: Option<Arc<Vec<SigningMiddlewareMapBody>>>,
58
59    /// Middleware to run after the tx is broadcast
60    pub middleware_map_resp: Option<Arc<Vec<SigningMiddlewareMapResp>>>,
61}
62
63impl<'a> TxBuilder<'a> {
64    const DEFAULT_TX_TIMEOUT_BLOCKS: u64 = 10;
65    const DEFAULT_GAS_MULTIPLIER: f32 = 1.5;
66    const DEFAULT_BROADCAST_MODE: layer_climb_proto::tx::BroadcastMode =
67        layer_climb_proto::tx::BroadcastMode::Sync;
68    const DEFAULT_BROADCAST_POLL_SLEEP_DURATION: std::time::Duration =
69        std::time::Duration::from_secs(1);
70    const DEFAULT_BROADCAST_POLL_TIMEOUT_DURATION: std::time::Duration =
71        std::time::Duration::from_secs(30);
72
73    pub fn new(querier: &'a QueryClient, signer: &'a dyn TxSigner) -> Self {
74        Self {
75            querier,
76            signer,
77            gas_coin: None,
78            sender: None,
79            memo: None,
80            tx_timeout_blocks: None,
81            sequence_strategy: None,
82            gas_units_or_simulate: None,
83            gas_simulate_multiplier: None,
84            account_number: None,
85            broadcast_mode: None,
86            broadcast_poll: true,
87            broadcast_poll_sleep_duration: None,
88            broadcast_poll_timeout_duration: None,
89            middleware_map_body: None,
90            middleware_map_resp: None,
91        }
92    }
93
94    pub fn set_tx_timeout_blocks(&mut self, tx_timeout_blocks: u64) -> &mut Self {
95        self.tx_timeout_blocks = Some(tx_timeout_blocks);
96        self
97    }
98
99    pub fn set_memo(&mut self, memo: impl Into<String>) -> &mut Self {
100        self.memo = Some(memo.into());
101        self
102    }
103
104    pub fn set_sequence_strategy(&mut self, sequence_strategy: SequenceStrategy) -> &mut Self {
105        self.sequence_strategy = Some(sequence_strategy);
106        self
107    }
108
109    pub fn set_sender(&mut self, sender: Address) -> &mut Self {
110        self.sender = Some(sender);
111        self
112    }
113
114    pub fn set_gas_coin(&mut self, gas_coin: layer_climb_proto::Coin) -> &mut Self {
115        self.gas_coin = Some(gas_coin);
116        self
117    }
118
119    pub fn set_gas_units_or_simulate(&mut self, gas_units: Option<u64>) -> &mut Self {
120        self.gas_units_or_simulate = gas_units;
121        self
122    }
123
124    pub fn set_gas_simulate_multiplier(&mut self, gas_multiplier: f32) -> &mut Self {
125        self.gas_simulate_multiplier = Some(gas_multiplier);
126        self
127    }
128
129    pub fn set_account_number(&mut self, account_number: u64) -> &mut Self {
130        self.account_number = Some(account_number);
131        self
132    }
133
134    pub fn set_broadcast_mode(
135        &mut self,
136        broadcast_mode: layer_climb_proto::tx::BroadcastMode,
137    ) -> &mut Self {
138        self.broadcast_mode = Some(broadcast_mode);
139        self
140    }
141
142    pub fn set_broadcast_poll(&mut self, broadcast_poll: bool) -> &mut Self {
143        self.broadcast_poll = broadcast_poll;
144        self
145    }
146
147    pub fn set_broadcast_poll_sleep_duration(
148        &mut self,
149        broadcast_poll_sleep_duration: std::time::Duration,
150    ) -> &mut Self {
151        self.broadcast_poll_sleep_duration = Some(broadcast_poll_sleep_duration);
152        self
153    }
154
155    pub fn set_broadcast_poll_timeout_duration(
156        &mut self,
157        broadcast_poll_timeout_duration: std::time::Duration,
158    ) -> &mut Self {
159        self.broadcast_poll_timeout_duration = Some(broadcast_poll_timeout_duration);
160        self
161    }
162
163    pub fn set_middleware_map_body(
164        &mut self,
165        middleware_map_body: Arc<Vec<SigningMiddlewareMapBody>>,
166    ) -> &mut Self {
167        self.middleware_map_body = Some(middleware_map_body);
168        self
169    }
170
171    pub fn set_middleware_map_resp(
172        &mut self,
173        middleware_map_resp: Arc<Vec<SigningMiddlewareMapResp>>,
174    ) -> &mut Self {
175        self.middleware_map_resp = Some(middleware_map_resp);
176        self
177    }
178
179    async fn query_base_account(&self) -> Result<layer_climb_proto::auth::BaseAccount> {
180        self.querier
181            .base_account(
182                self.sender
183                    .as_ref()
184                    .with_context(|| "must provide a sender if no sequence")?,
185            )
186            .await
187    }
188
189    pub async fn broadcast(
190        self,
191        messages: impl IntoIterator<Item = layer_climb_proto::Any>,
192    ) -> Result<layer_climb_proto::abci::TxResponse> {
193        let messages = messages.into_iter().collect();
194        let resp = self.broadcast_raw(messages).await?;
195
196        match resp {
197            AnyTxResponse::Abci(tx_response) => Ok(tx_response),
198            AnyTxResponse::Rpc(_) => Err(anyhow!(
199                "Unexpected AnyTxResponse type - did you mean to call broadcast_raw instead?"
200            )),
201        }
202    }
203
204    pub async fn simulate_gas(
205        &self,
206        signer_info: layer_climb_proto::tx::SignerInfo,
207        account_number: u64,
208        // mutable so we can set the timeout_height here
209        tx_body: &mut layer_climb_proto::tx::TxBody,
210    ) -> Result<layer_climb_proto::abci::GasInfo> {
211        let fee = FeeCalculation::Simulation {
212            chain_config: &self.querier.chain_config,
213        }
214        .calculate()?;
215
216        let simulate_tx_resp = self
217            .querier
218            .simulate_tx(
219                self.sign_tx(signer_info, account_number, tx_body, fee, true)
220                    .await?,
221            )
222            .await?;
223
224        simulate_tx_resp
225            .gas_info
226            .context("unable to get gas from simulation")
227    }
228
229    /// Typically do _not_ want to do this directly, use `broadcast` instead
230    /// however, in a case where you do not want to wait for the tx to be committed, you can use this
231    /// (and if the original tx response is AnyTxResponse::Rpc, it will stay that way)
232    pub async fn broadcast_raw(
233        self,
234        messages: Vec<layer_climb_proto::Any>,
235    ) -> Result<AnyTxResponse> {
236        let mut base_account: Option<layer_climb_proto::auth::BaseAccount> = None;
237
238        let sequence = match &self.sequence_strategy {
239            Some(sequence_strategy) => match sequence_strategy.kind {
240                SequenceStrategyKind::Query => {
241                    base_account = Some(self.query_base_account().await?);
242                    base_account.as_ref().unwrap().sequence
243                }
244                SequenceStrategyKind::QueryAndIncrement => {
245                    if !sequence_strategy
246                        .has_queried
247                        .load(std::sync::atomic::Ordering::SeqCst)
248                    {
249                        base_account = Some(self.query_base_account().await?);
250                        sequence_strategy
251                            .has_queried
252                            .store(true, std::sync::atomic::Ordering::SeqCst);
253                        sequence_strategy.value.store(
254                            base_account.as_ref().unwrap().sequence,
255                            std::sync::atomic::Ordering::SeqCst,
256                        );
257                        base_account.as_ref().unwrap().sequence
258                    } else {
259                        sequence_strategy
260                            .value
261                            .load(std::sync::atomic::Ordering::SeqCst)
262                    }
263                }
264                SequenceStrategyKind::SetAndIncrement(_) => sequence_strategy
265                    .value
266                    .load(std::sync::atomic::Ordering::SeqCst),
267                SequenceStrategyKind::Constant(n) => n,
268            },
269            None => {
270                base_account = Some(self.query_base_account().await?);
271                base_account.as_ref().unwrap().sequence
272            }
273        };
274
275        let account_number = match self.account_number {
276            Some(account_number) => account_number,
277            None => match base_account {
278                Some(base_account) => base_account.account_number,
279                None => self.query_base_account().await?.account_number,
280            },
281        };
282
283        let mut body = layer_climb_proto::tx::TxBody {
284            messages,
285            memo: self.memo.as_deref().unwrap_or("").to_string(),
286            timeout_height: 0, // will be set later so we don't get delayed by other async calls before we send
287            extension_options: Default::default(),
288            non_critical_extension_options: Default::default(),
289        };
290
291        if let Some(middleware) = self.middleware_map_body.as_ref() {
292            for middleware in middleware.iter() {
293                body = match middleware.map_body(body).await {
294                    Ok(req) => req,
295                    Err(e) => return Err(e),
296                }
297            }
298        }
299
300        let gas_units = match self.gas_units_or_simulate {
301            Some(gas_units) => gas_units,
302            None => {
303                let gas_multiplier = self
304                    .gas_simulate_multiplier
305                    .unwrap_or(Self::DEFAULT_GAS_MULTIPLIER);
306
307                let signer_info = self
308                    .signer
309                    .signer_info(sequence, layer_climb_proto::tx::SignMode::Unspecified)
310                    .await?;
311
312                let gas_info = self
313                    .simulate_gas(signer_info, account_number, &mut body)
314                    .await?;
315
316                (gas_info.gas_used as f32 * gas_multiplier).ceil() as u64
317            }
318        };
319
320        let fee = match self.gas_coin.clone() {
321            Some(gas_coin) => FeeCalculation::RealCoin {
322                gas_coin,
323                gas_units,
324            }
325            .calculate()?,
326            None => FeeCalculation::RealNetwork {
327                chain_config: &self.querier.chain_config,
328                gas_units,
329            }
330            .calculate()?,
331        };
332
333        let signer_info = self
334            .signer
335            .signer_info(sequence, layer_climb_proto::tx::SignMode::Direct)
336            .await?;
337
338        let tx_bytes = self
339            .sign_tx(signer_info, account_number, &mut body, fee, false)
340            .await?;
341        let broadcast_mode = self.broadcast_mode.unwrap_or(Self::DEFAULT_BROADCAST_MODE);
342
343        let tx_response = self
344            .querier
345            .broadcast_tx_bytes(tx_bytes, broadcast_mode)
346            .await?;
347
348        // TODO not sure about this... only increase on success? how does this interact with simulations?
349        if let Some(sequence) = self.sequence_strategy {
350            match sequence.kind {
351                SequenceStrategyKind::QueryAndIncrement => {
352                    sequence
353                        .value
354                        .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
355                }
356                SequenceStrategyKind::SetAndIncrement(_) => {
357                    sequence
358                        .value
359                        .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
360                }
361                _ => {}
362            }
363        }
364
365        if tx_response.code() != 0 {
366            bail!(
367                "tx failed with code: {}, codespace: {}, raw_log: {}",
368                tx_response.code(),
369                tx_response.codespace(),
370                tx_response.raw_log()
371            );
372        }
373
374        let mut tx_response = if self.broadcast_poll {
375            let sleep_duration = self
376                .broadcast_poll_sleep_duration
377                .unwrap_or(Self::DEFAULT_BROADCAST_POLL_SLEEP_DURATION);
378            let timeout_duration = self
379                .broadcast_poll_timeout_duration
380                .unwrap_or(Self::DEFAULT_BROADCAST_POLL_TIMEOUT_DURATION);
381
382            AnyTxResponse::Abci(
383                self.querier
384                    .poll_until_tx_ready(tx_response.tx_hash(), sleep_duration, timeout_duration)
385                    .await?
386                    .tx_response,
387            )
388        } else {
389            tx_response
390        };
391
392        if tx_response.code() != 0 {
393            bail!(
394                "tx failed with code: {}, codespace: {}, raw_log: {}",
395                tx_response.code(),
396                tx_response.codespace(),
397                tx_response.raw_log()
398            );
399        }
400
401        if let Some(middleware) = self.middleware_map_resp.as_ref() {
402            for middleware in middleware.iter() {
403                tx_response = match middleware.map_resp(tx_response).await {
404                    Ok(req) => req,
405                    Err(e) => return Err(e),
406                }
407            }
408        }
409
410        Ok(tx_response)
411    }
412
413    async fn sign_tx(
414        &self,
415        signer_info: layer_climb_proto::tx::SignerInfo,
416        account_number: u64,
417        // mutable so we can set the timeout_height here
418        body: &mut layer_climb_proto::tx::TxBody,
419        fee: layer_climb_proto::tx::Fee,
420        simulate_only: bool,
421    ) -> Result<Vec<u8>> {
422        #[allow(deprecated)]
423        let auth_info = layer_climb_proto::tx::AuthInfo {
424            signer_infos: vec![signer_info],
425            fee: Some(fee),
426            tip: None,
427        };
428
429        let block_height = self.querier.block_height().await?;
430
431        let tx_timeout_blocks = self
432            .tx_timeout_blocks
433            .unwrap_or(Self::DEFAULT_TX_TIMEOUT_BLOCKS);
434
435        // latest possible time we can grab the current block height
436        body.timeout_height = block_height + tx_timeout_blocks;
437
438        let sign_doc = layer_climb_proto::tx::SignDoc {
439            body_bytes: proto_into_bytes(body)?,
440            auth_info_bytes: proto_into_bytes(&auth_info)?,
441            chain_id: self.querier.chain_config.chain_id.to_string(),
442            account_number,
443        };
444
445        let signature = match simulate_only {
446            true => Vec::new(),
447            false => self.signer.sign(&sign_doc).await?,
448        };
449
450        let tx_raw = layer_climb_proto::tx::TxRaw {
451            body_bytes: sign_doc.body_bytes.clone(),
452            auth_info_bytes: sign_doc.auth_info_bytes.clone(),
453            signatures: vec![signature],
454        };
455
456        proto_into_bytes(&tx_raw)
457    }
458}
459
460#[derive(Clone, Debug)]
461pub struct SequenceStrategy {
462    pub kind: SequenceStrategyKind,
463    pub value: Arc<AtomicU64>,
464    pub has_queried: Arc<AtomicBool>,
465}
466
467impl SequenceStrategy {
468    pub fn new(kind: SequenceStrategyKind) -> Self {
469        Self {
470            value: Arc::new(AtomicU64::new(match kind {
471                SequenceStrategyKind::Query => 0,             // will be ignored
472                SequenceStrategyKind::QueryAndIncrement => 0, // will be ignored
473                SequenceStrategyKind::SetAndIncrement(n) => n,
474                SequenceStrategyKind::Constant(n) => n,
475            })),
476            kind,
477            has_queried: Arc::new(AtomicBool::new(false)),
478        }
479    }
480}
481
482#[derive(Clone, Debug)]
483pub enum SequenceStrategyKind {
484    /// Always query
485    Query,
486    /// Query the first time, and then increment each successful tx
487    QueryAndIncrement,
488    /// Set to this the first time, and then increment each successful tx
489    SetAndIncrement(u64),
490    /// Set to this each time
491    Constant(u64),
492}
493
494pub enum FeeCalculation<'a> {
495    Simulation {
496        chain_config: &'a ChainConfig,
497    },
498    RealNetwork {
499        chain_config: &'a ChainConfig,
500        gas_units: u64,
501    },
502    RealCoin {
503        gas_coin: layer_climb_proto::Coin,
504        gas_units: u64,
505    },
506}
507
508impl FeeCalculation<'_> {
509    pub fn calculate(&self) -> Result<layer_climb_proto::tx::Fee> {
510        let (gas_coin, gas_limit) = match self {
511            Self::Simulation { chain_config } => (new_coin(0, &chain_config.gas_denom), 0),
512            Self::RealNetwork {
513                chain_config,
514                gas_units,
515            } => {
516                let amount = (chain_config.gas_price * *gas_units as f32).ceil() as u128;
517                (new_coin(amount, &chain_config.gas_denom), *gas_units)
518            }
519            Self::RealCoin {
520                gas_coin,
521                gas_units,
522            } => (gas_coin.clone(), *gas_units),
523        };
524
525        Ok(layer_climb_proto::tx::Fee {
526            amount: vec![gas_coin],
527            gas_limit,
528            payer: "".to_string(),
529            granter: "".to_string(),
530        })
531    }
532}