diff --git a/solana-programs/Cargo.lock b/solana-programs/Cargo.lock index 8ae2b25..62a3523 100644 --- a/solana-programs/Cargo.lock +++ b/solana-programs/Cargo.lock @@ -596,7 +596,7 @@ dependencies = [ [[package]] name = "cron" -version = "0.2.5" +version = "0.2.6" dependencies = [ "anchor-lang", "anchor-spl", @@ -2756,7 +2756,7 @@ dependencies = [ [[package]] name = "tuktuk" -version = "0.2.4" +version = "0.2.6" dependencies = [ "anchor-lang", "anchor-spl", diff --git a/solana-programs/programs/tuktuk/Cargo.toml b/solana-programs/programs/tuktuk/Cargo.toml index 3a7b7d6..017a46c 100644 --- a/solana-programs/programs/tuktuk/Cargo.toml +++ b/solana-programs/programs/tuktuk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuktuk" -version = "0.2.5" +version = "0.2.6" description = "Created with Anchor" edition = "2021" diff --git a/solana-programs/programs/tuktuk/src/instructions/dequeue_task_v0.rs b/solana-programs/programs/tuktuk/src/instructions/dequeue_task_v0.rs index 7af12d5..2236262 100644 --- a/solana-programs/programs/tuktuk/src/instructions/dequeue_task_v0.rs +++ b/solana-programs/programs/tuktuk/src/instructions/dequeue_task_v0.rs @@ -1,6 +1,6 @@ use anchor_lang::prelude::*; -use crate::state::{TaskQueueAuthorityV0, TaskQueueV0, TaskV0}; +use crate::state::{TaskQueueAuthorityV0, TaskQueueDataWrapper, TaskV0}; #[derive(Accounts)] pub struct DequeuetaskV0<'info> { @@ -12,21 +12,23 @@ pub struct DequeuetaskV0<'info> { seeds = [b"task_queue_authority", task_queue.key().as_ref(), queue_authority.key().as_ref()], bump = task_queue_authority.bump_seed, )] - pub task_queue_authority: Box>, + pub task_queue_authority: Account<'info, TaskQueueAuthorityV0>, + /// CHECK: We manually deserialize this using TaskQueueDataWrapper for memory efficiency #[account(mut)] - pub task_queue: Box>, + pub task_queue: UncheckedAccount<'info>, #[account( mut, close = rent_refund, has_one = rent_refund, has_one = task_queue, )] - pub task: Box>, + pub task: Account<'info, TaskV0>, } pub fn handler(ctx: Context) -> Result<()> { - ctx.accounts - .task_queue - .set_task_exists(ctx.accounts.task.id, false); + let task_queue_account_info = ctx.accounts.task_queue.to_account_info(); + let mut task_queue_data = task_queue_account_info.try_borrow_mut_data()?; + let mut task_queue = TaskQueueDataWrapper::new(*task_queue_data)?; + task_queue.set_task_exists(ctx.accounts.task.id, false); Ok(()) } diff --git a/solana-programs/programs/tuktuk/src/instructions/queue_task_v0.rs b/solana-programs/programs/tuktuk/src/instructions/queue_task_v0.rs index efdcaa1..1818d66 100644 --- a/solana-programs/programs/tuktuk/src/instructions/queue_task_v0.rs +++ b/solana-programs/programs/tuktuk/src/instructions/queue_task_v0.rs @@ -5,8 +5,7 @@ use anchor_lang::{ use crate::{ error::ErrorCode, - resize_to_fit::resize_to_fit, - state::{TaskQueueAuthorityV0, TaskQueueV0, TaskV0, TransactionSourceV0, TriggerV0}, + state::{TaskQueueAuthorityV0, TaskQueueDataWrapper, TaskV0, TransactionSourceV0, TriggerV0}, }; #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] @@ -34,25 +33,39 @@ pub struct QueueTaskV0<'info> { seeds = [b"task_queue_authority", task_queue.key().as_ref(), queue_authority.key().as_ref()], bump = task_queue_authority.bump_seed, )] - pub task_queue_authority: Box>, + pub task_queue_authority: Account<'info, TaskQueueAuthorityV0>, + /// CHECK: We manually deserialize this using TaskQueueDataWrapper for memory efficiency #[account(mut)] - pub task_queue: Box>, + pub task_queue: UncheckedAccount<'info>, #[account( init, payer = payer, - space = 8 + std::mem::size_of::() + 60 + args.description.len(), - constraint = !task_queue.task_exists(args.id) @ ErrorCode::TaskAlreadyExists, - constraint = args.id < task_queue.capacity, + space = 8 + std::mem::size_of::() + args.transaction.size() + 4 + args.description.len() + 60, seeds = [b"task".as_ref(), task_queue.key().as_ref(), &args.id.to_le_bytes()[..]], bump, )] - pub task: Box>, + pub task: Account<'info, TaskV0>, pub system_program: Program<'info, System>, } pub fn handler(ctx: Context, args: QueueTaskArgsV0) -> Result<()> { + // Use memory-efficient wrapper to avoid deserializing the entire task queue + let task_queue_account_info = ctx.accounts.task_queue.to_account_info(); + let mut task_queue_data = task_queue_account_info.try_borrow_mut_data()?; + let mut task_queue = TaskQueueDataWrapper::new(*task_queue_data)?; + + // Validate constraints that were removed from the account struct + require!( + !task_queue.task_exists(args.id), + ErrorCode::TaskAlreadyExists + ); + require!( + args.id < task_queue.header().capacity, + ErrorCode::InvalidTaskId + ); + require_gte!( - ctx.accounts.task_queue.capacity, + task_queue.header().capacity, (args.free_tasks + 1) as u16, ErrorCode::FreeTasksGreaterThanCapacity ); @@ -63,15 +76,14 @@ pub fn handler(ctx: Context, args: QueueTaskArgsV0) -> Result<()> { ); let crank_reward = args .crank_reward - .unwrap_or(ctx.accounts.task_queue.min_crank_reward); - require_gte!(crank_reward, ctx.accounts.task_queue.min_crank_reward); + .unwrap_or(task_queue.header().min_crank_reward); + require_gte!(crank_reward, task_queue.header().min_crank_reward); - let mut transaction = args.transaction.clone(); - if let TransactionSourceV0::CompiledV0(mut compiled_tx) = transaction { + let mut transaction = args.transaction; + if let TransactionSourceV0::CompiledV0(ref mut compiled_tx) = transaction { compiled_tx .accounts .extend(ctx.remaining_accounts.iter().map(|a| a.key())); - transaction = TransactionSourceV0::CompiledV0(compiled_tx); } ctx.accounts.task.set_inner(TaskV0 { free_tasks: args.free_tasks, @@ -86,14 +98,14 @@ pub fn handler(ctx: Context, args: QueueTaskArgsV0) -> Result<()> { bump_seed: ctx.bumps.task, queued_at: Clock::get()?.unix_timestamp, }); - ctx.accounts.task_queue.set_task_exists(args.id, true); - ctx.accounts.task_queue.updated_at = Clock::get()?.unix_timestamp; - resize_to_fit( - &ctx.accounts.payer.to_account_info(), - &ctx.accounts.system_program.to_account_info(), - &ctx.accounts.task, - )?; + // Update the task queue bitmap and metadata + task_queue.set_task_exists(args.id, true); + task_queue.header_mut().updated_at = Clock::get()?.unix_timestamp; + task_queue.save()?; + + // Drop the borrow + drop(task_queue_data); let rented_amount = ctx.accounts.task.to_account_info().lamports(); ctx.accounts.task.rent_amount = rented_amount; diff --git a/solana-programs/programs/tuktuk/src/instructions/run_task_v0.rs b/solana-programs/programs/tuktuk/src/instructions/run_task_v0.rs index de79637..1e6b34f 100644 --- a/solana-programs/programs/tuktuk/src/instructions/run_task_v0.rs +++ b/solana-programs/programs/tuktuk/src/instructions/run_task_v0.rs @@ -14,8 +14,8 @@ use anchor_lang::{ use crate::{ error::ErrorCode, state::{ - CompiledInstructionV0, CompiledTransactionV0, TaskQueueV0, TaskV0, TransactionSourceV0, - TriggerV0, + CompiledInstructionV0, CompiledTransactionV0, TaskQueueDataWrapper, TaskV0, + TransactionSourceV0, TriggerV0, }, task_seeds, utils, }; @@ -113,8 +113,9 @@ pub struct RunTaskV0<'info> { /// CHECK: Via has one #[account(mut)] pub rent_refund: AccountInfo<'info>, + /// CHECK: We manually deserialize this using TaskQueueDataWrapper for memory efficiency #[account(mut)] - pub task_queue: Account<'info, TaskQueueV0>, + pub task_queue: UncheckedAccount<'info>, #[account( mut, has_one = task_queue, @@ -139,6 +140,12 @@ struct TaskProcessor<'a, 'info> { free_task_index: usize, signer_addresses: std::collections::HashSet, signers: Vec>>, + // Task queue data we need for validation + min_crank_reward: u64, + capacity: u16, + // Changes to make to task queue + tasks_to_set: Vec, // Task IDs to set as existing + queue_lamports_needed: u64, } impl<'a, 'info> TaskProcessor<'a, 'info> { @@ -146,6 +153,8 @@ impl<'a, 'info> TaskProcessor<'a, 'info> { ctx: Context<'a, 'a, 'a, 'info, RunTaskV0<'info>>, transaction: &'a CompiledTransactionV0, mut free_task_ids: Vec, + min_crank_reward: u64, + capacity: u16, ) -> Result { free_task_ids.reverse(); @@ -177,6 +186,10 @@ impl<'a, 'info> TaskProcessor<'a, 'info> { free_task_index: transaction.accounts.len(), signer_addresses, signers: signers_inner_u8, + min_crank_reward, + capacity, + tasks_to_set: Vec::new(), + queue_lamports_needed: 0, }) } @@ -237,6 +250,7 @@ impl<'a, 'info> TaskProcessor<'a, 'info> { .map(|s| s.as_slice()) .collect::>(), )?; + msg!("Invoked"); if let Some((_, return_data)) = solana_program::program::get_return_data() { match self.process_return_data(&return_data, &accounts) { @@ -299,33 +313,27 @@ impl<'a, 'info> TaskProcessor<'a, 'info> { task.description.len(), ErrorCode::InvalidDescriptionLength ); + require_gte!( - task.crank_reward - .unwrap_or(self.ctx.accounts.task_queue.min_crank_reward), - self.ctx.accounts.task_queue.min_crank_reward, + task.crank_reward.unwrap_or(self.min_crank_reward), + self.min_crank_reward, ErrorCode::InvalidCrankReward ); require_gte!( - self.ctx.accounts.task_queue.capacity, + self.capacity, (task.free_tasks + 1) as u16, ErrorCode::FreeTasksGreaterThanCapacity ); let free_task_account = &self.ctx.remaining_accounts[self.free_task_index]; self.free_task_index += 1; - let task_queue = &mut self.ctx.accounts.task_queue; - let task_queue_key = task_queue.key(); + let task_queue_key = self.ctx.accounts.task_queue.key(); let task_id = self .free_task_ids .pop() .ok_or(error!(ErrorCode::TooManyReturnedTasks))?; - require!( - !task_queue.task_exists(task_id), - ErrorCode::TaskIdAlreadyInUse - ); - // Verify the account is empty require!( free_task_account.data_is_empty(), @@ -343,29 +351,21 @@ impl<'a, 'info> TaskProcessor<'a, 'info> { rent_refund: task_queue_key, trigger: task.trigger.clone(), transaction: task.transaction.clone(), - crank_reward: task.crank_reward.unwrap_or(task_queue.min_crank_reward), + crank_reward: task.crank_reward.unwrap_or(self.min_crank_reward), bump_seed, queued_at: Clock::get()?.unix_timestamp, free_tasks: task.free_tasks, rent_amount: 0, }; - task_queue.set_task_exists(task_data.id, true); + // Track that we need to set this task as existing + self.tasks_to_set.push(task_data.id); let task_size = task_data.try_to_vec()?.len() + 8 + 60; let rent_lamports = Rent::get()?.minimum_balance(task_size); let lamports = rent_lamports + task_data.crank_reward; task_data.rent_amount = rent_lamports; - let task_queue_info = self.ctx.accounts.task_queue.to_account_info(); - let task_queue_min_lamports = Rent::get()?.minimum_balance(task_queue_info.data_len()); - - require_gt!( - task_queue_info.lamports(), - task_queue_min_lamports + lamports, - ErrorCode::TaskQueueInsufficientFunds - ); - system_program::assign( CpiContext::new_with_signer( self.ctx.accounts.system_program.to_account_info(), @@ -396,13 +396,21 @@ impl<'a, 'info> TaskProcessor<'a, 'info> { } if lamports_needed_from_queue > 0 { - task_queue_info.sub_lamports(lamports_needed_from_queue)?; + self.queue_lamports_needed += lamports_needed_from_queue; free_task_account.add_lamports(lamports_needed_from_queue)?; } let mut data = free_task_account.try_borrow_mut_data()?; task_data.try_serialize(&mut data.as_mut()) } + + fn get_tasks_to_set(&self) -> &[u16] { + &self.tasks_to_set + } + + fn get_queue_lamports_needed(&self) -> u64 { + self.queue_lamports_needed + } } pub fn handler<'info>( @@ -414,20 +422,21 @@ pub fn handler<'info>( TriggerV0::Now => now, TriggerV0::Timestamp(timestamp) => timestamp, }; - ctx.accounts.task_queue.updated_at = now; + + // Use memory-efficient wrapper to avoid deserializing the entire task queue + let task_queue_account_info = ctx.accounts.task_queue.to_account_info().clone(); + let task_queue_min_lamports = Rent::get()?.minimum_balance(task_queue_account_info.data_len()); + let mut task_queue_data = task_queue_account_info.try_borrow_mut_data()?; + let mut task_queue = TaskQueueDataWrapper::new(*task_queue_data)?; + + task_queue.header_mut().updated_at = now; + // Check for duplicate task IDs let mut seen_ids = std::collections::HashSet::new(); for id in args.free_task_ids.clone() { - require_gt!( - ctx.accounts.task_queue.capacity, - id, - ErrorCode::InvalidTaskId - ); + require_gte!(task_queue.header().capacity, id, ErrorCode::InvalidTaskId); // Ensure ID is not already in use in the task queue - require!( - !ctx.accounts.task_queue.task_exists(id), - ErrorCode::TaskIdAlreadyInUse - ); + require!(!task_queue.task_exists(id), ErrorCode::TaskIdAlreadyInUse); // Check for duplicates in provided IDs require!(seen_ids.insert(id), ErrorCode::DuplicateTaskIds); } @@ -508,20 +517,15 @@ pub fn handler<'info>( }; // Handle rewards - let reward = ctx.accounts.task.crank_reward; - // let protocol_fee = reward.checked_mul(5).unwrap().checked_div(100).unwrap(); - let protocol_fee = 0; - let task_fee = reward.checked_sub(protocol_fee).unwrap(); + let task_fee = ctx.accounts.task.crank_reward; let task_info = ctx.accounts.task.to_account_info(); let crank_turner_info = ctx.accounts.crank_turner.to_account_info(); - let task_queue_info = ctx.accounts.task_queue.to_account_info(); - ctx.accounts.task_queue.uncollected_protocol_fees += protocol_fee; + task_queue.set_task_exists(ctx.accounts.task.id, false); - ctx.accounts - .task_queue - .set_task_exists(ctx.accounts.task.id, false); + // Save the task queue changes + task_queue.save()?; // Validate that all free task accounts are empty and are valid PDAs let free_tasks_start_index = transaction.accounts.len(); @@ -532,8 +536,21 @@ pub fn handler<'info>( ErrorCode::MismatchedFreeTaskCounts ); - if now.saturating_sub(task_time) <= ctx.accounts.task_queue.stale_task_age as i64 { - let mut processor = TaskProcessor::new(ctx, &transaction, args.free_task_ids)?; + let stale_task_age = task_queue.stale_task_age(); + let min_crank_reward = task_queue.header().min_crank_reward; + let capacity = task_queue.header().capacity; + + if now.saturating_sub(task_time) <= stale_task_age as i64 { + task_queue.save()?; + // We can't hold on to a mutable reference because inner instructions may use the task queue. + drop(task_queue_data); + let mut processor = TaskProcessor::new( + ctx, + &transaction, + args.free_task_ids, + min_crank_reward, + capacity, + )?; // Validate account keys match for (i, account) in transaction.accounts.iter().enumerate() { @@ -548,6 +565,37 @@ pub fn handler<'info>( for ix in &transaction.instructions { processor.process_instruction(ix, remaining_accounts)?; } + + // Get the changes we need to make + let tasks_to_set = processor.get_tasks_to_set().to_vec(); + let queue_lamports_needed = processor.get_queue_lamports_needed(); + + drop(processor); + let task_queue_current_lamports = task_queue_account_info.lamports(); + if queue_lamports_needed > 0 { + msg!( + "Need {} lamports from the task queue to fund tasks. Task queue has {} lamports.", + queue_lamports_needed, + task_queue_current_lamports + ); + } + require_gt!( + task_queue_current_lamports.saturating_sub(queue_lamports_needed), + task_queue_min_lamports, + ErrorCode::TaskQueueInsufficientFunds + ); + + if queue_lamports_needed > 0 { + task_queue_account_info.sub_lamports(queue_lamports_needed)?; + } + + let mut task_queue_data = task_queue_account_info.try_borrow_mut_data()?; + let mut task_queue = TaskQueueDataWrapper::new(*task_queue_data)?; + + // Apply the changes to the task queue + for task_id in tasks_to_set { + task_queue.set_task_exists(task_id, true); + } } else { msg!( "Task is stale with run time {:?}, current time {:?}, closing task", @@ -556,16 +604,10 @@ pub fn handler<'info>( ); } - msg!( - "Paying out reward {:?}, crank turner gets {:?}, protocol fee {:?}", - reward, - task_fee, - protocol_fee - ); + msg!("Paying out reward {:?}", task_fee); - task_info.sub_lamports(reward)?; + task_info.sub_lamports(task_fee)?; crank_turner_info.add_lamports(task_fee)?; - task_queue_info.add_lamports(protocol_fee)?; Ok(()) } diff --git a/solana-programs/programs/tuktuk/src/state.rs b/solana-programs/programs/tuktuk/src/state.rs index c56fe0c..824dd75 100644 --- a/solana-programs/programs/tuktuk/src/state.rs +++ b/solana-programs/programs/tuktuk/src/state.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use borsh::{BorshDeserialize, BorshSerialize}; #[account] #[derive(Default, InitSpace)] @@ -46,6 +47,22 @@ pub struct TaskQueueV0 { pub stale_task_age: u32, } +/// Header portion of TaskQueueV0 for memory-efficient access +#[derive(BorshSerialize, BorshDeserialize, Clone)] +pub struct TaskQueueHeader { + pub tuktuk_config: Pubkey, + pub id: u32, + pub update_authority: Pubkey, + pub reserved: Pubkey, + pub min_crank_reward: u64, + pub uncollected_protocol_fees: u64, + pub capacity: u16, + pub created_at: i64, + pub updated_at: i64, + pub bump_seed: u8, + pub bitmap_size: u32, +} + #[macro_export] macro_rules! task_queue_seeds { ($task_queue:expr) => { @@ -98,6 +115,127 @@ impl TaskQueueV0 { } } +/// Memory-efficient wrapper for TaskQueueV0 that avoids deserializing the entire bitmap +pub struct TaskQueueDataWrapper<'a> { + data: &'a mut [u8], + header: TaskQueueHeader, + bitmap_offset: usize, + stale_task_age_offset: usize, +} + +impl<'a> TaskQueueDataWrapper<'a> { + /// The size of the Anchor discriminator (8 bytes) + pub const DISCRIMINATOR_SIZE: usize = 8; + /// The size of the TaskQueueHeader in bytes (32+4+32+32+8+8+2+8+8+1+4 = 139 bytes) + pub const HEADER_SIZE: usize = 139; + /// Total offset to the bitmap + pub const BITMAP_OFFSET: usize = Self::DISCRIMINATOR_SIZE + Self::HEADER_SIZE; + + /// Create a new wrapper from account data + /// Validates the discriminator and deserializes only the header + pub fn new(data: &'a mut [u8]) -> Result { + // Verify we have enough data for discriminator + header + require_gte!( + data.len(), + Self::BITMAP_OFFSET, + anchor_lang::error::ErrorCode::AccountDidNotDeserialize + ); + + // Verify the discriminator matches TaskQueueV0 + require!( + &data[0..8] == TaskQueueV0::DISCRIMINATOR, + anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch + ); + + // Deserialize only the header + let header = + TaskQueueHeader::deserialize(&mut &data[Self::DISCRIMINATOR_SIZE..Self::BITMAP_OFFSET]) + .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize)?; + + // Calculate offsets for fields after the bitmap + let bitmap_size = header.bitmap_size as usize; + let name_offset = Self::BITMAP_OFFSET + bitmap_size; + + // Read name length (4 bytes) and calculate name data size + let name_len = u32::from_le_bytes([ + data[name_offset], + data[name_offset + 1], + data[name_offset + 2], + data[name_offset + 3], + ]) as usize; + let lookup_tables_offset = name_offset + 4 + name_len; + + // Read lookup tables length (4 bytes) and calculate lookup tables data size + let lookup_tables_len = u32::from_le_bytes([ + data[lookup_tables_offset], + data[lookup_tables_offset + 1], + data[lookup_tables_offset + 2], + data[lookup_tables_offset + 3], + ]) as usize; + let stale_task_age_offset = lookup_tables_offset + 4 + (lookup_tables_len * 32) + 2; // +2 for num_queue_authorities + + Ok(Self { + data, + header, + bitmap_offset: Self::BITMAP_OFFSET, + stale_task_age_offset, + }) + } + + /// Get reference to the header + pub fn header(&self) -> &TaskQueueHeader { + &self.header + } + + /// Get mutable reference to the header + pub fn header_mut(&mut self) -> &mut TaskQueueHeader { + &mut self.header + } + + /// Check if a task exists at the given index + pub fn task_exists(&self, task_idx: u16) -> bool { + let byte_idx = self.bitmap_offset + (task_idx as usize / 8); + let bit_idx = task_idx % 8; + self.data[byte_idx] & (1 << bit_idx) != 0 + } + + /// Set whether a task exists at the given index + pub fn set_task_exists(&mut self, task_idx: u16, exists: bool) { + let byte_idx = self.bitmap_offset + (task_idx as usize / 8); + let bit_idx = task_idx % 8; + if exists { + self.data[byte_idx] |= 1 << bit_idx; + } else { + self.data[byte_idx] &= !(1 << bit_idx); + } + } + + /// Get the stale task age + pub fn stale_task_age(&self) -> u32 { + let bytes = &self.data[self.stale_task_age_offset..self.stale_task_age_offset + 4]; + u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) + } + + /// Set the stale task age + pub fn set_stale_task_age(&mut self, age: u32) { + let bytes = age.to_le_bytes(); + self.data[self.stale_task_age_offset..self.stale_task_age_offset + 4] + .copy_from_slice(&bytes); + } + + /// Persist the header changes back to the data + pub fn save(&mut self) -> Result<()> { + let mut header_bytes = Vec::new(); + self.header + .serialize(&mut header_bytes) + .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotSerialize)?; + + self.data[Self::DISCRIMINATOR_SIZE..Self::BITMAP_OFFSET].copy_from_slice(&header_bytes); + + Ok(()) + } +} + #[account] #[derive(Default, InitSpace)] pub struct TaskQueueNameMappingV0 { @@ -166,7 +304,7 @@ pub struct CompiledInstructionV0 { impl CompiledInstructionV0 { pub fn size(&self) -> usize { - 1 + self.accounts.len() + self.data.len() + 1 + 4 + self.accounts.len() + 4 + self.data.len() } } @@ -192,6 +330,37 @@ pub struct CompiledTransactionV0 { impl CompiledTransactionV0 { pub fn size(&self) -> usize { - 1 + self.accounts.len() + self.instructions.iter().map(|i| i.size()).sum::() + // Calculate the size of the transaction header (3 u8 fields + 1 byte for instruction count) + let header_size = 3 + 1; + + // Calculate the maximum account index across all instructions + let max_accounts = 1 + self + .instructions + .iter() + .flat_map(|i| i.accounts.iter()) + .max() + .copied() + .unwrap_or(self.accounts.len() as u8) as usize; + + // Calculate the size of all accounts (4 bytes for length + each Pubkey is 32 bytes) + let accounts_size = 4 + max_accounts * 32; + + // Calculate the size of all instructions (4 bytes for length + instruction data) + let instructions_size = 4 + self.instructions.iter().map(|i| i.size()).sum::(); + + header_size + accounts_size + instructions_size + } +} + +impl TransactionSourceV0 { + pub fn size(&self) -> usize { + match self { + TransactionSourceV0::CompiledV0(compiled_tx) => 4 + compiled_tx.size(), + TransactionSourceV0::RemoteV0 { url, signer: _ } => { + // For remote transactions, we need to account for the URL string length + // and the signer pubkey (32 bytes) + 4 + 32 + 4 + url.len() // 4 bytes for string length prefix + } + } } }