solana_block_decoder/transaction/
versioned_transaction.rs

1use {
2    crate::{
3        errors::{
4            decode_error::DecodeError,
5        },
6        block::{
7            encoded_block::{
8                EncodedTransaction,
9                EncodedTransactionWithStatusMeta,
10            },
11        },
12        message::{
13            message::Message,
14            message_v0::Message as MessageV0,
15            versioned_message::VersionedMessage,
16        },
17        transaction::{
18            tx_status_meta::TransactionStatusMeta,
19        },
20        decodable::{
21            Decodable,
22            DecodableWithMeta,
23        },
24    },
25    serde::{
26        Deserialize, Serialize,
27    },
28    solana_short_vec as short_vec,
29    solana_signature::Signature,
30    solana_transaction::{
31        versioned::TransactionVersion
32    },
33    solana_transaction_status_client_types::{
34        UiMessage,
35        UiTransactionEncoding,
36    },
37    base64::{Engine, prelude::BASE64_STANDARD},
38};
39
40#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
41pub struct VersionedTransactionWithStatusMeta {
42    pub transaction: VersionedTransaction,
43    pub meta: TransactionStatusMeta,
44}
45
46impl VersionedTransactionWithStatusMeta {
47    pub fn decode(
48        encoded: EncodedTransactionWithStatusMeta,
49        encoding: UiTransactionEncoding,
50    ) -> Result<Self, DecodeError> {
51        // Decoding the transaction
52        let transaction = match VersionedTransaction::decode_with_meta(encoded.transaction, encoding, encoded.version /*, meta*/) {
53            Ok(decoded) => decoded,
54            Err(e) => return Err(e),
55        };
56
57        // Decoding the meta
58        let meta = match encoded.meta {
59            Some(ui_meta) => match TransactionStatusMeta::try_from(ui_meta) {
60                Ok(meta) => meta,
61                Err(_) => return Err(DecodeError::InvalidData),
62            },
63            None => return Err(DecodeError::InvalidData),
64        };
65
66        Ok(Self {
67            transaction,
68            meta,
69        })
70    }
71}
72
73#[derive(Debug, PartialEq, Default, Eq, Clone, Serialize, Deserialize)]
74pub struct VersionedTransaction {
75    /// List of signatures
76    #[serde(with = "short_vec")]
77    pub signatures: Vec<Signature>,
78    /// Message to sign.
79    pub message: VersionedMessage,
80}
81
82
83impl DecodableWithMeta for VersionedTransaction {
84    type Encoded = EncodedTransaction;
85    type Decoded = VersionedTransaction;
86
87    fn decode_with_meta(
88        encoded: Self::Encoded,
89        decoding: UiTransactionEncoding,
90        version: Option<TransactionVersion>
91    ) -> Result<Self::Decoded, DecodeError> {
92        match decoding {
93            UiTransactionEncoding::Binary | UiTransactionEncoding::Base58 => {
94                if let EncodedTransaction::LegacyBinary(encoded_string) = encoded {
95                    let decoded_bytes = bs58::decode(encoded_string).into_vec().unwrap();
96                    let decoded: Self::Decoded =
97                        bincode::deserialize(&decoded_bytes).map_err(|_| DecodeError::DeserializeFailed)?;
98                    Ok(decoded)
99                } else {
100                    Err(DecodeError::UnsupportedEncoding)
101                }
102            }
103            UiTransactionEncoding::Base64 => {
104                if let EncodedTransaction::Binary(encoded_string, _) = encoded {
105                    let decoded_bytes = BASE64_STANDARD.decode(encoded_string).unwrap();
106                    let decoded: Self::Decoded =
107                        bincode::deserialize(&decoded_bytes).map_err(|_| DecodeError::DeserializeFailed)?;
108                    Ok(decoded)
109                } else {
110                    Err(DecodeError::UnsupportedEncoding)
111                }
112            }
113            UiTransactionEncoding::Json => Self::json_decode(encoded, version),
114            UiTransactionEncoding::JsonParsed => Err(DecodeError::UnsupportedEncoding),
115        }
116    }
117
118    fn json_decode(encoded: Self::Encoded, version: Option<TransactionVersion>) -> Result<Self::Decoded, DecodeError> {
119        if let EncodedTransaction::Json(ui_transaction) = encoded {
120            let signatures = ui_transaction
121                .signatures
122                .iter()
123                .map(|s| s.parse::<Signature>())
124                .collect::<Result<Vec<_>, _>>()
125                .map_err(|err| DecodeError::ParseSignatureFailed(err))?;
126
127            let message = match ui_transaction.message {
128                UiMessage::Raw(_) => {
129                    match version {
130                        Some(TransactionVersion::Number(0)) => {
131                            // Handle Version 0 message decoding for raw messages
132                            let v0_message = MessageV0::json_decode(ui_transaction.message, version)?;
133                            VersionedMessage::V0(v0_message)
134                        }
135                        Some(TransactionVersion::Legacy(_)) | None => {
136                            // Default to legacy message decoding for raw messages
137                            let legacy_message = Message::decode(&ui_transaction.message)?;
138                            VersionedMessage::Legacy(legacy_message)
139                        }
140                        // Add additional cases here for other versions as needed
141                        _ => {
142                            // Handle other versions or return an error if not supported
143                            return Err(DecodeError::UnsupportedVersion);
144                        }
145                    }
146                }
147                UiMessage::Parsed(_) => {
148                    return Err(DecodeError::UnsupportedEncoding);
149                }
150            };
151
152            Ok(Self {
153                signatures,
154                message,
155            })
156        } else {
157            Err(DecodeError::UnsupportedEncoding)
158        }
159    }
160}
161
162impl From<VersionedTransaction> for solana_transaction::versioned::VersionedTransaction {
163    fn from(tx: VersionedTransaction) -> Self {
164        Self {
165            signatures: tx.signatures,
166            message: tx.message.into(),
167        }
168    }
169}
170
171impl From<VersionedTransactionWithStatusMeta> for solana_transaction_status::VersionedTransactionWithStatusMeta {
172    fn from(tx: VersionedTransactionWithStatusMeta) -> Self {
173        Self {
174            transaction: tx.transaction.into(),
175            meta: tx.meta.into(),
176        }
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use std::str::FromStr;
184
185    use solana_transaction_status_client_types::{
186        UiTransactionEncoding, UiTransaction, UiMessage, UiRawMessage,
187        UiTransactionStatusMeta,
188    };
189    use solana_transaction_status::TransactionBinaryEncoding;
190    use serde_json::json;
191    use solana_signature::SIGNATURE_BYTES;
192
193    fn make_valid_ui_meta() -> UiTransactionStatusMeta {
194        serde_json::from_value(json!({
195            "status": { "Ok": null },
196            "fee": 0,
197            "preBalances": [100u64, 50u64],
198            "postBalances": [90u64, 50u64],
199            "rewards": [],
200            "logMessages": [],
201            "innerInstructions": [],
202            "preTokenBalances": null,
203            "postTokenBalances": null,
204            "loadedAddresses": { "readonly": [], "writable": [] },
205            "returnData": null,
206            "computeUnitsConsumed": 0
207        })).expect("Failed to deserialize a valid UiTransactionStatusMeta")
208    }
209
210    fn make_invalid_ui_meta() -> UiTransactionStatusMeta {
211        serde_json::from_value(json!({
212            "invalid": "meta"
213        })).expect("Unexpectedly parsed 'invalid' meta successfully")
214    }
215
216    #[test]
217    fn test_decode_versioned_transaction_with_status_meta_success() {
218        // 1. Create a valid VersionedTransaction
219        let valid_transaction = VersionedTransaction {
220            signatures: vec![Signature::default()],
221            message: VersionedMessage::Legacy(Message::default()),
222        };
223
224        // 2. Serialize it correctly before encoding to Base64
225        let raw_bytes = bincode::serialize(&valid_transaction)
226            .expect("Failed to serialize VersionedTransaction");
227
228        let encoded_string = base64::engine::general_purpose::STANDARD.encode(&raw_bytes);
229
230        println!("Base64 Encoded Transaction: {}", encoded_string);  // Debugging output
231
232        // 3. Create a correctly formatted EncodedTransaction
233        let encoded_tx = EncodedTransaction::Binary(
234            encoded_string,
235            TransactionBinaryEncoding::Base64,
236        );
237
238        // 4. Construct a valid `UiTransactionStatusMeta`
239        let meta_json = json!({
240            "status": { "Ok": null },
241            "fee": 0,
242            "preBalances": [100u64, 50u64],
243            "postBalances": [90u64, 50u64],
244            "rewards": [],
245            "logMessages": [],
246            "innerInstructions": [],
247            "preTokenBalances": null,
248            "postTokenBalances": null,
249            "loadedAddresses": { "readonly": [], "writable": [] },
250            "returnData": null,
251            "computeUnitsConsumed": 0
252        });
253
254        println!("Meta JSON: {}", meta_json.to_string());  // Debugging output
255
256        let encoded_meta = Some(serde_json::from_value(meta_json)
257            .expect("Failed to create valid meta"));
258
259        println!("Parsed Meta: {:?}", encoded_meta); // Debugging output
260
261        let encoded = EncodedTransactionWithStatusMeta {
262            transaction: encoded_tx,
263            meta: encoded_meta,
264            version: None,
265        };
266
267        // 5. Decode the transaction and meta
268        let result = dbg!(VersionedTransactionWithStatusMeta::decode(encoded, UiTransactionEncoding::Base64));
269
270        // Ensure decoding succeeds
271        assert!(result.is_ok(), "Expected successful decoding, but got {:?}", result);
272    }
273
274    #[test]
275    fn test_decode_versioned_transaction_with_status_meta_invalid_data() {
276        // 1. Create a valid Base64-encoded transaction
277        let valid_transaction = VersionedTransaction {
278            signatures: vec![Signature::default()],  // Valid 64-byte signature
279            message: VersionedMessage::Legacy(Message::default()), // Valid structure
280        };
281
282        let raw_bytes = bincode::serialize(&valid_transaction)
283            .expect("Failed to serialize VersionedTransaction");
284
285        let encoded_string = base64::engine::general_purpose::STANDARD.encode(&raw_bytes);
286
287        let encoded_tx = EncodedTransaction::Binary(
288            encoded_string,
289            TransactionBinaryEncoding::Base64,
290        );
291
292        // 2. Construct an invalid `UiTransactionStatusMeta`
293        let encoded_meta = Some(serde_json::from_value(json!({
294            "status": { "Ok": null },
295            "fee": u64::MAX,
296            "preBalances": [],
297            "postBalances": [],
298            "rewards": [],
299            "logMessages": None::<Vec<String>>,
300            "innerInstructions": None::<Vec<String>>
301        })).expect("Failed to create invalid meta"));
302
303        let encoded = EncodedTransactionWithStatusMeta {
304            transaction: encoded_tx,
305            meta: encoded_meta,
306            version: None,
307        };
308
309        // 3. Attempt to decode
310        let result = VersionedTransactionWithStatusMeta::decode(encoded, UiTransactionEncoding::Base64);
311
312        // 4. Ensure decoding fails
313        match result {
314            Err(DecodeError::InvalidData) => {}
315            Ok(_) => panic!("Expected InvalidData error, but decoding succeeded!"),
316            Err(e) => panic!("Expected InvalidData error, got {:?}", e),
317        }
318    }
319
320    #[test]
321    fn test_decode_versioned_transaction_with_status_meta_no_meta() {
322        let valid_transaction = VersionedTransaction {
323            signatures: vec![Signature::default()],
324            message: VersionedMessage::Legacy(Message::default()),
325        };
326
327        let raw_bytes = bincode::serialize(&valid_transaction).unwrap();
328        let encoded_string = base64::engine::general_purpose::STANDARD.encode(&raw_bytes);
329        let encoded_tx = EncodedTransaction::Binary(encoded_string, TransactionBinaryEncoding::Base64);
330
331        let encoded = EncodedTransactionWithStatusMeta {
332            transaction: encoded_tx,
333            meta: None,
334            version: None,
335        };
336
337        let result = VersionedTransactionWithStatusMeta::decode(encoded, UiTransactionEncoding::Base64);
338        assert!(matches!(result, Err(DecodeError::InvalidData)));
339    }
340
341    #[test]
342    fn test_from_versioned_transaction() {
343        // Generate a valid base58-encoded 64-byte signature
344        let valid_signature_bytes = [0u8; SIGNATURE_BYTES]; // 64 bytes of zeros
345        let valid_signature_str = bs58::encode(valid_signature_bytes).into_string();
346
347        // Ensure that the signature is correctly formatted
348        let signature = Signature::from_str(&valid_signature_str)
349            .expect("Failed to parse valid base58 signature");
350
351        let tx = VersionedTransaction {
352            signatures: vec![signature],
353            message: VersionedMessage::Legacy(Message::default()),
354        };
355
356        let converted: solana_transaction::versioned::VersionedTransaction = tx.into();
357
358        // Ensure the number of signatures is correct
359        assert_eq!(converted.signatures.len(), 1);
360    }
361
362
363
364    //
365    // Encoding: Binary | Base58
366    //
367
368    #[test]
369    fn test_decode_versioned_transaction_success_legacy_binary() {
370        // Serialize a valid VersionedTransaction
371        let raw_bytes = bincode::serialize(&VersionedTransaction {
372            signatures: vec![Signature::default()],
373            message: VersionedMessage::Legacy(Message::default()),
374        }).expect("Failed to serialize VersionedTransaction");
375
376        // Correctly encode it into base58
377        let encoded_string = bs58::encode(&raw_bytes).into_string();
378
379        // Ensure we use the correct encoding variant
380        let encoded = EncodedTransaction::LegacyBinary(encoded_string);
381
382        let result = VersionedTransaction::decode_with_meta(
383            encoded,
384            UiTransactionEncoding::Base58,
385            None,
386        );
387
388        // Ensure that decoding works as expected
389        assert!(result.is_ok(), "Expected successful decoding, but got {:?}", result);
390    }
391
392    #[test]
393    fn test_decode_versioned_transaction_invalid_base58_format() {
394        let encoded = EncodedTransaction::LegacyBinary("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".to_string());
395        let result = VersionedTransaction::decode_with_meta(encoded, UiTransactionEncoding::Base58, None);
396        assert!(matches!(result, Err(DecodeError::DeserializeFailed)));
397    }
398
399    #[test]
400    fn test_decode_versioned_transaction_invalid_legacy_binary() {
401        // Provide an invalid base58 string
402        let encoded = EncodedTransaction::Binary(
403            "invalid-base58".to_string(),
404            TransactionBinaryEncoding::Base58,
405        );
406
407        let result = VersionedTransaction::decode_with_meta(
408            encoded,
409            UiTransactionEncoding::Base58,
410            None,
411        );
412
413        // Assert that decoding fails
414        assert!(result.is_err(), "Expected decoding to fail for invalid encoding type");
415    }
416
417
418    //
419    // Encoding: Base64
420    //
421
422    #[test]
423    fn test_decode_versioned_transaction_success_base64() {
424        // bincode-serialize a simple VersionedTransaction
425        let raw_bytes = bincode::serialize(&VersionedTransaction {
426            signatures: vec![Signature::default()],
427            message: VersionedMessage::Legacy(Message::default()),
428        }).unwrap();
429
430        let encoded_string = base64::engine::general_purpose::STANDARD.encode(&raw_bytes);
431
432        let encoded = EncodedTransaction::Binary(
433            encoded_string,
434            TransactionBinaryEncoding::Base64,
435        );
436
437        let result = VersionedTransaction::decode_with_meta(
438            encoded,
439            UiTransactionEncoding::Base64,
440            None,
441        );
442
443        assert!(result.is_ok());
444    }
445
446    #[test]
447    fn test_decode_versioned_transaction_success_base64_v0() {
448        // bincode-serialize a simple VersionedTransaction
449        let raw_bytes = bincode::serialize(&VersionedTransaction {
450            signatures: vec![Signature::default()],
451            message: VersionedMessage::Legacy(Message::default()),
452        })
453            .unwrap();
454
455        let encoded_string = base64::engine::general_purpose::STANDARD.encode(&raw_bytes);
456
457        let encoded = EncodedTransaction::Binary(
458            encoded_string,
459            TransactionBinaryEncoding::Base64,
460        );
461
462        let result = VersionedTransaction::decode_with_meta(
463            encoded,
464            UiTransactionEncoding::Base64,
465            Some(TransactionVersion::Number(0)),
466        );
467
468        assert!(result.is_ok());
469    }
470
471    #[test]
472    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidByte")]
473    fn test_decode_versioned_transaction_invalid_base64() {
474        // Provide an invalid base64 string
475        let encoded = EncodedTransaction::Binary(
476            "invalid-base64".to_string(),
477            TransactionBinaryEncoding::Base64,
478        );
479
480        // Since `BASE64_STANDARD.decode()` fails immediately, the function panics.
481        // This test should confirm that it panics as expected.
482        let _ = VersionedTransaction::decode_with_meta(
483            encoded,
484            UiTransactionEncoding::Base64,
485            None,
486        );
487    }
488
489    #[test]
490    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidByte")]
491    fn test_decode_versioned_transaction_invalid_base64_v0() {
492        // Provide an invalid base64 string
493        let encoded = EncodedTransaction::Binary(
494            "invalid-base64".to_string(),
495            TransactionBinaryEncoding::Base64,
496        );
497
498        // Since `BASE64_STANDARD.decode()` fails immediately, the function panics.
499        // This test should confirm that it panics as expected.
500        let _ = VersionedTransaction::decode_with_meta(
501            encoded,
502            UiTransactionEncoding::Base64,
503            Some(TransactionVersion::Number(0)),
504        );
505    }
506
507    #[test]
508    fn test_decode_versioned_transaction_invalid_base64_format() {
509        let encoded = EncodedTransaction::Binary("aGVsbG8gd29ybGQ=".to_string(), TransactionBinaryEncoding::Base64);
510        let result = VersionedTransaction::decode_with_meta(encoded, UiTransactionEncoding::Base64, None);
511        assert!(matches!(result, Err(DecodeError::DeserializeFailed)));
512    }
513
514
515    //
516    // Encoding: Json
517    //
518
519    #[test]
520    fn test_decode_versioned_transaction_json_invalid_signature() {
521        let ui_transaction = UiTransaction {
522            signatures: vec!["invalid-signature".to_string()],
523            message: UiMessage::Raw(serde_json::from_value(json!({
524                "header": {
525                    "numRequiredSignatures": 0,
526                    "numReadonlySignedAccounts": 0,
527                    "numReadonlyUnsignedAccounts": 0
528                },
529                "accountKeys": [],
530                "instructions": [],
531                "recentBlockhash": "some-blockhash"
532            })).unwrap()),
533        };
534
535        let json_encoded = EncodedTransaction::Json(ui_transaction);
536        let result = VersionedTransaction::decode_with_meta(json_encoded, UiTransactionEncoding::Json, None);
537        assert!(matches!(result, Err(DecodeError::ParseSignatureFailed(_))));
538    }
539
540    #[test]
541    fn test_decode_versioned_transaction_json_invalid_version() {
542        let ui_transaction = UiTransaction {
543            signatures: vec![Signature::default().to_string()],
544            message: UiMessage::Raw(serde_json::from_value(json!({
545                "header": {
546                    "numRequiredSignatures": 0,
547                    "numReadonlySignedAccounts": 0,
548                    "numReadonlyUnsignedAccounts": 0
549                },
550                "accountKeys": [],
551                "instructions": [],
552                "recentBlockhash": "some-blockhash"
553            })).unwrap()),
554        };
555
556        let json_encoded = EncodedTransaction::Json(ui_transaction);
557        let result = VersionedTransaction::decode_with_meta(
558            json_encoded,
559            UiTransactionEncoding::Json,
560            Some(TransactionVersion::Number(99))
561        );
562        assert!(matches!(result, Err(DecodeError::UnsupportedVersion)));
563    }
564
565    #[test]
566    fn test_decode_versioned_transaction_invalid_json_encoding() {
567        let ui_transaction = UiTransaction {
568            signatures: vec![Signature::default().to_string()],
569            message: UiMessage::Parsed(serde_json::from_value(json!({
570                "accountKeys": [],
571                "recentBlockhash": "some-blockhash",
572                "instructions": []
573            })).unwrap()),
574        };
575
576        let json_encoded = EncodedTransaction::Json(ui_transaction);
577        let result = VersionedTransaction::decode_with_meta(
578            json_encoded,
579            UiTransactionEncoding::JsonParsed,
580            None
581        );
582        assert!(matches!(result, Err(DecodeError::UnsupportedEncoding)));
583    }
584
585    #[test]
586    fn test_decode_versioned_transaction_json() {
587        let ui_transaction = UiTransaction {
588            signatures: vec!["signature".to_string()],
589            message: UiMessage::Raw(
590                serde_json::from_value(json!({
591                    "header": {
592                        "numRequiredSignatures": 0,
593                        "numReadonlySignedAccounts": 0,
594                        "numReadonlyUnsignedAccounts": 0
595                    },
596                    "accountKeys": [],
597                    "instructions": [],
598                    "recentBlockhash": "some-blockhash"
599                })).unwrap()
600            ),
601        };
602
603        let json_encoded = EncodedTransaction::Json(ui_transaction);
604
605        let result = VersionedTransaction::decode_with_meta(
606            json_encoded,
607            UiTransactionEncoding::Json,
608            Some(TransactionVersion::Number(0)),
609        );
610
611        assert!(result.is_err());
612    }
613
614
615    //
616    // Encoding: JsonParsed
617    //
618
619    #[test]
620    fn test_decode_versioned_transaction_unsupported_encoding() {
621        // Construct a valid JSON-encoded transaction
622        let ui_transaction = UiTransaction {
623            signatures: vec!["signature".to_string()],
624            message: UiMessage::Parsed(serde_json::from_value(json!({
625                "accountKeys": [],
626                "recentBlockhash": "some-blockhash",
627                "instructions": []
628            })).expect("Failed to construct valid UiMessage::Parsed")),
629        };
630
631        let json_encoded = EncodedTransaction::Json(ui_transaction);
632
633        let result = VersionedTransaction::decode_with_meta(
634            json_encoded,
635            UiTransactionEncoding::JsonParsed, // Using JsonParsed to trigger unsupported error
636            None,
637        );
638
639        // Ensure decoding fails with UnsupportedEncoding
640        assert!(result.is_err(), "Expected decoding to fail with UnsupportedEncoding");
641    }
642
643    #[test]
644    fn test_decode_versioned_transaction_unsupported_encoding_variant() {
645        let encoded = EncodedTransaction::Binary("invalid".to_string(), TransactionBinaryEncoding::Base64);
646        let result = VersionedTransaction::decode_with_meta(
647            encoded,
648            UiTransactionEncoding::JsonParsed,
649            None
650        );
651        assert!(matches!(result, Err(DecodeError::UnsupportedEncoding)));
652    }
653}