Skip to main content

sof_tx/
builder.rs

1//! High-level transaction builder APIs.
2
3use solana_compute_budget_interface::ComputeBudgetInstruction;
4use solana_message::{Hash, Instruction, Message, VersionedMessage};
5use solana_pubkey::Pubkey;
6use solana_signer::{SignerError, signers::Signers};
7use solana_system_interface::instruction as system_instruction;
8use solana_transaction::versioned::VersionedTransaction;
9use thiserror::Error;
10
11/// Default lamports tipped by [`TxBuilder::tip_developer`].
12pub const DEFAULT_DEVELOPER_TIP_LAMPORTS: u64 = 5_000;
13
14/// Default developer tip recipient used by [`TxBuilder::tip_developer`].
15pub const DEFAULT_DEVELOPER_TIP_RECIPIENT: Pubkey =
16    Pubkey::from_str_const("G3WHMVjx7Cb3MFhBAHe52zw8yhbHodWnas5gYLceaqze");
17
18/// Builder-layer errors.
19#[derive(Debug, Error)]
20pub enum BuilderError {
21    /// Signing failed with signer-level error.
22    #[error("failed to sign transaction: {source}")]
23    SignTransaction {
24        /// Underlying signer error.
25        source: SignerError,
26    },
27}
28
29/// Unsigned transaction wrapper.
30#[derive(Debug, Clone)]
31pub struct UnsignedTx {
32    /// Versioned message ready to sign.
33    message: VersionedMessage,
34}
35
36impl UnsignedTx {
37    /// Returns the message payload.
38    #[must_use]
39    pub const fn message(&self) -> &VersionedMessage {
40        &self.message
41    }
42
43    /// Signs the message with provided signers.
44    ///
45    /// # Errors
46    ///
47    /// Returns [`BuilderError::SignTransaction`] when signer validation or signing fails.
48    pub fn sign<T>(self, signers: &T) -> Result<VersionedTransaction, BuilderError>
49    where
50        T: Signers + ?Sized,
51    {
52        VersionedTransaction::try_new(self.message, signers)
53            .map_err(|source| BuilderError::SignTransaction { source })
54    }
55}
56
57/// High-level builder for legacy-versioned transaction messages.
58#[derive(Debug, Clone)]
59pub struct TxBuilder {
60    /// Fee payer and signer.
61    payer: Pubkey,
62    /// User-provided instructions.
63    instructions: Vec<Instruction>,
64    /// Optional compute unit limit.
65    compute_unit_limit: Option<u32>,
66    /// Optional priority fee (micro-lamports per compute unit).
67    priority_fee_micro_lamports: Option<u64>,
68    /// Optional developer tip lamports.
69    developer_tip_lamports: Option<u64>,
70    /// Tip recipient used when tip is enabled.
71    developer_tip_recipient: Pubkey,
72}
73
74impl TxBuilder {
75    /// Creates a transaction builder for a fee payer.
76    #[must_use]
77    pub const fn new(payer: Pubkey) -> Self {
78        Self {
79            payer,
80            instructions: Vec::new(),
81            compute_unit_limit: None,
82            priority_fee_micro_lamports: None,
83            developer_tip_lamports: None,
84            developer_tip_recipient: DEFAULT_DEVELOPER_TIP_RECIPIENT,
85        }
86    }
87
88    /// Appends one instruction.
89    #[must_use]
90    pub fn add_instruction(mut self, instruction: Instruction) -> Self {
91        self.instructions.push(instruction);
92        self
93    }
94
95    /// Appends many instructions.
96    #[must_use]
97    pub fn add_instructions<I>(mut self, instructions: I) -> Self
98    where
99        I: IntoIterator<Item = Instruction>,
100    {
101        self.instructions.extend(instructions);
102        self
103    }
104
105    /// Sets compute unit limit.
106    #[must_use]
107    pub const fn with_compute_unit_limit(mut self, units: u32) -> Self {
108        self.compute_unit_limit = Some(units);
109        self
110    }
111
112    /// Sets priority fee in micro-lamports.
113    #[must_use]
114    pub const fn with_priority_fee_micro_lamports(mut self, micro_lamports: u64) -> Self {
115        self.priority_fee_micro_lamports = Some(micro_lamports);
116        self
117    }
118
119    /// Enables default developer tip.
120    #[must_use]
121    pub const fn tip_developer(mut self) -> Self {
122        self.developer_tip_lamports = Some(DEFAULT_DEVELOPER_TIP_LAMPORTS);
123        self
124    }
125
126    /// Enables developer tip with explicit lamports.
127    #[must_use]
128    pub const fn tip_developer_lamports(mut self, lamports: u64) -> Self {
129        self.developer_tip_lamports = Some(lamports);
130        self
131    }
132
133    /// Sets a custom tip recipient and lamports.
134    #[must_use]
135    pub const fn tip_to(mut self, recipient: Pubkey, lamports: u64) -> Self {
136        self.developer_tip_recipient = recipient;
137        self.developer_tip_lamports = Some(lamports);
138        self
139    }
140
141    /// Builds an unsigned transaction wrapper.
142    #[must_use]
143    pub fn build_unsigned(self, recent_blockhash: [u8; 32]) -> UnsignedTx {
144        UnsignedTx {
145            message: self.build_message(recent_blockhash),
146        }
147    }
148
149    /// Builds and signs a transaction in one step.
150    ///
151    /// # Errors
152    ///
153    /// Returns [`BuilderError::SignTransaction`] when signer validation or signing fails.
154    pub fn build_and_sign<T>(
155        self,
156        recent_blockhash: [u8; 32],
157        signers: &T,
158    ) -> Result<VersionedTransaction, BuilderError>
159    where
160        T: Signers + ?Sized,
161    {
162        self.build_unsigned(recent_blockhash).sign(signers)
163    }
164
165    /// Builds a legacy message wrapped as a versioned message.
166    #[must_use]
167    pub fn build_message(self, recent_blockhash: [u8; 32]) -> VersionedMessage {
168        let mut instructions = Vec::new();
169        if let Some(units) = self.compute_unit_limit {
170            instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(units));
171        }
172        if let Some(micro_lamports) = self.priority_fee_micro_lamports {
173            instructions.push(ComputeBudgetInstruction::set_compute_unit_price(
174                micro_lamports,
175            ));
176        }
177        instructions.extend(self.instructions);
178        if let Some(lamports) = self.developer_tip_lamports {
179            instructions.push(system_instruction::transfer(
180                &self.payer,
181                &self.developer_tip_recipient,
182                lamports,
183            ));
184        }
185        let blockhash = Hash::new_from_array(recent_blockhash);
186        let message = Message::new_with_blockhash(&instructions, Some(&self.payer), &blockhash);
187        VersionedMessage::Legacy(message)
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use solana_keypair::Keypair;
194    use solana_signer::Signer;
195
196    use super::*;
197
198    #[test]
199    fn tip_developer_adds_system_transfer_instruction() {
200        let payer = Keypair::new();
201        let message = TxBuilder::new(payer.pubkey())
202            .tip_developer()
203            .build_message([1_u8; 32]);
204
205        let keys = message.static_account_keys();
206        let instructions = message.instructions();
207        assert_eq!(instructions.len(), 1);
208
209        let first = instructions.first();
210        assert!(first.is_some());
211        if let Some(instruction) = first {
212            let program_idx = usize::from(instruction.program_id_index);
213            let program = keys.get(program_idx);
214            assert!(program.is_some());
215            if let Some(program) = program {
216                assert_eq!(*program, solana_system_interface::program::ID);
217            }
218        }
219    }
220
221    #[test]
222    fn compute_budget_instructions_are_prefixed() {
223        let payer = Keypair::new();
224        let recipient = Pubkey::new_unique();
225        let message = TxBuilder::new(payer.pubkey())
226            .with_compute_unit_limit(500_000)
227            .with_priority_fee_micro_lamports(10_000)
228            .add_instruction(system_instruction::transfer(&payer.pubkey(), &recipient, 1))
229            .build_message([2_u8; 32]);
230
231        let instructions = message.instructions();
232        assert_eq!(instructions.len(), 3);
233        let first = instructions.first();
234        assert!(first.is_some());
235        if let Some(first) = first {
236            assert_eq!(first.data.first().copied(), Some(2_u8));
237        }
238        let second = instructions.get(1);
239        assert!(second.is_some());
240        if let Some(second) = second {
241            assert_eq!(second.data.first().copied(), Some(3_u8));
242        }
243    }
244
245    #[test]
246    fn build_and_sign_generates_signature() {
247        let payer = Keypair::new();
248        let recipient = Pubkey::new_unique();
249        let tx_result = TxBuilder::new(payer.pubkey())
250            .add_instruction(system_instruction::transfer(&payer.pubkey(), &recipient, 1))
251            .build_and_sign([3_u8; 32], &[&payer]);
252
253        assert!(tx_result.is_ok());
254        if let Ok(tx) = tx_result {
255            assert_eq!(tx.signatures.len(), 1);
256            let first = tx.signatures.first();
257            assert!(first.is_some());
258            if let Some(first) = first {
259                assert_ne!(*first, solana_signature::Signature::default());
260            }
261        }
262    }
263}