starknet_accounts/account/
declaration.rs

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