Skip to main content

chains_sdk/solana/
token_extensions.rs

1//! SPL Token-2022 extension instructions.
2//!
3//! Provides instruction builders for Token-2022 extensions:
4//! - Transfer Hook
5//! - Confidential Transfer
6//! - Transfer Fee
7//! - Default Account State
8//! - CPI Guard
9//! - Memo Required
10//! - Permanent Delegate
11//! - Interest-Bearing Config
12//! - Metadata (Token Metadata Interface)
13
14use super::transaction::{AccountMeta, Instruction};
15
16// ═══════════════════════════════════════════════════════════════════
17// Program IDs
18// ═══════════════════════════════════════════════════════════════════
19
20/// Token-2022 Program ID: `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb`
21pub const TOKEN_2022_ID: [u8; 32] = [
22    0x06, 0xDD, 0xF6, 0xE1, 0xEE, 0x75, 0x8F, 0xDE, 0x18, 0x42, 0x5D, 0xBC, 0xE4, 0x6C, 0xCD, 0xDA,
23    0xB6, 0x1A, 0xFC, 0x4D, 0x83, 0xB9, 0x0D, 0x27, 0xFE, 0xBD, 0xF9, 0x28, 0xD8, 0xA1, 0x8B, 0xFC,
24];
25
26// ═══════════════════════════════════════════════════════════════════
27// Default Account State Extension
28// ═══════════════════════════════════════════════════════════════════
29
30/// Account states for Default Account State extension.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum AccountState {
33    /// Uninitialized / default.
34    Uninitialized = 0,
35    /// Initialized (active).
36    Initialized = 1,
37    /// Frozen (cannot transfer).
38    Frozen = 2,
39}
40
41/// Initialize DefaultAccountState extension on a mint.
42///
43/// Sets the default state for new token accounts created for this mint.
44#[must_use]
45pub fn initialize_default_account_state(mint: &[u8; 32], state: AccountState) -> Instruction {
46    // Extension instruction: discriminator = 29 (DefaultAccountState)
47    // Sub-instruction: 0 = Initialize
48    let data = vec![29, 0, state as u8];
49    Instruction {
50        program_id: TOKEN_2022_ID,
51        accounts: vec![AccountMeta::new(*mint, false)],
52        data,
53    }
54}
55
56/// Update DefaultAccountState on a mint.
57#[must_use]
58pub fn update_default_account_state(
59    mint: &[u8; 32],
60    freeze_authority: &[u8; 32],
61    state: AccountState,
62) -> Instruction {
63    let data = vec![29, 1, state as u8];
64    Instruction {
65        program_id: TOKEN_2022_ID,
66        accounts: vec![
67            AccountMeta::new(*mint, false),
68            AccountMeta::new_readonly(*freeze_authority, true),
69        ],
70        data,
71    }
72}
73
74// ═══════════════════════════════════════════════════════════════════
75// Transfer Fee Extension
76// ═══════════════════════════════════════════════════════════════════
77
78/// Initialize TransferFeeConfig extension on a mint.
79///
80/// # Arguments
81/// - `mint` — The mint to configure
82/// - `transfer_fee_config_authority` — Authority to update fee config
83/// - `withdraw_withheld_authority` — Authority to withdraw withheld fees
84/// - `transfer_fee_basis_points` — Fee in basis points (100 = 1%)
85/// - `maximum_fee` — Maximum fee per transfer (in token smallest units)
86#[must_use]
87pub fn initialize_transfer_fee_config(
88    mint: &[u8; 32],
89    transfer_fee_config_authority: Option<&[u8; 32]>,
90    withdraw_withheld_authority: Option<&[u8; 32]>,
91    transfer_fee_basis_points: u16,
92    maximum_fee: u64,
93) -> Instruction {
94    // Extension instruction: discriminator = 26 (TransferFeeExtension)
95    // Sub-instruction: 0 = Initialize
96    let mut data = vec![26, 0];
97
98    // COption<Pubkey> for config authority
99    match transfer_fee_config_authority {
100        Some(auth) => {
101            data.push(1);
102            data.extend_from_slice(auth);
103        }
104        None => {
105            data.push(0);
106            data.extend_from_slice(&[0u8; 32]);
107        }
108    }
109
110    // COption<Pubkey> for withdraw authority
111    match withdraw_withheld_authority {
112        Some(auth) => {
113            data.push(1);
114            data.extend_from_slice(auth);
115        }
116        None => {
117            data.push(0);
118            data.extend_from_slice(&[0u8; 32]);
119        }
120    }
121
122    data.extend_from_slice(&transfer_fee_basis_points.to_le_bytes());
123    data.extend_from_slice(&maximum_fee.to_le_bytes());
124
125    Instruction {
126        program_id: TOKEN_2022_ID,
127        accounts: vec![AccountMeta::new(*mint, false)],
128        data,
129    }
130}
131
132/// Create a `HarvestWithheldTokensToMint` instruction.
133///
134/// Harvests withheld tokens from token accounts back to the mint.
135#[must_use]
136pub fn harvest_withheld_tokens_to_mint(
137    mint: &[u8; 32],
138    token_accounts: &[[u8; 32]],
139) -> Instruction {
140    let data = vec![26, 4]; // TransferFee + HarvestWithheld
141    let mut accounts = vec![AccountMeta::new(*mint, false)];
142    for acct in token_accounts {
143        accounts.push(AccountMeta::new(*acct, false));
144    }
145    Instruction {
146        program_id: TOKEN_2022_ID,
147        accounts,
148        data,
149    }
150}
151
152/// Create a `WithdrawWithheldTokensFromMint` instruction.
153#[must_use]
154pub fn withdraw_withheld_tokens_from_mint(
155    mint: &[u8; 32],
156    destination: &[u8; 32],
157    withdraw_authority: &[u8; 32],
158) -> Instruction {
159    let data = vec![26, 3]; // TransferFee + WithdrawFromMint
160    Instruction {
161        program_id: TOKEN_2022_ID,
162        accounts: vec![
163            AccountMeta::new(*mint, false),
164            AccountMeta::new(*destination, false),
165            AccountMeta::new_readonly(*withdraw_authority, true),
166        ],
167        data,
168    }
169}
170
171// ═══════════════════════════════════════════════════════════════════
172// Transfer Hook Extension
173// ═══════════════════════════════════════════════════════════════════
174
175/// Initialize TransferHook extension on a mint.
176///
177/// Sets the program that will be CPI-called on every token transfer.
178#[must_use]
179pub fn initialize_transfer_hook(
180    mint: &[u8; 32],
181    authority: Option<&[u8; 32]>,
182    transfer_hook_program_id: &[u8; 32],
183) -> Instruction {
184    // Extension instruction: discriminator = 36 (TransferHook)
185    // Sub-instruction: 0 = Initialize
186    let mut data = vec![36, 0];
187    match authority {
188        Some(auth) => {
189            data.push(1);
190            data.extend_from_slice(auth);
191        }
192        None => {
193            data.push(0);
194            data.extend_from_slice(&[0u8; 32]);
195        }
196    }
197    data.extend_from_slice(transfer_hook_program_id);
198
199    Instruction {
200        program_id: TOKEN_2022_ID,
201        accounts: vec![AccountMeta::new(*mint, false)],
202        data,
203    }
204}
205
206/// Update the TransferHook program ID on a mint.
207#[must_use]
208pub fn update_transfer_hook(
209    mint: &[u8; 32],
210    authority: &[u8; 32],
211    new_program_id: &[u8; 32],
212) -> Instruction {
213    let mut data = vec![36, 1]; // TransferHook + Update
214    data.extend_from_slice(new_program_id);
215    Instruction {
216        program_id: TOKEN_2022_ID,
217        accounts: vec![
218            AccountMeta::new(*mint, false),
219            AccountMeta::new_readonly(*authority, true),
220        ],
221        data,
222    }
223}
224
225// ═══════════════════════════════════════════════════════════════════
226// CPI Guard Extension
227// ═══════════════════════════════════════════════════════════════════
228
229/// Enable CPI Guard on a token account.
230///
231/// Prevents certain actions from being performed via CPI (cross-program invocation).
232#[must_use]
233pub fn enable_cpi_guard(account: &[u8; 32], owner: &[u8; 32]) -> Instruction {
234    let data = vec![37, 0]; // CpiGuard + Enable
235    Instruction {
236        program_id: TOKEN_2022_ID,
237        accounts: vec![
238            AccountMeta::new(*account, false),
239            AccountMeta::new_readonly(*owner, true),
240        ],
241        data,
242    }
243}
244
245/// Disable CPI Guard on a token account.
246#[must_use]
247pub fn disable_cpi_guard(account: &[u8; 32], owner: &[u8; 32]) -> Instruction {
248    let data = vec![37, 1]; // CpiGuard + Disable
249    Instruction {
250        program_id: TOKEN_2022_ID,
251        accounts: vec![
252            AccountMeta::new(*account, false),
253            AccountMeta::new_readonly(*owner, true),
254        ],
255        data,
256    }
257}
258
259// ═══════════════════════════════════════════════════════════════════
260// Permanent Delegate Extension
261// ═══════════════════════════════════════════════════════════════════
262
263/// Initialize PermanentDelegate extension on a mint.
264///
265/// Sets a delegate that can transfer/burn from any account of this mint.
266#[must_use]
267pub fn initialize_permanent_delegate(mint: &[u8; 32], delegate: &[u8; 32]) -> Instruction {
268    let mut data = vec![35, 0]; // PermanentDelegate + Initialize
269    data.extend_from_slice(delegate);
270    Instruction {
271        program_id: TOKEN_2022_ID,
272        accounts: vec![AccountMeta::new(*mint, false)],
273        data,
274    }
275}
276
277// ═══════════════════════════════════════════════════════════════════
278// Memo Required Extension (MemoTransfer)
279// ═══════════════════════════════════════════════════════════════════
280
281/// Enable required memo on incoming transfers for a token account.
282#[must_use]
283pub fn enable_required_memo_transfers(account: &[u8; 32], owner: &[u8; 32]) -> Instruction {
284    let data = vec![30, 0]; // MemoTransfer + Enable
285    Instruction {
286        program_id: TOKEN_2022_ID,
287        accounts: vec![
288            AccountMeta::new(*account, false),
289            AccountMeta::new_readonly(*owner, true),
290        ],
291        data,
292    }
293}
294
295/// Disable required memo on incoming transfers for a token account.
296#[must_use]
297pub fn disable_required_memo_transfers(account: &[u8; 32], owner: &[u8; 32]) -> Instruction {
298    let data = vec![30, 1]; // MemoTransfer + Disable
299    Instruction {
300        program_id: TOKEN_2022_ID,
301        accounts: vec![
302            AccountMeta::new(*account, false),
303            AccountMeta::new_readonly(*owner, true),
304        ],
305        data,
306    }
307}
308
309// ═══════════════════════════════════════════════════════════════════
310// Interest-Bearing Config Extension
311// ═══════════════════════════════════════════════════════════════════
312
313/// Initialize InterestBearingConfig extension on a mint.
314///
315/// # Arguments
316/// - `rate_authority` — Authority that can update the interest rate
317/// - `rate` — Interest rate in basis points (per year)
318#[must_use]
319pub fn initialize_interest_bearing_config(
320    mint: &[u8; 32],
321    rate_authority: Option<&[u8; 32]>,
322    rate: i16,
323) -> Instruction {
324    let mut data = vec![33, 0]; // InterestBearingConfig + Initialize
325    match rate_authority {
326        Some(auth) => {
327            data.push(1);
328            data.extend_from_slice(auth);
329        }
330        None => {
331            data.push(0);
332            data.extend_from_slice(&[0u8; 32]);
333        }
334    }
335    data.extend_from_slice(&rate.to_le_bytes());
336
337    Instruction {
338        program_id: TOKEN_2022_ID,
339        accounts: vec![AccountMeta::new(*mint, false)],
340        data,
341    }
342}
343
344/// Update the interest rate.
345#[must_use]
346pub fn update_interest_rate(
347    mint: &[u8; 32],
348    rate_authority: &[u8; 32],
349    new_rate: i16,
350) -> Instruction {
351    let mut data = vec![33, 1]; // InterestBearingConfig + Update
352    data.extend_from_slice(&new_rate.to_le_bytes());
353    Instruction {
354        program_id: TOKEN_2022_ID,
355        accounts: vec![
356            AccountMeta::new(*mint, false),
357            AccountMeta::new_readonly(*rate_authority, true),
358        ],
359        data,
360    }
361}
362
363// ═══════════════════════════════════════════════════════════════════
364// Token Metadata Interface (SPL Token Metadata)
365// ═══════════════════════════════════════════════════════════════════
366
367/// Initialize token metadata on a Token-2022 mint.
368///
369/// Sets the name, symbol, and URI for the token.
370#[must_use]
371pub fn initialize_token_metadata(
372    mint: &[u8; 32],
373    update_authority: &[u8; 32],
374    mint_authority: &[u8; 32],
375    name: &str,
376    symbol: &str,
377    uri: &str,
378) -> Instruction {
379    let mut data = Vec::new();
380    // Anchor-compatible discriminator for spl_token_metadata_interface::Initialize
381    // SHA256("spl_token_metadata_interface:initialize")[..8]
382    data.extend_from_slice(&[210, 225, 30, 162, 88, 184, 226, 143]);
383
384    // Borsh: string = u32_len + bytes
385    let name_bytes = name.as_bytes();
386    data.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
387    data.extend_from_slice(name_bytes);
388
389    let symbol_bytes = symbol.as_bytes();
390    data.extend_from_slice(&(symbol_bytes.len() as u32).to_le_bytes());
391    data.extend_from_slice(symbol_bytes);
392
393    let uri_bytes = uri.as_bytes();
394    data.extend_from_slice(&(uri_bytes.len() as u32).to_le_bytes());
395    data.extend_from_slice(uri_bytes);
396
397    Instruction {
398        program_id: TOKEN_2022_ID,
399        accounts: vec![
400            AccountMeta::new(*mint, false),
401            AccountMeta::new_readonly(*update_authority, false),
402            AccountMeta::new_readonly(*mint_authority, true),
403        ],
404        data,
405    }
406}
407
408/// Update a metadata field on a Token-2022 mint.
409///
410/// # Arguments
411/// - `field` — Field name (e.g., "name", "symbol", "uri", or custom key)
412/// - `value` — New value for the field
413#[must_use]
414pub fn update_token_metadata_field(
415    mint: &[u8; 32],
416    update_authority: &[u8; 32],
417    field: &str,
418    value: &str,
419) -> Instruction {
420    let mut data = Vec::new();
421    // SHA256("spl_token_metadata_interface:updating_field")[..8]
422    data.extend_from_slice(&[221, 233, 49, 45, 181, 202, 220, 200]);
423
424    let field_bytes = field.as_bytes();
425    data.extend_from_slice(&(field_bytes.len() as u32).to_le_bytes());
426    data.extend_from_slice(field_bytes);
427
428    let value_bytes = value.as_bytes();
429    data.extend_from_slice(&(value_bytes.len() as u32).to_le_bytes());
430    data.extend_from_slice(value_bytes);
431
432    Instruction {
433        program_id: TOKEN_2022_ID,
434        accounts: vec![
435            AccountMeta::new(*mint, false),
436            AccountMeta::new_readonly(*update_authority, true),
437        ],
438        data,
439    }
440}
441
442// ═══════════════════════════════════════════════════════════════════
443// Group / Member Token Extensions
444// ═══════════════════════════════════════════════════════════════════
445
446/// Initialize GroupPointer extension on a mint.
447#[must_use]
448pub fn initialize_group_pointer(
449    mint: &[u8; 32],
450    authority: Option<&[u8; 32]>,
451    group_address: &[u8; 32],
452) -> Instruction {
453    let mut data = vec![40, 0]; // GroupPointer + Initialize
454    match authority {
455        Some(auth) => {
456            data.push(1);
457            data.extend_from_slice(auth);
458        }
459        None => {
460            data.push(0);
461            data.extend_from_slice(&[0u8; 32]);
462        }
463    }
464    data.extend_from_slice(group_address);
465    Instruction {
466        program_id: TOKEN_2022_ID,
467        accounts: vec![AccountMeta::new(*mint, false)],
468        data,
469    }
470}
471
472/// Initialize GroupMemberPointer extension on a mint.
473#[must_use]
474pub fn initialize_group_member_pointer(
475    mint: &[u8; 32],
476    authority: Option<&[u8; 32]>,
477    member_address: &[u8; 32],
478) -> Instruction {
479    let mut data = vec![41, 0]; // GroupMemberPointer + Initialize
480    match authority {
481        Some(auth) => {
482            data.push(1);
483            data.extend_from_slice(auth);
484        }
485        None => {
486            data.push(0);
487            data.extend_from_slice(&[0u8; 32]);
488        }
489    }
490    data.extend_from_slice(member_address);
491    Instruction {
492        program_id: TOKEN_2022_ID,
493        accounts: vec![AccountMeta::new(*mint, false)],
494        data,
495    }
496}
497
498// ═══════════════════════════════════════════════════════════════════
499// Tests
500// ═══════════════════════════════════════════════════════════════════
501
502#[cfg(test)]
503#[allow(clippy::unwrap_used, clippy::expect_used)]
504mod tests {
505    use super::*;
506
507    const MINT: [u8; 32] = [0xAA; 32];
508    const AUTH: [u8; 32] = [0xBB; 32];
509    const DEST: [u8; 32] = [0xCC; 32];
510
511    // ─── Program ID ──────────────────────────────────────────────
512
513    #[test]
514    fn test_token_2022_program_id_length() {
515        assert_eq!(TOKEN_2022_ID.len(), 32);
516    }
517
518    #[test]
519    fn test_all_instructions_use_token_2022_id() {
520        let ix1 = initialize_default_account_state(&MINT, AccountState::Frozen);
521        let ix2 = initialize_transfer_fee_config(&MINT, None, None, 0, 0);
522        let ix3 = initialize_transfer_hook(&MINT, None, &[0; 32]);
523        let ix4 = enable_cpi_guard(&MINT, &AUTH);
524        let ix5 = initialize_permanent_delegate(&MINT, &AUTH);
525        let ix6 = enable_required_memo_transfers(&MINT, &AUTH);
526        let ix7 = initialize_interest_bearing_config(&MINT, None, 0);
527        for ix in [ix1, ix2, ix3, ix4, ix5, ix6, ix7] {
528            assert_eq!(ix.program_id, TOKEN_2022_ID);
529        }
530    }
531
532    // ─── Default Account State ───────────────────────────────────
533
534    #[test]
535    fn test_initialize_default_account_state() {
536        let ix = initialize_default_account_state(&MINT, AccountState::Frozen);
537        assert_eq!(ix.data[0], 29);
538        assert_eq!(ix.data[1], 0);
539        assert_eq!(ix.data[2], 2); // Frozen
540    }
541
542    #[test]
543    fn test_initialize_default_account_state_initialized() {
544        let ix = initialize_default_account_state(&MINT, AccountState::Initialized);
545        assert_eq!(ix.data[2], 1);
546    }
547
548    #[test]
549    fn test_initialize_default_account_state_uninitialized() {
550        let ix = initialize_default_account_state(&MINT, AccountState::Uninitialized);
551        assert_eq!(ix.data[2], 0);
552    }
553
554    #[test]
555    fn test_update_default_account_state() {
556        let ix = update_default_account_state(&MINT, &AUTH, AccountState::Initialized);
557        assert_eq!(ix.data[0], 29);
558        assert_eq!(ix.data[1], 1);
559        assert_eq!(ix.data[2], 1);
560        assert_eq!(ix.accounts.len(), 2);
561    }
562
563    #[test]
564    fn test_update_default_account_state_accounts() {
565        let ix = update_default_account_state(&MINT, &AUTH, AccountState::Frozen);
566        assert_eq!(ix.accounts[0].pubkey, MINT);
567        assert!(!ix.accounts[0].is_signer);
568        assert_eq!(ix.accounts[1].pubkey, AUTH);
569        assert!(ix.accounts[1].is_signer);
570    }
571
572    // ─── Transfer Fee ────────────────────────────────────────────
573
574    #[test]
575    fn test_initialize_transfer_fee_config() {
576        let ix = initialize_transfer_fee_config(&MINT, Some(&AUTH), Some(&DEST), 100, 1_000_000);
577        assert_eq!(ix.data[0], 26);
578        assert_eq!(ix.data[1], 0);
579        assert!(ix.data.len() > 70);
580    }
581
582    #[test]
583    fn test_initialize_transfer_fee_config_data_structure() {
584        let ix = initialize_transfer_fee_config(
585            &MINT,
586            Some(&AUTH),
587            Some(&DEST),
588            200, // 2%
589            5_000_000,
590        );
591        // [26, 0, 1, AUTH(32), 1, DEST(32), fee_bps(2), max_fee(8)]
592        assert_eq!(ix.data[0], 26);
593        assert_eq!(ix.data[1], 0);
594        assert_eq!(ix.data[2], 1); // Some for config authority
595        assert_eq!(&ix.data[3..35], &AUTH);
596        assert_eq!(ix.data[35], 1); // Some for withdraw authority
597        assert_eq!(&ix.data[36..68], &DEST);
598        // Fee basis points
599        let bps = u16::from_le_bytes([ix.data[68], ix.data[69]]);
600        assert_eq!(bps, 200);
601        // Max fee
602        let max_fee = u64::from_le_bytes(ix.data[70..78].try_into().unwrap());
603        assert_eq!(max_fee, 5_000_000);
604    }
605
606    #[test]
607    fn test_initialize_transfer_fee_config_no_authorities() {
608        let ix = initialize_transfer_fee_config(&MINT, None, None, 50, 500);
609        assert_eq!(ix.data[0], 26);
610        assert_eq!(ix.data[2], 0); // None for first authority
611        assert_eq!(&ix.data[3..35], &[0u8; 32]); // zero pubkey
612        assert_eq!(ix.data[35], 0); // None for second
613    }
614
615    #[test]
616    fn test_harvest_withheld_tokens() {
617        let accounts = vec![[0x11; 32], [0x22; 32]];
618        let ix = harvest_withheld_tokens_to_mint(&MINT, &accounts);
619        assert_eq!(ix.data, vec![26, 4]);
620        assert_eq!(ix.accounts.len(), 3);
621    }
622
623    #[test]
624    fn test_harvest_withheld_tokens_empty() {
625        let ix = harvest_withheld_tokens_to_mint(&MINT, &[]);
626        assert_eq!(ix.accounts.len(), 1); // just the mint
627    }
628
629    #[test]
630    fn test_harvest_withheld_tokens_many() {
631        let accounts: Vec<[u8; 32]> = (0..10).map(|i| [i; 32]).collect();
632        let ix = harvest_withheld_tokens_to_mint(&MINT, &accounts);
633        assert_eq!(ix.accounts.len(), 11); // mint + 10
634    }
635
636    #[test]
637    fn test_withdraw_withheld_tokens() {
638        let ix = withdraw_withheld_tokens_from_mint(&MINT, &DEST, &AUTH);
639        assert_eq!(ix.data, vec![26, 3]);
640        assert_eq!(ix.accounts.len(), 3);
641        assert_eq!(ix.accounts[0].pubkey, MINT);
642        assert_eq!(ix.accounts[1].pubkey, DEST);
643        assert_eq!(ix.accounts[2].pubkey, AUTH);
644        assert!(ix.accounts[2].is_signer);
645    }
646
647    // ─── Transfer Hook ───────────────────────────────────────────
648
649    #[test]
650    fn test_initialize_transfer_hook() {
651        let hook_program = [0xDD; 32];
652        let ix = initialize_transfer_hook(&MINT, Some(&AUTH), &hook_program);
653        assert_eq!(ix.data[0], 36);
654        assert_eq!(ix.data[1], 0);
655        assert_eq!(ix.data[2], 1); // Some
656        assert_eq!(&ix.data[3..35], &AUTH);
657        assert_eq!(&ix.data[35..67], &hook_program);
658    }
659
660    #[test]
661    fn test_initialize_transfer_hook_no_authority() {
662        let hook_program = [0xDD; 32];
663        let ix = initialize_transfer_hook(&MINT, None, &hook_program);
664        assert_eq!(ix.data[2], 0); // None
665        assert_eq!(&ix.data[3..35], &[0u8; 32]);
666        assert_eq!(&ix.data[35..67], &hook_program);
667    }
668
669    #[test]
670    fn test_update_transfer_hook() {
671        let new_program = [0xEE; 32];
672        let ix = update_transfer_hook(&MINT, &AUTH, &new_program);
673        assert_eq!(ix.data[0], 36);
674        assert_eq!(ix.data[1], 1);
675        assert_eq!(&ix.data[2..34], &new_program);
676    }
677
678    #[test]
679    fn test_update_transfer_hook_accounts() {
680        let ix = update_transfer_hook(&MINT, &AUTH, &[0; 32]);
681        assert_eq!(ix.accounts[0].pubkey, MINT);
682        assert!(!ix.accounts[0].is_signer);
683        assert_eq!(ix.accounts[1].pubkey, AUTH);
684        assert!(ix.accounts[1].is_signer);
685    }
686
687    // ─── CPI Guard ───────────────────────────────────────────────
688
689    #[test]
690    fn test_enable_cpi_guard() {
691        let ix = enable_cpi_guard(&MINT, &AUTH);
692        assert_eq!(ix.data, vec![37, 0]);
693        assert_eq!(ix.accounts.len(), 2);
694    }
695
696    #[test]
697    fn test_disable_cpi_guard() {
698        let ix = disable_cpi_guard(&MINT, &AUTH);
699        assert_eq!(ix.data, vec![37, 1]);
700    }
701
702    #[test]
703    fn test_cpi_guard_toggle_differ() {
704        let enable = enable_cpi_guard(&MINT, &AUTH);
705        let disable = disable_cpi_guard(&MINT, &AUTH);
706        assert_ne!(enable.data, disable.data);
707    }
708
709    // ─── Permanent Delegate ──────────────────────────────────────
710
711    #[test]
712    fn test_initialize_permanent_delegate() {
713        let ix = initialize_permanent_delegate(&MINT, &AUTH);
714        assert_eq!(ix.data[0], 35);
715        assert_eq!(ix.data[1], 0);
716        assert_eq!(&ix.data[2..34], &AUTH);
717        assert_eq!(ix.accounts.len(), 1);
718    }
719
720    #[test]
721    fn test_permanent_delegate_data_length() {
722        let ix = initialize_permanent_delegate(&MINT, &AUTH);
723        assert_eq!(ix.data.len(), 2 + 32); // discriminator + delegate pubkey
724    }
725
726    // ─── Memo Transfer ───────────────────────────────────────────
727
728    #[test]
729    fn test_enable_required_memo() {
730        let ix = enable_required_memo_transfers(&MINT, &AUTH);
731        assert_eq!(ix.data, vec![30, 0]);
732    }
733
734    #[test]
735    fn test_disable_required_memo() {
736        let ix = disable_required_memo_transfers(&MINT, &AUTH);
737        assert_eq!(ix.data, vec![30, 1]);
738    }
739
740    #[test]
741    fn test_memo_toggle_differ() {
742        let enable = enable_required_memo_transfers(&MINT, &AUTH);
743        let disable = disable_required_memo_transfers(&MINT, &AUTH);
744        assert_ne!(enable.data, disable.data);
745    }
746
747    // ─── Interest-Bearing Config ─────────────────────────────────
748
749    #[test]
750    fn test_initialize_interest_bearing() {
751        let ix = initialize_interest_bearing_config(&MINT, Some(&AUTH), 500);
752        assert_eq!(ix.data[0], 33);
753        assert_eq!(ix.data[1], 0);
754        assert_eq!(ix.data[2], 1); // Some
755        let rate_offset = ix.data.len() - 2;
756        let rate = i16::from_le_bytes([ix.data[rate_offset], ix.data[rate_offset + 1]]);
757        assert_eq!(rate, 500);
758    }
759
760    #[test]
761    fn test_initialize_interest_bearing_no_authority() {
762        let ix = initialize_interest_bearing_config(&MINT, None, 100);
763        assert_eq!(ix.data[2], 0); // None
764        assert_eq!(&ix.data[3..35], &[0u8; 32]);
765    }
766
767    #[test]
768    fn test_initialize_interest_bearing_negative_rate() {
769        let ix = initialize_interest_bearing_config(&MINT, Some(&AUTH), -500);
770        let rate_offset = ix.data.len() - 2;
771        let rate = i16::from_le_bytes([ix.data[rate_offset], ix.data[rate_offset + 1]]);
772        assert_eq!(rate, -500);
773    }
774
775    #[test]
776    fn test_update_interest_rate() {
777        let ix = update_interest_rate(&MINT, &AUTH, -100);
778        assert_eq!(ix.data[0], 33);
779        assert_eq!(ix.data[1], 1);
780        let rate = i16::from_le_bytes([ix.data[2], ix.data[3]]);
781        assert_eq!(rate, -100);
782    }
783
784    #[test]
785    fn test_update_interest_rate_positive() {
786        let ix = update_interest_rate(&MINT, &AUTH, 1000);
787        let rate = i16::from_le_bytes([ix.data[2], ix.data[3]]);
788        assert_eq!(rate, 1000);
789    }
790
791    // ─── Token Metadata ──────────────────────────────────────────
792
793    #[test]
794    fn test_initialize_token_metadata() {
795        let ix = initialize_token_metadata(
796            &MINT,
797            &AUTH,
798            &AUTH,
799            "Test Token",
800            "TEST",
801            "https://example.com/meta.json",
802        );
803        assert_eq!(ix.program_id, TOKEN_2022_ID);
804        assert_eq!(ix.accounts.len(), 3);
805        assert_eq!(ix.data.len(), 8 + 4 + 10 + 4 + 4 + 4 + 29);
806    }
807
808    #[test]
809    fn test_initialize_token_metadata_discriminator() {
810        let ix = initialize_token_metadata(&MINT, &AUTH, &AUTH, "X", "Y", "Z");
811        // Known discriminator: [210, 225, 30, 162, 88, 184, 226, 143]
812        assert_eq!(&ix.data[0..8], &[210, 225, 30, 162, 88, 184, 226, 143]);
813    }
814
815    #[test]
816    fn test_initialize_token_metadata_string_encoding() {
817        let ix = initialize_token_metadata(&MINT, &AUTH, &AUTH, "MyToken", "MTK", "https://uri");
818        // After 8-byte discriminator:
819        // name: u32(7) + "MyToken"
820        let name_len = u32::from_le_bytes(ix.data[8..12].try_into().unwrap());
821        assert_eq!(name_len, 7);
822        assert_eq!(&ix.data[12..19], b"MyToken");
823        // symbol: u32(3) + "MTK"
824        let sym_len = u32::from_le_bytes(ix.data[19..23].try_into().unwrap());
825        assert_eq!(sym_len, 3);
826        assert_eq!(&ix.data[23..26], b"MTK");
827        // uri
828        let uri_len = u32::from_le_bytes(ix.data[26..30].try_into().unwrap());
829        assert_eq!(uri_len, 11);
830        assert_eq!(&ix.data[30..41], b"https://uri");
831    }
832
833    #[test]
834    fn test_initialize_token_metadata_accounts() {
835        let mint_auth = [0xDD; 32];
836        let ix = initialize_token_metadata(&MINT, &AUTH, &mint_auth, "X", "Y", "Z");
837        assert_eq!(ix.accounts[0].pubkey, MINT);
838        assert!(!ix.accounts[0].is_signer);
839        assert_eq!(ix.accounts[1].pubkey, AUTH);
840        assert!(!ix.accounts[1].is_signer); // update_authority is not signer for init
841        assert_eq!(ix.accounts[2].pubkey, mint_auth);
842        assert!(ix.accounts[2].is_signer); // mint_authority IS signer
843    }
844
845    #[test]
846    fn test_update_token_metadata_field() {
847        let ix = update_token_metadata_field(&MINT, &AUTH, "name", "New Name");
848        assert_eq!(ix.program_id, TOKEN_2022_ID);
849        assert_eq!(ix.accounts.len(), 2);
850        assert_eq!(ix.data.len(), 8 + 4 + 4 + 4 + 8);
851    }
852
853    #[test]
854    fn test_update_token_metadata_field_discriminator() {
855        let ix = update_token_metadata_field(&MINT, &AUTH, "x", "y");
856        assert_eq!(&ix.data[0..8], &[221, 233, 49, 45, 181, 202, 220, 200]);
857    }
858
859    #[test]
860    fn test_update_token_metadata_field_encoding() {
861        let ix = update_token_metadata_field(&MINT, &AUTH, "uri", "https://new");
862        let field_len = u32::from_le_bytes(ix.data[8..12].try_into().unwrap());
863        assert_eq!(field_len, 3);
864        assert_eq!(&ix.data[12..15], b"uri");
865        let val_len = u32::from_le_bytes(ix.data[15..19].try_into().unwrap());
866        assert_eq!(val_len, 11);
867        assert_eq!(&ix.data[19..30], b"https://new");
868    }
869
870    // ─── Group / Member ──────────────────────────────────────────
871
872    #[test]
873    fn test_initialize_group_pointer() {
874        let group_addr = [0xFF; 32];
875        let ix = initialize_group_pointer(&MINT, Some(&AUTH), &group_addr);
876        assert_eq!(ix.data[0], 40);
877        assert_eq!(ix.data[1], 0);
878        assert_eq!(ix.data[2], 1); // Some
879        assert_eq!(&ix.data[3..35], &AUTH);
880        assert_eq!(&ix.data[35..67], &group_addr);
881    }
882
883    #[test]
884    fn test_initialize_group_pointer_no_authority() {
885        let group_addr = [0xFF; 32];
886        let ix = initialize_group_pointer(&MINT, None, &group_addr);
887        assert_eq!(ix.data[2], 0); // None
888        assert_eq!(&ix.data[3..35], &[0u8; 32]);
889    }
890
891    #[test]
892    fn test_initialize_group_member_pointer() {
893        let member_addr = [0xEE; 32];
894        let ix = initialize_group_member_pointer(&MINT, None, &member_addr);
895        assert_eq!(ix.data[0], 41);
896        assert_eq!(ix.data[1], 0);
897        assert_eq!(ix.data[2], 0); // None
898    }
899
900    #[test]
901    fn test_initialize_group_member_pointer_with_authority() {
902        let member_addr = [0xEE; 32];
903        let ix = initialize_group_member_pointer(&MINT, Some(&AUTH), &member_addr);
904        assert_eq!(ix.data[2], 1); // Some
905        assert_eq!(&ix.data[3..35], &AUTH);
906        assert_eq!(&ix.data[35..67], &member_addr);
907    }
908
909    // ─── Account State Enum ──────────────────────────────────────
910
911    #[test]
912    fn test_account_state_values() {
913        assert_eq!(AccountState::Uninitialized as u8, 0);
914        assert_eq!(AccountState::Initialized as u8, 1);
915        assert_eq!(AccountState::Frozen as u8, 2);
916    }
917
918    #[test]
919    fn test_account_state_eq() {
920        assert_eq!(AccountState::Frozen, AccountState::Frozen);
921        assert_ne!(AccountState::Frozen, AccountState::Initialized);
922    }
923
924    // ─── Extension Discriminators ────────────────────────────────
925
926    #[test]
927    fn test_all_discriminator_values() {
928        // Verify each extension uses a distinct discriminator
929        let discriminators = [
930            initialize_transfer_fee_config(&MINT, None, None, 0, 0).data[0], // 26
931            initialize_default_account_state(&MINT, AccountState::Frozen).data[0], // 29
932            enable_required_memo_transfers(&MINT, &AUTH).data[0],            // 30
933            initialize_interest_bearing_config(&MINT, None, 0).data[0],      // 33
934            initialize_permanent_delegate(&MINT, &AUTH).data[0],             // 35
935            initialize_transfer_hook(&MINT, None, &[0; 32]).data[0],         // 36
936            enable_cpi_guard(&MINT, &AUTH).data[0],                          // 37
937            initialize_group_pointer(&MINT, None, &[0; 32]).data[0],         // 40
938            initialize_group_member_pointer(&MINT, None, &[0; 32]).data[0],  // 41
939        ];
940        assert_eq!(discriminators, [26, 29, 30, 33, 35, 36, 37, 40, 41]);
941    }
942}