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