whisky_wallet/wallet/
mod.rs

1pub mod derivation_indices;
2pub mod mnemonic;
3pub mod root_key;
4
5use bip39::{Language, Mnemonic};
6use derivation_indices::DerivationIndices;
7pub use mnemonic::MnemonicWallet;
8pub use root_key::RootKeyWallet;
9use whisky_common::{Fetcher, Submitter, UTxO, WError};
10use whisky_csl::{
11    csl::{
12        BaseAddress, Bip32PrivateKey, Credential, EnterpriseAddress, FixedTransaction, PrivateKey,
13        PublicKey,
14    },
15    sign_transaction,
16};
17
18#[derive(Copy, Clone)]
19pub enum NetworkId {
20    Preprod = 0, // Default
21    Mainnet = 1,
22}
23
24pub enum AddressType {
25    Enterprise,
26    Payment,
27}
28
29pub enum WalletType {
30    MnemonicWallet(MnemonicWallet),
31    RootKeyWallet(RootKeyWallet),
32    Cli(String),
33}
34
35/// Represents a Cardano wallet.
36///
37/// A wallet manages addresses and cryptographic keys needed for transaction
38/// signing and verification. It supports different wallet types, including
39/// mnemonic-based, root key-based, and CLI-based wallets.
40pub struct Wallet {
41    pub wallet_type: WalletType,
42    pub network_id: NetworkId,
43    pub addresses: Addresses,
44    pub fetcher: Option<Box<dyn Fetcher>>,
45    pub submitter: Option<Box<dyn Submitter>>,
46    pub account: Option<Account>,
47}
48pub struct Addresses {
49    pub base_address: Option<BaseAddress>,
50    pub enterprise_address: Option<EnterpriseAddress>,
51}
52
53pub struct Account {
54    pub private_key: PrivateKey,
55    pub public_key: PublicKey,
56}
57
58impl Account {
59    /// Signs a transaction using the account's private key.
60    ///
61    /// # Arguments
62    ///
63    /// * `tx_hex` - The transaction to sign in hexadecimal format
64    ///
65    /// # Returns
66    ///
67    /// A Result containing either the signed transaction in hexadecimal format or an error
68    pub fn sign_transaction(&self, tx_hex: &str) -> Result<String, WError> {
69        let mut tx = FixedTransaction::from_hex(tx_hex)
70            .map_err(WError::from_err("Account - failed to deserialize tx hex"))?;
71        tx.sign_and_add_vkey_signature(&self.private_key)
72            .map_err(WError::from_err("Account - failed to sign transaction"))?;
73        Ok(tx.to_hex())
74    }
75}
76
77impl Wallet {
78    // Private helper method for basic wallet initialization
79    fn empty() -> Self {
80        Self {
81            wallet_type: WalletType::Cli("".to_string()),
82            network_id: NetworkId::Preprod,
83            addresses: Addresses {
84                base_address: None,
85                enterprise_address: None,
86            },
87            fetcher: None,
88            submitter: None,
89            account: None,
90        }
91    }
92
93    /// Creates a new wallet with the specified wallet type.
94    ///
95    /// This is a generic constructor that initializes addresses based on the wallet type.
96    ///
97    /// # Arguments
98    ///
99    /// * `wallet_type` - The type of wallet to create
100    ///
101    /// # Returns
102    ///
103    /// A new `Wallet` instance with initialized addresses
104    pub fn new(wallet_type: WalletType) -> Result<Self, WError> {
105        let mut wallet = Self::default();
106        wallet.wallet_type = wallet_type;
107        wallet.account = Some(
108            Self::get_account(&wallet.wallet_type)
109                .map_err(WError::from_err("Wallet - new - failed to get account"))?,
110        );
111        wallet.init_addresses();
112        Ok(wallet)
113    }
114
115    /// Creates a new CLI-based wallet using the provided signing key.
116    ///
117    /// # Arguments
118    ///
119    /// * `cli_skey` - The signing key string in hex format
120    ///
121    /// # Returns
122    ///
123    /// A new `Wallet` instance
124    pub fn new_cli(cli_skey: &str) -> Result<Self, WError> {
125        let mut wallet = Self::default();
126        wallet.wallet_type = WalletType::Cli(cli_skey.to_string());
127        wallet.account = Some(
128            Self::get_account(&wallet.wallet_type)
129                .map_err(WError::from_err("Wallet - new_cli - failed to get account"))?,
130        );
131        wallet.init_addresses();
132        Ok(wallet)
133    }
134
135    /// Creates a new mnemonic-based wallet using the provided mnemonic phrase.
136    ///
137    /// # Arguments
138    ///
139    /// * `mnemonic_phrase` - The BIP39 mnemonic phrase
140    ///
141    /// # Returns
142    ///
143    /// A new `Wallet` instance with initialized addresses
144    pub fn new_mnemonic(mnemonic_phrase: &str) -> Result<Self, WError> {
145        let wallet_type = WalletType::MnemonicWallet(MnemonicWallet {
146            mnemonic_phrase: mnemonic_phrase.to_string(),
147            derivation_indices: DerivationIndices::default(),
148        });
149        let mut wallet = Self::empty();
150        wallet.wallet_type = wallet_type;
151        wallet.account = Some(
152            Self::get_account(&wallet.wallet_type)
153                .map_err(WError::from_err("Wallet - new_mnemonic"))?,
154        );
155        wallet.init_addresses();
156        Ok(wallet)
157    }
158
159    /// Creates a new root key-based wallet using the provided root key.
160    ///
161    /// # Arguments
162    ///
163    /// * `root_key` - The bech32-encoded root key
164    ///
165    /// # Returns
166    ///
167    /// A new `Wallet` instance with initialized addresses
168    pub fn new_root_key(root_key: &str) -> Result<Self, WError> {
169        let mut wallet = Self::default();
170        let wallet_type = WalletType::RootKeyWallet(RootKeyWallet {
171            root_key: root_key.to_string(),
172            derivation_indices: DerivationIndices::default(),
173        });
174        wallet.wallet_type = wallet_type;
175        wallet.account = Some(
176            Self::get_account(&wallet.wallet_type).map_err(WError::from_err(
177                "Wallet - new_root_key - failed to get account",
178            ))?,
179        );
180        wallet.init_addresses();
181        Ok(wallet)
182    }
183
184    /// Sets the network ID for the wallet and reinitializes addresses.
185    ///
186    /// # Arguments
187    ///
188    /// * `network_id` - The network ID to use (Preprod or Mainnet)
189    ///
190    /// # Returns
191    ///
192    /// The updated wallet with reinitialized addresses
193    pub fn with_network_id(mut self, network_id: NetworkId) -> Self {
194        self.network_id = network_id;
195        self.init_addresses();
196        self
197    }
198
199    /// Attaches a fetcher implementation to the wallet.
200    ///
201    /// A fetcher is used to fetch UTxOs and other blockchain data.
202    ///
203    /// # Arguments
204    ///
205    /// * `fetcher` - The fetcher implementation to use
206    ///
207    /// # Returns
208    ///
209    /// The updated wallet with fetcher capability
210    pub fn with_fetcher<F: Fetcher + 'static>(mut self, fetcher: F) -> Self {
211        self.fetcher = Some(Box::new(fetcher));
212        self
213    }
214
215    /// Attaches a submitter implementation to the wallet.
216    ///
217    /// A submitter is used to submit transactions to the blockchain.
218    ///
219    /// # Arguments
220    ///
221    /// * `submitter` - The submitter implementation to use
222    ///
223    /// # Returns
224    ///
225    /// The updated wallet with submitter capability
226    pub fn with_submitter<S: Submitter + 'static>(mut self, submitter: S) -> Self {
227        self.submitter = Some(Box::new(submitter));
228        self
229    }
230
231    /// Sets the payment account indices for the wallet.
232    ///
233    /// This updates the derivation path for the payment address.
234    ///
235    /// # Arguments
236    ///
237    /// * `account_index` - The account index to use
238    /// * `key_index` - The key index to use
239    ///
240    /// # Returns
241    ///
242    /// A mutable reference to self for method chaining
243    pub fn payment_account(
244        &mut self,
245        account_index: u32,
246        key_index: u32,
247    ) -> Result<&mut Self, WError> {
248        match &mut self.wallet_type {
249            WalletType::MnemonicWallet(mnemonic_wallet) => {
250                mnemonic_wallet.payment_account(account_index, key_index);
251            }
252            WalletType::RootKeyWallet(root_key_wallet) => {
253                root_key_wallet.payment_account(account_index, key_index);
254            }
255            _ => {}
256        }
257        self.account = Some(
258            Self::get_account(&self.wallet_type).map_err(WError::from_err(
259                "Wallet - payment_account - failed to get account",
260            ))?,
261        );
262        self.init_addresses();
263        Ok(self)
264    }
265
266    /// Sets the stake account indices for the wallet.
267    ///
268    /// This updates the derivation path for the stake address.
269    ///
270    /// # Arguments
271    ///
272    /// * `account_index` - The account index to use
273    /// * `key_index` - The key index to use
274    ///
275    /// # Returns
276    ///
277    /// A mutable reference to self for method chaining
278    pub fn stake_account(
279        &mut self,
280        account_index: u32,
281        key_index: u32,
282    ) -> Result<&mut Self, WError> {
283        match &mut self.wallet_type {
284            WalletType::MnemonicWallet(mnemonic_wallet) => {
285                mnemonic_wallet.stake_account(account_index, key_index);
286            }
287            WalletType::RootKeyWallet(root_key_wallet) => {
288                root_key_wallet.stake_account(account_index, key_index);
289            }
290            _ => {}
291        }
292        self.account = Some(
293            Self::get_account(&self.wallet_type).map_err(WError::from_err(
294                "Wallet - stake_account - failed to get account",
295            ))?,
296        );
297        self.init_addresses();
298        Ok(self)
299    }
300
301    /// Sets the delegation representative (DRep) account indices for the wallet.
302    ///
303    /// This updates the derivation path for the DRep address.
304    ///
305    /// # Arguments
306    ///
307    /// * `account_index` - The account index to use
308    /// * `key_index` - The key index to use
309    ///
310    /// # Returns
311    ///
312    /// A mutable reference to self for method chaining
313    pub fn drep_account(
314        &mut self,
315        account_index: u32,
316        key_index: u32,
317    ) -> Result<&mut Self, WError> {
318        match &mut self.wallet_type {
319            WalletType::MnemonicWallet(mnemonic_wallet) => {
320                mnemonic_wallet.drep_account(account_index, key_index);
321            }
322            WalletType::RootKeyWallet(root_key_wallet) => {
323                root_key_wallet.drep_account(account_index, key_index);
324            }
325            _ => {}
326        }
327        self.account = Some(
328            Self::get_account(&self.wallet_type).map_err(WError::from_err(
329                "Wallet - drep_account - failed to get account",
330            ))?,
331        );
332        self.init_addresses();
333        Ok(self)
334    }
335
336    /// Initializes or re-initializes wallet addresses based on the wallet type and current network ID.
337    ///
338    /// This method generates base and enterprise addresses for mnemonic and root key wallets.
339    /// It's automatically called when constructing a wallet or when changing wallet parameters
340    /// that affect addresses.
341    ///
342    /// # Returns
343    ///
344    /// A mutable reference to self for method chaining.
345    ///
346    /// # Panics
347    ///
348    /// May panic if there are issues creating a mnemonic or decoding a root key.
349    /// Consider using a version that returns Result instead if you need to handle these errors.
350    pub fn init_addresses(&mut self) -> &mut Self {
351        self.addresses = match &self.wallet_type {
352            WalletType::MnemonicWallet(mnemonic_wallet) => {
353                let mnemonic =
354                    Mnemonic::from_phrase(&mnemonic_wallet.mnemonic_phrase, Language::English)
355                        .map_err(WError::from_err(
356                            "Wallet - init_addresses - failed to create mnemonic",
357                        ))
358                        .unwrap();
359                let entropy = mnemonic.entropy();
360                let mut root_key = Bip32PrivateKey::from_bip39_entropy(entropy, &[]);
361                for index in mnemonic_wallet.derivation_indices.0.iter().take(3) {
362                    root_key = root_key.derive(index.clone());
363                }
364
365                let payment_credential = Credential::from_keyhash(
366                    &root_key
367                        .derive(mnemonic_wallet.derivation_indices.0[3].clone())
368                        .derive(mnemonic_wallet.derivation_indices.0[4].clone())
369                        .to_public()
370                        .to_raw_key()
371                        .hash(),
372                );
373
374                let stake_credential = Credential::from_keyhash(
375                    &root_key.derive(2).derive(0).to_public().to_raw_key().hash(),
376                );
377
378                self.create_addresses(payment_credential, stake_credential)
379            }
380            WalletType::RootKeyWallet(root_key_wallet) => {
381                let mut root_key = Bip32PrivateKey::from_bech32(&root_key_wallet.root_key)
382                    .map_err(WError::from_err(
383                        "Wallet - init_addresses - invalid root key hex",
384                    ))
385                    .unwrap();
386                for index in root_key_wallet.derivation_indices.0.iter().take(3) {
387                    root_key = root_key.derive(index.clone());
388                }
389
390                let payment_credential = Credential::from_keyhash(
391                    &root_key
392                        .derive(root_key_wallet.derivation_indices.0[3].clone())
393                        .derive(root_key_wallet.derivation_indices.0[4].clone())
394                        .to_public()
395                        .to_raw_key()
396                        .hash(),
397                );
398
399                let stake_credential = Credential::from_keyhash(
400                    &root_key.derive(2).derive(0).to_public().to_raw_key().hash(),
401                );
402
403                self.create_addresses(payment_credential, stake_credential)
404            }
405            WalletType::Cli(_private_key) => Addresses {
406                base_address: None,
407                enterprise_address: None,
408            },
409        };
410        self
411    }
412
413    /// Helper method to create addresses from payment and stake credentials.
414    /// This reduces code duplication between wallet types.
415    fn create_addresses(
416        &self,
417        payment_credential: Credential,
418        stake_credential: Credential,
419    ) -> Addresses {
420        Addresses {
421            base_address: Some(BaseAddress::new(
422                self.network_id as u8,
423                &payment_credential,
424                &stake_credential,
425            )),
426            enterprise_address: Some(EnterpriseAddress::new(
427                self.network_id as u8,
428                &payment_credential,
429            )),
430        }
431    }
432
433    pub fn sign_tx(&self, tx_hex: &str) -> Result<String, WError> {
434        match &self.wallet_type {
435            WalletType::Cli(cli_skey) => {
436                let signed_tx = sign_transaction(tx_hex, &[cli_skey])
437                    .map_err(WError::from_err("Wallet - sign_tx"))?;
438                Ok(signed_tx)
439            }
440            _ => {
441                let account = self.account.as_ref().ok_or_else(WError::from_opt(
442                    "Wallet - sign_tx",
443                    "get account from wallet",
444                ))?;
445                let signed_tx = account
446                    .sign_transaction(tx_hex)
447                    .map_err(WError::from_err("Wallet - sign_tx"))?;
448                Ok(signed_tx.to_string())
449            }
450        }
451    }
452
453    pub fn get_account(wallet_type: &WalletType) -> Result<Account, WError> {
454        let private_key: PrivateKey = match wallet_type {
455            WalletType::MnemonicWallet(mnemonic_wallet) => {
456                let mnemonic =
457                    Mnemonic::from_phrase(&mnemonic_wallet.mnemonic_phrase, Language::English)
458                        .map_err(WError::from_err(
459                            "Wallet - get_account - failed to create mnemonic",
460                        ))?;
461                let entropy = mnemonic.entropy();
462                let mut root_key = Bip32PrivateKey::from_bip39_entropy(entropy, &[]);
463                for index in &mnemonic_wallet.derivation_indices.0 {
464                    root_key = root_key.derive(index.clone());
465                }
466                root_key.to_raw_key()
467            }
468            WalletType::RootKeyWallet(root_key_wallet) => {
469                let mut root_key = Bip32PrivateKey::from_bech32(&root_key_wallet.root_key)
470                    .map_err(WError::from_err(
471                        "Wallet - get_account - invalid root key hex",
472                    ))?;
473                for index in &root_key_wallet.derivation_indices.0 {
474                    root_key = root_key.derive(index.clone());
475                }
476                root_key.to_raw_key()
477            }
478            WalletType::Cli(private_key) => PrivateKey::from_hex(&private_key).map_err(
479                WError::from_err("Wallet - get_account - invalid private key hex"),
480            )?,
481        };
482        let public_key = private_key.to_public();
483        Ok(Account {
484            private_key,
485            public_key,
486        })
487    }
488
489    /// Gets a wallet address based on the specified address type.
490    ///
491    /// # Arguments
492    ///
493    /// * `address_type` - The type of address to get (Payment or Enterprise)
494    ///
495    /// # Returns
496    ///
497    /// A Result containing either the bech32-encoded address or an error
498    pub fn get_change_address(&self, address_type: AddressType) -> Result<String, WError> {
499        match address_type {
500            AddressType::Payment => {
501                if let Some(base_address) = &self.addresses.base_address {
502                    let address = base_address.to_address();
503                    address.to_bech32(None).map_err(WError::from_err(
504                        "Failed to convert payment address to bech32",
505                    ))
506                } else {
507                    Err(WError::from_err(
508                        "Base address not available for this wallet type",
509                    )("Base address not initialized"))
510                }
511            }
512            AddressType::Enterprise => {
513                if let Some(enterprise_address) = &self.addresses.enterprise_address {
514                    let address = enterprise_address.to_address();
515                    address.to_bech32(None).map_err(WError::from_err(
516                        "Failed to convert enterprise address to bech32",
517                    ))
518                } else {
519                    Err(WError::from_err(
520                        "Enterprise address not available for this wallet type",
521                    )("Enterprise address not initialized"))
522                }
523            }
524        }
525    }
526
527    /// Fetches unspent transaction outputs (UTxOs) for a wallet address.
528    ///
529    /// # Arguments
530    ///
531    /// * `address_type` - Optional address type (Payment or Enterprise). Defaults to Payment if not specified.
532    /// * `asset` - Optional asset ID to filter UTxOs. If specified, only UTxOs containing the asset will be returned.
533    ///
534    /// # Returns
535    ///
536    /// A Result containing either a vector of UTxOs or an error
537    ///
538    /// # Errors
539    ///
540    /// Returns an error if no fetcher is configured or if there's an issue getting the address or fetching UTxOs.
541    pub async fn get_utxos(
542        &self,
543        address_type: Option<AddressType>,
544        asset: Option<&str>,
545    ) -> Result<Vec<UTxO>, WError> {
546        let fetcher = self.fetcher.as_ref().ok_or_else(|| {
547            WError::from_err("Fetcher is required to fetch UTxOs. Please provide a fetcher.")(
548                "No fetcher provided",
549            )
550        })?;
551
552        let address_type = address_type.unwrap_or(AddressType::Payment);
553        let address = self.get_change_address(address_type)?;
554
555        fetcher
556            .fetch_address_utxos(&address, asset)
557            .await
558            .map_err(WError::from_err("Failed to fetch UTxOs"))
559    }
560
561    /// Fetches suitable collateral UTXOs from the wallet.
562    ///
563    /// Collateral UTXOs must:
564    /// 1. Contain only lovelace (no other assets)
565    /// 2. Have at least 5,000,000 lovelace (5 ADA)
566    ///
567    /// This method returns the smallest suitable UTxO to minimize locked collateral.
568    ///
569    /// # Arguments
570    ///
571    /// * `address_type` - Optional address type to fetch UTXOs from. Defaults to Payment.
572    ///
573    /// # Returns
574    ///
575    /// A Result containing either a vector with the smallest suitable collateral UTxO,
576    /// or an empty vector if no suitable UTxO is found, or an error.
577    pub async fn get_collateral(
578        &self,
579        address_type: Option<AddressType>,
580    ) -> Result<Vec<UTxO>, WError> {
581        let address_type = address_type.unwrap_or(AddressType::Payment);
582        let utxos = self.get_utxos(Some(address_type), None).await?;
583
584        let mut collateral_candidates: Vec<UTxO> = utxos
585            .into_iter()
586            .filter(|utxo| {
587                utxo.output.amount.len() == 1
588                    && utxo.output.amount[0].unit() == "lovelace"
589                    && utxo.output.amount[0].quantity_i128() >= 5_000_000
590            })
591            .collect();
592
593        collateral_candidates.sort_by(|a, b| {
594            let a_quantity = a.output.amount[0].quantity_i128();
595            let b_quantity = b.output.amount[0].quantity_i128();
596            a_quantity.cmp(&b_quantity)
597        });
598
599        if let Some(smallest_utxo) = collateral_candidates.first() {
600            Ok(vec![smallest_utxo.clone()])
601        } else {
602            Ok(vec![])
603        }
604    }
605
606    /// Submits a transaction to the Cardano blockchain.
607    ///
608    /// This method uses the configured submitter to send the transaction to the network.
609    /// A submitter must be attached to the wallet using `with_submitter` before calling this method.
610    ///
611    /// # Arguments
612    ///
613    /// * `tx_hex` - The transaction in hexadecimal format to submit
614    ///
615    /// # Returns
616    ///
617    /// A Result containing either the transaction hash (if successful) or an error
618    ///
619    /// # Errors
620    ///
621    /// Returns an error if:
622    /// - No submitter is configured for the wallet
623    /// - The submitter encounters an issue while submitting the transaction
624    /// - The transaction format is invalid
625    ///
626    pub async fn submit_tx(&self, tx_hex: &str) -> Result<String, WError> {
627        let submitter = self.submitter.as_ref().ok_or_else(|| {
628            WError::from_err(
629                "Submitter is required to submit transactions. Please provide a submitter.",
630            )("No submitter provided")
631        })?;
632
633        submitter.submit_tx(tx_hex).await
634    }
635}
636
637impl Default for Wallet {
638    fn default() -> Self {
639        Self::empty()
640    }
641}