Skip to main content

chains_sdk/solana/
programs.rs

1//! Additional Solana program helpers: ATA, Memo, Stake, Durable Nonce.
2
3use super::transaction::{AccountMeta, Instruction};
4
5// ═══════════════════════════════════════════════════════════════════
6// Associated Token Account (ATA)
7// ═══════════════════════════════════════════════════════════════════
8
9/// ATA Program ID: `ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL`.
10pub const ATA_PROGRAM_ID: [u8; 32] = [
11    140, 151, 37, 143, 78, 36, 137, 241, 187, 61, 16, 41, 20, 142, 13, 131, 11, 90, 19, 153, 218,
12    255, 16, 132, 4, 142, 123, 216, 219, 233, 248, 89,
13];
14
15/// SPL Token Program ID.
16pub const SPL_TOKEN_PROGRAM_ID: [u8; 32] = [
17    6, 221, 246, 225, 215, 101, 161, 147, 217, 203, 225, 70, 206, 235, 121, 172, 28, 180, 133, 237,
18    95, 91, 55, 145, 58, 140, 245, 133, 126, 255, 0, 169,
19];
20
21/// System Program ID.
22const SYSTEM_PROGRAM_ID: [u8; 32] = [0; 32];
23
24/// Derive the Associated Token Account address.
25///
26/// ATA = PDA(ATA_PROGRAM_ID, [wallet, TOKEN_PROGRAM_ID, mint])
27///
28/// Returns the deterministic ATA address as 32 bytes.
29pub fn derive_ata_address(wallet: &[u8; 32], mint: &[u8; 32]) -> [u8; 32] {
30    // Simplified PDA derivation: SHA-256(seeds || program_id || "ProgramDerivedAddress")
31    use sha2::{Digest, Sha256};
32    let mut hasher = Sha256::new();
33    hasher.update(wallet);
34    hasher.update(SPL_TOKEN_PROGRAM_ID);
35    hasher.update(mint);
36    hasher.update(ATA_PROGRAM_ID);
37    hasher.update(b"ProgramDerivedAddress");
38    let result = hasher.finalize();
39    let mut out = [0u8; 32];
40    out.copy_from_slice(&result);
41    out
42}
43
44/// Create an Associated Token Account instruction.
45///
46/// Creates the ATA for `wallet` and `mint` if it doesn't exist.
47/// The payer covers the rent-exempt balance.
48pub fn create_ata(payer: [u8; 32], wallet: [u8; 32], mint: [u8; 32]) -> Instruction {
49    let ata = derive_ata_address(&wallet, &mint);
50
51    Instruction {
52        program_id: ATA_PROGRAM_ID,
53        accounts: vec![
54            AccountMeta::new(payer, true),            // payer (writable, signer)
55            AccountMeta::new(ata, false),             // ATA (writable)
56            AccountMeta::new_readonly(wallet, false), // wallet owner
57            AccountMeta::new_readonly(mint, false),   // token mint
58            AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false), // system program
59            AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false), // token program
60        ],
61        data: vec![0], // CreateAssociatedTokenAccount = index 0
62    }
63}
64
65/// Create an ATA instruction with idempotency (CreateIdempotent).
66///
67/// Like `create_ata` but doesn't fail if the account already exists.
68pub fn create_ata_idempotent(payer: [u8; 32], wallet: [u8; 32], mint: [u8; 32]) -> Instruction {
69    let ata = derive_ata_address(&wallet, &mint);
70
71    Instruction {
72        program_id: ATA_PROGRAM_ID,
73        accounts: vec![
74            AccountMeta::new(payer, true),
75            AccountMeta::new(ata, false),
76            AccountMeta::new_readonly(wallet, false),
77            AccountMeta::new_readonly(mint, false),
78            AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
79            AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false),
80        ],
81        data: vec![1], // CreateIdempotent = index 1
82    }
83}
84
85// ═══════════════════════════════════════════════════════════════════
86// Memo Program
87// ═══════════════════════════════════════════════════════════════════
88
89/// Memo Program ID (v2): `MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr`.
90pub const MEMO_PROGRAM_ID: [u8; 32] = [
91    5, 74, 83, 80, 248, 93, 200, 130, 214, 20, 165, 86, 114, 120, 138, 41, 109, 223, 30, 171, 171,
92    208, 166, 6, 120, 136, 73, 50, 244, 238, 246, 160,
93];
94
95/// Create a Memo instruction (v2 — supports signer verification).
96///
97/// # Arguments
98/// - `memo` — UTF-8 memo text (max ~566 bytes for a single tx)
99/// - `signers` — Optional signer public keys that must sign this memo
100pub fn memo(memo_text: &str, signers: &[[u8; 32]]) -> Instruction {
101    let accounts: Vec<AccountMeta> = signers
102        .iter()
103        .map(|pk| AccountMeta::new_readonly(*pk, true))
104        .collect();
105
106    Instruction {
107        program_id: MEMO_PROGRAM_ID,
108        accounts,
109        data: memo_text.as_bytes().to_vec(),
110    }
111}
112
113/// Create a simple memo instruction with no required signers.
114pub fn memo_unsigned(memo_text: &str) -> Instruction {
115    Instruction {
116        program_id: MEMO_PROGRAM_ID,
117        accounts: vec![],
118        data: memo_text.as_bytes().to_vec(),
119    }
120}
121
122// ═══════════════════════════════════════════════════════════════════
123// Stake Program
124// ═══════════════════════════════════════════════════════════════════
125
126/// Stake Program ID: `Stake11111111111111111111111111111111111111`.
127pub const STAKE_PROGRAM_ID: [u8; 32] = [
128    6, 161, 216, 23, 145, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
129    0, 0, 0,
130];
131
132/// Stake Config ID.
133pub const STAKE_CONFIG_ID: [u8; 32] = [
134    6, 161, 216, 23, 165, 55, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
135    0, 0, 0,
136];
137
138/// Clock sysvar.
139pub const CLOCK_SYSVAR: [u8; 32] = [
140    6, 167, 213, 23, 24, 199, 116, 201, 40, 86, 99, 152, 105, 29, 94, 182, 139, 94, 184, 163, 155,
141    75, 109, 92, 115, 85, 91, 33, 0, 0, 0, 0,
142];
143
144/// Stake History sysvar.
145pub const STAKE_HISTORY_SYSVAR: [u8; 32] = [
146    6, 167, 213, 23, 25, 47, 10, 175, 198, 242, 101, 227, 251, 119, 204, 122, 218, 130, 197, 41,
147    208, 190, 59, 19, 110, 45, 0, 85, 32, 0, 0, 0,
148];
149
150/// Create a DelegateStake instruction.
151///
152/// Delegates a stake account to a validator's vote account.
153pub fn stake_delegate(
154    stake_account: [u8; 32],
155    vote_account: [u8; 32],
156    stake_authority: [u8; 32],
157) -> Instruction {
158    let mut data = vec![0u8; 4]; // instruction index 2 = Delegate
159    data[0] = 2;
160
161    Instruction {
162        program_id: STAKE_PROGRAM_ID,
163        accounts: vec![
164            AccountMeta::new(stake_account, false), // stake account (writable)
165            AccountMeta::new_readonly(vote_account, false), // vote account
166            AccountMeta::new_readonly(CLOCK_SYSVAR, false),
167            AccountMeta::new_readonly(STAKE_HISTORY_SYSVAR, false),
168            AccountMeta::new_readonly(STAKE_CONFIG_ID, false),
169            AccountMeta::new_readonly(stake_authority, true), // stake authority (signer)
170        ],
171        data,
172    }
173}
174
175/// Create a Deactivate instruction.
176///
177/// Deactivates a stake account, beginning the cooldown period.
178pub fn stake_deactivate(stake_account: [u8; 32], stake_authority: [u8; 32]) -> Instruction {
179    let mut data = vec![0u8; 4];
180    data[0] = 5; // instruction index 5 = Deactivate
181
182    Instruction {
183        program_id: STAKE_PROGRAM_ID,
184        accounts: vec![
185            AccountMeta::new(stake_account, false),
186            AccountMeta::new_readonly(CLOCK_SYSVAR, false),
187            AccountMeta::new_readonly(stake_authority, true),
188        ],
189        data,
190    }
191}
192
193/// Create a Withdraw instruction.
194///
195/// Withdraws SOL from a deactivated stake account.
196pub fn stake_withdraw(
197    stake_account: [u8; 32],
198    withdraw_authority: [u8; 32],
199    recipient: [u8; 32],
200    lamports: u64,
201) -> Instruction {
202    let mut data = vec![0u8; 12];
203    data[0] = 4; // instruction index 4 = Withdraw
204    data[4..12].copy_from_slice(&lamports.to_le_bytes());
205
206    Instruction {
207        program_id: STAKE_PROGRAM_ID,
208        accounts: vec![
209            AccountMeta::new(stake_account, false),
210            AccountMeta::new(recipient, false),
211            AccountMeta::new_readonly(CLOCK_SYSVAR, false),
212            AccountMeta::new_readonly(STAKE_HISTORY_SYSVAR, false),
213            AccountMeta::new_readonly(withdraw_authority, true),
214        ],
215        data,
216    }
217}
218
219// ═══════════════════════════════════════════════════════════════════
220// Durable Nonce Transactions
221// ═══════════════════════════════════════════════════════════════════
222
223/// Create an AdvanceNonceAccount instruction.
224///
225/// This must be the first instruction in a durable nonce transaction.
226/// It advances the nonce value, preventing replay.
227pub fn advance_nonce(nonce_account: [u8; 32], nonce_authority: [u8; 32]) -> Instruction {
228    // Recent Sysvar ID
229    let recent_blockhashes_sysvar: [u8; 32] = [
230        6, 167, 213, 23, 24, 199, 116, 201, 40, 86, 99, 152, 105, 29, 94, 182, 139, 94, 184, 163,
231        155, 75, 109, 92, 115, 85, 91, 32, 0, 0, 0, 0,
232    ];
233
234    let mut data = vec![0u8; 4];
235    data[0] = 4; // AdvanceNonceAccount = SystemInstruction index 4
236
237    Instruction {
238        program_id: SYSTEM_PROGRAM_ID,
239        accounts: vec![
240            AccountMeta::new(nonce_account, false),
241            AccountMeta::new_readonly(recent_blockhashes_sysvar, false),
242            AccountMeta::new_readonly(nonce_authority, true),
243        ],
244        data,
245    }
246}
247
248/// Create a NonceInitialize instruction.
249///
250/// Initializes a nonce account with a specified authority.
251pub fn initialize_nonce(nonce_account: [u8; 32], nonce_authority: [u8; 32]) -> Instruction {
252    let recent_blockhashes_sysvar: [u8; 32] = [
253        6, 167, 213, 23, 24, 199, 116, 201, 40, 86, 99, 152, 105, 29, 94, 182, 139, 94, 184, 163,
254        155, 75, 109, 92, 115, 85, 91, 32, 0, 0, 0, 0,
255    ];
256    // Rent sysvar
257    let rent_sysvar: [u8; 32] = [
258        6, 167, 213, 23, 25, 47, 10, 175, 198, 242, 101, 227, 251, 119, 204, 122, 218, 130, 197,
259        41, 208, 190, 59, 19, 110, 45, 0, 85, 31, 0, 0, 0,
260    ];
261
262    let mut data = vec![0u8; 36];
263    data[0] = 6; // InitializeNonceAccount = SystemInstruction index 6
264    data[4..36].copy_from_slice(&nonce_authority);
265
266    Instruction {
267        program_id: SYSTEM_PROGRAM_ID,
268        accounts: vec![
269            AccountMeta::new(nonce_account, false),
270            AccountMeta::new_readonly(recent_blockhashes_sysvar, false),
271            AccountMeta::new_readonly(rent_sysvar, false),
272        ],
273        data,
274    }
275}
276
277// ═══════════════════════════════════════════════════════════════════
278// Address Lookup Table Program
279// ═══════════════════════════════════════════════════════════════════
280
281/// Address Lookup Table program helpers.
282pub mod address_lookup_table {
283    use super::*;
284
285    /// Address Lookup Table Program ID: `AddressLookupTab1e1111111111111111111111111`
286    pub const ID: [u8; 32] = [
287        0x06, 0xa1, 0xd8, 0x17, 0x91, 0x37, 0x68, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
288        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
289        0x00, 0x00,
290    ];
291
292    /// Create an Address Lookup Table.
293    ///
294    /// # Arguments
295    /// - `authority` — Authority that can extend/deactivate/close the table
296    /// - `payer` — Account that pays for storage
297    /// - `lookup_table` — The derived lookup table address
298    /// - `recent_slot` — A recent slot for derivation
299    #[must_use]
300    pub fn create(
301        authority: [u8; 32],
302        payer: [u8; 32],
303        lookup_table: [u8; 32],
304        recent_slot: u64,
305    ) -> Instruction {
306        let mut data = vec![0u8; 4]; // CreateLookupTable = 0
307        data.extend_from_slice(&recent_slot.to_le_bytes());
308
309        Instruction {
310            program_id: ID,
311            accounts: vec![
312                AccountMeta::new(lookup_table, false),
313                AccountMeta::new_readonly(authority, true),
314                AccountMeta::new(payer, true),
315                AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
316            ],
317            data,
318        }
319    }
320
321    /// Extend an Address Lookup Table with new addresses.
322    #[must_use]
323    pub fn extend(
324        lookup_table: [u8; 32],
325        authority: [u8; 32],
326        payer: [u8; 32],
327        new_addresses: &[[u8; 32]],
328    ) -> Instruction {
329        let mut data = vec![0u8; 4];
330        data[0] = 2; // ExtendLookupTable = 2
331                     // u32 count of addresses
332        data.extend_from_slice(&(new_addresses.len() as u32).to_le_bytes());
333        for addr in new_addresses {
334            data.extend_from_slice(addr);
335        }
336
337        Instruction {
338            program_id: ID,
339            accounts: vec![
340                AccountMeta::new(lookup_table, false),
341                AccountMeta::new_readonly(authority, true),
342                AccountMeta::new(payer, true),
343                AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
344            ],
345            data,
346        }
347    }
348
349    /// Deactivate an Address Lookup Table.
350    ///
351    /// After deactivation, the table enters a cooldown and can then be closed.
352    #[must_use]
353    pub fn deactivate(lookup_table: [u8; 32], authority: [u8; 32]) -> Instruction {
354        let mut data = vec![0u8; 4];
355        data[0] = 3; // DeactivateLookupTable = 3
356
357        Instruction {
358            program_id: ID,
359            accounts: vec![
360                AccountMeta::new(lookup_table, false),
361                AccountMeta::new_readonly(authority, true),
362            ],
363            data,
364        }
365    }
366
367    /// Close a deactivated Address Lookup Table and reclaim rent.
368    #[must_use]
369    pub fn close(lookup_table: [u8; 32], authority: [u8; 32], recipient: [u8; 32]) -> Instruction {
370        let mut data = vec![0u8; 4];
371        data[0] = 4; // CloseLookupTable = 4
372
373        Instruction {
374            program_id: ID,
375            accounts: vec![
376                AccountMeta::new(lookup_table, false),
377                AccountMeta::new_readonly(authority, true),
378                AccountMeta::new(recipient, false),
379            ],
380            data,
381        }
382    }
383}
384
385// ═══════════════════════════════════════════════════════════════════
386// Metaplex Token Metadata Program
387// ═══════════════════════════════════════════════════════════════════
388
389/// Metaplex Token Metadata program helpers.
390pub mod token_metadata {
391    use super::*;
392
393    /// Metaplex Token Metadata Program ID: `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s`
394    pub const ID: [u8; 32] = [
395        0x0b, 0x74, 0x65, 0x78, 0x74, 0x50, 0x55, 0x73, 0x40, 0x6a, 0xc2, 0x14, 0x12, 0xf3, 0x26,
396        0xf7, 0x1b, 0x1e, 0xce, 0xf0, 0x77, 0x87, 0x28, 0x76, 0xf8, 0xba, 0x16, 0x1b, 0x70, 0x4c,
397        0x9f, 0x04,
398    ];
399
400    /// Token Metadata data for CreateMetadataAccountV3.
401    #[derive(Debug, Clone)]
402    pub struct DataV2 {
403        /// The name of the asset.
404        pub name: String,
405        /// The symbol for the asset.
406        pub symbol: String,
407        /// URI pointing to metadata JSON (Arweave, IPFS, etc).
408        pub uri: String,
409        /// Royalty basis points (e.g., 500 = 5%).
410        pub seller_fee_basis_points: u16,
411        /// Optional creators list.
412        pub creators: Option<Vec<Creator>>,
413    }
414
415    /// A creator with an address and share.
416    #[derive(Debug, Clone)]
417    pub struct Creator {
418        /// Creator's public key.
419        pub address: [u8; 32],
420        /// Whether this creator has verified the metadata.
421        pub verified: bool,
422        /// Share of royalties (0-100, all shares must sum to 100).
423        pub share: u8,
424    }
425
426    /// Derive the metadata account address for a given mint.
427    ///
428    /// PDA: `["metadata", metadata_program_id, mint]`
429    pub fn derive_metadata_address(mint: &[u8; 32]) -> [u8; 32] {
430        use sha2::{Digest, Sha256};
431        // Simplified PDA — in production use find_program_address
432        let mut hasher = Sha256::new();
433        hasher.update(b"metadata");
434        hasher.update(ID);
435        hasher.update(mint);
436        hasher.update(ID);
437        hasher.update(b"ProgramDerivedAddress");
438        let result = hasher.finalize();
439        let mut out = [0u8; 32];
440        out.copy_from_slice(&result);
441        out
442    }
443
444    /// Serialize a Borsh-encoded string (u32 len + UTF-8 bytes).
445    fn borsh_string(s: &str) -> Vec<u8> {
446        let bytes = s.as_bytes();
447        let mut out = Vec::with_capacity(4 + bytes.len());
448        out.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
449        out.extend_from_slice(bytes);
450        out
451    }
452
453    /// Serialize DataV2 to Borsh bytes.
454    fn serialize_data_v2(data: &DataV2) -> Vec<u8> {
455        let mut buf = Vec::new();
456        buf.extend(borsh_string(&data.name));
457        buf.extend(borsh_string(&data.symbol));
458        buf.extend(borsh_string(&data.uri));
459        buf.extend_from_slice(&data.seller_fee_basis_points.to_le_bytes());
460
461        // Creators: Option<Vec<Creator>>
462        match &data.creators {
463            None => buf.push(0),
464            Some(creators) => {
465                buf.push(1);
466                buf.extend_from_slice(&(creators.len() as u32).to_le_bytes());
467                for c in creators {
468                    buf.extend_from_slice(&c.address);
469                    buf.push(u8::from(c.verified));
470                    buf.push(c.share);
471                }
472            }
473        }
474        buf
475    }
476
477    /// Create a `CreateMetadataAccountV3` instruction.
478    ///
479    /// # Arguments
480    /// - `metadata` — The derived metadata account PDA
481    /// - `mint` — The token mint
482    /// - `mint_authority` — Current authority of the mint
483    /// - `payer` — Who pays for the account
484    /// - `update_authority` — Who can update the metadata
485    /// - `data` — The token metadata
486    /// - `is_mutable` — Whether the metadata can be updated later
487    pub fn create_metadata_v3(
488        metadata: [u8; 32],
489        mint: [u8; 32],
490        mint_authority: [u8; 32],
491        payer: [u8; 32],
492        update_authority: [u8; 32],
493        data: &DataV2,
494        is_mutable: bool,
495    ) -> Instruction {
496        // Instruction discriminator for CreateMetadataAccountV3 = 33
497        let mut ix_data = vec![33];
498        ix_data.extend(serialize_data_v2(data));
499        ix_data.push(u8::from(is_mutable));
500        // collection_details: Option = None
501        ix_data.push(0);
502
503        Instruction {
504            program_id: ID,
505            accounts: vec![
506                AccountMeta::new(metadata, false),
507                AccountMeta::new_readonly(mint, false),
508                AccountMeta::new_readonly(mint_authority, true),
509                AccountMeta::new(payer, true),
510                AccountMeta::new_readonly(update_authority, false),
511                AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
512            ],
513            data: ix_data,
514        }
515    }
516
517    /// Create an `UpdateMetadataAccountV2` instruction.
518    ///
519    /// # Arguments
520    /// - `metadata` — The metadata account to update
521    /// - `update_authority` — Current update authority (signer)
522    /// - `new_data` — Optional new data (pass None to keep existing)
523    /// - `new_update_authority` — Optional new update authority
524    /// - `primary_sale_happened` — Optional flag
525    /// - `is_mutable` — Optional mutability flag
526    pub fn update_metadata_v2(
527        metadata: [u8; 32],
528        update_authority: [u8; 32],
529        new_data: Option<&DataV2>,
530        new_update_authority: Option<&[u8; 32]>,
531        primary_sale_happened: Option<bool>,
532        is_mutable: Option<bool>,
533    ) -> Instruction {
534        // Instruction discriminator for UpdateMetadataAccountV2 = 15
535        let mut ix_data = vec![15];
536
537        // Optional<DataV2>
538        match new_data {
539            None => ix_data.push(0),
540            Some(d) => {
541                ix_data.push(1);
542                ix_data.extend(serialize_data_v2(d));
543            }
544        }
545
546        // Optional<Pubkey>
547        match new_update_authority {
548            None => ix_data.push(0),
549            Some(auth) => {
550                ix_data.push(1);
551                ix_data.extend_from_slice(auth);
552            }
553        }
554
555        // Optional<bool> primary_sale_happened
556        match primary_sale_happened {
557            None => ix_data.push(0),
558            Some(val) => {
559                ix_data.push(1);
560                ix_data.push(u8::from(val));
561            }
562        }
563
564        // Optional<bool> is_mutable
565        match is_mutable {
566            None => ix_data.push(0),
567            Some(val) => {
568                ix_data.push(1);
569                ix_data.push(u8::from(val));
570            }
571        }
572
573        Instruction {
574            program_id: ID,
575            accounts: vec![
576                AccountMeta::new(metadata, false),
577                AccountMeta::new_readonly(update_authority, true),
578            ],
579            data: ix_data,
580        }
581    }
582}
583
584// ═══════════════════════════════════════════════════════════════════
585// Tests
586// ═══════════════════════════════════════════════════════════════════
587
588#[cfg(test)]
589#[allow(clippy::unwrap_used, clippy::expect_used)]
590mod tests {
591    use super::*;
592
593    const WALLET: [u8; 32] = [1; 32];
594    const MINT: [u8; 32] = [2; 32];
595    const PAYER: [u8; 32] = [3; 32];
596
597    // ─── ATA Tests ──────────────────────────────────────────────
598
599    #[test]
600    fn test_derive_ata_deterministic() {
601        let ata1 = derive_ata_address(&WALLET, &MINT);
602        let ata2 = derive_ata_address(&WALLET, &MINT);
603        assert_eq!(ata1, ata2);
604    }
605
606    #[test]
607    fn test_derive_ata_different_mints() {
608        let mint2 = [3; 32];
609        let ata1 = derive_ata_address(&WALLET, &MINT);
610        let ata2 = derive_ata_address(&WALLET, &mint2);
611        assert_ne!(ata1, ata2);
612    }
613
614    #[test]
615    fn test_create_ata_instruction() {
616        let ix = create_ata(PAYER, WALLET, MINT);
617        assert_eq!(ix.program_id, ATA_PROGRAM_ID);
618        assert_eq!(ix.accounts.len(), 6);
619        assert_eq!(ix.data, vec![0]); // CreateAssociatedTokenAccount
620        assert!(ix.accounts[0].is_signer); // payer signs
621    }
622
623    #[test]
624    fn test_create_ata_idempotent() {
625        let ix = create_ata_idempotent(PAYER, WALLET, MINT);
626        assert_eq!(ix.data, vec![1]); // CreateIdempotent
627    }
628
629    // ─── Memo Tests ─────────────────────────────────────────────
630
631    #[test]
632    fn test_memo_basic() {
633        let ix = memo("Hello, Solana!", &[WALLET]);
634        assert_eq!(ix.program_id, MEMO_PROGRAM_ID);
635        assert_eq!(ix.data, b"Hello, Solana!");
636        assert_eq!(ix.accounts.len(), 1);
637        assert!(ix.accounts[0].is_signer);
638    }
639
640    #[test]
641    fn test_memo_unsigned() {
642        let ix = memo_unsigned("test memo");
643        assert!(ix.accounts.is_empty());
644        assert_eq!(ix.data, b"test memo");
645    }
646
647    #[test]
648    fn test_memo_multiple_signers() {
649        let signer2 = [4; 32];
650        let ix = memo("multi-signer memo", &[WALLET, signer2]);
651        assert_eq!(ix.accounts.len(), 2);
652    }
653
654    // ─── Stake Tests ────────────────────────────────────────────
655
656    #[test]
657    fn test_stake_delegate() {
658        let vote = [5; 32];
659        let ix = stake_delegate(WALLET, vote, PAYER);
660        assert_eq!(ix.program_id, STAKE_PROGRAM_ID);
661        assert_eq!(ix.accounts.len(), 6);
662        assert_eq!(ix.data[0], 2); // DelegateStake index
663    }
664
665    #[test]
666    fn test_stake_deactivate() {
667        let ix = stake_deactivate(WALLET, PAYER);
668        assert_eq!(ix.accounts.len(), 3);
669        assert_eq!(ix.data[0], 5); // Deactivate index
670    }
671
672    #[test]
673    fn test_stake_withdraw() {
674        let recipient = [6; 32];
675        let ix = stake_withdraw(WALLET, PAYER, recipient, 1_000_000_000);
676        assert_eq!(ix.data[0], 4); // Withdraw index
677        let lamports = u64::from_le_bytes(ix.data[4..12].try_into().unwrap());
678        assert_eq!(lamports, 1_000_000_000);
679    }
680
681    // ─── Durable Nonce Tests ────────────────────────────────────
682
683    #[test]
684    fn test_advance_nonce() {
685        let nonce_account = [7; 32];
686        let ix = advance_nonce(nonce_account, PAYER);
687        assert_eq!(ix.program_id, SYSTEM_PROGRAM_ID);
688        assert_eq!(ix.accounts.len(), 3);
689        assert_eq!(ix.data[0], 4); // AdvanceNonceAccount
690    }
691
692    #[test]
693    fn test_initialize_nonce() {
694        let nonce_account = [8; 32];
695        let ix = initialize_nonce(nonce_account, PAYER);
696        assert_eq!(ix.data[0], 6); // InitializeNonceAccount
697        assert_eq!(&ix.data[4..36], &PAYER); // authority
698    }
699
700    // ─── Address Lookup Table Tests ─────────────────────────────
701
702    #[test]
703    fn test_alt_create() {
704        let table = [9; 32];
705        let ix = address_lookup_table::create(PAYER, PAYER, table, 12345);
706        assert_eq!(ix.program_id, address_lookup_table::ID);
707        assert_eq!(ix.accounts.len(), 4);
708        assert_eq!(ix.data[0], 0); // CreateLookupTable
709        let slot = u64::from_le_bytes(ix.data[4..12].try_into().unwrap());
710        assert_eq!(slot, 12345);
711    }
712
713    #[test]
714    fn test_alt_extend() {
715        let table = [9; 32];
716        let addrs = [[10u8; 32], [11u8; 32]];
717        let ix = address_lookup_table::extend(table, PAYER, PAYER, &addrs);
718        assert_eq!(ix.data[0], 2); // ExtendLookupTable
719        let count = u32::from_le_bytes(ix.data[4..8].try_into().unwrap());
720        assert_eq!(count, 2);
721        assert_eq!(&ix.data[8..40], &addrs[0]);
722        assert_eq!(&ix.data[40..72], &addrs[1]);
723    }
724
725    #[test]
726    fn test_alt_deactivate() {
727        let table = [9; 32];
728        let ix = address_lookup_table::deactivate(table, PAYER);
729        assert_eq!(ix.data[0], 3);
730        assert_eq!(ix.accounts.len(), 2);
731    }
732
733    #[test]
734    fn test_alt_close() {
735        let table = [9; 32];
736        let recipient = [10; 32];
737        let ix = address_lookup_table::close(table, PAYER, recipient);
738        assert_eq!(ix.data[0], 4);
739        assert_eq!(ix.accounts.len(), 3);
740    }
741
742    // ─── Metaplex Token Metadata Tests ──────────────────────────
743
744    #[test]
745    fn test_derive_metadata_deterministic() {
746        let addr1 = token_metadata::derive_metadata_address(&MINT);
747        let addr2 = token_metadata::derive_metadata_address(&MINT);
748        assert_eq!(addr1, addr2);
749    }
750
751    #[test]
752    fn test_create_metadata_v3() {
753        let metadata = [12; 32];
754        let update_auth = [13; 32];
755        let data = token_metadata::DataV2 {
756            name: "My NFT".to_string(),
757            symbol: "MNFT".to_string(),
758            uri: "https://example.com/meta.json".to_string(),
759            seller_fee_basis_points: 500,
760            creators: Some(vec![token_metadata::Creator {
761                address: PAYER,
762                verified: true,
763                share: 100,
764            }]),
765        };
766        let ix = token_metadata::create_metadata_v3(
767            metadata,
768            MINT,
769            PAYER,
770            PAYER,
771            update_auth,
772            &data,
773            true,
774        );
775        assert_eq!(ix.program_id, token_metadata::ID);
776        assert_eq!(ix.accounts.len(), 6);
777        assert_eq!(ix.data[0], 33); // CreateMetadataAccountV3
778    }
779
780    #[test]
781    fn test_update_metadata_v2() {
782        let metadata = [12; 32];
783        let ix = token_metadata::update_metadata_v2(metadata, PAYER, None, None, Some(true), None);
784        assert_eq!(ix.data[0], 15); // UpdateMetadataAccountV2
785        assert_eq!(ix.accounts.len(), 2);
786    }
787
788    #[test]
789    fn test_metadata_without_creators() {
790        let metadata = [12; 32];
791        let update_auth = [13; 32];
792        let data = token_metadata::DataV2 {
793            name: "Token".to_string(),
794            symbol: "TKN".to_string(),
795            uri: "https://example.com".to_string(),
796            seller_fee_basis_points: 0,
797            creators: None,
798        };
799        let ix = token_metadata::create_metadata_v3(
800            metadata,
801            MINT,
802            PAYER,
803            PAYER,
804            update_auth,
805            &data,
806            false,
807        );
808        assert_eq!(ix.data[0], 33);
809        // is_mutable should be false (0) near the end
810        let last_data = ix.data.last().unwrap();
811        // collection_details = None (0)
812        assert_eq!(*last_data, 0);
813    }
814}