Skip to main content

chains_sdk/solana/
transaction.rs

1//! Solana transaction building, signing, and program interaction helpers.
2//!
3//! Implements the Solana transaction wire format including:
4//! - Compact-u16 encoding
5//! - Instructions and message serialization (legacy + v0 versioned)
6//! - System Program helpers (transfer, create_account, allocate)
7//! - SPL Token helpers (transfer, approve, mint_to)
8//! - Compute Budget (priority fees)
9//! - Address Lookup Table references (versioned transactions)
10//!
11//! # Example
12//! ```no_run
13//! use chains_sdk::solana::transaction::*;
14//! use chains_sdk::solana::SolanaSigner;
15//! use chains_sdk::traits::KeyPair;
16//!
17//! fn example() -> Result<(), Box<dyn std::error::Error>> {
18//!     let signer = SolanaSigner::generate()?;
19//!     let to = [0xBB; 32];
20//!     let ix = system_program::transfer(&signer.public_key_bytes_32(), &to, 1_000_000);
21//!     let msg = Message::new(&[ix], signer.public_key_bytes_32());
22//!     let tx = Transaction::sign(&msg, &[&signer], [0u8; 32])?;
23//!     let raw = tx.serialize();
24//!     Ok(())
25//! }
26//! ```
27
28use super::SolanaSigner;
29use crate::error::SignerError;
30use ed25519_dalek::Signer as DalekSigner;
31
32// ─── Compact-u16 Encoding ──────────────────────────────────────────
33
34/// Encode a `u16` as a Solana compact-u16 (variable-length encoding).
35///
36/// Used throughout Solana wire format for lengths.
37#[must_use]
38pub fn encode_compact_u16(val: u16) -> Vec<u8> {
39    if val < 0x80 {
40        vec![val as u8]
41    } else if val < 0x4000 {
42        vec![(val & 0x7F | 0x80) as u8, (val >> 7) as u8]
43    } else {
44        vec![
45            (val & 0x7F | 0x80) as u8,
46            ((val >> 7) & 0x7F | 0x80) as u8,
47            (val >> 14) as u8,
48        ]
49    }
50}
51
52/// Decode a compact-u16 from bytes. Returns (value, bytes_consumed).
53pub fn decode_compact_u16(data: &[u8]) -> Result<(u16, usize), SignerError> {
54    if data.is_empty() {
55        return Err(SignerError::ParseError("compact-u16: empty".into()));
56    }
57    let b0 = data[0] as u16;
58    if b0 < 0x80 {
59        return Ok((b0, 1));
60    }
61    if data.len() < 2 {
62        return Err(SignerError::ParseError("compact-u16: truncated".into()));
63    }
64    let b1 = data[1] as u16;
65    if b1 < 0x80 {
66        return Ok(((b0 & 0x7F) | (b1 << 7), 2));
67    }
68    if data.len() < 3 {
69        return Err(SignerError::ParseError("compact-u16: truncated".into()));
70    }
71    let b2 = data[2] as u16;
72    Ok(((b0 & 0x7F) | ((b1 & 0x7F) << 7) | (b2 << 14), 3))
73}
74
75// ─── Account Meta ──────────────────────────────────────────────────
76
77/// An account reference in a Solana instruction.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct AccountMeta {
80    /// 32-byte public key.
81    pub pubkey: [u8; 32],
82    /// Whether this account is a signer.
83    pub is_signer: bool,
84    /// Whether this account is writable.
85    pub is_writable: bool,
86}
87
88impl AccountMeta {
89    /// Create a writable signer account.
90    #[must_use]
91    pub fn new(pubkey: [u8; 32], is_signer: bool) -> Self {
92        Self {
93            pubkey,
94            is_signer,
95            is_writable: true,
96        }
97    }
98
99    /// Create a read-only account.
100    #[must_use]
101    pub fn new_readonly(pubkey: [u8; 32], is_signer: bool) -> Self {
102        Self {
103            pubkey,
104            is_signer,
105            is_writable: false,
106        }
107    }
108}
109
110// ─── Instruction ───────────────────────────────────────────────────
111
112/// A Solana instruction.
113#[derive(Debug, Clone)]
114pub struct Instruction {
115    /// Program ID (32 bytes).
116    pub program_id: [u8; 32],
117    /// Account references.
118    pub accounts: Vec<AccountMeta>,
119    /// Instruction data.
120    pub data: Vec<u8>,
121}
122
123// ─── Message ───────────────────────────────────────────────────────
124
125/// A Solana transaction message (legacy format).
126#[derive(Debug, Clone)]
127pub struct Message {
128    /// Number of required signatures.
129    pub num_required_signatures: u8,
130    /// Number of read-only signed accounts.
131    pub num_readonly_signed_accounts: u8,
132    /// Number of read-only unsigned accounts.
133    pub num_readonly_unsigned_accounts: u8,
134    /// All account keys referenced by the message.
135    pub account_keys: Vec<[u8; 32]>,
136    /// Recent blockhash (32 bytes).
137    pub recent_blockhash: [u8; 32],
138    /// Compiled instructions.
139    pub instructions: Vec<CompiledInstruction>,
140}
141
142/// A compiled instruction (indices into account_keys array).
143#[derive(Debug, Clone)]
144pub struct CompiledInstruction {
145    /// Index of the program ID in account_keys.
146    pub program_id_index: u8,
147    /// Indices of accounts in account_keys.
148    pub accounts: Vec<u8>,
149    /// Instruction data.
150    pub data: Vec<u8>,
151}
152
153impl Message {
154    /// Build a message from instructions and a fee payer.
155    ///
156    /// Deduplicates accounts and sorts them per Solana's rules:
157    /// 1. Writable signers (fee payer first)
158    /// 2. Read-only signers
159    /// 3. Writable non-signers
160    /// 4. Read-only non-signers
161    #[must_use]
162    pub fn new(instructions: &[Instruction], fee_payer: [u8; 32]) -> Self {
163        let mut writable_signers: Vec<[u8; 32]> = vec![fee_payer];
164        let mut readonly_signers: Vec<[u8; 32]> = Vec::new();
165        let mut writable_nonsigners: Vec<[u8; 32]> = Vec::new();
166        let mut readonly_nonsigners: Vec<[u8; 32]> = Vec::new();
167
168        for ix in instructions {
169            for acc in &ix.accounts {
170                // Skip if already fee payer
171                if acc.pubkey == fee_payer {
172                    continue;
173                }
174                match (acc.is_signer, acc.is_writable) {
175                    (true, true) => {
176                        if !writable_signers.contains(&acc.pubkey) {
177                            writable_signers.push(acc.pubkey);
178                        }
179                    }
180                    (true, false) => {
181                        if !readonly_signers.contains(&acc.pubkey) {
182                            readonly_signers.push(acc.pubkey);
183                        }
184                    }
185                    (false, true) => {
186                        if !writable_nonsigners.contains(&acc.pubkey) {
187                            writable_nonsigners.push(acc.pubkey);
188                        }
189                    }
190                    (false, false) => {
191                        if !readonly_nonsigners.contains(&acc.pubkey) {
192                            readonly_nonsigners.push(acc.pubkey);
193                        }
194                    }
195                }
196            }
197            // Add program IDs as read-only non-signers
198            if !writable_signers.contains(&ix.program_id)
199                && !readonly_signers.contains(&ix.program_id)
200                && !writable_nonsigners.contains(&ix.program_id)
201                && !readonly_nonsigners.contains(&ix.program_id)
202            {
203                readonly_nonsigners.push(ix.program_id);
204            }
205        }
206
207        let num_required_signatures = (writable_signers.len() + readonly_signers.len()) as u8;
208        let num_readonly_signed = readonly_signers.len() as u8;
209        let num_readonly_unsigned = readonly_nonsigners.len() as u8;
210
211        let mut account_keys = Vec::new();
212        account_keys.extend_from_slice(&writable_signers);
213        account_keys.extend_from_slice(&readonly_signers);
214        account_keys.extend_from_slice(&writable_nonsigners);
215        account_keys.extend_from_slice(&readonly_nonsigners);
216
217        // Compile instructions
218        let compiled = instructions
219            .iter()
220            .map(|ix| {
221                let program_id_index = account_keys
222                    .iter()
223                    .position(|k| *k == ix.program_id)
224                    .unwrap_or(0) as u8;
225                let accounts: Vec<u8> = ix
226                    .accounts
227                    .iter()
228                    .map(|a| {
229                        account_keys
230                            .iter()
231                            .position(|k| *k == a.pubkey)
232                            .unwrap_or(0) as u8
233                    })
234                    .collect();
235                CompiledInstruction {
236                    program_id_index,
237                    accounts,
238                    data: ix.data.clone(),
239                }
240            })
241            .collect();
242
243        Self {
244            num_required_signatures,
245            num_readonly_signed_accounts: num_readonly_signed,
246            num_readonly_unsigned_accounts: num_readonly_unsigned,
247            account_keys,
248            recent_blockhash: [0u8; 32], // set later
249            instructions: compiled,
250        }
251    }
252
253    /// Serialize the message to bytes for signing.
254    #[must_use]
255    pub fn serialize(&self) -> Vec<u8> {
256        let mut buf = Vec::new();
257        buf.push(self.num_required_signatures);
258        buf.push(self.num_readonly_signed_accounts);
259        buf.push(self.num_readonly_unsigned_accounts);
260
261        buf.extend_from_slice(&encode_compact_u16(self.account_keys.len() as u16));
262        for key in &self.account_keys {
263            buf.extend_from_slice(key);
264        }
265
266        buf.extend_from_slice(&self.recent_blockhash);
267
268        buf.extend_from_slice(&encode_compact_u16(self.instructions.len() as u16));
269        for ix in &self.instructions {
270            buf.push(ix.program_id_index);
271            buf.extend_from_slice(&encode_compact_u16(ix.accounts.len() as u16));
272            buf.extend_from_slice(&ix.accounts);
273            buf.extend_from_slice(&encode_compact_u16(ix.data.len() as u16));
274            buf.extend_from_slice(&ix.data);
275        }
276        buf
277    }
278}
279
280// ─── Transaction ───────────────────────────────────────────────────
281
282/// A signed Solana transaction.
283#[derive(Debug, Clone)]
284pub struct Transaction {
285    /// Ed25519 signatures (64 bytes each).
286    pub signatures: Vec<[u8; 64]>,
287    /// The message that was signed.
288    pub message: Message,
289}
290
291impl Transaction {
292    /// Sign a message with one or more signers.
293    pub fn sign(
294        message: &Message,
295        signers: &[&SolanaSigner],
296        recent_blockhash: [u8; 32],
297    ) -> Result<Self, SignerError> {
298        let mut msg = message.clone();
299        msg.recent_blockhash = recent_blockhash;
300        let serialized = msg.serialize();
301
302        let mut signatures = Vec::new();
303        for signer in signers {
304            let sig = signer.signing_key.sign(&serialized);
305            signatures.push(sig.to_bytes());
306        }
307
308        Ok(Self {
309            signatures,
310            message: msg,
311        })
312    }
313
314    /// Serialize the transaction for sending via `sendTransaction` RPC.
315    #[must_use]
316    pub fn serialize(&self) -> Vec<u8> {
317        let mut buf = Vec::new();
318        buf.extend_from_slice(&encode_compact_u16(self.signatures.len() as u16));
319        for sig in &self.signatures {
320            buf.extend_from_slice(sig);
321        }
322        buf.extend_from_slice(&self.message.serialize());
323        buf
324    }
325}
326
327// ═══════════════════════════════════════════════════════════════════
328// System Program
329// ═══════════════════════════════════════════════════════════════════
330
331/// Solana System Program helpers.
332pub mod system_program {
333    use super::*;
334
335    /// System Program ID: `11111111111111111111111111111111`
336    pub const ID: [u8; 32] = [0; 32];
337
338    /// Create a SOL transfer instruction.
339    ///
340    /// # Arguments
341    /// - `from` — Sender pubkey (must be signer)
342    /// - `to` — Recipient pubkey
343    /// - `lamports` — Amount in lamports (1 SOL = 1_000_000_000 lamports)
344    #[must_use]
345    pub fn transfer(from: &[u8; 32], to: &[u8; 32], lamports: u64) -> Instruction {
346        let mut data = vec![2, 0, 0, 0]; // Transfer instruction index
347        data.extend_from_slice(&lamports.to_le_bytes());
348        Instruction {
349            program_id: ID,
350            accounts: vec![AccountMeta::new(*from, true), AccountMeta::new(*to, false)],
351            data,
352        }
353    }
354
355    /// Create a `CreateAccount` instruction.
356    #[must_use]
357    pub fn create_account(
358        from: &[u8; 32],
359        new_account: &[u8; 32],
360        lamports: u64,
361        space: u64,
362        owner: &[u8; 32],
363    ) -> Instruction {
364        let mut data = vec![0, 0, 0, 0]; // CreateAccount instruction index
365        data.extend_from_slice(&lamports.to_le_bytes());
366        data.extend_from_slice(&space.to_le_bytes());
367        data.extend_from_slice(owner);
368        Instruction {
369            program_id: ID,
370            accounts: vec![
371                AccountMeta::new(*from, true),
372                AccountMeta::new(*new_account, true),
373            ],
374            data,
375        }
376    }
377
378    /// Create an `Allocate` instruction.
379    #[must_use]
380    pub fn allocate(account: &[u8; 32], space: u64) -> Instruction {
381        let mut data = vec![8, 0, 0, 0]; // Allocate instruction index
382        data.extend_from_slice(&space.to_le_bytes());
383        Instruction {
384            program_id: ID,
385            accounts: vec![AccountMeta::new(*account, true)],
386            data,
387        }
388    }
389}
390
391// ═══════════════════════════════════════════════════════════════════
392// SPL Token Program
393// ═══════════════════════════════════════════════════════════════════
394
395/// SPL Token Program helpers.
396pub mod spl_token {
397    use super::*;
398
399    /// SPL Token Program ID: `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA`
400    pub const ID: [u8; 32] = [
401        0x06, 0xDD, 0xF6, 0xE1, 0xD7, 0x65, 0xA1, 0x93, 0xD9, 0xCB, 0xE1, 0x46, 0xCE, 0xEB, 0x79,
402        0xAC, 0x1C, 0xB4, 0x85, 0xED, 0x5F, 0x5B, 0x37, 0x91, 0x3A, 0x8C, 0xF5, 0x85, 0x7E, 0xFF,
403        0x00, 0xA9,
404    ];
405
406    /// Create an SPL Token `Transfer` instruction.
407    ///
408    /// # Arguments
409    /// - `source` — Source token account
410    /// - `destination` — Destination token account
411    /// - `authority` — Owner of the source account (signer)
412    /// - `amount` — Token amount (in smallest unit)
413    #[must_use]
414    pub fn transfer(
415        source: &[u8; 32],
416        destination: &[u8; 32],
417        authority: &[u8; 32],
418        amount: u64,
419    ) -> Instruction {
420        let mut data = vec![3]; // Transfer instruction discriminator
421        data.extend_from_slice(&amount.to_le_bytes());
422        Instruction {
423            program_id: ID,
424            accounts: vec![
425                AccountMeta::new(*source, false),
426                AccountMeta::new(*destination, false),
427                AccountMeta::new_readonly(*authority, true),
428            ],
429            data,
430        }
431    }
432
433    /// Create an SPL Token `Approve` instruction.
434    #[must_use]
435    pub fn approve(
436        source: &[u8; 32],
437        delegate: &[u8; 32],
438        authority: &[u8; 32],
439        amount: u64,
440    ) -> Instruction {
441        let mut data = vec![4]; // Approve instruction discriminator
442        data.extend_from_slice(&amount.to_le_bytes());
443        Instruction {
444            program_id: ID,
445            accounts: vec![
446                AccountMeta::new(*source, false),
447                AccountMeta::new_readonly(*delegate, false),
448                AccountMeta::new_readonly(*authority, true),
449            ],
450            data,
451        }
452    }
453
454    /// Create an SPL Token `MintTo` instruction.
455    #[must_use]
456    pub fn mint_to(
457        mint: &[u8; 32],
458        destination: &[u8; 32],
459        authority: &[u8; 32],
460        amount: u64,
461    ) -> Instruction {
462        let mut data = vec![7]; // MintTo instruction discriminator
463        data.extend_from_slice(&amount.to_le_bytes());
464        Instruction {
465            program_id: ID,
466            accounts: vec![
467                AccountMeta::new(*mint, false),
468                AccountMeta::new(*destination, false),
469                AccountMeta::new_readonly(*authority, true),
470            ],
471            data,
472        }
473    }
474
475    /// Create an SPL Token `Burn` instruction.
476    #[must_use]
477    pub fn burn(
478        token_account: &[u8; 32],
479        mint: &[u8; 32],
480        authority: &[u8; 32],
481        amount: u64,
482    ) -> Instruction {
483        let mut data = vec![8]; // Burn
484        data.extend_from_slice(&amount.to_le_bytes());
485        Instruction {
486            program_id: ID,
487            accounts: vec![
488                AccountMeta::new(*token_account, false),
489                AccountMeta::new(*mint, false),
490                AccountMeta::new_readonly(*authority, true),
491            ],
492            data,
493        }
494    }
495
496    /// Create an SPL Token `CloseAccount` instruction.
497    #[must_use]
498    pub fn close_account(
499        account: &[u8; 32],
500        destination: &[u8; 32],
501        authority: &[u8; 32],
502    ) -> Instruction {
503        Instruction {
504            program_id: ID,
505            accounts: vec![
506                AccountMeta::new(*account, false),
507                AccountMeta::new(*destination, false),
508                AccountMeta::new_readonly(*authority, true),
509            ],
510            data: vec![9], // CloseAccount
511        }
512    }
513
514    /// Create an SPL Token `FreezeAccount` instruction.
515    #[must_use]
516    pub fn freeze_account(
517        account: &[u8; 32],
518        mint: &[u8; 32],
519        freeze_authority: &[u8; 32],
520    ) -> Instruction {
521        Instruction {
522            program_id: ID,
523            accounts: vec![
524                AccountMeta::new(*account, false),
525                AccountMeta::new_readonly(*mint, false),
526                AccountMeta::new_readonly(*freeze_authority, true),
527            ],
528            data: vec![10], // FreezeAccount
529        }
530    }
531
532    /// Create an SPL Token `ThawAccount` instruction.
533    #[must_use]
534    pub fn thaw_account(
535        account: &[u8; 32],
536        mint: &[u8; 32],
537        freeze_authority: &[u8; 32],
538    ) -> Instruction {
539        Instruction {
540            program_id: ID,
541            accounts: vec![
542                AccountMeta::new(*account, false),
543                AccountMeta::new_readonly(*mint, false),
544                AccountMeta::new_readonly(*freeze_authority, true),
545            ],
546            data: vec![11], // ThawAccount
547        }
548    }
549
550    /// Create an SPL Token `InitializeMint` instruction.
551    ///
552    /// # Arguments
553    /// - `mint` — The mint account to initialize
554    /// - `decimals` — Number of decimals for the token
555    /// - `mint_authority` — Authority that can mint new tokens
556    /// - `freeze_authority` — Optional authority that can freeze accounts
557    #[must_use]
558    pub fn initialize_mint(
559        mint: &[u8; 32],
560        decimals: u8,
561        mint_authority: &[u8; 32],
562        freeze_authority: Option<&[u8; 32]>,
563    ) -> Instruction {
564        let rent_sysvar: [u8; 32] = {
565            let mut id = [0u8; 32];
566            // SysvarRent111111111111111111111111111111111
567            id[0] = 0x06;
568            id[1] = 0xa7;
569            id[2] = 0xd5;
570            id[3] = 0x17;
571            id[4] = 0x19;
572            id[5] = 0x2c;
573            id
574        };
575        let mut data = vec![0]; // InitializeMint
576        data.push(decimals);
577        data.extend_from_slice(mint_authority);
578        match freeze_authority {
579            Some(auth) => {
580                data.push(1); // COption::Some
581                data.extend_from_slice(auth);
582            }
583            None => {
584                data.push(0); // COption::None
585                data.extend_from_slice(&[0u8; 32]);
586            }
587        }
588        Instruction {
589            program_id: ID,
590            accounts: vec![
591                AccountMeta::new(*mint, false),
592                AccountMeta::new_readonly(rent_sysvar, false),
593            ],
594            data,
595        }
596    }
597
598    /// Create an SPL Token `InitializeAccount` instruction.
599    #[must_use]
600    pub fn initialize_account(
601        account: &[u8; 32],
602        mint: &[u8; 32],
603        owner: &[u8; 32],
604    ) -> Instruction {
605        let rent_sysvar: [u8; 32] = {
606            let mut id = [0u8; 32];
607            id[0] = 0x06;
608            id[1] = 0xa7;
609            id[2] = 0xd5;
610            id[3] = 0x17;
611            id[4] = 0x19;
612            id[5] = 0x2c;
613            id
614        };
615        Instruction {
616            program_id: ID,
617            accounts: vec![
618                AccountMeta::new(*account, false),
619                AccountMeta::new_readonly(*mint, false),
620                AccountMeta::new_readonly(*owner, false),
621                AccountMeta::new_readonly(rent_sysvar, false),
622            ],
623            data: vec![1], // InitializeAccount
624        }
625    }
626
627    /// SPL Token authority types for `SetAuthority`.
628    #[derive(Debug, Clone, Copy)]
629    pub enum AuthorityType {
630        /// Authority to mint new tokens.
631        MintTokens = 0,
632        /// Authority to freeze accounts.
633        FreezeAccount = 1,
634        /// Owner of a token account.
635        AccountOwner = 2,
636        /// Authority to close a token account.
637        CloseAccount = 3,
638    }
639
640    /// Create an SPL Token `SetAuthority` instruction.
641    #[must_use]
642    pub fn set_authority(
643        account_or_mint: &[u8; 32],
644        current_authority: &[u8; 32],
645        authority_type: AuthorityType,
646        new_authority: Option<&[u8; 32]>,
647    ) -> Instruction {
648        let mut data = vec![6]; // SetAuthority
649        data.push(authority_type as u8);
650        match new_authority {
651            Some(auth) => {
652                data.push(1); // COption::Some
653                data.extend_from_slice(auth);
654            }
655            None => {
656                data.push(0); // COption::None
657                data.extend_from_slice(&[0u8; 32]);
658            }
659        }
660        Instruction {
661            program_id: ID,
662            accounts: vec![
663                AccountMeta::new(*account_or_mint, false),
664                AccountMeta::new_readonly(*current_authority, true),
665            ],
666            data,
667        }
668    }
669
670    /// Create an SPL Token `Revoke` instruction (revoke delegate).
671    #[must_use]
672    pub fn revoke(source: &[u8; 32], authority: &[u8; 32]) -> Instruction {
673        Instruction {
674            program_id: ID,
675            accounts: vec![
676                AccountMeta::new(*source, false),
677                AccountMeta::new_readonly(*authority, true),
678            ],
679            data: vec![5], // Revoke
680        }
681    }
682
683    /// Create an SPL Token `TransferChecked` instruction.
684    ///
685    /// Like `transfer` but also verifies the token's decimals and mint.
686    #[must_use]
687    pub fn transfer_checked(
688        source: &[u8; 32],
689        mint: &[u8; 32],
690        destination: &[u8; 32],
691        authority: &[u8; 32],
692        amount: u64,
693        decimals: u8,
694    ) -> Instruction {
695        let mut data = vec![12]; // TransferChecked
696        data.extend_from_slice(&amount.to_le_bytes());
697        data.push(decimals);
698        Instruction {
699            program_id: ID,
700            accounts: vec![
701                AccountMeta::new(*source, false),
702                AccountMeta::new_readonly(*mint, false),
703                AccountMeta::new(*destination, false),
704                AccountMeta::new_readonly(*authority, true),
705            ],
706            data,
707        }
708    }
709}
710
711// ═══════════════════════════════════════════════════════════════════
712// SPL Token-2022 Program
713// ═══════════════════════════════════════════════════════════════════
714
715/// SPL Token-2022 (Token Extensions) program helpers.
716pub mod spl_token_2022 {
717    use super::*;
718
719    /// Token-2022 Program ID: `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb`
720    pub const ID: [u8; 32] = [
721        0x06, 0xDD, 0xF6, 0xE1, 0xD7, 0x65, 0xA1, 0x93, 0xD9, 0xCB, 0xE1, 0x46, 0xCE, 0xEB, 0x79,
722        0xAC, 0x1C, 0xB4, 0x85, 0xED, 0x5F, 0x5B, 0x37, 0x91, 0x3A, 0x8C, 0xF5, 0x85, 0x7E, 0xFF,
723        0x00, 0xAA, // last byte differs from Token
724    ];
725
726    /// Create a Token-2022 `TransferChecked` instruction.
727    #[must_use]
728    pub fn transfer_checked(
729        source: &[u8; 32],
730        mint: &[u8; 32],
731        destination: &[u8; 32],
732        authority: &[u8; 32],
733        amount: u64,
734        decimals: u8,
735    ) -> Instruction {
736        let mut data = vec![12]; // TransferChecked
737        data.extend_from_slice(&amount.to_le_bytes());
738        data.push(decimals);
739        Instruction {
740            program_id: ID,
741            accounts: vec![
742                AccountMeta::new(*source, false),
743                AccountMeta::new_readonly(*mint, false),
744                AccountMeta::new(*destination, false),
745                AccountMeta::new_readonly(*authority, true),
746            ],
747            data,
748        }
749    }
750
751    /// Create a Token-2022 `TransferCheckedWithFee` instruction.
752    ///
753    /// Used when the mint has a TransferFeeConfig extension.
754    #[must_use]
755    pub fn transfer_checked_with_fee(
756        source: &[u8; 32],
757        mint: &[u8; 32],
758        destination: &[u8; 32],
759        authority: &[u8; 32],
760        amount: u64,
761        decimals: u8,
762        fee: u64,
763    ) -> Instruction {
764        let mut data = vec![26]; // TransferCheckedWithFee
765        data.extend_from_slice(&amount.to_le_bytes());
766        data.push(decimals);
767        data.extend_from_slice(&fee.to_le_bytes());
768        Instruction {
769            program_id: ID,
770            accounts: vec![
771                AccountMeta::new(*source, false),
772                AccountMeta::new_readonly(*mint, false),
773                AccountMeta::new(*destination, false),
774                AccountMeta::new_readonly(*authority, true),
775            ],
776            data,
777        }
778    }
779}
780
781// ═══════════════════════════════════════════════════════════════════
782// Compute Budget (Priority Fees)
783// ═══════════════════════════════════════════════════════════════════
784
785/// Compute Budget program helpers for priority fees.
786pub mod compute_budget {
787    use super::*;
788
789    /// Compute Budget Program ID.
790    pub const ID: [u8; 32] = [
791        0x03, 0x06, 0x46, 0x6F, 0xE5, 0x21, 0x17, 0x32, 0xFF, 0xEC, 0xAD, 0xBA, 0x72, 0xC3, 0x9B,
792        0xE7, 0xBC, 0x8C, 0xE5, 0xBB, 0xC5, 0xF7, 0x12, 0x6B, 0x2C, 0x43, 0x9B, 0x3A, 0x40, 0x00,
793        0x00, 0x00,
794    ];
795
796    /// Set the compute unit limit.
797    #[must_use]
798    pub fn set_compute_unit_limit(units: u32) -> Instruction {
799        let mut data = vec![2]; // SetComputeUnitLimit
800        data.extend_from_slice(&units.to_le_bytes());
801        Instruction {
802            program_id: ID,
803            accounts: vec![],
804            data,
805        }
806    }
807
808    /// Set the compute unit price (priority fee in micro-lamports).
809    #[must_use]
810    pub fn set_compute_unit_price(micro_lamports: u64) -> Instruction {
811        let mut data = vec![3]; // SetComputeUnitPrice
812        data.extend_from_slice(&micro_lamports.to_le_bytes());
813        Instruction {
814            program_id: ID,
815            accounts: vec![],
816            data,
817        }
818    }
819}
820
821// ═══════════════════════════════════════════════════════════════════
822// Versioned Transactions (v0)
823// ═══════════════════════════════════════════════════════════════════
824
825/// Address lookup table reference for versioned transactions.
826#[derive(Debug, Clone)]
827pub struct AddressLookupTable {
828    /// Account key of the lookup table.
829    pub account_key: [u8; 32],
830    /// Indices of writable accounts in the table.
831    pub writable_indexes: Vec<u8>,
832    /// Indices of read-only accounts in the table.
833    pub readonly_indexes: Vec<u8>,
834}
835
836/// A versioned transaction message (v0).
837#[derive(Debug, Clone)]
838pub struct MessageV0 {
839    /// The legacy message portion.
840    pub message: Message,
841    /// Address lookup table references.
842    pub address_table_lookups: Vec<AddressLookupTable>,
843}
844
845impl MessageV0 {
846    /// Serialize a v0 message.
847    ///
848    /// Format: `0x80 || legacy_message_bytes || address_table_lookups`
849    #[must_use]
850    pub fn serialize(&self) -> Vec<u8> {
851        let mut buf = Vec::new();
852        buf.push(0x80); // Version prefix (v0 = 0x80)
853        buf.extend_from_slice(&self.message.serialize());
854
855        // Serialize address table lookups
856        buf.extend_from_slice(&encode_compact_u16(self.address_table_lookups.len() as u16));
857        for table in &self.address_table_lookups {
858            buf.extend_from_slice(&table.account_key);
859            buf.extend_from_slice(&encode_compact_u16(table.writable_indexes.len() as u16));
860            buf.extend_from_slice(&table.writable_indexes);
861            buf.extend_from_slice(&encode_compact_u16(table.readonly_indexes.len() as u16));
862            buf.extend_from_slice(&table.readonly_indexes);
863        }
864        buf
865    }
866}
867
868/// A signed versioned transaction (v0).
869#[derive(Debug, Clone)]
870pub struct VersionedTransaction {
871    /// Ed25519 signatures.
872    pub signatures: Vec<[u8; 64]>,
873    /// The v0 message.
874    pub message: MessageV0,
875}
876
877impl VersionedTransaction {
878    /// Sign a v0 message with one or more signers.
879    pub fn sign(
880        message: &MessageV0,
881        signers: &[&SolanaSigner],
882        recent_blockhash: [u8; 32],
883    ) -> Result<Self, SignerError> {
884        let mut msg = message.clone();
885        msg.message.recent_blockhash = recent_blockhash;
886        let serialized = msg.serialize();
887
888        let mut signatures = Vec::new();
889        for signer in signers {
890            let sig = signer.signing_key.sign(&serialized);
891            signatures.push(sig.to_bytes());
892        }
893
894        Ok(Self {
895            signatures,
896            message: msg,
897        })
898    }
899
900    /// Serialize the versioned transaction for sending.
901    #[must_use]
902    pub fn serialize(&self) -> Vec<u8> {
903        let mut buf = Vec::new();
904        buf.extend_from_slice(&encode_compact_u16(self.signatures.len() as u16));
905        for sig in &self.signatures {
906            buf.extend_from_slice(sig);
907        }
908        buf.extend_from_slice(&self.message.serialize());
909        buf
910    }
911}
912
913// ═══════════════════════════════════════════════════════════════════
914// Program Derived Addresses (PDA)
915// ═══════════════════════════════════════════════════════════════════
916
917/// Find a Program Derived Address (PDA) for the given seeds and program ID.
918///
919/// Iterates bump seeds from 255 down to 0, returning the first valid
920/// off-curve address along with its bump seed.
921///
922/// # Returns
923/// `(address, bump)` — the 32-byte PDA and the bump seed used.
924///
925/// # Example
926/// ```
927/// use chains_sdk::solana::transaction::find_program_address;
928///
929/// let program_id = [0xAA; 32];
930/// let (pda, bump) = find_program_address(&[b"vault", &[1u8; 32]], &program_id).unwrap();
931/// assert_eq!(pda.len(), 32);
932/// assert!(bump <= 255);
933/// ```
934pub fn find_program_address(
935    seeds: &[&[u8]],
936    program_id: &[u8; 32],
937) -> Result<([u8; 32], u8), SignerError> {
938    for bump in (0..=255u8).rev() {
939        if let Ok(addr) = create_program_address(seeds, &[bump], program_id) {
940            return Ok((addr, bump));
941        }
942    }
943    Err(SignerError::SigningFailed(
944        "PDA: no valid bump found".into(),
945    ))
946}
947
948/// Create a Program Derived Address from seeds, a bump, and a program ID.
949///
950/// Returns `Err` if the resulting point is on the Ed25519 curve (not a valid PDA).
951pub fn create_program_address(
952    seeds: &[&[u8]],
953    bump: &[u8],
954    program_id: &[u8; 32],
955) -> Result<[u8; 32], SignerError> {
956    use sha2::{Digest, Sha256};
957
958    let mut hasher = Sha256::new();
959    for seed in seeds {
960        if seed.len() > 32 {
961            return Err(SignerError::SigningFailed("PDA seed > 32 bytes".into()));
962        }
963        hasher.update(seed);
964    }
965    hasher.update(bump);
966    hasher.update(program_id);
967    hasher.update(b"ProgramDerivedAddress");
968    let hash = hasher.finalize();
969
970    let mut candidate = [0u8; 32];
971    candidate.copy_from_slice(&hash);
972
973    // Check that the resulting point is NOT on the Ed25519 curve.
974    // A valid PDA must not be a valid public key.
975    // ed25519_dalek::VerifyingKey::from_bytes checks if the point decompresses
976    // on the curve — if it succeeds, the hash is on-curve and NOT a valid PDA.
977    if ed25519_dalek::VerifyingKey::from_bytes(&candidate).is_ok() {
978        return Err(SignerError::SigningFailed("PDA: on curve".into()));
979    }
980
981    Ok(candidate)
982}
983
984// ═══════════════════════════════════════════════════════════════════
985// Transaction & Message Deserialization
986// ═══════════════════════════════════════════════════════════════════
987
988impl Message {
989    /// Deserialize a legacy message from wire-format bytes.
990    pub fn deserialize(data: &[u8]) -> Result<Self, SignerError> {
991        if data.len() < 3 {
992            return Err(SignerError::ParseError("message too short".into()));
993        }
994        let num_required_signatures = data[0];
995        let num_readonly_signed_accounts = data[1];
996        let num_readonly_unsigned_accounts = data[2];
997        let mut pos = 3;
998
999        // Account keys
1000        let (num_keys, consumed) = decode_compact_u16(&data[pos..])?;
1001        pos += consumed;
1002        let mut account_keys = Vec::with_capacity(num_keys as usize);
1003        for _ in 0..num_keys {
1004            if pos + 32 > data.len() {
1005                return Err(SignerError::ParseError(
1006                    "message: truncated account key".into(),
1007                ));
1008            }
1009            let mut key = [0u8; 32];
1010            key.copy_from_slice(&data[pos..pos + 32]);
1011            account_keys.push(key);
1012            pos += 32;
1013        }
1014
1015        // Recent blockhash
1016        if pos + 32 > data.len() {
1017            return Err(SignerError::ParseError(
1018                "message: truncated blockhash".into(),
1019            ));
1020        }
1021        let mut recent_blockhash = [0u8; 32];
1022        recent_blockhash.copy_from_slice(&data[pos..pos + 32]);
1023        pos += 32;
1024
1025        // Instructions
1026        let (num_ix, consumed) = decode_compact_u16(&data[pos..])?;
1027        pos += consumed;
1028        let mut instructions = Vec::with_capacity(num_ix as usize);
1029        for _ in 0..num_ix {
1030            if pos >= data.len() {
1031                return Err(SignerError::ParseError(
1032                    "message: truncated instruction".into(),
1033                ));
1034            }
1035            let program_id_index = data[pos];
1036            pos += 1;
1037
1038            let (num_accounts, consumed) = decode_compact_u16(&data[pos..])?;
1039            pos += consumed;
1040            if pos + num_accounts as usize > data.len() {
1041                return Err(SignerError::ParseError(
1042                    "message: truncated instruction accounts".into(),
1043                ));
1044            }
1045            let accounts = data[pos..pos + num_accounts as usize].to_vec();
1046            pos += num_accounts as usize;
1047
1048            let (data_len, consumed) = decode_compact_u16(&data[pos..])?;
1049            pos += consumed;
1050            if pos + data_len as usize > data.len() {
1051                return Err(SignerError::ParseError(
1052                    "message: truncated instruction data".into(),
1053                ));
1054            }
1055            let ix_data = data[pos..pos + data_len as usize].to_vec();
1056            pos += data_len as usize;
1057
1058            instructions.push(CompiledInstruction {
1059                program_id_index,
1060                accounts,
1061                data: ix_data,
1062            });
1063        }
1064
1065        Ok(Self {
1066            num_required_signatures,
1067            num_readonly_signed_accounts,
1068            num_readonly_unsigned_accounts,
1069            account_keys,
1070            recent_blockhash,
1071            instructions,
1072        })
1073    }
1074}
1075
1076impl Transaction {
1077    /// Deserialize a signed legacy transaction from wire-format bytes.
1078    pub fn deserialize(data: &[u8]) -> Result<Self, SignerError> {
1079        let mut pos = 0;
1080
1081        // Signatures
1082        let (num_sigs, consumed) = decode_compact_u16(&data[pos..])?;
1083        pos += consumed;
1084        let mut signatures = Vec::with_capacity(num_sigs as usize);
1085        for _ in 0..num_sigs {
1086            if pos + 64 > data.len() {
1087                return Err(SignerError::ParseError(
1088                    "transaction: truncated signature".into(),
1089                ));
1090            }
1091            let mut sig = [0u8; 64];
1092            sig.copy_from_slice(&data[pos..pos + 64]);
1093            signatures.push(sig);
1094            pos += 64;
1095        }
1096
1097        // Message
1098        let message = Message::deserialize(&data[pos..])?;
1099
1100        Ok(Self {
1101            signatures,
1102            message,
1103        })
1104    }
1105}
1106
1107// ═══════════════════════════════════════════════════════════════════
1108// Instruction Data Encoding (Borsh-compatible)
1109// ═══════════════════════════════════════════════════════════════════
1110
1111/// Borsh-compatible instruction data encoder.
1112///
1113/// Builds instruction data by appending fields in order, matching
1114/// the Borsh serialization format used by most Solana programs.
1115///
1116/// # Example
1117/// ```
1118/// use chains_sdk::solana::transaction::InstructionDataBuilder;
1119///
1120/// let data = InstructionDataBuilder::new()
1121///     .write_u8(1)                    // discriminator
1122///     .write_u64(1_000_000)           // amount
1123///     .write_bytes(&[0xAA; 32])       // pubkey
1124///     .write_string("hello")          // string (Borsh: u32 len + UTF-8)
1125///     .build();
1126/// ```
1127pub struct InstructionDataBuilder {
1128    buf: Vec<u8>,
1129}
1130
1131impl InstructionDataBuilder {
1132    /// Create a new empty builder.
1133    #[must_use]
1134    pub fn new() -> Self {
1135        Self { buf: Vec::new() }
1136    }
1137
1138    /// Write a `u8`.
1139    #[must_use]
1140    pub fn write_u8(mut self, val: u8) -> Self {
1141        self.buf.push(val);
1142        self
1143    }
1144
1145    /// Write a `u16` (little-endian).
1146    #[must_use]
1147    pub fn write_u16(mut self, val: u16) -> Self {
1148        self.buf.extend_from_slice(&val.to_le_bytes());
1149        self
1150    }
1151
1152    /// Write a `u32` (little-endian).
1153    #[must_use]
1154    pub fn write_u32(mut self, val: u32) -> Self {
1155        self.buf.extend_from_slice(&val.to_le_bytes());
1156        self
1157    }
1158
1159    /// Write a `u64` (little-endian).
1160    #[must_use]
1161    pub fn write_u64(mut self, val: u64) -> Self {
1162        self.buf.extend_from_slice(&val.to_le_bytes());
1163        self
1164    }
1165
1166    /// Write a `i64` (little-endian).
1167    #[must_use]
1168    pub fn write_i64(mut self, val: i64) -> Self {
1169        self.buf.extend_from_slice(&val.to_le_bytes());
1170        self
1171    }
1172
1173    /// Write a `bool` (1 byte: 0 or 1).
1174    #[must_use]
1175    pub fn write_bool(mut self, val: bool) -> Self {
1176        self.buf.push(u8::from(val));
1177        self
1178    }
1179
1180    /// Write raw bytes (no length prefix).
1181    #[must_use]
1182    pub fn write_bytes(mut self, data: &[u8]) -> Self {
1183        self.buf.extend_from_slice(data);
1184        self
1185    }
1186
1187    /// Write a 32-byte public key.
1188    #[must_use]
1189    pub fn write_pubkey(self, key: &[u8; 32]) -> Self {
1190        self.write_bytes(key)
1191    }
1192
1193    /// Write a Borsh-encoded string (u32 length prefix + UTF-8 bytes).
1194    #[must_use]
1195    pub fn write_string(mut self, s: &str) -> Self {
1196        let bytes = s.as_bytes();
1197        self.buf
1198            .extend_from_slice(&(bytes.len() as u32).to_le_bytes());
1199        self.buf.extend_from_slice(bytes);
1200        self
1201    }
1202
1203    /// Write a Borsh-encoded `Option<T>` (0 = None, 1 + data = Some).
1204    #[must_use]
1205    pub fn write_option(mut self, val: Option<&[u8]>) -> Self {
1206        match val {
1207            None => {
1208                self.buf.push(0);
1209            }
1210            Some(data) => {
1211                self.buf.push(1);
1212                self.buf.extend_from_slice(data);
1213            }
1214        }
1215        self
1216    }
1217
1218    /// Write a Borsh-encoded `Vec<u8>` (u32 length prefix + bytes).
1219    #[must_use]
1220    pub fn write_vec(mut self, data: &[u8]) -> Self {
1221        self.buf
1222            .extend_from_slice(&(data.len() as u32).to_le_bytes());
1223        self.buf.extend_from_slice(data);
1224        self
1225    }
1226
1227    /// Finalize and return the instruction data.
1228    #[must_use]
1229    pub fn build(self) -> Vec<u8> {
1230        self.buf
1231    }
1232}
1233
1234impl Default for InstructionDataBuilder {
1235    fn default() -> Self {
1236        Self::new()
1237    }
1238}
1239
1240/// Borsh-compatible instruction data decoder.
1241///
1242/// Reads fields from instruction data in order.
1243pub struct InstructionDataReader<'a> {
1244    data: &'a [u8],
1245    pos: usize,
1246}
1247
1248impl<'a> InstructionDataReader<'a> {
1249    /// Create a new reader over `data`.
1250    pub fn new(data: &'a [u8]) -> Self {
1251        Self { data, pos: 0 }
1252    }
1253
1254    /// Read a `u8`.
1255    pub fn read_u8(&mut self) -> Result<u8, SignerError> {
1256        if self.pos >= self.data.len() {
1257            return Err(SignerError::ParseError("read_u8: EOF".into()));
1258        }
1259        let val = self.data[self.pos];
1260        self.pos += 1;
1261        Ok(val)
1262    }
1263
1264    /// Read a `u16` (little-endian).
1265    pub fn read_u16(&mut self) -> Result<u16, SignerError> {
1266        if self.pos + 2 > self.data.len() {
1267            return Err(SignerError::ParseError("read_u16: EOF".into()));
1268        }
1269        let val = u16::from_le_bytes(
1270            self.data[self.pos..self.pos + 2]
1271                .try_into()
1272                .map_err(|_| SignerError::ParseError("read_u16: bad bytes".into()))?,
1273        );
1274        self.pos += 2;
1275        Ok(val)
1276    }
1277
1278    /// Read a `u32` (little-endian).
1279    pub fn read_u32(&mut self) -> Result<u32, SignerError> {
1280        if self.pos + 4 > self.data.len() {
1281            return Err(SignerError::ParseError("read_u32: EOF".into()));
1282        }
1283        let val = u32::from_le_bytes(
1284            self.data[self.pos..self.pos + 4]
1285                .try_into()
1286                .map_err(|_| SignerError::ParseError("read_u32: bad bytes".into()))?,
1287        );
1288        self.pos += 4;
1289        Ok(val)
1290    }
1291
1292    /// Read a `u64` (little-endian).
1293    pub fn read_u64(&mut self) -> Result<u64, SignerError> {
1294        if self.pos + 8 > self.data.len() {
1295            return Err(SignerError::ParseError("read_u64: EOF".into()));
1296        }
1297        let val = u64::from_le_bytes(
1298            self.data[self.pos..self.pos + 8]
1299                .try_into()
1300                .map_err(|_| SignerError::ParseError("read_u64: bad bytes".into()))?,
1301        );
1302        self.pos += 8;
1303        Ok(val)
1304    }
1305
1306    /// Read a `bool`.
1307    pub fn read_bool(&mut self) -> Result<bool, SignerError> {
1308        Ok(self.read_u8()? != 0)
1309    }
1310
1311    /// Read exactly `n` bytes.
1312    pub fn read_bytes(&mut self, n: usize) -> Result<&'a [u8], SignerError> {
1313        if self.pos + n > self.data.len() {
1314            return Err(SignerError::ParseError(format!("read_bytes({n}): EOF")));
1315        }
1316        let slice = &self.data[self.pos..self.pos + n];
1317        self.pos += n;
1318        Ok(slice)
1319    }
1320
1321    /// Read a 32-byte public key.
1322    pub fn read_pubkey(&mut self) -> Result<[u8; 32], SignerError> {
1323        let bytes = self.read_bytes(32)?;
1324        let mut key = [0u8; 32];
1325        key.copy_from_slice(bytes);
1326        Ok(key)
1327    }
1328
1329    /// Read a Borsh-encoded string (u32 len + UTF-8).
1330    pub fn read_string(&mut self) -> Result<String, SignerError> {
1331        let len = self.read_u32()? as usize;
1332        let bytes = self.read_bytes(len)?;
1333        String::from_utf8(bytes.to_vec())
1334            .map_err(|e| SignerError::ParseError(format!("read_string: {e}")))
1335    }
1336
1337    /// Remaining unread bytes.
1338    #[must_use]
1339    pub fn remaining(&self) -> &'a [u8] {
1340        &self.data[self.pos..]
1341    }
1342
1343    /// Whether all data has been consumed.
1344    #[must_use]
1345    pub fn is_empty(&self) -> bool {
1346        self.pos >= self.data.len()
1347    }
1348}
1349
1350// ═══════════════════════════════════════════════════════════════════
1351// Tests
1352// ═══════════════════════════════════════════════════════════════════
1353
1354#[cfg(test)]
1355#[allow(clippy::unwrap_used, clippy::expect_used)]
1356mod tests {
1357    use super::*;
1358    use crate::traits::KeyPair;
1359
1360    // ─── Compact-u16 Tests (Solana specification vectors) ──────────
1361
1362    #[test]
1363    fn test_compact_u16_zero() {
1364        assert_eq!(encode_compact_u16(0), vec![0]);
1365        assert_eq!(decode_compact_u16(&[0]).unwrap(), (0, 1));
1366    }
1367
1368    #[test]
1369    fn test_compact_u16_small() {
1370        assert_eq!(encode_compact_u16(5), vec![5]);
1371        assert_eq!(decode_compact_u16(&[5]).unwrap(), (5, 1));
1372    }
1373
1374    #[test]
1375    fn test_compact_u16_127_boundary() {
1376        assert_eq!(encode_compact_u16(0x7F), vec![0x7F]);
1377        assert_eq!(decode_compact_u16(&[0x7F]).unwrap(), (0x7F, 1));
1378    }
1379
1380    #[test]
1381    fn test_compact_u16_128() {
1382        let encoded = encode_compact_u16(0x80);
1383        assert_eq!(encoded.len(), 2);
1384        assert_eq!(decode_compact_u16(&encoded).unwrap(), (0x80, 2));
1385    }
1386
1387    #[test]
1388    fn test_compact_u16_16383() {
1389        // Maximum 2-byte value
1390        let encoded = encode_compact_u16(0x3FFF);
1391        assert_eq!(encoded.len(), 2);
1392        assert_eq!(decode_compact_u16(&encoded).unwrap(), (0x3FFF, 2));
1393    }
1394
1395    #[test]
1396    fn test_compact_u16_16384() {
1397        // First 3-byte value
1398        let encoded = encode_compact_u16(0x4000);
1399        assert_eq!(encoded.len(), 3);
1400        assert_eq!(decode_compact_u16(&encoded).unwrap(), (0x4000, 3));
1401    }
1402
1403    #[test]
1404    fn test_compact_u16_roundtrip_all_boundaries() {
1405        for val in [0u16, 1, 127, 128, 255, 256, 16383, 16384, 32767, 65535] {
1406            let encoded = encode_compact_u16(val);
1407            let (decoded, _) = decode_compact_u16(&encoded).unwrap();
1408            assert_eq!(decoded, val, "roundtrip failed for {val}");
1409        }
1410    }
1411
1412    // ─── System Program Tests ──────────────────────────────────────
1413
1414    #[test]
1415    fn test_system_transfer_instruction() {
1416        let from = [0xAA; 32];
1417        let to = [0xBB; 32];
1418        let ix = system_program::transfer(&from, &to, 1_000_000_000);
1419        assert_eq!(ix.program_id, system_program::ID);
1420        assert_eq!(ix.accounts.len(), 2);
1421        assert_eq!(ix.accounts[0].pubkey, from);
1422        assert!(ix.accounts[0].is_signer);
1423        assert!(!ix.accounts[1].is_signer);
1424        // Data: 4 bytes instruction index + 8 bytes lamports
1425        assert_eq!(ix.data.len(), 12);
1426        assert_eq!(&ix.data[..4], &[2, 0, 0, 0]);
1427        let lamports = u64::from_le_bytes(ix.data[4..12].try_into().unwrap());
1428        assert_eq!(lamports, 1_000_000_000);
1429    }
1430
1431    #[test]
1432    fn test_system_create_account() {
1433        let from = [0xAA; 32];
1434        let new = [0xBB; 32];
1435        let owner = [0xCC; 32];
1436        let ix = system_program::create_account(&from, &new, 1_000_000, 165, &owner);
1437        assert_eq!(ix.program_id, system_program::ID);
1438        assert_eq!(ix.accounts.len(), 2);
1439        // Data: 4 index + 8 lamports + 8 space + 32 owner = 52
1440        assert_eq!(ix.data.len(), 52);
1441    }
1442
1443    #[test]
1444    fn test_system_allocate() {
1445        let account = [0xAA; 32];
1446        let ix = system_program::allocate(&account, 1024);
1447        assert_eq!(ix.data.len(), 12);
1448    }
1449
1450    // ─── SPL Token Tests ───────────────────────────────────────────
1451
1452    #[test]
1453    fn test_spl_token_transfer() {
1454        let src = [0x11; 32];
1455        let dst = [0x22; 32];
1456        let auth = [0x33; 32];
1457        let ix = spl_token::transfer(&src, &dst, &auth, 1_000_000);
1458        assert_eq!(ix.program_id, spl_token::ID);
1459        assert_eq!(ix.accounts.len(), 3);
1460        assert!(!ix.accounts[0].is_signer); // source
1461        assert!(!ix.accounts[1].is_signer); // destination
1462        assert!(ix.accounts[2].is_signer); // authority
1463        assert_eq!(ix.data[0], 3); // Transfer discriminator
1464        assert_eq!(ix.data.len(), 9);
1465    }
1466
1467    #[test]
1468    fn test_spl_token_approve() {
1469        let src = [0x11; 32];
1470        let delegate = [0x22; 32];
1471        let auth = [0x33; 32];
1472        let ix = spl_token::approve(&src, &delegate, &auth, 500_000);
1473        assert_eq!(ix.data[0], 4); // Approve discriminator
1474    }
1475
1476    #[test]
1477    fn test_spl_token_mint_to() {
1478        let mint = [0x11; 32];
1479        let dst = [0x22; 32];
1480        let auth = [0x33; 32];
1481        let ix = spl_token::mint_to(&mint, &dst, &auth, 1_000);
1482        assert_eq!(ix.data[0], 7); // MintTo discriminator
1483    }
1484
1485    // ─── Compute Budget Tests ──────────────────────────────────────
1486
1487    #[test]
1488    fn test_compute_unit_limit() {
1489        let ix = compute_budget::set_compute_unit_limit(200_000);
1490        assert_eq!(ix.data[0], 2);
1491        let units = u32::from_le_bytes(ix.data[1..5].try_into().unwrap());
1492        assert_eq!(units, 200_000);
1493        assert!(ix.accounts.is_empty());
1494    }
1495
1496    #[test]
1497    fn test_compute_unit_price() {
1498        let ix = compute_budget::set_compute_unit_price(50_000);
1499        assert_eq!(ix.data[0], 3);
1500        let price = u64::from_le_bytes(ix.data[1..9].try_into().unwrap());
1501        assert_eq!(price, 50_000);
1502    }
1503
1504    // ─── Message Building Tests ────────────────────────────────────
1505
1506    #[test]
1507    fn test_message_building() {
1508        let payer = [0xAA; 32];
1509        let to = [0xBB; 32];
1510        let ix = system_program::transfer(&payer, &to, 100);
1511        let msg = Message::new(&[ix], payer);
1512
1513        assert_eq!(msg.num_required_signatures, 1);
1514        assert_eq!(msg.num_readonly_signed_accounts, 0);
1515        // system program = readonly unsigned
1516        assert_eq!(msg.num_readonly_unsigned_accounts, 1);
1517        // payer, to, system_program
1518        assert_eq!(msg.account_keys.len(), 3);
1519        assert_eq!(msg.account_keys[0], payer); // fee payer first
1520    }
1521
1522    #[test]
1523    fn test_message_serialization() {
1524        let payer = [0xAA; 32];
1525        let to = [0xBB; 32];
1526        let ix = system_program::transfer(&payer, &to, 100);
1527        let msg = Message::new(&[ix], payer);
1528        let bytes = msg.serialize();
1529        assert!(!bytes.is_empty());
1530        // Header: 3 bytes
1531        assert_eq!(bytes[0], 1); // num_required_signatures
1532        assert_eq!(bytes[1], 0); // num_readonly_signed
1533        assert_eq!(bytes[2], 1); // num_readonly_unsigned (system program)
1534    }
1535
1536    // ─── Transaction Tests ─────────────────────────────────────────
1537
1538    #[test]
1539    fn test_transaction_sign_and_serialize() {
1540        let signer = SolanaSigner::generate().unwrap();
1541        let payer = signer.public_key_bytes_32();
1542        let to = [0xBB; 32];
1543        let ix = system_program::transfer(&payer, &to, 1_000_000);
1544        let msg = Message::new(&[ix], payer);
1545        let blockhash = [0xCC; 32];
1546
1547        let tx = Transaction::sign(&msg, &[&signer], blockhash).unwrap();
1548        assert_eq!(tx.signatures.len(), 1);
1549        assert_eq!(tx.signatures[0].len(), 64);
1550
1551        let raw = tx.serialize();
1552        assert!(!raw.is_empty());
1553        // First byte should be compact-u16(1) = 0x01
1554        assert_eq!(raw[0], 1);
1555    }
1556
1557    #[test]
1558    fn test_transaction_deterministic() {
1559        let signer = SolanaSigner::from_bytes(&[0x42; 32]).unwrap();
1560        let payer = signer.public_key_bytes_32();
1561        let ix = system_program::transfer(&payer, &[0xBB; 32], 100);
1562        let msg = Message::new(&[ix], payer);
1563
1564        let tx1 = Transaction::sign(&msg, &[&signer], [0; 32]).unwrap();
1565        let tx2 = Transaction::sign(&msg, &[&signer], [0; 32]).unwrap();
1566        assert_eq!(tx1.serialize(), tx2.serialize());
1567    }
1568
1569    // ─── Versioned Transaction Tests ───────────────────────────────
1570
1571    #[test]
1572    fn test_v0_message_has_version_prefix() {
1573        let payer = [0xAA; 32];
1574        let ix = system_program::transfer(&payer, &[0xBB; 32], 100);
1575        let msg = Message::new(&[ix], payer);
1576        let v0 = MessageV0 {
1577            message: msg,
1578            address_table_lookups: vec![],
1579        };
1580        let bytes = v0.serialize();
1581        assert_eq!(bytes[0], 0x80, "v0 messages start with 0x80");
1582    }
1583
1584    #[test]
1585    fn test_v0_with_lookup_table() {
1586        let payer = [0xAA; 32];
1587        let ix = system_program::transfer(&payer, &[0xBB; 32], 100);
1588        let msg = Message::new(&[ix], payer);
1589        let v0 = MessageV0 {
1590            message: msg,
1591            address_table_lookups: vec![AddressLookupTable {
1592                account_key: [0xDD; 32],
1593                writable_indexes: vec![0, 1],
1594                readonly_indexes: vec![2],
1595            }],
1596        };
1597        let bytes = v0.serialize();
1598        assert_eq!(bytes[0], 0x80);
1599        assert!(bytes.len() > 100); // includes lookup table data
1600    }
1601
1602    // ─── PDA Tests ────────────────────────────────────────────────
1603
1604    #[test]
1605    fn test_find_program_address() {
1606        let program_id = [0xAA; 32];
1607        let (pda, bump) = find_program_address(&[b"test_seed"], &program_id).unwrap();
1608        assert_eq!(pda.len(), 32);
1609        assert!(bump <= 255);
1610        // Same inputs → same output
1611        let (pda2, bump2) = find_program_address(&[b"test_seed"], &program_id).unwrap();
1612        assert_eq!(pda, pda2);
1613        assert_eq!(bump, bump2);
1614    }
1615
1616    #[test]
1617    fn test_pda_different_seeds() {
1618        let program_id = [0xBB; 32];
1619        let (pda1, _) = find_program_address(&[b"seed_a"], &program_id).unwrap();
1620        let (pda2, _) = find_program_address(&[b"seed_b"], &program_id).unwrap();
1621        assert_ne!(pda1, pda2);
1622    }
1623
1624    #[test]
1625    fn test_pda_seed_too_long() {
1626        let program_id = [0xCC; 32];
1627        let long_seed = [0u8; 33]; // > 32 bytes
1628        let result = create_program_address(&[&long_seed[..]], &[0], &program_id);
1629        assert!(result.is_err());
1630    }
1631
1632    // ─── Transaction Deserialization Tests ─────────────────────────
1633
1634    #[test]
1635    fn test_message_serialize_deserialize_roundtrip() {
1636        let payer = [0xAA; 32];
1637        let to = [0xBB; 32];
1638        let ix = system_program::transfer(&payer, &to, 1_000_000);
1639        let msg = Message::new(&[ix], payer);
1640        let serialized = msg.serialize();
1641
1642        let restored = Message::deserialize(&serialized).unwrap();
1643        assert_eq!(
1644            restored.num_required_signatures,
1645            msg.num_required_signatures
1646        );
1647        assert_eq!(restored.account_keys.len(), msg.account_keys.len());
1648        assert_eq!(restored.instructions.len(), msg.instructions.len());
1649        assert_eq!(restored.instructions[0].data, msg.instructions[0].data);
1650    }
1651
1652    #[test]
1653    fn test_transaction_serialize_deserialize_roundtrip() {
1654        let signer = SolanaSigner::from_bytes(&[0x42; 32]).unwrap();
1655        let payer = signer.public_key_bytes_32();
1656        let ix = system_program::transfer(&payer, &[0xBB; 32], 100);
1657        let msg = Message::new(&[ix], payer);
1658        let tx = Transaction::sign(&msg, &[&signer], [0xDD; 32]).unwrap();
1659
1660        let raw = tx.serialize();
1661        let restored = Transaction::deserialize(&raw).unwrap();
1662
1663        assert_eq!(restored.signatures.len(), tx.signatures.len());
1664        assert_eq!(restored.signatures[0], tx.signatures[0]);
1665        assert_eq!(
1666            restored.message.account_keys.len(),
1667            tx.message.account_keys.len()
1668        );
1669    }
1670
1671    #[test]
1672    fn test_deserialize_empty_fails() {
1673        assert!(Transaction::deserialize(&[]).is_err());
1674        assert!(Message::deserialize(&[]).is_err());
1675        assert!(Message::deserialize(&[0, 0]).is_err()); // too short
1676    }
1677
1678    // ─── Instruction Data Builder/Reader Tests ────────────────────
1679
1680    #[test]
1681    fn test_instruction_data_builder_reader_roundtrip() {
1682        let data = InstructionDataBuilder::new()
1683            .write_u8(42)
1684            .write_u16(1000)
1685            .write_u32(100_000)
1686            .write_u64(1_000_000_000)
1687            .write_bool(true)
1688            .write_pubkey(&[0xAA; 32])
1689            .write_string("hello solana")
1690            .build();
1691
1692        let mut reader = InstructionDataReader::new(&data);
1693        assert_eq!(reader.read_u8().unwrap(), 42);
1694        assert_eq!(reader.read_u16().unwrap(), 1000);
1695        assert_eq!(reader.read_u32().unwrap(), 100_000);
1696        assert_eq!(reader.read_u64().unwrap(), 1_000_000_000);
1697        assert!(reader.read_bool().unwrap());
1698        assert_eq!(reader.read_pubkey().unwrap(), [0xAA; 32]);
1699        assert_eq!(reader.read_string().unwrap(), "hello solana");
1700        assert!(reader.is_empty());
1701    }
1702
1703    #[test]
1704    fn test_instruction_data_builder_option() {
1705        let none_data = InstructionDataBuilder::new().write_option(None).build();
1706        assert_eq!(none_data, vec![0]);
1707
1708        let some_data = InstructionDataBuilder::new()
1709            .write_option(Some(&[1, 2, 3]))
1710            .build();
1711        assert_eq!(some_data, vec![1, 1, 2, 3]);
1712    }
1713
1714    #[test]
1715    fn test_reader_eof_errors() {
1716        let mut reader = InstructionDataReader::new(&[]);
1717        assert!(reader.read_u8().is_err());
1718        assert!(reader.read_u64().is_err());
1719        assert!(reader.read_pubkey().is_err());
1720    }
1721
1722    // ─── SPL Token Extended Tests ─────────────────────────────────
1723
1724    #[test]
1725    fn test_spl_token_burn() {
1726        let account = [0x11; 32];
1727        let mint = [0x22; 32];
1728        let auth = [0x33; 32];
1729        let ix = spl_token::burn(&account, &mint, &auth, 500);
1730        assert_eq!(ix.data[0], 8);
1731        assert_eq!(ix.accounts.len(), 3);
1732    }
1733
1734    #[test]
1735    fn test_spl_token_close_account() {
1736        let ix = spl_token::close_account(&[0x11; 32], &[0x22; 32], &[0x33; 32]);
1737        assert_eq!(ix.data, vec![9]);
1738    }
1739
1740    #[test]
1741    fn test_spl_token_freeze_thaw() {
1742        let ix_freeze = spl_token::freeze_account(&[0x11; 32], &[0x22; 32], &[0x33; 32]);
1743        assert_eq!(ix_freeze.data, vec![10]);
1744        let ix_thaw = spl_token::thaw_account(&[0x11; 32], &[0x22; 32], &[0x33; 32]);
1745        assert_eq!(ix_thaw.data, vec![11]);
1746    }
1747
1748    #[test]
1749    fn test_spl_token_initialize_mint() {
1750        let mint = [0x11; 32];
1751        let auth = [0x22; 32];
1752        let ix = spl_token::initialize_mint(&mint, 6, &auth, Some(&[0x33; 32]));
1753        assert_eq!(ix.data[0], 0); // InitializeMint
1754        assert_eq!(ix.data[1], 6); // decimals
1755    }
1756
1757    #[test]
1758    fn test_spl_token_set_authority() {
1759        let ix = spl_token::set_authority(
1760            &[0x11; 32],
1761            &[0x22; 32],
1762            spl_token::AuthorityType::MintTokens,
1763            Some(&[0x33; 32]),
1764        );
1765        assert_eq!(ix.data[0], 6); // SetAuthority
1766        assert_eq!(ix.data[1], 0); // MintTokens
1767        assert_eq!(ix.data[2], 1); // Some
1768    }
1769
1770    #[test]
1771    fn test_spl_token_revoke() {
1772        let ix = spl_token::revoke(&[0x11; 32], &[0x22; 32]);
1773        assert_eq!(ix.data, vec![5]);
1774    }
1775
1776    #[test]
1777    fn test_spl_token_transfer_checked() {
1778        let ix = spl_token::transfer_checked(
1779            &[0x11; 32],
1780            &[0x22; 32],
1781            &[0x33; 32],
1782            &[0x44; 32],
1783            1000,
1784            6,
1785        );
1786        assert_eq!(ix.data[0], 12); // TransferChecked
1787        assert_eq!(ix.accounts.len(), 4); // source, mint, dest, authority
1788    }
1789
1790    // ─── Token-2022 Tests ─────────────────────────────────────────
1791
1792    #[test]
1793    fn test_token_2022_transfer_checked() {
1794        let ix = spl_token_2022::transfer_checked(
1795            &[0x11; 32],
1796            &[0x22; 32],
1797            &[0x33; 32],
1798            &[0x44; 32],
1799            1000,
1800            9,
1801        );
1802        assert_eq!(ix.program_id, spl_token_2022::ID);
1803        assert_eq!(ix.data[0], 12);
1804    }
1805
1806    #[test]
1807    fn test_token_2022_transfer_checked_with_fee() {
1808        let ix = spl_token_2022::transfer_checked_with_fee(
1809            &[0x11; 32],
1810            &[0x22; 32],
1811            &[0x33; 32],
1812            &[0x44; 32],
1813            1000,
1814            9,
1815            50,
1816        );
1817        assert_eq!(ix.data[0], 26); // TransferCheckedWithFee
1818        let fee = u64::from_le_bytes(ix.data[10..18].try_into().unwrap());
1819        assert_eq!(fee, 50);
1820    }
1821
1822    // ─── VersionedTransaction Tests ───────────────────────────────
1823
1824    #[test]
1825    fn test_versioned_transaction_sign() {
1826        let signer = SolanaSigner::from_bytes(&[0x42; 32]).unwrap();
1827        let payer = signer.public_key_bytes_32();
1828        let ix = system_program::transfer(&payer, &[0xBB; 32], 100);
1829        let msg = Message::new(&[ix], payer);
1830        let v0 = MessageV0 {
1831            message: msg,
1832            address_table_lookups: vec![],
1833        };
1834
1835        let vtx = VersionedTransaction::sign(&v0, &[&signer], [0xCC; 32]).unwrap();
1836        assert_eq!(vtx.signatures.len(), 1);
1837        let raw = vtx.serialize();
1838        assert!(!raw.is_empty());
1839    }
1840
1841    #[test]
1842    fn test_versioned_transaction_deterministic() {
1843        let signer = SolanaSigner::from_bytes(&[0x42; 32]).unwrap();
1844        let payer = signer.public_key_bytes_32();
1845        let ix = system_program::transfer(&payer, &[0xBB; 32], 100);
1846        let msg = Message::new(&[ix], payer);
1847        let v0 = MessageV0 {
1848            message: msg,
1849            address_table_lookups: vec![],
1850        };
1851
1852        let vtx1 = VersionedTransaction::sign(&v0, &[&signer], [0; 32]).unwrap();
1853        let vtx2 = VersionedTransaction::sign(&v0, &[&signer], [0; 32]).unwrap();
1854        assert_eq!(vtx1.serialize(), vtx2.serialize());
1855    }
1856}