solana_address_book/
address_book.rs

1//! Core address book implementation for managing Solana addresses with labels and roles.
2
3use crate::pda_seeds::find_pda_with_bump_and_strings;
4use crate::registered_address::{AddressRole, RegisteredAddress};
5use anchor_lang::prelude::*;
6use anchor_lang::solana_program::system_program;
7use anyhow::{Result, anyhow};
8use colored::*;
9use std::collections::{HashMap, HashSet};
10
11/// Address book for mapping public keys to registered addresses with labels.
12///
13/// This structure maintains multiple mappings to efficiently track and query
14/// Solana addresses by their public keys, labels, and roles. It's designed
15/// to help with debugging and transaction analysis by providing meaningful
16/// context for addresses.
17#[derive(Clone, Debug, Default)]
18pub struct AddressBook {
19    addresses: HashMap<Pubkey, Vec<(String, RegisteredAddress)>>,
20    registered_addresses: HashSet<RegisteredAddress>,
21    labels: HashMap<String, RegisteredAddress>,
22}
23
24impl AddressBook {
25    /// Creates a new empty address book.
26    ///
27    /// # Example
28    ///
29    /// ```
30    /// use solana_address_book::AddressBook;
31    ///
32    /// let book = AddressBook::new();
33    /// assert!(book.is_empty());
34    /// ```
35    pub fn new() -> Self {
36        Self {
37            addresses: HashMap::new(),
38            registered_addresses: HashSet::new(),
39            labels: HashMap::new(),
40        }
41    }
42
43    /// Adds default Solana system programs to the address book.
44    ///
45    /// This includes:
46    /// - System Program
47    /// - Token Program
48    /// - Associated Token Program
49    ///
50    /// # Errors
51    ///
52    /// Returns an error if any of the default programs fail to be added
53    /// (e.g., due to duplicate labels).
54    ///
55    /// # Example
56    ///
57    /// ```
58    /// use solana_address_book::AddressBook;
59    ///
60    /// let mut book = AddressBook::new();
61    /// book.add_default_accounts().unwrap();
62    ///
63    /// // The system program is now registered
64    /// assert!(book.contains(&anchor_lang::solana_program::system_program::ID));
65    /// ```
66    pub fn add_default_accounts(&mut self) -> Result<()> {
67        self.add_program(system_program::ID, "system_program")?;
68        self.add_program(anchor_spl::token::ID, "token_program")?;
69        self.add_program(anchor_spl::associated_token::ID, "associated_token_program")?;
70        Ok(())
71    }
72
73    /// Gets the label for a given public key.
74    ///
75    /// If the address is registered, returns its label. Otherwise, returns
76    /// the string representation of the public key.
77    ///
78    /// # Example
79    ///
80    /// ```
81    /// use solana_address_book::AddressBook;
82    /// use anchor_lang::prelude::*;
83    ///
84    /// let mut book = AddressBook::new();
85    /// let wallet = Pubkey::new_unique();
86    ///
87    /// // Before registration, returns the pubkey string
88    /// assert_eq!(book.get_label(&wallet), wallet.to_string());
89    ///
90    /// // After registration, returns the label
91    /// book.add_wallet(wallet, "alice".to_string()).unwrap();
92    /// assert_eq!(book.get_label(&wallet), "alice");
93    /// ```
94    pub fn get_label(&self, pubkey: &Pubkey) -> String {
95        self.addresses
96            .get(pubkey)
97            .and_then(|v| v.first())
98            .map(|(label, _)| label.clone())
99            .unwrap_or_else(|| pubkey.to_string())
100    }
101
102    /// Adds an address with a registered address and label to the address book.
103    ///
104    /// This is the core method for adding addresses. All other add methods
105    /// (add_wallet, add_mint, etc.) internally use this method.
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if the label already exists with a different address or role.
110    ///
111    /// # Example
112    ///
113    /// ```
114    /// use solana_address_book::{AddressBook, RegisteredAddress};
115    /// use anchor_lang::prelude::*;
116    ///
117    /// let mut book = AddressBook::new();
118    /// let wallet = Pubkey::new_unique();
119    /// let registered = RegisteredAddress::wallet(wallet);
120    ///
121    /// book.add(wallet, "my_wallet".to_string(), registered).unwrap();
122    /// assert_eq!(book.get_label(&wallet), "my_wallet");
123    /// ```
124    pub fn add(
125        &mut self,
126        pubkey: Pubkey,
127        label: String,
128        registered_address: RegisteredAddress,
129    ) -> Result<()> {
130        // Check if this label already exists
131        if let Some(existing_address) = self.labels.get(&label) {
132            if existing_address.key != pubkey || existing_address.role != registered_address.role {
133                return Err(anyhow!("Label '{}' already exists in address book", label));
134            }
135            return Ok(());
136        }
137
138        // Add to labels and registered addresses
139        self.labels
140            .insert(label.clone(), registered_address.clone());
141        self.registered_addresses.insert(registered_address.clone());
142
143        // Add to addresses vector (allows multiple registrations per pubkey)
144        self.addresses
145            .entry(pubkey)
146            .or_default()
147            .push((label, registered_address));
148
149        Ok(())
150    }
151
152    /// Adds a wallet address to the address book.
153    ///
154    /// # Errors
155    ///
156    /// Returns an error if the label already exists with a different address.
157    ///
158    /// # Example
159    ///
160    /// ```
161    /// use solana_address_book::AddressBook;
162    /// use anchor_lang::prelude::*;
163    ///
164    /// let mut book = AddressBook::new();
165    /// let wallet = Pubkey::new_unique();
166    ///
167    /// book.add_wallet(wallet, "alice".to_string()).unwrap();
168    ///
169    /// // The wallet is now registered
170    /// assert!(book.contains(&wallet));
171    /// assert_eq!(book.get_label(&wallet), "alice");
172    /// ```
173    pub fn add_wallet(&mut self, pubkey: Pubkey, label: String) -> Result<()> {
174        self.add(pubkey, label, RegisteredAddress::wallet(pubkey))
175    }
176
177    /// Adds a custom role address to the address book.
178    ///
179    /// Use this for addresses that don't fit into the standard categories.
180    ///
181    /// # Errors
182    ///
183    /// Returns an error if the label already exists with a different address.
184    ///
185    /// # Example
186    ///
187    /// ```
188    /// use solana_address_book::AddressBook;
189    /// use anchor_lang::prelude::*;
190    ///
191    /// let mut book = AddressBook::new();
192    /// let address = Pubkey::new_unique();
193    ///
194    /// book.add_custom(
195    ///     address,
196    ///     "dao_treasury".to_string(),
197    ///     "governance".to_string()
198    /// ).unwrap();
199    ///
200    /// assert_eq!(book.get_label(&address), "dao_treasury");
201    /// ```
202    pub fn add_custom(&mut self, pubkey: Pubkey, label: String, custom_role: String) -> Result<()> {
203        self.add(
204            pubkey,
205            label,
206            RegisteredAddress::custom(pubkey, &custom_role),
207        )
208    }
209
210    /// Adds a Program Derived Address (PDA) to the address book.
211    ///
212    /// # Arguments
213    ///
214    /// * `pubkey` - The PDA's public key
215    /// * `label` - Human-readable label for the PDA
216    /// * `seeds` - The string representations of seeds used to derive the PDA
217    /// * `program_id` - The program that owns the PDA
218    /// * `bump` - The bump seed used to derive the PDA
219    ///
220    /// # Errors
221    ///
222    /// Returns an error if the label already exists with a different address.
223    ///
224    /// # Example
225    ///
226    /// ```
227    /// use solana_address_book::AddressBook;
228    /// use anchor_lang::prelude::*;
229    ///
230    /// let mut book = AddressBook::new();
231    /// let pda = Pubkey::new_unique();
232    /// let program = Pubkey::new_unique();
233    ///
234    /// book.add_pda(
235    ///     pda,
236    ///     "vault".to_string(),
237    ///     vec!["vault".to_string(), "v1".to_string()],
238    ///     program,
239    ///     255
240    /// ).unwrap();
241    ///
242    /// assert_eq!(book.get_label(&pda), "vault");
243    /// ```
244    pub fn add_pda(
245        &mut self,
246        pubkey: Pubkey,
247        label: String,
248        seeds: Vec<String>,
249        program_id: Pubkey,
250        bump: u8,
251    ) -> Result<()> {
252        self.add(
253            pubkey,
254            label,
255            RegisteredAddress::pda_from_parts(pubkey, seeds, program_id, bump),
256        )
257    }
258
259    /// Adds a program address to the address book.
260    ///
261    /// # Errors
262    ///
263    /// Returns an error if the label already exists with a different address.
264    ///
265    /// # Example
266    ///
267    /// ```
268    /// use solana_address_book::AddressBook;
269    /// use anchor_lang::prelude::*;
270    ///
271    /// let mut book = AddressBook::new();
272    /// let program = Pubkey::new_unique();
273    ///
274    /// book.add_program(program, "my_program").unwrap();
275    /// assert_eq!(book.get_label(&program), "my_program");
276    /// ```
277    pub fn add_program(&mut self, pubkey: Pubkey, label: &str) -> Result<()> {
278        self.add(
279            pubkey,
280            label.to_string(),
281            RegisteredAddress::program(pubkey),
282        )
283    }
284
285    /// Finds a PDA with bump and adds it to the address book.
286    ///
287    /// This method derives the PDA from the provided seeds and program ID,
288    /// then automatically registers it in the address book.
289    ///
290    /// # Returns
291    ///
292    /// A tuple containing the derived PDA public key and bump seed.
293    ///
294    /// # Errors
295    ///
296    /// Returns an error if the label already exists with a different address.
297    ///
298    /// # Example
299    ///
300    /// ```
301    /// use solana_address_book::AddressBook;
302    /// use anchor_lang::prelude::*;
303    ///
304    /// let mut book = AddressBook::new();
305    /// let program = Pubkey::new_unique();
306    /// let user = Pubkey::new_unique();
307    ///
308    /// let (pda, bump) = book.find_pda_with_bump(
309    ///     "user_vault",
310    ///     &[b"vault", user.as_ref()],
311    ///     program
312    /// ).unwrap();
313    ///
314    /// assert_eq!(book.get_label(&pda), "user_vault");
315    /// ```
316    pub fn find_pda_with_bump(
317        &mut self,
318        label: &str,
319        seeds: &[&[u8]],
320        program_id: Pubkey,
321    ) -> Result<(Pubkey, u8)> {
322        // Use the helper function from pda_seeds module
323        let derived_pda = find_pda_with_bump_and_strings(seeds, &program_id);
324
325        // Add to address book
326        self.add_pda(
327            derived_pda.key,
328            label.to_string(),
329            derived_pda.seed_strings,
330            program_id,
331            derived_pda.bump,
332        )?;
333
334        Ok((derived_pda.key, derived_pda.bump))
335    }
336
337    /// Gets all registered addresses for a public key.
338    ///
339    /// A single public key can have multiple registrations with different labels.
340    ///
341    /// # Example
342    ///
343    /// ```
344    /// use solana_address_book::AddressBook;
345    /// use anchor_lang::prelude::*;
346    ///
347    /// let mut book = AddressBook::new();
348    /// let wallet = Pubkey::new_unique();
349    ///
350    /// book.add_wallet(wallet, "alice".to_string()).unwrap();
351    ///
352    /// let registrations = book.get(&wallet);
353    /// assert!(registrations.is_some());
354    /// assert_eq!(registrations.unwrap().len(), 1);
355    /// ```
356    pub fn get(&self, pubkey: &Pubkey) -> Option<&Vec<(String, RegisteredAddress)>> {
357        self.addresses.get(pubkey)
358    }
359
360    /// Gets the first registered address for a public key.
361    ///
362    /// Returns the first label and registered address pair if the pubkey exists.
363    ///
364    /// # Example
365    ///
366    /// ```
367    /// use solana_address_book::AddressBook;
368    /// use anchor_lang::prelude::*;
369    ///
370    /// let mut book = AddressBook::new();
371    /// let wallet = Pubkey::new_unique();
372    ///
373    /// book.add_wallet(wallet, "alice".to_string()).unwrap();
374    ///
375    /// let (label, reg) = book.get_first(&wallet).unwrap();
376    /// assert_eq!(*label, "alice");
377    /// ```
378    pub fn get_first(&self, pubkey: &Pubkey) -> Option<(&String, &RegisteredAddress)> {
379        self.addresses
380            .get(pubkey)
381            .and_then(|v| v.first())
382            .map(|(label, reg)| (label, reg))
383    }
384
385    /// Finds an address by its role.
386    ///
387    /// Returns the first address that matches the specified role.
388    ///
389    /// # Example
390    ///
391    /// ```
392    /// use solana_address_book::{AddressBook, AddressRole};
393    /// use anchor_lang::prelude::*;
394    ///
395    /// let mut book = AddressBook::new();
396    /// let wallet = Pubkey::new_unique();
397    ///
398    /// book.add_wallet(wallet, "alice".to_string()).unwrap();
399    ///
400    /// let found = book.get_by_role(&AddressRole::Wallet);
401    /// assert_eq!(found, Some(wallet));
402    /// ```
403    pub fn get_by_role(&self, role: &AddressRole) -> Option<Pubkey> {
404        for registered in self.registered_addresses.iter() {
405            if &registered.role == role {
406                return Some(registered.key);
407            }
408        }
409        None
410    }
411
412    /// Gets all addresses with a specific role type.
413    ///
414    /// Role types are: "wallet", "mint", "ata", "pda", "program", "custom"
415    ///
416    /// # Example
417    ///
418    /// ```
419    /// use solana_address_book::AddressBook;
420    /// use anchor_lang::prelude::*;
421    ///
422    /// let mut book = AddressBook::new();
423    /// let wallet1 = Pubkey::new_unique();
424    /// let wallet2 = Pubkey::new_unique();
425    ///
426    /// book.add_wallet(wallet1, "alice".to_string()).unwrap();
427    /// book.add_wallet(wallet2, "bob".to_string()).unwrap();
428    ///
429    /// let wallets = book.get_all_by_role_type("wallet");
430    /// assert_eq!(wallets.len(), 2);
431    /// assert!(wallets.contains(&wallet1));
432    /// assert!(wallets.contains(&wallet2));
433    /// ```
434    pub fn get_all_by_role_type(&self, role_type: &str) -> Vec<Pubkey> {
435        let mut addresses = Vec::new();
436        for registered in self.registered_addresses.iter() {
437            match (&registered.role, role_type) {
438                (AddressRole::Wallet, "wallet")
439                | (AddressRole::Mint, "mint")
440                | (AddressRole::Program, "program") => {
441                    addresses.push(registered.key);
442                }
443                (AddressRole::Ata { .. }, "ata") => {
444                    addresses.push(registered.key);
445                }
446                (AddressRole::Pda { .. }, "pda") => {
447                    addresses.push(registered.key);
448                }
449                (AddressRole::Custom(_), "custom") => {
450                    addresses.push(registered.key);
451                }
452                _ => {}
453            }
454        }
455        addresses
456    }
457
458    /// Gets a formatted string representation of an address with colors.
459    ///
460    /// If the address is registered, returns a colored label with its role.
461    /// Otherwise, returns the address string in red.
462    ///
463    /// # Example
464    ///
465    /// ```
466    /// use solana_address_book::AddressBook;
467    /// use anchor_lang::prelude::*;
468    ///
469    /// let mut book = AddressBook::new();
470    /// let wallet = Pubkey::new_unique();
471    ///
472    /// book.add_wallet(wallet, "alice".to_string()).unwrap();
473    ///
474    /// let formatted = book.format_address(&wallet);
475    /// assert!(formatted.contains("alice"));
476    /// assert!(formatted.contains("[wallet]"));
477    /// ```
478    pub fn format_address(&self, pubkey: &Pubkey) -> String {
479        match self.get_first(pubkey) {
480            Some((label, registered_address)) => match &registered_address.role {
481                AddressRole::Wallet => format!(
482                    "{} {}",
483                    label.bright_cyan().bold(),
484                    "[wallet]".to_string().dimmed()
485                ),
486                AddressRole::Mint => format!(
487                    "{} {}",
488                    label.bright_green().bold(),
489                    "[mint]".to_string().dimmed()
490                ),
491                AddressRole::Ata { .. } => format!(
492                    "{} {}",
493                    label.bright_yellow().bold(),
494                    "[ata]".to_string().dimmed()
495                ),
496                AddressRole::Pda { seeds, .. } => format!(
497                    "{} {}",
498                    label.bright_magenta().bold(),
499                    format!("[pda:{}]", seeds.first().unwrap_or(&"".to_string())).dimmed()
500                ),
501                AddressRole::Program => format!(
502                    "{} {}",
503                    label.bright_blue().bold(),
504                    "[program]".to_string().dimmed()
505                ),
506                AddressRole::Custom(role) => format!(
507                    "{} {}",
508                    label.bright_white().bold(),
509                    format!("[{role}]").dimmed()
510                ),
511            },
512            None => format!("{}", pubkey.to_string().bright_red()),
513        }
514    }
515
516    /// Replaces all public key addresses in text with their labels.
517    ///
518    /// Scans the provided text for any registered public keys and replaces
519    /// them with their colored labels.
520    ///
521    /// # Example
522    ///
523    /// ```
524    /// use solana_address_book::{AddressBook, RegisteredAddress};
525    /// use anchor_lang::prelude::*;
526    ///
527    /// let mut book = AddressBook::new();
528    /// let token = Pubkey::new_unique();
529    ///
530    /// book.add(token, "my_token".to_string(), RegisteredAddress::mint(token)).unwrap();
531    ///
532    /// let text = format!("Transfer to {}", token);
533    /// let formatted = book.replace_addresses_in_text(&text);
534    ///
535    /// // The pubkey is replaced with the colored label
536    /// assert!(formatted.contains("my_token"));
537    /// assert!(!formatted.contains(&token.to_string()));
538    /// ```
539    pub fn replace_addresses_in_text(&self, text: &str) -> String {
540        let mut result = text.to_string();
541
542        // Sort by pubkey string length (longest first) to avoid partial replacements
543        let mut sorted_addresses: Vec<_> = self.addresses.iter().collect();
544        sorted_addresses.sort_by_key(|(pubkey, _)| std::cmp::Reverse(pubkey.to_string().len()));
545
546        for (pubkey, registered_addresses) in sorted_addresses {
547            if let Some((label, registered_address)) = registered_addresses.first() {
548                let pubkey_str = pubkey.to_string();
549                let replacement = match &registered_address.role {
550                    AddressRole::Wallet => format!("{}", label.bright_cyan().bold()),
551                    AddressRole::Mint => format!("{}", label.bright_green().bold()),
552                    AddressRole::Ata { .. } => format!("{}", label.bright_yellow().bold()),
553                    AddressRole::Pda { .. } => format!("{}", label.bright_magenta().bold()),
554                    AddressRole::Program => format!("{}", label.bright_blue().bold()),
555                    AddressRole::Custom(_) => format!("{}", label.bright_white().bold()),
556                };
557                result = result.replace(&pubkey_str, &replacement);
558            }
559        }
560
561        result
562    }
563
564    /// Prints all addresses in the address book with colored formatting.
565    ///
566    /// Addresses are grouped by role type and displayed with appropriate colors.
567    /// This is useful for debugging and getting an overview of all registered addresses.
568    ///
569    /// # Example
570    ///
571    /// ```
572    /// use solana_address_book::{AddressBook, RegisteredAddress};
573    /// use anchor_lang::prelude::*;
574    ///
575    /// let mut book = AddressBook::new();
576    /// book.add_wallet(Pubkey::new_unique(), "alice".to_string()).unwrap();
577    /// let mint = Pubkey::new_unique();
578    /// book.add(mint, "usdc".to_string(), RegisteredAddress::mint(mint)).unwrap();
579    ///
580    /// // Prints a formatted table of all addresses
581    /// book.print_all();
582    /// ```
583    pub fn print_all(&self) {
584        if self.addresses.is_empty() {
585            println!("📖 Address book is empty");
586            return;
587        }
588
589        println!("\n{}", "═".repeat(80).dimmed());
590        println!(
591            "📖 {} ({} entries):",
592            "Address Book".bold(),
593            self.addresses.len()
594        );
595        println!("{}", "─".repeat(80).dimmed());
596
597        // Group by role type
598        let mut wallets = Vec::new();
599        let mut mints = Vec::new();
600        let mut atas = Vec::new();
601        let mut pdas = Vec::new();
602        let mut programs = Vec::new();
603        let mut custom = Vec::new();
604
605        for (pubkey, regs) in &self.addresses {
606            for (label, reg) in regs {
607                match &reg.role {
608                    AddressRole::Wallet => wallets.push((pubkey, label, reg)),
609                    AddressRole::Mint => mints.push((pubkey, label, reg)),
610                    AddressRole::Ata { .. } => atas.push((pubkey, label, reg)),
611                    AddressRole::Pda { .. } => pdas.push((pubkey, label, reg)),
612                    AddressRole::Program => programs.push((pubkey, label, reg)),
613                    AddressRole::Custom(_) => custom.push((pubkey, label, reg)),
614                }
615            }
616        }
617
618        // Print each category
619        if !programs.is_empty() {
620            println!(
621                "\n  {} {}:",
622                "Programs".bright_blue().bold(),
623                format!("({})", programs.len()).dimmed()
624            );
625            for (pubkey, label, _reg) in programs {
626                println!(
627                    "    {} {:<30} {}",
628                    "•".to_string().bright_blue(),
629                    label.bright_blue().bold(),
630                    pubkey.to_string().dimmed()
631                );
632            }
633        }
634
635        if !wallets.is_empty() {
636            println!(
637                "\n  {} {}:",
638                "Wallets".bright_cyan().bold(),
639                format!("({})", wallets.len()).dimmed()
640            );
641            for (pubkey, label, _reg) in wallets {
642                println!(
643                    "    {} {:<30} {}",
644                    "•".to_string().bright_cyan(),
645                    label.bright_cyan().bold(),
646                    pubkey.to_string().dimmed()
647                );
648            }
649        }
650
651        if !mints.is_empty() {
652            println!(
653                "\n  {} {}:",
654                "Mints".bright_green().bold(),
655                format!("({})", mints.len()).dimmed()
656            );
657            for (pubkey, label, _reg) in mints {
658                println!(
659                    "    {} {:<30} {}",
660                    "•".to_string().bright_green(),
661                    label.bright_green().bold(),
662                    pubkey.to_string().dimmed()
663                );
664            }
665        }
666
667        if !pdas.is_empty() {
668            println!(
669                "\n  {} {}:",
670                "PDAs".bright_magenta().bold(),
671                format!("({})", pdas.len()).dimmed()
672            );
673            for (pubkey, label, reg) in pdas {
674                if let AddressRole::Pda { seeds, .. } = &reg.role {
675                    println!(
676                        "    {} {:<30} {} [{}]",
677                        "•".to_string().bright_magenta(),
678                        label.to_string().bright_magenta().bold(),
679                        pubkey.to_string().dimmed(),
680                        seeds.join(",").dimmed()
681                    );
682                }
683            }
684        }
685
686        if !atas.is_empty() {
687            println!(
688                "\n  {} {}:",
689                "ATAs".bright_yellow().bold(),
690                format!("({})", atas.len()).dimmed()
691            );
692            for (pubkey, label, _reg) in atas {
693                println!(
694                    "    {} {:<30} {}",
695                    "•".to_string().bright_yellow(),
696                    label.bright_yellow().bold(),
697                    pubkey.to_string().dimmed()
698                );
699            }
700        }
701
702        if !custom.is_empty() {
703            println!(
704                "\n  {} {}:",
705                "Custom".bright_white().bold(),
706                format!("({})", custom.len()).dimmed()
707            );
708            for (pubkey, label, reg) in custom {
709                if let AddressRole::Custom(role) = &reg.role {
710                    println!(
711                        "    {} {:<30} {} [{}]",
712                        "•".to_string().bright_white(),
713                        label.bright_white().bold(),
714                        pubkey.to_string().dimmed(),
715                        role.dimmed()
716                    );
717                }
718            }
719        }
720
721        println!("{}", "═".repeat(80).dimmed());
722    }
723
724    /// Checks if an address exists in the book.
725    ///
726    /// # Example
727    ///
728    /// ```
729    /// use solana_address_book::AddressBook;
730    /// use anchor_lang::prelude::*;
731    ///
732    /// let mut book = AddressBook::new();
733    /// let wallet = Pubkey::new_unique();
734    ///
735    /// assert!(!book.contains(&wallet));
736    ///
737    /// book.add_wallet(wallet, "alice".to_string()).unwrap();
738    /// assert!(book.contains(&wallet));
739    /// ```
740    pub fn contains(&self, pubkey: &Pubkey) -> bool {
741        self.addresses.contains_key(pubkey)
742    }
743
744    /// Returns the number of unique public keys in the address book.
745    ///
746    /// Note: This counts unique public keys, not total registrations.
747    /// A single pubkey can have multiple registrations with different labels.
748    ///
749    /// # Example
750    ///
751    /// ```
752    /// use solana_address_book::AddressBook;
753    /// use anchor_lang::prelude::*;
754    ///
755    /// let mut book = AddressBook::new();
756    /// assert_eq!(book.len(), 0);
757    ///
758    /// book.add_wallet(Pubkey::new_unique(), "alice".to_string()).unwrap();
759    /// assert_eq!(book.len(), 1);
760    /// ```
761    pub fn len(&self) -> usize {
762        self.addresses.len()
763    }
764
765    /// Checks if the address book is empty.
766    ///
767    /// # Example
768    ///
769    /// ```
770    /// use solana_address_book::AddressBook;
771    /// use anchor_lang::prelude::*;
772    ///
773    /// let mut book = AddressBook::new();
774    /// assert!(book.is_empty());
775    ///
776    /// book.add_wallet(Pubkey::new_unique(), "alice".to_string()).unwrap();
777    /// assert!(!book.is_empty());
778    /// ```
779    pub fn is_empty(&self) -> bool {
780        self.addresses.is_empty()
781    }
782}
783
784#[cfg(test)]
785mod tests {
786    use super::*;
787
788    #[test]
789    fn test_address_book_new() {
790        let book = AddressBook::new();
791        assert_eq!(book.len(), 0);
792        assert!(book.is_empty());
793    }
794
795    #[test]
796    fn test_add_wallet() {
797        let mut book = AddressBook::new();
798        let pubkey = Pubkey::new_unique();
799
800        book.add_wallet(pubkey, "test_wallet".to_string()).unwrap();
801
802        assert_eq!(book.len(), 1);
803        assert!(book.contains(&pubkey));
804        assert_eq!(book.get_label(&pubkey), "test_wallet");
805    }
806
807    #[test]
808    fn test_add_mint() -> Result<()> {
809        let mut book = AddressBook::new();
810        let pubkey = Pubkey::new_unique();
811
812        book.add(
813            pubkey,
814            "test_mint".to_string(),
815            RegisteredAddress::mint(pubkey),
816        )?;
817
818        let (label, registered) = book.get_first(&pubkey).unwrap();
819        assert_eq!(*label, "test_mint");
820        matches!(registered.role, AddressRole::Mint);
821
822        Ok(())
823    }
824
825    #[test]
826    fn test_add_ata() -> Result<()> {
827        let mut book = AddressBook::new();
828        let ata_pubkey = Pubkey::new_unique();
829        let mint_pubkey = Pubkey::new_unique();
830        let owner_pubkey = Pubkey::new_unique();
831
832        book.add(
833            ata_pubkey,
834            "test_ata".to_string(),
835            RegisteredAddress::ata(ata_pubkey, mint_pubkey, owner_pubkey),
836        )?;
837
838        let (label, registered) = book.get_first(&ata_pubkey).unwrap();
839        assert_eq!(*label, "test_ata");
840        if let AddressRole::Ata { mint, owner } = &registered.role {
841            assert_eq!(*mint, mint_pubkey);
842            assert_eq!(*owner, owner_pubkey);
843        } else {
844            panic!("Expected ATA role");
845        };
846        Ok(())
847    }
848
849    #[test]
850    fn test_add_program() {
851        let mut book = AddressBook::new();
852        let pubkey = Pubkey::new_unique();
853
854        book.add_program(pubkey, "test_program").unwrap();
855
856        let (label, registered) = book.get_first(&pubkey).unwrap();
857        assert_eq!(*label, "test_program");
858        matches!(registered.role, AddressRole::Program);
859    }
860
861    #[test]
862    fn test_add_custom() {
863        let mut book = AddressBook::new();
864        let pubkey = Pubkey::new_unique();
865
866        book.add_custom(
867            pubkey,
868            "test_custom".to_string(),
869            "special_role".to_string(),
870        )
871        .unwrap();
872
873        let (label, registered) = book.get_first(&pubkey).unwrap();
874        assert_eq!(*label, "test_custom");
875        if let AddressRole::Custom(role) = &registered.role {
876            assert_eq!(role, "special_role");
877        } else {
878            panic!("Expected Custom role");
879        }
880    }
881
882    #[test]
883    fn test_duplicate_label_error() {
884        let mut book = AddressBook::new();
885        let pubkey1 = Pubkey::new_unique();
886        let pubkey2 = Pubkey::new_unique();
887
888        book.add_wallet(pubkey1, "duplicate".to_string()).unwrap();
889
890        let result = book.add_wallet(pubkey2, "duplicate".to_string());
891        assert!(result.is_err());
892        assert!(result.unwrap_err().to_string().contains("already exists"));
893    }
894
895    #[test]
896    fn test_get_by_role() {
897        let mut book = AddressBook::new();
898        let pubkey = Pubkey::new_unique();
899
900        book.add_wallet(pubkey, "test_wallet".to_string()).unwrap();
901
902        let found = book.get_by_role(&AddressRole::Wallet);
903        assert_eq!(found, Some(pubkey));
904
905        let not_found = book.get_by_role(&AddressRole::Mint);
906        assert_eq!(not_found, None);
907    }
908
909    #[test]
910    fn test_get_all_by_role_type() {
911        let mut book = AddressBook::new();
912        let wallet1 = Pubkey::new_unique();
913        let wallet2 = Pubkey::new_unique();
914        let mint = Pubkey::new_unique();
915
916        book.add_wallet(wallet1, "wallet1".to_string()).unwrap();
917        book.add_wallet(wallet2, "wallet2".to_string()).unwrap();
918        book.add(mint, "mint1".to_string(), RegisteredAddress::mint(mint))
919            .unwrap();
920
921        let wallets = book.get_all_by_role_type("wallet");
922        assert_eq!(wallets.len(), 2);
923        assert!(wallets.contains(&wallet1));
924        assert!(wallets.contains(&wallet2));
925
926        let mints = book.get_all_by_role_type("mint");
927        assert_eq!(mints.len(), 1);
928        assert!(mints.contains(&mint));
929    }
930
931    #[test]
932    fn test_pda_creation() {
933        let program_id = Pubkey::new_unique();
934        let (_pubkey, bump, registered) = RegisteredAddress::pda(&[b"test", b"seed"], &program_id);
935
936        if let AddressRole::Pda {
937            seeds: pda_seeds,
938            program_id: pda_program_id,
939            bump: pda_bump,
940        } = &registered.role
941        {
942            assert_eq!(pda_seeds, &vec!["test".to_string(), "seed".to_string()]);
943            assert_eq!(*pda_program_id, program_id);
944            assert_eq!(*pda_bump, bump);
945        } else {
946            panic!("Expected PDA role");
947        }
948    }
949
950    #[test]
951    fn test_format_address() {
952        let mut book = AddressBook::new();
953        let pubkey = Pubkey::new_unique();
954        let unknown_pubkey = Pubkey::new_unique();
955
956        book.add_wallet(pubkey, "test_wallet".to_string()).unwrap();
957
958        let formatted = book.format_address(&pubkey);
959        assert!(formatted.contains("test_wallet"));
960
961        let unknown_formatted = book.format_address(&unknown_pubkey);
962        assert!(unknown_formatted.contains(&unknown_pubkey.to_string()));
963    }
964}