Add serialization-control enum for RPC Options (#27676)

* Add test to verify output when deserializing/reserializing empty fields

* Add test to verify serialization from UiTransactionStatusMeta constructors

* Add OptionSerializer

* Use OptionSerializer for inner_instructions

* Use OptionSerializer for loaded_addresses

* Remove Default variant, use into instead

* Add as_ref implementation

* Use OptionSerializer for log_messages and rewards

* Use OptionSerializer for token_balances

* Use OptionSerializer for return_data

* Use OptionSerializer for compute_units_consumed
This commit is contained in:
Tyera Eulberg 2022-09-14 15:58:50 -07:00 committed by GitHub
parent 6a7fbf7aba
commit 360ca07a40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 324 additions and 54 deletions

View File

@ -263,10 +263,14 @@ fn write_transaction<W: io::Write>(
write_status(w, &transaction_status.status, prefix)?; write_status(w, &transaction_status.status, prefix)?;
write_fees(w, transaction_status.fee, prefix)?; write_fees(w, transaction_status.fee, prefix)?;
write_balances(w, transaction_status, prefix)?; write_balances(w, transaction_status, prefix)?;
write_compute_units_consumed(w, transaction_status.compute_units_consumed, prefix)?; write_compute_units_consumed(
write_log_messages(w, transaction_status.log_messages.as_ref(), prefix)?; w,
write_return_data(w, transaction_status.return_data.as_ref(), prefix)?; transaction_status.compute_units_consumed.clone().into(),
write_rewards(w, transaction_status.rewards.as_ref(), prefix)?; 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 { } else {
writeln!(w, "{}Status: Unavailable", prefix)?; writeln!(w, "{}Status: Unavailable", prefix)?;
} }

View File

@ -31,7 +31,8 @@ use {
transaction::{self, Transaction, TransactionError, TransactionVersion}, transaction::{self, Transaction, TransactionError, TransactionVersion},
}, },
solana_transaction_status::{ solana_transaction_status::{
EncodedConfirmedBlock, EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction, option_serializer::OptionSerializer, EncodedConfirmedBlock,
EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction,
EncodedTransactionWithStatusMeta, Rewards, TransactionBinaryEncoding, EncodedTransactionWithStatusMeta, Rewards, TransactionBinaryEncoding,
TransactionConfirmationStatus, TransactionStatus, UiCompiledInstruction, UiMessage, TransactionConfirmationStatus, TransactionStatus, UiCompiledInstruction, UiMessage,
UiRawMessage, UiTransaction, UiTransactionStatusMeta, UiRawMessage, UiTransaction, UiTransactionStatusMeta,
@ -223,14 +224,14 @@ impl RpcSender for MockSender {
fee: 0, fee: 0,
pre_balances: vec![499999999999999950, 50, 1], pre_balances: vec![499999999999999950, 50, 1],
post_balances: vec![499999999999999950, 50, 1], post_balances: vec![499999999999999950, 50, 1],
inner_instructions: None, inner_instructions: OptionSerializer::None,
log_messages: None, log_messages: OptionSerializer::None,
pre_token_balances: None, pre_token_balances: OptionSerializer::None,
post_token_balances: None, post_token_balances: OptionSerializer::None,
rewards: None, rewards: OptionSerializer::None,
loaded_addresses: None, loaded_addresses: OptionSerializer::Skip,
return_data: None, return_data: OptionSerializer::Skip,
compute_units_consumed: None, compute_units_consumed: OptionSerializer::Skip,
}), }),
}, },
block_time: Some(1628633791), block_time: Some(1628633791),

View File

