solana_sdk/
fee.rs

1//! Fee structures.
2
3#[cfg(not(target_os = "solana"))]
4use solana_program::message::SanitizedMessage;
5use {solana_native_token::sol_to_lamports, std::num::NonZeroU32};
6
7/// A fee and its associated compute unit limit
8#[derive(Debug, Default, Clone, Eq, PartialEq)]
9pub struct FeeBin {
10    /// maximum compute units for which this fee will be charged
11    pub limit: u64,
12    /// fee in lamports
13    pub fee: u64,
14}
15
16pub struct FeeBudgetLimits {
17    pub loaded_accounts_data_size_limit: NonZeroU32,
18    pub heap_cost: u64,
19    pub compute_unit_limit: u64,
20    pub prioritization_fee: u64,
21}
22
23/// Information used to calculate fees
24#[derive(Debug, Clone, Eq, PartialEq)]
25pub struct FeeStructure {
26    /// lamports per signature
27    pub lamports_per_signature: u64,
28    /// lamports_per_write_lock
29    pub lamports_per_write_lock: u64,
30    /// Compute unit fee bins
31    pub compute_fee_bins: Vec<FeeBin>,
32}
33
34#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)]
35pub struct FeeDetails {
36    transaction_fee: u64,
37    prioritization_fee: u64,
38    remove_rounding_in_fee_calculation: bool,
39}
40
41impl FeeDetails {
42    pub fn new(
43        transaction_fee: u64,
44        prioritization_fee: u64,
45        remove_rounding_in_fee_calculation: bool,
46    ) -> Self {
47        Self {
48            transaction_fee,
49            prioritization_fee,
50            remove_rounding_in_fee_calculation,
51        }
52    }
53
54    pub fn total_fee(&self) -> u64 {
55        let total_fee = self.transaction_fee.saturating_add(self.prioritization_fee);
56        if self.remove_rounding_in_fee_calculation {
57            total_fee
58        } else {
59            // backward compatible behavior
60            (total_fee as f64).round() as u64
61        }
62    }
63
64    pub fn accumulate(&mut self, fee_details: &FeeDetails) {
65        self.transaction_fee = self
66            .transaction_fee
67            .saturating_add(fee_details.transaction_fee);
68        self.prioritization_fee = self
69            .prioritization_fee
70            .saturating_add(fee_details.prioritization_fee)
71    }
72
73    pub fn transaction_fee(&self) -> u64 {
74        self.transaction_fee
75    }
76
77    pub fn prioritization_fee(&self) -> u64 {
78        self.prioritization_fee
79    }
80}
81
82pub const ACCOUNT_DATA_COST_PAGE_SIZE: u64 = 32_u64.saturating_mul(1024);
83
84impl FeeStructure {
85    pub fn new(
86        sol_per_signature: f64,
87        sol_per_write_lock: f64,
88        compute_fee_bins: Vec<(u64, f64)>,
89    ) -> Self {
90        let compute_fee_bins = compute_fee_bins
91            .iter()
92            .map(|(limit, sol)| FeeBin {
93                limit: *limit,
94                fee: sol_to_lamports(*sol),
95            })
96            .collect::<Vec<_>>();
97        FeeStructure {
98            lamports_per_signature: sol_to_lamports(sol_per_signature),
99            lamports_per_write_lock: sol_to_lamports(sol_per_write_lock),
100            compute_fee_bins,
101        }
102    }
103
104    pub fn get_max_fee(&self, num_signatures: u64, num_write_locks: u64) -> u64 {
105        num_signatures
106            .saturating_mul(self.lamports_per_signature)
107            .saturating_add(num_write_locks.saturating_mul(self.lamports_per_write_lock))
108            .saturating_add(
109                self.compute_fee_bins
110                    .last()
111                    .map(|bin| bin.fee)
112                    .unwrap_or_default(),
113            )
114    }
115
116    pub fn calculate_memory_usage_cost(
117        loaded_accounts_data_size_limit: u32,
118        heap_cost: u64,
119    ) -> u64 {
120        (loaded_accounts_data_size_limit as u64)
121            .saturating_add(ACCOUNT_DATA_COST_PAGE_SIZE.saturating_sub(1))
122            .saturating_div(ACCOUNT_DATA_COST_PAGE_SIZE)
123            .saturating_mul(heap_cost)
124    }
125
126    /// Calculate fee for `SanitizedMessage`
127    #[cfg(not(target_os = "solana"))]
128    #[deprecated(
129        since = "2.1.0",
130        note = "Please use `solana_fee::calculate_fee` instead."
131    )]
132    pub fn calculate_fee(
133        &self,
134        message: &SanitizedMessage,
135        lamports_per_signature: u64,
136        budget_limits: &FeeBudgetLimits,
137        include_loaded_account_data_size_in_fee: bool,
138        remove_rounding_in_fee_calculation: bool,
139    ) -> u64 {
140        #[allow(deprecated)]
141        self.calculate_fee_details(
142            message,
143            lamports_per_signature,
144            budget_limits,
145            include_loaded_account_data_size_in_fee,
146            remove_rounding_in_fee_calculation,
147        )
148        .total_fee()
149    }
150
151    /// Calculate fee details for `SanitizedMessage`
152    #[cfg(not(target_os = "solana"))]
153    #[deprecated(
154        since = "2.1.0",
155        note = "Please use `solana_fee::calculate_fee_details` instead."
156    )]
157    pub fn calculate_fee_details(
158        &self,
159        message: &SanitizedMessage,
160        lamports_per_signature: u64,
161        budget_limits: &FeeBudgetLimits,
162        include_loaded_account_data_size_in_fee: bool,
163        remove_rounding_in_fee_calculation: bool,
164    ) -> FeeDetails {
165        // Backward compatibility - lamports_per_signature == 0 means to clear
166        // transaction fee to zero
167        if lamports_per_signature == 0 {
168            return FeeDetails::default();
169        }
170
171        let signature_fee = message
172            .num_total_signatures()
173            .saturating_mul(self.lamports_per_signature);
174        let write_lock_fee = message
175            .num_write_locks()
176            .saturating_mul(self.lamports_per_write_lock);
177
178        // `compute_fee` covers costs for both requested_compute_units and
179        // requested_loaded_account_data_size
180        let loaded_accounts_data_size_cost = if include_loaded_account_data_size_in_fee {
181            FeeStructure::calculate_memory_usage_cost(
182                budget_limits.loaded_accounts_data_size_limit.get(),
183                budget_limits.heap_cost,
184            )
185        } else {
186            0_u64
187        };
188        let total_compute_units =
189            loaded_accounts_data_size_cost.saturating_add(budget_limits.compute_unit_limit);
190        let compute_fee = self
191            .compute_fee_bins
192            .iter()
193            .find(|bin| total_compute_units <= bin.limit)
194            .map(|bin| bin.fee)
195            .unwrap_or_else(|| {
196                self.compute_fee_bins
197                    .last()
198                    .map(|bin| bin.fee)
199                    .unwrap_or_default()
200            });
201
202        FeeDetails {
203            transaction_fee: signature_fee
204                .saturating_add(write_lock_fee)
205                .saturating_add(compute_fee),
206            prioritization_fee: budget_limits.prioritization_fee,
207            remove_rounding_in_fee_calculation,
208        }
209    }
210}
211
212impl Default for FeeStructure {
213    fn default() -> Self {
214        Self::new(0.000005, 0.0, vec![(1_400_000, 0.0)])
215    }
216}
217
218#[cfg(feature = "frozen-abi")]
219impl ::solana_frozen_abi::abi_example::AbiExample for FeeStructure {
220    fn example() -> Self {
221        FeeStructure::default()
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_calculate_memory_usage_cost() {
231        let heap_cost = 99;
232        const K: u32 = 1024;
233
234        // accounts data size are priced in block of 32K, ...
235
236        // ... requesting less than 32K should still be charged as one block
237        assert_eq!(
238            heap_cost,
239            FeeStructure::calculate_memory_usage_cost(31 * K, heap_cost)
240        );
241
242        // ... requesting exact 32K should be charged as one block
243        assert_eq!(
244            heap_cost,
245            FeeStructure::calculate_memory_usage_cost(32 * K, heap_cost)
246        );
247
248        // ... requesting slightly above 32K should be charged as 2 block
249        assert_eq!(
250            heap_cost * 2,
251            FeeStructure::calculate_memory_usage_cost(33 * K, heap_cost)
252        );
253
254        // ... requesting exact 64K should be charged as 2 block
255        assert_eq!(
256            heap_cost * 2,
257            FeeStructure::calculate_memory_usage_cost(64 * K, heap_cost)
258        );
259    }
260
261    #[test]
262    fn test_total_fee_rounding() {
263        // round large `f64` can lost precision, see feature gate:
264        // "Removing unwanted rounding in fee calculation #34982"
265
266        let transaction_fee = u64::MAX - 11;
267        let prioritization_fee = 1;
268        let expected_large_fee = u64::MAX - 10;
269
270        let details_with_rounding = FeeDetails {
271            transaction_fee,
272            prioritization_fee,
273            remove_rounding_in_fee_calculation: false,
274        };
275        let details_without_rounding = FeeDetails {
276            transaction_fee,
277            prioritization_fee,
278            remove_rounding_in_fee_calculation: true,
279        };
280
281        assert_eq!(details_without_rounding.total_fee(), expected_large_fee);
282        assert_ne!(details_with_rounding.total_fee(), expected_large_fee);
283    }
284}