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