starknet_accounts/account/
execution.rs

1use super::{
2    super::NotPreparedError, Account, AccountError, ConnectedAccount, ExecutionV3,
3    PreparedExecutionV3, RawExecutionV3,
4};
5use crate::ExecutionEncoder;
6
7use starknet_core::types::{
8    BroadcastedInvokeTransactionV3, BroadcastedTransaction, Call, DataAvailabilityMode,
9    FeeEstimate, Felt, InvokeTransactionResult, ResourceBounds, ResourceBoundsMapping,
10    SimulatedTransaction, SimulationFlag, SimulationFlagForEstimateFee,
11};
12use starknet_crypto::PoseidonHasher;
13use starknet_providers::Provider;
14use starknet_signers::SignerInteractivityContext;
15
16/// Cairo string for "invoke"
17const PREFIX_INVOKE: Felt = Felt::from_raw([
18    513398556346534256,
19    18446744073709551615,
20    18446744073709551615,
21    18443034532770911073,
22]);
23
24/// 2 ^ 128 + 3
25const QUERY_VERSION_THREE: Felt = Felt::from_raw([
26    576460752142432688,
27    18446744073709551584,
28    17407,
29    18446744073700081569,
30]);
31
32impl<'a, A> ExecutionV3<'a, A> {
33    /// Constructs a new [`ExecutionV3`].
34    ///
35    /// Users would typically use [`execute_v3`](fn.execute_v3) on an [`Account`] instead of
36    /// directly calling this method.
37    pub const fn new(calls: Vec<Call>, account: &'a A) -> Self {
38        Self {
39            account,
40            calls,
41            nonce: None,
42            l1_gas: None,
43            l1_gas_price: None,
44            l2_gas: None,
45            l2_gas_price: None,
46            l1_data_gas: None,
47            l1_data_gas_price: None,
48            gas_estimate_multiplier: 1.5,
49            gas_price_estimate_multiplier: 1.5,
50            tip: None,
51        }
52    }
53
54    /// Returns a new [`ExecutionV3`] with the `nonce`.
55    pub fn nonce(self, nonce: Felt) -> Self {
56        Self {
57            nonce: Some(nonce),
58            ..self
59        }
60    }
61
62    /// Returns a new [`ExecutionV3`] with the `l1_gas`.
63    pub fn l1_gas(self, l1_gas: u64) -> Self {
64        Self {
65            l1_gas: Some(l1_gas),
66            ..self
67        }
68    }
69
70    /// Returns a new [`ExecutionV3`] with the `l1_gas_price`.
71    pub fn l1_gas_price(self, l1_gas_price: u128) -> Self {
72        Self {
73            l1_gas_price: Some(l1_gas_price),
74            ..self
75        }
76    }
77
78    /// Returns a new [`ExecutionV3`] with the `l2_gas`.
79    pub fn l2_gas(self, l2_gas: u64) -> Self {
80        Self {
81            l2_gas: Some(l2_gas),
82            ..self
83        }
84    }
85
86    /// Returns a new [`ExecutionV3`] with the `l2_gas_price`.
87    pub fn l2_gas_price(self, l2_gas_price: u128) -> Self {
88        Self {
89            l2_gas_price: Some(l2_gas_price),
90            ..self
91        }
92    }
93
94    /// Returns a new [`ExecutionV3`] with the `l1_data_gas`.
95    pub fn l1_data_gas(self, l1_data_gas: u64) -> Self {
96        Self {
97            l1_data_gas: Some(l1_data_gas),
98            ..self
99        }
100    }
101
102    /// Returns a new [`ExecutionV3`] with the `l1_data_gas_price`.
103    pub fn l1_data_gas_price(self, l1_data_gas_price: u128) -> Self {
104        Self {
105            l1_data_gas_price: Some(l1_data_gas_price),
106            ..self
107        }
108    }
109
110    /// Returns a new [`ExecutionV3`] with the gas amount estimate multiplier.  The multiplier is
111    /// used when the gas amount is not manually specified and must be fetched from a [`Provider`]
112    /// instead.
113    pub fn gas_estimate_multiplier(self, gas_estimate_multiplier: f64) -> Self {
114        Self {
115            gas_estimate_multiplier,
116            ..self
117        }
118    }
119
120    /// Returns a new [`ExecutionV3`] with the gas price estimate multiplier.  The multiplier is
121    /// used when the gas price is not manually specified and must be fetched from a [`Provider`]
122    /// instead.
123    pub fn gas_price_estimate_multiplier(self, gas_price_estimate_multiplier: f64) -> Self {
124        Self {
125            gas_price_estimate_multiplier,
126            ..self
127        }
128    }
129
130    /// Returns a new [`ExecutionV3`] with the `tip`.
131    pub fn tip(self, tip: u64) -> Self {
132        Self {
133            tip: Some(tip),
134            ..self
135        }
136    }
137
138    /// Calling this function after manually specifying `nonce`, `gas` and `gas_price` turns
139    /// [`ExecutionV3`] into [`PreparedExecutionV3`]. Returns `Err` if any field is `None`.
140    pub fn prepared(self) -> Result<PreparedExecutionV3<'a, A>, NotPreparedError> {
141        let nonce = self.nonce.ok_or(NotPreparedError)?;
142        let l1_gas = self.l1_gas.ok_or(NotPreparedError)?;
143        let l1_gas_price = self.l1_gas_price.ok_or(NotPreparedError)?;
144        let l2_gas = self.l2_gas.ok_or(NotPreparedError)?;
145        let l2_gas_price = self.l2_gas_price.ok_or(NotPreparedError)?;
146        let l1_data_gas = self.l1_data_gas.ok_or(NotPreparedError)?;
147        let l1_data_gas_price = self.l1_data_gas_price.ok_or(NotPreparedError)?;
148        let tip = self.tip.ok_or(NotPreparedError)?;
149
150        Ok(PreparedExecutionV3 {
151            account: self.account,
152            inner: RawExecutionV3 {
153                calls: self.calls,
154                nonce,
155                l1_gas,
156                l1_gas_price,
157                l2_gas,
158                l2_gas_price,
159                l1_data_gas,
160                l1_data_gas_price,
161                tip,
162            },
163        })
164    }
165}
166
167impl<'a, A> ExecutionV3<'a, A>
168where
169    A: ConnectedAccount + Sync,
170{
171    /// Estimates transaction fees from a [`Provider`].
172    pub async fn estimate_fee(&self) -> Result<FeeEstimate, AccountError<A::SignError>> {
173        // Resolves nonce
174        let nonce = match self.nonce {
175            Some(value) => value,
176            None => self
177                .account
178                .get_nonce()
179                .await
180                .map_err(AccountError::Provider)?,
181        };
182
183        self.estimate_fee_with_nonce(nonce).await
184    }
185
186    /// Simulates the transaction from a [`Provider`]. Transaction validation and fee transfer can
187    /// be skipped.
188    pub async fn simulate(
189        &self,
190        skip_validate: bool,
191        skip_fee_charge: bool,
192    ) -> Result<SimulatedTransaction, AccountError<A::SignError>> {
193        // Resolves nonce
194        let nonce = match self.nonce {
195            Some(value) => value,
196            None => self
197                .account
198                .get_nonce()
199                .await
200                .map_err(AccountError::Provider)?,
201        };
202
203        self.simulate_with_nonce(nonce, skip_validate, skip_fee_charge)
204            .await
205    }
206
207    /// Signs and broadcasts the transaction to the network.
208    pub async fn send(&self) -> Result<InvokeTransactionResult, AccountError<A::SignError>> {
209        self.prepare().await?.send().await
210    }
211
212    async fn prepare(&self) -> Result<PreparedExecutionV3<'a, A>, AccountError<A::SignError>> {
213        // Resolves nonce
214        let nonce = match self.nonce {
215            Some(value) => value,
216            None => self
217                .account
218                .get_nonce()
219                .await
220                .map_err(AccountError::Provider)?,
221        };
222
223        // Resolves fee settings
224        let (
225            l1_gas,
226            l1_gas_price,
227            l2_gas,
228            l2_gas_price,
229            l1_data_gas,
230            l1_data_gas_price,
231            full_block,
232        ) = match (
233            self.l1_gas,
234            self.l1_gas_price,
235            self.l2_gas,
236            self.l2_gas_price,
237            self.l1_data_gas,
238            self.l1_data_gas_price,
239        ) {
240            (
241                Some(l1_gas),
242                Some(l1_gas_price),
243                Some(l2_gas),
244                Some(l2_gas_price),
245                Some(l1_data_gas),
246                Some(l1_data_gas_price),
247            ) => (
248                l1_gas,
249                l1_gas_price,
250                l2_gas,
251                l2_gas_price,
252                l1_data_gas,
253                l1_data_gas_price,
254                None,
255            ),
256            (Some(l1_gas), _, Some(l2_gas), _, Some(l1_data_gas), _) => {
257                // When all `gas` fields are specified, we only need the gas prices in FRI. By
258                // specifying all gas values, the user might be trying to avoid a full fee
259                // estimation (e.g. flaky dependencies), so it's inappropriate to call
260                // `estimate_fee` here.
261
262                let (block_l1_gas_price, block_l2_gas_price, block_l1_data_gas_price, full_block) =
263                    if self.tip.is_some() {
264                        // No need to estimate tip. Just fetch the lightest-weight block we can get.
265                        let block = self
266                            .account
267                            .provider()
268                            .get_block_with_tx_hashes(self.account.block_id())
269                            .await
270                            .map_err(AccountError::Provider)?;
271                        (
272                            block.l1_gas_price().price_in_fri,
273                            block.l2_gas_price().price_in_fri,
274                            block.l1_data_gas_price().price_in_fri,
275                            None,
276                        )
277                    } else {
278                        // We only need th block header here but still fetching the full block to be used
279                        // for tip estimation below.
280                        let block = self
281                            .account
282                            .provider()
283                            .get_block_with_txs(self.account.block_id())
284                            .await
285                            .map_err(AccountError::Provider)?;
286                        (
287                            block.l1_gas_price().price_in_fri,
288                            block.l2_gas_price().price_in_fri,
289                            block.l1_data_gas_price().price_in_fri,
290                            Some(block),
291                        )
292                    };
293
294                let adjusted_l1_gas_price =
295                    ((TryInto::<u64>::try_into(block_l1_gas_price)
296                        .map_err(|_| AccountError::FeeOutOfRange)? as f64)
297                        * self.gas_price_estimate_multiplier) as u128;
298                let adjusted_l2_gas_price =
299                    ((TryInto::<u64>::try_into(block_l2_gas_price)
300                        .map_err(|_| AccountError::FeeOutOfRange)? as f64)
301                        * self.gas_price_estimate_multiplier) as u128;
302                let adjusted_l1_data_gas_price =
303                    ((TryInto::<u64>::try_into(block_l1_data_gas_price)
304                        .map_err(|_| AccountError::FeeOutOfRange)? as f64)
305                        * self.gas_price_estimate_multiplier) as u128;
306
307                (
308                    l1_gas,
309                    adjusted_l1_gas_price,
310                    l2_gas,
311                    adjusted_l2_gas_price,
312                    l1_data_gas,
313                    adjusted_l1_data_gas_price,
314                    full_block,
315                )
316            }
317            // We have to perform fee estimation as long as gas is not specified
318            _ => {
319                let fee_estimate = self.estimate_fee_with_nonce(nonce).await?;
320
321                (
322                    ((fee_estimate.l1_gas_consumed as f64) * self.gas_estimate_multiplier) as u64,
323                    ((TryInto::<u64>::try_into(fee_estimate.l1_gas_price)
324                        .map_err(|_| AccountError::FeeOutOfRange)? as f64)
325                        * self.gas_price_estimate_multiplier) as u128,
326                    ((fee_estimate.l2_gas_consumed as f64) * self.gas_estimate_multiplier) as u64,
327                    ((TryInto::<u64>::try_into(fee_estimate.l2_gas_price)
328                        .map_err(|_| AccountError::FeeOutOfRange)? as f64)
329                        * self.gas_price_estimate_multiplier) as u128,
330                    ((fee_estimate.l1_data_gas_consumed as f64) * self.gas_estimate_multiplier)
331                        as u64,
332                    ((TryInto::<u64>::try_into(fee_estimate.l1_data_gas_price)
333                        .map_err(|_| AccountError::FeeOutOfRange)? as f64)
334                        * self.gas_price_estimate_multiplier) as u128,
335                    None,
336                )
337            }
338        };
339
340        let tip = match self.tip {
341            Some(tip) => tip,
342            None => {
343                // Need to estimate tip from median. Maybe a full block has already been fetched?
344                let block = match full_block {
345                    Some(block) => block,
346                    None => self
347                        .account
348                        .provider()
349                        .get_block_with_txs(self.account.block_id())
350                        .await
351                        .map_err(AccountError::Provider)?,
352                };
353                block.median_tip()
354            }
355        };
356
357        Ok(PreparedExecutionV3 {
358            account: self.account,
359            inner: RawExecutionV3 {
360                calls: self.calls.clone(),
361                nonce,
362                l1_gas,
363                l1_gas_price,
364                l2_gas,
365                l2_gas_price,
366                l1_data_gas,
367                l1_data_gas_price,
368                tip,
369            },
370        })
371    }
372
373    async fn estimate_fee_with_nonce(
374        &self,
375        nonce: Felt,
376    ) -> Result<FeeEstimate, AccountError<A::SignError>> {
377        let skip_signature = self
378            .account
379            .is_signer_interactive(SignerInteractivityContext::Execution { calls: &self.calls });
380
381        let prepared = PreparedExecutionV3 {
382            account: self.account,
383            inner: RawExecutionV3 {
384                calls: self.calls.clone(),
385                nonce,
386                l1_gas: 0,
387                l1_gas_price: 0,
388                l2_gas: 0,
389                l2_gas_price: 0,
390                l1_data_gas: 0,
391                l1_data_gas_price: 0,
392                tip: 0,
393            },
394        };
395        let invoke = prepared
396            .get_invoke_request(true, skip_signature)
397            .await
398            .map_err(AccountError::Signing)?;
399
400        self.account
401            .provider()
402            .estimate_fee_single(
403                BroadcastedTransaction::Invoke(invoke),
404                if skip_signature {
405                    // Validation would fail since real signature was not requested
406                    vec![SimulationFlagForEstimateFee::SkipValidate]
407                } else {
408                    // With the correct signature in place, run validation for accurate results
409                    vec![]
410                },
411                self.account.block_id(),
412            )
413            .await
414            .map_err(AccountError::Provider)
415    }
416
417    async fn simulate_with_nonce(
418        &self,
419        nonce: Felt,
420        skip_validate: bool,
421        skip_fee_charge: bool,
422    ) -> Result<SimulatedTransaction, AccountError<A::SignError>> {
423        let skip_signature = if self
424            .account
425            .is_signer_interactive(SignerInteractivityContext::Execution { calls: &self.calls })
426        {
427            // If signer is interactive, we would try to minimize signing requests. However, if the
428            // caller has decided to not skip validation, it's best we still request a real
429            // signature, as otherwise the simulation would most likely fail.
430            skip_validate
431        } else {
432            // Signing with non-interactive signers is cheap so always request signatures.
433            false
434        };
435
436        let prepared = PreparedExecutionV3 {
437            account: self.account,
438            inner: RawExecutionV3 {
439                calls: self.calls.clone(),
440                nonce,
441                l1_gas: self.l1_gas.unwrap_or_default(),
442                l1_gas_price: self.l1_gas_price.unwrap_or_default(),
443                l2_gas: self.l2_gas.unwrap_or_default(),
444                l2_gas_price: self.l2_gas_price.unwrap_or_default(),
445                l1_data_gas: self.l1_data_gas.unwrap_or_default(),
446                l1_data_gas_price: self.l1_data_gas_price.unwrap_or_default(),
447                tip: self.tip.unwrap_or_default(),
448            },
449        };
450        let invoke = prepared
451            .get_invoke_request(true, skip_signature)
452            .await
453            .map_err(AccountError::Signing)?;
454
455        let mut flags = vec![];
456
457        if skip_validate {
458            flags.push(SimulationFlag::SkipValidate);
459        }
460        if skip_fee_charge {
461            flags.push(SimulationFlag::SkipFeeCharge);
462        }
463
464        self.account
465            .provider()
466            .simulate_transaction(
467                self.account.block_id(),
468                BroadcastedTransaction::Invoke(invoke),
469                &flags,
470            )
471            .await
472            .map_err(AccountError::Provider)
473    }
474}
475
476impl RawExecutionV3 {
477    /// Calculates transaction hash given `chain_id`, `address`, `query_only`, and `encoder`.
478    pub fn transaction_hash<E>(
479        &self,
480        chain_id: Felt,
481        address: Felt,
482        query_only: bool,
483        encoder: E,
484    ) -> Felt
485    where
486        E: ExecutionEncoder,
487    {
488        let mut hasher = PoseidonHasher::new();
489
490        hasher.update(PREFIX_INVOKE);
491        hasher.update(if query_only {
492            QUERY_VERSION_THREE
493        } else {
494            Felt::THREE
495        });
496        hasher.update(address);
497
498        hasher.update({
499            let mut fee_hasher = PoseidonHasher::new();
500
501            fee_hasher.update(self.tip.into());
502
503            let mut resource_buffer = [
504                0, 0, b'L', b'1', b'_', b'G', b'A', b'S', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
505                0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
506            ];
507            resource_buffer[8..(8 + 8)].copy_from_slice(&self.l1_gas.to_be_bytes());
508            resource_buffer[(8 + 8)..].copy_from_slice(&self.l1_gas_price.to_be_bytes());
509            fee_hasher.update(Felt::from_bytes_be(&resource_buffer));
510
511            let mut resource_buffer = [
512                0, 0, b'L', b'2', b'_', b'G', b'A', b'S', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
513                0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
514            ];
515            resource_buffer[8..(8 + 8)].copy_from_slice(&self.l2_gas.to_be_bytes());
516            resource_buffer[(8 + 8)..].copy_from_slice(&self.l2_gas_price.to_be_bytes());
517            fee_hasher.update(Felt::from_bytes_be(&resource_buffer));
518
519            let mut resource_buffer = [
520                0, b'L', b'1', b'_', b'D', b'A', b'T', b'A', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
521                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
522            ];
523            resource_buffer[8..(8 + 8)].copy_from_slice(&self.l1_data_gas.to_be_bytes());
524            resource_buffer[(8 + 8)..].copy_from_slice(&self.l1_data_gas_price.to_be_bytes());
525            fee_hasher.update(Felt::from_bytes_be(&resource_buffer));
526
527            fee_hasher.finalize()
528        });
529
530        // Hard-coded empty `paymaster_data`
531        hasher.update(PoseidonHasher::new().finalize());
532
533        hasher.update(chain_id);
534        hasher.update(self.nonce);
535
536        // Hard-coded L1 DA mode for nonce and fee
537        hasher.update(Felt::ZERO);
538
539        // Hard-coded empty `account_deployment_data`
540        hasher.update(PoseidonHasher::new().finalize());
541
542        hasher.update({
543            let mut calldata_hasher = PoseidonHasher::new();
544
545            encoder
546                .encode_calls(&self.calls)
547                .into_iter()
548                .for_each(|element| calldata_hasher.update(element));
549
550            calldata_hasher.finalize()
551        });
552
553        hasher.finalize()
554    }
555
556    /// Gets a reference to the list of contract calls included in the execution.
557    pub fn calls(&self) -> &[Call] {
558        &self.calls
559    }
560
561    /// Gets the `nonce` of the execution request.
562    pub const fn nonce(&self) -> Felt {
563        self.nonce
564    }
565
566    /// Gets the `l1_gas` of the execution request.
567    pub const fn l1_gas(&self) -> u64 {
568        self.l1_gas
569    }
570
571    /// Gets the `l1_gas_price` of the execution request.
572    pub const fn l1_gas_price(&self) -> u128 {
573        self.l1_gas_price
574    }
575
576    /// Gets the `l2_gas` of the execution request.
577    pub const fn l2_gas(&self) -> u64 {
578        self.l2_gas
579    }
580
581    /// Gets the `l2_gas_price` of the execution request.
582    pub const fn l2_gas_price(&self) -> u128 {
583        self.l2_gas_price
584    }
585
586    /// Gets the `l1_data_gas` of the execution request.
587    pub const fn l1_data_gas(&self) -> u64 {
588        self.l1_data_gas
589    }
590
591    /// Gets the `l1_data_gas_price` of the execution request.
592    pub const fn l1_data_gas_price(&self) -> u128 {
593        self.l1_data_gas_price
594    }
595
596    /// Gets the `tip` of the execution request.
597    pub const fn tip(&self) -> u64 {
598        self.tip
599    }
600}
601
602impl<A> PreparedExecutionV3<'_, A>
603where
604    A: Account,
605{
606    /// Locally calculates the hash of the transaction to be sent from this execution given the
607    /// parameters.
608    pub fn transaction_hash(&self, query_only: bool) -> Felt {
609        self.inner.transaction_hash(
610            self.account.chain_id(),
611            self.account.address(),
612            query_only,
613            self.account,
614        )
615    }
616}
617
618impl<A> PreparedExecutionV3<'_, A>
619where
620    A: ConnectedAccount,
621{
622    /// Signs and broadcasts the transaction to the network.
623    pub async fn send(&self) -> Result<InvokeTransactionResult, AccountError<A::SignError>> {
624        let tx_request = self
625            .get_invoke_request(false, false)
626            .await
627            .map_err(AccountError::Signing)?;
628        self.account
629            .provider()
630            .add_invoke_transaction(tx_request)
631            .await
632            .map_err(AccountError::Provider)
633    }
634
635    // The `simulate` function is temporarily removed until it's supported in [Provider]
636    // TODO: add `simulate` back once transaction simulation in supported
637
638    /// Get the broadcasted invoke transaction request.
639    pub async fn get_invoke_request(
640        &self,
641        query_only: bool,
642        skip_signature: bool,
643    ) -> Result<BroadcastedInvokeTransactionV3, A::SignError> {
644        Ok(BroadcastedInvokeTransactionV3 {
645            sender_address: self.account.address(),
646            calldata: self.account.encode_calls(&self.inner.calls),
647            signature: if skip_signature {
648                vec![]
649            } else {
650                self.account
651                    .sign_execution_v3(&self.inner, query_only)
652                    .await?
653            },
654            nonce: self.inner.nonce,
655            resource_bounds: ResourceBoundsMapping {
656                l1_gas: ResourceBounds {
657                    max_amount: self.inner.l1_gas,
658                    max_price_per_unit: self.inner.l1_gas_price,
659                },
660                l1_data_gas: ResourceBounds {
661                    max_amount: self.inner.l1_data_gas,
662                    max_price_per_unit: self.inner.l1_data_gas_price,
663                },
664                l2_gas: ResourceBounds {
665                    max_amount: self.inner.l2_gas,
666                    max_price_per_unit: self.inner.l2_gas_price,
667                },
668            },
669            tip: self.inner.tip,
670            // Hard-coded empty `paymaster_data`
671            paymaster_data: vec![],
672            // Hard-coded empty `account_deployment_data`
673            account_deployment_data: vec![],
674            // Hard-coded L1 DA mode for nonce and fee
675            nonce_data_availability_mode: DataAvailabilityMode::L1,
676            fee_data_availability_mode: DataAvailabilityMode::L1,
677            is_query: query_only,
678        })
679    }
680}