arch_token_metadata/
state.rs

1//! State types and serialization sizes
2
3use {
4    arch_program::{
5        program_error::ProgramError,
6        program_pack::{IsInitialized, Pack, Sealed},
7        pubkey::Pubkey,
8    },
9    borsh::{BorshDeserialize, BorshSerialize},
10};
11
12/// Maximum length for name
13pub const NAME_MAX_LEN: usize = 256;
14
15/// Maximum length for symbol
16pub const SYMBOL_MAX_LEN: usize = 16;
17
18/// Maximum length for image
19pub const IMAGE_MAX_LEN: usize = 512;
20
21/// Maximum length for description
22pub const DESCRIPTION_MAX_LEN: usize = 512;
23
24/// Maximum length for attribute key
25pub const MAX_KEY_LENGTH: usize = 64;
26
27/// Maximum length for attribute value
28pub const MAX_VALUE_LENGTH: usize = 240;
29
30/// Maximum number of attributes
31pub const MAX_ATTRIBUTES: usize = 32;
32
33/// Calculate the maximum serialized length (in bytes) for the TokenMetadata account using Borsh
34/// String layout: 4-byte LE length prefix + bytes
35/// Option<Pubkey> worst-case: 1-byte tag + 32 bytes
36pub const TOKEN_METADATA_MAX_LEN: usize = 1 + // is_initialized (bool)
37    32 + // mint
38    (4 + NAME_MAX_LEN) +
39    (4 + SYMBOL_MAX_LEN) +
40    (4 + IMAGE_MAX_LEN) +
41    (4 + DESCRIPTION_MAX_LEN) +
42    (1 + 32); // update_authority = Some(Pubkey)
43
44/// Calculate the maximum serialized length (in bytes) for the TokenMetadataAttributes account
45/// Vec layout: 4-byte LE length + elements; each element is a tuple of two Strings
46/// Tuple(String, String) layout: (4+key_len) + (4+value_len)
47pub const TOKEN_METADATA_ATTRIBUTES_MAX_LEN: usize = 1 + // is_initialized (bool)
48    32 + // mint
49    4 + // vec length prefix
50    (MAX_ATTRIBUTES * ((4 + MAX_KEY_LENGTH) + (4 + MAX_VALUE_LENGTH)));
51
52/// Core metadata account - always present, optimized for performance
53#[derive(BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq)]
54pub struct TokenMetadata {
55    /// Initialization flag
56    pub is_initialized: bool,
57    /// The mint address this metadata belongs to
58    pub mint: Pubkey,
59    /// The name of the token
60    pub name: String,
61    /// The symbol of the token
62    pub symbol: String,
63    /// The image URI for the token
64    pub image: String,
65    /// The description of the token
66    pub description: String,
67    /// Optional update authority for the metadata
68    pub update_authority: Option<Pubkey>,
69}
70
71impl Sealed for TokenMetadata {}
72impl IsInitialized for TokenMetadata {
73    fn is_initialized(&self) -> bool {
74        self.is_initialized
75    }
76}
77
78impl Pack for TokenMetadata {
79    const LEN: usize = TOKEN_METADATA_MAX_LEN;
80
81    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
82        // Use streaming deserialization so trailing zero padding is ignored
83        let mut slice_ref: &[u8] = src;
84        BorshDeserialize::deserialize(&mut slice_ref).map_err(|_| ProgramError::InvalidAccountData)
85    }
86
87    fn pack_into_slice(&self, dst: &mut [u8]) {
88        let data = borsh::to_vec(self).unwrap();
89        // Copy serialized data into destination; assume destination has been sized to LEN
90        dst[..data.len()].copy_from_slice(&data);
91        // Zero the remainder for determinism
92        if data.len() < dst.len() {
93            for b in &mut dst[data.len()..] {
94                *b = 0;
95            }
96        }
97    }
98}
99
100/// Optional metadata attributes account - linked to core metadata
101#[derive(BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq)]
102pub struct TokenMetadataAttributes {
103    /// Initialization flag
104    pub is_initialized: bool,
105    /// The mint address this attributes belong to
106    pub mint: Pubkey,
107    /// Key-value pairs for extensible attributes
108    pub data: Vec<(String, String)>, // Key-value pairs for extensibility
109}
110
111impl Sealed for TokenMetadataAttributes {}
112impl IsInitialized for TokenMetadataAttributes {
113    fn is_initialized(&self) -> bool {
114        self.is_initialized
115    }
116}
117
118impl Pack for TokenMetadataAttributes {
119    const LEN: usize = TOKEN_METADATA_ATTRIBUTES_MAX_LEN;
120
121    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
122        let mut slice_ref: &[u8] = src;
123        BorshDeserialize::deserialize(&mut slice_ref).map_err(|_| ProgramError::InvalidAccountData)
124    }
125
126    fn pack_into_slice(&self, dst: &mut [u8]) {
127        let data = borsh::to_vec(self).unwrap();
128        dst[..data.len()].copy_from_slice(&data);
129        if data.len() < dst.len() {
130            for b in &mut dst[data.len()..] {
131                *b = 0;
132            }
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use arch_program::program_pack::Pack;
141
142    fn pk(byte: u8) -> Pubkey {
143        let bytes = [byte; 32];
144        Pubkey::from_slice(&bytes)
145    }
146
147    #[test]
148    fn token_metadata_pack_unpack_roundtrip() {
149        let md = TokenMetadata {
150            is_initialized: true,
151            mint: pk(1),
152            name: "Arch Pioneer Token".to_string(),
153            symbol: "APT".to_string(),
154            image: "https://arweave.net/abc123.png".to_string(),
155            description: "The first token launched on Arch Network".to_string(),
156            update_authority: Some(pk(2)),
157        };
158
159        let mut buf = vec![0u8; TokenMetadata::LEN];
160        TokenMetadata::pack_into_slice(&md, &mut buf);
161
162        // First byte reflects is_initialized (borsh bool -> u8)
163        assert_eq!(buf[0], 1);
164
165        let md2 = TokenMetadata::unpack_from_slice(&buf).expect("unpack md");
166        assert_eq!(md, md2);
167    }
168
169    #[test]
170    fn token_metadata_unpack_ignores_trailing_zeros() {
171        let md = TokenMetadata {
172            is_initialized: true,
173            mint: pk(9),
174            name: "Name".to_string(),
175            symbol: "SYM".to_string(),
176            image: "img".to_string(),
177            description: "desc".to_string(),
178            update_authority: None,
179        };
180
181        let mut packed = borsh::to_vec(&md).unwrap();
182        // Append a bunch of trailing zeros beyond the serialized size
183        packed.extend_from_slice(&vec![0u8; 512]);
184
185        let unpacked = TokenMetadata::unpack_from_slice(&packed).expect("unpack md");
186        assert_eq!(md, unpacked);
187    }
188
189    #[test]
190    fn token_metadata_attributes_pack_unpack_roundtrip() {
191        let attrs = TokenMetadataAttributes {
192            is_initialized: true,
193            mint: pk(3),
194            data: vec![
195                ("key1".to_string(), "value1".to_string()),
196                ("k".to_string(), "v".to_string()),
197            ],
198        };
199
200        let mut buf = vec![0u8; TokenMetadataAttributes::LEN];
201        TokenMetadataAttributes::pack_into_slice(&attrs, &mut buf);
202        assert_eq!(buf[0], 1);
203
204        let attrs2 = TokenMetadataAttributes::unpack_from_slice(&buf).expect("unpack attrs");
205        assert_eq!(attrs, attrs2);
206    }
207
208    #[test]
209    fn token_metadata_attributes_unpack_ignores_trailing_zeros() {
210        let attrs = TokenMetadataAttributes {
211            is_initialized: true,
212            mint: pk(7),
213            data: vec![("alpha".into(), "beta".into())],
214        };
215
216        let mut packed = borsh::to_vec(&attrs).unwrap();
217        packed.extend_from_slice(&vec![0u8; 256]);
218
219        let unpacked = TokenMetadataAttributes::unpack_from_slice(&packed).expect("unpack attrs");
220        assert_eq!(attrs, unpacked);
221    }
222}