gmsol_solana_utils/
transaction_group.rs

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/// Transaction Group Options.
14#[derive(Debug, Clone)]
15pub struct TransactionGroupOptions {
16    /// Max transaction size.
17    pub max_transaction_size: usize,
18    /// Max instructions per transaction.
19    /// # Note
20    /// - Compute budget instructions are ignored.
21    pub max_instructions_per_tx: usize,
22    /// Compute unit price in micro lamports.
23    pub compute_unit_price_micro_lamports: Option<u64>,
24    /// Memo for each transaction in this group.
25    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                // If the current group is empty, it can be considered as already merged into the following group.
108                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/// Transaction Group.
131#[derive(Debug, Clone, Default)]
132pub struct TransactionGroup {
133    options: TransactionGroupOptions,
134    luts: AddressLookupTables,
135    groups: Vec<ParallelGroup>,
136}
137
138impl TransactionGroup {
139    /// Create with the given [`TransactionGroupOptions`] and [`AddressLookupTables`].
140    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    /// Returns [`Ok`] if the given [`ParallelGroup`] can be added without error.
167    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    /// Add a [`ParallelGroup`].
175    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    /// Optimize the transactions by repacking instructions to maximize space efficiency.
186    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    /// Build transactions.
229    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
246/// Transaction Group Iter.
247pub 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}