1use solana_compute_budget_interface::ComputeBudgetInstruction;
4use solana_message::{Hash, Instruction, Message, VersionedMessage, v0};
5use solana_packet::PACKET_DATA_SIZE;
6use solana_pubkey::Pubkey;
7use solana_signer::{SignerError, signers::Signers};
8use solana_system_interface::instruction as system_instruction;
9use solana_transaction::sanitized::MAX_TX_ACCOUNT_LOCKS as SOLANA_MAX_TX_ACCOUNT_LOCKS;
10use solana_transaction::versioned::VersionedTransaction;
11use thiserror::Error;
12
13pub const DEFAULT_DEVELOPER_TIP_LAMPORTS: u64 = 5_000;
15
16pub const DEFAULT_DEVELOPER_TIP_RECIPIENT: Pubkey =
18 Pubkey::from_str_const("G3WHMVjx7Cb3MFhBAHe52zw8yhbHodWnas5gYLceaqze");
19
20pub const MAX_TRANSACTION_WIRE_BYTES: usize = PACKET_DATA_SIZE;
22
23pub const MAX_TRANSACTION_ACCOUNT_LOCKS: usize = SOLANA_MAX_TX_ACCOUNT_LOCKS;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum TxMessageVersion {
29 Legacy,
31 #[default]
33 V0,
34}
35
36#[derive(Debug, Error)]
38pub enum BuilderError {
39 #[error("failed to sign transaction: {source}")]
41 SignTransaction {
42 source: SignerError,
44 },
45}
46
47#[derive(Debug, Clone)]
49pub struct UnsignedTx {
50 message: VersionedMessage,
52}
53
54impl UnsignedTx {
55 #[must_use]
57 pub const fn message(&self) -> &VersionedMessage {
58 &self.message
59 }
60
61 pub fn sign<T>(self, signers: &T) -> Result<VersionedTransaction, BuilderError>
67 where
68 T: Signers + ?Sized,
69 {
70 VersionedTransaction::try_new(self.message, signers)
71 .map_err(|source| BuilderError::SignTransaction { source })
72 }
73}
74
75#[derive(Debug, Clone)]
77pub struct TxBuilder {
78 payer: Pubkey,
80 instructions: Vec<Instruction>,
82 compute_unit_limit: Option<u32>,
84 priority_fee_micro_lamports: Option<u64>,
86 developer_tip_lamports: Option<u64>,
88 developer_tip_recipient: Pubkey,
90 message_version: TxMessageVersion,
92}
93
94impl TxBuilder {
95 #[must_use]
97 pub const fn new(payer: Pubkey) -> Self {
98 Self {
99 payer,
100 instructions: Vec::new(),
101 compute_unit_limit: None,
102 priority_fee_micro_lamports: None,
103 developer_tip_lamports: None,
104 developer_tip_recipient: DEFAULT_DEVELOPER_TIP_RECIPIENT,
105 message_version: TxMessageVersion::V0,
106 }
107 }
108
109 #[must_use]
111 pub fn add_instruction(mut self, instruction: Instruction) -> Self {
112 self.instructions.push(instruction);
113 self
114 }
115
116 #[must_use]
118 pub fn add_instructions<I>(mut self, instructions: I) -> Self
119 where
120 I: IntoIterator<Item = Instruction>,
121 {
122 self.instructions.extend(instructions);
123 self
124 }
125
126 #[must_use]
128 pub const fn with_compute_unit_limit(mut self, units: u32) -> Self {
129 self.compute_unit_limit = Some(units);
130 self
131 }
132
133 #[must_use]
135 pub const fn without_compute_unit_limit(mut self) -> Self {
136 self.compute_unit_limit = None;
137 self
138 }
139
140 #[must_use]
142 pub const fn with_priority_fee_micro_lamports(mut self, micro_lamports: u64) -> Self {
143 self.priority_fee_micro_lamports = Some(micro_lamports);
144 self
145 }
146
147 #[must_use]
149 pub const fn without_priority_fee_micro_lamports(mut self) -> Self {
150 self.priority_fee_micro_lamports = None;
151 self
152 }
153
154 #[must_use]
156 pub const fn tip_developer(mut self) -> Self {
157 self.developer_tip_lamports = Some(DEFAULT_DEVELOPER_TIP_LAMPORTS);
158 self
159 }
160
161 #[must_use]
163 pub const fn tip_developer_lamports(mut self, lamports: u64) -> Self {
164 self.developer_tip_lamports = Some(lamports);
165 self
166 }
167
168 #[must_use]
170 pub const fn tip_to(mut self, recipient: Pubkey, lamports: u64) -> Self {
171 self.developer_tip_recipient = recipient;
172 self.developer_tip_lamports = Some(lamports);
173 self
174 }
175
176 #[must_use]
178 pub const fn with_message_version(mut self, version: TxMessageVersion) -> Self {
179 self.message_version = version;
180 self
181 }
182
183 #[must_use]
185 pub const fn with_legacy_message(self) -> Self {
186 self.with_message_version(TxMessageVersion::Legacy)
187 }
188
189 #[must_use]
191 pub const fn with_v0_message(self) -> Self {
192 self.with_message_version(TxMessageVersion::V0)
193 }
194
195 #[must_use]
197 pub fn build_unsigned(self, recent_blockhash: [u8; 32]) -> UnsignedTx {
198 UnsignedTx {
199 message: self.build_message(recent_blockhash),
200 }
201 }
202
203 pub fn build_and_sign<T>(
209 self,
210 recent_blockhash: [u8; 32],
211 signers: &T,
212 ) -> Result<VersionedTransaction, BuilderError>
213 where
214 T: Signers + ?Sized,
215 {
216 self.build_unsigned(recent_blockhash).sign(signers)
217 }
218
219 #[must_use]
221 pub fn build_message(self, recent_blockhash: [u8; 32]) -> VersionedMessage {
222 let mut instructions = Vec::new();
223 if let Some(units) = self.compute_unit_limit {
224 instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(units));
225 }
226 if let Some(micro_lamports) = self.priority_fee_micro_lamports {
227 instructions.push(ComputeBudgetInstruction::set_compute_unit_price(
228 micro_lamports,
229 ));
230 }
231 instructions.extend(self.instructions);
232 if let Some(lamports) = self.developer_tip_lamports {
233 instructions.push(system_instruction::transfer(
234 &self.payer,
235 &self.developer_tip_recipient,
236 lamports,
237 ));
238 }
239 let blockhash = Hash::new_from_array(recent_blockhash);
240 let legacy_message =
241 Message::new_with_blockhash(&instructions, Some(&self.payer), &blockhash);
242 match self.message_version {
243 TxMessageVersion::Legacy => VersionedMessage::Legacy(legacy_message),
244 TxMessageVersion::V0 => VersionedMessage::V0(v0::Message {
245 header: legacy_message.header,
246 account_keys: legacy_message.account_keys,
247 recent_blockhash: legacy_message.recent_blockhash,
248 instructions: legacy_message.instructions,
249 address_table_lookups: Vec::new(),
250 }),
251 }
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use solana_keypair::Keypair;
258 use solana_signer::Signer;
259
260 use super::*;
261
262 #[test]
263 fn tip_developer_adds_system_transfer_instruction() {
264 let payer = Keypair::new();
265 let message = TxBuilder::new(payer.pubkey())
266 .tip_developer()
267 .build_message([1_u8; 32]);
268
269 let keys = message.static_account_keys();
270 let instructions = message.instructions();
271 assert_eq!(instructions.len(), 1);
272 assert!(matches!(message, VersionedMessage::V0(_)));
273
274 let first = instructions.first();
275 assert!(first.is_some());
276 if let Some(instruction) = first {
277 let program_idx = usize::from(instruction.program_id_index);
278 let program = keys.get(program_idx);
279 assert!(program.is_some());
280 if let Some(program) = program {
281 assert_eq!(*program, solana_system_interface::program::ID);
282 }
283 }
284 }
285
286 #[test]
287 fn compute_budget_instructions_are_prefixed() {
288 let payer = Keypair::new();
289 let recipient = Pubkey::new_unique();
290 let message = TxBuilder::new(payer.pubkey())
291 .with_compute_unit_limit(500_000)
292 .with_priority_fee_micro_lamports(10_000)
293 .add_instruction(system_instruction::transfer(&payer.pubkey(), &recipient, 1))
294 .build_message([2_u8; 32]);
295
296 let instructions = message.instructions();
297 assert_eq!(instructions.len(), 3);
298 assert!(matches!(message, VersionedMessage::V0(_)));
299 let first = instructions.first();
300 assert!(first.is_some());
301 if let Some(first) = first {
302 assert_eq!(first.data.first().copied(), Some(2_u8));
303 }
304 let second = instructions.get(1);
305 assert!(second.is_some());
306 if let Some(second) = second {
307 assert_eq!(second.data.first().copied(), Some(3_u8));
308 }
309 }
310
311 #[test]
312 fn build_and_sign_generates_signature() {
313 let payer = Keypair::new();
314 let recipient = Pubkey::new_unique();
315 let tx_result = TxBuilder::new(payer.pubkey())
316 .add_instruction(system_instruction::transfer(&payer.pubkey(), &recipient, 1))
317 .build_and_sign([3_u8; 32], &[&payer]);
318
319 assert!(tx_result.is_ok());
320 if let Ok(tx) = tx_result {
321 assert_eq!(tx.signatures.len(), 1);
322 let first = tx.signatures.first();
323 assert!(first.is_some());
324 if let Some(first) = first {
325 assert_ne!(*first, solana_signature::Signature::default());
326 }
327 }
328 }
329
330 #[test]
331 fn legacy_message_override_builds_legacy_message() {
332 let payer = Keypair::new();
333 let recipient = Pubkey::new_unique();
334 let message = TxBuilder::new(payer.pubkey())
335 .with_legacy_message()
336 .add_instruction(system_instruction::transfer(&payer.pubkey(), &recipient, 1))
337 .build_message([4_u8; 32]);
338
339 assert!(matches!(message, VersionedMessage::Legacy(_)));
340 }
341
342 #[test]
343 fn exported_limit_constants_match_runtime_constants() {
344 assert_eq!(MAX_TRANSACTION_WIRE_BYTES, PACKET_DATA_SIZE);
345 assert_eq!(MAX_TRANSACTION_ACCOUNT_LOCKS, SOLANA_MAX_TX_ACCOUNT_LOCKS);
346 }
347}