Skip to main content

chains_sdk/solana/
metaplex.rs

1//! Metaplex NFT instruction builders for Solana.
2//!
3//! Supports Token Metadata v3 (create, update, verify collection)
4//! and Bubblegum compressed NFTs (mint, transfer).
5//!
6//! # Example
7//! ```no_run
8//! use chains_sdk::solana::metaplex::*;
9//! use chains_sdk::solana::transaction::AccountMeta;
10//!
11//! let mint = [0xAA; 32];
12//! let authority = [0xBB; 32];
13//! let payer = [0xCC; 32];
14//! let metadata_acct = [0xDD; 32];
15//!
16//! let data = MetadataData {
17//!     name: "Cool NFT".into(),
18//!     symbol: "COOL".into(),
19//!     uri: "https://example.com/nft.json".into(),
20//!     seller_fee_basis_points: 500,
21//!     creators: vec![Creator { address: authority, verified: true, share: 100 }],
22//! };
23//!
24//! let ix = create_metadata_account_v3(&metadata_acct, &mint, &authority, &payer, &authority, &data, true, None);
25//! ```
26
27use crate::solana::transaction::{AccountMeta, Instruction};
28
29// ═══════════════════════════════════════════════════════════════════
30// Program IDs
31// ═══════════════════════════════════════════════════════════════════
32
33/// Metaplex Token Metadata program ID.
34/// `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s`
35pub const METADATA_PROGRAM_ID: [u8; 32] = [
36    11, 112, 101, 177, 227, 209, 124, 69, 56, 157, 82, 127, 107, 4, 195, 205, 88, 184, 108, 115,
37    26, 160, 253, 181, 73, 182, 209, 188, 3, 248, 41, 70,
38];
39
40/// Bubblegum program ID (compressed NFTs).
41/// `BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY`
42pub const BUBBLEGUM_PROGRAM_ID: [u8; 32] = [
43    0x98, 0x8B, 0x80, 0xEB, 0x79, 0x35, 0x28, 0x69, 0xB2, 0x24, 0x74, 0x5F, 0x59, 0xDD, 0xBF, 0x8A,
44    0x26, 0x58, 0xCA, 0x13, 0xDC, 0x68, 0x81, 0x21, 0x26, 0x35, 0x1C, 0xAE, 0x07, 0xC1, 0xA5, 0xA5,
45];
46
47/// System program ID (for rent/payer operations).
48const SYSTEM_PROGRAM_ID: [u8; 32] = [0u8; 32];
49
50/// Sysvar Rent ID: `SysvarRent111111111111111111111111111111111`
51const SYSVAR_RENT_ID: [u8; 32] = [
52    0x06, 0xA7, 0xD5, 0x17, 0x19, 0x2C, 0x5C, 0x51, 0x21, 0x8C, 0xC9, 0x4C, 0x3D, 0x4A, 0xF1, 0x7F,
53    0x58, 0xDA, 0xEE, 0x08, 0x9B, 0xA1, 0xFD, 0x44, 0xE3, 0xDB, 0xD9, 0x8A, 0x00, 0x00, 0x00, 0x00,
54];
55
56// ═══════════════════════════════════════════════════════════════════
57// Data Types
58// ═══════════════════════════════════════════════════════════════════
59
60/// Creator info for NFT metadata.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Creator {
63    /// Creator's wallet address.
64    pub address: [u8; 32],
65    /// Whether this creator has verified the metadata.
66    pub verified: bool,
67    /// Percentage share (0-100, all must sum to 100).
68    pub share: u8,
69}
70
71/// Metadata for an NFT.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct MetadataData {
74    /// Name of the NFT (max 32 characters).
75    pub name: String,
76    /// Symbol (max 10 characters).
77    pub symbol: String,
78    /// URI to off-chain metadata JSON (max 200 characters).
79    pub uri: String,
80    /// Royalty basis points (e.g., 500 = 5%).
81    pub seller_fee_basis_points: u16,
82    /// List of creators.
83    pub creators: Vec<Creator>,
84}
85
86/// Collection info (optional).
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct Collection {
89    /// Whether this NFT is verified as part of the collection.
90    pub verified: bool,
91    /// The collection NFT mint address.
92    pub key: [u8; 32],
93}
94
95/// Uses configuration (optional).
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct Uses {
98    /// Use method.
99    pub use_method: UseMethod,
100    /// Remaining uses.
101    pub remaining: u64,
102    /// Total uses.
103    pub total: u64,
104}
105
106/// How the NFT can be "used".
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108#[repr(u8)]
109pub enum UseMethod {
110    /// Can be burned.
111    Burn = 0,
112    /// Usable multiple times.
113    Multiple = 1,
114    /// Single-use.
115    Single = 2,
116}
117
118// ═══════════════════════════════════════════════════════════════════
119// Serialization Helpers
120// ═══════════════════════════════════════════════════════════════════
121
122fn borsh_string(data: &mut Vec<u8>, s: &str) {
123    data.extend_from_slice(&(s.len() as u32).to_le_bytes());
124    data.extend_from_slice(s.as_bytes());
125}
126
127fn borsh_option_pubkey(data: &mut Vec<u8>, key: Option<&[u8; 32]>) {
128    match key {
129        Some(k) => {
130            data.push(1); // Some
131            data.extend_from_slice(k);
132        }
133        None => data.push(0), // None
134    }
135}
136
137fn borsh_creators(data: &mut Vec<u8>, creators: &[Creator]) {
138    data.push(1); // Some(creators)
139    data.extend_from_slice(&(creators.len() as u32).to_le_bytes());
140    for c in creators {
141        data.extend_from_slice(&c.address);
142        data.push(u8::from(c.verified));
143        data.push(c.share);
144    }
145}
146
147fn borsh_metadata_data(data: &mut Vec<u8>, md: &MetadataData) {
148    borsh_string(data, &md.name);
149    borsh_string(data, &md.symbol);
150    borsh_string(data, &md.uri);
151    data.extend_from_slice(&md.seller_fee_basis_points.to_le_bytes());
152    borsh_creators(data, &md.creators);
153}
154
155fn borsh_collection(data: &mut Vec<u8>, collection: Option<&Collection>) {
156    match collection {
157        Some(c) => {
158            data.push(1);
159            data.push(u8::from(c.verified));
160            data.extend_from_slice(&c.key);
161        }
162        None => data.push(0),
163    }
164}
165
166fn borsh_uses(data: &mut Vec<u8>, uses: Option<&Uses>) {
167    match uses {
168        Some(u) => {
169            data.push(1);
170            data.push(u.use_method as u8);
171            data.extend_from_slice(&u.remaining.to_le_bytes());
172            data.extend_from_slice(&u.total.to_le_bytes());
173        }
174        None => data.push(0),
175    }
176}
177
178// ═══════════════════════════════════════════════════════════════════
179// Token Metadata v3 Instructions
180// ═══════════════════════════════════════════════════════════════════
181
182/// Create a metadata account for an NFT (CreateMetadataAccountV3, discriminator 33).
183///
184/// # Accounts
185/// 0. `[writable]` Metadata account (PDA)
186/// 1. `[]` Mint
187/// 2. `[signer]` Mint authority
188/// 3. `[signer, writable]` Payer
189/// 4. `[]` Update authority
190/// 5. `[]` System program
191/// 6. `[]` Rent sysvar
192#[allow(clippy::too_many_arguments)]
193pub fn create_metadata_account_v3(
194    metadata_account: &[u8; 32],
195    mint: &[u8; 32],
196    mint_authority: &[u8; 32],
197    payer: &[u8; 32],
198    update_authority: &[u8; 32],
199    metadata: &MetadataData,
200    is_mutable: bool,
201    collection: Option<&Collection>,
202) -> Instruction {
203    let mut data = Vec::with_capacity(256);
204    data.push(33); // CreateMetadataAccountV3 discriminator
205
206    borsh_metadata_data(&mut data, metadata);
207    data.push(u8::from(is_mutable));
208    borsh_collection(&mut data, collection);
209    borsh_uses(&mut data, None); // uses: None
210
211    Instruction {
212        program_id: METADATA_PROGRAM_ID,
213        accounts: vec![
214            AccountMeta::new(*metadata_account, false), // metadata (writable)
215            AccountMeta::new_readonly(*mint, false),    // mint
216            AccountMeta::new_readonly(*mint_authority, true), // mint authority (signer)
217            AccountMeta::new(*payer, true),             // payer (signer, writable)
218            AccountMeta::new_readonly(*update_authority, false), // update authority
219            AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
220            AccountMeta::new_readonly(SYSVAR_RENT_ID, false),
221        ],
222        data,
223    }
224}
225
226/// Create a master edition (CreateMasterEditionV3, discriminator 17).
227///
228/// # Accounts
229/// 0. `[writable]` Edition account (PDA)
230/// 1. `[writable]` Mint
231/// 2. `[signer]` Update authority
232/// 3. `[signer]` Mint authority
233/// 4. `[signer, writable]` Payer
234/// 5. `[]` Metadata account
235/// 6. `[]` Token program
236/// 7. `[]` System program
237/// 8. `[]` Rent sysvar
238pub fn create_master_edition_v3(
239    edition_account: &[u8; 32],
240    mint: &[u8; 32],
241    update_authority: &[u8; 32],
242    mint_authority: &[u8; 32],
243    payer: &[u8; 32],
244    metadata_account: &[u8; 32],
245    max_supply: Option<u64>,
246) -> Instruction {
247    let mut data = Vec::with_capacity(16);
248    data.push(17); // CreateMasterEditionV3 discriminator
249
250    match max_supply {
251        Some(supply) => {
252            data.push(1); // Some
253            data.extend_from_slice(&supply.to_le_bytes());
254        }
255        None => data.push(0), // None (unlimited)
256    }
257
258    // SPL Token program ID: `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA`
259    let token_program: [u8; 32] = [
260        0x06, 0xDD, 0xF6, 0xE1, 0xD7, 0x65, 0xA1, 0x93, 0xD9, 0xCB, 0xE1, 0x46, 0xCE, 0xEB, 0x79,
261        0xAC, 0x1C, 0xB4, 0x85, 0xED, 0x5F, 0x5B, 0x37, 0x91, 0x3A, 0x8C, 0xF5, 0x85, 0x7E, 0xFF,
262        0x00, 0xA9,
263    ];
264
265    Instruction {
266        program_id: METADATA_PROGRAM_ID,
267        accounts: vec![
268            AccountMeta::new(*edition_account, false),
269            AccountMeta::new(*mint, false),
270            AccountMeta::new_readonly(*update_authority, true),
271            AccountMeta::new_readonly(*mint_authority, true),
272            AccountMeta::new(*payer, true),
273            AccountMeta::new_readonly(*metadata_account, false),
274            AccountMeta::new_readonly(token_program, false),
275            AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
276            AccountMeta::new_readonly(SYSVAR_RENT_ID, false),
277        ],
278        data,
279    }
280}
281
282/// Verify an NFT as part of a collection (VerifyCollection, discriminator 18).
283///
284/// # Accounts
285/// 0. `[writable]` Metadata account
286/// 1. `[signer]` Collection authority
287/// 2. `[signer, writable]` Payer
288/// 3. `[]` Collection mint
289/// 4. `[]` Collection metadata account
290/// 5. `[]` Collection master edition account
291pub fn verify_collection(
292    metadata_account: &[u8; 32],
293    collection_authority: &[u8; 32],
294    payer: &[u8; 32],
295    collection_mint: &[u8; 32],
296    collection_metadata: &[u8; 32],
297    collection_edition: &[u8; 32],
298) -> Instruction {
299    let data = vec![18]; // VerifyCollection discriminator
300
301    Instruction {
302        program_id: METADATA_PROGRAM_ID,
303        accounts: vec![
304            AccountMeta::new(*metadata_account, false),
305            AccountMeta::new_readonly(*collection_authority, true),
306            AccountMeta::new(*payer, true),
307            AccountMeta::new_readonly(*collection_mint, false),
308            AccountMeta::new_readonly(*collection_metadata, false),
309            AccountMeta::new_readonly(*collection_edition, false),
310        ],
311        data,
312    }
313}
314
315/// Update metadata (UpdateMetadataAccountV2, discriminator 15).
316///
317/// # Accounts
318/// 0. `[writable]` Metadata account
319/// 1. `[signer]` Update authority
320pub fn update_metadata_account_v2(
321    metadata_account: &[u8; 32],
322    update_authority: &[u8; 32],
323    new_data: Option<&MetadataData>,
324    new_update_authority: Option<&[u8; 32]>,
325    primary_sale_happened: Option<bool>,
326    is_mutable: Option<bool>,
327) -> Instruction {
328    let mut data = Vec::with_capacity(256);
329    data.push(15); // UpdateMetadataAccountV2 discriminator
330
331    // Option<Data>
332    match new_data {
333        Some(md) => {
334            data.push(1);
335            borsh_metadata_data(&mut data, md);
336        }
337        None => data.push(0),
338    }
339
340    // Option<Pubkey> new update authority
341    borsh_option_pubkey(&mut data, new_update_authority);
342
343    // Option<bool> primary_sale_happened
344    match primary_sale_happened {
345        Some(v) => {
346            data.push(1);
347            data.push(u8::from(v));
348        }
349        None => data.push(0),
350    }
351
352    // Option<bool> is_mutable
353    match is_mutable {
354        Some(v) => {
355            data.push(1);
356            data.push(u8::from(v));
357        }
358        None => data.push(0),
359    }
360
361    Instruction {
362        program_id: METADATA_PROGRAM_ID,
363        accounts: vec![
364            AccountMeta::new(*metadata_account, false),
365            AccountMeta::new_readonly(*update_authority, true),
366        ],
367        data,
368    }
369}
370
371// ═══════════════════════════════════════════════════════════════════
372// Bubblegum (Compressed NFT) Instructions
373// ═══════════════════════════════════════════════════════════════════
374
375/// Mint a compressed NFT (Bubblegum MintV1).
376///
377/// Uses the Anchor discriminator for `mint_v1`: SHA-256("global:mint_v1")[..8]
378///
379/// # Accounts
380/// 0. `[writable]` Tree authority PDA
381/// 1. `[]` Leaf owner
382/// 2. `[]` Leaf delegate
383/// 3. `[writable]` Merkle tree account
384/// 4. `[signer, writable]` Payer
385/// 5. `[signer]` Tree delegate
386/// 6. `[]` Log wrapper (SPL Noop)
387/// 7. `[]` Compression program
388/// 8. `[]` System program
389#[allow(clippy::too_many_arguments)]
390pub fn mint_v1(
391    tree_authority: &[u8; 32],
392    leaf_owner: &[u8; 32],
393    leaf_delegate: &[u8; 32],
394    merkle_tree: &[u8; 32],
395    payer: &[u8; 32],
396    tree_delegate: &[u8; 32],
397    log_wrapper: &[u8; 32],
398    compression_program: &[u8; 32],
399    metadata: &MetadataData,
400) -> Instruction {
401    // Anchor discriminator for "mint_v1"
402    let discriminator: [u8; 8] = [145, 98, 192, 118, 184, 147, 118, 104];
403
404    let mut data = Vec::with_capacity(256);
405    data.extend_from_slice(&discriminator);
406    borsh_metadata_data(&mut data, metadata);
407
408    Instruction {
409        program_id: BUBBLEGUM_PROGRAM_ID,
410        accounts: vec![
411            AccountMeta::new(*tree_authority, false),
412            AccountMeta::new_readonly(*leaf_owner, false),
413            AccountMeta::new_readonly(*leaf_delegate, false),
414            AccountMeta::new(*merkle_tree, false),
415            AccountMeta::new(*payer, true),
416            AccountMeta::new_readonly(*tree_delegate, true),
417            AccountMeta::new_readonly(*log_wrapper, false),
418            AccountMeta::new_readonly(*compression_program, false),
419            AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
420        ],
421        data,
422    }
423}
424
425/// Transfer a compressed NFT.
426///
427/// Anchor discriminator for "transfer": `SHA-256("global:transfer")[..8]`
428///
429/// # Accounts
430///
431/// - 0. `[writable]` Tree authority PDA
432/// - 1. `[]` Leaf owner
433/// - 2. `[]` Leaf delegate
434/// - 3. `[]` New leaf owner
435/// - 4. `[writable]` Merkle tree
436/// - 5. `[]` Log wrapper
437/// - 6. `[]` Compression program
438/// - 7. `[]` System program
439/// - 8..N. `[]` Proof accounts
440#[allow(clippy::too_many_arguments)]
441pub fn transfer(
442    tree_authority: &[u8; 32],
443    leaf_owner: &[u8; 32],
444    leaf_delegate: &[u8; 32],
445    new_leaf_owner: &[u8; 32],
446    merkle_tree: &[u8; 32],
447    log_wrapper: &[u8; 32],
448    compression_program: &[u8; 32],
449    root: &[u8; 32],
450    data_hash: &[u8; 32],
451    creator_hash: &[u8; 32],
452    nonce: u64,
453    index: u32,
454    proof: &[[u8; 32]],
455) -> Instruction {
456    // Anchor discriminator for "transfer"
457    let discriminator: [u8; 8] = [163, 52, 200, 231, 140, 3, 69, 186];
458
459    let mut data = Vec::with_capacity(128);
460    data.extend_from_slice(&discriminator);
461    data.extend_from_slice(root);
462    data.extend_from_slice(data_hash);
463    data.extend_from_slice(creator_hash);
464    data.extend_from_slice(&nonce.to_le_bytes());
465    data.extend_from_slice(&index.to_le_bytes());
466
467    let mut accounts = vec![
468        AccountMeta::new(*tree_authority, false),
469        AccountMeta::new_readonly(*leaf_owner, true), // signer
470        AccountMeta::new_readonly(*leaf_delegate, false),
471        AccountMeta::new_readonly(*new_leaf_owner, false),
472        AccountMeta::new(*merkle_tree, false),
473        AccountMeta::new_readonly(*log_wrapper, false),
474        AccountMeta::new_readonly(*compression_program, false),
475        AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
476    ];
477
478    // Append proof accounts
479    for node in proof {
480        accounts.push(AccountMeta::new_readonly(*node, false));
481    }
482
483    Instruction {
484        program_id: BUBBLEGUM_PROGRAM_ID,
485        accounts,
486        data,
487    }
488}
489
490// ═══════════════════════════════════════════════════════════════════
491// Tests
492// ═══════════════════════════════════════════════════════════════════
493
494#[cfg(test)]
495#[allow(clippy::unwrap_used, clippy::expect_used)]
496mod tests {
497    use super::*;
498
499    const MINT: [u8; 32] = [0xAA; 32];
500    const AUTH: [u8; 32] = [0xBB; 32];
501    const PAYER: [u8; 32] = [0xCC; 32];
502    const META_ACCT: [u8; 32] = [0xDD; 32];
503    const EDITION: [u8; 32] = [0xEE; 32];
504
505    fn sample_metadata() -> MetadataData {
506        MetadataData {
507            name: "Test NFT".into(),
508            symbol: "TNFT".into(),
509            uri: "https://example.com/nft.json".into(),
510            seller_fee_basis_points: 500,
511            creators: vec![Creator {
512                address: AUTH,
513                verified: true,
514                share: 100,
515            }],
516        }
517    }
518
519    // ─── Program IDs ────────────────────────────────────────────
520
521    #[test]
522    fn test_metadata_program_id_length() {
523        assert_eq!(METADATA_PROGRAM_ID.len(), 32);
524    }
525
526    #[test]
527    fn test_bubblegum_program_id_length() {
528        assert_eq!(BUBBLEGUM_PROGRAM_ID.len(), 32);
529    }
530
531    #[test]
532    fn test_program_ids_differ() {
533        assert_ne!(METADATA_PROGRAM_ID, BUBBLEGUM_PROGRAM_ID);
534    }
535
536    // ─── CreateMetadataAccountV3 ────────────────────────────────
537
538    #[test]
539    fn test_create_metadata_v3_discriminator() {
540        let ix = create_metadata_account_v3(
541            &META_ACCT,
542            &MINT,
543            &AUTH,
544            &PAYER,
545            &AUTH,
546            &sample_metadata(),
547            true,
548            None,
549        );
550        assert_eq!(ix.data[0], 33);
551    }
552
553    #[test]
554    fn test_create_metadata_v3_program_id() {
555        let ix = create_metadata_account_v3(
556            &META_ACCT,
557            &MINT,
558            &AUTH,
559            &PAYER,
560            &AUTH,
561            &sample_metadata(),
562            true,
563            None,
564        );
565        assert_eq!(ix.program_id, METADATA_PROGRAM_ID);
566    }
567
568    #[test]
569    fn test_create_metadata_v3_accounts() {
570        let ix = create_metadata_account_v3(
571            &META_ACCT,
572            &MINT,
573            &AUTH,
574            &PAYER,
575            &AUTH,
576            &sample_metadata(),
577            true,
578            None,
579        );
580        assert_eq!(ix.accounts.len(), 7);
581        assert_eq!(ix.accounts[0].pubkey, META_ACCT);
582        assert!(ix.accounts[0].is_writable);
583        assert_eq!(ix.accounts[2].pubkey, AUTH);
584        assert!(ix.accounts[2].is_signer); // mint authority
585        assert_eq!(ix.accounts[3].pubkey, PAYER);
586        assert!(ix.accounts[3].is_signer); // payer
587    }
588
589    #[test]
590    fn test_create_metadata_v3_data_encoding() {
591        let md = MetadataData {
592            name: "A".into(),
593            symbol: "B".into(),
594            uri: "C".into(),
595            seller_fee_basis_points: 100,
596            creators: vec![],
597        };
598        let ix =
599            create_metadata_account_v3(&META_ACCT, &MINT, &AUTH, &PAYER, &AUTH, &md, false, None);
600        // [0]: discriminator 33
601        assert_eq!(ix.data[0], 33);
602        // [1..5]: name length (1)
603        let name_len = u32::from_le_bytes(ix.data[1..5].try_into().unwrap());
604        assert_eq!(name_len, 1);
605        assert_eq!(ix.data[5], b'A');
606    }
607
608    #[test]
609    fn test_create_metadata_v3_with_collection() {
610        let col = Collection {
611            verified: false,
612            key: [0xFF; 32],
613        };
614        let ix = create_metadata_account_v3(
615            &META_ACCT,
616            &MINT,
617            &AUTH,
618            &PAYER,
619            &AUTH,
620            &sample_metadata(),
621            true,
622            Some(&col),
623        );
624        // Data should be longer with collection
625        assert!(ix.data.len() > 50);
626    }
627
628    // ─── CreateMasterEditionV3 ──────────────────────────────────
629
630    #[test]
631    fn test_create_master_edition_v3_discriminator() {
632        let ix =
633            create_master_edition_v3(&EDITION, &MINT, &AUTH, &AUTH, &PAYER, &META_ACCT, Some(1));
634        assert_eq!(ix.data[0], 17);
635    }
636
637    #[test]
638    fn test_create_master_edition_v3_max_supply() {
639        let ix =
640            create_master_edition_v3(&EDITION, &MINT, &AUTH, &AUTH, &PAYER, &META_ACCT, Some(100));
641        assert_eq!(ix.data[1], 1); // Some
642        let supply = u64::from_le_bytes(ix.data[2..10].try_into().unwrap());
643        assert_eq!(supply, 100);
644    }
645
646    #[test]
647    fn test_create_master_edition_v3_unlimited() {
648        let ix = create_master_edition_v3(&EDITION, &MINT, &AUTH, &AUTH, &PAYER, &META_ACCT, None);
649        assert_eq!(ix.data[1], 0); // None
650        assert_eq!(ix.data.len(), 2);
651    }
652
653    #[test]
654    fn test_create_master_edition_v3_accounts() {
655        let ix = create_master_edition_v3(&EDITION, &MINT, &AUTH, &AUTH, &PAYER, &META_ACCT, None);
656        assert_eq!(ix.accounts.len(), 9);
657        assert!(ix.accounts[2].is_signer); // update authority
658        assert!(ix.accounts[3].is_signer); // mint authority
659        assert!(ix.accounts[4].is_signer); // payer
660    }
661
662    // ─── VerifyCollection ───────────────────────────────────────
663
664    #[test]
665    fn test_verify_collection_discriminator() {
666        let ix = verify_collection(&META_ACCT, &AUTH, &PAYER, &MINT, &[0x11; 32], &[0x22; 32]);
667        assert_eq!(ix.data, vec![18]);
668    }
669
670    #[test]
671    fn test_verify_collection_accounts() {
672        let ix = verify_collection(&META_ACCT, &AUTH, &PAYER, &MINT, &[0x11; 32], &[0x22; 32]);
673        assert_eq!(ix.accounts.len(), 6);
674        assert!(ix.accounts[1].is_signer); // collection authority
675        assert!(ix.accounts[2].is_signer); // payer
676    }
677
678    // ─── UpdateMetadataAccountV2 ────────────────────────────────
679
680    #[test]
681    fn test_update_metadata_v2_discriminator() {
682        let ix = update_metadata_account_v2(&META_ACCT, &AUTH, None, None, None, None);
683        assert_eq!(ix.data[0], 15);
684    }
685
686    #[test]
687    fn test_update_metadata_v2_no_changes() {
688        let ix = update_metadata_account_v2(&META_ACCT, &AUTH, None, None, None, None);
689        // [15, 0, 0, 0, 0] = discriminator + 4 × None
690        assert_eq!(ix.data, vec![15, 0, 0, 0, 0]);
691    }
692
693    #[test]
694    fn test_update_metadata_v2_with_new_authority() {
695        let new_auth = [0xFF; 32];
696        let ix = update_metadata_account_v2(&META_ACCT, &AUTH, None, Some(&new_auth), None, None);
697        assert_eq!(ix.data[0], 15);
698        assert_eq!(ix.data[1], 0); // no data
699        assert_eq!(ix.data[2], 1); // Some(new_authority)
700        assert_eq!(&ix.data[3..35], &new_auth);
701    }
702
703    #[test]
704    fn test_update_metadata_v2_accounts() {
705        let ix = update_metadata_account_v2(&META_ACCT, &AUTH, None, None, None, None);
706        assert_eq!(ix.accounts.len(), 2);
707        assert!(ix.accounts[1].is_signer);
708    }
709
710    // ─── Bubblegum MintV1 ───────────────────────────────────────
711
712    #[test]
713    fn test_bubblegum_mint_v1_discriminator() {
714        let ix = mint_v1(
715            &[0x01; 32],
716            &[0x02; 32],
717            &[0x03; 32],
718            &[0x04; 32],
719            &PAYER,
720            &AUTH,
721            &[0x05; 32],
722            &[0x06; 32],
723            &sample_metadata(),
724        );
725        assert_eq!(&ix.data[0..8], &[145, 98, 192, 118, 184, 147, 118, 104]);
726    }
727
728    #[test]
729    fn test_bubblegum_mint_v1_program_id() {
730        let ix = mint_v1(
731            &[0x01; 32],
732            &[0x02; 32],
733            &[0x03; 32],
734            &[0x04; 32],
735            &PAYER,
736            &AUTH,
737            &[0x05; 32],
738            &[0x06; 32],
739            &sample_metadata(),
740        );
741        assert_eq!(ix.program_id, BUBBLEGUM_PROGRAM_ID);
742    }
743
744    #[test]
745    fn test_bubblegum_mint_v1_accounts() {
746        let ix = mint_v1(
747            &[0x01; 32],
748            &[0x02; 32],
749            &[0x03; 32],
750            &[0x04; 32],
751            &PAYER,
752            &AUTH,
753            &[0x05; 32],
754            &[0x06; 32],
755            &sample_metadata(),
756        );
757        assert_eq!(ix.accounts.len(), 9);
758        assert!(ix.accounts[4].is_signer); // payer
759        assert!(ix.accounts[5].is_signer); // tree delegate
760    }
761
762    // ─── Bubblegum Transfer ─────────────────────────────────────
763
764    #[test]
765    fn test_bubblegum_transfer_discriminator() {
766        let ix = transfer(
767            &[0x01; 32],
768            &[0x02; 32],
769            &[0x03; 32],
770            &[0x04; 32],
771            &[0x05; 32],
772            &[0x06; 32],
773            &[0x07; 32],
774            &[0xAA; 32],
775            &[0xBB; 32],
776            &[0xCC; 32],
777            0,
778            0,
779            &[],
780        );
781        assert_eq!(&ix.data[0..8], &[163, 52, 200, 231, 140, 3, 69, 186]);
782    }
783
784    #[test]
785    fn test_bubblegum_transfer_data_encoding() {
786        let root = [0xAA; 32];
787        let data_hash = [0xBB; 32];
788        let creator_hash = [0xCC; 32];
789        let ix = transfer(
790            &[0x01; 32],
791            &[0x02; 32],
792            &[0x03; 32],
793            &[0x04; 32],
794            &[0x05; 32],
795            &[0x06; 32],
796            &[0x07; 32],
797            &root,
798            &data_hash,
799            &creator_hash,
800            42,
801            7,
802            &[],
803        );
804        assert_eq!(&ix.data[8..40], &root);
805        assert_eq!(&ix.data[40..72], &data_hash);
806        assert_eq!(&ix.data[72..104], &creator_hash);
807        let nonce = u64::from_le_bytes(ix.data[104..112].try_into().unwrap());
808        assert_eq!(nonce, 42);
809        let idx = u32::from_le_bytes(ix.data[112..116].try_into().unwrap());
810        assert_eq!(idx, 7);
811    }
812
813    #[test]
814    fn test_bubblegum_transfer_with_proof() {
815        let proof = vec![[0x11; 32], [0x22; 32], [0x33; 32]];
816        let ix = transfer(
817            &[0x01; 32],
818            &[0x02; 32],
819            &[0x03; 32],
820            &[0x04; 32],
821            &[0x05; 32],
822            &[0x06; 32],
823            &[0x07; 32],
824            &[0xAA; 32],
825            &[0xBB; 32],
826            &[0xCC; 32],
827            0,
828            0,
829            &proof,
830        );
831        assert_eq!(ix.accounts.len(), 8 + 3); // base + proof
832    }
833
834    #[test]
835    fn test_bubblegum_transfer_leaf_owner_is_signer() {
836        let ix = transfer(
837            &[0x01; 32],
838            &[0x02; 32],
839            &[0x03; 32],
840            &[0x04; 32],
841            &[0x05; 32],
842            &[0x06; 32],
843            &[0x07; 32],
844            &[0xAA; 32],
845            &[0xBB; 32],
846            &[0xCC; 32],
847            0,
848            0,
849            &[],
850        );
851        assert_eq!(ix.accounts[1].pubkey, [0x02; 32]);
852        assert!(ix.accounts[1].is_signer); // leaf owner must sign
853    }
854
855    // ─── Borsh Encoding ─────────────────────────────────────────
856
857    #[test]
858    fn test_borsh_string() {
859        let mut buf = Vec::new();
860        borsh_string(&mut buf, "Hello");
861        assert_eq!(buf.len(), 4 + 5);
862        let len = u32::from_le_bytes(buf[0..4].try_into().unwrap());
863        assert_eq!(len, 5);
864        assert_eq!(&buf[4..], b"Hello");
865    }
866
867    #[test]
868    fn test_borsh_string_empty() {
869        let mut buf = Vec::new();
870        borsh_string(&mut buf, "");
871        assert_eq!(buf.len(), 4);
872        assert_eq!(u32::from_le_bytes(buf[..4].try_into().unwrap()), 0);
873    }
874
875    #[test]
876    fn test_borsh_creators() {
877        let mut buf = Vec::new();
878        borsh_creators(
879            &mut buf,
880            &[Creator {
881                address: [0xFF; 32],
882                verified: true,
883                share: 50,
884            }],
885        );
886        assert_eq!(buf[0], 1); // Some
887        let count = u32::from_le_bytes(buf[1..5].try_into().unwrap());
888        assert_eq!(count, 1);
889        assert_eq!(&buf[5..37], &[0xFF; 32]);
890        assert_eq!(buf[37], 1); // verified
891        assert_eq!(buf[38], 50); // share
892    }
893
894    #[test]
895    fn test_borsh_collection_some() {
896        let mut buf = Vec::new();
897        let col = Collection {
898            verified: true,
899            key: [0xAA; 32],
900        };
901        borsh_collection(&mut buf, Some(&col));
902        assert_eq!(buf[0], 1); // Some
903        assert_eq!(buf[1], 1); // verified
904        assert_eq!(&buf[2..34], &[0xAA; 32]);
905    }
906
907    #[test]
908    fn test_borsh_collection_none() {
909        let mut buf = Vec::new();
910        borsh_collection(&mut buf, None);
911        assert_eq!(buf, vec![0]); // None
912    }
913
914    #[test]
915    fn test_use_method_values() {
916        assert_eq!(UseMethod::Burn as u8, 0);
917        assert_eq!(UseMethod::Multiple as u8, 1);
918        assert_eq!(UseMethod::Single as u8, 2);
919    }
920}