Skip to main content

solana_primitives/types/
transaction.rs

1use crate::Result;
2use crate::crypto::sign_message;
3use crate::error::SolanaError;
4use crate::instructions::program_ids::COMPUTE_BUDGET_PROGRAM_ID;
5use crate::types::{
6    CompiledInstruction, Instruction, LegacyMessage, MAX_TRANSACTION_SIZE, Message,
7    MessageAddressTableLookup, Pubkey, SignatureBytes, VersionedMessage, VersionedMessageV0,
8};
9use borsh::{BorshDeserialize, BorshSerialize};
10use serde::{Deserialize, Serialize};
11
12/// A Solana transaction
13#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
14pub struct Transaction {
15    /// The signatures
16    pub signatures: Vec<SignatureBytes>,
17    /// The message
18    pub message: Message,
19}
20
21impl Transaction {
22    /// Create a new transaction
23    pub fn new(message: Message) -> Self {
24        Self {
25            signatures: Vec::new(),
26            message,
27        }
28    }
29
30    /// Add a signature to the transaction
31    pub fn add_signature(&mut self, signature: SignatureBytes) {
32        self.signatures.push(signature);
33    }
34
35    /// Get the number of required signatures
36    pub fn num_required_signatures(&self) -> u8 {
37        self.message.num_required_signatures()
38    }
39
40    /// Get the number of read-only signed accounts
41    pub fn num_readonly_signed_accounts(&self) -> u8 {
42        self.message.num_readonly_signed_accounts()
43    }
44
45    /// Get the number of read-only unsigned accounts
46    pub fn num_readonly_unsigned_accounts(&self) -> u8 {
47        self.message.num_readonly_unsigned_accounts()
48    }
49
50    /// Get the account keys
51    pub fn account_keys(&self) -> &[Pubkey] {
52        &self.message.account_keys
53    }
54
55    /// Get the recent blockhash
56    pub fn recent_blockhash(&self) -> &[u8; 32] {
57        &self.message.recent_blockhash
58    }
59
60    /// Get the instructions
61    pub fn instructions(&self) -> &[CompiledInstruction] {
62        &self.message.instructions
63    }
64
65    /// Deserialize a transaction from bytes
66    pub fn deserialize_with_version(bytes: &[u8]) -> Result<Self> {
67        if bytes.is_empty() {
68            return Err(SolanaError::DeserializationError(
69                "Empty transaction data".to_string(),
70            ));
71        }
72
73        // Signature count is shortvec-encoded
74        let (num_signatures, len_bytes_consumed) = crate::decode_compact_u16_len(bytes)
75            .map_err(|e| SolanaError::DeserializationError(e.to_string()))?;
76
77        // Check if there are enough bytes for signatures
78        if bytes.len() < len_bytes_consumed + (num_signatures * 64) {
79            return Err(SolanaError::DeserializationError(
80                "Not enough bytes for signatures".to_string(),
81            ));
82        }
83
84        // Extract signatures
85        let mut signatures = Vec::with_capacity(num_signatures);
86        let mut offset = len_bytes_consumed; // Skip shortvec length bytes
87
88        for _ in 0..num_signatures {
89            if offset + 64 > bytes.len() {
90                return Err(SolanaError::DeserializationError(
91                    "Invalid signature data".to_string(),
92                ));
93            }
94
95            let sig_bytes: [u8; 64] = bytes[offset..offset + 64].try_into().map_err(|_| {
96                SolanaError::DeserializationError("Failed to convert signature bytes".to_string())
97            })?;
98
99            signatures.push(SignatureBytes::new(sig_bytes));
100            offset += 64;
101        }
102
103        // The rest is the message
104        let message_bytes = &bytes[offset..];
105
106        // Use our manual decoder to decode the legacy message
107        match manual_decode::decode_legacy_message(message_bytes, Vec::new()) {
108            Ok(VersionedTransaction::Legacy { message, .. }) => {
109                // We know this is a legacy message, convert to regular Message
110                let regular_message = Message {
111                    header: message.header,
112                    account_keys: message.account_keys,
113                    recent_blockhash: message.recent_blockhash,
114                    instructions: message.instructions,
115                };
116
117                Ok(Self {
118                    signatures,
119                    message: regular_message,
120                })
121            }
122            _ => Err(SolanaError::DeserializationError(
123                "Failed to decode legacy message for Transaction".to_string(),
124            )),
125        }
126    }
127
128    /// Serializes the full transaction into the Solana legacy wire format.
129    pub fn serialize_legacy(&self) -> Result<Vec<u8>> {
130        let mut tx_wire_bytes: Vec<u8> = Vec::new();
131
132        // 1. Number of signatures (Compact-U16 encoded)
133        let sig_len_bytes = crate::encode_length_to_compact_u16_bytes(self.signatures.len())?;
134        tx_wire_bytes.extend_from_slice(&sig_len_bytes);
135
136        // 2. Signatures
137        for sig_bytes_wrapper in &self.signatures {
138            tx_wire_bytes.extend_from_slice(sig_bytes_wrapper.as_bytes());
139        }
140
141        // 3. Serialized Message
142        // The `serialize_for_signing` method in `Message` returns Result<Vec<u8>, String>
143        let serialized_message = self
144            .message
145            .serialize_for_signing()
146            .map_err(SolanaError::SerializationError)?;
147        tx_wire_bytes.extend_from_slice(&serialized_message);
148
149        Ok(tx_wire_bytes)
150    }
151
152    /// Sign the transaction with one or more private keys
153    /// The private keys must correspond to the signing accounts in the same order
154    pub fn sign(&mut self, private_keys: &[&[u8]]) -> Result<()> {
155        // Get message bytes for signing
156        let message_bytes = self
157            .message
158            .serialize_for_signing()
159            .map_err(SolanaError::SerializationError)?;
160
161        // Clear existing signatures
162        self.signatures.clear();
163
164        // Get number of required signatures
165        let num_required_sigs = self.message.header.num_required_signatures as usize;
166
167        // Validate we have enough private keys
168        if private_keys.len() < num_required_sigs {
169            return Err(SolanaError::InvalidSignature(format!(
170                "insufficient private keys: {}, required: {}",
171                private_keys.len(),
172                num_required_sigs
173            )));
174        }
175
176        // Sign with each private key
177        for private_key in private_keys.iter().take(num_required_sigs) {
178            let signature = sign_message(private_key, &message_bytes)?;
179            self.signatures.push(signature);
180        }
181
182        Ok(())
183    }
184
185    /// Partially sign the transaction with specific private keys
186    /// Updates only the signatures for the provided keys based on their public key positions
187    pub fn partial_sign(&mut self, private_keys: &[&[u8]], public_keys: &[Pubkey]) -> Result<()> {
188        if private_keys.len() != public_keys.len() {
189            return Err(SolanaError::InvalidSignature(format!(
190                "private keys count ({}) does not match public keys count ({})",
191                private_keys.len(),
192                public_keys.len()
193            )));
194        }
195
196        // Get message bytes for signing
197        let message_bytes = self
198            .message
199            .serialize_for_signing()
200            .map_err(SolanaError::SerializationError)?;
201
202        // Ensure we have enough signature slots
203        let num_required_sigs = self.message.header.num_required_signatures as usize;
204        if self.signatures.len() < num_required_sigs {
205            self.signatures
206                .resize(num_required_sigs, SignatureBytes::new([0u8; 64]));
207        }
208
209        // Sign with each private key and place signature at correct index
210        for (private_key, public_key) in private_keys.iter().zip(public_keys.iter()) {
211            // Find the index of this public key in account_keys
212            if let Some(index) = self
213                .message
214                .account_keys
215                .iter()
216                .position(|k| k == public_key)
217                && index < num_required_sigs
218            {
219                let signature = sign_message(private_key, &message_bytes)?;
220                self.signatures[index] = signature;
221            }
222        }
223
224        Ok(())
225    }
226
227    /// Check if the transaction has been signed by all required signers
228    pub fn is_signed(&self) -> bool {
229        let num_required = self.message.header.num_required_signatures as usize;
230        if self.signatures.len() < num_required {
231            return false;
232        }
233
234        // Check that none of the required signatures are empty
235        for i in 0..num_required {
236            if self.signatures[i].as_bytes().iter().all(|&b| b == 0) {
237                return false;
238            }
239        }
240
241        true
242    }
243
244    /// Validate transaction size is within limits (1232 bytes)
245    pub fn validate_size(&self) -> Result<()> {
246        let serialized = self.serialize_legacy()?;
247
248        if serialized.len() > MAX_TRANSACTION_SIZE {
249            return Err(SolanaError::SerializationError(format!(
250                "Transaction size {} exceeds maximum of {} bytes",
251                serialized.len(),
252                MAX_TRANSACTION_SIZE
253            )));
254        }
255
256        Ok(())
257    }
258}
259
260/// Versioned transaction format
261#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
262pub enum VersionedTransaction {
263    /// Legacy transaction format (pre-versioned transactions)
264    Legacy {
265        /// List of signatures
266        signatures: Vec<SignatureBytes>,
267        /// Message to sign
268        message: LegacyMessage,
269    },
270    /// Versioned transaction format V0
271    V0 {
272        /// List of signatures
273        signatures: Vec<SignatureBytes>,
274        /// Message to sign
275        message: VersionedMessageV0,
276    },
277}
278
279impl VersionedTransaction {
280    /// Create a new versioned transaction
281    pub fn new(message: VersionedMessage) -> Self {
282        match message {
283            VersionedMessage::Legacy(msg) => Self::Legacy {
284                signatures: Vec::new(),
285                message: msg,
286            },
287            VersionedMessage::V0(msg) => Self::V0 {
288                signatures: Vec::new(),
289                message: msg,
290            },
291        }
292    }
293
294    /// Add a signature to the transaction
295    pub fn add_signature(&mut self, signature: SignatureBytes) {
296        match self {
297            Self::Legacy { signatures, .. } => signatures.push(signature),
298            Self::V0 { signatures, .. } => signatures.push(signature),
299        }
300    }
301
302    /// Get the number of required signatures
303    pub fn num_required_signatures(&self) -> u8 {
304        match self {
305            Self::Legacy { message, .. } => message.header.num_required_signatures,
306            Self::V0 { message, .. } => message.header.num_required_signatures,
307        }
308    }
309
310    /// Get the number of read-only signed accounts
311    pub fn num_readonly_signed_accounts(&self) -> u8 {
312        match self {
313            Self::Legacy { message, .. } => message.header.num_readonly_signed_accounts,
314            Self::V0 { message, .. } => message.header.num_readonly_signed_accounts,
315        }
316    }
317
318    /// Get the number of read-only unsigned accounts
319    pub fn num_readonly_unsigned_accounts(&self) -> u8 {
320        match self {
321            Self::Legacy { message, .. } => message.header.num_readonly_unsigned_accounts,
322            Self::V0 { message, .. } => message.header.num_readonly_unsigned_accounts,
323        }
324    }
325
326    /// Get the account keys
327    pub fn account_keys(&self) -> &[Pubkey] {
328        match self {
329            Self::Legacy { message, .. } => &message.account_keys,
330            Self::V0 { message, .. } => &message.account_keys,
331        }
332    }
333
334    /// Get the recent blockhash
335    pub fn recent_blockhash(&self) -> &[u8; 32] {
336        match self {
337            Self::Legacy { message, .. } => &message.recent_blockhash,
338            Self::V0 { message, .. } => &message.recent_blockhash,
339        }
340    }
341
342    /// Get the instructions
343    pub fn instructions(&self) -> &[CompiledInstruction] {
344        match self {
345            Self::Legacy { message, .. } => &message.instructions,
346            Self::V0 { message, .. } => &message.instructions,
347        }
348    }
349
350    pub fn signatures(&self) -> &[SignatureBytes] {
351        match self {
352            Self::Legacy { signatures, .. } => signatures,
353            Self::V0 { signatures, .. } => signatures,
354        }
355    }
356
357    pub fn signatures_mut(&mut self) -> &mut Vec<SignatureBytes> {
358        match self {
359            Self::Legacy { signatures, .. } => signatures,
360            Self::V0 { signatures, .. } => signatures,
361        }
362    }
363
364    pub fn instructions_mut(&mut self) -> &mut Vec<CompiledInstruction> {
365        match self {
366            Self::Legacy { message, .. } => &mut message.instructions,
367            Self::V0 { message, .. } => &mut message.instructions,
368        }
369    }
370
371    fn compute_budget_program_index(&self) -> Option<u8> {
372        let cb_pubkey = Pubkey::from_base58(COMPUTE_BUDGET_PROGRAM_ID).ok()?;
373        self.account_keys()
374            .iter()
375            .position(|k| *k == cb_pubkey)
376            .map(|i| i as u8)
377    }
378
379    pub fn get_compute_unit_price(&self) -> Option<u64> {
380        let idx = self.compute_budget_program_index()?;
381        for ix in self.instructions() {
382            if ix.program_id_index == idx && ix.data.len() == 9 && ix.data[0] == 3 {
383                return Some(u64::from_le_bytes(ix.data[1..9].try_into().ok()?));
384            }
385        }
386        None
387    }
388
389    pub fn set_compute_unit_price(&mut self, micro_lamports: u64) -> Result<bool> {
390        if let Some(idx) = self.compute_budget_program_index() {
391            for ix in self.instructions_mut() {
392                if ix.program_id_index == idx && ix.data.len() == 9 && ix.data[0] == 3 {
393                    ix.data[1..9].copy_from_slice(&micro_lamports.to_le_bytes());
394                    return Ok(true);
395                }
396            }
397        }
398        Ok(false)
399    }
400
401    pub fn get_compute_unit_limit(&self) -> Option<u32> {
402        let idx = self.compute_budget_program_index()?;
403        for ix in self.instructions() {
404            if ix.program_id_index == idx && ix.data.len() == 5 && ix.data[0] == 2 {
405                return Some(u32::from_le_bytes(ix.data[1..5].try_into().ok()?));
406            }
407        }
408        None
409    }
410
411    pub fn set_compute_unit_limit(&mut self, units: u32) -> Result<bool> {
412        if let Some(idx) = self.compute_budget_program_index() {
413            for ix in self.instructions_mut() {
414                if ix.program_id_index == idx && ix.data.len() == 5 && ix.data[0] == 2 {
415                    ix.data[1..5].copy_from_slice(&units.to_le_bytes());
416                    return Ok(true);
417                }
418            }
419        }
420        Ok(false)
421    }
422
423    pub fn add_instruction(&mut self, instruction: Instruction) -> Result<()> {
424        let message = match self {
425            Self::Legacy { message, .. } => message,
426            _ => {
427                return Err(SolanaError::SerializationError(
428                    "add_instruction only supported for legacy transactions".to_string(),
429                ));
430            }
431        };
432
433        let mut new_writable_non_signers: Vec<Pubkey> = Vec::new();
434        let mut new_readonly_non_signers: Vec<Pubkey> = Vec::new();
435
436        if !message.account_keys.contains(&instruction.program_id) {
437            new_readonly_non_signers.push(instruction.program_id);
438        }
439
440        for meta in &instruction.accounts {
441            if !message.account_keys.contains(&meta.pubkey)
442                && !new_writable_non_signers.contains(&meta.pubkey)
443                && !new_readonly_non_signers.contains(&meta.pubkey)
444            {
445                if meta.is_writable && !meta.is_signer {
446                    new_writable_non_signers.push(meta.pubkey);
447                } else if !meta.is_signer {
448                    new_readonly_non_signers.push(meta.pubkey);
449                }
450            }
451        }
452
453        let insert_pos = message
454            .account_keys
455            .len()
456            .checked_sub(message.header.num_readonly_unsigned_accounts as usize)
457            .ok_or(SolanaError::InvalidMessage)?;
458        for (i, pubkey) in new_writable_non_signers.iter().enumerate() {
459            message.account_keys.insert(insert_pos + i, *pubkey);
460        }
461
462        let num_inserted = new_writable_non_signers.len();
463        if num_inserted > 0 {
464            for ix in &mut message.instructions {
465                if (ix.program_id_index as usize) >= insert_pos {
466                    ix.program_id_index += num_inserted as u8;
467                }
468                for acc in &mut ix.accounts {
469                    if (*acc as usize) >= insert_pos {
470                        *acc += num_inserted as u8;
471                    }
472                }
473            }
474        }
475
476        for pubkey in &new_readonly_non_signers {
477            message.account_keys.push(*pubkey);
478        }
479        message.header.num_readonly_unsigned_accounts += new_readonly_non_signers.len() as u8;
480
481        let program_id_index = message
482            .account_keys
483            .iter()
484            .position(|k| *k == instruction.program_id)
485            .unwrap() as u8;
486        let accounts: Vec<u8> = instruction
487            .accounts
488            .iter()
489            .map(|meta| {
490                message
491                    .account_keys
492                    .iter()
493                    .position(|k| *k == meta.pubkey)
494                    .unwrap() as u8
495            })
496            .collect();
497
498        message.instructions.push(CompiledInstruction {
499            program_id_index,
500            accounts,
501            data: instruction.data,
502        });
503
504        Ok(())
505    }
506
507    pub fn serialize_message(&self) -> Result<Vec<u8>> {
508        match self {
509            Self::Legacy { message, .. } => message
510                .serialize_for_signing()
511                .map_err(SolanaError::SerializationError),
512            Self::V0 { message, .. } => message
513                .serialize_for_signing()
514                .map_err(SolanaError::SerializationError),
515        }
516    }
517
518    pub fn serialize(&self) -> Result<Vec<u8>> {
519        let mut bytes = Vec::new();
520        let signatures = self.signatures();
521        let sig_len = crate::encode_length_to_compact_u16_bytes(signatures.len())
522            .map_err(SolanaError::SerializationError)?;
523        bytes.extend_from_slice(&sig_len);
524        for sig in signatures {
525            bytes.extend_from_slice(sig.as_bytes());
526        }
527        let message_bytes = self.serialize_message()?;
528        bytes.extend_from_slice(&message_bytes);
529        Ok(bytes)
530    }
531
532    /// Deserialize a versioned transaction from bytes
533    pub fn deserialize_with_version(bytes: &[u8]) -> Result<Self> {
534        if bytes.is_empty() {
535            return Err(SolanaError::DeserializationError(
536                "Empty transaction data".to_string(),
537            ));
538        }
539
540        // Signature count is shortvec-encoded
541        let (num_signatures, len_bytes_consumed) = crate::decode_compact_u16_len(bytes)
542            .map_err(|e| SolanaError::DeserializationError(e.to_string()))?;
543
544        // Check if there are enough bytes for signatures
545        if bytes.len() < len_bytes_consumed + (num_signatures * 64) {
546            return Err(SolanaError::DeserializationError(
547                "Not enough bytes for signatures".to_string(),
548            ));
549        }
550
551        // Extract signatures
552        let mut signatures = Vec::with_capacity(num_signatures);
553        let mut offset = len_bytes_consumed; // Skip shortvec length bytes
554
555        for _ in 0..num_signatures {
556            if offset + 64 > bytes.len() {
557                return Err(SolanaError::DeserializationError(
558                    "Invalid signature data".to_string(),
559                ));
560            }
561
562            let sig_bytes: [u8; 64] = bytes[offset..offset + 64].try_into().map_err(|_| {
563                SolanaError::DeserializationError("Failed to convert signature bytes".to_string())
564            })?;
565
566            signatures.push(SignatureBytes::new(sig_bytes));
567            offset += 64;
568        }
569
570        // The rest is the message
571        let message_bytes = &bytes[offset..];
572
573        // Manually decode the message
574        self::manual_decode::decode_message(message_bytes, signatures)
575    }
576}
577
578/// Module for manual decoding of Solana message format
579mod manual_decode {
580    use super::*;
581    use crate::types::MessageHeader;
582
583    /// Validates each header count against its own section, not just the total length.
584    fn validate_header_counts(header: &MessageHeader, account_keys_len: usize) -> Result<()> {
585        let num_required_signatures = header.num_required_signatures as usize;
586        if num_required_signatures > account_keys_len {
587            return Err(SolanaError::DeserializationError(
588                "Message header num_required_signatures exceeds account_keys length".to_string(),
589            ));
590        }
591        if header.num_readonly_signed_accounts as usize > num_required_signatures {
592            return Err(SolanaError::DeserializationError(
593                "Message header num_readonly_signed_accounts exceeds num_required_signatures"
594                    .to_string(),
595            ));
596        }
597        if header.num_readonly_unsigned_accounts as usize
598            > account_keys_len - num_required_signatures
599        {
600            return Err(SolanaError::DeserializationError(
601                "Message header num_readonly_unsigned_accounts exceeds the number of unsigned accounts"
602                    .to_string(),
603            ));
604        }
605        Ok(())
606    }
607
608    /// Decode a message based on the Solana binary format
609    /// The format is:
610    /// 1. If the high bit of the first byte is set, it's a versioned message
611    ///    - The version is in the lower 7 bits
612    ///    - Rest of message follows based on version
613    /// 2. Otherwise, it's a legacy message with format:
614    ///    - 3 bytes header (num_required_signatures, num_readonly_signed, num_readonly_unsigned)
615    ///    - Account keys (1 byte count, then count * 32 bytes)
616    ///    - Recent blockhash (32 bytes)
617    ///    - Instructions (1 byte count, then variable length instructions)
618    pub fn decode_message(
619        bytes: &[u8],
620        signatures: Vec<SignatureBytes>,
621    ) -> Result<VersionedTransaction> {
622        if bytes.len() < 3 {
623            return Err(SolanaError::DeserializationError(
624                "Message bytes too short, need at least 3 bytes for header".to_string(),
625            ));
626        }
627
628        // Check if this is a versioned message (first bit set)
629        let is_versioned = (bytes[0] & 0x80) != 0;
630
631        if is_versioned {
632            // Extract version from first byte (low 7 bits)
633            let version = bytes[0] & 0x7F;
634
635            // Currently only V0 messages are supported
636            if version == 0 {
637                decode_v0_message(&bytes[1..], signatures)
638            } else {
639                Err(SolanaError::DeserializationError(format!(
640                    "Unsupported message version: {version}"
641                )))
642            }
643        } else {
644            // Legacy message (no version byte)
645            decode_legacy_message(bytes, signatures)
646        }
647    }
648
649    /// Decode a legacy (non-versioned) message
650    /// The format is:
651    /// 1. Header (3 bytes)
652    ///    - num_required_signatures (1 byte)
653    ///    - num_readonly_signed_accounts (1 byte)
654    ///    - num_readonly_unsigned_accounts (1 byte)
655    /// 2. Account keys
656    ///    - count (1 byte)
657    ///    - public keys (count * 32 bytes)
658    /// 3. Recent blockhash (32 bytes)
659    /// 4. Instructions
660    ///    - count (1 byte)
661    ///    - instructions (variable length)
662    pub fn decode_legacy_message(
663        bytes: &[u8],
664        signatures: Vec<SignatureBytes>,
665    ) -> Result<VersionedTransaction> {
666        if bytes.len() < 3 {
667            return Err(SolanaError::DeserializationError(
668                "Legacy message too short".to_string(),
669            ));
670        }
671
672        // Header: 3 bytes
673        let header = MessageHeader {
674            num_required_signatures: bytes[0],
675            num_readonly_signed_accounts: bytes[1],
676            num_readonly_unsigned_accounts: bytes[2],
677        };
678
679        let mut offset = 3;
680
681        // Account keys
682        if offset >= bytes.len() {
683            return Err(SolanaError::DeserializationError(
684                "Message too short: no account count".to_string(),
685            ));
686        }
687        let (account_count, len_bytes_consumed) =
688            crate::decode_compact_u16_len(&bytes[offset..]).map_err(SolanaError::from)?;
689        offset += len_bytes_consumed;
690
691        if offset + (account_count * 32) > bytes.len() {
692            return Err(SolanaError::DeserializationError(
693                "Message too short: not enough bytes for accounts".to_string(),
694            ));
695        }
696
697        let mut account_keys = Vec::with_capacity(account_count);
698        for _ in 0..account_count {
699            let mut key = [0u8; 32];
700            key.copy_from_slice(&bytes[offset..offset + 32]);
701            account_keys.push(Pubkey::new(key));
702            offset += 32;
703        }
704
705        validate_header_counts(&header, account_keys.len())?;
706
707        // Recent blockhash (always 32 bytes)
708        if offset + 32 > bytes.len() {
709            return Err(SolanaError::DeserializationError(
710                "Message too short: no recent blockhash".to_string(),
711            ));
712        }
713        let mut recent_blockhash = [0u8; 32];
714        recent_blockhash.copy_from_slice(&bytes[offset..offset + 32]);
715        offset += 32;
716
717        // Instructions
718        if offset >= bytes.len() {
719            return Err(SolanaError::DeserializationError(
720                "Message too short: no instruction count".to_string(),
721            ));
722        }
723        let (instruction_count, len_bytes_consumed) =
724            crate::decode_compact_u16_len(&bytes[offset..]).map_err(SolanaError::from)?;
725        offset += len_bytes_consumed;
726
727        // Each instruction needs >= 3 bytes; reject counts that can't fit in what's left.
728        let remaining = bytes.len().saturating_sub(offset);
729        if instruction_count.saturating_mul(3) > remaining {
730            return Err(SolanaError::DeserializationError(
731                "Message too short: instruction count exceeds remaining bytes".to_string(),
732            ));
733        }
734
735        let mut instructions = Vec::with_capacity(instruction_count);
736        for _ in 0..instruction_count {
737            if offset >= bytes.len() {
738                return Err(SolanaError::DeserializationError(
739                    "Message too short: incomplete instruction".to_string(),
740                ));
741            }
742
743            // Program ID index (1 byte)
744            let program_id_index = bytes[offset];
745            offset += 1;
746
747            if offset >= bytes.len() {
748                return Err(SolanaError::DeserializationError(
749                    "Message too short: no account indices count".to_string(),
750                ));
751            }
752
753            // Account indices (compact-u16 length, then count bytes)
754            let (account_indices_count, len_bytes_consumed) =
755                crate::decode_compact_u16_len(&bytes[offset..]).map_err(SolanaError::from)?;
756            offset += len_bytes_consumed;
757
758            if offset + account_indices_count > bytes.len() {
759                return Err(SolanaError::DeserializationError(
760                    "Message too short: not enough account indices".to_string(),
761                ));
762            }
763
764            let accounts = bytes[offset..offset + account_indices_count].to_vec();
765            offset += account_indices_count;
766
767            if offset >= bytes.len() {
768                // This check ensures there's at least one byte for the length itself.
769                return Err(SolanaError::DeserializationError(
770                    "Message too short: no instruction data length".to_string(),
771                ));
772            }
773
774            // Instruction data (compact-u16 length, then length bytes)
775            let (data_length, len_bytes_consumed) =
776                crate::decode_compact_u16_len(&bytes[offset..]).map_err(SolanaError::from)?;
777            offset += len_bytes_consumed;
778
779            if offset + data_length > bytes.len() {
780                return Err(SolanaError::DeserializationError(
781                    "Message too short: not enough instruction data".to_string(),
782                ));
783            }
784
785            let data = bytes[offset..offset + data_length].to_vec();
786            offset += data_length;
787
788            instructions.push(CompiledInstruction {
789                program_id_index,
790                accounts,
791                data,
792            });
793        }
794
795        Ok(VersionedTransaction::Legacy {
796            signatures,
797            message: LegacyMessage {
798                header,
799                account_keys,
800                recent_blockhash,
801                instructions,
802            },
803        })
804    }
805
806    /// Decode a V0 versioned message
807    /// V0 messages support address lookup tables
808    pub fn decode_v0_message(
809        bytes: &[u8],
810        signatures: Vec<SignatureBytes>,
811    ) -> Result<VersionedTransaction> {
812        if bytes.len() < 3 {
813            return Err(SolanaError::DeserializationError(
814                "V0 message too short".to_string(),
815            ));
816        }
817
818        // Header: 3 bytes
819        let header = MessageHeader {
820            num_required_signatures: bytes[0],
821            num_readonly_signed_accounts: bytes[1],
822            num_readonly_unsigned_accounts: bytes[2],
823        };
824
825        let mut offset = 3;
826
827        // Account keys
828        if offset >= bytes.len() {
829            return Err(SolanaError::DeserializationError(
830                "Message too short: no account count".to_string(),
831            ));
832        }
833        let (account_count, len_bytes_consumed) =
834            crate::decode_compact_u16_len(&bytes[offset..]).map_err(SolanaError::from)?;
835        offset += len_bytes_consumed;
836
837        if offset + (account_count * 32) > bytes.len() {
838            return Err(SolanaError::DeserializationError(
839                "Message too short: not enough bytes for accounts".to_string(),
840            ));
841        }
842
843        let mut account_keys = Vec::with_capacity(account_count);
844        for _ in 0..account_count {
845            let mut key = [0u8; 32];
846            key.copy_from_slice(&bytes[offset..offset + 32]);
847            account_keys.push(Pubkey::new(key));
848            offset += 32;
849        }
850
851        validate_header_counts(&header, account_keys.len())?;
852
853        // Recent blockhash (always 32 bytes)
854        if offset + 32 > bytes.len() {
855            return Err(SolanaError::DeserializationError(
856                "Message too short: no recent blockhash".to_string(),
857            ));
858        }
859        let mut recent_blockhash = [0u8; 32];
860        recent_blockhash.copy_from_slice(&bytes[offset..offset + 32]);
861        offset += 32;
862
863        // Instructions
864        if offset >= bytes.len() {
865            return Err(SolanaError::DeserializationError(
866                "Message too short: no instruction count".to_string(),
867            ));
868        }
869        let (instruction_count, len_bytes_consumed) =
870            crate::decode_compact_u16_len(&bytes[offset..]).map_err(SolanaError::from)?;
871        offset += len_bytes_consumed;
872
873        // Each instruction needs >= 3 bytes; reject counts that can't fit in what's left.
874        let remaining = bytes.len().saturating_sub(offset);
875        if instruction_count.saturating_mul(3) > remaining {
876            return Err(SolanaError::DeserializationError(
877                "Message too short: instruction count exceeds remaining bytes".to_string(),
878            ));
879        }
880
881        let mut instructions = Vec::with_capacity(instruction_count);
882        for _ in 0..instruction_count {
883            if offset >= bytes.len() {
884                return Err(SolanaError::DeserializationError(
885                    "Message too short: incomplete instruction".to_string(),
886                ));
887            }
888
889            // Program ID index (1 byte)
890            let program_id_index = bytes[offset];
891            offset += 1;
892
893            if offset >= bytes.len() {
894                return Err(SolanaError::DeserializationError(
895                    "Message too short: no account indices count".to_string(),
896                ));
897            }
898
899            // Account indices (compact-u16 length, then count bytes)
900            let (account_indices_count, len_bytes_consumed) =
901                crate::decode_compact_u16_len(&bytes[offset..]).map_err(SolanaError::from)?;
902            offset += len_bytes_consumed;
903
904            if offset + account_indices_count > bytes.len() {
905                return Err(SolanaError::DeserializationError(
906                    "Message too short: not enough account indices".to_string(),
907                ));
908            }
909
910            let accounts = bytes[offset..offset + account_indices_count].to_vec();
911            offset += account_indices_count;
912
913            if offset >= bytes.len() {
914                // This check ensures there's at least one byte for the length itself.
915                return Err(SolanaError::DeserializationError(
916                    "Message too short: no instruction data length".to_string(),
917                ));
918            }
919
920            // Instruction data (compact-u16 length, then length bytes)
921            let (data_length, len_bytes_consumed) =
922                crate::decode_compact_u16_len(&bytes[offset..]).map_err(SolanaError::from)?;
923            offset += len_bytes_consumed;
924
925            if offset + data_length > bytes.len() {
926                return Err(SolanaError::DeserializationError(
927                    "Message too short: not enough instruction data".to_string(),
928                ));
929            }
930
931            let data = bytes[offset..offset + data_length].to_vec();
932            offset += data_length;
933
934            instructions.push(CompiledInstruction {
935                program_id_index,
936                accounts,
937                data,
938            });
939        }
940
941        // Address table lookups (new in V0)
942        let mut address_table_lookups = Vec::new();
943
944        // Check if we have more data (for address table lookups)
945        if offset < bytes.len() {
946            let (lookup_table_count, len_bytes_consumed) =
947                crate::decode_compact_u16_len(&bytes[offset..])
948                    .map_err(|e| SolanaError::DeserializationError(e.to_string()))?;
949            offset += len_bytes_consumed;
950
951            for _ in 0..lookup_table_count {
952                if offset + 32 > bytes.len() {
953                    return Err(SolanaError::DeserializationError(
954                        "Message too short: incomplete address lookup table".to_string(),
955                    ));
956                }
957
958                // Lookup table account key
959                let mut key = [0u8; 32];
960                key.copy_from_slice(&bytes[offset..offset + 32]);
961                let lookup_table_key = Pubkey::new(key);
962                offset += 32;
963
964                // Writable indexes
965                if offset >= bytes.len() {
966                    return Err(SolanaError::DeserializationError(
967                        "Message too short: no writable indexes count".to_string(),
968                    ));
969                }
970                let (writable_indexes_count, len_bytes_consumed) =
971                    crate::decode_compact_u16_len(&bytes[offset..])
972                        .map_err(|e| SolanaError::DeserializationError(e.to_string()))?;
973                offset += len_bytes_consumed;
974
975                if offset + writable_indexes_count > bytes.len() {
976                    return Err(SolanaError::DeserializationError(
977                        "Message too short: not enough writable indexes".to_string(),
978                    ));
979                }
980
981                let writable_indexes = bytes[offset..offset + writable_indexes_count].to_vec();
982                offset += writable_indexes_count;
983
984                // Readonly indexes
985                if offset >= bytes.len() {
986                    return Err(SolanaError::DeserializationError(
987                        "Message too short: no readonly indexes count".to_string(),
988                    ));
989                }
990                let (readonly_indexes_count, len_bytes_consumed) =
991                    crate::decode_compact_u16_len(&bytes[offset..])
992                        .map_err(|e| SolanaError::DeserializationError(e.to_string()))?;
993                offset += len_bytes_consumed;
994
995                if offset + readonly_indexes_count > bytes.len() {
996                    return Err(SolanaError::DeserializationError(
997                        "Message too short: not enough readonly indexes".to_string(),
998                    ));
999                }
1000
1001                let readonly_indexes = bytes[offset..offset + readonly_indexes_count].to_vec();
1002                offset += readonly_indexes_count;
1003
1004                address_table_lookups.push(MessageAddressTableLookup {
1005                    account_key: lookup_table_key,
1006                    writable_indexes,
1007                    readonly_indexes,
1008                });
1009            }
1010        }
1011
1012        Ok(VersionedTransaction::V0 {
1013            signatures,
1014            message: VersionedMessageV0 {
1015                header,
1016                account_keys,
1017                recent_blockhash,
1018                instructions,
1019                address_table_lookups,
1020            },
1021        })
1022    }
1023}
1024
1025#[cfg(test)]
1026mod tests {
1027    use super::*;
1028    use crate::{
1029        instructions::system,
1030        types::{Pubkey, SignatureBytes},
1031    };
1032    use base64::{Engine, engine::general_purpose::STANDARD};
1033
1034    /// Legacy tx with SetComputeUnitLimit(420000) and SetComputeUnitPrice(70000).
1035    const LEGACY_TX: &str = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAgWAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEbrtjJdvWJAv9GZTGL8LaZtMvDe4j2ery4z7rOkRbioxZflXLFqWqlAt1REFSiam0ljvfB1tbBruEpGRTcUQIyQ+ddH9NRneQZQXje5U/3c4cZ2f1JESi76CvBvRoQ6I1LeNzfZ4ZONkowCnqCyeo5+D6Q21gn3U7HVw/KD3HyUW5gVpu5F8ZojWkXLg/+3N6q3ojiaqYyBIbz7VP7jS5Yktrxv5b22C/EFSDs5jUPA7Gz3GLdBNs0iwBHlqUqNEeyNpDX0HWNHV2LiVDOx6m018ea6P+1xroNvWKhmDeTW7oqHXAEK1ih5IO68BBiiKqWNR5VZdBgBsnR+rZKfpfuyE3yQziYO+SoWzCXuvQLyVcRCNKJrACzaN8XXUR1z3rOt8T1lYUIIAQS7tqgcLRsn18N4vVQgXQyv3bQWjh3JtpQT3Bgy9N9myGC4PDjGuVnx2Y7mF4eqlysb0rgrdrB2+FMK6YBPXtlXF4QPTY6rEe+hxkBpCoGK7UJu5BHUK4gJhAewgMolkoyq6sTbFQFuR86447k9ky2veh5uGg40gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FkDBkZv5SEXMv/srbpyw5vnvIzlu8X3EmssQ5s6QAAAAMb6evO+2606PWXzaqvJdDGxu+TC0vbg5HymAgNFL11hBUpTWpkpIQZNJOhxYNo4fHw1td28kruB5B+oQEEFRI0Gm4hX/quBhPtof2NGGMA12sQ53BrrO1WYoPAAAAAAAQbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpDgNoX46QkFPkWBIcZvWnau3HcGqhHIL4qpUqjyt4ealuCa42Moiy1mB8REcWJlkis4eCMyKfY2HMRfldn8r2XwcQAAUCoGgGABAACQNwEQEAAAAAAA8GAAYAEw4UAQAVERQUEgAHExEGCQoCBAULDAgBMSsE7QsayR5iC50OAAAAAAA8XqkAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEBAAAABgIUAwYAAAEJFAMKAwAJA8wSAAAAAAAADgIADQwCAAAAODEAAAAAAAA=";
1036
1037    /// https://solscan.io/tx/2DZEgrPpdwCu2JcQZJCFivcmLSNMHMmDth9ujqFFZ8UeaEX6EqJmFTfZ43c7LgWqu85wiFhqo2h8PukruvpS4g4u
1038    const MAYAN_V0_TX: &str = "ATzYOiofQZSWsNe3SxxEPip+Xp9A2Fji+h0xfs7FkmvQxNNgwjeEbTlMr7+e42q9vcvExw2CX4PgNBRuY77O+waAAQAEDPlBHYJN7SVAqQdmNtdFQsCIDVJuEnf59VTtTCOGI7yLh4jpmImexNtJSORTO+sbJ63Aysdx88si41jIW1Wf65qHxwlVbaZ8xI24o/VzmleK1NqPB2lMTcy78ZFbqJ6agIqQAqWC7XmuIVDA/VxhSMZPxFOazPZMJbWyD+TYtXxA3sS/qzC61MydFxPOY3xt62Ug5Tp3r/hC0NimkXNfrMH0UmoX+WTY7c2jVeACjg8EqVgtZZSXgaQRvotGaelPhCySBd5s0S8tvrZZSGGBUknE3Jjh4aGsgXpNY0QHkFnJayU0QDsmAQ7sF/E5yI6Oq1k8w8tnKB6wJR28JzZwp3KVGAf9PgfpG6VoBYOYtT4QWhLzz8wJo5Da/9f9tVVfo7Qj5Z1paZLqq3kUJ1PAm9bYE1qpQE9jUkcSHEnSn0OVAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAAAGTCSuZOXkbU4/LKndRkF4gm16E7to0DdpTPoefoS0rYF08m4FFLws+yIpIkWYIyALDIz0sekCn1BgZGSqLNo5CwsAF0FkUEJ2ZE5kVGxlWmNsc25JeDVkeUExCgAFAhxCBwAKAAkDBBcBAAAAAAAJBxUABgUWGRQACQcVAAEAGxkUAQEJAxkAAQwCAAAAC/UHPQYAAAAJAhQBAREJBxUAAwAWGRQBAQgoHAABAxsWFBQGHRwhACITAQMPERIUBBACBxweAA4gHw0MAQMbFhQUGjIBLQAAALtk+swxxK8UC/UHPQYAAAD8nvqKAAAAAGQAAAAAAAIAAAAaQAYAAl8A0CAAAgkEFAEAAAEJCQoYAAAFBgMWFxQZxgEgTCkMJ6KE2yRjS4oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhOLnPgTBemY6Iqc117gEL72WnXMeAAAAAAAAAAAAAAAAAIM1ifzW7bbgj0x8MtT3G1S9oCkT/ySLiQAAAAAAAAAAAAAAAG4DAAAAAAAA8dcFAAAAAAA7a4ppAAAAAAAAAAAAAAAAAAAAAN3bmpXkQ6IE64ZQ1epXjtcH/iEjAAMC0SsrhG32PclzqA5blk8lqOrlLpR5OoOt60ksQpGLgw4D2cMN+5YRja4DNaX11bThHmwP8vCzdRYtSPXpFGWT2KsACgUGESAhKSowMTQme3jXiWKuyj4qkRn+CZK3WspZpXBM+tnHyaYm4WA/BAMICgsDBgkMIbxt+8RM8X78HZP9nB+0Ah2xfOX9io4UH0AdkLgPT00Fdnd6fH4CdHU=";
1039
1040    fn decode_legacy_tx() -> VersionedTransaction {
1041        let data = STANDARD.decode(LEGACY_TX).unwrap();
1042        VersionedTransaction::deserialize_with_version(&data).unwrap()
1043    }
1044
1045    fn decode_mayan_tx() -> VersionedTransaction {
1046        let data = STANDARD.decode(MAYAN_V0_TX).unwrap();
1047        VersionedTransaction::deserialize_with_version(&data).unwrap()
1048    }
1049
1050    #[test]
1051    fn decode_legacy() {
1052        let tx = decode_legacy_tx();
1053        assert!(matches!(tx, VersionedTransaction::Legacy { .. }));
1054        assert_eq!(tx.signatures().len(), 1);
1055        assert_eq!(tx.account_keys().len(), 22);
1056        assert_eq!(tx.instructions().len(), 7);
1057    }
1058
1059    #[test]
1060    fn decode_v0() {
1061        let tx = decode_mayan_tx();
1062        assert!(matches!(tx, VersionedTransaction::V0 { .. }));
1063        assert_eq!(tx.signatures().len(), 1);
1064    }
1065
1066    #[test]
1067    fn signatures_accessors() {
1068        let mut tx = decode_legacy_tx();
1069        let original_sig = tx.signatures()[0];
1070
1071        tx.signatures_mut()[0] = SignatureBytes::new([42; 64]);
1072        assert_eq!(tx.signatures()[0], SignatureBytes::new([42; 64]));
1073        assert_ne!(tx.signatures()[0], original_sig);
1074    }
1075
1076    #[test]
1077    fn get_compute_unit_price_from_legacy() {
1078        assert_eq!(decode_legacy_tx().get_compute_unit_price(), Some(70_000));
1079    }
1080
1081    #[test]
1082    fn get_compute_unit_price_from_v0() {
1083        assert_eq!(decode_mayan_tx().get_compute_unit_price(), Some(71_428));
1084    }
1085
1086    #[test]
1087    fn set_compute_unit_price_legacy() {
1088        let mut tx = decode_legacy_tx();
1089        assert!(tx.set_compute_unit_price(999_999).unwrap());
1090        assert_eq!(tx.get_compute_unit_price(), Some(999_999));
1091    }
1092
1093    #[test]
1094    fn get_compute_unit_limit_from_legacy() {
1095        assert_eq!(decode_legacy_tx().get_compute_unit_limit(), Some(420_000));
1096    }
1097
1098    #[test]
1099    fn get_compute_unit_limit_from_v0() {
1100        assert_eq!(decode_mayan_tx().get_compute_unit_limit(), Some(475_676));
1101    }
1102
1103    #[test]
1104    fn set_compute_unit_limit_legacy() {
1105        let mut tx = decode_legacy_tx();
1106        assert!(tx.set_compute_unit_limit(500_000).unwrap());
1107        assert_eq!(tx.get_compute_unit_limit(), Some(500_000));
1108    }
1109
1110    #[test]
1111    fn add_instruction_appends_to_legacy() {
1112        let mut tx = decode_legacy_tx();
1113        let initial_ix_count = tx.instructions().len();
1114        let price_before = tx.get_compute_unit_price().unwrap();
1115
1116        let from = tx.account_keys()[0];
1117        let to = Pubkey::new([99; 32]);
1118        tx.add_instruction(system::transfer(&from, &to, 5000))
1119            .unwrap();
1120
1121        assert_eq!(tx.instructions().len(), initial_ix_count + 1);
1122        assert!(tx.account_keys().contains(&to));
1123        assert_eq!(tx.get_compute_unit_price(), Some(price_before));
1124    }
1125
1126    #[test]
1127    fn add_instruction_errors_on_v0() {
1128        let mut tx = decode_mayan_tx();
1129        let from = tx.account_keys()[0];
1130        let to = Pubkey::new([2; 32]);
1131        assert!(
1132            tx.add_instruction(system::transfer(&from, &to, 100))
1133                .is_err()
1134        );
1135    }
1136
1137    /// Builds a legacy-message prefix: header + `num_accounts` zero keys + zero blockhash.
1138    fn legacy_message_prefix(header: [u8; 3], num_accounts: u8) -> Vec<u8> {
1139        let mut bytes = header.to_vec();
1140        bytes.push(num_accounts);
1141        bytes.extend(std::iter::repeat_n(0u8, 32 * num_accounts as usize));
1142        bytes.extend_from_slice(&[0u8; 32]);
1143        bytes
1144    }
1145
1146    #[test]
1147    fn decode_legacy_message_rejects_huge_instruction_count() {
1148        let mut bytes = legacy_message_prefix([1, 0, 0], 1);
1149        bytes.extend_from_slice(&crate::encode_length_to_compact_u16_bytes(60_000).unwrap());
1150
1151        let result = manual_decode::decode_legacy_message(&bytes, Vec::new());
1152        assert!(
1153            result.is_err(),
1154            "huge instruction count exceeding remaining bytes must be rejected"
1155        );
1156
1157        // Also rejected end-to-end (prefixed with an empty signature count).
1158        let mut tx_bytes = vec![0u8];
1159        tx_bytes.extend_from_slice(&bytes);
1160        assert!(VersionedTransaction::deserialize_with_version(&tx_bytes).is_err());
1161    }
1162
1163    #[test]
1164    fn decode_legacy_message_rejects_inconsistent_header() {
1165        // num_readonly_unsigned_accounts = 5, but only 1 account key is present.
1166        let mut bytes = legacy_message_prefix([1, 0, 5], 1);
1167        bytes.push(0); // instruction count = 0
1168
1169        let result = manual_decode::decode_legacy_message(&bytes, Vec::new());
1170        assert!(
1171            result.is_err(),
1172            "header counts inconsistent with account_keys length must be rejected at parse time"
1173        );
1174    }
1175
1176    #[test]
1177    fn decode_legacy_message_rejects_readonly_unsigned_exceeding_unsigned_section() {
1178        // 1 required signature + 2 keys leaves only 1 unsigned account, but this claims 2.
1179        let mut bytes = legacy_message_prefix([1, 0, 2], 2);
1180        bytes.push(0); // instruction count = 0
1181
1182        let result = manual_decode::decode_legacy_message(&bytes, Vec::new());
1183        assert!(
1184            result.is_err(),
1185            "num_readonly_unsigned_accounts exceeding the unsigned section must be rejected"
1186        );
1187    }
1188
1189    #[test]
1190    fn decode_legacy_message_rejects_readonly_signed_exceeding_required_signatures() {
1191        // num_readonly_signed_accounts (2) exceeds num_required_signatures (1).
1192        let mut bytes = legacy_message_prefix([1, 2, 0], 2);
1193        bytes.push(0); // instruction count = 0
1194
1195        let result = manual_decode::decode_legacy_message(&bytes, Vec::new());
1196        assert!(
1197            result.is_err(),
1198            "num_readonly_signed_accounts exceeding num_required_signatures must be rejected"
1199        );
1200    }
1201
1202    #[test]
1203    fn decode_v0_message_rejects_readonly_unsigned_exceeding_unsigned_section() {
1204        // Same malformed shape as the legacy case, but through the V0 decoder.
1205        let mut bytes = legacy_message_prefix([1, 0, 2], 2);
1206        bytes.push(0); // instruction count = 0
1207        bytes.push(0); // address table lookup count = 0
1208
1209        let result = manual_decode::decode_v0_message(&bytes, Vec::new());
1210        assert!(
1211            result.is_err(),
1212            "num_readonly_unsigned_accounts exceeding the unsigned section must be rejected in V0 too"
1213        );
1214    }
1215
1216    #[test]
1217    fn serialize_roundtrip_legacy() {
1218        let data = STANDARD.decode(LEGACY_TX).unwrap();
1219        let tx = VersionedTransaction::deserialize_with_version(&data).unwrap();
1220
1221        let reserialized = tx.serialize().unwrap();
1222        assert_eq!(reserialized, data, "byte-exact roundtrip failed");
1223
1224        let tx2 = VersionedTransaction::deserialize_with_version(&reserialized).unwrap();
1225        assert!(matches!(tx2, VersionedTransaction::Legacy { .. }));
1226        assert_eq!(tx2.get_compute_unit_price(), Some(70_000));
1227        assert_eq!(tx2.get_compute_unit_limit(), Some(420_000));
1228    }
1229
1230    #[test]
1231    fn serialize_roundtrip_v0() {
1232        let data = STANDARD.decode(MAYAN_V0_TX).unwrap();
1233        let tx = VersionedTransaction::deserialize_with_version(&data).unwrap();
1234
1235        let reserialized = tx.serialize().unwrap();
1236        assert_eq!(reserialized, data, "byte-exact roundtrip failed");
1237
1238        let tx2 = VersionedTransaction::deserialize_with_version(&reserialized).unwrap();
1239        assert!(matches!(tx2, VersionedTransaction::V0 { .. }));
1240        assert_eq!(tx2.get_compute_unit_price(), Some(71_428));
1241        assert_eq!(tx2.get_compute_unit_limit(), Some(475_676));
1242    }
1243
1244    #[test]
1245    fn sign_and_roundtrip() {
1246        let mut tx = decode_legacy_tx();
1247        let private_key = [1u8; 32];
1248
1249        let message_bytes = tx.serialize_message().unwrap();
1250        let sig = sign_message(&private_key, &message_bytes).unwrap();
1251        tx.signatures_mut()[0] = sig;
1252
1253        let bytes = tx.serialize().unwrap();
1254        let deserialized = VersionedTransaction::deserialize_with_version(&bytes).unwrap();
1255        assert_eq!(deserialized.signatures()[0], sig);
1256        assert_ne!(deserialized.signatures()[0], SignatureBytes::default());
1257    }
1258}