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 ®istered.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 (®istered.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 ®istered_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 ®istered_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 ®.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, .. } = ®.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) = ®.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 } = ®istered.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) = ®istered.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 } = ®istered.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}