@@ -30,9 +30,7 @@ pub enum AccountState {
3030 frozen,
3131}
3232
33- #[ derive(
34- Debug , PartialEq , Eq , BorshDeserialize , BorshSerialize , Clone , ToSchema , Serialize , Default ,
35- ) ]
33+ #[ derive( Debug , PartialEq , Eq , Clone , ToSchema , Serialize , Default ) ]
3634#[ serde( rename_all = "camelCase" ) ]
3735pub struct TokenData {
3836 /// The mint associated with this account
@@ -46,6 +44,247 @@ pub struct TokenData {
4644 pub delegate : Option < SerializablePubkey > ,
4745 /// The account's state
4846 pub state : AccountState ,
49- /// Placeholder for TokenExtension tlv data (unimplemented )
47+ /// TokenExtension TLV data (raw bytes, opaque to the indexer )
5048 pub tlv : Option < Base64String > ,
5149}
50+
51+ impl TokenData {
52+ /// Deserializes base fields via Borsh, then reads TLV as raw bytes (1-byte
53+ /// option tag + remaining bytes).
54+ pub fn parse ( data : & [ u8 ] ) -> Result < Self , std:: io:: Error > {
55+ let mut buf = data;
56+
57+ let mint = SerializablePubkey :: deserialize ( & mut buf) ?;
58+ let owner = SerializablePubkey :: deserialize ( & mut buf) ?;
59+ let amount = UnsignedInteger :: deserialize ( & mut buf) ?;
60+ let delegate = Option :: < SerializablePubkey > :: deserialize ( & mut buf) ?;
61+ let state = AccountState :: deserialize ( & mut buf) ?;
62+
63+ let tlv = if buf. is_empty ( ) {
64+ None
65+ } else {
66+ let option_tag = buf[ 0 ] ;
67+ buf = & buf[ 1 ..] ;
68+
69+ match option_tag {
70+ 0 => None ,
71+ 1 if !buf. is_empty ( ) => Some ( Base64String ( buf. to_vec ( ) ) ) ,
72+ other => {
73+ log:: warn!(
74+ "Unexpected TLV: option_tag={}, remaining_bytes={}" ,
75+ other,
76+ buf. len( )
77+ ) ;
78+ None
79+ }
80+ }
81+ } ;
82+
83+ Ok ( TokenData {
84+ mint,
85+ owner,
86+ amount,
87+ delegate,
88+ state,
89+ tlv,
90+ } )
91+ }
92+ }
93+
94+ impl BorshSerialize for TokenData {
95+ fn serialize < W : std:: io:: Write > ( & self , writer : & mut W ) -> Result < ( ) , std:: io:: Error > {
96+ borsh:: BorshSerialize :: serialize ( & self . mint , writer) ?;
97+ borsh:: BorshSerialize :: serialize ( & self . owner , writer) ?;
98+ borsh:: BorshSerialize :: serialize ( & self . amount , writer) ?;
99+ borsh:: BorshSerialize :: serialize ( & self . delegate , writer) ?;
100+ borsh:: BorshSerialize :: serialize ( & self . state , writer) ?;
101+
102+ match & self . tlv {
103+ None => writer. write_all ( & [ 0 ] ) ,
104+ Some ( tlv_bytes) => {
105+ writer. write_all ( & [ 1 ] ) ?;
106+ writer. write_all ( & tlv_bytes. 0 )
107+ }
108+ }
109+ }
110+ }
111+
112+ #[ cfg( test) ]
113+ mod tests {
114+ use super :: * ;
115+ use solana_pubkey:: Pubkey ;
116+
117+ fn build_onchain_token_data_bytes ( has_delegate : bool , tlv : Option < & [ u8 ] > ) -> Vec < u8 > {
118+ let mut bytes = Vec :: new ( ) ;
119+ bytes. extend_from_slice ( & [ 0x11u8 ; 32 ] ) ; // mint
120+ bytes. extend_from_slice ( & [ 0x22u8 ; 32 ] ) ; // owner
121+ bytes. extend_from_slice ( & 100u64 . to_le_bytes ( ) ) ; // amount
122+ if has_delegate {
123+ bytes. push ( 1 ) ;
124+ bytes. extend_from_slice ( & [ 0x33u8 ; 32 ] ) ;
125+ } else {
126+ bytes. push ( 0 ) ;
127+ }
128+ bytes. push ( 0 ) ; // state = initialized
129+ match tlv {
130+ Some ( tlv_data) => {
131+ bytes. push ( 1 ) ; // Option tag = Some
132+ bytes. extend_from_slice ( tlv_data) ;
133+ }
134+ None => {
135+ bytes. push ( 0 ) ; // Option tag = None
136+ }
137+ }
138+ bytes
139+ }
140+
141+ fn build_compressed_only_tlv ( ) -> Vec < u8 > {
142+ let mut tlv = Vec :: new ( ) ;
143+ tlv. extend_from_slice ( & 1u32 . to_le_bytes ( ) ) ; // vec len = 1 element
144+ tlv. push ( 31 ) ; // ExtensionStruct enum variant = CompressedOnly
145+ tlv. extend_from_slice ( & 500u64 . to_le_bytes ( ) ) ; // delegated_amount
146+ tlv. extend_from_slice ( & 0u64 . to_le_bytes ( ) ) ; // withheld_transfer_fee
147+ tlv. push ( 1 ) ; // is_ata
148+ tlv
149+ }
150+
151+ #[ test]
152+ fn parse_v1_no_delegate_no_tlv ( ) {
153+ let bytes = build_onchain_token_data_bytes ( false , None ) ;
154+ let td = TokenData :: parse ( & bytes) . unwrap ( ) ;
155+ assert_eq ! ( td. mint. 0 , Pubkey :: from( [ 0x11 ; 32 ] ) ) ;
156+ assert_eq ! ( td. owner. 0 , Pubkey :: from( [ 0x22 ; 32 ] ) ) ;
157+ assert_eq ! ( td. amount. 0 , 100 ) ;
158+ assert ! ( td. delegate. is_none( ) ) ;
159+ assert_eq ! ( td. state, AccountState :: initialized) ;
160+ assert ! ( td. tlv. is_none( ) ) ;
161+ }
162+
163+ #[ test]
164+ fn parse_v1_with_delegate_no_tlv ( ) {
165+ let bytes = build_onchain_token_data_bytes ( true , None ) ;
166+ let td = TokenData :: parse ( & bytes) . unwrap ( ) ;
167+ assert_eq ! ( td. delegate. unwrap( ) . 0 , Pubkey :: from( [ 0x33 ; 32 ] ) ) ;
168+ assert ! ( td. tlv. is_none( ) ) ;
169+ }
170+
171+ #[ test]
172+ fn parse_v3_with_compressed_only_tlv ( ) {
173+ let tlv_raw = build_compressed_only_tlv ( ) ;
174+ let bytes = build_onchain_token_data_bytes ( true , Some ( & tlv_raw) ) ;
175+
176+ let td = TokenData :: parse ( & bytes) . unwrap ( ) ;
177+ assert_eq ! ( td. amount. 0 , 100 ) ;
178+ assert ! ( td. delegate. is_some( ) ) ;
179+ let tlv = td. tlv . unwrap ( ) ;
180+ assert_eq ! ( tlv. 0 , tlv_raw) ;
181+ }
182+
183+ #[ test]
184+ fn parse_v3_no_delegate_with_tlv ( ) {
185+ let tlv_raw = build_compressed_only_tlv ( ) ;
186+ let bytes = build_onchain_token_data_bytes ( false , Some ( & tlv_raw) ) ;
187+
188+ let td = TokenData :: parse ( & bytes) . unwrap ( ) ;
189+ assert ! ( td. delegate. is_none( ) ) ;
190+ assert ! ( td. tlv. is_some( ) ) ;
191+ assert_eq ! ( td. tlv. unwrap( ) . 0 , tlv_raw) ;
192+ }
193+
194+ #[ derive( Debug , BorshDeserialize ) ]
195+ struct OldTokenData {
196+ pub mint : SerializablePubkey ,
197+ pub owner : SerializablePubkey ,
198+ pub amount : UnsignedInteger ,
199+ pub delegate : Option < SerializablePubkey > ,
200+ pub state : AccountState ,
201+ pub tlv : Option < Base64String > ,
202+ }
203+
204+ #[ test]
205+ fn old_try_from_slice_fails_with_not_all_bytes_read ( ) {
206+ let tlv_raw = build_compressed_only_tlv ( ) ;
207+ let bytes = build_onchain_token_data_bytes ( true , Some ( & tlv_raw) ) ;
208+
209+ let err = OldTokenData :: try_from_slice ( & bytes)
210+ . expect_err ( "old BorshDeserialize must fail on V3 TLV data" ) ;
211+
212+ assert_eq ! ( err. kind( ) , std:: io:: ErrorKind :: InvalidData ) ;
213+ assert ! (
214+ err. to_string( ) . contains( "Not all bytes read" ) ,
215+ "Expected exact production error 'Not all bytes read', got: {}" ,
216+ err
217+ ) ;
218+ }
219+
220+ #[ test]
221+ fn new_parse_succeeds_where_old_try_from_slice_fails ( ) {
222+ let tlv_raw = build_compressed_only_tlv ( ) ;
223+ let bytes = build_onchain_token_data_bytes ( true , Some ( & tlv_raw) ) ;
224+
225+ // Old code fails:
226+ assert ! ( OldTokenData :: try_from_slice( & bytes) . is_err( ) ) ;
227+ // New code succeeds and captures TLV:
228+ let td = TokenData :: parse ( & bytes) . unwrap ( ) ;
229+ assert_eq ! ( td. tlv. unwrap( ) . 0 , tlv_raw) ;
230+ }
231+
232+ #[ test]
233+ fn roundtrip_no_tlv ( ) {
234+ let original = TokenData {
235+ mint : SerializablePubkey ( Pubkey :: new_unique ( ) ) ,
236+ owner : SerializablePubkey ( Pubkey :: new_unique ( ) ) ,
237+ amount : UnsignedInteger ( 42 ) ,
238+ delegate : None ,
239+ state : AccountState :: initialized,
240+ tlv : None ,
241+ } ;
242+ let mut bytes = Vec :: new ( ) ;
243+ borsh:: BorshSerialize :: serialize ( & original, & mut bytes) . unwrap ( ) ;
244+ let parsed = TokenData :: parse ( & bytes) . unwrap ( ) ;
245+ assert_eq ! ( parsed, original) ;
246+ }
247+
248+ #[ test]
249+ fn roundtrip_with_tlv ( ) {
250+ let original = TokenData {
251+ mint : SerializablePubkey ( Pubkey :: new_unique ( ) ) ,
252+ owner : SerializablePubkey ( Pubkey :: new_unique ( ) ) ,
253+ amount : UnsignedInteger ( 1_000_000 ) ,
254+ delegate : Some ( SerializablePubkey ( Pubkey :: new_unique ( ) ) ) ,
255+ state : AccountState :: frozen,
256+ tlv : Some ( Base64String ( vec ! [ 0xAA , 0xBB , 0xCC ] ) ) ,
257+ } ;
258+ let mut bytes = Vec :: new ( ) ;
259+ borsh:: BorshSerialize :: serialize ( & original, & mut bytes) . unwrap ( ) ;
260+ let parsed = TokenData :: parse ( & bytes) . unwrap ( ) ;
261+ assert_eq ! ( parsed, original) ;
262+ }
263+
264+ #[ test]
265+ fn byte_layout_no_delegate_no_tlv ( ) {
266+ let bytes = build_onchain_token_data_bytes ( false , None ) ;
267+ // mint(32) + owner(32) + amount(8) + delegate_none(1) + state(1) + tlv_none(1) = 75
268+ assert_eq ! ( bytes. len( ) , 75 ) ;
269+ let td = TokenData :: parse ( & bytes) . unwrap ( ) ;
270+ assert ! ( td. tlv. is_none( ) ) ;
271+ }
272+
273+ #[ test]
274+ fn byte_layout_with_delegate_no_tlv ( ) {
275+ let bytes = build_onchain_token_data_bytes ( true , None ) ;
276+ // mint(32) + owner(32) + amount(8) + delegate_some(1+32) + state(1) + tlv_none(1) = 107
277+ assert_eq ! ( bytes. len( ) , 107 ) ;
278+ }
279+
280+ #[ test]
281+ fn byte_layout_with_delegate_and_tlv ( ) {
282+ let tlv_raw = build_compressed_only_tlv ( ) ;
283+ // tlv_raw: u32(4) + discriminant(1) + u64(8) + u64(8) + u8(1) = 22
284+ assert_eq ! ( tlv_raw. len( ) , 22 ) ;
285+ let bytes = build_onchain_token_data_bytes ( true , Some ( & tlv_raw) ) ;
286+ // 107 (with delegate, no tlv excl the None byte) - 1 (remove None byte)
287+ // + 1 (Some tag) + 22 (tlv_raw) = 129
288+ assert_eq ! ( bytes. len( ) , 129 ) ;
289+ }
290+ }
0 commit comments