@ -3,6 +3,7 @@
pub use {crate::extract_memos::extract_and_fmt_memos, solana_sdk::reward_type::RewardType}; pub use {crate::extract_memos::extract_and_fmt_memos, solana_sdk::reward_type::RewardType};
use { use {
crate::{ crate::{
option_serializer::OptionSerializer,
parse_accounts::{parse_legacy_message_accounts, parse_v0_message_accounts, ParsedAccount}, parse_accounts::{parse_legacy_message_accounts, parse_v0_message_accounts, ParsedAccount},
parse_instruction::{parse, ParsedInstruction}, parse_instruction::{parse, ParsedInstruction},
}, },
@ -33,6 +34,7 @@ extern crate lazy_static;
extern crate serde_derive; extern crate serde_derive;
pub mod extract_memos; pub mod extract_memos;
pub mod option_serializer;
pub mod parse_accounts; pub mod parse_accounts;
pub mod parse_address_lookup_table; pub mod parse_address_lookup_table;
pub mod parse_associated_token; pub mod parse_associated_token;
@ -250,10 +252,16 @@ pub struct UiTransactionTokenBalance {
pub account_index: u8, pub account_index: u8,
pub mint: String, pub mint: String,
pub ui_token_amount: UiTokenAmount, pub ui_token_amount: UiTokenAmount,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(
pub owner: Option<String>, default = "OptionSerializer::skip",
#[serde(default, skip_serializing_if = "Option::is_none")] skip_serializing_if = "OptionSerializer::should_skip"
pub program_id: Option<String>, )]
pub owner: OptionSerializer<String>,
#[serde(
default = "OptionSerializer::skip",
skip_serializing_if = "OptionSerializer::should_skip"
)]
pub program_id: OptionSerializer<String>,
} }
impl From<TransactionTokenBalance> for UiTransactionTokenBalance { impl From<TransactionTokenBalance> for UiTransactionTokenBalance {
@ -263,14 +271,14 @@ impl From<TransactionTokenBalance> for UiTransactionTokenBalance {
mint: token_balance.mint, mint: token_balance.mint,
ui_token_amount: token_balance.ui_token_amount, ui_token_amount: token_balance.ui_token_amount,
owner: if !token_balance.owner.is_empty() { owner: if !token_balance.owner.is_empty() {
Some(token_balance.owner) OptionSerializer::Some(token_balance.owner)
} else { } else {
None OptionSerializer::Skip
}, },
program_id: if !token_balance.program_id.is_empty() { program_id: if !token_balance.program_id.is_empty() {
Some(token_balance.program_id) OptionSerializer::Some(token_balance.program_id)
} else { } else {
None OptionSerializer::Skip
}, },
} }
} }
@ -320,17 +328,46 @@ pub struct UiTransactionStatusMeta {
pub fee: u64, pub fee: u64,
pub pre_balances: Vec<u64>, pub pre_balances: Vec<u64>,
pub post_balances: Vec<u64>, pub post_balances: Vec<u64>,
pub inner_instructions: Option<Vec<UiInnerInstructions>>, #[serde(
pub log_messages: Option<Vec<String>>, default = "OptionSerializer::none",
pub pre_token_balances: Option<Vec<UiTransactionTokenBalance>>, skip_serializing_if = "OptionSerializer::should_skip"
pub post_token_balances: Option<Vec<UiTransactionTokenBalance>>, )]
pub rewards: Option<Rewards>, pub inner_instructions: OptionSerializer<Vec<UiInnerInstructions>>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(
pub loaded_addresses: Option<UiLoadedAddresses>, default = "OptionSerializer::none",
#[serde(default, skip_serializing_if = "Option::is_none")] skip_serializing_if = "OptionSerializer::should_skip"
pub return_data: Option<UiTransactionReturnData>, )]
#[serde(default, skip_serializing_if = "Option::is_none")] pub log_messages: OptionSerializer<Vec<String>>,
pub compute_units_consumed: Option<u64>, #[serde(
default = "OptionSerializer::none",
skip_serializing_if = "OptionSerializer::should_skip"
)]
pub pre_token_balances: OptionSerializer<Vec<UiTransactionTokenBalance>>,
#[serde(
default = "OptionSerializer::none",
skip_serializing_if = "OptionSerializer::should_skip"
)]
pub post_token_balances: OptionSerializer<Vec<UiTransactionTokenBalance>>,
#[serde(
default = "OptionSerializer::none",
skip_serializing_if = "OptionSerializer::should_skip"
)]
pub rewards: OptionSerializer<Rewards>,
#[serde(
default = "OptionSerializer::skip",
skip_serializing_if = "OptionSerializer::should_skip"
)]
pub loaded_addresses: OptionSerializer<UiLoadedAddresses>,
#[serde(
default = "OptionSerializer::skip",
skip_serializing_if = "OptionSerializer::should_skip"
)]
pub return_data: OptionSerializer<UiTransactionReturnData>,
#[serde(
default = "OptionSerializer::skip",
skip_serializing_if = "OptionSerializer::should_skip"
)]
pub compute_units_consumed: OptionSerializer<u64>,
} }
/// A duplicate representation of LoadedAddresses /// A duplicate representation of LoadedAddresses
@ -367,22 +404,29 @@ impl UiTransactionStatusMeta {
fee: meta.fee, fee: meta.fee,
pre_balances: meta.pre_balances, pre_balances: meta.pre_balances,
post_balances: meta.post_balances, post_balances: meta.post_balances,
inner_instructions: meta.inner_instructions.map(|ixs| { inner_instructions: meta
ixs.into_iter() .inner_instructions
.map(|ix| UiInnerInstructions::parse(ix, &account_keys)) .map(|ixs| {
.collect() ixs.into_iter()
}), .map(|ix| UiInnerInstructions::parse(ix, &account_keys))
log_messages: meta.log_messages, .collect()
})
.into(),
log_messages: meta.log_messages.into(),
pre_token_balances: meta pre_token_balances: meta
.pre_token_balances .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: meta
.post_token_balances .post_token_balances
.map(|balance| balance.into_iter().map(Into::into).collect()), .map(|balance| balance.into_iter().map(Into::into).collect())
rewards: if show_rewards { meta.rewards } else { None }, .into(),
loaded_addresses: None, rewards: if show_rewards { meta.rewards } else { None }.into(),
return_data: meta.return_data.map(|return_data| return_data.into()), loaded_addresses: OptionSerializer::Skip,
compute_units_consumed: meta.compute_units_consumed, 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<TransactionStatusMeta> for UiTransactionStatusMeta {
post_balances: meta.post_balances, post_balances: meta.post_balances,
inner_instructions: meta inner_instructions: meta
.inner_instructions .inner_instructions
.map(|ixs| ixs.into_iter().map(Into::into).collect()), .map(|ixs| ixs.into_iter().map(Into::into).collect())
log_messages: meta.log_messages, .into(),
log_messages: meta.log_messages.into(),
pre_token_balances: meta pre_token_balances: meta
.pre_token_balances .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: meta
.post_token_balances .post_token_balances
.map(|balance| balance.into_iter().map(Into::into).collect()), .map(|balance| balance.into_iter().map(Into::into).collect())
rewards: meta.rewards, .into(),
loaded_addresses: Some(UiLoadedAddresses::from(&meta.loaded_addresses)), rewards: meta.rewards.into(),
return_data: meta.return_data.map(|return_data| return_data.into()), loaded_addresses: Some(UiLoadedAddresses::from(&meta.loaded_addresses)).into(),
compute_units_consumed: meta.compute_units_consumed, 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); let mut meta = UiTransactionStatusMeta::from(self.meta);
if !show_rewards { if !show_rewards {
meta.rewards = None; meta.rewards = OptionSerializer::None;
} }
meta meta
} }
@ -1039,6 +1088,15 @@ pub struct UiTransactionReturnData {
pub data: (String, UiReturnDataEncoding), pub data: (String, UiReturnDataEncoding),
} }
impl Default for UiTransactionReturnData {
fn default() -> Self {
Self {
program_id: String::default(),
data: (String::default(), UiReturnDataEncoding::Base64),
}
}
}
impl From<TransactionReturnData> for UiTransactionReturnData { impl From<TransactionReturnData> for UiTransactionReturnData {
fn from(return_data: TransactionReturnData) -> Self { fn from(return_data: TransactionReturnData) -> Self {
Self { Self {
@ -1059,7 +1117,7 @@ pub enum UiReturnDataEncoding {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use {super::*, serde_json::json};
#[test] #[test]
fn test_decode_invalid_transaction() { fn test_decode_invalid_transaction() {
@ -1153,4 +1211,134 @@ mod test {
}; };
assert!(status.satisfies_commitment(CommitmentConfig::confirmed())); 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::<UiTransactionStatusMeta>(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::<UiTransactionTokenBalance>(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
);
}
} }

View File

@ -0,0 +1,77 @@
use serde::{ser::Error, Deserialize, Deserializer, Serialize, Serializer};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OptionSerializer<T> {
Some(T),
None,
Skip,
}
impl<T> OptionSerializer<T> {
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<T>) -> 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<T> From<Option<T>> for OptionSerializer<T> {
fn from(option: Option<T>) -> Self {
match option {
Option::Some(item) => Self::Some(item),
Option::None => Self::None,
}
}
}
impl<T> From<OptionSerializer<T>> for Option<T> {
fn from(option: OptionSerializer<T>) -> Self {
match option {
OptionSerializer::Some(item) => Self::Some(item),
_ => Self::None,
}
}
}
impl<T: Serialize> Serialize for OptionSerializer<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Option::deserialize(deserializer).map(Into::into)
}
}