soroban_rs/
account.rs

1//! # Soroban Account Management
2//!
3//! This module provides types and functionality for handling Stellar accounts in Soroban,
4//! including transaction signing for both single and multi-signature (multisig) accounts.
5//!
6//! ## Features
7//!
8//! - Account sequence number tracking and management
9//! - Single and multi-signature account support
10//! - Transaction signing with authorization control
11//! - Account configuration (thresholds, weights, signers)
12//!
13//! ## Example
14//!
15//! ```rust,no_run
16//! use soroban_rs::{Account, Env, EnvConfigs, Signer};
17//! use ed25519_dalek::SigningKey;
18//!
19//! // Example private key (32 bytes)
20//! let private_key_bytes: [u8; 32] = [
21//!     1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
22//!     26, 27, 28, 29, 30, 31, 32,
23//! ];
24//!
25//! // Create a signer from a secret key
26//! let signing_key = SigningKey::from_bytes(&private_key_bytes);
27//! let signer = Signer::new(signing_key);
28//!
29//! // Single-signature account
30//! let account = Account::single(signer);
31//! ```
32use crate::{Env, Signer, TransactionBuilder, error::SorobanHelperError};
33use stellar_strkey::ed25519::PublicKey;
34use stellar_xdr::curr::{
35    AccountEntry, AccountId, DecoratedSignature, Hash, Operation, OperationBody, SetOptionsOp,
36    Signer as XdrSigner, SignerKey, Transaction, TransactionEnvelope, TransactionV1Envelope, VecM,
37};
38
39/// Represents a transaction sequence number for a Stellar account.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct AccountSequence(i64);
42
43impl AccountSequence {
44    /// Creates a new sequence number with the specified value.
45    ///
46    /// # Parameters
47    ///
48    /// * `val` - The i64 sequence number value
49    pub fn new(val: i64) -> Self {
50        AccountSequence(val)
51    }
52
53    /// Returns a new SequenceNumber incremented by one.
54    pub fn next(&self) -> Self {
55        AccountSequence(self.0 + 1)
56    }
57
58    /// Increments the sequence number and returns the new value.
59    ///
60    /// Unlike next() which leaves the original value unchanged, this method
61    /// replaces the original sequence number.
62    pub fn increment(self) -> Self {
63        self.next()
64    }
65
66    /// Returns the raw i64 sequence number.
67    pub fn value(self) -> i64 {
68        self.0
69    }
70}
71
72/// Tracks and limits the number of authorized transaction calls for an account.
73///
74/// This provides a safety mechanism to limit the number of transactions that can be
75/// submitted from a particular account, helping to prevent accidental or malicious
76/// transaction spamming.
77#[derive(Debug, Clone, Copy)]
78pub struct AuthorizedCalls(u16);
79
80impl AuthorizedCalls {
81    /// Creates a new AuthorizedCalls with the specified limit.
82    ///
83    /// # Parameters
84    ///
85    /// * `calls` - The maximum number of calls allowed
86    pub fn new(calls: u16) -> Self {
87        AuthorizedCalls(calls)
88    }
89
90    /// Checks if the account can make additional calls.
91    ///
92    /// Returns `true` if there are remaining calls available.
93    pub fn can_call(&self) -> bool {
94        self.0 > 0
95    }
96
97    /// Attempts to decrement the authorized calls counter.
98    ///
99    /// # Returns
100    ///
101    /// * `Ok(())` if the call counter was successfully decremented
102    /// * `Err` if no calls remain
103    pub fn try_decrement(&mut self) -> Result<(), SorobanHelperError> {
104        if self.can_call() {
105            self.0 -= 1;
106            Ok(())
107        } else {
108            Err(SorobanHelperError::Unauthorized(
109                "Account has reached the max number of authorized calls".to_string(),
110            ))
111        }
112    }
113
114    /// Returns the current number of authorized calls remaining.
115    pub fn value(&self) -> u16 {
116        self.0
117    }
118}
119
120/// Configuration options for setting up or modifying a Stellar account.
121///
122/// Used to configure thresholds and signers for an account. This is particularly
123/// useful for creating or modifying multisig accounts with specific
124/// threshold requirements.
125///
126/// # Example
127///
128/// ```rust,no_run
129/// use soroban_rs::AccountConfig;
130/// use stellar_strkey::ed25519::PublicKey;
131///
132/// let config = AccountConfig::new()
133///     .with_master_weight(10)
134///     .with_thresholds(1, 5, 10)
135///     .add_signer(PublicKey::from_string("PUBLIC KEY").unwrap(), 5);
136/// ```
137pub struct AccountConfig {
138    /// Weight assigned to the master key (account owner)
139    pub master_weight: Option<u32>,
140    /// Threshold for low security operations
141    pub low_threshold: Option<u32>,
142    /// Threshold for medium security operations
143    pub med_threshold: Option<u32>,
144    /// Threshold for high security operations
145    pub high_threshold: Option<u32>,
146    /// Additional signers with their respective weights
147    pub signers: Vec<(PublicKey, u32)>,
148}
149
150impl Default for AccountConfig {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156impl AccountConfig {
157    /// Creates a new empty account configuration.
158    pub fn new() -> Self {
159        Self {
160            master_weight: None,
161            low_threshold: None,
162            med_threshold: None,
163            high_threshold: None,
164            signers: Vec::new(),
165        }
166    }
167
168    /// Sets the master key weight for the account.
169    ///
170    /// # Parameters
171    ///
172    /// * `weight` - The weight to assign to the master key
173    ///   Set to 0 to prevent the master key from being used for signing
174    pub fn with_master_weight(mut self, weight: u32) -> Self {
175        self.master_weight = Some(weight);
176        self
177    }
178
179    /// Sets the threshold values for low, medium, and high security operations.
180    ///
181    /// # Parameters
182    ///
183    /// * `low` - Threshold for low security operations (e.g., setting options)
184    /// * `med` - Threshold for medium security operations (e.g., payments)
185    /// * `high` - Threshold for high security operations (e.g., account merge)
186    pub fn with_thresholds(mut self, low: u32, med: u32, high: u32) -> Self {
187        self.low_threshold = Some(low);
188        self.med_threshold = Some(med);
189        self.high_threshold = Some(high);
190        self
191    }
192
193    /// Adds a new signer with the specified weight.
194    ///
195    /// # Parameters
196    ///
197    /// * `key` - The public key of the signer to add
198    /// * `weight` - The weight to assign to this signer
199    pub fn add_signer(mut self, key: PublicKey, weight: u32) -> Self {
200        self.signers.push((key, weight));
201        self
202    }
203}
204
205/// Represents a single-signature account.
206#[derive(Clone)]
207pub struct SingleAccount {
208    /// The account's identifier
209    pub account_id: AccountId,
210    /// Signer associated with this account
211    pub signers: Vec<Signer>,
212    /// Tracks and limits the number of authorized calls for this account
213    pub authorized_calls: AuthorizedCalls,
214}
215
216/// Represents a multisig account.
217#[derive(Clone)]
218pub struct MultisigAccount {
219    /// The account's identifier
220    pub account_id: AccountId,
221    /// Signers associated with this account
222    pub signers: Vec<Signer>,
223    /// Tracks and limits the number of authorized calls for this account
224    pub authorized_calls: AuthorizedCalls,
225}
226
227/// Represents either a single-signature or multisig account.
228///
229/// This is the main account type used for interacting with the Stellar network.
230/// It provides methods for signing transactions, configuring account settings,
231/// and managing sequence numbers.
232#[derive(Clone)]
233pub enum Account {
234    /// Single-signature account with one key pair
235    KeyPair(SingleAccount),
236    /// Multi-signature account
237    Multisig(MultisigAccount),
238}
239
240impl Account {
241    /// Creates a new single-signature Account instance with the provided signer.
242    ///
243    /// # Parameters
244    ///
245    /// * `signer` - The signer for this account
246    ///
247    /// # Returns
248    ///
249    /// A new `Account` instance with the KeyPair variant
250    pub fn single(signer: Signer) -> Self {
251        Self::KeyPair(SingleAccount {
252            account_id: signer.account_id(),
253            signers: vec![signer],
254            authorized_calls: AuthorizedCalls::new(i16::MAX as u16),
255        })
256    }
257
258    /// Creates a new multisig Account instance with the provided account ID and signers.
259    ///
260    /// # Parameters
261    ///
262    /// * `account_id` - The identifier for this account
263    /// * `signers` - A vector of signers for this multi-signature account
264    ///
265    /// # Returns
266    ///
267    /// A new `Account` instance with the Multisig variant
268    pub fn multisig(account_id: AccountId, signers: Vec<Signer>) -> Self {
269        Self::Multisig(MultisigAccount {
270            account_id,
271            signers,
272            authorized_calls: AuthorizedCalls::new(i16::MAX as u16),
273        })
274    }
275
276    /// Loads the account information from the network.
277    ///
278    /// # Parameters
279    ///
280    /// * `env` - The environment to use for loading the account
281    ///
282    /// # Returns
283    ///
284    /// The account entry from the Stellar network
285    pub async fn load(&self, env: &Env) -> Result<AccountEntry, SorobanHelperError> {
286        env.get_account(&self.account_id().to_string()).await
287    }
288
289    /// Returns the account ID.
290    pub fn account_id(&self) -> AccountId {
291        match self {
292            Self::KeyPair(account) => account.account_id.clone(),
293            Self::Multisig(account) => account.account_id.clone(),
294        }
295    }
296
297    /// Gets the current sequence number for the account.
298    ///
299    /// # Parameters
300    ///
301    /// * `env` - The environment to use for fetching the sequence number
302    ///
303    /// # Returns
304    ///
305    /// The current sequence number wrapped in `AccountSequence`
306    pub async fn get_sequence(&self, env: &Env) -> Result<AccountSequence, SorobanHelperError> {
307        let entry = self.load(env).await?;
308        Ok(AccountSequence::new(entry.seq_num.0))
309    }
310
311    /// Retrieves the next available sequence number.
312    ///
313    /// This is useful when preparing a new transaction.
314    ///
315    /// # Parameters
316    ///
317    /// * `env` - The environment to use for fetching the sequence number
318    ///
319    /// # Returns
320    ///
321    /// The next sequence number (current + 1) wrapped in `AccountSequence`
322    pub async fn next_sequence(&self, env: &Env) -> Result<AccountSequence, SorobanHelperError> {
323        let current = self.get_sequence(env).await?;
324        Ok(current.next())
325    }
326
327    /// Returns a reference to the account's signers.
328    fn signers(&self) -> &[Signer] {
329        match self {
330            Self::KeyPair(account) => &account.signers,
331            Self::Multisig(account) => &account.signers,
332        }
333    }
334
335    /// Returns the current authorized calls tracker.
336    fn authorized_calls(&self) -> AuthorizedCalls {
337        match self {
338            Self::KeyPair(account) => account.authorized_calls,
339            Self::Multisig(account) => account.authorized_calls,
340        }
341    }
342
343    /// Returns a mutable reference to the authorized calls tracker.
344    fn authorized_calls_mut(&mut self) -> &mut AuthorizedCalls {
345        match self {
346            Self::KeyPair(account) => &mut account.authorized_calls,
347            Self::Multisig(account) => &mut account.authorized_calls,
348        }
349    }
350
351    /// Sets the number of authorized calls for the account.
352    ///
353    /// # Parameters
354    ///
355    /// * `authorized_calls` - The number of calls to authorize
356    pub fn set_authorized_calls(&mut self, authorized_calls: i16) {
357        *self.authorized_calls_mut() = AuthorizedCalls::new(authorized_calls as u16);
358    }
359
360    /// Sign a transaction using the account's signers.
361    ///
362    /// # Parameters
363    ///
364    /// * `tx` - The transaction to sign
365    /// * `network_id` - The network ID hash
366    /// * `signers` - The signers to use
367    ///
368    /// # Returns
369    ///
370    /// A vector of decorated signatures
371    fn sign_with_tx(
372        tx: &Transaction,
373        network_id: &Hash,
374        signers: &[Signer],
375    ) -> Result<VecM<DecoratedSignature, 20>, SorobanHelperError> {
376        let signatures: Vec<DecoratedSignature> = signers
377            .iter()
378            .map(|signer| signer.sign_transaction(tx, network_id))
379            .collect::<Result<_, _>>()
380            .map_err(|e| SorobanHelperError::XdrEncodingFailed(e.to_string()))?;
381        signatures.try_into().map_err(|_| {
382            SorobanHelperError::XdrEncodingFailed("Failed to convert signatures to XDR".to_string())
383        })
384    }
385
386    /// Signs a transaction without checking or decrementing the authorized_calls counter.
387    ///
388    /// This method bypasses authorization checks and should be used with caution.
389    ///
390    /// # Parameters
391    ///
392    /// * `tx` - The transaction to sign
393    /// * `network_id` - The network ID hash
394    ///
395    /// # Returns
396    ///
397    /// A signed transaction envelope
398    pub fn sign_transaction_unsafe(
399        &self,
400        tx: &Transaction,
401        network_id: &Hash,
402    ) -> Result<TransactionEnvelope, SorobanHelperError> {
403        let signatures = Self::sign_with_tx(tx, network_id, self.signers())?;
404        Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
405            tx: tx.clone(),
406            signatures,
407        }))
408    }
409
410    /// Signs a transaction, ensuring the account still has authorized calls.
411    ///
412    /// Decrements the authorized call counter when successful.
413    ///
414    /// # Parameters
415    ///
416    /// * `tx` - The transaction to sign
417    /// * `network_id` - The network ID hash
418    ///
419    /// # Returns
420    ///
421    /// A signed transaction envelope
422    pub fn sign_transaction(
423        &mut self,
424        tx: &Transaction,
425        network_id: &Hash,
426    ) -> Result<TransactionEnvelope, SorobanHelperError> {
427        if !self.authorized_calls().can_call() {
428            return Err(SorobanHelperError::Unauthorized(
429                "Account has reached the max number of authorized calls".to_string(),
430            ));
431        }
432
433        let signatures = Self::sign_with_tx(tx, network_id, self.signers())?;
434        self.authorized_calls_mut().try_decrement()?;
435
436        Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
437            tx: tx.clone(),
438            signatures,
439        }))
440    }
441
442    /// Signs a transaction envelope by appending new signatures.
443    ///
444    /// # Parameters
445    ///
446    /// * `tx_envelope` - The transaction envelope to sign
447    /// * `network_id` - The network ID hash
448    ///
449    /// # Returns
450    ///
451    /// A transaction envelope with the new signatures appended
452    pub fn sign_transaction_envelope(
453        &mut self,
454        tx_envelope: &TransactionEnvelope,
455        network_id: &Hash,
456    ) -> Result<TransactionEnvelope, SorobanHelperError> {
457        if !self.authorized_calls().can_call() {
458            return Err(SorobanHelperError::Unauthorized(
459                "Account has reached the max number of authorized calls".to_string(),
460            ));
461        }
462
463        let tx_v1 = match tx_envelope {
464            TransactionEnvelope::Tx(tx_v1) => tx_v1,
465            _ => {
466                return Err(SorobanHelperError::XdrEncodingFailed(
467                    "Invalid transaction envelope".to_string(),
468                ));
469            }
470        };
471
472        let prev_signatures = tx_v1.signatures.clone();
473        let new_signatures = Self::sign_with_tx(&tx_v1.tx, network_id, self.signers())?;
474
475        let mut all_signatures: Vec<DecoratedSignature> = prev_signatures.to_vec();
476        all_signatures.extend(new_signatures.to_vec());
477        let signatures: VecM<DecoratedSignature, 20> = all_signatures.try_into().map_err(|_| {
478            SorobanHelperError::XdrEncodingFailed("Too many signatures for XDR vector".to_string())
479        })?;
480
481        self.authorized_calls_mut().try_decrement()?;
482
483        Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
484            tx: tx_v1.tx.clone(),
485            signatures,
486        }))
487    }
488
489    /// Configures the account by building and signing a transaction that sets options.
490    ///
491    /// This can be used to add signers, set thresholds, and modify other account settings.
492    ///
493    /// # Parameters
494    ///
495    /// * `env` - The environment to use for transaction building
496    /// * `config` - The account configuration to apply
497    ///
498    /// # Returns
499    ///
500    /// A signed transaction envelope containing the set options operations
501    pub async fn configure(
502        mut self,
503        env: &Env,
504        config: AccountConfig,
505    ) -> Result<TransactionEnvelope, SorobanHelperError> {
506        let mut tx = TransactionBuilder::new(&self, env);
507
508        // Add set options operation for each signer configuration.
509        for (public_key, weight) in config.signers {
510            let signer_key = SignerKey::Ed25519(public_key.0.into());
511            tx = tx.add_operation(Operation {
512                source_account: None,
513                body: OperationBody::SetOptions(SetOptionsOp {
514                    inflation_dest: None,
515                    clear_flags: None,
516                    set_flags: None,
517                    master_weight: None,
518                    low_threshold: None,
519                    med_threshold: None,
520                    high_threshold: None,
521                    home_domain: None,
522                    signer: Some(XdrSigner {
523                        key: signer_key,
524                        weight,
525                    }),
526                }),
527            });
528        }
529
530        // Add thresholds if any are specified.
531        if config.master_weight.is_some()
532            || config.low_threshold.is_some()
533            || config.med_threshold.is_some()
534            || config.high_threshold.is_some()
535        {
536            tx = tx.add_operation(Operation {
537                source_account: None,
538                body: OperationBody::SetOptions(SetOptionsOp {
539                    inflation_dest: None,
540                    clear_flags: None,
541                    set_flags: None,
542                    master_weight: config.master_weight,
543                    low_threshold: config.low_threshold,
544                    med_threshold: config.med_threshold,
545                    high_threshold: config.high_threshold,
546                    home_domain: None,
547                    signer: None,
548                }),
549            });
550        }
551
552        let tx = tx
553            .simulate_and_build(env, &self)
554            .await
555            .map_err(|e| SorobanHelperError::TransactionBuildFailed(e.to_string()))?;
556
557        self.sign_transaction(&tx, &env.network_id())
558    }
559}
560
561#[cfg(test)]
562mod test {
563    use stellar_xdr::curr::{OperationBody, Signer as XdrSigner, SignerKey, TransactionEnvelope};
564
565    use crate::account::AuthorizedCalls;
566    use crate::mock::{all_signers, mock_env, mock_signer1, mock_signer3};
567    use crate::{Account, AccountConfig, TransactionBuilder};
568
569    #[tokio::test]
570    async fn load_account() {
571        let env = mock_env(None, None, None);
572
573        // Test single account operations
574        let account = Account::single(mock_signer1());
575
576        // Test account loading
577        let entry = account.load(&env).await;
578
579        let expected_account_id = mock_signer1().account_id().0.to_string();
580        let res_account_id = entry.unwrap().account_id.0.to_string();
581
582        assert_eq!(expected_account_id, res_account_id);
583    }
584
585    #[tokio::test]
586    async fn multisig() {
587        let env = mock_env(None, None, None);
588
589        // Test single account operations
590        let account = Account::multisig(mock_signer3().account_id(), all_signers());
591
592        // Test account loading
593        let entry = account.load(&env).await;
594
595        let expected_account_id = mock_signer3().account_id().0.to_string();
596        let res_account_id = entry.unwrap().account_id.0.to_string();
597
598        let signers = account.signers();
599
600        for (i, sig) in signers.iter().enumerate() {
601            assert_eq!(sig.account_id(), all_signers()[i].account_id())
602        }
603
604        assert_eq!(expected_account_id, res_account_id);
605    }
606
607    #[tokio::test]
608    async fn sign_transaction() {
609        let env = mock_env(None, None, None);
610
611        // Test single account operations
612        let mut account = Account::single(mock_signer1());
613
614        // Test sign transaction
615        let tx = TransactionBuilder::new(&account, &env)
616            .build()
617            .await
618            .unwrap();
619
620        account.set_authorized_calls(1);
621
622        let signed_tx = account.sign_transaction(&tx, &env.network_id());
623
624        assert!(signed_tx.is_ok());
625    }
626
627    #[tokio::test]
628    async fn sign_transaction_unsafe() {
629        let env = mock_env(None, None, None);
630
631        // Test single account operations
632        let mut account = Account::single(mock_signer1());
633
634        // Test sign transaction
635        let tx = TransactionBuilder::new(&account, &env)
636            .build()
637            .await
638            .unwrap();
639
640        // no authorized calls
641        account.set_authorized_calls(0);
642
643        // sign unsafe does not check the remaining authorized calls.
644        let signed_tx = account.sign_transaction_unsafe(&tx, &env.network_id());
645
646        assert!(signed_tx.is_ok());
647    }
648
649    #[test]
650    fn test_authorized_calls_decrement() {
651        let mut auth = AuthorizedCalls::new(2);
652        assert!(auth.can_call());
653        assert!(auth.try_decrement().is_ok());
654        assert!(auth.try_decrement().is_ok());
655        assert!(auth.try_decrement().is_err());
656    }
657
658    #[test]
659    fn test_authorized_calls_value() {
660        let auth_zero = AuthorizedCalls::new(0);
661        assert_eq!(auth_zero.value(), 0);
662        assert!(!auth_zero.can_call());
663
664        let auth_with_calls = AuthorizedCalls::new(42);
665        assert_eq!(auth_with_calls.value(), 42);
666        assert!(auth_with_calls.can_call());
667
668        let mut auth_decrement = AuthorizedCalls::new(5);
669        assert_eq!(auth_decrement.value(), 5);
670
671        assert!(auth_decrement.try_decrement().is_ok());
672        assert_eq!(auth_decrement.value(), 4);
673
674        assert!(auth_decrement.try_decrement().is_ok());
675        assert_eq!(auth_decrement.value(), 3);
676    }
677
678    #[tokio::test]
679    async fn test_next_sequence() {
680        let env = mock_env(None, None, None);
681        let account = Account::single(mock_signer1());
682
683        let current_seq = account.get_sequence(&env).await.unwrap();
684
685        let next_seq = account.next_sequence(&env).await.unwrap();
686        assert_eq!(next_seq.value(), current_seq.value() + 1);
687
688        let second_current = account.get_sequence(&env).await.unwrap();
689        let second_next = account.next_sequence(&env).await.unwrap();
690        assert_eq!(second_current.value(), current_seq.value());
691
692        assert_eq!(second_next.value(), second_current.value() + 1);
693    }
694
695    #[tokio::test]
696    async fn test_configure() {
697        let env = mock_env(None, None, None);
698
699        let account = Account::single(mock_signer1());
700        let config = AccountConfig::new()
701            .with_master_weight(10)
702            .with_thresholds(1, 2, 3)
703            .add_signer(mock_signer3().public_key(), 5);
704
705        let tx = account.configure(&env, config).await;
706
707        if let TransactionEnvelope::Tx(tx_env) = tx.unwrap() {
708            if let OperationBody::SetOptions(op) = &tx_env.tx.operations[0].body {
709                assert_eq!(
710                    op.signer,
711                    Some(XdrSigner {
712                        key: SignerKey::Ed25519(mock_signer3().public_key().0.into()),
713                        weight: 5
714                    })
715                );
716            }
717
718            if let OperationBody::SetOptions(op) = &tx_env.tx.operations[1].body {
719                assert_eq!(op.master_weight, Some(10));
720                assert_eq!(op.low_threshold, Some(1));
721                assert_eq!(op.med_threshold, Some(2));
722                assert_eq!(op.high_threshold, Some(3));
723            }
724        }
725    }
726
727    #[tokio::test]
728    async fn test_sign_transaction_envelope() {
729        let env = mock_env(None, None, None);
730
731        let mut first_account = Account::single(mock_signer1());
732        let mut second_account = Account::single(mock_signer3());
733
734        first_account.set_authorized_calls(1);
735        second_account.set_authorized_calls(1);
736
737        let tx = TransactionBuilder::new(&first_account, &env)
738            .build()
739            .await
740            .unwrap();
741
742        let first_signed_envelope = first_account
743            .sign_transaction(&tx, &env.network_id())
744            .unwrap();
745
746        let first_envelope_signatures = match &first_signed_envelope {
747            TransactionEnvelope::Tx(tx_v1) => &tx_v1.signatures,
748            _ => &[][..], // Empty slice as fallback, assertion below will fail if this happens
749        };
750        assert_eq!(first_envelope_signatures.len(), 1); // First envelope should have exactly one signature
751
752        let final_envelope = second_account
753            .sign_transaction_envelope(&first_signed_envelope, &env.network_id())
754            .unwrap();
755
756        let final_signatures = match &final_envelope {
757            TransactionEnvelope::Tx(tx_v1) => &tx_v1.signatures,
758            _ => &[][..], // Empty slice as fallback, assertion below will fail if this happens
759        };
760        assert_eq!(final_signatures.len(), 2); // Final envelope should have exactly two signatures
761
762        let first_public_key = mock_signer1().public_key();
763        let second_public_key = mock_signer3().public_key();
764        assert_eq!(final_signatures[0].hint.0, &first_public_key.0[28..32]); //  First signature should match first account's public key
765        assert_eq!(final_signatures[1].hint.0, &second_public_key.0[28..32]); // Second signature should match second account's public key
766
767        assert_eq!(second_account.authorized_calls().value(), 0); // Authorized calls should be decremented after signing
768    }
769}