gmsol_solana_utils/
transaction_group.rs

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/// Transaction Group Options.
17#[derive(Debug, Clone)]
18pub struct TransactionGroupOptions {
19    /// Max transaction size.
20    pub max_transaction_size: usize,
21    /// Max instructions per transaction.
22    /// # Note
23    /// - Compute budget instructions are ignored.
24    pub max_instructions_per_tx: usize,
25    // /// Compute unit price in micro lamports.
26    // pub compute_unit_price_micro_lamports: Option<u64>,
27    /// Memo for each transaction in this group.
28    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            // compute_unit_price_micro_lamports: None,
37            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                // If the current group is empty, it can be considered as already merged into the following group.
118                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/// Transaction Group.
141#[derive(Debug, Clone, Default)]
142pub struct TransactionGroup {
143    options: TransactionGroupOptions,
144    luts: AddressLookupTables,
145    groups: Vec<ParallelGroup>,
146}
147
148impl TransactionGroup {
149    /// Create with the given [`TransactionGroupOptions`] and [`AddressLookupTables`].
150    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    /// Returns [`Ok`] if the given [`ParallelGroup`] can be added without error.
177    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    /// Add a [`ParallelGroup`].
185    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    /// Optimize the transactions by repacking instructions to maximize space efficiency.
196    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    /// Build transactions.
246    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    /// Build transactions.
262    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    /// Returns whether the transaction group is empty.
286    pub fn is_empty(&self) -> bool {
287        self.groups.is_empty()
288    }
289
290    /// Returns the total number of transactions.
291    pub fn len(&self) -> usize {
292        self.groups.iter().map(|pg| pg.len()).sum()
293    }
294
295    /// Returns the options.
296    pub fn options(&self) -> &TransactionGroupOptions {
297        &self.options
298    }
299
300    /// Estimates the execution fee of the result transaction.
301    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    /// Returns [`ParallelGroup`]s.
318    pub fn groups(&self) -> &[ParallelGroup] {
319        &self.groups
320    }
321
322    /// Returns Address Lookup Tables.
323    pub fn luts(&self) -> &AddressLookupTables {
324        &self.luts
325    }
326}
327
328/// Transaction Group Iter.
329pub 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}