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_address::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        tx_body: &layer_climb_proto::tx::TxBody,
209    ) -> Result<layer_climb_proto::abci::GasInfo> {
210        let fee = FeeCalculation::Simulation {
211            chain_config: &self.querier.chain_config,
212        }
213        .calculate()?;
214
215        let simulate_tx_resp = self
216            .querier
217            .simulate_tx(
218                self.sign_tx(signer_info, account_number, tx_body, fee, true)
219                    .await?,
220            )
221            .await?;
222
223        simulate_tx_resp
224            .gas_info
225            .context("unable to get gas from simulation")
226    }
227
228    /// Typically do _not_ want to do this directly, use `broadcast` instead
229    /// however, in a case where you do not want to wait for the tx to be committed, you can use this
230    /// (and if the original tx response is AnyTxResponse::Rpc, it will stay that way)
231    pub async fn broadcast_raw(
232        self,
233        messages: Vec<layer_climb_proto::Any>,
234    ) -> Result<AnyTxResponse> {
235        let block_height = self.querier.block_height().await?;
236
237        let tx_timeout_blocks = self
238            .tx_timeout_blocks
239            .unwrap_or(Self::DEFAULT_TX_TIMEOUT_BLOCKS);
240
241        let mut body = layer_climb_proto::tx::TxBody {
242            messages,
243            memo: self.memo.as_deref().unwrap_or("").to_string(),
244            timeout_height: block_height + tx_timeout_blocks,
245            extension_options: Default::default(),
246            non_critical_extension_options: Default::default(),
247        };
248
249        if let Some(middleware) = self.middleware_map_body.as_ref() {
250            for middleware in middleware.iter() {
251                body = match middleware.map_body(body).await {
252                    Ok(req) => req,
253                    Err(e) => return Err(e),
254                }
255            }
256        }
257
258        let mut base_account: Option<layer_climb_proto::auth::BaseAccount> = None;
259
260        let sequence = match &self.sequence_strategy {
261            Some(sequence_strategy) => match sequence_strategy.kind {
262                SequenceStrategyKind::Query => {
263                    base_account = Some(self.query_base_account().await?);
264                    base_account.as_ref().unwrap().sequence
265                }
266                SequenceStrategyKind::QueryAndIncrement => {
267                    if !sequence_strategy
268                        .has_queried
269                        .load(std::sync::atomic::Ordering::SeqCst)
270                    {
271                        base_account = Some(self.query_base_account().await?);
272                        sequence_strategy
273                            .has_queried
274                            .store(true, std::sync::atomic::Ordering::SeqCst);
275                        sequence_strategy.value.store(
276                            base_account.as_ref().unwrap().sequence,
277                            std::sync::atomic::Ordering::SeqCst,
278                        );
279                        base_account.as_ref().unwrap().sequence
280                    } else {
281                        sequence_strategy
282                            .value
283                            .load(std::sync::atomic::Ordering::SeqCst)
284                    }
285                }
286                SequenceStrategyKind::SetAndIncrement(_) => sequence_strategy
287                    .value
288                    .load(std::sync::atomic::Ordering::SeqCst),
289                SequenceStrategyKind::Constant(n) => n,
290            },
291            None => {
292                base_account = Some(self.query_base_account().await?);
293                base_account.as_ref().unwrap().sequence
294            }
295        };
296
297        let account_number = match self.account_number {
298            Some(account_number) => account_number,
299            None => match base_account {
300                Some(base_account) => base_account.account_number,
301                None => self.query_base_account().await?.account_number,
302            },
303        };
304
305        let gas_units = match self.gas_units_or_simulate {
306            Some(gas_units) => gas_units,
307            None => {
308                let gas_multiplier = self
309                    .gas_simulate_multiplier
310                    .unwrap_or(Self::DEFAULT_GAS_MULTIPLIER);
311
312                let signer_info = self
313                    .signer
314                    .signer_info(sequence, layer_climb_proto::tx::SignMode::Unspecified)
315                    .await?;
316
317                let gas_info = self
318                    .simulate_gas(signer_info, account_number, &body)
319                    .await?;
320
321                (gas_info.gas_used as f32 * gas_multiplier).ceil() as u64
322            }
323        };
324
325        let fee = match self.gas_coin.clone() {
326            Some(gas_coin) => FeeCalculation::RealCoin {
327                gas_coin,
328                gas_units,
329            }
330            .calculate()?,
331            None => FeeCalculation::RealNetwork {
332                chain_config: &self.querier.chain_config,
333                gas_units,
334            }
335            .calculate()?,
336        };
337
338        let signer_info = self
339            .signer
340            .signer_info(sequence, layer_climb_proto::tx::SignMode::Direct)
341            .await?;
342
343        let tx_bytes = self
344            .sign_tx(signer_info, account_number, &body, fee, false)
345            .await?;
346        let broadcast_mode = self.broadcast_mode.unwrap_or(Self::DEFAULT_BROADCAST_MODE);
347
348        let tx_response = self
349            .querier
350            .broadcast_tx_bytes(tx_bytes, broadcast_mode)
351            .await?;
352
353        // TODO not sure about this... only increase on success? how does this interact with simulations?
354        if let Some(sequence) = self.sequence_strategy {
355            match sequence.kind {
356                SequenceStrategyKind::QueryAndIncrement => {
357                    sequence
358                        .value
359                        .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
360                }
361                SequenceStrategyKind::SetAndIncrement(_) => {
362                    sequence
363                        .value
364                        .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
365                }
366                _ => {}
367            }
368        }
369
370        if tx_response.code() != 0 {
371            bail!(
372                "tx failed with code: {}, codespace: {}, raw_log: {}",
373                tx_response.code(),
374                tx_response.codespace(),
375                tx_response.raw_log()
376            );
377        }
378
379        let mut tx_response = if self.broadcast_poll {
380            let sleep_duration = self
381                .broadcast_poll_sleep_duration
382                .unwrap_or(Self::DEFAULT_BROADCAST_POLL_SLEEP_DURATION);
383            let timeout_duration = self
384                .broadcast_poll_timeout_duration
385                .unwrap_or(Self::DEFAULT_BROADCAST_POLL_TIMEOUT_DURATION);
386
387            AnyTxResponse::Abci(
388                self.querier
389                    .poll_until_tx_ready(tx_response.tx_hash(), sleep_duration, timeout_duration)
390                    .await?
391                    .tx_response,
392            )
393        } else {
394            tx_response
395        };
396
397        if tx_response.code() != 0 {
398            bail!(
399                "tx failed with code: {}, codespace: {}, raw_log: {}",
400                tx_response.code(),
401                tx_response.codespace(),
402                tx_response.raw_log()
403            );
404        }
405
406        if let Some(middleware) = self.middleware_map_resp.as_ref() {
407            for middleware in middleware.iter() {
408                tx_response = match middleware.map_resp(tx_response).await {
409                    Ok(req) => req,
410                    Err(e) => return Err(e),
411                }
412            }
413        }
414
415        Ok(tx_response)
416    }
417
418    async fn sign_tx(
419        &self,
420        signer_info: layer_climb_proto::tx::SignerInfo,
421        account_number: u64,
422        body: &layer_climb_proto::tx::TxBody,
423        fee: layer_climb_proto::tx::Fee,
424        simulate_only: bool,
425    ) -> Result<Vec<u8>> {
426        #[allow(deprecated)]
427        let auth_info = layer_climb_proto::tx::AuthInfo {
428            signer_infos: vec![signer_info],
429            fee: Some(fee),
430            tip: None,
431        };
432
433        let sign_doc = layer_climb_proto::tx::SignDoc {
434            body_bytes: proto_into_bytes(body)?,
435            auth_info_bytes: proto_into_bytes(&auth_info)?,
436            chain_id: self.querier.chain_config.chain_id.to_string(),
437            account_number,
438        };
439
440        let signature = match simulate_only {
441            true => Vec::new(),
442            false => self.signer.sign(&sign_doc).await?,
443        };
444
445        let tx_raw = layer_climb_proto::tx::TxRaw {
446            body_bytes: sign_doc.body_bytes.clone(),
447            auth_info_bytes: sign_doc.auth_info_bytes.clone(),
448            signatures: vec![signature],
449        };
450
451        proto_into_bytes(&tx_raw)
452    }
453}
454
455#[derive(Clone, Debug)]
456pub struct SequenceStrategy {
457    pub kind: SequenceStrategyKind,
458    pub value: Arc<AtomicU64>,
459    pub has_queried: Arc<AtomicBool>,
460}
461
462impl SequenceStrategy {
463    pub fn new(kind: SequenceStrategyKind) -> Self {
464        Self {
465            value: Arc::new(AtomicU64::new(match kind {
466                SequenceStrategyKind::Query => 0,             // will be ignored
467                SequenceStrategyKind::QueryAndIncrement => 0, // will be ignored
468                SequenceStrategyKind::SetAndIncrement(n) => n,
469                SequenceStrategyKind::Constant(n) => n,
470            })),
471            kind,
472            has_queried: Arc::new(AtomicBool::new(false)),
473        }
474    }
475}
476
477#[derive(Clone, Debug)]
478pub enum SequenceStrategyKind {
479    /// Always query
480    Query,
481    /// Query the first time, and then increment each successful tx
482    QueryAndIncrement,
483    /// Set to this the first time, and then increment each successful tx
484    SetAndIncrement(u64),
485    /// Set to this each time
486    Constant(u64),
487}
488
489pub enum FeeCalculation<'a> {
490    Simulation {
491        chain_config: &'a ChainConfig,
492    },
493    RealNetwork {
494        chain_config: &'a ChainConfig,
495        gas_units: u64,
496    },
497    RealCoin {
498        gas_coin: layer_climb_proto::Coin,
499        gas_units: u64,
500    },
501}
502
503impl FeeCalculation<'_> {
504    pub fn calculate(&self) -> Result<layer_climb_proto::tx::Fee> {
505        let (gas_coin, gas_limit) = match self {
506            Self::Simulation { chain_config } => (new_coin(0, &chain_config.gas_denom), 0),
507            Self::RealNetwork {
508                chain_config,
509                gas_units,
510            } => {
511                let amount = (chain_config.gas_price * *gas_units as f32).ceil() as u128;
512                (new_coin(amount, &chain_config.gas_denom), *gas_units)
513            }
514            Self::RealCoin {
515                gas_coin,
516                gas_units,
517            } => (gas_coin.clone(), *gas_units),
518        };
519
520        Ok(layer_climb_proto::tx::Fee {
521            amount: vec![gas_coin],
522            gas_limit,
523            payer: "".to_string(),
524            granter: "".to_string(),
525        })
526    }
527}