use crate::crypto::sign_message;
use crate::error::SolanaError;
use crate::types::{
CompiledInstruction, LegacyMessage, Message, MessageAddressTableLookup, Pubkey, SignatureBytes,
VersionedMessage, VersionedMessageV0, MAX_TRANSACTION_SIZE,
};
use crate::Result;
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct Transaction {
pub signatures: Vec<SignatureBytes>,
pub message: Message,
}
impl Transaction {
pub fn new(message: Message) -> Self {
Self {
signatures: Vec::new(),
message,
}
}
pub fn add_signature(&mut self, signature: SignatureBytes) {
self.signatures.push(signature);
}
pub fn num_required_signatures(&self) -> u8 {
self.message.num_required_signatures()
}
pub fn num_readonly_signed_accounts(&self) -> u8 {
self.message.num_readonly_signed_accounts()
}
pub fn num_readonly_unsigned_accounts(&self) -> u8 {
self.message.num_readonly_unsigned_accounts()
}
pub fn account_keys(&self) -> &[Pubkey] {
&self.message.account_keys
}
pub fn recent_blockhash(&self) -> &[u8; 32] {
&self.message.recent_blockhash
}
pub fn instructions(&self) -> &[CompiledInstruction] {
&self.message.instructions
}
pub fn deserialize_with_version(bytes: &[u8]) -> Result<Self> {
if bytes.is_empty() {
return Err(SolanaError::DeserializationError(
"Empty transaction data".to_string(),
));
}
let (num_signatures, len_bytes_consumed) = crate::decode_compact_u16_len(bytes)
.map_err(|e| SolanaError::DeserializationError(e.to_string()))?;
if bytes.len() < len_bytes_consumed + (num_signatures * 64) {
return Err(SolanaError::DeserializationError(
"Not enough bytes for signatures".to_string(),
));
}
let mut signatures = Vec::with_capacity(num_signatures);
let mut offset = len_bytes_consumed;
for _ in 0..num_signatures {
if offset + 64 > bytes.len() {
return Err(SolanaError::DeserializationError(
"Invalid signature data".to_string(),
));
}
let sig_bytes: [u8; 64] = bytes[offset..offset + 64].try_into().map_err(|_| {
SolanaError::DeserializationError("Failed to convert signature bytes".to_string())
})?;
signatures.push(SignatureBytes::new(sig_bytes));
offset += 64;
}
let message_bytes = &bytes[offset..];
match manual_decode::decode_legacy_message(message_bytes, Vec::new()) {
Ok(VersionedTransaction::Legacy { message, .. }) => {
let regular_message = Message {
header: message.header,
account_keys: message.account_keys,
recent_blockhash: message.recent_blockhash,
instructions: message.instructions,
};
Ok(Self {
signatures,
message: regular_message,
})
}
_ => Err(SolanaError::DeserializationError(
"Failed to decode legacy message for Transaction".to_string(),
)),
}
}
pub fn serialize_legacy(&self) -> Result<Vec<u8>> {
let mut tx_wire_bytes: Vec<u8> = Vec::new();
let sig_len_bytes = crate::encode_length_to_compact_u16_bytes(self.signatures.len())?;
tx_wire_bytes.extend_from_slice(&sig_len_bytes);
for sig_bytes_wrapper in &self.signatures {
tx_wire_bytes.extend_from_slice(sig_bytes_wrapper.as_bytes());
}
let serialized_message = self
.message
.serialize_for_signing()
.map_err(SolanaError::SerializationError)?;
tx_wire_bytes.extend_from_slice(&serialized_message);
Ok(tx_wire_bytes)
}
pub fn sign(&mut self, private_keys: &[&[u8]]) -> Result<()> {
let message_bytes = self
.message
.serialize_for_signing()
.map_err(SolanaError::SerializationError)?;
self.signatures.clear();
let num_required_sigs = self.message.header.num_required_signatures as usize;
if private_keys.len() < num_required_sigs {
return Err(SolanaError::InvalidSignature(format!(
"insufficient private keys: {}, required: {}",
private_keys.len(),
num_required_sigs
)));
}
for private_key in private_keys.iter().take(num_required_sigs) {
let signature = sign_message(private_key, &message_bytes)?;
self.signatures.push(signature);
}
Ok(())
}
pub fn partial_sign(&mut self, private_keys: &[&[u8]], public_keys: &[Pubkey]) -> Result<()> {
if private_keys.len() != public_keys.len() {
return Err(SolanaError::InvalidSignature(format!(
"private keys count ({}) does not match public keys count ({})",
private_keys.len(),
public_keys.len()
)));
}
let message_bytes = self
.message
.serialize_for_signing()
.map_err(SolanaError::SerializationError)?;
let num_required_sigs = self.message.header.num_required_signatures as usize;
if self.signatures.len() < num_required_sigs {
self.signatures
.resize(num_required_sigs, SignatureBytes::new([0u8; 64]));
}
for (private_key, public_key) in private_keys.iter().zip(public_keys.iter()) {
if let Some(index) = self
.message
.account_keys
.iter()
.position(|k| k == public_key)
{
if index < num_required_sigs {
let signature = sign_message(private_key, &message_bytes)?;
self.signatures[index] = signature;
}
}
}
Ok(())
}
pub fn is_signed(&self) -> bool {
let num_required = self.message.header.num_required_signatures as usize;
if self.signatures.len() < num_required {
return false;
}
for i in 0..num_required {
if self.signatures[i].as_bytes().iter().all(|&b| b == 0) {
return false;
}
}
true
}
pub fn validate_size(&self) -> Result<()> {
let serialized = self.serialize_legacy()?;
if serialized.len() > MAX_TRANSACTION_SIZE {
return Err(SolanaError::SerializationError(format!(
"Transaction size {} exceeds maximum of {} bytes",
serialized.len(),
MAX_TRANSACTION_SIZE
)));
}
Ok(())
}
}
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub enum VersionedTransaction {
Legacy {
signatures: Vec<SignatureBytes>,
message: LegacyMessage,
},
V0 {
signatures: Vec<SignatureBytes>,
message: VersionedMessageV0,
},
}
impl VersionedTransaction {
pub fn new(message: VersionedMessage) -> Self {
match message {
VersionedMessage::Legacy(msg) => Self::Legacy {
signatures: Vec::new(),
message: msg,
},
VersionedMessage::V0(msg) => Self::V0 {
signatures: Vec::new(),
message: msg,
},
}
}
pub fn add_signature(&mut self, signature: SignatureBytes) {
match self {
Self::Legacy { signatures, .. } => signatures.push(signature),
Self::V0 { signatures, .. } => signatures.push(signature),
}
}
pub fn num_required_signatures(&self) -> u8 {
match self {
Self::Legacy { message, .. } => message.header.num_required_signatures,
Self::V0 { message, .. } => message.header.num_required_signatures,
}
}
pub fn num_readonly_signed_accounts(&self) -> u8 {
match self {
Self::Legacy { message, .. } => message.header.num_readonly_signed_accounts,
Self::V0 { message, .. } => message.header.num_readonly_signed_accounts,
}
}
pub fn num_readonly_unsigned_accounts(&self) -> u8 {
match self {
Self::Legacy { message, .. } => message.header.num_readonly_unsigned_accounts,
Self::V0 { message, .. } => message.header.num_readonly_unsigned_accounts,
}
}
pub fn account_keys(&self) -> &[Pubkey] {
match self {
Self::Legacy { message, .. } => &message.account_keys,
Self::V0 { message, .. } => &message.account_keys,
}
}
pub fn recent_blockhash(&self) -> &[u8; 32] {
match self {
Self::Legacy { message, .. } => &message.recent_blockhash,
Self::V0 { message, .. } => &message.recent_blockhash,
}
}
pub fn instructions(&self) -> &[CompiledInstruction] {
match self {
Self::Legacy { message, .. } => &message.instructions,
Self::V0 { message, .. } => &message.instructions,
}
}
pub fn deserialize_with_version(bytes: &[u8]) -> Result<Self> {
if bytes.is_empty() {
return Err(SolanaError::DeserializationError(
"Empty transaction data".to_string(),
));
}
let (num_signatures, len_bytes_consumed) = crate::decode_compact_u16_len(bytes)
.map_err(|e| SolanaError::DeserializationError(e.to_string()))?;
if bytes.len() < len_bytes_consumed + (num_signatures * 64) {
return Err(SolanaError::DeserializationError(
"Not enough bytes for signatures".to_string(),
));
}
let mut signatures = Vec::with_capacity(num_signatures);
let mut offset = len_bytes_consumed;
for _ in 0..num_signatures {
if offset + 64 > bytes.len() {
return Err(SolanaError::DeserializationError(
"Invalid signature data".to_string(),
));
}
let sig_bytes: [u8; 64] = bytes[offset..offset + 64].try_into().map_err(|_| {
SolanaError::DeserializationError("Failed to convert signature bytes".to_string())
})?;
signatures.push(SignatureBytes::new(sig_bytes));
offset += 64;
}
let message_bytes = &bytes[offset..];
self::manual_decode::decode_message(message_bytes, signatures)
}
}
mod manual_decode {
use super::*;
use crate::types::MessageHeader;
pub fn decode_message(
bytes: &[u8],
signatures: Vec<SignatureBytes>,
) -> Result<VersionedTransaction> {
if bytes.len() < 3 {
return Err(SolanaError::DeserializationError(
"Message bytes too short, need at least 3 bytes for header".to_string(),
));
}
let is_versioned = (bytes[0] & 0x80) != 0;
if is_versioned {
let version = bytes[0] & 0x7F;
if version == 0 {
decode_v0_message(&bytes[1..], signatures)
} else {
Err(SolanaError::DeserializationError(format!(
"Unsupported message version: {version}"
)))
}
} else {
decode_legacy_message(bytes, signatures)
}
}
pub fn decode_legacy_message(
bytes: &[u8],
signatures: Vec<SignatureBytes>,
) -> Result<VersionedTransaction> {
if bytes.len() < 3 {
return Err(SolanaError::DeserializationError(
"Legacy message too short".to_string(),
));
}
let header = MessageHeader {
num_required_signatures: bytes[0],
num_readonly_signed_accounts: bytes[1],
num_readonly_unsigned_accounts: bytes[2],
};
let mut offset = 3;
if offset >= bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: no account count".to_string(),
));
}
let (account_count, len_bytes_consumed) =
crate::decode_compact_u16_len(&bytes[offset..]).map_err(SolanaError::from)?;
offset += len_bytes_consumed;
if offset + (account_count * 32) > bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: not enough bytes for accounts".to_string(),
));
}
let mut account_keys = Vec::with_capacity(account_count);
for _ in 0..account_count {
let mut key = [0u8; 32];
key.copy_from_slice(&bytes[offset..offset + 32]);
account_keys.push(Pubkey::new(key));
offset += 32;
}
if offset + 32 > bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: no recent blockhash".to_string(),
));
}
let mut recent_blockhash = [0u8; 32];
recent_blockhash.copy_from_slice(&bytes[offset..offset + 32]);
offset += 32;
if offset >= bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: no instruction count".to_string(),
));
}
let (instruction_count, len_bytes_consumed) =
crate::decode_compact_u16_len(&bytes[offset..]).map_err(SolanaError::from)?;
offset += len_bytes_consumed;
let mut instructions = Vec::with_capacity(instruction_count);
for _ in 0..instruction_count {
if offset >= bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: incomplete instruction".to_string(),
));
}
let program_id_index = bytes[offset];
offset += 1;
if offset >= bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: no account indices count".to_string(),
));
}
let (account_indices_count, len_bytes_consumed) =
crate::decode_compact_u16_len(&bytes[offset..]).map_err(SolanaError::from)?;
offset += len_bytes_consumed;
if offset + account_indices_count > bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: not enough account indices".to_string(),
));
}
let accounts = bytes[offset..offset + account_indices_count].to_vec();
offset += account_indices_count;
if offset >= bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: no instruction data length".to_string(),
));
}
let (data_length, len_bytes_consumed) =
crate::decode_compact_u16_len(&bytes[offset..]).map_err(SolanaError::from)?;
offset += len_bytes_consumed;
if offset + data_length > bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: not enough instruction data".to_string(),
));
}
let data = bytes[offset..offset + data_length].to_vec();
offset += data_length;
instructions.push(CompiledInstruction {
program_id_index,
accounts,
data,
});
}
Ok(VersionedTransaction::Legacy {
signatures,
message: LegacyMessage {
header,
account_keys,
recent_blockhash,
instructions,
},
})
}
pub fn decode_v0_message(
bytes: &[u8],
signatures: Vec<SignatureBytes>,
) -> Result<VersionedTransaction> {
if bytes.len() < 3 {
return Err(SolanaError::DeserializationError(
"V0 message too short".to_string(),
));
}
let header = MessageHeader {
num_required_signatures: bytes[0],
num_readonly_signed_accounts: bytes[1],
num_readonly_unsigned_accounts: bytes[2],
};
let mut offset = 3;
if offset >= bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: no account count".to_string(),
));
}
let (account_count, len_bytes_consumed) =
crate::decode_compact_u16_len(&bytes[offset..]).map_err(SolanaError::from)?;
offset += len_bytes_consumed;
if offset + (account_count * 32) > bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: not enough bytes for accounts".to_string(),
));
}
let mut account_keys = Vec::with_capacity(account_count);
for _ in 0..account_count {
let mut key = [0u8; 32];
key.copy_from_slice(&bytes[offset..offset + 32]);
account_keys.push(Pubkey::new(key));
offset += 32;
}
if offset + 32 > bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: no recent blockhash".to_string(),
));
}
let mut recent_blockhash = [0u8; 32];
recent_blockhash.copy_from_slice(&bytes[offset..offset + 32]);
offset += 32;
if offset >= bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: no instruction count".to_string(),
));
}
let (instruction_count, len_bytes_consumed) =
crate::decode_compact_u16_len(&bytes[offset..]).map_err(SolanaError::from)?;
offset += len_bytes_consumed;
let mut instructions = Vec::with_capacity(instruction_count);
for _ in 0..instruction_count {
if offset >= bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: incomplete instruction".to_string(),
));
}
let program_id_index = bytes[offset];
offset += 1;
if offset >= bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: no account indices count".to_string(),
));
}
let (account_indices_count, len_bytes_consumed) =
crate::decode_compact_u16_len(&bytes[offset..]).map_err(SolanaError::from)?;
offset += len_bytes_consumed;
if offset + account_indices_count > bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: not enough account indices".to_string(),
));
}
let accounts = bytes[offset..offset + account_indices_count].to_vec();
offset += account_indices_count;
if offset >= bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: no instruction data length".to_string(),
));
}
let (data_length, len_bytes_consumed) =
crate::decode_compact_u16_len(&bytes[offset..]).map_err(SolanaError::from)?;
offset += len_bytes_consumed;
if offset + data_length > bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: not enough instruction data".to_string(),
));
}
let data = bytes[offset..offset + data_length].to_vec();
offset += data_length;
instructions.push(CompiledInstruction {
program_id_index,
accounts,
data,
});
}
let mut address_table_lookups = Vec::new();
if offset < bytes.len() {
let (lookup_table_count, len_bytes_consumed) =
crate::decode_compact_u16_len(&bytes[offset..])
.map_err(|e| SolanaError::DeserializationError(e.to_string()))?;
offset += len_bytes_consumed;
for _ in 0..lookup_table_count {
if offset + 32 > bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: incomplete address lookup table".to_string(),
));
}
let mut key = [0u8; 32];
key.copy_from_slice(&bytes[offset..offset + 32]);
let lookup_table_key = Pubkey::new(key);
offset += 32;
if offset >= bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: no writable indexes count".to_string(),
));
}
let (writable_indexes_count, len_bytes_consumed) =
crate::decode_compact_u16_len(&bytes[offset..])
.map_err(|e| SolanaError::DeserializationError(e.to_string()))?;
offset += len_bytes_consumed;
if offset + writable_indexes_count > bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: not enough writable indexes".to_string(),
));
}
let writable_indexes = bytes[offset..offset + writable_indexes_count].to_vec();
offset += writable_indexes_count;
if offset >= bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: no readonly indexes count".to_string(),
));
}
let (readonly_indexes_count, len_bytes_consumed) =
crate::decode_compact_u16_len(&bytes[offset..])
.map_err(|e| SolanaError::DeserializationError(e.to_string()))?;
offset += len_bytes_consumed;
if offset + readonly_indexes_count > bytes.len() {
return Err(SolanaError::DeserializationError(
"Message too short: not enough readonly indexes".to_string(),
));
}
let readonly_indexes = bytes[offset..offset + readonly_indexes_count].to_vec();
offset += readonly_indexes_count;
address_table_lookups.push(MessageAddressTableLookup {
account_key: lookup_table_key,
writable_indexes,
readonly_indexes,
});
}
}
Ok(VersionedTransaction::V0 {
signatures,
message: VersionedMessageV0 {
header,
account_keys,
recent_blockhash,
instructions,
address_table_lookups,
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
types::{CompiledInstruction, MessageHeader, Pubkey, SignatureBytes},
VersionedMessageV0,
};
fn create_test_message() -> Message {
let header = MessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1,
};
let account_keys = vec![Pubkey::new([0; 32]), Pubkey::new([1; 32])];
let recent_blockhash = [0u8; 32];
let instructions = vec![CompiledInstruction {
program_id_index: 1,
accounts: vec![0],
data: vec![],
}];
Message::new(header, account_keys, recent_blockhash, instructions)
}
#[test]
fn test_transaction() {
let message = create_test_message();
let mut transaction = Transaction::new(message);
assert_eq!(transaction.signatures.len(), 0);
assert_eq!(transaction.num_required_signatures(), 1);
assert_eq!(transaction.num_readonly_signed_accounts(), 0);
assert_eq!(transaction.num_readonly_unsigned_accounts(), 1);
let signature = SignatureBytes::new([0; 64]);
transaction.add_signature(signature);
assert_eq!(transaction.signatures.len(), 1);
}
#[test]
fn test_versioned_transaction() {
let message = VersionedMessage::V0(VersionedMessageV0 {
header: MessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1,
},
account_keys: vec![Pubkey::new([0; 32]), Pubkey::new([1; 32])],
recent_blockhash: [0u8; 32],
instructions: vec![CompiledInstruction {
program_id_index: 1,
accounts: vec![0],
data: vec![],
}],
address_table_lookups: Vec::new(),
});
let mut transaction = VersionedTransaction::new(message);
match &transaction {
VersionedTransaction::V0 { signatures, .. } => {
assert_eq!(signatures.len(), 0);
}
_ => panic!("Expected V0 transaction"),
}
assert_eq!(transaction.num_required_signatures(), 1);
assert_eq!(transaction.num_readonly_signed_accounts(), 0);
assert_eq!(transaction.num_readonly_unsigned_accounts(), 1);
let signature = SignatureBytes::new([0; 64]);
transaction.add_signature(signature);
match &transaction {
VersionedTransaction::V0 { signatures, .. } => {
assert_eq!(signatures.len(), 1);
}
_ => panic!("Expected V0 transaction"),
}
}
}