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 with_priority_fee_micro_lamports(mut self, micro_lamports: u64) -> Self {
136 self.priority_fee_micro_lamports = Some(micro_lamports);
137 self
138 }
139
140 #[must_use]
142 pub const fn tip_developer(mut self) -> Self {
143 self.developer_tip_lamports = Some(DEFAULT_DEVELOPER_TIP_LAMPORTS);
144 self
145 }
146
147 #[must_use]
149 pub const fn tip_developer_lamports(mut self, lamports: u64) -> Self {
150 self.developer_tip_lamports = Some(lamports);
151 self
152 }
153
154 #[must_use]
156 pub const fn tip_to(mut self, recipient: Pubkey, lamports: u64) -> Self {
157 self.developer_tip_recipient = recipient;
158 self.developer_tip_lamports = Some(lamports);
159 self
160 }
161
162 #[must_use]
164 pub const fn with_message_version(mut self, version: TxMessageVersion) -> Self {
165 self.message_version = version;
166 self
167 }
168
169 #[must_use]
171 pub const fn with_legacy_message(self) -> Self {
172 self.with_message_version(TxMessageVersion::Legacy)
173 }
174
175 #[must_use]
177 pub const fn with_v0_message(self) -> Self {
178 self.with_message_version(TxMessageVersion::V0)
179 }
180
181 #[must_use]
183 pub fn build_unsigned(self, recent_blockhash: [u8; 32]) -> UnsignedTx {
184 UnsignedTx {
185 message: self.build_message(recent_blockhash),
186 }
187 }
188
189 pub fn build_and_sign<T>(
195 self,
196 recent_blockhash: [u8; 32],
197 signers: &T,
198 ) -> Result<VersionedTransaction, BuilderError>
199 where
200 T: Signers + ?Sized,
201 {
202 self.build_unsigned(recent_blockhash).sign(signers)
203 }
204
205 #[must_use]
207 pub fn build_message(self, recent_blockhash: [u8; 32]) -> VersionedMessage {
208 let mut instructions = Vec::new();
209 if let Some(units) = self.compute_unit_limit {
210 instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(units));
211 }
212 if let Some(micro_lamports) = self.priority_fee_micro_lamports {
213 instructions.push(ComputeBudgetInstruction::set_compute_unit_price(
214 micro_lamports,
215 ));
216 }
217 instructions.extend(self.instructions);
218 if let Some(lamports) = self.developer_tip_lamports {
219 instructions.push(system_instruction::transfer(
220 &self.payer,
221 &self.developer_tip_recipient,
222 lamports,
223 ));
224 }
225 let blockhash = Hash::new_from_array(recent_blockhash);
226 let legacy_message =
227 Message::new_with_blockhash(&instructions, Some(&self.payer), &blockhash);
228 match self.message_version {
229 TxMessageVersion::Legacy => VersionedMessage::Legacy(legacy_message),
230 TxMessageVersion::V0 => VersionedMessage::V0(v0::Message {
231 header: legacy_message.header,
232 account_keys: legacy_message.account_keys,
233 recent_blockhash: legacy_message.recent_blockhash,
234 instructions: legacy_message.instructions,
235 address_table_lookups: Vec::new(),
236 }),
237 }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use solana_keypair::Keypair;
244 use solana_signer::Signer;
245
246 use super::*;
247
248 #[test]
249 fn tip_developer_adds_system_transfer_instruction() {
250 let payer = Keypair::new();
251 let message = TxBuilder::new(payer.pubkey())
252 .tip_developer()
253 .build_message([1_u8; 32]);
254
255 let keys = message.static_account_keys();
256 let instructions = message.instructions();
257 assert_eq!(instructions.len(), 1);
258 assert!(matches!(message, VersionedMessage::V0(_)));
259
260 let first = instructions.first();
261 assert!(first.is_some());
262 if let Some(instruction) = first {
263 let program_idx = usize::from(instruction.program_id_index);
264 let program = keys.get(program_idx);
265 assert!(program.is_some());
266 if let Some(program) = program {
267 assert_eq!(*program, solana_system_interface::program::ID);
268 }
269 }
270 }
271
272 #[test]
273 fn compute_budget_instructions_are_prefixed() {
274 let payer = Keypair::new();
275 let recipient = Pubkey::new_unique();
276 let message = TxBuilder::new(payer.pubkey())
277 .with_compute_unit_limit(500_000)
278 .with_priority_fee_micro_lamports(10_000)
279 .add_instruction(system_instruction::transfer(&payer.pubkey(), &recipient, 1))
280 .build_message([2_u8; 32]);
281
282 let instructions = message.instructions();
283 assert_eq!(instructions.len(), 3);
284 assert!(matches!(message, VersionedMessage::V0(_)));
285 let first = instructions.first();
286 assert!(first.is_some());
287 if let Some(first) = first {
288 assert_eq!(first.data.first().copied(), Some(2_u8));
289 }
290 let second = instructions.get(1);
291 assert!(second.is_some());
292 if let Some(second) = second {
293 assert_eq!(second.data.first().copied(), Some(3_u8));
294 }
295 }
296
297 #[test]
298 fn build_and_sign_generates_signature() {
299 let payer = Keypair::new();
300 let recipient = Pubkey::new_unique();
301 let tx_result = TxBuilder::new(payer.pubkey())
302 .add_instruction(system_instruction::transfer(&payer.pubkey(), &recipient, 1))
303 .build_and_sign([3_u8; 32], &[&payer]);
304
305 assert!(tx_result.is_ok());
306 if let Ok(tx) = tx_result {
307 assert_eq!(tx.signatures.len(), 1);
308 let first = tx.signatures.first();
309 assert!(first.is_some());
310 if let Some(first) = first {
311 assert_ne!(*first, solana_signature::Signature::default());
312 }
313 }
314 }
315
316 #[test]
317 fn legacy_message_override_builds_legacy_message() {
318 let payer = Keypair::new();
319 let recipient = Pubkey::new_unique();
320 let message = TxBuilder::new(payer.pubkey())
321 .with_legacy_message()
322 .add_instruction(system_instruction::transfer(&payer.pubkey(), &recipient, 1))
323 .build_message([4_u8; 32]);
324
325 assert!(matches!(message, VersionedMessage::Legacy(_)));
326 }
327
328 #[test]
329 fn exported_limit_constants_match_runtime_constants() {
330 assert_eq!(MAX_TRANSACTION_WIRE_BYTES, PACKET_DATA_SIZE);
331 assert_eq!(MAX_TRANSACTION_ACCOUNT_LOCKS, SOLANA_MAX_TX_ACCOUNT_LOCKS);
332 }
333}