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