1pub mod core_credit_pool_unlock_limit;
2mod core_dust_threshold;
3pub mod daily_withdrawal_limit;
4#[cfg(all(feature = "withdrawals-contract", feature = "system_contracts"))]
5mod document_try_into_asset_unlock_base_transaction_info;
6
7pub use core_dust_threshold::core_dust_threshold_duffs;
8
9use bincode::{Decode, DecodeUntrusted, Encode};
10use serde_repr::{Deserialize_repr, Serialize_repr};
11
12use crate::balances::credits::CREDITS_PER_DUFF;
13#[cfg(feature = "state-transitions")]
14use crate::consensus::basic::identity::InvalidCreditWithdrawalTransitionCoreFeeError;
15use crate::fee::Credits;
16#[cfg(feature = "state-transitions")]
17use crate::state_transition::identity_credit_withdrawal_transition::MIN_CORE_FEE_PER_BYTE;
18#[cfg(feature = "state-transitions")]
19use crate::validation::SimpleConsensusValidationResult;
20use dashcore::transaction::special_transaction::asset_unlock::qualified_asset_unlock::ASSET_UNLOCK_TX_SIZE;
21use platform_version::version::PlatformVersion;
22
23#[cfg(feature = "json-conversion")]
24use crate::serialization::JsonConvertible;
25#[cfg(feature = "value-conversion")]
26use crate::serialization::ValueConvertible;
27
28#[repr(u8)]
29#[derive(
30 Serialize_repr,
31 Deserialize_repr,
32 PartialEq,
33 Eq,
34 Clone,
35 Copy,
36 Debug,
37 Encode,
38 Decode,
39 Default,
40 DecodeUntrusted,
41)]
42pub enum Pooling {
43 #[default]
44 Never = 0,
45 IfAvailable = 1,
46 Standard = 2,
47}
48
49#[cfg(feature = "json-conversion")]
50impl JsonConvertible for Pooling {}
51
52#[cfg(feature = "value-conversion")]
53impl ValueConvertible for Pooling {}
54
55pub type WithdrawalTransactionIndex = u64;
57
58pub type WithdrawalTransactionIndexAndBytes = (WithdrawalTransactionIndex, Vec<u8>);
60
61pub fn core_fee_in_credits(core_fee_per_byte: u32) -> Option<Credits> {
63 (ASSET_UNLOCK_TX_SIZE as u64)
64 .checked_mul(core_fee_per_byte as u64)?
65 .checked_mul(CREDITS_PER_DUFF)
66}
67
68#[cfg(feature = "state-transitions")]
75pub fn validate_core_fee_per_byte_cap(
76 core_fee_per_byte: u32,
77 platform_version: &PlatformVersion,
78) -> SimpleConsensusValidationResult {
79 match platform_version.system_limits.max_core_fee_per_byte {
80 Some(max_core_fee_per_byte) if core_fee_per_byte > max_core_fee_per_byte => {
81 SimpleConsensusValidationResult::new_with_error(
82 InvalidCreditWithdrawalTransitionCoreFeeError::new(
83 core_fee_per_byte,
84 MIN_CORE_FEE_PER_BYTE,
85 )
86 .into(),
87 )
88 }
89 _ => SimpleConsensusValidationResult::new(),
90 }
91}
92
93pub fn min_withdrawal_amount_with_core_fee(
103 core_fee_per_byte: u32,
104 platform_version: &PlatformVersion,
105) -> Credits {
106 let core_fee = core_fee_in_credits(core_fee_per_byte).unwrap_or(Credits::MAX);
107 platform_version
108 .system_limits
109 .min_withdrawal_amount
110 .saturating_add(core_fee)
111}
112
113#[cfg(feature = "serde-conversion")]
126pub mod pooling_serde {
127 use super::Pooling;
128 use serde::{Deserializer, Serialize, Serializer};
129
130 pub fn serialize<S: Serializer>(pooling: &Pooling, serializer: S) -> Result<S::Ok, S::Error> {
131 if serializer.is_human_readable() {
132 let name = match pooling {
133 Pooling::Never => "never",
134 Pooling::IfAvailable => "ifAvailable",
135 Pooling::Standard => "standard",
136 };
137 serializer.serialize_str(name)
138 } else {
139 (*pooling as u8).serialize(serializer)
140 }
141 }
142
143 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Pooling, D::Error> {
151 struct PoolingVisitor;
152
153 impl<'de> serde::de::Visitor<'de> for PoolingVisitor {
154 type Value = Pooling;
155
156 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
157 f.write_str("a Pooling variant: 'never'/'ifAvailable'/'standard' or 0/1/2")
158 }
159
160 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Pooling, E> {
161 match v {
162 "never" | "Never" => Ok(Pooling::Never),
163 "ifAvailable" | "IfAvailable" | "ifavailable" => Ok(Pooling::IfAvailable),
164 "standard" | "Standard" => Ok(Pooling::Standard),
165 other => Err(E::custom(format!(
166 "unknown pooling variant '{}', expected 'never' | 'ifAvailable' | 'standard'",
167 other
168 ))),
169 }
170 }
171
172 fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Pooling, E> {
173 self.visit_str(&v)
174 }
175
176 fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Pooling, E> {
177 match v {
178 0 => Ok(Pooling::Never),
179 1 => Ok(Pooling::IfAvailable),
180 2 => Ok(Pooling::Standard),
181 other => Err(E::custom(format!("unknown pooling discriminant {}", other))),
182 }
183 }
184
185 fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Pooling, E> {
186 if v < 0 {
187 return Err(E::custom(format!("negative pooling discriminant {}", v)));
188 }
189 self.visit_u64(v as u64)
190 }
191
192 fn visit_u8<E: serde::de::Error>(self, v: u8) -> Result<Pooling, E> {
193 self.visit_u64(v as u64)
194 }
195 }
196
197 if deserializer.is_human_readable() {
198 deserializer.deserialize_any(PoolingVisitor)
199 } else {
200 deserializer.deserialize_u8(PoolingVisitor)
201 }
202 }
203
204 #[cfg(test)]
205 mod tests {
206 use super::*;
207 use serde::{Deserialize, Serialize};
208
209 #[derive(Serialize, Deserialize, PartialEq, Debug)]
210 struct Wrap(#[serde(with = "super")] Pooling);
211
212 #[test]
213 fn json_emits_camelcase_string() {
214 for (variant, expected) in [
215 (Pooling::Never, "\"never\""),
216 (Pooling::IfAvailable, "\"ifAvailable\""),
217 (Pooling::Standard, "\"standard\""),
218 ] {
219 let json = serde_json::to_string(&Wrap(variant)).expect("serialize");
220 assert_eq!(json, expected);
221 let restored: Wrap = serde_json::from_str(expected).expect("deserialize");
222 assert_eq!(restored, Wrap(variant));
223 }
224 }
225
226 #[test]
227 fn bincode_keeps_u8_discriminant() {
228 for (variant, expected_u8) in [
229 (Pooling::Never, 0),
230 (Pooling::IfAvailable, 1),
231 (Pooling::Standard, 2),
232 ] {
233 let bytes =
234 bincode::serde::encode_to_vec(Wrap(variant), bincode::config::standard())
235 .expect("bincode encode");
236 assert_eq!(bytes.last(), Some(&expected_u8));
237 let (restored, _): (Wrap, usize) =
238 bincode::serde::decode_from_slice(&bytes, bincode::config::standard())
239 .expect("bincode decode");
240 assert_eq!(restored, Wrap(variant));
241 }
242 }
243 }
244}
245
246#[cfg(all(
247 test,
248 feature = "json-conversion",
249 feature = "value-conversion",
250 feature = "serde-conversion"
251))]
252mod json_convertible_tests_pooling {
253 use super::*;
254 use platform_value::platform_value;
255 use serde_json::json;
256
257 #[test]
263 fn json_round_trip_never() {
264 use crate::serialization::JsonConvertible;
265 let original = Pooling::Never;
266 let json = original.to_json().expect("to_json");
267 assert_eq!(json, json!(0));
269 let recovered = Pooling::from_json(json).expect("from_json");
270 assert_eq!(original, recovered);
271 }
272
273 #[test]
274 fn json_round_trip_if_available() {
275 use crate::serialization::JsonConvertible;
276 let original = Pooling::IfAvailable;
277 let json = original.to_json().expect("to_json");
278 assert_eq!(json, json!(1));
279 let recovered = Pooling::from_json(json).expect("from_json");
280 assert_eq!(original, recovered);
281 }
282
283 #[test]
284 fn json_round_trip_standard() {
285 use crate::serialization::JsonConvertible;
286 let original = Pooling::Standard;
287 let json = original.to_json().expect("to_json");
288 assert_eq!(json, json!(2));
289 let recovered = Pooling::from_json(json).expect("from_json");
290 assert_eq!(original, recovered);
291 }
292
293 #[test]
294 fn value_round_trip_never() {
295 use crate::serialization::ValueConvertible;
296 let original = Pooling::Never;
297 let value = original.to_object().expect("to_object");
298 assert_eq!(value, platform_value!(0u8));
300 let recovered = Pooling::from_object(value).expect("from_object");
301 assert_eq!(original, recovered);
302 }
303
304 #[test]
305 fn value_round_trip_if_available() {
306 use crate::serialization::ValueConvertible;
307 let original = Pooling::IfAvailable;
308 let value = original.to_object().expect("to_object");
309 assert_eq!(value, platform_value!(1u8));
310 let recovered = Pooling::from_object(value).expect("from_object");
311 assert_eq!(original, recovered);
312 }
313
314 #[test]
315 fn value_round_trip_standard() {
316 use crate::serialization::ValueConvertible;
317 let original = Pooling::Standard;
318 let value = original.to_object().expect("to_object");
319 assert_eq!(value, platform_value!(2u8));
320 let recovered = Pooling::from_object(value).expect("from_object");
321 assert_eq!(original, recovered);
322 }
323}
324
325#[cfg(all(test, feature = "state-transitions"))]
326mod core_fee_tests {
327 use super::*;
328 use crate::consensus::basic::BasicError;
329 use crate::consensus::ConsensusError;
330 use assert_matches::assert_matches;
331
332 #[test]
333 fn should_reject_a_core_fee_rate_above_the_cap() {
334 let platform_version = PlatformVersion::latest();
335 let cap = platform_version
336 .system_limits
337 .max_core_fee_per_byte
338 .expect("the latest protocol version caps the Core fee rate");
339
340 assert!(validate_core_fee_per_byte_cap(cap, platform_version).is_valid());
341 assert_matches!(
342 validate_core_fee_per_byte_cap(cap + 1, platform_version)
343 .errors
344 .as_slice(),
345 [ConsensusError::BasicError(
346 BasicError::InvalidCreditWithdrawalTransitionCoreFeeError(error)
347 )] if error.core_fee_per_byte() == cap + 1
348 );
349 }
350
351 #[test]
352 fn should_accept_any_core_fee_rate_without_a_cap() {
353 let platform_version = PlatformVersion::get(13).expect("protocol version 13");
355 assert_eq!(platform_version.system_limits.max_core_fee_per_byte, None);
356
357 assert!(validate_core_fee_per_byte_cap(u32::MAX, platform_version).is_valid());
358 }
359
360 #[test]
361 fn should_add_the_core_fee_to_the_withdrawal_floor() {
362 let platform_version = PlatformVersion::latest();
363 let floor = platform_version.system_limits.min_withdrawal_amount;
364
365 assert_eq!(
366 min_withdrawal_amount_with_core_fee(1, platform_version),
367 floor + ASSET_UNLOCK_TX_SIZE as u64 * CREDITS_PER_DUFF
368 );
369 assert_eq!(
371 min_withdrawal_amount_with_core_fee(u32::MAX, platform_version),
372 floor + core_fee_in_credits(u32::MAX).expect("a u32 rate fits in credits")
373 );
374 }
375}