use borsh::{BorshDeserialize, BorshSerialize};
use light_compressed_account::Pubkey;
use light_token_interface::state::extensions::{
AdditionalMetadata, TokenMetadata as LightTokenMetadata,
};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use spl_pod::optional_keys::OptionalNonZeroPubkey;
use spl_token_metadata_interface::state::TokenMetadata as SplTokenMetadata;
type MetadataTestData = (
Option<Pubkey>,
Pubkey,
String,
String,
String,
Vec<(String, String)>,
);
fn generate_random_metadata() -> MetadataTestData {
let mut rng = thread_rng();
let update_authority = if rng.gen_bool(0.7) {
let mut bytes = [0u8; 32];
rng.fill(&mut bytes);
Some(Pubkey::from(bytes))
} else {
None
};
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: String = (&mut rng)
.sample_iter(&Alphanumeric)
.take(name_len)
.map(char::from)
.collect();
let symbol_len = rng.gen_range(1..=10);
let symbol: String = (&mut rng)
.sample_iter(&Alphanumeric)
.take(symbol_len)
.map(char::from)
.collect();
let uri_len = rng.gen_range(0..=200);
let uri: String = (&mut rng)
.sample_iter(&Alphanumeric)
.take(uri_len)
.map(char::from)
.collect();
let num_metadata = rng.gen_range(0..=5);
let additional_metadata: Vec<(String, String)> = (0..num_metadata)
.map(|_| {
let key_len = rng.gen_range(1..=20);
let key: String = (&mut rng)
.sample_iter(&Alphanumeric)
.take(key_len)
.map(char::from)
.collect();
let value_len = rng.gen_range(0..=100);
let value: String = (&mut rng)
.sample_iter(&Alphanumeric)
.take(value_len)
.map(char::from)
.collect();
(key, value)
})
.collect();
(
update_authority,
mint,
name,
symbol,
uri,
additional_metadata,
)
}
fn compare_metadata(light: &LightTokenMetadata, spl: &SplTokenMetadata, iteration: usize) {
let light_authority_bytes = if light.update_authority == Pubkey::from([0u8; 32]) {
None
} else {
Some(light.update_authority.to_bytes())
};
let spl_authority_bytes =
Option::<solana_pubkey::Pubkey>::from(spl.update_authority).map(|p| p.to_bytes());
assert_eq!(
light_authority_bytes, spl_authority_bytes,
"Update authority mismatch at iteration {}",
iteration
);
assert_eq!(
light.mint.to_bytes(),
spl.mint.to_bytes(),
"Mint mismatch at iteration {}",
iteration
);
let light_name = String::from_utf8(light.name.clone()).unwrap_or_default();
assert_eq!(
light_name, spl.name,
"Name mismatch at iteration {}",
iteration
);
let light_symbol = String::from_utf8(light.symbol.clone()).unwrap_or_default();
assert_eq!(
light_symbol, spl.symbol,
"Symbol mismatch at iteration {}",
iteration
);
let light_uri = String::from_utf8(light.uri.clone()).unwrap_or_default();
assert_eq!(
light_uri, spl.uri,
"URI mismatch at iteration {}",
iteration
);
assert_eq!(
light.additional_metadata.len(),
spl.additional_metadata.len(),
"Additional metadata count mismatch at iteration {}",
iteration
);
for (idx, (light_meta, spl_meta)) in light
.additional_metadata
.iter()
.zip(spl.additional_metadata.iter())
.enumerate()
{
let light_key = String::from_utf8(light_meta.key.clone()).unwrap_or_default();
let light_value = String::from_utf8(light_meta.value.clone()).unwrap_or_default();
assert_eq!(
light_key, spl_meta.0,
"Additional metadata key mismatch at iteration {}, index {}",
iteration, idx
);
assert_eq!(
light_value, spl_meta.1,
"Additional metadata value mismatch at iteration {}, index {}",
iteration, idx
);
}
let light_bytes = light.try_to_vec().unwrap();
let light_restored = LightTokenMetadata::try_from_slice(&light_bytes).unwrap();
assert_eq!(
light, &light_restored,
"Light serialization round-trip failed at iteration {}",
iteration
);
use spl_token_metadata_interface::borsh::{BorshDeserialize, BorshSerialize};
let mut spl_bytes = Vec::new();
spl.serialize(&mut spl_bytes).unwrap();
let spl_restored = SplTokenMetadata::deserialize(&mut spl_bytes.as_slice()).unwrap();
assert_eq!(
spl, &spl_restored,
"SPL serialization round-trip failed at iteration {}",
iteration
);
assert!(
!light_bytes.is_empty() && light_bytes.len() < 10000,
"Light serialized size {} is unreasonable at iteration {}",
light_bytes.len(),
iteration
);
assert!(
!spl_bytes.is_empty() && spl_bytes.len() < 10000,
"SPL serialized size {} is unreasonable at iteration {}",
spl_bytes.len(),
iteration
);
assert_eq!(light_bytes, spl_bytes);
}
#[test]
fn test_token_metadata_borsh_compatibility() {
for i in 0..1000 {
let (update_authority, mint, name, symbol, uri, additional_metadata) =
generate_random_metadata();
let light_metadata = LightTokenMetadata {
update_authority: update_authority.unwrap_or_else(|| Pubkey::from([0u8; 32])),
mint,
name: name.as_bytes().to_vec(),
symbol: symbol.as_bytes().to_vec(),
uri: uri.as_bytes().to_vec(),
additional_metadata: additional_metadata
.iter()
.map(|(k, v)| AdditionalMetadata {
key: k.as_bytes().to_vec(),
value: v.as_bytes().to_vec(),
})
.collect(),
};
let spl_update_authority = if let Some(pubkey) = update_authority {
OptionalNonZeroPubkey::try_from(Some(solana_pubkey::Pubkey::from(pubkey.to_bytes())))
.unwrap()
} else {
OptionalNonZeroPubkey::try_from(None).unwrap()
};
let spl_metadata = SplTokenMetadata {
update_authority: spl_update_authority,
mint: solana_pubkey::Pubkey::from(mint.to_bytes()),
name: name.clone(),
symbol: symbol.clone(),
uri: uri.clone(),
additional_metadata: additional_metadata.clone(),
};
compare_metadata(&light_metadata, &spl_metadata, i);
}
}
#[test]
fn test_token_metadata_edge_cases() {
let light_empty = LightTokenMetadata {
update_authority: Pubkey::from([0u8; 32]), mint: Pubkey::from([0u8; 32]),
name: b"X".to_vec(), symbol: b"X".to_vec(), uri: vec![], additional_metadata: vec![],
};
let spl_empty = SplTokenMetadata {
update_authority: OptionalNonZeroPubkey::try_from(None).unwrap(),
mint: solana_pubkey::Pubkey::from([0u8; 32]),
name: "X".to_string(),
symbol: "X".to_string(),
uri: String::new(),
additional_metadata: vec![],
};
compare_metadata(&light_empty, &spl_empty, 0);
let mut max_metadata_light = vec![];
let mut max_metadata_spl = vec![];
for i in 0..10 {
let key = format!("key_{}", i);
let value = format!("value_{}", i);
max_metadata_light.push(AdditionalMetadata {
key: key.as_bytes().to_vec(),
value: value.as_bytes().to_vec(),
});
max_metadata_spl.push((key, value));
}
let authority = Pubkey::from([255u8; 32]);
let mint = Pubkey::from([1u8; 32]);
let light_max = LightTokenMetadata {
update_authority: authority,
mint,
name: b"Maximum Length Token Name Here32".to_vec(), symbol: b"MAXSYMBOL1".to_vec(), uri: vec![b'h'; 200], additional_metadata: max_metadata_light,
};
let spl_max = SplTokenMetadata {
update_authority: OptionalNonZeroPubkey::try_from(Some(solana_pubkey::Pubkey::from(
[255u8; 32],
)))
.unwrap(),
mint: solana_pubkey::Pubkey::from([1u8; 32]),
name: "Maximum Length Token Name Here32".to_string(),
symbol: "MAXSYMBOL1".to_string(),
uri: "h".repeat(200),
additional_metadata: max_metadata_spl,
};
compare_metadata(&light_max, &spl_max, 1);
}