Skip to main content

sof_solana_compat/
tx_builder.rs

1use solana_compute_budget_interface::ComputeBudgetInstruction;
2use solana_message::{Hash, Instruction, Message, VersionedMessage, v0};
3use solana_packet::PACKET_DATA_SIZE;
4use solana_pubkey::Pubkey;
5use solana_signer::{Signer, SignerError, signers::Signers};
6use solana_system_interface::instruction as system_instruction;
7use solana_transaction::sanitized::MAX_TX_ACCOUNT_LOCKS as SOLANA_MAX_TX_ACCOUNT_LOCKS;
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/// Current maximum serialized transaction payload size accepted by Solana networking.
19pub const MAX_TRANSACTION_WIRE_BYTES: usize = PACKET_DATA_SIZE;
20
21/// Current maximum number of account locks allowed per transaction.
22pub const MAX_TRANSACTION_ACCOUNT_LOCKS: usize = SOLANA_MAX_TX_ACCOUNT_LOCKS;
23
24/// Transaction message version emitted by [`TxBuilder`].
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum TxMessageVersion {
27    /// Legacy message encoding.
28    Legacy,
29    /// Version 0 message encoding.
30    #[default]
31    V0,
32}
33
34/// Builder-layer errors.
35#[derive(Debug, Error)]
36pub enum BuilderError {
37    /// Signing failed with signer-level error.
38    #[error("failed to sign transaction: {source}")]
39    SignTransaction {
40        /// Underlying signer error.
41        source: SignerError,
42    },
43}
44
45/// Unsigned transaction wrapper.
46#[derive(Debug, Clone)]
47pub struct UnsignedTx {
48    /// Versioned message ready to sign.
49    message: VersionedMessage,
50}
51
52impl UnsignedTx {
53    /// Returns the message payload.
54    #[must_use]
55    pub const fn message(&self) -> &VersionedMessage {
56        &self.message
57    }
58
59    /// Signs the message with provided signers.
60    ///
61    /// # Errors
62    ///
63    /// Returns [`BuilderError::SignTransaction`] when signer validation or signing fails.
64    pub fn sign<T>(self, signers: &T) -> Result<VersionedTransaction, BuilderError>
65    where
66        T: Signers + ?Sized,
67    {
68        VersionedTransaction::try_new(self.message, signers)
69            .map_err(|source| BuilderError::SignTransaction { source })
70    }
71}
72
73/// High-level builder for Solana versioned transaction messages.
74#[derive(Debug, Clone)]
75pub struct TxBuilder {
76    /// Fee payer and signer.
77    payer: Pubkey,
78    /// User-provided instructions.
79    instructions: Vec<Instruction>,
80    /// Optional compute unit limit.
81    compute_unit_limit: Option<u32>,
82    /// Optional priority fee (micro-lamports per compute unit).
83    priority_fee_micro_lamports: Option<u64>,
84    /// Optional developer tip lamports.
85    developer_tip_lamports: Option<u64>,
86    /// Tip recipient used when tip is enabled.
87    developer_tip_recipient: Pubkey,
88    /// Message version emitted by the builder.
89    message_version: TxMessageVersion,
90}
91
92impl TxBuilder {
93    /// Creates a transaction builder for a fee payer.
94    #[must_use]
95    pub const fn new(payer: Pubkey) -> Self {
96        Self {
97            payer,
98            instructions: Vec::new(),
99            compute_unit_limit: None,
100            priority_fee_micro_lamports: None,
101            developer_tip_lamports: None,
102            developer_tip_recipient: DEFAULT_DEVELOPER_TIP_RECIPIENT,
103            message_version: TxMessageVersion::V0,
104        }
105    }
106
107    /// Appends one instruction.
108    #[must_use]
109    pub fn add_instruction(mut self, instruction: Instruction) -> Self {
110        self.instructions.push(instruction);
111        self
112    }
113
114    /// Appends many instructions.
115    #[must_use]
116    pub fn add_instructions<I>(mut self, instructions: I) -> Self
117    where
118        I: IntoIterator<Item = Instruction>,
119    {
120        self.instructions.extend(instructions);
121        self
122    }
123
124    /// Sets compute unit limit.
125    #[must_use]
126    pub const fn with_compute_unit_limit(mut self, units: u32) -> Self {
127        self.compute_unit_limit = Some(units);
128        self
129    }
130
131    /// Removes any explicit compute unit limit instruction.
132    #[must_use]
133    pub const fn without_compute_unit_limit(mut self) -> Self {
134        self.compute_unit_limit = None;
135        self
136    }
137
138    /// Sets priority fee in micro-lamports.
139    #[must_use]
140    pub const fn with_priority_fee_micro_lamports(mut self, micro_lamports: u64) -> Self {
141        self.priority_fee_micro_lamports = Some(micro_lamports);
142        self
143    }
144
145    /// Removes any explicit priority-fee instruction.
146    #[must_use]
147    pub const fn without_priority_fee_micro_lamports(mut self) -> Self {
148        self.priority_fee_micro_lamports = None;
149        self
150    }
151
152    /// Enables default developer tip.
153    #[must_use]
154    pub const fn tip_developer(mut self) -> Self {
155        self.developer_tip_lamports = Some(DEFAULT_DEVELOPER_TIP_LAMPORTS);
156        self
157    }
158
159    /// Enables developer tip with explicit lamports.
160    #[must_use]
161    pub const fn tip_developer_lamports(mut self, lamports: u64) -> Self {
162        self.developer_tip_lamports = Some(lamports);
163        self
164    }
165
166    /// Sets a custom tip recipient and lamports.
167    #[must_use]
168    pub const fn tip_to(mut self, recipient: Pubkey, lamports: u64) -> Self {
169        self.developer_tip_recipient = recipient;
170        self.developer_tip_lamports = Some(lamports);
171        self
172    }
173
174    /// Sets the message version emitted by the builder.
175    #[must_use]
176    pub const fn with_message_version(mut self, version: TxMessageVersion) -> Self {
177        self.message_version = version;
178        self
179    }
180
181    /// Forces legacy message output.
182    #[must_use]
183    pub const fn with_legacy_message(self) -> Self {
184        self.with_message_version(TxMessageVersion::Legacy)
185    }
186
187    /// Forces version 0 message output.
188    #[must_use]
189    pub const fn with_v0_message(self) -> Self {
190        self.with_message_version(TxMessageVersion::V0)
191    }
192
193    /// Builds an unsigned transaction wrapper.
194    #[must_use]
195    pub fn build_unsigned(self, recent_blockhash: [u8; 32]) -> UnsignedTx {
196        UnsignedTx {
197            message: self.build_message(recent_blockhash),
198        }
199    }
200
201    /// Builds and signs a transaction in one step.
202    ///
203    /// # Errors
204    ///
205    /// Returns [`BuilderError::SignTransaction`] when signer validation or signing fails.
206    pub fn build_and_sign<T>(
207        self,
208        recent_blockhash: [u8; 32],
209        signers: &T,
210    ) -> Result<VersionedTransaction, BuilderError>
211    where
212        T: Signers + ?Sized,
213    {
214        self.build_unsigned(recent_blockhash).sign(signers)
215    }
216
217    /// Builds a message wrapped as a versioned message.
218    #[must_use]
219    pub fn build_message(self, recent_blockhash: [u8; 32]) -> VersionedMessage {
220        let mut instructions = Vec::new();
221        if let Some(units) = self.compute_unit_limit {
222            instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(units));
223        }
224        if let Some(micro_lamports) = self.priority_fee_micro_lamports {
225            instructions.push(ComputeBudgetInstruction::set_compute_unit_price(
226                micro_lamports,
227            ));
228        }
229        instructions.extend(self.instructions);
230        if let Some(lamports) = self.developer_tip_lamports {
231            instructions.push(system_instruction::transfer(
232                &self.payer,
233                &self.developer_tip_recipient,
234                lamports,
235            ));
236        }
237        let blockhash = Hash::new_from_array(recent_blockhash);
238        let legacy_message =
239            Message::new_with_blockhash(&instructions, Some(&self.payer), &blockhash);
240        match self.message_version {
241            TxMessageVersion::Legacy => VersionedMessage::Legacy(legacy_message),
242            TxMessageVersion::V0 => VersionedMessage::V0(v0::Message {
243                header: legacy_message.header,
244                account_keys: legacy_message.account_keys,
245                recent_blockhash: legacy_message.recent_blockhash,
246                instructions: legacy_message.instructions,
247                address_table_lookups: Vec::new(),
248            }),
249        }
250    }
251}
252
253/// Borrowed signer reference for Solana-coupled call sites.
254#[derive(Clone, Copy)]
255pub struct SignerRef<'signer> {
256    /// Borrowed signer trait object.
257    signer: &'signer dyn Signer,
258}
259
260impl<'signer> SignerRef<'signer> {
261    /// Creates one borrowed signer reference.
262    #[must_use]
263    pub fn new(signer: &'signer dyn Signer) -> Self {
264        Self { signer }
265    }
266
267    /// Returns the borrowed signer trait object.
268    #[must_use]
269    pub fn as_signer(self) -> &'signer dyn Signer {
270        self.signer
271    }
272}