diff --git a/cli-output/src/display.rs b/cli-output/src/display.rs index c8450dd57..a2f12e034 100644 --- a/cli-output/src/display.rs +++ b/cli-output/src/display.rs @@ -263,10 +263,14 @@ fn write_transaction( write_status(w, &transaction_status.status, prefix)?; write_fees(w, transaction_status.fee, prefix)?; write_balances(w, transaction_status, prefix)?; - write_compute_units_consumed(w, transaction_status.compute_units_consumed, prefix)?; - write_log_messages(w, transaction_status.log_messages.as_ref(), prefix)?; - write_return_data(w, transaction_status.return_data.as_ref(), prefix)?; - write_rewards(w, transaction_status.rewards.as_ref(), prefix)?; + write_compute_units_consumed( + w, + transaction_status.compute_units_consumed.clone().into(), + prefix, + )?; + write_log_messages(w, transaction_status.log_messages.as_ref().into(), prefix)?; + write_return_data(w, transaction_status.return_data.as_ref().into(), prefix)?; + write_rewards(w, transaction_status.rewards.as_ref().into(), prefix)?; } else { writeln!(w, "{}Status: Unavailable", prefix)?; } diff --git a/rpc-client/src/mock_sender.rs b/rpc-client/src/mock_sender.rs index afea96653..346c75278 100644 --- a/rpc-client/src/mock_sender.rs +++ b/rpc-client/src/mock_sender.rs @@ -31,7 +31,8 @@ use { transaction::{self, Transaction, TransactionError, TransactionVersion}, }, solana_transaction_status::{ - EncodedConfirmedBlock, EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction, + option_serializer::OptionSerializer, EncodedConfirmedBlock, + EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction, EncodedTransactionWithStatusMeta, Rewards, TransactionBinaryEncoding, TransactionConfirmationStatus, TransactionStatus, UiCompiledInstruction, UiMessage, UiRawMessage, UiTransaction, UiTransactionStatusMeta, @@ -223,14 +224,14 @@ impl RpcSender for MockSender { fee: 0, pre_balances: vec![499999999999999950, 50, 1], post_balances: vec![499999999999999950, 50, 1], - inner_instructions: None, - log_messages: None, - pre_token_balances: None, - post_token_balances: None, - rewards: None, - loaded_addresses: None, - return_data: None, - compute_units_consumed: None, + inner_instructions: OptionSerializer::None, + log_messages: OptionSerializer::None, + pre_token_balances: OptionSerializer::None, + post_token_balances: OptionSerializer::None, + rewards: OptionSerializer::None, + loaded_addresses: OptionSerializer::Skip, + return_data: OptionSerializer::Skip, + compute_units_consumed: OptionSerializer::Skip, }), }, block_time: Some(1628633791), diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index 5704cdf44..32a0b5e0f 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -3,6 +3,7 @@ pub use {crate::extract_memos::extract_and_fmt_memos, solana_sdk::reward_type::RewardType}; use { crate::{ + option_serializer::OptionSerializer, parse_accounts::{parse_legacy_message_accounts, parse_v0_message_accounts, ParsedAccount}, parse_instruction::{parse, ParsedInstruction}, }, @@ -33,6 +34,7 @@ extern crate lazy_static; extern crate serde_derive; pub mod extract_memos; +pub mod option_serializer; pub mod parse_accounts; pub mod parse_address_lookup_table; pub mod parse_associated_token; @@ -250,10 +252,16 @@ pub struct UiTransactionTokenBalance { pub account_index: u8, pub mint: String, pub ui_token_amount: UiTokenAmount, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub owner: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub program_id: Option, + #[serde( + default = "OptionSerializer::skip", + skip_serializing_if = "OptionSerializer::should_skip" + )] + pub owner: OptionSerializer, + #[serde( + default = "OptionSerializer::skip", + skip_serializing_if = "OptionSerializer::should_skip" + )] + pub program_id: OptionSerializer, } impl From for UiTransactionTokenBalance { @@ -263,14 +271,14 @@ impl From for UiTransactionTokenBalance { mint: token_balance.mint, ui_token_amount: token_balance.ui_token_amount, owner: if !token_balance.owner.is_empty() { - Some(token_balance.owner) + OptionSerializer::Some(token_balance.owner) } else { - None + OptionSerializer::Skip }, program_id: if !token_balance.program_id.is_empty() { - Some(token_balance.program_id) + OptionSerializer::Some(token_balance.program_id) } else { - None + OptionSerializer::Skip }, } } @@ -320,17 +328,46 @@ pub struct UiTransactionStatusMeta { pub fee: u64, pub pre_balances: Vec, pub post_balances: Vec, - pub inner_instructions: Option>, - pub log_messages: Option>, - pub pre_token_balances: Option>, - pub post_token_balances: Option>, - pub rewards: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub loaded_addresses: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub return_data: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub compute_units_consumed: Option, + #[serde( + default = "OptionSerializer::none", + skip_serializing_if = "OptionSerializer::should_skip" + )] + pub inner_instructions: OptionSerializer>, + #[serde( + default = "OptionSerializer::none", + skip_serializing_if = "OptionSerializer::should_skip" + )] + pub log_messages: OptionSerializer>, + #[serde( + default = "OptionSerializer::none", + skip_serializing_if = "OptionSerializer::should_skip" + )] + pub pre_token_balances: OptionSerializer>, + #[serde( + default = "OptionSerializer::none", + skip_serializing_if = "OptionSerializer::should_skip" + )] + pub post_token_balances: OptionSerializer>, + #[serde( + default = "OptionSerializer::none", + skip_serializing_if = "OptionSerializer::should_skip" + )] + pub rewards: OptionSerializer, + #[serde( + default = "OptionSerializer::skip", + skip_serializing_if = "OptionSerializer::should_skip" + )] + pub loaded_addresses: OptionSerializer, + #[serde( + default = "OptionSerializer::skip", + skip_serializing_if = "OptionSerializer::should_skip" + )] + pub return_data: OptionSerializer, + #[serde( + default = "OptionSerializer::skip", + skip_serializing_if = "OptionSerializer::should_skip" + )] + pub compute_units_consumed: OptionSerializer, } /// A duplicate representation of LoadedAddresses @@ -367,22 +404,29 @@ impl UiTransactionStatusMeta { fee: meta.fee, pre_balances: meta.pre_balances, post_balances: meta.post_balances, - inner_instructions: meta.inner_instructions.map(|ixs| { - ixs.into_iter() - .map(|ix| UiInnerInstructions::parse(ix, &account_keys)) - .collect() - }), - log_messages: meta.log_messages, + inner_instructions: meta + .inner_instructions + .map(|ixs| { + ixs.into_iter() + .map(|ix| UiInnerInstructions::parse(ix, &account_keys)) + .collect() + }) + .into(), + log_messages: meta.log_messages.into(), pre_token_balances: meta .pre_token_balances - .map(|balance| balance.into_iter().map(Into::into).collect()), + .map(|balance| balance.into_iter().map(Into::into).collect()) + .into(), post_token_balances: meta .post_token_balances - .map(|balance| balance.into_iter().map(Into::into).collect()), - rewards: if show_rewards { meta.rewards } else { None }, - loaded_addresses: None, - return_data: meta.return_data.map(|return_data| return_data.into()), - compute_units_consumed: meta.compute_units_consumed, + .map(|balance| balance.into_iter().map(Into::into).collect()) + .into(), + rewards: if show_rewards { meta.rewards } else { None }.into(), + loaded_addresses: OptionSerializer::Skip, + return_data: OptionSerializer::or_skip( + meta.return_data.map(|return_data| return_data.into()), + ), + compute_units_consumed: OptionSerializer::or_skip(meta.compute_units_consumed), } } } @@ -397,18 +441,23 @@ impl From for UiTransactionStatusMeta { post_balances: meta.post_balances, inner_instructions: meta .inner_instructions - .map(|ixs| ixs.into_iter().map(Into::into).collect()), - log_messages: meta.log_messages, + .map(|ixs| ixs.into_iter().map(Into::into).collect()) + .into(), + log_messages: meta.log_messages.into(), pre_token_balances: meta .pre_token_balances - .map(|balance| balance.into_iter().map(Into::into).collect()), + .map(|balance| balance.into_iter().map(Into::into).collect()) + .into(), post_token_balances: meta .post_token_balances - .map(|balance| balance.into_iter().map(Into::into).collect()), - rewards: meta.rewards, - loaded_addresses: Some(UiLoadedAddresses::from(&meta.loaded_addresses)), - return_data: meta.return_data.map(|return_data| return_data.into()), - compute_units_consumed: meta.compute_units_consumed, + .map(|balance| balance.into_iter().map(Into::into).collect()) + .into(), + rewards: meta.rewards.into(), + loaded_addresses: Some(UiLoadedAddresses::from(&meta.loaded_addresses)).into(), + return_data: OptionSerializer::or_skip( + meta.return_data.map(|return_data| return_data.into()), + ), + compute_units_consumed: OptionSerializer::or_skip(meta.compute_units_consumed), } } } @@ -723,7 +772,7 @@ impl VersionedTransactionWithStatusMeta { _ => { let mut meta = UiTransactionStatusMeta::from(self.meta); if !show_rewards { - meta.rewards = None; + meta.rewards = OptionSerializer::None; } meta } @@ -1039,6 +1088,15 @@ pub struct UiTransactionReturnData { pub data: (String, UiReturnDataEncoding), } +impl Default for UiTransactionReturnData { + fn default() -> Self { + Self { + program_id: String::default(), + data: (String::default(), UiReturnDataEncoding::Base64), + } + } +} + impl From for UiTransactionReturnData { fn from(return_data: TransactionReturnData) -> Self { Self { @@ -1059,7 +1117,7 @@ pub enum UiReturnDataEncoding { #[cfg(test)] mod test { - use super::*; + use {super::*, serde_json::json}; #[test] fn test_decode_invalid_transaction() { @@ -1153,4 +1211,134 @@ mod test { }; assert!(status.satisfies_commitment(CommitmentConfig::confirmed())); } + + #[test] + fn test_serde_empty_fields() { + fn test_serde<'de, T: serde::Serialize + serde::Deserialize<'de>>( + json_input: &'de str, + expected_json_output: &str, + ) { + let typed_meta: T = serde_json::from_str(json_input).unwrap(); + let reserialized_value = json!(typed_meta); + + let expected_json_output_value: serde_json::Value = + serde_json::from_str(expected_json_output).unwrap(); + assert_eq!(reserialized_value, expected_json_output_value); + } + + let json_input = "{\ + \"err\":null,\ + \"status\":{\"Ok\":null},\ + \"fee\":1234,\ + \"preBalances\":[1,2,3],\ + \"postBalances\":[4,5,6]\ + }"; + let expected_json_output = "{\ + \"err\":null,\ + \"status\":{\"Ok\":null},\ + \"fee\":1234,\ + \"preBalances\":[1,2,3],\ + \"postBalances\":[4,5,6],\ + \"innerInstructions\":null,\ + \"logMessages\":null,\ + \"preTokenBalances\":null,\ + \"postTokenBalances\":null,\ + \"rewards\":null\ + }"; + test_serde::(json_input, expected_json_output); + + let json_input = "{\ + \"accountIndex\":5,\ + \"mint\":\"DXM2yVSouSg1twmQgHLKoSReqXhtUroehWxrTgPmmfWi\",\ + \"uiTokenAmount\": { + \"amount\": \"1\",\ + \"decimals\": 0,\ + \"uiAmount\": 1.0,\ + \"uiAmountString\": \"1\"\ + }\ + }"; + let expected_json_output = "{\ + \"accountIndex\":5,\ + \"mint\":\"DXM2yVSouSg1twmQgHLKoSReqXhtUroehWxrTgPmmfWi\",\ + \"uiTokenAmount\": { + \"amount\": \"1\",\ + \"decimals\": 0,\ + \"uiAmount\": 1.0,\ + \"uiAmountString\": \"1\"\ + }\ + }"; + test_serde::(json_input, expected_json_output); + } + + #[test] + fn test_ui_transaction_status_meta_ctors_serialization() { + let meta = TransactionStatusMeta { + status: Ok(()), + fee: 1234, + pre_balances: vec![1, 2, 3], + post_balances: vec![4, 5, 6], + inner_instructions: None, + log_messages: None, + pre_token_balances: None, + post_token_balances: None, + rewards: None, + loaded_addresses: LoadedAddresses { + writable: vec![], + readonly: vec![], + }, + return_data: None, + compute_units_consumed: None, + }; + let expected_json_output_value: serde_json::Value = serde_json::from_str( + "{\ + \"err\":null,\ + \"status\":{\"Ok\":null},\ + \"fee\":1234,\ + \"preBalances\":[1,2,3],\ + \"postBalances\":[4,5,6],\ + \"innerInstructions\":null,\ + \"logMessages\":null,\ + \"preTokenBalances\":null,\ + \"postTokenBalances\":null,\ + \"rewards\":null,\ + \"loadedAddresses\":{\ + \"readonly\": [],\ + \"writable\": []\ + }\ + }", + ) + .unwrap(); + let ui_meta_from: UiTransactionStatusMeta = meta.clone().into(); + assert_eq!( + serde_json::to_value(&ui_meta_from).unwrap(), + expected_json_output_value + ); + + let expected_json_output_value: serde_json::Value = serde_json::from_str( + "{\ + \"err\":null,\ + \"status\":{\"Ok\":null},\ + \"fee\":1234,\ + \"preBalances\":[1,2,3],\ + \"postBalances\":[4,5,6],\ + \"innerInstructions\":null,\ + \"logMessages\":null,\ + \"preTokenBalances\":null,\ + \"postTokenBalances\":null,\ + \"rewards\":null\ + }", + ) + .unwrap(); + let ui_meta_parse_with_rewards = UiTransactionStatusMeta::parse(meta.clone(), &[], true); + assert_eq!( + serde_json::to_value(&ui_meta_parse_with_rewards).unwrap(), + expected_json_output_value + ); + + let ui_meta_parse_no_rewards = UiTransactionStatusMeta::parse(meta, &[], false); + assert_eq!( + serde_json::to_value(&ui_meta_parse_no_rewards).unwrap(), + expected_json_output_value + ); + } } diff --git a/transaction-status/src/option_serializer.rs b/transaction-status/src/option_serializer.rs new file mode 100644 index 000000000..ca77c7304 --- /dev/null +++ b/transaction-status/src/option_serializer.rs @@ -0,0 +1,77 @@ +use serde::{ser::Error, Deserialize, Deserializer, Serialize, Serializer}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum OptionSerializer { + Some(T), + None, + Skip, +} + +impl OptionSerializer { + pub fn none() -> Self { + Self::None + } + + pub fn skip() -> Self { + Self::Skip + } + + pub fn should_skip(&self) -> bool { + matches!(self, Self::Skip) + } + + pub fn or_skip(option: Option) -> Self { + match option { + Option::Some(item) => Self::Some(item), + Option::None => Self::Skip, + } + } + + pub fn as_ref(&self) -> OptionSerializer<&T> { + match self { + OptionSerializer::Some(item) => OptionSerializer::Some(item), + OptionSerializer::None => OptionSerializer::None, + OptionSerializer::Skip => OptionSerializer::Skip, + } + } +} + +impl From> for OptionSerializer { + fn from(option: Option) -> Self { + match option { + Option::Some(item) => Self::Some(item), + Option::None => Self::None, + } + } +} + +impl From> for Option { + fn from(option: OptionSerializer) -> Self { + match option { + OptionSerializer::Some(item) => Self::Some(item), + _ => Self::None, + } + } +} + +impl Serialize for OptionSerializer { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Self::Some(item) => item.serialize(serializer), + Self::None => serializer.serialize_none(), + Self::Skip => Err(Error::custom("Skip variants should not be serialized")), + } + } +} + +impl<'de, T: Deserialize<'de>> Deserialize<'de> for OptionSerializer { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Option::deserialize(deserializer).map(Into::into) + } +}