Skip to main content

ownable_std/
metadata.rs

1use cosmwasm_std::Uint128;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4
5/// convert an ownable package name into a display title
6/// e.g. `ownable-my-first` -> `My First`
7pub fn package_title_from_name(name: &str) -> String {
8    name.trim_start_matches("ownable-")
9        .split(['-', '_'])
10        .filter(|part| !part.is_empty())
11        .map(|part| {
12            let mut chars = part.chars();
13            match chars.next() {
14                Some(first) => format!("{}{}", first.to_ascii_uppercase(), chars.as_str()),
15                None => String::new(),
16            }
17        })
18        .collect::<Vec<_>>()
19        .join(" ")
20}
21
22/// returns a hex color in string format from a hash
23pub fn get_random_color(hash: String) -> String {
24    let (red, green, blue) = derive_rgb_values(hash);
25    rgb_hex(red, green, blue)
26}
27
28/// takes a hex-encoded hash and derives a seemingly-random rgb tuple
29pub fn derive_rgb_values(hash: String) -> (u8, u8, u8) {
30    let mut s = hash.trim().trim_start_matches("0x").to_string();
31    if s.len() % 2 == 1 {
32        s.insert(0, '0');
33    }
34
35    match hex::decode(&s) {
36        Ok(mut bytes) => {
37            bytes.reverse();
38            let r = *bytes.get(0).unwrap_or(&0);
39            let g = *bytes.get(1).unwrap_or(&0);
40            let b = *bytes.get(2).unwrap_or(&0);
41            (r, g, b)
42        }
43        Err(_) => (0, 0, 0),
44    }
45}
46
47/// takes three u8 values representing rgb values (0-255)f
48/// and returns a hex string
49pub fn rgb_hex(r: u8, g: u8, b: u8) -> String {
50    format!("#{:02X}{:02X}{:02X}", r, g, b)
51}
52
53// from github.com/CosmWasm/cw-nfts/blob/main/contracts/cw721-metadata-onchain
54#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Default)]
55/// Standard NFT metadata object.
56pub struct Metadata {
57    pub image: Option<String>,
58    pub image_data: Option<String>,
59    pub external_url: Option<String>,
60    pub description: Option<String>,
61    pub name: Option<String>,
62    pub background_color: Option<String>,
63    pub animation_url: Option<String>,
64    pub youtube_url: Option<String>,
65}
66
67/// NFT reference used by ownables.
68#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
69pub struct NFT {
70    pub network: String,
71    pub id: Uint128,
72    pub address: String,
73    pub lock_service: Option<String>,
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn rgb_hex_formats_correctly() {
82        assert_eq!(rgb_hex(0, 0, 0), "#000000");
83        assert_eq!(rgb_hex(255, 255, 255), "#FFFFFF");
84        assert_eq!(rgb_hex(255, 0, 0), "#FF0000");
85        assert_eq!(rgb_hex(0, 128, 255), "#0080FF");
86    }
87
88    #[test]
89    fn derive_rgb_values_reads_last_three_bytes_reversed() {
90        assert_eq!(derive_rgb_values("010203".to_string()), (3, 2, 1));
91    }
92
93    #[test]
94    fn derive_rgb_values_strips_0x_prefix() {
95        assert_eq!(
96            derive_rgb_values("0x010203".to_string()),
97            derive_rgb_values("010203".to_string())
98        );
99    }
100
101    #[test]
102    fn derive_rgb_values_pads_odd_length_input() {
103        assert_eq!(derive_rgb_values("abc".to_string()), (0xbc, 0x0a, 0));
104    }
105
106    #[test]
107    fn derive_rgb_values_returns_zeros_for_invalid_hex() {
108        assert_eq!(derive_rgb_values("xyz".to_string()), (0, 0, 0));
109    }
110
111    #[test]
112    fn derive_rgb_values_returns_zeros_for_empty_input() {
113        assert_eq!(derive_rgb_values("".to_string()), (0, 0, 0));
114    }
115
116    #[test]
117    fn derive_rgb_values_uses_last_three_bytes_of_long_input() {
118        assert_eq!(
119            derive_rgb_values("aabbccdd11223344".to_string()),
120            (0x44, 0x33, 0x22)
121        );
122    }
123
124    #[test]
125    fn get_random_color_returns_hash_prefixed_hex() {
126        let color = get_random_color("010203".to_string());
127        assert!(color.starts_with('#'));
128        assert_eq!(color.len(), 7);
129    }
130
131    #[test]
132    fn get_random_color_is_deterministic() {
133        let hash = "deadbeef".to_string();
134        assert_eq!(get_random_color(hash.clone()), get_random_color(hash));
135    }
136}