use borsh::{BorshDeserialize, BorshSerialize};
use light_compressed_account::Pubkey;
use light_compressible::compression_info::CompressionInfo;
use light_token_interface::state::{
extensions::{AdditionalMetadata, ExtensionStruct, TokenMetadata},
mint::{BaseMint, Mint, MintMetadata, ACCOUNT_TYPE_MINT},
};
use light_zero_copy::traits::{ZeroCopyAt, ZeroCopyAtMut};
use rand::{thread_rng, Rng};
use spl_token_2022::{solana_program::program_pack::Pack, state::Mint as SplMint};
fn generate_random_token_metadata(rng: &mut impl Rng) -> TokenMetadata {
let update_authority = if rng.gen_bool(0.7) {
let mut bytes = [0u8; 32];
rng.fill(&mut bytes);
Pubkey::from(bytes)
} else {
Pubkey::from([0u8; 32]) };
let mut mint_bytes = [0u8; 32];
rng.fill(&mut mint_bytes);
let mint = Pubkey::from(mint_bytes);
let name_len = rng.gen_range(1..=32);
let name: Vec<u8> = (0..name_len).map(|_| rng.gen::<u8>()).collect();
let symbol_len = rng.gen_range(1..=10);
let symbol: Vec<u8> = (0..symbol_len).map(|_| rng.gen::<u8>()).collect();
let uri_len = rng.gen_range(0..=200);
let uri: Vec<u8> = (0..uri_len).map(|_| rng.gen::<u8>()).collect();
let num_metadata = rng.gen_range(0..=3);
let additional_metadata: Vec<AdditionalMetadata> = (0..num_metadata)
.map(|_| {
let key_len = rng.gen_range(1..=20);
let key: Vec<u8> = (0..key_len).map(|_| rng.gen::<u8>()).collect();
let value_len = rng.gen_range(0..=50);
let value: Vec<u8> = (0..value_len).map(|_| rng.gen::<u8>()).collect();
AdditionalMetadata { key, value }
})
.collect();
TokenMetadata {
update_authority,
mint,
name,
symbol,
uri,
additional_metadata,
}
}
fn generate_random_mint() -> Mint {
let mut rng = thread_rng();
let extensions = if rng.gen_bool(0.4) {
let token_metadata = generate_random_token_metadata(&mut rng);
Some(vec![ExtensionStruct::TokenMetadata(token_metadata)])
} else {
None
};
Mint {
base: BaseMint {
mint_authority: if rng.gen_bool(0.7) {
let mut bytes = [0u8; 32];
rng.fill(&mut bytes);
Some(Pubkey::from(bytes))
} else {
None
},
freeze_authority: if rng.gen_bool(0.5) {
let mut bytes = [0u8; 32];
rng.fill(&mut bytes);
Some(Pubkey::from(bytes))
} else {
None
},
supply: rng.gen::<u64>(),
decimals: rng.gen_range(0..=18),
is_initialized: true,
},
metadata: MintMetadata {
version: 3,
mint_decompressed: rng.gen_bool(0.5),
mint: {
let mut bytes = [0u8; 32];
rng.fill(&mut bytes);
Pubkey::from(bytes)
},
mint_signer: rng.gen::<[u8; 32]>(),
bump: rng.gen(),
},
reserved: [0u8; 16],
account_type: ACCOUNT_TYPE_MINT,
compression: CompressionInfo::default(),
extensions,
}
}
fn reconstruct_extensions(
zc_extensions: &Option<Vec<light_token_interface::state::extensions::ZExtensionStruct>>,
) -> Option<Vec<ExtensionStruct>> {
zc_extensions.as_ref().map(|exts| {
exts.iter()
.map(|ext| match ext {
light_token_interface::state::extensions::ZExtensionStruct::TokenMetadata(
zc_metadata,
) => ExtensionStruct::TokenMetadata(TokenMetadata {
update_authority: zc_metadata.update_authority,
mint: zc_metadata.mint,
name: zc_metadata.name.to_vec(),
symbol: zc_metadata.symbol.to_vec(),
uri: zc_metadata.uri.to_vec(),
additional_metadata: zc_metadata
.additional_metadata
.iter()
.map(|am| AdditionalMetadata {
key: am.key.to_vec(),
value: am.value.to_vec(),
})
.collect(),
}),
_ => panic!("Unexpected extension type in test"),
})
.collect()
})
}
fn compare_mint_borsh_vs_zero_copy(original: &Mint, borsh_bytes: &[u8]) {
let borsh_mint = Mint::try_from_slice(borsh_bytes).unwrap();
let (zc_mint, _) = Mint::zero_copy_at(borsh_bytes).unwrap();
let zc_extensions = reconstruct_extensions(&zc_mint.extensions);
let zc_reconstructed = Mint {
base: BaseMint {
mint_authority: zc_mint.base.mint_authority().copied(),
freeze_authority: zc_mint.base.freeze_authority().copied(),
supply: u64::from(zc_mint.base.supply),
decimals: zc_mint.base.decimals,
is_initialized: zc_mint.base.is_initialized != 0,
},
metadata: MintMetadata {
version: zc_mint.base.metadata.version,
mint_decompressed: zc_mint.base.metadata.mint_decompressed != 0,
mint: zc_mint.base.metadata.mint,
mint_signer: zc_mint.base.metadata.mint_signer,
bump: zc_mint.base.metadata.bump,
},
reserved: *zc_mint.base.reserved,
account_type: zc_mint.base.account_type,
compression: CompressionInfo::default(),
extensions: zc_extensions.clone(),
};
let mut mutable_bytes = borsh_bytes.to_vec();
let (zc_mint_mut, _) = Mint::zero_copy_at_mut(&mut mutable_bytes).unwrap();
let zc_mut_reconstructed = Mint {
base: BaseMint {
mint_authority: zc_mint_mut.base.mint_authority().copied(),
freeze_authority: zc_mint_mut.base.freeze_authority().copied(),
supply: u64::from(zc_mint_mut.base.supply),
decimals: zc_mint_mut.base.decimals,
is_initialized: zc_mint_mut.base.is_initialized != 0,
},
metadata: MintMetadata {
version: zc_mint_mut.base.metadata.version,
mint_decompressed: zc_mint_mut.base.metadata.mint_decompressed != 0,
mint: zc_mint_mut.base.metadata.mint,
mint_signer: zc_mint_mut.base.metadata.mint_signer,
bump: zc_mint_mut.base.metadata.bump,
},
reserved: *zc_mint_mut.base.reserved,
account_type: *zc_mint_mut.base.account_type,
compression: CompressionInfo::default(),
extensions: zc_extensions, };
assert_eq!(
(original, &borsh_mint, &zc_reconstructed, &zc_mut_reconstructed),
(original, original, original, original),
"Mismatch between original, Borsh, zero-copy read-only, and zero-copy mutable deserialized structs"
);
let mint = SplMint::unpack(&borsh_bytes[..SplMint::LEN]).unwrap();
let spl_reconstructed_base = BaseMint {
mint_authority: Option::<solana_pubkey::Pubkey>::from(mint.mint_authority)
.map(|p| Pubkey::from(p.to_bytes())),
freeze_authority: Option::<solana_pubkey::Pubkey>::from(mint.freeze_authority)
.map(|p| Pubkey::from(p.to_bytes())),
supply: mint.supply,
decimals: mint.decimals,
is_initialized: mint.is_initialized,
};
assert_eq!(
&original.base, &spl_reconstructed_base,
"Mismatch between original base mint and SPL pod deserialized base mint"
);
}
#[test]
fn test_mint_borsh_zero_copy_compatibility() {
for _ in 0..1000 {
let mint = generate_random_mint();
let borsh_bytes = mint.try_to_vec().unwrap();
compare_mint_borsh_vs_zero_copy(&mint, &borsh_bytes);
}
}
fn generate_mint_with_extensions() -> Mint {
let mut rng = thread_rng();
let token_metadata = generate_random_token_metadata(&mut rng);
Mint {
base: BaseMint {
mint_authority: Some(Pubkey::from(rng.gen::<[u8; 32]>())),
freeze_authority: Some(Pubkey::from(rng.gen::<[u8; 32]>())),
supply: rng.gen::<u64>(),
decimals: rng.gen_range(0..=18),
is_initialized: true,
},
metadata: MintMetadata {
version: 3,
mint_decompressed: rng.gen_bool(0.5),
mint: Pubkey::from(rng.gen::<[u8; 32]>()),
mint_signer: rng.gen::<[u8; 32]>(),
bump: rng.gen(),
},
reserved: [0u8; 16],
account_type: ACCOUNT_TYPE_MINT,
compression: CompressionInfo::default(),
extensions: Some(vec![ExtensionStruct::TokenMetadata(token_metadata)]),
}
}
#[test]
fn test_mint_with_extensions_borsh_zero_copy_compatibility() {
for _ in 0..500 {
let mint = generate_mint_with_extensions();
let borsh_bytes = mint.try_to_vec().unwrap();
compare_mint_borsh_vs_zero_copy(&mint, &borsh_bytes);
}
}
#[test]
fn test_mint_extension_edge_cases() {
let mint_empty_strings = Mint {
base: BaseMint {
mint_authority: Some(Pubkey::from([1u8; 32])),
freeze_authority: None,
supply: 1_000_000,
decimals: 9,
is_initialized: true,
},
metadata: MintMetadata {
version: 3,
mint_decompressed: false,
mint: Pubkey::from([2u8; 32]),
mint_signer: [0u8; 32],
bump: 0,
},
reserved: [0u8; 16],
account_type: ACCOUNT_TYPE_MINT,
compression: CompressionInfo::default(),
extensions: Some(vec![ExtensionStruct::TokenMetadata(TokenMetadata {
update_authority: Pubkey::from([3u8; 32]),
mint: Pubkey::from([2u8; 32]),
name: vec![], symbol: vec![], uri: vec![], additional_metadata: vec![], })]),
};
let borsh_bytes = mint_empty_strings.try_to_vec().unwrap();
compare_mint_borsh_vs_zero_copy(&mint_empty_strings, &borsh_bytes);
let mint_max_lengths = Mint {
base: BaseMint {
mint_authority: Some(Pubkey::from([0xffu8; 32])),
freeze_authority: Some(Pubkey::from([0xaau8; 32])),
supply: u64::MAX,
decimals: 18,
is_initialized: true,
},
metadata: MintMetadata {
version: 3,
mint_decompressed: true,
mint: Pubkey::from([0xbbu8; 32]),
mint_signer: [0xddu8; 32],
bump: 255,
},
reserved: [0u8; 16],
account_type: ACCOUNT_TYPE_MINT,
compression: CompressionInfo::default(),
extensions: Some(vec![ExtensionStruct::TokenMetadata(TokenMetadata {
update_authority: Pubkey::from([0xccu8; 32]),
mint: Pubkey::from([0xbbu8; 32]),
name: vec![b'A'; 64], symbol: vec![b'S'; 16], uri: vec![b'U'; 256], additional_metadata: vec![
AdditionalMetadata {
key: vec![b'K'; 32],
value: vec![b'V'; 128],
},
AdditionalMetadata {
key: vec![b'X'; 32],
value: vec![b'Y'; 128],
},
AdditionalMetadata {
key: vec![b'Z'; 32],
value: vec![b'W'; 128],
},
],
})]),
};
let borsh_bytes = mint_max_lengths.try_to_vec().unwrap();
compare_mint_borsh_vs_zero_copy(&mint_max_lengths, &borsh_bytes);
let mint_zero_authority = Mint {
base: BaseMint {
mint_authority: None,
freeze_authority: None,
supply: 0,
decimals: 0,
is_initialized: true,
},
metadata: MintMetadata {
version: 3,
mint_decompressed: false,
mint: Pubkey::from([4u8; 32]),
mint_signer: [5u8; 32],
bump: 255,
},
reserved: [0u8; 16],
account_type: ACCOUNT_TYPE_MINT,
compression: CompressionInfo::default(),
extensions: Some(vec![ExtensionStruct::TokenMetadata(TokenMetadata {
update_authority: Pubkey::from([0u8; 32]), mint: Pubkey::from([4u8; 32]),
name: b"Test Token".to_vec(),
symbol: b"TEST".to_vec(),
uri: b"https://example.com/token.json".to_vec(),
additional_metadata: vec![],
})]),
};
let borsh_bytes = mint_zero_authority.try_to_vec().unwrap();
compare_mint_borsh_vs_zero_copy(&mint_zero_authority, &borsh_bytes);
let mint_no_extensions = Mint {
base: BaseMint {
mint_authority: Some(Pubkey::from([5u8; 32])),
freeze_authority: Some(Pubkey::from([6u8; 32])),
supply: 500_000,
decimals: 6,
is_initialized: true,
},
metadata: MintMetadata {
version: 3,
mint_decompressed: true,
mint: Pubkey::from([7u8; 32]),
mint_signer: [8u8; 32],
bump: 255,
},
reserved: [0u8; 16],
account_type: ACCOUNT_TYPE_MINT,
compression: CompressionInfo::default(),
extensions: None,
};
let borsh_bytes = mint_no_extensions.try_to_vec().unwrap();
compare_mint_borsh_vs_zero_copy(&mint_no_extensions, &borsh_bytes);
}