Add CLI support for versioned transactions (#23606)
This commit is contained in:
parent
330d6db19a
commit
0eccacbd5b
|
@ -28,7 +28,7 @@ use {
|
||||||
signature::Signature,
|
signature::Signature,
|
||||||
stake::state::{Authorized, Lockup},
|
stake::state::{Authorized, Lockup},
|
||||||
stake_history::StakeHistoryEntry,
|
stake_history::StakeHistoryEntry,
|
||||||
transaction::{Transaction, TransactionError},
|
transaction::{Transaction, TransactionError, VersionedTransaction},
|
||||||
},
|
},
|
||||||
solana_transaction_status::{
|
solana_transaction_status::{
|
||||||
EncodedConfirmedBlock, EncodedTransaction, TransactionConfirmationStatus,
|
EncodedConfirmedBlock, EncodedTransaction, TransactionConfirmationStatus,
|
||||||
|
@ -2218,7 +2218,7 @@ pub enum CliSignatureVerificationStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CliSignatureVerificationStatus {
|
impl CliSignatureVerificationStatus {
|
||||||
pub fn verify_transaction(tx: &Transaction) -> Vec<Self> {
|
pub fn verify_transaction(tx: &VersionedTransaction) -> Vec<Self> {
|
||||||
tx.verify_with_results()
|
tx.verify_with_results()
|
||||||
.iter()
|
.iter()
|
||||||
.zip(&tx.signatures)
|
.zip(&tx.signatures)
|
||||||
|
@ -2354,7 +2354,7 @@ pub struct CliTransaction {
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
pub slot: Option<Slot>,
|
pub slot: Option<Slot>,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
pub decoded_transaction: Transaction,
|
pub decoded_transaction: VersionedTransaction,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
pub prefix: String,
|
pub prefix: String,
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
|
|
@ -7,12 +7,13 @@ use {
|
||||||
clock::UnixTimestamp,
|
clock::UnixTimestamp,
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
instruction::CompiledInstruction,
|
instruction::CompiledInstruction,
|
||||||
|
message::v0::MessageAddressTableLookup,
|
||||||
native_token::lamports_to_sol,
|
native_token::lamports_to_sol,
|
||||||
program_utils::limited_deserialize,
|
program_utils::limited_deserialize,
|
||||||
pubkey::Pubkey,
|
pubkey::Pubkey,
|
||||||
signature::Signature,
|
signature::Signature,
|
||||||
stake,
|
stake,
|
||||||
transaction::{Transaction, TransactionError},
|
transaction::{TransactionError, TransactionVersion, VersionedTransaction},
|
||||||
},
|
},
|
||||||
solana_transaction_status::{Rewards, UiTransactionStatusMeta},
|
solana_transaction_status::{Rewards, UiTransactionStatusMeta},
|
||||||
spl_memo::{id as spl_memo_id, v1::id as spl_memo_v1_id},
|
spl_memo::{id as spl_memo_id, v1::id as spl_memo_v1_id},
|
||||||
|
@ -171,7 +172,7 @@ fn format_account_mode(meta: CliAccountMeta) -> String {
|
||||||
|
|
||||||
fn write_transaction<W: io::Write>(
|
fn write_transaction<W: io::Write>(
|
||||||
w: &mut W,
|
w: &mut W,
|
||||||
transaction: &Transaction,
|
transaction: &VersionedTransaction,
|
||||||
transaction_status: Option<&UiTransactionStatusMeta>,
|
transaction_status: Option<&UiTransactionStatusMeta>,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
sigverify_status: Option<&[CliSignatureVerificationStatus]>,
|
sigverify_status: Option<&[CliSignatureVerificationStatus]>,
|
||||||
|
@ -181,48 +182,65 @@ fn write_transaction<W: io::Write>(
|
||||||
write_block_time(w, block_time, timezone, prefix)?;
|
write_block_time(w, block_time, timezone, prefix)?;
|
||||||
|
|
||||||
let message = &transaction.message;
|
let message = &transaction.message;
|
||||||
write_recent_blockhash(w, &message.recent_blockhash, prefix)?;
|
let account_keys: Vec<AccountKeyType> = {
|
||||||
|
let static_keys_iter = message
|
||||||
|
.static_account_keys()
|
||||||
|
.iter()
|
||||||
|
.map(AccountKeyType::Known);
|
||||||
|
let dynamic_keys: Vec<AccountKeyType> = message
|
||||||
|
.address_table_lookups()
|
||||||
|
.map(transform_lookups_to_unknown_keys)
|
||||||
|
.unwrap_or_default();
|
||||||
|
static_keys_iter.chain(dynamic_keys).collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
write_version(w, transaction.version(), prefix)?;
|
||||||
|
write_recent_blockhash(w, message.recent_blockhash(), prefix)?;
|
||||||
write_signatures(w, &transaction.signatures, sigverify_status, prefix)?;
|
write_signatures(w, &transaction.signatures, sigverify_status, prefix)?;
|
||||||
|
|
||||||
let mut fee_payer_index = None;
|
let mut fee_payer_index = None;
|
||||||
for (account_index, account) in message.account_keys.iter().enumerate() {
|
for (account_index, account) in account_keys.iter().enumerate() {
|
||||||
if fee_payer_index.is_none() && message.is_non_loader_key(account_index) {
|
if fee_payer_index.is_none() && message.is_non_loader_key(account_index) {
|
||||||
fee_payer_index = Some(account_index)
|
fee_payer_index = Some(account_index)
|
||||||
}
|
}
|
||||||
|
|
||||||
let account_meta = CliAccountMeta {
|
let account_meta = CliAccountMeta {
|
||||||
is_signer: message.is_signer(account_index),
|
is_signer: message.is_signer(account_index),
|
||||||
is_writable: message.is_writable(account_index),
|
is_writable: message.is_maybe_writable(account_index),
|
||||||
is_invoked: message.maybe_executable(account_index),
|
is_invoked: message.is_invoked(account_index),
|
||||||
};
|
};
|
||||||
|
|
||||||
write_account(
|
write_account(
|
||||||
w,
|
w,
|
||||||
account_index,
|
account_index,
|
||||||
account,
|
*account,
|
||||||
format_account_mode(account_meta),
|
format_account_mode(account_meta),
|
||||||
Some(account_index) == fee_payer_index,
|
Some(account_index) == fee_payer_index,
|
||||||
prefix,
|
prefix,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (instruction_index, instruction) in message.instructions.iter().enumerate() {
|
for (instruction_index, instruction) in message.instructions().iter().enumerate() {
|
||||||
let program_pubkey = message.account_keys[instruction.program_id_index as usize];
|
let program_pubkey = account_keys[instruction.program_id_index as usize];
|
||||||
let instruction_accounts = instruction.accounts.iter().map(|account_index| {
|
let instruction_accounts = instruction
|
||||||
let account_pubkey = &message.account_keys[*account_index as usize];
|
.accounts
|
||||||
(account_pubkey, *account_index)
|
.iter()
|
||||||
});
|
.map(|account_index| (account_keys[*account_index as usize], *account_index));
|
||||||
|
|
||||||
write_instruction(
|
write_instruction(
|
||||||
w,
|
w,
|
||||||
instruction_index,
|
instruction_index,
|
||||||
&program_pubkey,
|
program_pubkey,
|
||||||
instruction,
|
instruction,
|
||||||
instruction_accounts,
|
instruction_accounts,
|
||||||
prefix,
|
prefix,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(address_table_lookups) = message.address_table_lookups() {
|
||||||
|
write_address_table_lookups(w, address_table_lookups, prefix)?;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(transaction_status) = transaction_status {
|
if let Some(transaction_status) = transaction_status {
|
||||||
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)?;
|
||||||
|
@ -236,6 +254,36 @@ fn write_transaction<W: io::Write>(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn transform_lookups_to_unknown_keys(lookups: &[MessageAddressTableLookup]) -> Vec<AccountKeyType> {
|
||||||
|
let unknown_writable_keys = lookups
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.flat_map(|(lookup_index, lookup)| {
|
||||||
|
lookup
|
||||||
|
.writable_indexes
|
||||||
|
.iter()
|
||||||
|
.map(move |table_index| AccountKeyType::Unknown {
|
||||||
|
lookup_index,
|
||||||
|
table_index: *table_index,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let unknown_readonly_keys = lookups
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.flat_map(|(lookup_index, lookup)| {
|
||||||
|
lookup
|
||||||
|
.readonly_indexes
|
||||||
|
.iter()
|
||||||
|
.map(move |table_index| AccountKeyType::Unknown {
|
||||||
|
lookup_index,
|
||||||
|
table_index: *table_index,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
unknown_writable_keys.chain(unknown_readonly_keys).collect()
|
||||||
|
}
|
||||||
|
|
||||||
enum CliTimezone {
|
enum CliTimezone {
|
||||||
Local,
|
Local,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
@ -258,6 +306,18 @@ fn write_block_time<W: io::Write>(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_version<W: io::Write>(
|
||||||
|
w: &mut W,
|
||||||
|
version: TransactionVersion,
|
||||||
|
prefix: &str,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let version = match version {
|
||||||
|
TransactionVersion::Legacy(_) => "legacy".to_string(),
|
||||||
|
TransactionVersion::Number(number) => number.to_string(),
|
||||||
|
};
|
||||||
|
writeln!(w, "{}Version: {}", prefix, version)
|
||||||
|
}
|
||||||
|
|
||||||
fn write_recent_blockhash<W: io::Write>(
|
fn write_recent_blockhash<W: io::Write>(
|
||||||
w: &mut W,
|
w: &mut W,
|
||||||
recent_blockhash: &Hash,
|
recent_blockhash: &Hash,
|
||||||
|
@ -292,10 +352,37 @@ fn write_signatures<W: io::Write>(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
enum AccountKeyType<'a> {
|
||||||
|
Known(&'a Pubkey),
|
||||||
|
Unknown {
|
||||||
|
lookup_index: usize,
|
||||||
|
table_index: u8,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for AccountKeyType<'_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Known(address) => write!(f, "{}", address),
|
||||||
|
Self::Unknown {
|
||||||
|
lookup_index,
|
||||||
|
table_index,
|
||||||
|
} => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Unknown Address (uses lookup {} and index {})",
|
||||||
|
lookup_index, table_index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn write_account<W: io::Write>(
|
fn write_account<W: io::Write>(
|
||||||
w: &mut W,
|
w: &mut W,
|
||||||
account_index: usize,
|
account_index: usize,
|
||||||
account_address: &Pubkey,
|
account_address: AccountKeyType,
|
||||||
account_mode: String,
|
account_mode: String,
|
||||||
is_fee_payer: bool,
|
is_fee_payer: bool,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
|
@ -314,9 +401,9 @@ fn write_account<W: io::Write>(
|
||||||
fn write_instruction<'a, W: io::Write>(
|
fn write_instruction<'a, W: io::Write>(
|
||||||
w: &mut W,
|
w: &mut W,
|
||||||
instruction_index: usize,
|
instruction_index: usize,
|
||||||
program_pubkey: &Pubkey,
|
program_pubkey: AccountKeyType,
|
||||||
instruction: &CompiledInstruction,
|
instruction: &CompiledInstruction,
|
||||||
instruction_accounts: impl Iterator<Item = (&'a Pubkey, u8)>,
|
instruction_accounts: impl Iterator<Item = (AccountKeyType<'a>, u8)>,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
writeln!(w, "{}Instruction {}", prefix, instruction_index)?;
|
writeln!(w, "{}Instruction {}", prefix, instruction_index)?;
|
||||||
|
@ -334,33 +421,35 @@ fn write_instruction<'a, W: io::Write>(
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut raw = true;
|
let mut raw = true;
|
||||||
if program_pubkey == &solana_vote_program::id() {
|
if let AccountKeyType::Known(program_pubkey) = program_pubkey {
|
||||||
if let Ok(vote_instruction) = limited_deserialize::<
|
if program_pubkey == &solana_vote_program::id() {
|
||||||
solana_vote_program::vote_instruction::VoteInstruction,
|
if let Ok(vote_instruction) = limited_deserialize::<
|
||||||
>(&instruction.data)
|
solana_vote_program::vote_instruction::VoteInstruction,
|
||||||
{
|
>(&instruction.data)
|
||||||
writeln!(w, "{} {:?}", prefix, vote_instruction)?;
|
{
|
||||||
raw = false;
|
writeln!(w, "{} {:?}", prefix, vote_instruction)?;
|
||||||
}
|
raw = false;
|
||||||
} else if program_pubkey == &stake::program::id() {
|
}
|
||||||
if let Ok(stake_instruction) =
|
} else if program_pubkey == &stake::program::id() {
|
||||||
limited_deserialize::<stake::instruction::StakeInstruction>(&instruction.data)
|
if let Ok(stake_instruction) =
|
||||||
{
|
limited_deserialize::<stake::instruction::StakeInstruction>(&instruction.data)
|
||||||
writeln!(w, "{} {:?}", prefix, stake_instruction)?;
|
{
|
||||||
raw = false;
|
writeln!(w, "{} {:?}", prefix, stake_instruction)?;
|
||||||
}
|
raw = false;
|
||||||
} else if program_pubkey == &solana_sdk::system_program::id() {
|
}
|
||||||
if let Ok(system_instruction) = limited_deserialize::<
|
} else if program_pubkey == &solana_sdk::system_program::id() {
|
||||||
solana_sdk::system_instruction::SystemInstruction,
|
if let Ok(system_instruction) = limited_deserialize::<
|
||||||
>(&instruction.data)
|
solana_sdk::system_instruction::SystemInstruction,
|
||||||
{
|
>(&instruction.data)
|
||||||
writeln!(w, "{} {:?}", prefix, system_instruction)?;
|
{
|
||||||
raw = false;
|
writeln!(w, "{} {:?}", prefix, system_instruction)?;
|
||||||
}
|
raw = false;
|
||||||
} else if is_memo_program(program_pubkey) {
|
}
|
||||||
if let Ok(s) = std::str::from_utf8(&instruction.data) {
|
} else if is_memo_program(program_pubkey) {
|
||||||
writeln!(w, "{} Data: \"{}\"", prefix, s)?;
|
if let Ok(s) = std::str::from_utf8(&instruction.data) {
|
||||||
raw = false;
|
writeln!(w, "{} Data: \"{}\"", prefix, s)?;
|
||||||
|
raw = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,6 +460,30 @@ fn write_instruction<'a, W: io::Write>(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_address_table_lookups<W: io::Write>(
|
||||||
|
w: &mut W,
|
||||||
|
address_table_lookups: &[MessageAddressTableLookup],
|
||||||
|
prefix: &str,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
for (lookup_index, lookup) in address_table_lookups.iter().enumerate() {
|
||||||
|
writeln!(w, "{}Address Table Lookup {}", prefix, lookup_index,)?;
|
||||||
|
writeln!(w, "{} Table Account: {}", prefix, lookup.account_key,)?;
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"{} Writable Indexes: {:?}",
|
||||||
|
prefix,
|
||||||
|
&lookup.writable_indexes[..],
|
||||||
|
)?;
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"{} Readonly Indexes: {:?}",
|
||||||
|
prefix,
|
||||||
|
&lookup.readonly_indexes[..],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn write_rewards<W: io::Write>(
|
fn write_rewards<W: io::Write>(
|
||||||
w: &mut W,
|
w: &mut W,
|
||||||
rewards: Option<&Rewards>,
|
rewards: Option<&Rewards>,
|
||||||
|
@ -480,7 +593,7 @@ fn write_log_messages<W: io::Write>(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn println_transaction(
|
pub fn println_transaction(
|
||||||
transaction: &Transaction,
|
transaction: &VersionedTransaction,
|
||||||
transaction_status: Option<&UiTransactionStatusMeta>,
|
transaction_status: Option<&UiTransactionStatusMeta>,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
sigverify_status: Option<&[CliSignatureVerificationStatus]>,
|
sigverify_status: Option<&[CliSignatureVerificationStatus]>,
|
||||||
|
@ -506,7 +619,7 @@ pub fn println_transaction(
|
||||||
|
|
||||||
pub fn writeln_transaction(
|
pub fn writeln_transaction(
|
||||||
f: &mut dyn fmt::Write,
|
f: &mut dyn fmt::Write,
|
||||||
transaction: &Transaction,
|
transaction: &VersionedTransaction,
|
||||||
transaction_status: Option<&UiTransactionStatusMeta>,
|
transaction_status: Option<&UiTransactionStatusMeta>,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
sigverify_status: Option<&[CliSignatureVerificationStatus]>,
|
sigverify_status: Option<&[CliSignatureVerificationStatus]>,
|
||||||
|
@ -552,26 +665,59 @@ mod test {
|
||||||
use {
|
use {
|
||||||
super::*,
|
super::*,
|
||||||
solana_sdk::{
|
solana_sdk::{
|
||||||
message::{v0::LoadedAddresses, Message as LegacyMessage, MessageHeader},
|
message::{
|
||||||
|
v0::{self, LoadedAddresses},
|
||||||
|
Message as LegacyMessage, MessageHeader, VersionedMessage,
|
||||||
|
},
|
||||||
pubkey::Pubkey,
|
pubkey::Pubkey,
|
||||||
signature::{Keypair, Signer},
|
signature::{Keypair, Signer},
|
||||||
|
transaction::Transaction,
|
||||||
},
|
},
|
||||||
solana_transaction_status::{Reward, RewardType, TransactionStatusMeta},
|
solana_transaction_status::{Reward, RewardType, TransactionStatusMeta},
|
||||||
std::io::BufWriter,
|
std::io::BufWriter,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn test_keypair() -> Keypair {
|
fn new_test_keypair() -> Keypair {
|
||||||
let secret = ed25519_dalek::SecretKey::from_bytes(&[0u8; 32]).unwrap();
|
let secret = ed25519_dalek::SecretKey::from_bytes(&[0u8; 32]).unwrap();
|
||||||
let public = ed25519_dalek::PublicKey::from(&secret);
|
let public = ed25519_dalek::PublicKey::from(&secret);
|
||||||
let keypair = ed25519_dalek::Keypair { secret, public };
|
let keypair = ed25519_dalek::Keypair { secret, public };
|
||||||
Keypair::from_bytes(&keypair.to_bytes()).unwrap()
|
Keypair::from_bytes(&keypair.to_bytes()).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn new_test_v0_transaction() -> VersionedTransaction {
|
||||||
fn test_write_transaction() {
|
let keypair = new_test_keypair();
|
||||||
let keypair = test_keypair();
|
|
||||||
let account_key = Pubkey::new_from_array([1u8; 32]);
|
let account_key = Pubkey::new_from_array([1u8; 32]);
|
||||||
let transaction = Transaction::new(
|
let address_table_key = Pubkey::new_from_array([2u8; 32]);
|
||||||
|
VersionedTransaction::try_new(
|
||||||
|
VersionedMessage::V0(v0::Message {
|
||||||
|
header: MessageHeader {
|
||||||
|
num_required_signatures: 1,
|
||||||
|
num_readonly_signed_accounts: 0,
|
||||||
|
num_readonly_unsigned_accounts: 1,
|
||||||
|
},
|
||||||
|
recent_blockhash: Hash::default(),
|
||||||
|
account_keys: vec![keypair.pubkey(), account_key],
|
||||||
|
address_table_lookups: vec![MessageAddressTableLookup {
|
||||||
|
account_key: address_table_key,
|
||||||
|
writable_indexes: vec![0],
|
||||||
|
readonly_indexes: vec![1],
|
||||||
|
}],
|
||||||
|
instructions: vec![CompiledInstruction::new_from_raw_parts(
|
||||||
|
3,
|
||||||
|
vec![],
|
||||||
|
vec![1, 2],
|
||||||
|
)],
|
||||||
|
}),
|
||||||
|
&[&keypair],
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_write_legacy_transaction() {
|
||||||
|
let keypair = new_test_keypair();
|
||||||
|
let account_key = Pubkey::new_from_array([1u8; 32]);
|
||||||
|
let transaction = VersionedTransaction::from(Transaction::new(
|
||||||
&[&keypair],
|
&[&keypair],
|
||||||
LegacyMessage {
|
LegacyMessage {
|
||||||
header: MessageHeader {
|
header: MessageHeader {
|
||||||
|
@ -584,7 +730,7 @@ mod test {
|
||||||
instructions: vec![CompiledInstruction::new_from_raw_parts(1, vec![], vec![0])],
|
instructions: vec![CompiledInstruction::new_from_raw_parts(1, vec![], vec![0])],
|
||||||
},
|
},
|
||||||
Hash::default(),
|
Hash::default(),
|
||||||
);
|
));
|
||||||
|
|
||||||
let sigverify_status = CliSignatureVerificationStatus::verify_transaction(&transaction);
|
let sigverify_status = CliSignatureVerificationStatus::verify_transaction(&transaction);
|
||||||
let meta = TransactionStatusMeta {
|
let meta = TransactionStatusMeta {
|
||||||
|
@ -625,6 +771,7 @@ mod test {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
output,
|
output,
|
||||||
r#"Block Time: 2021-08-10T22:16:31Z
|
r#"Block Time: 2021-08-10T22:16:31Z
|
||||||
|
Version: legacy
|
||||||
Recent Blockhash: 11111111111111111111111111111111
|
Recent Blockhash: 11111111111111111111111111111111
|
||||||
Signature 0: 5pkjrE4VBa3Bu9CMKXgh1U345cT1gGo8QBVRTzHAo6gHeiPae5BTbShP15g6NgqRMNqu8Qrhph1ATmrfC1Ley3rx (pass)
|
Signature 0: 5pkjrE4VBa3Bu9CMKXgh1U345cT1gGo8QBVRTzHAo6gHeiPae5BTbShP15g6NgqRMNqu8Qrhph1ATmrfC1Ley3rx (pass)
|
||||||
Account 0: srw- 4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS (fee payer)
|
Account 0: srw- 4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS (fee payer)
|
||||||
|
@ -646,6 +793,85 @@ Rewards:
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_write_v0_transaction() {
|
||||||
|
let versioned_tx = new_test_v0_transaction();
|
||||||
|
let sigverify_status = CliSignatureVerificationStatus::verify_transaction(&versioned_tx);
|
||||||
|
let address_table_entry1 = Pubkey::new_from_array([3u8; 32]);
|
||||||
|
let address_table_entry2 = Pubkey::new_from_array([4u8; 32]);
|
||||||
|
let loaded_addresses = LoadedAddresses {
|
||||||
|
writable: vec![address_table_entry1],
|
||||||
|
readonly: vec![address_table_entry2],
|
||||||
|
};
|
||||||
|
let meta = TransactionStatusMeta {
|
||||||
|
status: Ok(()),
|
||||||
|
fee: 5000,
|
||||||
|
pre_balances: vec![5000, 10_000, 15_000, 20_000],
|
||||||
|
post_balances: vec![0, 10_000, 14_900, 20_000],
|
||||||
|
inner_instructions: None,
|
||||||
|
log_messages: Some(vec!["Test message".to_string()]),
|
||||||
|
pre_token_balances: None,
|
||||||
|
post_token_balances: None,
|
||||||
|
rewards: Some(vec![Reward {
|
||||||
|
pubkey: address_table_entry1.to_string(),
|
||||||
|
lamports: -100,
|
||||||
|
post_balance: 14_900,
|
||||||
|
reward_type: Some(RewardType::Rent),
|
||||||
|
commission: None,
|
||||||
|
}]),
|
||||||
|
loaded_addresses,
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = {
|
||||||
|
let mut write_buffer = BufWriter::new(Vec::new());
|
||||||
|
write_transaction(
|
||||||
|
&mut write_buffer,
|
||||||
|
&versioned_tx,
|
||||||
|
Some(&meta.into()),
|
||||||
|
"",
|
||||||
|
Some(&sigverify_status),
|
||||||
|
Some(1628633791),
|
||||||
|
CliTimezone::Utc,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let bytes = write_buffer.into_inner().unwrap();
|
||||||
|
String::from_utf8(bytes).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
output,
|
||||||
|
r#"Block Time: 2021-08-10T22:16:31Z
|
||||||
|
Version: 0
|
||||||
|
Recent Blockhash: 11111111111111111111111111111111
|
||||||
|
Signature 0: 5iEy3TT3ZhTA1NkuCY8GrQGNVY8d5m1bpjdh5FT3Ca4Py81fMipAZjafDuKJKrkw5q5UAAd8oPcgZ4nyXpHt4Fp7 (pass)
|
||||||
|
Account 0: srw- 4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS (fee payer)
|
||||||
|
Account 1: -r-- 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi
|
||||||
|
Account 2: -rw- Unknown Address (uses lookup 0 and index 0)
|
||||||
|
Account 3: -r-x Unknown Address (uses lookup 0 and index 1)
|
||||||
|
Instruction 0
|
||||||
|
Program: Unknown Address (uses lookup 0 and index 1) (3)
|
||||||
|
Account 0: 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi (1)
|
||||||
|
Account 1: Unknown Address (uses lookup 0 and index 0) (2)
|
||||||
|
Data: []
|
||||||
|
Address Table Lookup 0
|
||||||
|
Table Account: 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR
|
||||||
|
Writable Indexes: [0]
|
||||||
|
Readonly Indexes: [1]
|
||||||
|
Status: Ok
|
||||||
|
Fee: ◎0.000005
|
||||||
|
Account 0 balance: ◎0.000005 -> ◎0
|
||||||
|
Account 1 balance: ◎0.00001
|
||||||
|
Account 2 balance: ◎0.000015 -> ◎0.0000149
|
||||||
|
Account 3 balance: ◎0.00002
|
||||||
|
Log Messages:
|
||||||
|
Test message
|
||||||
|
Rewards:
|
||||||
|
Address Type Amount New Balance \0
|
||||||
|
CktRuQ2mttgRGkXJtyksdKHjUdc2C4TgDzyB98oEzy8 rent -◎0.000000100 ◎0.000014900 \0
|
||||||
|
"#.replace("\\0", "") // replace marker used to subvert trailing whitespace linter on CI
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_format_labeled_address() {
|
fn test_format_labeled_address() {
|
||||||
let pubkey = Pubkey::default().to_string();
|
let pubkey = Pubkey::default().to_string();
|
||||||
|
|
|
@ -30,7 +30,7 @@ use {
|
||||||
pubkey::Pubkey,
|
pubkey::Pubkey,
|
||||||
signature::{Signature, Signer, SignerError},
|
signature::{Signature, Signer, SignerError},
|
||||||
stake::{instruction::LockupArgs, state::Lockup},
|
stake::{instruction::LockupArgs, state::Lockup},
|
||||||
transaction::{Transaction, TransactionError},
|
transaction::{TransactionError, VersionedTransaction},
|
||||||
},
|
},
|
||||||
solana_vote_program::vote_state::VoteAuthorize,
|
solana_vote_program::vote_state::VoteAuthorize,
|
||||||
std::{collections::HashMap, error, io::stdout, str::FromStr, sync::Arc, time::Duration},
|
std::{collections::HashMap, error, io::stdout, str::FromStr, sync::Arc, time::Duration},
|
||||||
|
@ -385,7 +385,7 @@ pub enum CliCommand {
|
||||||
seed: String,
|
seed: String,
|
||||||
program_id: Pubkey,
|
program_id: Pubkey,
|
||||||
},
|
},
|
||||||
DecodeTransaction(Transaction),
|
DecodeTransaction(VersionedTransaction),
|
||||||
ResolveSigner(Option<String>),
|
ResolveSigner(Option<String>),
|
||||||
ShowAccount {
|
ShowAccount {
|
||||||
pubkey: Pubkey,
|
pubkey: Pubkey,
|
||||||
|
|
|
@ -1058,6 +1058,7 @@ pub fn process_get_block(
|
||||||
RpcBlockConfig {
|
RpcBlockConfig {
|
||||||
encoding: Some(UiTransactionEncoding::Base64),
|
encoding: Some(UiTransactionEncoding::Base64),
|
||||||
commitment: Some(CommitmentConfig::confirmed()),
|
commitment: Some(CommitmentConfig::confirmed()),
|
||||||
|
max_supported_transaction_version: Some(0),
|
||||||
..RpcBlockConfig::default()
|
..RpcBlockConfig::default()
|
||||||
},
|
},
|
||||||
)?
|
)?
|
||||||
|
@ -2042,7 +2043,7 @@ pub fn process_transaction_history(
|
||||||
RpcTransactionConfig {
|
RpcTransactionConfig {
|
||||||
encoding: Some(UiTransactionEncoding::Base64),
|
encoding: Some(UiTransactionEncoding::Base64),
|
||||||
commitment: Some(CommitmentConfig::confirmed()),
|
commitment: Some(CommitmentConfig::confirmed()),
|
||||||
max_supported_transaction_version: None,
|
max_supported_transaction_version: Some(0),
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Ok(confirmed_transaction) => {
|
Ok(confirmed_transaction) => {
|
||||||
|
|
|
@ -37,10 +37,11 @@ use {
|
||||||
stake,
|
stake,
|
||||||
system_instruction::{self, SystemError},
|
system_instruction::{self, SystemError},
|
||||||
system_program,
|
system_program,
|
||||||
transaction::Transaction,
|
transaction::{Transaction, VersionedTransaction},
|
||||||
},
|
},
|
||||||
solana_transaction_status::{
|
solana_transaction_status::{
|
||||||
Encodable, EncodedTransaction, TransactionBinaryEncoding, UiTransactionEncoding,
|
EncodableWithMeta, EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction,
|
||||||
|
TransactionBinaryEncoding, UiTransactionEncoding,
|
||||||
},
|
},
|
||||||
std::{fmt::Write as FmtWrite, fs::File, io::Write, sync::Arc},
|
std::{fmt::Write as FmtWrite, fs::File, io::Write, sync::Arc},
|
||||||
};
|
};
|
||||||
|
@ -561,23 +562,25 @@ pub fn process_confirm(
|
||||||
RpcTransactionConfig {
|
RpcTransactionConfig {
|
||||||
encoding: Some(UiTransactionEncoding::Base64),
|
encoding: Some(UiTransactionEncoding::Base64),
|
||||||
commitment: Some(CommitmentConfig::confirmed()),
|
commitment: Some(CommitmentConfig::confirmed()),
|
||||||
max_supported_transaction_version: None,
|
max_supported_transaction_version: Some(0),
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Ok(confirmed_transaction) => {
|
Ok(confirmed_transaction) => {
|
||||||
let decoded_transaction = confirmed_transaction
|
let EncodedConfirmedTransactionWithStatusMeta {
|
||||||
.transaction
|
block_time,
|
||||||
.transaction
|
slot,
|
||||||
.decode()
|
transaction: transaction_with_meta,
|
||||||
.expect("Successful decode");
|
} = confirmed_transaction;
|
||||||
let json_transaction =
|
|
||||||
decoded_transaction.encode(UiTransactionEncoding::Json);
|
let decoded_transaction =
|
||||||
|
transaction_with_meta.transaction.decode().unwrap();
|
||||||
|
let json_transaction = decoded_transaction.json_encode();
|
||||||
|
|
||||||
transaction = Some(CliTransaction {
|
transaction = Some(CliTransaction {
|
||||||
transaction: json_transaction,
|
transaction: json_transaction,
|
||||||
meta: confirmed_transaction.transaction.meta,
|
meta: transaction_with_meta.meta,
|
||||||
block_time: confirmed_transaction.block_time,
|
block_time,
|
||||||
slot: Some(confirmed_transaction.slot),
|
slot: Some(slot),
|
||||||
decoded_transaction,
|
decoded_transaction,
|
||||||
prefix: " ".to_string(),
|
prefix: " ".to_string(),
|
||||||
sigverify_status: vec![],
|
sigverify_status: vec![],
|
||||||
|
@ -609,11 +612,14 @@ pub fn process_confirm(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::unnecessary_wraps)]
|
#[allow(clippy::unnecessary_wraps)]
|
||||||
pub fn process_decode_transaction(config: &CliConfig, transaction: &Transaction) -> ProcessResult {
|
pub fn process_decode_transaction(
|
||||||
|
config: &CliConfig,
|
||||||
|
transaction: &VersionedTransaction,
|
||||||
|
) -> ProcessResult {
|
||||||
let sigverify_status = CliSignatureVerificationStatus::verify_transaction(transaction);
|
let sigverify_status = CliSignatureVerificationStatus::verify_transaction(transaction);
|
||||||
let decode_transaction = CliTransaction {
|
let decode_transaction = CliTransaction {
|
||||||
decoded_transaction: transaction.clone(),
|
decoded_transaction: transaction.clone(),
|
||||||
transaction: transaction.encode(UiTransactionEncoding::Json),
|
transaction: transaction.json_encode(),
|
||||||
meta: None,
|
meta: None,
|
||||||
block_time: None,
|
block_time: None,
|
||||||
slot: None,
|
slot: None,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/// The `bigtable` subcommand
|
//! The `bigtable` subcommand
|
||||||
use {
|
use {
|
||||||
crate::ledger_path::canonicalize_ledger_path,
|
crate::ledger_path::canonicalize_ledger_path,
|
||||||
clap::{
|
clap::{
|
||||||
|
@ -17,7 +17,7 @@ use {
|
||||||
solana_ledger::{blockstore::Blockstore, blockstore_db::AccessType},
|
solana_ledger::{blockstore::Blockstore, blockstore_db::AccessType},
|
||||||
solana_sdk::{clock::Slot, pubkey::Pubkey, signature::Signature},
|
solana_sdk::{clock::Slot, pubkey::Pubkey, signature::Signature},
|
||||||
solana_transaction_status::{
|
solana_transaction_status::{
|
||||||
BlockEncodingOptions, Encodable, EncodeError, LegacyConfirmedBlock, TransactionDetails,
|
BlockEncodingOptions, ConfirmedBlock, EncodeError, TransactionDetails,
|
||||||
UiTransactionEncoding,
|
UiTransactionEncoding,
|
||||||
},
|
},
|
||||||
std::{
|
std::{
|
||||||
|
@ -172,19 +172,17 @@ async fn confirm(
|
||||||
if verbose {
|
if verbose {
|
||||||
match bigtable.get_confirmed_transaction(signature).await {
|
match bigtable.get_confirmed_transaction(signature).await {
|
||||||
Ok(Some(confirmed_tx)) => {
|
Ok(Some(confirmed_tx)) => {
|
||||||
let legacy_confirmed_tx = confirmed_tx
|
let decoded_tx = confirmed_tx.get_transaction();
|
||||||
.into_legacy_confirmed_transaction()
|
let encoded_tx_with_meta = confirmed_tx
|
||||||
.ok_or_else(|| "Failed to read versioned transaction in block".to_string())?;
|
.tx_with_meta
|
||||||
|
.encode(UiTransactionEncoding::Json, Some(0))
|
||||||
|
.map_err(|_| "Failed to encode transaction in block".to_string())?;
|
||||||
transaction = Some(CliTransaction {
|
transaction = Some(CliTransaction {
|
||||||
transaction: legacy_confirmed_tx
|
transaction: encoded_tx_with_meta.transaction,
|
||||||
.tx_with_meta
|
meta: encoded_tx_with_meta.meta,
|
||||||
.transaction
|
block_time: confirmed_tx.block_time,
|
||||||
.encode(UiTransactionEncoding::Json),
|
slot: Some(confirmed_tx.slot),
|
||||||
meta: legacy_confirmed_tx.tx_with_meta.meta.map(|m| m.into()),
|
decoded_transaction: decoded_tx,
|
||||||
block_time: legacy_confirmed_tx.block_time,
|
|
||||||
slot: Some(legacy_confirmed_tx.slot),
|
|
||||||
decoded_transaction: legacy_confirmed_tx.tx_with_meta.transaction,
|
|
||||||
prefix: " ".to_string(),
|
prefix: " ".to_string(),
|
||||||
sigverify_status: vec![],
|
sigverify_status: vec![],
|
||||||
});
|
});
|
||||||
|
@ -216,7 +214,7 @@ pub async fn transaction_history(
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let bigtable = solana_storage_bigtable::LedgerStorage::new(true, None, None).await?;
|
let bigtable = solana_storage_bigtable::LedgerStorage::new(true, None, None).await?;
|
||||||
|
|
||||||
let mut loaded_block: Option<(Slot, LegacyConfirmedBlock)> = None;
|
let mut loaded_block: Option<(Slot, ConfirmedBlock)> = None;
|
||||||
while limit > 0 {
|
while limit > 0 {
|
||||||
let results = bigtable
|
let results = bigtable
|
||||||
.get_confirmed_signatures_for_address(
|
.get_confirmed_signatures_for_address(
|
||||||
|
@ -257,21 +255,22 @@ pub async fn transaction_history(
|
||||||
loop {
|
loop {
|
||||||
if let Some((slot, block)) = &loaded_block {
|
if let Some((slot, block)) = &loaded_block {
|
||||||
if *slot == result.slot {
|
if *slot == result.slot {
|
||||||
match block.transactions.get(index as usize) {
|
match block.transactions.get(index as usize).map(|tx_with_meta| {
|
||||||
|
(
|
||||||
|
tx_with_meta.get_transaction(),
|
||||||
|
tx_with_meta.get_status_meta(),
|
||||||
|
)
|
||||||
|
}) {
|
||||||
None => {
|
None => {
|
||||||
println!(
|
println!(
|
||||||
" Transaction info for {} is corrupt",
|
" Transaction info for {} is corrupt",
|
||||||
result.signature
|
result.signature
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Some(transaction_with_meta) => {
|
Some((transaction, meta)) => {
|
||||||
println_transaction(
|
println_transaction(
|
||||||
&transaction_with_meta.transaction,
|
&transaction,
|
||||||
transaction_with_meta
|
meta.map(|m| m.into()).as_ref(),
|
||||||
.meta
|
|
||||||
.clone()
|
|
||||||
.map(|m| m.into())
|
|
||||||
.as_ref(),
|
|
||||||
" ",
|
" ",
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
@ -287,10 +286,7 @@ pub async fn transaction_history(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Ok(confirmed_block) => {
|
Ok(confirmed_block) => {
|
||||||
let block = confirmed_block.into_legacy_block().ok_or_else(|| {
|
loaded_block = Some((result.slot, confirmed_block));
|
||||||
"Failed to read versioned transaction in block".to_string()
|
|
||||||
})?;
|
|
||||||
loaded_block = Some((result.slot, block));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#![allow(clippy::integer_arithmetic)]
|
#![allow(clippy::integer_arithmetic)]
|
||||||
use {
|
use {
|
||||||
|
crate::{bigtable::*, ledger_path::*},
|
||||||
clap::{
|
clap::{
|
||||||
crate_description, crate_name, value_t, value_t_or_exit, values_t_or_exit, App,
|
crate_description, crate_name, value_t, value_t_or_exit, values_t_or_exit, App,
|
||||||
AppSettings, Arg, ArgMatches, SubCommand,
|
AppSettings, Arg, ArgMatches, SubCommand,
|
||||||
|
@ -63,6 +64,7 @@ use {
|
||||||
transaction::{MessageHash, SanitizedTransaction, SimpleAddressLoader},
|
transaction::{MessageHash, SanitizedTransaction, SimpleAddressLoader},
|
||||||
},
|
},
|
||||||
solana_stake_program::stake_state::{self, PointValue},
|
solana_stake_program::stake_state::{self, PointValue},
|
||||||
|
solana_transaction_status::VersionedTransactionWithStatusMeta,
|
||||||
solana_vote_program::{
|
solana_vote_program::{
|
||||||
self,
|
self,
|
||||||
vote_state::{self, VoteState},
|
vote_state::{self, VoteState},
|
||||||
|
@ -83,9 +85,7 @@ use {
|
||||||
};
|
};
|
||||||
|
|
||||||
mod bigtable;
|
mod bigtable;
|
||||||
use bigtable::*;
|
|
||||||
mod ledger_path;
|
mod ledger_path;
|
||||||
use ledger_path::*;
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
enum LedgerOutputMethod {
|
enum LedgerOutputMethod {
|
||||||
|
@ -147,7 +147,7 @@ fn output_entry(
|
||||||
for (transactions_index, transaction) in entry.transactions.into_iter().enumerate() {
|
for (transactions_index, transaction) in entry.transactions.into_iter().enumerate() {
|
||||||
println!(" Transaction {}", transactions_index);
|
println!(" Transaction {}", transactions_index);
|
||||||
let tx_signature = transaction.signatures[0];
|
let tx_signature = transaction.signatures[0];
|
||||||
let tx_status = blockstore
|
let tx_with_meta = blockstore
|
||||||
.read_transaction_status((tx_signature, slot))
|
.read_transaction_status((tx_signature, slot))
|
||||||
.unwrap_or_else(|err| {
|
.unwrap_or_else(|err| {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
|
@ -156,21 +156,17 @@ fn output_entry(
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
})
|
})
|
||||||
.map(|transaction_status| transaction_status.into());
|
.map(|meta| VersionedTransactionWithStatusMeta { transaction, meta });
|
||||||
|
|
||||||
if let Some(legacy_tx) = transaction.into_legacy_transaction() {
|
if let Some(tx_with_meta) = tx_with_meta {
|
||||||
|
let status = tx_with_meta.meta.into();
|
||||||
solana_cli_output::display::println_transaction(
|
solana_cli_output::display::println_transaction(
|
||||||
&legacy_tx,
|
&tx_with_meta.transaction,
|
||||||
tx_status.as_ref(),
|
Some(&status),
|
||||||
" ",
|
" ",
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
eprintln!(
|
|
||||||
"Failed to print unsupported transaction for {} at slot {}",
|
|
||||||
tx_signature, slot
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ use {
|
||||||
crate::{
|
crate::{
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
instruction::CompiledInstruction,
|
instruction::CompiledInstruction,
|
||||||
message::{legacy::Message as LegacyMessage, MessageHeader},
|
message::{legacy::Message as LegacyMessage, v0::MessageAddressTableLookup, MessageHeader},
|
||||||
pubkey::Pubkey,
|
pubkey::Pubkey,
|
||||||
sanitize::{Sanitize, SanitizeError},
|
sanitize::{Sanitize, SanitizeError},
|
||||||
short_vec,
|
short_vec,
|
||||||
|
@ -50,6 +50,55 @@ impl VersionedMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn address_table_lookups(&self) -> Option<&[MessageAddressTableLookup]> {
|
||||||
|
match self {
|
||||||
|
Self::Legacy(_) => None,
|
||||||
|
Self::V0(message) => Some(&message.address_table_lookups),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the account at the specified index signed this
|
||||||
|
/// message.
|
||||||
|
pub fn is_signer(&self, index: usize) -> bool {
|
||||||
|
index < usize::from(self.header().num_required_signatures)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the account at the specified index is writable by the
|
||||||
|
/// instructions in this message. Since dynamically loaded addresses can't
|
||||||
|
/// have write locks demoted without loading addresses, this shouldn't be
|
||||||
|
/// used in the runtime.
|
||||||
|
pub fn is_maybe_writable(&self, index: usize) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Legacy(message) => message.is_writable(index),
|
||||||
|
Self::V0(message) => message.is_maybe_writable(index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the account at the specified index is an input to some
|
||||||
|
/// program instruction in this message.
|
||||||
|
fn is_key_passed_to_program(&self, key_index: usize) -> bool {
|
||||||
|
if let Ok(key_index) = u8::try_from(key_index) {
|
||||||
|
self.instructions()
|
||||||
|
.iter()
|
||||||
|
.any(|ix| ix.accounts.contains(&key_index))
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_invoked(&self, key_index: usize) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Legacy(message) => message.is_key_called_as_program(key_index),
|
||||||
|
Self::V0(message) => message.is_key_called_as_program(key_index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the account at the specified index is not invoked as a
|
||||||
|
/// program or, if invoked, is passed to a program.
|
||||||
|
pub fn is_non_loader_key(&self, key_index: usize) -> bool {
|
||||||
|
!self.is_invoked(key_index) || self.is_key_passed_to_program(key_index)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn recent_blockhash(&self) -> &Hash {
|
pub fn recent_blockhash(&self) -> &Hash {
|
||||||
match self {
|
match self {
|
||||||
Self::Legacy(message) => &message.recent_blockhash,
|
Self::Legacy(message) => &message.recent_blockhash,
|
||||||
|
@ -64,6 +113,8 @@ impl VersionedMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Program instructions that will be executed in sequence and committed in
|
||||||
|
/// one atomic transaction if all succeed.
|
||||||
pub fn instructions(&self) -> &[CompiledInstruction] {
|
pub fn instructions(&self) -> &[CompiledInstruction] {
|
||||||
match self {
|
match self {
|
||||||
Self::Legacy(message) => &message.instructions,
|
Self::Legacy(message) => &message.instructions,
|
||||||
|
|
|
@ -10,12 +10,13 @@
|
||||||
//! [future message format]: https://docs.solana.com/proposals/transactions-v2
|
//! [future message format]: https://docs.solana.com/proposals/transactions-v2
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
bpf_loader_upgradeable,
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
instruction::CompiledInstruction,
|
instruction::CompiledInstruction,
|
||||||
message::{MessageHeader, MESSAGE_VERSION_PREFIX},
|
message::{legacy::BUILTIN_PROGRAMS_KEYS, MessageHeader, MESSAGE_VERSION_PREFIX},
|
||||||
pubkey::Pubkey,
|
pubkey::Pubkey,
|
||||||
sanitize::{Sanitize, SanitizeError},
|
sanitize::{Sanitize, SanitizeError},
|
||||||
short_vec,
|
short_vec, sysvar,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod loaded;
|
mod loaded;
|
||||||
|
@ -138,6 +139,70 @@ impl Message {
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
bincode::serialize(&(MESSAGE_VERSION_PREFIX, self)).unwrap()
|
bincode::serialize(&(MESSAGE_VERSION_PREFIX, self)).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the account at the specified index is called as a program by an instruction
|
||||||
|
pub fn is_key_called_as_program(&self, key_index: usize) -> bool {
|
||||||
|
if let Ok(key_index) = u8::try_from(key_index) {
|
||||||
|
self.instructions
|
||||||
|
.iter()
|
||||||
|
.any(|ix| ix.program_id_index == key_index)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the account at the specified index was requested to be
|
||||||
|
/// writable. This method should not be used directly.
|
||||||
|
fn is_writable_index(&self, key_index: usize) -> bool {
|
||||||
|
let header = &self.header;
|
||||||
|
let num_account_keys = self.account_keys.len();
|
||||||
|
let num_signed_accounts = usize::from(header.num_required_signatures);
|
||||||
|
if key_index >= num_account_keys {
|
||||||
|
let loaded_addresses_index = key_index.saturating_sub(num_account_keys);
|
||||||
|
let num_writable_dynamic_addresses = self
|
||||||
|
.address_table_lookups
|
||||||
|
.iter()
|
||||||
|
.map(|lookup| lookup.writable_indexes.len())
|
||||||
|
.sum();
|
||||||
|
loaded_addresses_index < num_writable_dynamic_addresses
|
||||||
|
} else if key_index >= num_signed_accounts {
|
||||||
|
let num_unsigned_accounts = num_account_keys.saturating_sub(num_signed_accounts);
|
||||||
|
let num_writable_unsigned_accounts = num_unsigned_accounts
|
||||||
|
.saturating_sub(usize::from(header.num_readonly_unsigned_accounts));
|
||||||
|
let unsigned_account_index = key_index.saturating_sub(num_signed_accounts);
|
||||||
|
unsigned_account_index < num_writable_unsigned_accounts
|
||||||
|
} else {
|
||||||
|
let num_writable_signed_accounts = num_signed_accounts
|
||||||
|
.saturating_sub(usize::from(header.num_readonly_signed_accounts));
|
||||||
|
key_index < num_writable_signed_accounts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if any static account key is the bpf upgradeable loader
|
||||||
|
fn is_upgradeable_loader_in_static_keys(&self) -> bool {
|
||||||
|
self.account_keys
|
||||||
|
.iter()
|
||||||
|
.any(|&key| key == bpf_loader_upgradeable::id())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the account at the specified index was requested as writable.
|
||||||
|
/// Before loading addresses, we can't demote write locks for dynamically loaded
|
||||||
|
/// addresses so this should not be used by the runtime.
|
||||||
|
pub fn is_maybe_writable(&self, key_index: usize) -> bool {
|
||||||
|
self.is_writable_index(key_index)
|
||||||
|
&& !{
|
||||||
|
// demote reserved ids
|
||||||
|
self.account_keys
|
||||||
|
.get(key_index)
|
||||||
|
.map(|key| sysvar::is_sysvar_id(key) || BUILTIN_PROGRAMS_KEYS.contains(key))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
&& !{
|
||||||
|
// demote program ids
|
||||||
|
self.is_key_called_as_program(key_index)
|
||||||
|
&& !self.is_upgradeable_loader_in_static_keys()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -1,19 +1,4 @@
|
||||||
#![allow(clippy::integer_arithmetic)]
|
#![allow(clippy::integer_arithmetic)]
|
||||||
#[macro_use]
|
|
||||||
extern crate lazy_static;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate serde_derive;
|
|
||||||
|
|
||||||
pub mod extract_memos;
|
|
||||||
pub mod parse_accounts;
|
|
||||||
pub mod parse_associated_token;
|
|
||||||
pub mod parse_bpf_loader;
|
|
||||||
pub mod parse_instruction;
|
|
||||||
pub mod parse_stake;
|
|
||||||
pub mod parse_system;
|
|
||||||
pub mod parse_token;
|
|
||||||
pub mod parse_vote;
|
|
||||||
pub mod token_balances;
|
|
||||||
|
|
||||||
pub use {crate::extract_memos::extract_and_fmt_memos, solana_runtime::bank::RewardType};
|
pub use {crate::extract_memos::extract_and_fmt_memos, solana_runtime::bank::RewardType};
|
||||||
use {
|
use {
|
||||||
|
@ -42,6 +27,22 @@ use {
|
||||||
thiserror::Error,
|
thiserror::Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate lazy_static;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate serde_derive;
|
||||||
|
|
||||||
|
pub mod extract_memos;
|
||||||
|
pub mod parse_accounts;
|
||||||
|
pub mod parse_associated_token;
|
||||||
|
pub mod parse_bpf_loader;
|
||||||
|
pub mod parse_instruction;
|
||||||
|
pub mod parse_stake;
|
||||||
|
pub mod parse_system;
|
||||||
|
pub mod parse_token;
|
||||||
|
pub mod parse_vote;
|
||||||
|
pub mod token_balances;
|
||||||
|
|
||||||
pub struct BlockEncodingOptions {
|
pub struct BlockEncodingOptions {
|
||||||
pub transaction_details: TransactionDetails,
|
pub transaction_details: TransactionDetails,
|
||||||
pub show_rewards: bool,
|
pub show_rewards: bool,
|
||||||
|
@ -68,6 +69,7 @@ pub trait EncodableWithMeta {
|
||||||
encoding: UiTransactionEncoding,
|
encoding: UiTransactionEncoding,
|
||||||
meta: &TransactionStatusMeta,
|
meta: &TransactionStatusMeta,
|
||||||
) -> Self::Encoded;
|
) -> Self::Encoded;
|
||||||
|
fn json_encode(&self) -> Self::Encoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
@ -488,38 +490,6 @@ pub struct VersionedConfirmedBlock {
|
||||||
pub block_height: Option<u64>,
|
pub block_height: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirmed block which only supports legacy transactions. Used
|
|
||||||
// until migration to versioned transactions is completed.
|
|
||||||
pub struct LegacyConfirmedBlock {
|
|
||||||
pub previous_blockhash: String,
|
|
||||||
pub blockhash: String,
|
|
||||||
pub parent_slot: Slot,
|
|
||||||
pub transactions: Vec<LegacyTransactionWithStatusMeta>,
|
|
||||||
pub rewards: Rewards,
|
|
||||||
pub block_time: Option<UnixTimestamp>,
|
|
||||||
pub block_height: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfirmedBlock {
|
|
||||||
/// Downgrades a versioned block into a legacy block type
|
|
||||||
/// if it only contains legacy transactions
|
|
||||||
pub fn into_legacy_block(self) -> Option<LegacyConfirmedBlock> {
|
|
||||||
Some(LegacyConfirmedBlock {
|
|
||||||
previous_blockhash: self.previous_blockhash,
|
|
||||||
blockhash: self.blockhash,
|
|
||||||
parent_slot: self.parent_slot,
|
|
||||||
transactions: self
|
|
||||||
.transactions
|
|
||||||
.into_iter()
|
|
||||||
.map(|tx_with_meta| tx_with_meta.into_legacy_transaction_with_meta())
|
|
||||||
.collect::<Option<Vec<_>>>()?,
|
|
||||||
rewards: self.rewards,
|
|
||||||
block_time: self.block_time,
|
|
||||||
block_height: self.block_height,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<VersionedConfirmedBlock> for ConfirmedBlock {
|
impl From<VersionedConfirmedBlock> for ConfirmedBlock {
|
||||||
fn from(block: VersionedConfirmedBlock) -> Self {
|
fn from(block: VersionedConfirmedBlock) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -641,12 +611,21 @@ pub struct VersionedTransactionWithStatusMeta {
|
||||||
pub meta: TransactionStatusMeta,
|
pub meta: TransactionStatusMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LegacyTransactionWithStatusMeta {
|
|
||||||
pub transaction: Transaction,
|
|
||||||
pub meta: Option<TransactionStatusMeta>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TransactionWithStatusMeta {
|
impl TransactionWithStatusMeta {
|
||||||
|
pub fn get_status_meta(&self) -> Option<TransactionStatusMeta> {
|
||||||
|
match self {
|
||||||
|
Self::MissingMetadata(_) => None,
|
||||||
|
Self::Complete(tx_with_meta) => Some(tx_with_meta.meta.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_transaction(&self) -> VersionedTransaction {
|
||||||
|
match self {
|
||||||
|
Self::MissingMetadata(transaction) => VersionedTransaction::from(transaction.clone()),
|
||||||
|
Self::Complete(tx_with_meta) => tx_with_meta.transaction.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn transaction_signature(&self) -> &Signature {
|
pub fn transaction_signature(&self) -> &Signature {
|
||||||
match self {
|
match self {
|
||||||
Self::MissingMetadata(transaction) => &transaction.signatures[0],
|
Self::MissingMetadata(transaction) => &transaction.signatures[0],
|
||||||
|
@ -673,20 +652,6 @@ impl TransactionWithStatusMeta {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_legacy_transaction_with_meta(self) -> Option<LegacyTransactionWithStatusMeta> {
|
|
||||||
match self {
|
|
||||||
TransactionWithStatusMeta::MissingMetadata(transaction) => {
|
|
||||||
Some(LegacyTransactionWithStatusMeta {
|
|
||||||
transaction,
|
|
||||||
meta: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
TransactionWithStatusMeta::Complete(tx_with_meta) => {
|
|
||||||
tx_with_meta.into_legacy_transaction_with_meta()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn account_keys(&self) -> AccountKeys {
|
pub fn account_keys(&self) -> AccountKeys {
|
||||||
match self {
|
match self {
|
||||||
Self::MissingMetadata(tx) => AccountKeys::new(&tx.message.account_keys, None),
|
Self::MissingMetadata(tx) => AccountKeys::new(&tx.message.account_keys, None),
|
||||||
|
@ -739,13 +704,6 @@ impl VersionedTransactionWithStatusMeta {
|
||||||
Some(&self.meta.loaded_addresses),
|
Some(&self.meta.loaded_addresses),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_legacy_transaction_with_meta(self) -> Option<LegacyTransactionWithStatusMeta> {
|
|
||||||
Some(LegacyTransactionWithStatusMeta {
|
|
||||||
transaction: self.transaction.into_legacy_transaction()?,
|
|
||||||
meta: Some(self.meta),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
@ -771,25 +729,7 @@ pub struct VersionedConfirmedTransactionWithStatusMeta {
|
||||||
pub block_time: Option<UnixTimestamp>,
|
pub block_time: Option<UnixTimestamp>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LegacyConfirmedTransactionWithStatusMeta {
|
|
||||||
pub slot: Slot,
|
|
||||||
pub tx_with_meta: LegacyTransactionWithStatusMeta,
|
|
||||||
pub block_time: Option<UnixTimestamp>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfirmedTransactionWithStatusMeta {
|
impl ConfirmedTransactionWithStatusMeta {
|
||||||
/// Downgrades a versioned confirmed transaction into a legacy
|
|
||||||
/// confirmed transaction if it contains a legacy transaction.
|
|
||||||
pub fn into_legacy_confirmed_transaction(
|
|
||||||
self,
|
|
||||||
) -> Option<LegacyConfirmedTransactionWithStatusMeta> {
|
|
||||||
Some(LegacyConfirmedTransactionWithStatusMeta {
|
|
||||||
tx_with_meta: self.tx_with_meta.into_legacy_transaction_with_meta()?,
|
|
||||||
block_time: self.block_time,
|
|
||||||
slot: self.slot,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn encode(
|
pub fn encode(
|
||||||
self,
|
self,
|
||||||
encoding: UiTransactionEncoding,
|
encoding: UiTransactionEncoding,
|
||||||
|
@ -803,6 +743,10 @@ impl ConfirmedTransactionWithStatusMeta {
|
||||||
block_time: self.block_time,
|
block_time: self.block_time,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_transaction(&self) -> VersionedTransaction {
|
||||||
|
self.tx_with_meta.get_transaction()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
@ -841,17 +785,29 @@ impl EncodableWithMeta for VersionedTransaction {
|
||||||
base64::encode(bincode::serialize(self).unwrap()),
|
base64::encode(bincode::serialize(self).unwrap()),
|
||||||
TransactionBinaryEncoding::Base64,
|
TransactionBinaryEncoding::Base64,
|
||||||
),
|
),
|
||||||
UiTransactionEncoding::Json | UiTransactionEncoding::JsonParsed => {
|
UiTransactionEncoding::Json => self.json_encode(),
|
||||||
EncodedTransaction::Json(UiTransaction {
|
UiTransactionEncoding::JsonParsed => EncodedTransaction::Json(UiTransaction {
|
||||||
signatures: self.signatures.iter().map(ToString::to_string).collect(),
|
signatures: self.signatures.iter().map(ToString::to_string).collect(),
|
||||||
message: match &self.message {
|
message: match &self.message {
|
||||||
VersionedMessage::Legacy(message) => message.encode(encoding),
|
VersionedMessage::Legacy(message) => {
|
||||||
VersionedMessage::V0(message) => message.encode_with_meta(encoding, meta),
|
message.encode(UiTransactionEncoding::JsonParsed)
|
||||||
},
|
}
|
||||||
})
|
VersionedMessage::V0(message) => {
|
||||||
}
|
message.encode_with_meta(UiTransactionEncoding::JsonParsed, meta)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn json_encode(&self) -> Self::Encoded {
|
||||||
|
EncodedTransaction::Json(UiTransaction {
|
||||||
|
signatures: self.signatures.iter().map(ToString::to_string).collect(),
|
||||||
|
message: match &self.message {
|
||||||
|
VersionedMessage::Legacy(message) => message.encode(UiTransactionEncoding::Json),
|
||||||
|
VersionedMessage::V0(message) => message.json_encode(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Encodable for Transaction {
|
impl Encodable for Transaction {
|
||||||
|
@ -880,23 +836,23 @@ impl Encodable for Transaction {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EncodedTransaction {
|
impl EncodedTransaction {
|
||||||
pub fn decode(&self) -> Option<Transaction> {
|
pub fn decode(&self) -> Option<VersionedTransaction> {
|
||||||
let transaction: Option<Transaction> = match self {
|
let (blob, encoding) = match self {
|
||||||
EncodedTransaction::Json(_) => None,
|
Self::Json(_) => return None,
|
||||||
EncodedTransaction::LegacyBinary(blob) => bs58::decode(blob)
|
Self::LegacyBinary(blob) => (blob, TransactionBinaryEncoding::Base58),
|
||||||
|
Self::Binary(blob, encoding) => (blob, *encoding),
|
||||||
|
};
|
||||||
|
|
||||||
|
let transaction: Option<VersionedTransaction> = match encoding {
|
||||||
|
TransactionBinaryEncoding::Base58 => bs58::decode(blob)
|
||||||
.into_vec()
|
.into_vec()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|bytes| bincode::deserialize(&bytes).ok()),
|
.and_then(|bytes| bincode::deserialize(&bytes).ok()),
|
||||||
EncodedTransaction::Binary(blob, encoding) => match *encoding {
|
TransactionBinaryEncoding::Base64 => base64::decode(blob)
|
||||||
TransactionBinaryEncoding::Base58 => bs58::decode(blob)
|
.ok()
|
||||||
.into_vec()
|
.and_then(|bytes| bincode::deserialize(&bytes).ok()),
|
||||||
.ok()
|
|
||||||
.and_then(|bytes| bincode::deserialize(&bytes).ok()),
|
|
||||||
TransactionBinaryEncoding::Base64 => base64::decode(blob)
|
|
||||||
.ok()
|
|
||||||
.and_then(|bytes| bincode::deserialize(&bytes).ok()),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
transaction.filter(|transaction| transaction.sanitize().is_ok())
|
transaction.filter(|transaction| transaction.sanitize().is_ok())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -966,17 +922,20 @@ impl EncodableWithMeta for v0::Message {
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
UiMessage::Raw(UiRawMessage {
|
self.json_encode()
|
||||||
header: self.header,
|
|
||||||
account_keys: self.account_keys.iter().map(ToString::to_string).collect(),
|
|
||||||
recent_blockhash: self.recent_blockhash.to_string(),
|
|
||||||
instructions: self.instructions.iter().map(Into::into).collect(),
|
|
||||||
address_table_lookups: Some(
|
|
||||||
self.address_table_lookups.iter().map(Into::into).collect(),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn json_encode(&self) -> Self::Encoded {
|
||||||
|
UiMessage::Raw(UiRawMessage {
|
||||||
|
header: self.header,
|
||||||
|
account_keys: self.account_keys.iter().map(ToString::to_string).collect(),
|
||||||
|
recent_blockhash: self.recent_blockhash.to_string(),
|
||||||
|
instructions: self.instructions.iter().map(Into::into).collect(),
|
||||||
|
address_table_lookups: Some(
|
||||||
|
self.address_table_lookups.iter().map(Into::into).collect(),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A duplicate representation of a Message, in raw format, for pretty JSON serialization
|
/// A duplicate representation of a Message, in raw format, for pretty JSON serialization
|
||||||
|
|
Loading…
Reference in New Issue