tensor_eigen/commands/whitelist/
compare.rs

1use std::{collections::HashMap, fs::File, path::PathBuf};
2
3use {
4    anyhow::Result,
5    serde::{Deserialize, Serialize},
6    serde_with::{serde_as, DisplayFromStr},
7    solana_account_decoder::UiAccountEncoding,
8    solana_client::{
9        rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
10        rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType},
11    },
12    solana_program::{pubkey, pubkey::Pubkey},
13    solana_sdk::{account::Account, commitment_config::CommitmentConfig},
14    tensor_whitelist::{
15        accounts::{Whitelist, WhitelistV2},
16        programs::TENSOR_WHITELIST_ID,
17        types::{Condition, Mode},
18    },
19};
20
21use crate::{
22    discriminators::{deserialize_account, Discriminator},
23    formatting::CustomFormat,
24    setup::CliConfig,
25    spinner::create_spinner,
26};
27
28pub const WHITELIST_SIGNER_PUBKEY: Pubkey = pubkey!("DD92UoQnVAaNgRnhvPQhxR7GJkQ9EXhHYq2TEpN8mn1J");
29
30const DEVNET_GENESIS_HASH: &str = "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG";
31const MAINNET_GENESIS_HASH: &str = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d";
32
33const DEFAULT_ROOT_HASH: [u8; 32] = [0; 32];
34
35#[serde_as]
36#[derive(Clone, Debug, Serialize, Deserialize)]
37pub struct WhitelistPair {
38    #[serde_as(as = "DisplayFromStr")]
39    pub v1_pubkey: Pubkey,
40    pub v1_data: Whitelist,
41    #[serde_as(as = "DisplayFromStr")]
42    pub v2_pubkey: Pubkey,
43    pub v2_data: Option<WhitelistV2>,
44}
45
46#[serde_as]
47#[derive(Debug, Clone, Deserialize, Serialize)]
48pub struct MissingWhitelistPair {
49    #[serde_as(as = "DisplayFromStr")]
50    pub v1_pubkey: Pubkey,
51    #[serde_as(as = "DisplayFromStr")]
52    pub v2_pubkey: Pubkey,
53}
54
55#[serde_as]
56#[derive(Debug, Clone, Deserialize, Serialize)]
57pub struct ComparisonResult {
58    #[serde_as(as = "DisplayFromStr")]
59    pub whitelist_v1: Pubkey,
60    #[serde_as(as = "DisplayFromStr")]
61    pub whitelist_v2: Pubkey,
62    pub mismatch: Option<Mismatch>,
63}
64
65#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
66pub enum Mismatch {
67    Uuid,
68    MerkleRoot,
69    Voc,
70    Fvc,
71    V2Missing,
72    V2ConditionsLength,
73    UnexpectedV2Conditions,
74}
75
76pub struct CompareParams {
77    pub keypair_path: Option<PathBuf>,
78    pub rpc_url: Option<String>,
79    pub list: Option<PathBuf>,
80    pub namespace: Option<Pubkey>,
81    pub verbose: bool,
82}
83
84pub fn handle_compare(args: CompareParams) -> Result<()> {
85    let cli_config = CliConfig::new(args.keypair_path, args.rpc_url)?;
86
87    let genesis_hash = cli_config.client.get_genesis_hash()?.to_string();
88
89    let namespace = args.namespace.unwrap_or(WHITELIST_SIGNER_PUBKEY);
90
91    let cluster = if genesis_hash == MAINNET_GENESIS_HASH {
92        "mainnet"
93    } else if genesis_hash == DEVNET_GENESIS_HASH {
94        "devnet"
95    } else {
96        "unknown"
97    };
98
99    println!("Fetching whitelists from: {}", cluster);
100
101    // Spinner with empty message we populate later.
102    let spinner = create_spinner("")?;
103
104    // Open the list file and decode into a vector of Pubkeys
105    let whitelists: Vec<(Pubkey, Account)> = if let Some(list) = args.list {
106        spinner.set_message("Opening specified file...");
107
108        let list: Vec<Pubkey> = serde_json::from_reader(File::open(&list)?)?;
109
110        let pubkeys: Vec<_> = list
111            .iter()
112            .map(|p| Whitelist::find_pda(p.to_bytes()).0)
113            .collect();
114
115        cli_config
116            .client
117            .get_multiple_accounts(&pubkeys)?
118            .into_iter()
119            .flatten()
120            .map(|account| {
121                (
122                    Whitelist::find_pda(account.data[8..40].try_into().unwrap()).0,
123                    account,
124                )
125            })
126            .collect()
127    } else {
128        spinner.set_message("Running gPA call to get all whitelist v1s...");
129
130        // GPA to find all whitelists v1s
131        let mut disc = Vec::with_capacity(8);
132        disc.extend(Whitelist::discriminator());
133
134        let filter = RpcFilterType::Memcmp(Memcmp::new(0, MemcmpEncodedBytes::Bytes(disc)));
135        let filters = vec![filter];
136
137        let config = RpcProgramAccountsConfig {
138            filters: Some(filters),
139            account_config: RpcAccountInfoConfig {
140                data_slice: None,
141                encoding: Some(UiAccountEncoding::Base64),
142                commitment: Some(CommitmentConfig::confirmed()),
143                min_context_slot: None,
144            },
145            with_context: None,
146        };
147
148        cli_config
149            .client
150            .get_program_accounts_with_config(&TENSOR_WHITELIST_ID, config)?
151    };
152    spinner.finish_and_clear();
153
154    println!("Found {} v1 whitelists on-chain", whitelists.len());
155
156    // GPA to find all whitelists v2s
157    let mut disc = Vec::with_capacity(8);
158    disc.extend(WhitelistV2::discriminator());
159
160    let filter = RpcFilterType::Memcmp(Memcmp::new(0, MemcmpEncodedBytes::Bytes(disc)));
161    let filters = vec![filter];
162
163    let config = RpcProgramAccountsConfig {
164        filters: Some(filters),
165        account_config: RpcAccountInfoConfig {
166            data_slice: None,
167            encoding: Some(UiAccountEncoding::Base64),
168            commitment: Some(CommitmentConfig::confirmed()),
169            min_context_slot: None,
170        },
171        with_context: None,
172    };
173
174    let spinner = create_spinner("Running gPA call to get all whitelist v2s...")?;
175
176    let on_chain_whitelist_v2s: HashMap<Pubkey, Account> = cli_config
177        .client
178        .get_program_accounts_with_config(&TENSOR_WHITELIST_ID, config)?
179        .into_iter()
180        .collect();
181
182    spinner.finish_and_clear();
183
184    println!(
185        "Found {} v2 whitelists on-chain",
186        on_chain_whitelist_v2s.len()
187    );
188
189    let whitelist_pairs: Vec<WhitelistPair> = whitelists
190        .into_iter()
191        .map(|(pubkey, account)| {
192            (
193                pubkey,
194                deserialize_account::<Whitelist>(&account.data).unwrap(),
195            )
196        })
197        .map(|(v1_pubkey, v1_data)| {
198            let v2_pubkey = WhitelistV2::find_pda(&namespace, v1_data.uuid).0;
199            let v2_data = on_chain_whitelist_v2s
200                .get(&v2_pubkey)
201                .and_then(|account| deserialize_account::<WhitelistV2>(&account.data).ok());
202
203            WhitelistPair {
204                v1_pubkey,
205                v1_data,
206                v2_pubkey,
207                v2_data,
208            }
209        })
210        .collect();
211
212    println!("Built pairs");
213
214    // Find missing V2s by finding all the None values in the v2 field
215    let (missing_v2s, existing_v2s): (Vec<WhitelistPair>, Vec<WhitelistPair>) = whitelist_pairs
216        .into_iter()
217        .partition(|pair| pair.v2_data.is_none());
218
219    println!("{} whitelists have no v2 on chain", missing_v2s.len());
220
221    let no_missing_v2s = missing_v2s.is_empty();
222    let number_of_missing_v2s = missing_v2s.len();
223
224    let missing_pairs: Vec<MissingWhitelistPair> = missing_v2s
225        .into_iter()
226        .map(|pair| MissingWhitelistPair {
227            v1_pubkey: pair.v1_pubkey,
228            v2_pubkey: pair.v2_pubkey,
229        })
230        .collect();
231
232    let spinner = create_spinner("Writing missing v2s to file...")?;
233    // Write v2_missing to a file
234    let file = File::create(format!("{}_v2_missing.json", cluster))?;
235    serde_json::to_writer_pretty(file, &missing_pairs)?;
236    spinner.finish_and_clear();
237
238    // Only compare the whitelists that have a v2 on chain
239    let comparison_results = compare_whitelists(&existing_v2s);
240
241    // Write any comparison results with a mismatch to a file. We need to filter out the ones with no mismatch.
242    let mismatches = comparison_results
243        .iter()
244        .filter(|result| result.mismatch.is_some())
245        .collect::<Vec<_>>();
246
247    println!(
248        "Of the {} whitelist v1s with a v2 on chain, {} have a mismatch",
249        existing_v2s.len(),
250        mismatches.len()
251    );
252
253    let spinner = create_spinner("Writing mismatches to file...")?;
254
255    let file = File::create(format!("{}_mismatches.json", cluster))?;
256    serde_json::to_writer_pretty(file, &mismatches)?;
257
258    spinner.finish_and_clear();
259
260    if args.verbose {
261        for result in comparison_results.iter() {
262            println!("{}", result.custom_format());
263            println!(); // Add a blank line between comparisons for readability
264        }
265    }
266
267    if mismatches.is_empty() && no_missing_v2s {
268        println!("All good! ✅ 😎");
269    } else {
270        println!(
271            "There are {} mismatches and {} missing v2s",
272            mismatches.len(),
273            number_of_missing_v2s
274        );
275    }
276
277    Ok(())
278}
279
280fn has_matching_condition(conditions: &[Condition], mode: Mode, value: &Pubkey) -> bool {
281    conditions
282        .iter()
283        .any(|condition| condition.mode == mode && condition.value == *value)
284}
285
286pub fn compare_whitelists(whitelist_pairs: &[WhitelistPair]) -> Vec<ComparisonResult> {
287    whitelist_pairs
288        .iter()
289        .map(|pair| {
290            let mut result = ComparisonResult {
291                whitelist_v1: pair.v1_pubkey,
292                whitelist_v2: pair.v2_pubkey,
293                mismatch: None,
294            };
295
296            let v1 = &pair.v1_data;
297            let v2 = match &pair.v2_data {
298                Some(v2) => v2,
299                None => {
300                    result.mismatch = Some(Mismatch::V2Missing);
301                    return result;
302                }
303            };
304
305            // Check UUID
306            if v1.uuid != v2.uuid {
307                result.mismatch = Some(Mismatch::Uuid);
308                return result;
309            }
310
311            // Check conditions length
312            if v2.conditions.len() != 1 {
313                result.mismatch = Some(Mismatch::V2ConditionsLength);
314                return result;
315            }
316
317            // Return early if we find a matching condition
318            if v1.root_hash != DEFAULT_ROOT_HASH {
319                if has_matching_condition(
320                    &v2.conditions,
321                    Mode::MerkleTree,
322                    &Pubkey::new_from_array(v1.root_hash),
323                ) {
324                    return result;
325                } else {
326                    result.mismatch = Some(Mismatch::MerkleRoot);
327                    return result;
328                }
329            }
330
331            if let Some(voc) = v1.voc {
332                if has_matching_condition(&v2.conditions, Mode::VOC, &voc) {
333                    return result;
334                } else {
335                    result.mismatch = Some(Mismatch::Voc);
336                    return result;
337                }
338            }
339
340            if let Some(fvc) = v1.fvc {
341                if has_matching_condition(&v2.conditions, Mode::FVC, &fvc) {
342                    return result;
343                } else {
344                    result.mismatch = Some(Mismatch::Fvc);
345                    return result;
346                }
347            }
348
349            if !has_matching_condition(
350                &v2.conditions,
351                Mode::MerkleTree,
352                &pubkey!("1thX6LZfHDZZKUs92febYZhYRcXddmzfzF2NvTkPNE"),
353            ) {
354                result.mismatch = Some(Mismatch::UnexpectedV2Conditions);
355            }
356
357            result
358        })
359        .collect()
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365    use solana_program::pubkey::Pubkey;
366    use tensor_whitelist::types::{Condition, Mode, State};
367
368    fn create_whitelist(
369        uuid: [u8; 32],
370        root_hash: [u8; 32],
371        voc: Option<Pubkey>,
372        fvc: Option<Pubkey>,
373    ) -> Whitelist {
374        Whitelist {
375            discriminator: [0; 8],
376            version: 0,
377            bump: 0,
378            verified: false,
379            root_hash,
380            uuid,
381            name: [0; 32],
382            frozen: false,
383            voc,
384            fvc,
385            reserved: [0; 64],
386        }
387    }
388
389    fn create_whitelist_v2(
390        uuid: [u8; 32],
391        conditions: Vec<Condition>,
392        namespace: Pubkey,
393    ) -> WhitelistV2 {
394        WhitelistV2 {
395            discriminator: [0; 8],
396            version: 0,
397            bump: 0,
398            state: State::Unfrozen,
399            update_authority: Pubkey::new_unique(),
400            namespace,
401            freeze_authority: Pubkey::new_unique(),
402            conditions,
403            uuid,
404        }
405    }
406
407    #[test]
408    fn test_merkle_whitelist() {
409        // Test a whitelist v1 with merkle root set, and corresponding v2 with matching condition
410
411        let uuid = [1u8; 32];
412
413        // Set root_hash to some non-default value
414        let root_hash: [u8; 32] = [1; 32];
415
416        let namespace = Pubkey::new_unique();
417
418        let v1_data = create_whitelist(uuid, root_hash, None, None);
419
420        let condition = Condition {
421            mode: Mode::MerkleTree,
422            value: Pubkey::new_from_array(root_hash),
423        };
424        let v2_data = create_whitelist_v2(uuid, vec![condition], namespace);
425
426        let pair = WhitelistPair {
427            v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
428            v1_data,
429            v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data.uuid).0,
430            v2_data: Some(v2_data),
431        };
432
433        let results = compare_whitelists(&[pair]);
434
435        assert_eq!(results.len(), 1);
436        assert!(results[0].mismatch.is_none());
437    }
438
439    #[test]
440    fn test_voc_whitelist() {
441        // Test a whitelist v1 with VOC set, and corresponding v2 with matching condition
442
443        let uuid = [2u8; 32];
444        let namespace = Pubkey::new_unique();
445
446        let voc = Pubkey::new_unique();
447
448        let v1_data = create_whitelist(uuid, DEFAULT_ROOT_HASH, Some(voc), None);
449
450        let condition = Condition {
451            mode: Mode::VOC,
452            value: voc,
453        };
454        let v2_data = create_whitelist_v2(uuid, vec![condition], namespace);
455
456        let pair = WhitelistPair {
457            v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
458            v1_data,
459            v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data.uuid).0,
460            v2_data: Some(v2_data),
461        };
462
463        let results = compare_whitelists(&[pair]);
464
465        assert_eq!(results.len(), 1);
466        assert!(results[0].mismatch.is_none());
467    }
468
469    #[test]
470    fn test_fvc_whitelist() {
471        // Test a whitelist v1 with FVC set, and corresponding v2 with matching condition
472
473        let uuid = [3u8; 32];
474        let namespace = Pubkey::new_unique();
475
476        let fvc = Pubkey::new_unique();
477
478        let v1_data = create_whitelist(uuid, DEFAULT_ROOT_HASH, None, Some(fvc));
479
480        let condition = Condition {
481            mode: Mode::FVC,
482            value: fvc,
483        };
484        let v2_data = create_whitelist_v2(uuid, vec![condition], namespace);
485
486        let pair = WhitelistPair {
487            v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
488            v1_data,
489            v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data.uuid).0,
490            v2_data: Some(v2_data),
491        };
492
493        let results = compare_whitelists(&[pair]);
494
495        assert_eq!(results.len(), 1);
496        assert!(results[0].mismatch.is_none());
497    }
498
499    #[test]
500    fn test_voc_and_fvc_set() {
501        // Test a whitelist v1 with both VOC and FVC set
502
503        let uuid = [4u8; 32];
504        let namespace = Pubkey::new_unique();
505
506        let voc = Pubkey::new_unique();
507        let fvc = Pubkey::new_unique();
508
509        let v1_data = create_whitelist(uuid, DEFAULT_ROOT_HASH, Some(voc), Some(fvc));
510
511        // Test with v2 condition matching VOC
512        let condition_voc = Condition {
513            mode: Mode::VOC,
514            value: voc,
515        };
516        let v2_data_voc = create_whitelist_v2(uuid, vec![condition_voc], namespace);
517
518        let pair_voc = WhitelistPair {
519            v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
520            v1_data: v1_data.clone(),
521            v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data_voc.uuid).0,
522            v2_data: Some(v2_data_voc),
523        };
524
525        let results_voc = compare_whitelists(&[pair_voc]);
526
527        assert_eq!(results_voc.len(), 1);
528        assert!(results_voc[0].mismatch.is_none());
529
530        // Test with v2 condition matching FVC
531        let condition_fvc = Condition {
532            mode: Mode::FVC,
533            value: fvc,
534        };
535        let v2_data_fvc = create_whitelist_v2(uuid, vec![condition_fvc], namespace);
536
537        let pair_fvc = WhitelistPair {
538            v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
539            v1_data: v1_data.clone(),
540            v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data_fvc.uuid).0,
541            v2_data: Some(v2_data_fvc),
542        };
543
544        let results_fvc = compare_whitelists(&[pair_fvc]);
545
546        assert_eq!(results_fvc.len(), 1);
547        // Even though v2 has a condition matching FVC, it should still mismatch Voc is set on v1
548        // and takes priority over FVC.
549        assert_eq!(results_fvc[0].mismatch, Some(Mismatch::Voc));
550    }
551
552    #[test]
553    fn test_merkle_and_fvc_set() {
554        // Test a whitelist v1 with Merkle root and FVC set
555
556        let uuid = [5u8; 32];
557        let namespace = Pubkey::new_unique();
558
559        let root_hash: [u8; 32] = [2; 32];
560        let fvc = Pubkey::new_unique();
561
562        let v1_data = create_whitelist(uuid, root_hash, None, Some(fvc));
563
564        // Test with v2 condition matching Merkle root
565        let condition_merkle = Condition {
566            mode: Mode::MerkleTree,
567            value: Pubkey::new_from_array(root_hash),
568        };
569        let v2_data_merkle = create_whitelist_v2(uuid, vec![condition_merkle], namespace);
570
571        let pair_merkle = WhitelistPair {
572            v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
573            v1_data: v1_data.clone(),
574            v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data_merkle.uuid).0,
575            v2_data: Some(v2_data_merkle),
576        };
577
578        let results_merkle = compare_whitelists(&[pair_merkle]);
579
580        assert_eq!(results_merkle.len(), 1);
581        assert!(results_merkle[0].mismatch.is_none());
582
583        // Test with v2 condition matching FVC
584        let condition_fvc: Condition = Condition {
585            mode: Mode::FVC,
586            value: fvc,
587        };
588        let v2_data_fvc = create_whitelist_v2(uuid, vec![condition_fvc], namespace);
589
590        let pair_fvc = WhitelistPair {
591            v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
592            v1_data,
593            v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data_fvc.uuid).0,
594            v2_data: Some(v2_data_fvc),
595        };
596
597        let results_fvc = compare_whitelists(&[pair_fvc]);
598
599        assert_eq!(results_fvc.len(), 1);
600        // Even though v2 has a condition matching FVC, it should still mismatch Merklet is set
601        // on v1 and takes priority.
602        assert_eq!(results_fvc[0].mismatch, Some(Mismatch::MerkleRoot));
603    }
604
605    #[test]
606    fn test_voc_with_multiple_v2_conditions() {
607        // v1 has VOC set but v2 has two conditions (VOC and FVC)
608        // Assert V2 conditions length mismatch
609
610        let uuid = [6u8; 32];
611        let namespace = Pubkey::new_unique();
612
613        let voc = Pubkey::new_unique();
614        let fvc = Pubkey::new_unique();
615
616        let v1_data = create_whitelist(uuid, DEFAULT_ROOT_HASH, Some(voc), None);
617
618        let conditions = vec![
619            Condition {
620                mode: Mode::VOC,
621                value: voc,
622            },
623            Condition {
624                mode: Mode::FVC,
625                value: fvc,
626            },
627        ];
628
629        let v2_data = create_whitelist_v2(uuid, conditions, namespace);
630
631        let pair = WhitelistPair {
632            v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
633            v1_data,
634            v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data.uuid).0,
635            v2_data: Some(v2_data),
636        };
637
638        let results = compare_whitelists(&[pair]);
639
640        assert_eq!(results.len(), 1);
641        // Expect a V2ConditionsLength mismatch because v2 has more than one condition
642        assert_eq!(results[0].mismatch, Some(Mismatch::V2ConditionsLength));
643    }
644
645    #[test]
646    fn test_voc_and_fvc_v1_v2_has_voc() {
647        // v1 has VOC and FVC set, v2 has just VOC set
648        // Assert mismatch is None (successful match)
649
650        let uuid = [7u8; 32];
651        let namespace = Pubkey::new_unique();
652
653        let voc = Pubkey::new_unique();
654        let fvc = Pubkey::new_unique();
655
656        let v1_data = create_whitelist(uuid, DEFAULT_ROOT_HASH, Some(voc), Some(fvc));
657
658        let condition = Condition {
659            mode: Mode::VOC,
660            value: voc,
661        };
662        let v2_data = create_whitelist_v2(uuid, vec![condition], namespace);
663
664        let pair = WhitelistPair {
665            v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
666            v1_data,
667            v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data.uuid).0,
668            v2_data: Some(v2_data),
669        };
670
671        let results = compare_whitelists(&[pair]);
672
673        assert_eq!(results.len(), 1);
674        // Expect no mismatch since v2 matches one of the v1 conditions
675        assert!(results[0].mismatch.is_none());
676    }
677
678    #[test]
679    fn test_merkle_with_multiple_v2_conditions() {
680        // v1 has Merkle root set, v2 has Merkle and FVC conditions
681        // Assert V2 conditions length mismatch
682
683        let uuid = [8u8; 32];
684        let namespace = Pubkey::new_unique();
685
686        let root_hash: [u8; 32] = [3; 32];
687        let fvc = Pubkey::new_unique();
688
689        let v1_data = create_whitelist(uuid, root_hash, None, None);
690
691        let conditions = vec![
692            Condition {
693                mode: Mode::MerkleTree,
694                value: Pubkey::new_from_array(root_hash),
695            },
696            Condition {
697                mode: Mode::FVC,
698                value: fvc,
699            },
700        ];
701
702        let v2_data = create_whitelist_v2(uuid, conditions, namespace);
703
704        let pair = WhitelistPair {
705            v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
706            v1_data,
707            v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data.uuid).0,
708            v2_data: Some(v2_data),
709        };
710
711        let results = compare_whitelists(&[pair]);
712
713        assert_eq!(results.len(), 1);
714        // Expect a V2ConditionsLength mismatch because v2 has more than one condition
715        assert_eq!(results[0].mismatch, Some(Mismatch::V2ConditionsLength));
716    }
717
718    #[test]
719    fn test_v1_no_conditions_v2_has_fvc() {
720        // v1 has none of the conditions set, v2 has FVC condition
721        // Assert mismatch is UnexpectedV2Conditions
722
723        let uuid = [9u8; 32];
724        let namespace = Pubkey::new_unique();
725
726        let fvc = Pubkey::new_unique();
727
728        let v1_data = create_whitelist(uuid, DEFAULT_ROOT_HASH, None, None);
729
730        let condition = Condition {
731            mode: Mode::FVC,
732            value: fvc,
733        };
734        let v2_data = create_whitelist_v2(uuid, vec![condition], namespace);
735
736        let pair = WhitelistPair {
737            v1_pubkey: Whitelist::find_pda(v1_data.uuid).0,
738            v1_data,
739            v2_pubkey: WhitelistV2::find_pda(&namespace, v2_data.uuid).0,
740            v2_data: Some(v2_data),
741        };
742
743        let results = compare_whitelists(&[pair]);
744
745        assert_eq!(results.len(), 1);
746        assert_eq!(results[0].mismatch, Some(Mismatch::UnexpectedV2Conditions));
747    }
748}