Skip to main content

dpp/withdrawal/
mod.rs

1pub mod daily_withdrawal_limit;
2#[cfg(all(feature = "withdrawals-contract", feature = "system_contracts"))]
3mod document_try_into_asset_unlock_base_transaction_info;
4
5use bincode::{Decode, Encode};
6use serde_repr::{Deserialize_repr, Serialize_repr};
7
8#[cfg(feature = "json-conversion")]
9use crate::serialization::JsonConvertible;
10#[cfg(feature = "value-conversion")]
11use crate::serialization::ValueConvertible;
12
13#[repr(u8)]
14#[derive(
15    Serialize_repr, Deserialize_repr, PartialEq, Eq, Clone, Copy, Debug, Encode, Decode, Default,
16)]
17pub enum Pooling {
18    #[default]
19    Never = 0,
20    IfAvailable = 1,
21    Standard = 2,
22}
23
24#[cfg(feature = "json-conversion")]
25impl JsonConvertible for Pooling {}
26
27#[cfg(feature = "value-conversion")]
28impl ValueConvertible for Pooling {}
29
30/// Transaction index type
31pub type WithdrawalTransactionIndex = u64;
32
33/// Simple type alias for withdrawal transaction with it's index
34pub type WithdrawalTransactionIndexAndBytes = (WithdrawalTransactionIndex, Vec<u8>);
35
36/// Serde helper for `Pooling` fields exposed through the JS surface.
37///
38/// `Pooling` is `#[repr(u8)]` with `Serialize_repr` / `Deserialize_repr`, so the
39/// default wire shape is the numeric discriminant (`0`/`1`/`2`). That number
40/// leaks into JSON / Object output and makes `XxxJSON.pooling: string`
41/// declarations false. The helper switches the **human-readable** path to a
42/// camelCase string (`"never"`/`"ifAvailable"`/`"standard"`) while keeping the
43/// non-HR path at the original `u8` so bincode (consensus binary format) is
44/// untouched.
45///
46/// Apply via `#[serde(with = "crate::withdrawal::pooling_serde")]` on the
47/// `pooling` field of any state transition that surfaces it to JS.
48#[cfg(feature = "serde-conversion")]
49pub mod pooling_serde {
50    use super::Pooling;
51    use serde::{Deserializer, Serialize, Serializer};
52
53    pub fn serialize<S: Serializer>(pooling: &Pooling, serializer: S) -> Result<S::Ok, S::Error> {
54        if serializer.is_human_readable() {
55            let name = match pooling {
56                Pooling::Never => "never",
57                Pooling::IfAvailable => "ifAvailable",
58                Pooling::Standard => "standard",
59            };
60            serializer.serialize_str(name)
61        } else {
62            (*pooling as u8).serialize(serializer)
63        }
64    }
65
66    /// Deserialize accepts both shapes regardless of the deserializer's
67    /// human-readable flag — mirrors the `BinaryData` / `Identifier` pattern.
68    /// Necessary because `platform_value::to_value` reports HR=false (emits the
69    /// numeric discriminant on the way to `JsValue`), but
70    /// `platform_value::from_value` reports HR=true on the way back. Without
71    /// dual acceptance, the `fromObject(toObject())` round-trip fails on the
72    /// `pooling` field.
73    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Pooling, D::Error> {
74        struct PoolingVisitor;
75
76        impl<'de> serde::de::Visitor<'de> for PoolingVisitor {
77            type Value = Pooling;
78
79            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
80                f.write_str("a Pooling variant: 'never'/'ifAvailable'/'standard' or 0/1/2")
81            }
82
83            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Pooling, E> {
84                match v {
85                    "never" | "Never" => Ok(Pooling::Never),
86                    "ifAvailable" | "IfAvailable" | "ifavailable" => Ok(Pooling::IfAvailable),
87                    "standard" | "Standard" => Ok(Pooling::Standard),
88                    other => Err(E::custom(format!(
89                        "unknown pooling variant '{}', expected 'never' | 'ifAvailable' | 'standard'",
90                        other
91                    ))),
92                }
93            }
94
95            fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Pooling, E> {
96                self.visit_str(&v)
97            }
98
99            fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Pooling, E> {
100                match v {
101                    0 => Ok(Pooling::Never),
102                    1 => Ok(Pooling::IfAvailable),
103                    2 => Ok(Pooling::Standard),
104                    other => Err(E::custom(format!("unknown pooling discriminant {}", other))),
105                }
106            }
107
108            fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Pooling, E> {
109                if v < 0 {
110                    return Err(E::custom(format!("negative pooling discriminant {}", v)));
111                }
112                self.visit_u64(v as u64)
113            }
114
115            fn visit_u8<E: serde::de::Error>(self, v: u8) -> Result<Pooling, E> {
116                self.visit_u64(v as u64)
117            }
118        }
119
120        if deserializer.is_human_readable() {
121            deserializer.deserialize_any(PoolingVisitor)
122        } else {
123            deserializer.deserialize_u8(PoolingVisitor)
124        }
125    }
126
127    #[cfg(test)]
128    mod tests {
129        use super::*;
130        use serde::{Deserialize, Serialize};
131
132        #[derive(Serialize, Deserialize, PartialEq, Debug)]
133        struct Wrap(#[serde(with = "super")] Pooling);
134
135        #[test]
136        fn json_emits_camelcase_string() {
137            for (variant, expected) in [
138                (Pooling::Never, "\"never\""),
139                (Pooling::IfAvailable, "\"ifAvailable\""),
140                (Pooling::Standard, "\"standard\""),
141            ] {
142                let json = serde_json::to_string(&Wrap(variant)).expect("serialize");
143                assert_eq!(json, expected);
144                let restored: Wrap = serde_json::from_str(expected).expect("deserialize");
145                assert_eq!(restored, Wrap(variant));
146            }
147        }
148
149        #[test]
150        fn bincode_keeps_u8_discriminant() {
151            for (variant, expected_u8) in [
152                (Pooling::Never, 0),
153                (Pooling::IfAvailable, 1),
154                (Pooling::Standard, 2),
155            ] {
156                let bytes =
157                    bincode::serde::encode_to_vec(Wrap(variant), bincode::config::standard())
158                        .expect("bincode encode");
159                assert_eq!(bytes.last(), Some(&expected_u8));
160                let (restored, _): (Wrap, usize) =
161                    bincode::serde::decode_from_slice(&bytes, bincode::config::standard())
162                        .expect("bincode decode");
163                assert_eq!(restored, Wrap(variant));
164            }
165        }
166    }
167}
168
169#[cfg(all(
170    test,
171    feature = "json-conversion",
172    feature = "value-conversion",
173    feature = "serde-conversion"
174))]
175mod json_convertible_tests_pooling {
176    use super::*;
177    use platform_value::platform_value;
178    use serde_json::json;
179
180    // `Pooling` is `#[repr(u8)]` with `Serialize_repr` / `Deserialize_repr`, so
181    // the wire shape is the raw `u8` discriminant: `0` / `1` / `2`. JSON has
182    // only one number type, so `0u8` is erased to `Number(0)`; the value-path
183    // assertion uses explicit `0u8` etc. to lock in `Value::U8`.
184
185    #[test]
186    fn json_round_trip_never() {
187        use crate::serialization::JsonConvertible;
188        let original = Pooling::Never;
189        let json = original.to_json().expect("to_json");
190        // u8 size erased in JSON.
191        assert_eq!(json, json!(0));
192        let recovered = Pooling::from_json(json).expect("from_json");
193        assert_eq!(original, recovered);
194    }
195
196    #[test]
197    fn json_round_trip_if_available() {
198        use crate::serialization::JsonConvertible;
199        let original = Pooling::IfAvailable;
200        let json = original.to_json().expect("to_json");
201        assert_eq!(json, json!(1));
202        let recovered = Pooling::from_json(json).expect("from_json");
203        assert_eq!(original, recovered);
204    }
205
206    #[test]
207    fn json_round_trip_standard() {
208        use crate::serialization::JsonConvertible;
209        let original = Pooling::Standard;
210        let json = original.to_json().expect("to_json");
211        assert_eq!(json, json!(2));
212        let recovered = Pooling::from_json(json).expect("from_json");
213        assert_eq!(original, recovered);
214    }
215
216    #[test]
217    fn value_round_trip_never() {
218        use crate::serialization::ValueConvertible;
219        let original = Pooling::Never;
220        let value = original.to_object().expect("to_object");
221        // `0u8` locks `Value::U8` (not I32 from a bare `0`).
222        assert_eq!(value, platform_value!(0u8));
223        let recovered = Pooling::from_object(value).expect("from_object");
224        assert_eq!(original, recovered);
225    }
226
227    #[test]
228    fn value_round_trip_if_available() {
229        use crate::serialization::ValueConvertible;
230        let original = Pooling::IfAvailable;
231        let value = original.to_object().expect("to_object");
232        assert_eq!(value, platform_value!(1u8));
233        let recovered = Pooling::from_object(value).expect("from_object");
234        assert_eq!(original, recovered);
235    }
236
237    #[test]
238    fn value_round_trip_standard() {
239        use crate::serialization::ValueConvertible;
240        let original = Pooling::Standard;
241        let value = original.to_object().expect("to_object");
242        assert_eq!(value, platform_value!(2u8));
243        let recovered = Pooling::from_object(value).expect("from_object");
244        assert_eq!(original, recovered);
245    }
246}