1use std::{borrow::BorrowMut, ops::Deref};
2
3use solana_sdk::{
4 hash::Hash, message::VersionedMessage, packet::PACKET_DATA_SIZE, pubkey::Pubkey,
5 signer::Signer, transaction::VersionedTransaction,
6};
7
8use crate::{
9 address_lookup_table::AddressLookupTables,
10 instruction_group::{ComputeBudgetOptions, GetInstructionsOptions},
11 signer::TransactionSigners,
12 transaction_builder::default_before_sign,
13 AtomicGroup, ParallelGroup,
14};
15
16#[derive(Debug, Clone)]
18pub struct TransactionGroupOptions {
19 pub max_transaction_size: usize,
21 pub max_instructions_per_tx: usize,
25 pub memo: Option<String>,
29}
30
31impl Default for TransactionGroupOptions {
32 fn default() -> Self {
33 Self {
34 max_transaction_size: PACKET_DATA_SIZE,
35 max_instructions_per_tx: 14,
36 memo: None,
38 }
39 }
40}
41
42impl TransactionGroupOptions {
43 fn instruction_options(&self, compute_budget: &ComputeBudgetOptions) -> GetInstructionsOptions {
44 GetInstructionsOptions {
45 compute_budget: compute_budget.clone(),
46 memo: self.memo.clone(),
47 }
48 }
49
50 #[allow(clippy::too_many_arguments)]
51 fn build_transaction_batch<C: Deref<Target = impl Signer + ?Sized>>(
52 &self,
53 recent_blockhash: Hash,
54 luts: &AddressLookupTables,
55 compute_budget: &ComputeBudgetOptions,
56 group: &ParallelGroup,
57 signers: &TransactionSigners<C>,
58 allow_partial_sign: bool,
59 mut before_sign: impl FnMut(&VersionedMessage) -> crate::Result<()>,
60 ) -> crate::Result<Vec<VersionedTransaction>> {
61 group
62 .iter()
63 .map(|ag| {
64 signers.sign_atomic_instruction_group(
65 ag,
66 recent_blockhash,
67 self.instruction_options(compute_budget),
68 Some(luts),
69 allow_partial_sign,
70 &mut before_sign,
71 )
72 })
73 .collect()
74 }
75
76 fn optimizable(
77 &self,
78 x: &AtomicGroup,
79 y: &AtomicGroup,
80 luts: &AddressLookupTables,
81 allow_payer_change: bool,
82 ) -> bool {
83 if !x.is_mergeable() || !y.is_mergeable() {
84 return false;
85 }
86
87 if !allow_payer_change && x.payer() != y.payer() {
88 return false;
89 }
90
91 let num_ixs = x.len() + y.len();
92 if num_ixs > self.max_instructions_per_tx {
93 return false;
94 }
95
96 let size = x.transaction_size_after_merge(y, true, Some(luts), Default::default());
97 if size > self.max_transaction_size {
98 return false;
99 }
100
101 true
102 }
103
104 pub(crate) fn optimize<T: BorrowMut<AtomicGroup>>(
105 &self,
106 groups: &mut [T],
107 luts: &AddressLookupTables,
108 allow_payer_change: bool,
109 ) -> bool {
110 let indices = (0..groups.len()).collect::<Vec<_>>();
111
112 let mut merged = false;
113 let default_pubkey = Pubkey::default();
114 for pair in indices.windows(2) {
115 let [i, j] = *pair else { unreachable!() };
116 if groups[i].borrow().is_empty() {
117 merged = true;
119 continue;
120 }
121 if !self.optimizable(
122 groups[i].borrow(),
123 groups[j].borrow(),
124 luts,
125 allow_payer_change,
126 ) {
127 continue;
128 }
129 let mut group = AtomicGroup::new(&default_pubkey);
130 std::mem::swap(groups[i].borrow_mut(), &mut group);
131 std::mem::swap(groups[j].borrow_mut(), &mut group);
132 groups[j].borrow_mut().merge(group);
133 merged = true;
134 }
135
136 merged
137 }
138}
139
140#[derive(Debug, Clone, Default)]
142pub struct TransactionGroup {
143 options: TransactionGroupOptions,
144 luts: AddressLookupTables,
145 groups: Vec<ParallelGroup>,
146}
147
148impl TransactionGroup {
149 pub fn with_options_and_luts(
151 options: TransactionGroupOptions,
152 luts: AddressLookupTables,
153 ) -> Self {
154 Self {
155 options,
156 luts,
157 groups: Default::default(),
158 }
159 }
160
161 fn validate_one(&self, group: &AtomicGroup) -> crate::Result<()> {
162 if group.len() > self.options.max_instructions_per_tx {
163 return Err(crate::Error::AddTransaction(
164 "Too many instructions for a signle transaction",
165 ));
166 }
167 let size = group.transaction_size(true, Some(&self.luts), Default::default());
168 if size > self.options.max_transaction_size {
169 return Err(crate::Error::AddTransaction(
170 "Transaction size exceeds the `max_transaction_size` config",
171 ));
172 }
173 Ok(())
174 }
175
176 pub fn validate_instruction_group(&self, group: &ParallelGroup) -> crate::Result<()> {
178 for insts in group.iter() {
179 self.validate_one(insts)?;
180 }
181 Ok(())
182 }
183
184 pub fn add(&mut self, group: impl Into<ParallelGroup>) -> crate::Result<&mut Self> {
186 let group = group.into();
187 if group.is_empty() {
188 return Ok(self);
189 }
190 self.validate_instruction_group(&group)?;
191 self.groups.push(group);
192 Ok(self)
193 }
194
195 pub fn optimize(&mut self, allow_payer_change: bool) -> &mut Self {
197 for group in self.groups.iter_mut() {
198 group.optimize(&self.options, &self.luts, allow_payer_change);
199 }
200
201 let indices = (0..self.groups.len()).collect::<Vec<_>>();
202 let groups = &mut self.groups;
203
204 let mut merged = false;
205 for pair in indices.windows(2) {
206 let [i, j] = *pair else {
207 unreachable!();
208 };
209 let pg_i = &groups[i];
210 let pg_j = &groups[j];
211
212 if !pg_i.is_mergeable() || !pg_j.is_mergeable() {
213 continue;
214 }
215
216 let (Some(group_i), Some(group_j)) = (pg_i.single(), pg_j.single()) else {
217 continue;
218 };
219 if !self
220 .options
221 .optimizable(group_i, group_j, &self.luts, allow_payer_change)
222 {
223 continue;
224 }
225 let mut group = std::mem::take(&mut groups[i]);
226 std::mem::swap(&mut groups[j], &mut group);
227 groups[j]
228 .single_mut()
229 .unwrap()
230 .merge(group.into_single().unwrap());
231 merged = true;
232 }
233
234 if merged {
235 self.groups = self
236 .groups
237 .drain(..)
238 .filter(|group| !group.is_empty())
239 .collect();
240 }
241
242 self
243 }
244
245 pub fn to_transactions<'a, C: Deref<Target = impl Signer + ?Sized>>(
247 &'a self,
248 signers: &'a TransactionSigners<C>,
249 recent_blockhash: Hash,
250 allow_partial_sign: bool,
251 ) -> TransactionGroupIter<'a, C, fn(&VersionedMessage) -> crate::Result<()>> {
252 self.to_transactions_with_options(
253 signers,
254 recent_blockhash,
255 allow_partial_sign,
256 Default::default(),
257 default_before_sign,
258 )
259 }
260
261 pub fn to_transactions_with_options<'a, C: Deref<Target = impl Signer + ?Sized>, F>(
263 &'a self,
264 signers: &'a TransactionSigners<C>,
265 recent_blockhash: Hash,
266 allow_partial_sign: bool,
267 compute_budget: ComputeBudgetOptions,
268 before_sign: F,
269 ) -> TransactionGroupIter<'a, C, F>
270 where
271 F: FnMut(&VersionedMessage) -> crate::Result<()>,
272 {
273 TransactionGroupIter {
274 signers,
275 recent_blockhash,
276 compute_budget,
277 options: &self.options,
278 luts: &self.luts,
279 iter: self.groups.iter(),
280 allow_partial_sign,
281 before_sign,
282 }
283 }
284
285 pub fn is_empty(&self) -> bool {
287 self.groups.is_empty()
288 }
289
290 pub fn len(&self) -> usize {
292 self.groups.iter().map(|pg| pg.len()).sum()
293 }
294
295 pub fn options(&self) -> &TransactionGroupOptions {
297 &self.options
298 }
299
300 pub fn estimate_execution_fee(
302 &self,
303 compute_unit_price_micro_lamports: Option<u64>,
304 compute_unit_min_priority_lamports: Option<u64>,
305 ) -> u64 {
306 self.groups
307 .iter()
308 .map(|pg| {
309 pg.estimate_execution_fee(
310 compute_unit_price_micro_lamports,
311 compute_unit_min_priority_lamports,
312 )
313 })
314 .sum()
315 }
316
317 pub fn groups(&self) -> &[ParallelGroup] {
319 &self.groups
320 }
321
322 pub fn luts(&self) -> &AddressLookupTables {
324 &self.luts
325 }
326}
327
328pub struct TransactionGroupIter<'a, C, F> {
330 signers: &'a TransactionSigners<C>,
331 recent_blockhash: Hash,
332 compute_budget: ComputeBudgetOptions,
333 options: &'a TransactionGroupOptions,
334 luts: &'a AddressLookupTables,
335 iter: std::slice::Iter<'a, ParallelGroup>,
336 allow_partial_sign: bool,
337 before_sign: F,
338}
339
340impl<C: Deref<Target = impl Signer + ?Sized>, F> Iterator for TransactionGroupIter<'_, C, F>
341where
342 F: FnMut(&VersionedMessage) -> crate::Result<()>,
343{
344 type Item = crate::Result<Vec<VersionedTransaction>>;
345
346 fn next(&mut self) -> Option<Self::Item> {
347 let group = self.iter.next()?;
348 Some(self.options.build_transaction_batch(
349 self.recent_blockhash,
350 self.luts,
351 &self.compute_budget,
352 group,
353 self.signers,
354 self.allow_partial_sign,
355 &mut self.before_sign,
356 ))
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use std::sync::Arc;
363
364 use solana_sdk::{
365 pubkey::Pubkey,
366 signature::{Keypair, Signature},
367 };
368
369 use super::*;
370
371 #[test]
372 fn fully_sign() -> crate::Result<()> {
373 use solana_sdk::system_instruction::transfer;
374
375 let payer_1 = Arc::new(Keypair::new());
376 let payer_1_pubkey = payer_1.pubkey();
377
378 let payer_2 = Arc::new(Keypair::new());
379 let payer_2_pubkey = payer_2.pubkey();
380
381 let payer_3 = Arc::new(Keypair::new());
382 let payer_3_pubkey = payer_3.pubkey();
383
384 let signers = TransactionSigners::from_iter([payer_1, payer_2, payer_3]);
385
386 let ig = [
387 {
388 let mut ag = AtomicGroup::with_instructions(
389 &payer_1_pubkey,
390 [
391 transfer(&payer_1_pubkey, &Pubkey::new_unique(), 1),
392 transfer(&payer_2_pubkey, &payer_1_pubkey, 1),
393 ],
394 );
395 ag.add_signer(&payer_2_pubkey);
396 ag
397 },
398 AtomicGroup::with_instructions(
399 &payer_3_pubkey,
400 [
401 transfer(&payer_3_pubkey, &Pubkey::new_unique(), 1),
402 transfer(&payer_3_pubkey, &Pubkey::new_unique(), 1),
403 ],
404 ),
405 ]
406 .into_iter()
407 .collect::<ParallelGroup>();
408
409 let mut group = TransactionGroup::default();
410 let txns = group
411 .add(ig)?
412 .to_transactions(&signers, Hash::default(), false);
413
414 for (idx, res) in txns.enumerate() {
415 for txn in res.inspect_err(|err| eprintln!("[{idx}]: {err}"))? {
416 txn.verify_and_hash_message()
417 .expect("should be fully signed");
418 }
419 }
420 Ok(())
421 }
422
423 #[test]
424 fn partially_sign() -> crate::Result<()> {
425 use solana_sdk::system_instruction::transfer;
426
427 let payer_1 = Arc::new(Keypair::new());
428 let payer_1_pubkey = payer_1.pubkey();
429
430 let payer_2 = Arc::new(Keypair::new());
431 let payer_2_pubkey = payer_2.pubkey();
432
433 let payer_3 = Arc::new(Keypair::new());
434 let payer_3_pubkey = payer_3.pubkey();
435
436 let signers = TransactionSigners::from_iter([payer_1, payer_3]);
437
438 let ig = [
439 {
440 let mut ag = AtomicGroup::with_instructions(
441 &payer_1_pubkey,
442 [
443 transfer(&payer_1_pubkey, &Pubkey::new_unique(), 1),
444 transfer(&payer_2_pubkey, &payer_1_pubkey, 1),
445 ],
446 );
447 ag.add_signer(&payer_2_pubkey);
448 ag
449 },
450 AtomicGroup::with_instructions(
451 &payer_3_pubkey,
452 [
453 transfer(&payer_3_pubkey, &Pubkey::new_unique(), 1),
454 transfer(&payer_3_pubkey, &Pubkey::new_unique(), 1),
455 ],
456 ),
457 ]
458 .into_iter()
459 .collect::<ParallelGroup>();
460
461 let mut group = TransactionGroup::default();
462 let txns = group
463 .add(ig)?
464 .to_transactions(&signers, Hash::default(), true);
465
466 for res in txns {
467 for txn in res? {
468 let results = txn.verify_with_results();
469 for (idx, result) in results.into_iter().enumerate() {
470 if !result {
471 assert_eq!(txn.signatures[idx], Signature::default());
472 }
473 }
474 }
475 }
476
477 Ok(())
478 }
479
480 #[test]
481 fn optimize() -> crate::Result<()> {
482 use solana_sdk::system_instruction::transfer;
483
484 let payer_1 = Arc::new(Keypair::new());
485 let payer_1_pubkey = payer_1.pubkey();
486
487 let payer_2 = Arc::new(Keypair::new());
488 let payer_2_pubkey = payer_2.pubkey();
489
490 let payer_3 = Arc::new(Keypair::new());
491 let payer_3_pubkey = payer_3.pubkey();
492
493 let signers = TransactionSigners::from_iter([payer_1, payer_2, payer_3]);
494
495 let ig_1 = [
496 {
497 let mut ag = AtomicGroup::with_instructions(
498 &payer_1_pubkey,
499 [
500 transfer(&payer_1_pubkey, &Pubkey::new_unique(), 1),
501 transfer(&payer_2_pubkey, &payer_1_pubkey, 1),
502 ],
503 );
504 ag.add_signer(&payer_2_pubkey);
505 ag
506 },
507 AtomicGroup::with_instructions(
508 &payer_3_pubkey,
509 [
510 transfer(&payer_3_pubkey, &Pubkey::new_unique(), 1),
511 transfer(&payer_3_pubkey, &Pubkey::new_unique(), 1),
512 ],
513 ),
514 ]
515 .into_iter()
516 .collect::<ParallelGroup>();
517
518 let ig_2 = [
519 {
520 let mut ag = AtomicGroup::with_instructions(
521 &payer_1_pubkey,
522 [
523 transfer(&payer_1_pubkey, &Pubkey::new_unique(), 1),
524 transfer(&payer_2_pubkey, &payer_1_pubkey, 1),
525 ],
526 );
527 ag.add_signer(&payer_2_pubkey);
528 ag
529 },
530 AtomicGroup::with_instructions(
531 &payer_3_pubkey,
532 [
533 transfer(&payer_3_pubkey, &Pubkey::new_unique(), 1),
534 transfer(&payer_3_pubkey, &Pubkey::new_unique(), 1),
535 ],
536 ),
537 ]
538 .into_iter()
539 .collect::<ParallelGroup>();
540
541 let mut group = TransactionGroup::default();
542 let txns = group
543 .add(ig_1)?
544 .add(ig_2)?
545 .optimize(true)
546 .to_transactions(&signers, Hash::default(), false)
547 .flat_map(|res| match res {
548 Ok(txns) => txns.into_iter().map(Ok).collect(),
549 Err(err) => vec![Err(err)],
550 })
551 .collect::<crate::Result<Vec<_>>>()?;
552 assert_eq!(txns.len(), 1);
553 assert!(bincode::serialize(&txns[0]).unwrap().len() <= PACKET_DATA_SIZE);
554 txns[0]
555 .verify_and_hash_message()
556 .expect("should be fully signed");
557 Ok(())
558 }
559
560 #[test]
561 fn optimize_deny_payer_change() -> crate::Result<()> {
562 use solana_sdk::system_instruction::transfer;
563
564 let payer_1 = Arc::new(Keypair::new());
565 let payer_1_pubkey = payer_1.pubkey();
566
567 let payer_2 = Arc::new(Keypair::new());
568 let payer_2_pubkey = payer_2.pubkey();
569
570 let payer_3 = Arc::new(Keypair::new());
571 let payer_3_pubkey = payer_3.pubkey();
572
573 let signers = TransactionSigners::from_iter([payer_1, payer_2, payer_3]);
574
575 let ig_1 = [
576 {
577 let mut ag = AtomicGroup::with_instructions(
578 &payer_1_pubkey,
579 [
580 transfer(&payer_1_pubkey, &Pubkey::new_unique(), 1),
581 transfer(&payer_2_pubkey, &payer_1_pubkey, 1),
582 ],
583 );
584 ag.add_signer(&payer_2_pubkey);
585 ag
586 },
587 {
588 let mut ag = AtomicGroup::with_instructions(
589 &payer_1_pubkey,
590 [
591 transfer(&payer_3_pubkey, &Pubkey::new_unique(), 1),
592 transfer(&payer_3_pubkey, &Pubkey::new_unique(), 1),
593 ],
594 );
595 ag.add_signer(&payer_3_pubkey);
596 ag
597 },
598 ]
599 .into_iter()
600 .collect::<ParallelGroup>();
601
602 let ig_2 = [
603 {
604 let mut ag = AtomicGroup::with_instructions(
605 &payer_3_pubkey,
606 [
607 transfer(&payer_1_pubkey, &Pubkey::new_unique(), 1),
608 transfer(&payer_2_pubkey, &payer_1_pubkey, 1),
609 ],
610 );
611 ag.add_signer(&payer_1_pubkey).add_signer(&payer_2_pubkey);
612 ag
613 },
614 AtomicGroup::with_instructions(
615 &payer_3_pubkey,
616 [
617 transfer(&payer_3_pubkey, &Pubkey::new_unique(), 1),
618 transfer(&payer_3_pubkey, &Pubkey::new_unique(), 1),
619 ],
620 ),
621 ]
622 .into_iter()
623 .collect::<ParallelGroup>();
624
625 let mut group = TransactionGroup::default();
626 let txns = group
627 .add(ig_1)?
628 .add(ig_2)?
629 .optimize(false)
630 .to_transactions(&signers, Hash::default(), false)
631 .flat_map(|res| match res {
632 Ok(txns) => txns.into_iter().map(Ok).collect(),
633 Err(err) => vec![Err(err)],
634 })
635 .collect::<crate::Result<Vec<_>>>()?;
636 assert_eq!(txns.len(), 2);
637
638 for txn in txns {
639 assert!(bincode::serialize(&txn).unwrap().len() <= PACKET_DATA_SIZE);
640 txn.verify_and_hash_message()
641 .expect("should be fully signed");
642 }
643
644 Ok(())
645 }
646}