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