gmsol_solana_utils/
transaction_builder.rs

1use std::{
2    collections::{HashMap, HashSet},
3    ops::Deref,
4};
5
6use solana_sdk::{
7    address_lookup_table::AddressLookupTableAccount,
8    commitment_config::CommitmentConfig,
9    hash::Hash,
10    instruction::{AccountMeta, Instruction},
11    message::{v0, VersionedMessage},
12    pubkey::Pubkey,
13    signer::Signer,
14    transaction::VersionedTransaction,
15};
16
17#[cfg(anchor)]
18use anchor_lang::prelude::*;
19
20#[cfg(client)]
21use solana_client::{nonblocking::rpc_client::RpcClient, rpc_config::RpcSendTransactionConfig};
22
23#[cfg(client)]
24use solana_sdk::signature::Signature;
25
26use crate::{cluster::Cluster, compute_budget::ComputeBudget, signer::BoxClonableSigner};
27
28#[cfg(client)]
29use crate::{
30    bundle_builder::{BundleBuilder, BundleOptions, CreateBundleOptions},
31    client::SendAndConfirm,
32    utils::WithSlot,
33};
34
35/// Wallet Config.
36#[derive(Clone)]
37pub struct Config<C> {
38    cluster: Cluster,
39    payer: C,
40    options: CommitmentConfig,
41}
42
43impl<C> Config<C> {
44    /// Create a new wallet config.
45    pub fn new(cluster: Cluster, payer: C, options: CommitmentConfig) -> Self {
46        Self {
47            cluster,
48            payer,
49            options,
50        }
51    }
52
53    /// Get cluster.
54    pub fn cluster(&self) -> &Cluster {
55        &self.cluster
56    }
57
58    /// Get commitment config.
59    pub fn commitment(&self) -> &CommitmentConfig {
60        &self.options
61    }
62
63    /// Create a Solana RPC Client.
64    #[cfg(client)]
65    pub fn rpc(&self) -> RpcClient {
66        self.cluster.rpc(self.options)
67    }
68
69    /// Set payer.
70    pub fn set_payer<C2>(self, payer: C2) -> Config<C2> {
71        Config {
72            cluster: self.cluster,
73            payer,
74            options: self.options,
75        }
76    }
77
78    /// Set cluster.
79    pub fn set_cluster(mut self, url: impl AsRef<str>) -> crate::Result<Self> {
80        self.cluster = url.as_ref().parse()?;
81        Ok(self)
82    }
83
84    /// Set options.
85    pub fn set_options(mut self, options: CommitmentConfig) -> Self {
86        self.options = options;
87        self
88    }
89}
90
91impl<C: Deref<Target = impl Signer>> Config<C> {
92    /// Get payer pubkey.
93    pub fn payer(&self) -> Pubkey {
94        self.payer.pubkey()
95    }
96}
97
98/// A builder for a transaction.
99#[must_use = "transaction builder do nothing if not built"]
100#[derive(Clone)]
101pub struct TransactionBuilder<'a, C, T = ()> {
102    output: T,
103    program_id: Pubkey,
104    cfg: Config<C>,
105    signers: Vec<&'a dyn Signer>,
106    owned_signers: Vec<BoxClonableSigner<'static>>,
107    pre_instructions: Vec<Instruction>,
108    accounts: Vec<AccountMeta>,
109    instruction_data: Option<Vec<u8>>,
110    compute_budget: ComputeBudget,
111    luts: HashMap<Pubkey, Vec<Pubkey>>,
112}
113
114impl<'a, C: Deref<Target = impl Signer> + Clone> TransactionBuilder<'a, C> {
115    /// Create a new transaction builder.
116    pub fn new(program_id: Pubkey, cfg: &'a Config<C>) -> Self {
117        Self {
118            output: (),
119            program_id,
120            cfg: cfg.clone(),
121            signers: Default::default(),
122            owned_signers: Default::default(),
123            pre_instructions: Default::default(),
124            accounts: Default::default(),
125            instruction_data: None,
126            compute_budget: ComputeBudget::default(),
127            luts: Default::default(),
128        }
129    }
130
131    /// Merge other [`TransactionBuilder`]. The rpc fields will be empty after merging,
132    /// i.e., `take_rpc` will return `None`.
133    /// ## Panics
134    /// Return if there are any errors.
135    /// ## Notes
136    /// - All options including `cluster`, `commiment` and `program_id` will still be
137    ///   the same of `self` after merging.
138    #[inline]
139    pub fn merge(mut self, mut other: Self) -> Self {
140        self.try_merge(&mut other)
141            .unwrap_or_else(|err| panic!("failed to merge: {err}"));
142        self
143    }
144
145    /// Merge other [`TransactionBuilder`]. The rpc fields will be empty after merging,
146    /// i.e., `take_rpc` will return `None`.
147    /// ## Errors
148    /// Return error if the `payer`s are not the same.
149    /// ## Notes
150    /// - When success, the `other` will become a empty [`TransactionBuilder`].
151    /// - All options including `cluster`, `commiment`, and `program_id` will still be
152    ///   the same of `self` after merging.
153    pub fn try_merge(&mut self, other: &mut Self) -> crate::Result<()> {
154        if self.cfg.payer() != other.cfg.payer() {
155            return Err(crate::Error::MergeTransaction("payer mismatched"));
156        }
157
158        // Push the rpc ix before merging.
159        if let Some(ix) = self.take_instruction() {
160            self.pre_instructions.push(ix);
161        }
162
163        // Merge ixs.
164        self.pre_instructions.append(&mut other.pre_instructions);
165        if let Some(ix) = other.take_instruction() {
166            self.pre_instructions.push(ix);
167        }
168
169        // Merge signers.
170        self.signers.append(&mut other.signers);
171
172        // Merge owned signers.
173        self.owned_signers.append(&mut other.owned_signers);
174
175        // Merge compute budget.
176        self.compute_budget += std::mem::take(&mut other.compute_budget);
177
178        // Merge LUTs.
179        self.luts.extend(other.luts.drain());
180        Ok(())
181    }
182
183    /// Convert into a [`BundleBuilder`] with the given options.
184    #[cfg(client)]
185    pub fn into_bundle_with_options(
186        self,
187        options: BundleOptions,
188    ) -> crate::Result<BundleBuilder<'a, C>> {
189        let mut bundle = BundleBuilder::new_with_options(CreateBundleOptions {
190            cluster: self.cfg.cluster.clone(),
191            commitment: *self.cfg.commitment(),
192            options,
193        });
194        bundle.push(self)?;
195        Ok(bundle)
196    }
197
198    /// Convert into a [`BundleBuilder`].
199    #[cfg(client)]
200    pub fn into_bundle(self) -> crate::Result<BundleBuilder<'a, C>> {
201        self.into_bundle_with_options(Default::default())
202    }
203}
204
205impl<'a, C: Deref<Target = impl Signer> + Clone, T> TransactionBuilder<'a, C, T> {
206    /// Set payer.
207    pub fn payer<C2>(self, payer: C2) -> TransactionBuilder<'a, C2, T> {
208        TransactionBuilder {
209            output: self.output,
210            program_id: self.program_id,
211            cfg: self.cfg.set_payer(payer),
212            signers: self.signers,
213            owned_signers: self.owned_signers,
214            pre_instructions: self.pre_instructions,
215            accounts: self.accounts,
216            instruction_data: self.instruction_data,
217            compute_budget: self.compute_budget,
218            luts: self.luts,
219        }
220    }
221
222    /// Get the pubkey of the payer.
223    pub fn get_payer(&self) -> Pubkey {
224        self.cfg.payer()
225    }
226
227    /// Set cluster.
228    pub fn cluster(mut self, url: impl AsRef<str>) -> crate::Result<Self> {
229        self.cfg = self.cfg.set_cluster(url)?;
230        Ok(self)
231    }
232
233    /// Set commiment options.
234    pub fn options(mut self, options: CommitmentConfig) -> Self {
235        self.cfg = self.cfg.set_options(options);
236        self
237    }
238
239    /// Add a signer to the signer list.
240    pub fn signer(mut self, signer: &'a dyn Signer) -> Self {
241        self.signers.push(signer);
242        self
243    }
244
245    /// Add a owned sigenr to the signer list.
246    pub fn owned_signer(mut self, signer: impl Signer + Clone + 'static) -> Self {
247        self.owned_signers.push(BoxClonableSigner::new(signer));
248        self
249    }
250
251    /// Set program id.
252    pub fn program(mut self, program_id: Pubkey) -> Self {
253        self.program_id = program_id;
254        self
255    }
256
257    /// Append accounts for the main instruction.
258    pub fn accounts(mut self, mut accounts: Vec<AccountMeta>) -> Self {
259        self.accounts.append(&mut accounts);
260        self
261    }
262
263    /// Append accounts for the main instruction.
264    #[cfg(feature = "anchor")]
265    pub fn anchor_accounts(self, accounts: impl ToAccountMetas) -> Self {
266        self.accounts(accounts.to_account_metas(None))
267    }
268
269    /// Set arguments for the main instruction.
270    pub fn args(mut self, args: Vec<u8>) -> Self {
271        self.instruction_data = Some(args);
272        self
273    }
274
275    /// Set arguments for the main instruction.
276    #[cfg(feature = "anchor")]
277    pub fn anchor_args(self, args: impl anchor_lang::InstructionData) -> Self {
278        self.args(args.data())
279    }
280
281    /// Set compute budget.
282    pub fn compute_budget(mut self, budget: ComputeBudget) -> Self {
283        self.compute_budget = budget;
284        self
285    }
286
287    /// Get mutable reference to the compute budget.
288    pub fn compute_budget_mut(&mut self) -> &mut ComputeBudget {
289        &mut self.compute_budget
290    }
291
292    fn get_compute_budget_instructions(
293        &self,
294        compute_unit_price_micro_lamports: Option<u64>,
295    ) -> Vec<Instruction> {
296        self.compute_budget
297            .compute_budget_instructions(compute_unit_price_micro_lamports)
298    }
299
300    /// Take and construct the "main" instruction if present.
301    pub fn take_instruction(&mut self) -> Option<Instruction> {
302        let ix_data = self.instruction_data.take()?;
303        Some(Instruction {
304            program_id: self.program_id,
305            data: ix_data,
306            accounts: std::mem::take(&mut self.accounts),
307        })
308    }
309
310    /// Construct the "main" instruction if present.
311    fn get_instruction(&self) -> Option<Instruction> {
312        let ix_data = self.instruction_data.as_ref()?;
313        Some(Instruction {
314            program_id: self.program_id,
315            data: ix_data.clone(),
316            accounts: self.accounts.clone(),
317        })
318    }
319
320    /// Construct all instructions.
321    pub fn instructions(&self) -> Vec<Instruction> {
322        self.instructions_with_options(false, None)
323    }
324
325    /// Construct all instructions with options.
326    pub fn instructions_with_options(
327        &self,
328        without_compute_budget: bool,
329        compute_unit_price_micro_lamports: Option<u64>,
330    ) -> Vec<Instruction> {
331        let mut instructions = if without_compute_budget {
332            Vec::default()
333        } else {
334            self.get_compute_budget_instructions(compute_unit_price_micro_lamports)
335        };
336        instructions.append(&mut self.pre_instructions.clone());
337        if let Some(ix) = self.get_instruction() {
338            instructions.push(ix);
339        }
340        instructions
341    }
342
343    /// Get the output.
344    pub fn get_output(&self) -> &T {
345        &self.output
346    }
347
348    /// Set the output and return the previous.
349    pub fn swap_output<U>(self, output: U) -> (TransactionBuilder<'a, C, U>, T) {
350        let Self {
351            cfg,
352            signers,
353            owned_signers,
354            output: previous,
355            program_id,
356            pre_instructions,
357            accounts,
358            instruction_data,
359            compute_budget,
360            luts,
361        } = self;
362
363        (
364            TransactionBuilder {
365                cfg,
366                signers,
367                owned_signers,
368                output,
369                program_id,
370                pre_instructions,
371                accounts,
372                instruction_data,
373                compute_budget,
374                luts,
375            },
376            previous,
377        )
378    }
379
380    /// Set the output.
381    pub fn output<U>(self, output: U) -> TransactionBuilder<'a, C, U> {
382        self.swap_output(output).0
383    }
384
385    /// Clear the output.
386    pub fn clear_output(self) -> TransactionBuilder<'a, C, ()> {
387        self.swap_output(()).0
388    }
389
390    /// Insert an instruction before the "main" instruction.
391    pub fn pre_instruction(mut self, ix: Instruction) -> Self {
392        self.pre_instructions.push(ix);
393        self
394    }
395
396    /// Insert instructions before the "main" instruction.
397    pub fn pre_instructions(mut self, mut ixs: Vec<Instruction>) -> Self {
398        self.pre_instructions.append(&mut ixs);
399        self
400    }
401
402    /// Insert an address lookup table account.
403    pub fn lookup_table(mut self, account: AddressLookupTableAccount) -> Self {
404        self.luts.insert(account.key, account.addresses);
405        self
406    }
407
408    /// Insert many address lookup tables.
409    pub fn lookup_tables(
410        mut self,
411        tables: impl IntoIterator<Item = (Pubkey, Vec<Pubkey>)>,
412    ) -> Self {
413        self.luts.extend(tables);
414        self
415    }
416
417    fn v0_message_with_blockhash_and_options(
418        &self,
419        latest_hash: Hash,
420        without_compute_budget: bool,
421        compute_unit_price_micro_lamports: Option<u64>,
422        with_luts: bool,
423    ) -> crate::Result<v0::Message> {
424        let instructions = self
425            .instructions_with_options(without_compute_budget, compute_unit_price_micro_lamports);
426        let luts = if with_luts {
427            self.luts
428                .iter()
429                .map(|(key, addresses)| AddressLookupTableAccount {
430                    key: *key,
431                    addresses: addresses.clone(),
432                })
433                .collect::<Vec<_>>()
434        } else {
435            vec![]
436        };
437        let message =
438            v0::Message::try_compile(&self.cfg.payer(), &instructions, &luts, latest_hash)?;
439
440        Ok(message)
441    }
442
443    /// Get versioned message with the given hash and options.
444    pub fn message_with_blockhash_and_options(
445        &self,
446        latest_hash: Hash,
447        without_compute_budget: bool,
448        compute_unit_price_micro_lamports: Option<u64>,
449    ) -> crate::Result<VersionedMessage> {
450        Ok(VersionedMessage::V0(
451            self.v0_message_with_blockhash_and_options(
452                latest_hash,
453                without_compute_budget,
454                compute_unit_price_micro_lamports,
455                true,
456            )?,
457        ))
458    }
459
460    /// Get versioned message with options.
461    #[cfg(client)]
462    pub async fn message_with_options(
463        &self,
464        without_compute_budget: bool,
465        compute_unit_price_micro_lamports: Option<u64>,
466    ) -> crate::Result<VersionedMessage> {
467        let client = self.cfg.rpc();
468        let latest_hash = client.get_latest_blockhash().await.map_err(Box::new)?;
469
470        self.message_with_blockhash_and_options(
471            latest_hash,
472            without_compute_budget,
473            compute_unit_price_micro_lamports,
474        )
475    }
476
477    /// Get signed transaction with blockhash and options.
478    pub fn signed_transaction_with_blockhash_and_options(
479        &self,
480        latest_hash: Hash,
481        without_compute_budget: bool,
482        compute_unit_price_micro_lamports: Option<u64>,
483    ) -> crate::Result<VersionedTransaction> {
484        let message = self.message_with_blockhash_and_options(
485            latest_hash,
486            without_compute_budget,
487            compute_unit_price_micro_lamports,
488        )?;
489
490        let mut signers = self.signers.clone();
491        signers.push(&*self.cfg.payer);
492        for signer in self.owned_signers.iter() {
493            signers.push(signer);
494        }
495
496        let tx = VersionedTransaction::try_new(message, &signers)?;
497
498        Ok(tx)
499    }
500
501    /// Get signed transactoin with options.
502    #[cfg(client)]
503    pub async fn signed_transaction_with_options(
504        &self,
505        without_compute_budget: bool,
506        compute_unit_price_micro_lamports: Option<u64>,
507    ) -> crate::Result<VersionedTransaction> {
508        let client = self.cfg.rpc();
509        let latest_hash = client.get_latest_blockhash().await.map_err(Box::new)?;
510
511        self.signed_transaction_with_blockhash_and_options(
512            latest_hash,
513            without_compute_budget,
514            compute_unit_price_micro_lamports,
515        )
516    }
517
518    /// Sign and send the transaction with options.
519    #[cfg(client)]
520    pub async fn send_with_options(
521        &self,
522        without_compute_budget: bool,
523        compute_unit_price_micro_lamports: Option<u64>,
524        mut config: RpcSendTransactionConfig,
525    ) -> crate::Result<WithSlot<Signature>> {
526        let client = self.cfg.rpc();
527        let latest_hash = client.get_latest_blockhash().await.map_err(Box::new)?;
528
529        let tx = self.signed_transaction_with_blockhash_and_options(
530            latest_hash,
531            without_compute_budget,
532            compute_unit_price_micro_lamports,
533        )?;
534
535        config.preflight_commitment = config
536            .preflight_commitment
537            .or(Some(client.commitment().commitment));
538
539        let signature = client
540            .send_and_confirm_transaction_with_config(&tx, config)
541            .await
542            .map_err(Box::new)?;
543
544        Ok(signature)
545    }
546
547    /// Build and send the transaction without preflight.
548    #[cfg(client)]
549    pub async fn send_without_preflight(self) -> crate::Result<Signature> {
550        Ok(self
551            .send_with_options(
552                false,
553                None,
554                RpcSendTransactionConfig {
555                    skip_preflight: true,
556                    ..Default::default()
557                },
558            )
559            .await?
560            .into_value())
561    }
562
563    /// Build and send the transaction with default options.
564    #[cfg(client)]
565    pub async fn send(self) -> crate::Result<Signature> {
566        Ok(self
567            .send_with_options(false, None, Default::default())
568            .await?
569            .into_value())
570    }
571
572    /// Get complete lookup table.
573    pub fn get_complete_lookup_table(&self) -> HashSet<Pubkey> {
574        self.luts
575            .values()
576            .flatten()
577            .copied()
578            .collect::<HashSet<_>>()
579    }
580
581    /// Get luts.
582    pub fn get_luts(&self) -> &HashMap<Pubkey, Vec<Pubkey>> {
583        &self.luts
584    }
585
586    /// Estimates the size of the result transaction.
587    ///
588    /// See [`transaction_size()`](crate::utils::transaction_size()) for more information.
589    pub fn transaction_size(&self, is_versioned_transaction: bool) -> usize {
590        let lookup_table = self.get_complete_lookup_table();
591        crate::utils::transaction_size(
592            &self.instructions(),
593            is_versioned_transaction,
594            Some(&lookup_table),
595            self.get_luts().len(),
596        )
597    }
598
599    /// Estimated the execution fee of the result transaction.
600    #[cfg(client)]
601    pub async fn estimate_execution_fee(
602        &self,
603        _client: &RpcClient,
604        compute_unit_price_micro_lamports: Option<u64>,
605    ) -> crate::Result<u64> {
606        let ixs = self.instructions_with_options(true, None);
607
608        let num_signers = ixs
609            .iter()
610            .flat_map(|ix| ix.accounts.iter())
611            .filter(|meta| meta.is_signer)
612            .map(|meta| &meta.pubkey)
613            .collect::<HashSet<_>>()
614            .len() as u64;
615        let fee = num_signers * 5_000 + self.compute_budget.fee(compute_unit_price_micro_lamports);
616        Ok(fee)
617    }
618}