use solana_instruction::Instruction;
use solana_pubkey::Pubkey;
pub const PACKET_DATA_SIZE: usize = 1232;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstructionTooLargeError {
pub instruction_index: usize,
pub estimated_size: usize,
pub max_size: usize,
}
impl std::fmt::Display for InstructionTooLargeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"instruction at index {} exceeds max transaction size: {} > {}",
self.instruction_index, self.estimated_size, self.max_size
)
}
}
impl std::error::Error for InstructionTooLargeError {}
pub fn split_by_tx_size(
instructions: Vec<Instruction>,
payer: &Pubkey,
max_size: Option<usize>,
) -> Result<Vec<Vec<Instruction>>, InstructionTooLargeError> {
let max_size = max_size.unwrap_or(PACKET_DATA_SIZE);
if instructions.is_empty() {
return Ok(vec![]);
}
let mut batches = Vec::new();
let mut current_batch = Vec::new();
for (idx, ix) in instructions.into_iter().enumerate() {
let mut trial = current_batch.clone();
trial.push(ix.clone());
if estimate_tx_size(&trial, payer) > max_size {
let single_ix_size = estimate_tx_size(std::slice::from_ref(&ix), payer);
if single_ix_size > max_size {
return Err(InstructionTooLargeError {
instruction_index: idx,
estimated_size: single_ix_size,
max_size,
});
}
if !current_batch.is_empty() {
batches.push(current_batch);
}
current_batch = vec![ix];
} else {
current_batch.push(ix);
}
}
if !current_batch.is_empty() {
batches.push(current_batch);
}
Ok(batches)
}
fn count_signers(instructions: &[Instruction], payer: &Pubkey) -> usize {
let mut signers = vec![*payer];
for ix in instructions {
for meta in &ix.accounts {
if meta.is_signer && !signers.contains(&meta.pubkey) {
signers.push(meta.pubkey);
}
}
}
signers.len()
}
fn estimate_tx_size(instructions: &[Instruction], payer: &Pubkey) -> usize {
let num_signers = count_signers(instructions, payer);
let mut accounts = vec![*payer];
for ix in instructions {
if !accounts.contains(&ix.program_id) {
accounts.push(ix.program_id);
}
for meta in &ix.accounts {
if !accounts.contains(&meta.pubkey) {
accounts.push(meta.pubkey);
}
}
}
let mut size = 3;
size += compact_len(accounts.len()) + accounts.len() * 32;
size += 32;
size += compact_len(instructions.len());
for ix in instructions {
size += 1; size += compact_len(ix.accounts.len()) + ix.accounts.len();
size += compact_len(ix.data.len()) + ix.data.len();
}
size += compact_len(num_signers) + num_signers * 64;
size
}
#[inline]
fn compact_len(val: usize) -> usize {
if val < 0x80 {
1
} else if val < 0x4000 {
2
} else {
3
}
}
#[cfg(test)]
mod tests {
use solana_instruction::AccountMeta;
use super::*;
#[test]
fn test_split_by_tx_size() {
let payer = Pubkey::new_unique();
let instructions: Vec<Instruction> = (0..10)
.map(|_| Instruction {
program_id: Pubkey::new_unique(),
accounts: (0..10)
.map(|_| AccountMeta::new(Pubkey::new_unique(), false))
.collect(),
data: vec![0u8; 200],
})
.collect();
let batches = split_by_tx_size(instructions, &payer, None).unwrap();
assert!(batches.len() > 1);
for batch in &batches {
assert!(estimate_tx_size(batch, &payer) <= PACKET_DATA_SIZE);
}
}
#[test]
fn test_split_by_tx_size_oversized_instruction() {
let payer = Pubkey::new_unique();
let oversized_ix = Instruction {
program_id: Pubkey::new_unique(),
accounts: (0..5)
.map(|_| AccountMeta::new(Pubkey::new_unique(), false))
.collect(),
data: vec![0u8; 2000], };
let small_ix = Instruction {
program_id: Pubkey::new_unique(),
accounts: vec![AccountMeta::new(Pubkey::new_unique(), false)],
data: vec![0u8; 10],
};
let instructions = vec![small_ix.clone(), oversized_ix, small_ix];
let result = split_by_tx_size(instructions, &payer, None);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.instruction_index, 1);
assert!(err.estimated_size > err.max_size);
assert_eq!(err.max_size, PACKET_DATA_SIZE);
}
#[test]
fn test_signer_count_derived_from_metadata() {
let payer = Pubkey::new_unique();
let extra_signer = Pubkey::new_unique();
let ix_with_signer = Instruction {
program_id: Pubkey::new_unique(),
accounts: vec![
AccountMeta::new(Pubkey::new_unique(), false),
AccountMeta::new(extra_signer, true), ],
data: vec![0u8; 10],
};
let ix_no_signer = Instruction {
program_id: Pubkey::new_unique(),
accounts: vec![AccountMeta::new(Pubkey::new_unique(), false)],
data: vec![0u8; 10],
};
assert_eq!(
count_signers(std::slice::from_ref(&ix_no_signer), &payer),
1
);
assert_eq!(
count_signers(std::slice::from_ref(&ix_with_signer), &payer),
2
);
let ix_payer_signer = Instruction {
program_id: Pubkey::new_unique(),
accounts: vec![AccountMeta::new(payer, true)],
data: vec![0u8; 10],
};
assert_eq!(count_signers(&[ix_payer_signer], &payer), 1);
}
}