1use 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
11pub const DEFAULT_DEVELOPER_TIP_LAMPORTS: u64 = 5_000;
13
14pub const DEFAULT_DEVELOPER_TIP_RECIPIENT: Pubkey =
16 Pubkey::from_str_const("G3WHMVjx7Cb3MFhBAHe52zw8yhbHodWnas5gYLceaqze");
17
18#[derive(Debug, Error)]
20pub enum BuilderError {
21 #[error("failed to sign transaction: {source}")]
23 SignTransaction {
24 source: SignerError,
26 },
27}
28
29#[derive(Debug, Clone)]
31pub struct UnsignedTx {
32 message: VersionedMessage,
34}
35
36impl UnsignedTx {
37 #[must_use]
39 pub const fn message(&self) -> &VersionedMessage {
40 &self.message
41 }
42
43 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#[derive(Debug, Clone)]
59pub struct TxBuilder {
60 payer: Pubkey,
62 instructions: Vec<Instruction>,
64 compute_unit_limit: Option<u32>,
66 priority_fee_micro_lamports: Option<u64>,
68 developer_tip_lamports: Option<u64>,
70 developer_tip_recipient: Pubkey,
72}
73
74impl TxBuilder {
75 #[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 #[must_use]
90 pub fn add_instruction(mut self, instruction: Instruction) -> Self {
91 self.instructions.push(instruction);
92 self
93 }
94
95 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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}