Skip to main content

dpp/address_funds/fee_strategy/
mod.rs

1pub mod deduct_fee_from_inputs_and_outputs;
2
3pub use deduct_fee_from_inputs_and_outputs::FeeDeductionResult;
4
5#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
6use crate::serialization::JsonConvertible;
7#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
8use crate::serialization::ValueConvertible;
9use bincode::{Decode, DecodeUntrusted, Encode};
10#[cfg(feature = "serde-conversion")]
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq, Hash, DecodeUntrusted)]
14pub enum AddressFundsFeeStrategyStep {
15    /// Deduct fee from a specific input address by index.
16    /// The input must have remaining balance after its contribution to outputs.
17    DeductFromInput(u16),
18    /// Reduce a specific output by the fee amount.
19    /// The output amount will be reduced to cover the fee.
20    ReduceOutput(u16),
21}
22
23impl Default for AddressFundsFeeStrategyStep {
24    fn default() -> Self {
25        AddressFundsFeeStrategyStep::DeductFromInput(0)
26    }
27}
28
29pub type AddressFundsFeeStrategy = Vec<AddressFundsFeeStrategyStep>;
30
31// Custom serde impls so JSON / wasm Object output uses the standard
32// `{ "$type": "...", "index": N }` discriminator shape used elsewhere in
33// the DPP wasm bindings. The bincode `Encode` / `Decode` derives above are
34// the consensus-critical binary format and are intentionally untouched.
35#[cfg(feature = "serde-conversion")]
36impl Serialize for AddressFundsFeeStrategyStep {
37    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
38    where
39        S: serde::Serializer,
40    {
41        use serde::ser::SerializeStruct;
42
43        let mut state = serializer.serialize_struct("AddressFundsFeeStrategyStep", 2)?;
44        match self {
45            AddressFundsFeeStrategyStep::DeductFromInput(index) => {
46                state.serialize_field("$type", "deductFromInput")?;
47                state.serialize_field("index", index)?;
48            }
49            AddressFundsFeeStrategyStep::ReduceOutput(index) => {
50                state.serialize_field("$type", "reduceOutput")?;
51                state.serialize_field("index", index)?;
52            }
53        }
54        state.end()
55    }
56}
57
58#[cfg(feature = "serde-conversion")]
59impl<'de> Deserialize<'de> for AddressFundsFeeStrategyStep {
60    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
61    where
62        D: serde::Deserializer<'de>,
63    {
64        use serde::de::{self, MapAccess, Visitor};
65        use std::fmt;
66
67        struct StepVisitor;
68
69        impl<'de> Visitor<'de> for StepVisitor {
70            type Value = AddressFundsFeeStrategyStep;
71
72            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
73                formatter.write_str("an AddressFundsFeeStrategyStep struct with type and index")
74            }
75
76            fn visit_map<V>(self, mut map: V) -> Result<AddressFundsFeeStrategyStep, V::Error>
77            where
78                V: MapAccess<'de>,
79            {
80                let mut step_type: Option<String> = None;
81                let mut index: Option<u16> = None;
82
83                while let Some(key) = map.next_key::<String>()? {
84                    match key.as_str() {
85                        "$type" => {
86                            if step_type.is_some() {
87                                return Err(de::Error::duplicate_field("$type"));
88                            }
89                            step_type = Some(map.next_value()?);
90                        }
91                        "index" => {
92                            if index.is_some() {
93                                return Err(de::Error::duplicate_field("index"));
94                            }
95                            index = Some(map.next_value()?);
96                        }
97                        _ => {
98                            let _: serde::de::IgnoredAny = map.next_value()?;
99                        }
100                    }
101                }
102
103                let step_type = step_type.ok_or_else(|| de::Error::missing_field("$type"))?;
104                let index = index.ok_or_else(|| de::Error::missing_field("index"))?;
105
106                match step_type.as_str() {
107                    "deductFromInput" => Ok(AddressFundsFeeStrategyStep::DeductFromInput(index)),
108                    "reduceOutput" => Ok(AddressFundsFeeStrategyStep::ReduceOutput(index)),
109                    other => Err(de::Error::unknown_variant(
110                        other,
111                        &["deductFromInput", "reduceOutput"],
112                    )),
113                }
114            }
115        }
116
117        deserializer.deserialize_struct(
118            "AddressFundsFeeStrategyStep",
119            &["$type", "index"],
120            StepVisitor,
121        )
122    }
123}
124
125#[cfg(all(test, feature = "serde-conversion"))]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn deduct_from_input_serializes_with_type_and_index() {
131        let step = AddressFundsFeeStrategyStep::DeductFromInput(7);
132        let json = serde_json::to_value(&step).unwrap();
133        assert_eq!(
134            json,
135            serde_json::json!({ "$type": "deductFromInput", "index": 7 })
136        );
137    }
138
139    #[test]
140    fn reduce_output_serializes_with_type_and_index() {
141        let step = AddressFundsFeeStrategyStep::ReduceOutput(3);
142        let json = serde_json::to_value(&step).unwrap();
143        assert_eq!(
144            json,
145            serde_json::json!({ "$type": "reduceOutput", "index": 3 })
146        );
147    }
148
149    #[test]
150    fn deserializes_from_type_and_index() {
151        let step: AddressFundsFeeStrategyStep =
152            serde_json::from_value(serde_json::json!({ "$type": "deductFromInput", "index": 9 }))
153                .unwrap();
154        assert_eq!(step, AddressFundsFeeStrategyStep::DeductFromInput(9));
155
156        let step: AddressFundsFeeStrategyStep =
157            serde_json::from_value(serde_json::json!({ "$type": "reduceOutput", "index": 2 }))
158                .unwrap();
159        assert_eq!(step, AddressFundsFeeStrategyStep::ReduceOutput(2));
160    }
161
162    #[test]
163    fn rejects_unknown_variant() {
164        let result: Result<AddressFundsFeeStrategyStep, _> =
165            serde_json::from_value(serde_json::json!({ "$type": "burn", "index": 0 }));
166        assert!(result.is_err());
167    }
168
169    #[test]
170    fn round_trips_through_json() {
171        for original in [
172            AddressFundsFeeStrategyStep::DeductFromInput(0),
173            AddressFundsFeeStrategyStep::DeductFromInput(42),
174            AddressFundsFeeStrategyStep::ReduceOutput(0),
175            AddressFundsFeeStrategyStep::ReduceOutput(42),
176        ] {
177            let json = serde_json::to_string(&original).unwrap();
178            let restored: AddressFundsFeeStrategyStep = serde_json::from_str(&json).unwrap();
179            assert_eq!(original, restored);
180        }
181    }
182}
183
184#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
185impl JsonConvertible for AddressFundsFeeStrategyStep {}
186
187#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
188impl ValueConvertible for AddressFundsFeeStrategyStep {}
189
190#[cfg(all(
191    test,
192    feature = "json-conversion",
193    feature = "value-conversion",
194    feature = "serde-conversion"
195))]
196mod json_convertible_tests_address_funds_fee_strategy_step {
197    use super::*;
198
199    use platform_value::platform_value;
200    use serde_json::json;
201
202    #[test]
203    fn json_round_trip_deduct_from_input() {
204        use crate::serialization::JsonConvertible;
205        let original = AddressFundsFeeStrategyStep::DeductFromInput(7);
206        let json = original.to_json().expect("to_json");
207        // `index` is a `u16` in the source type. JSON has only one number
208        // type, so the wire shape erases the U16 distinction (the value-path
209        // assertion below uses `7u16` explicitly to lock in the typed variant).
210        assert_eq!(json, json!({"$type": "deductFromInput", "index": 7}));
211        let recovered = AddressFundsFeeStrategyStep::from_json(json).expect("from_json");
212        assert_eq!(recovered, AddressFundsFeeStrategyStep::DeductFromInput(7));
213    }
214
215    #[test]
216    fn json_round_trip_reduce_output() {
217        use crate::serialization::JsonConvertible;
218        let original = AddressFundsFeeStrategyStep::ReduceOutput(u16::MAX);
219        let json = original.to_json().expect("to_json");
220        // `index` is a `u16`; JSON erases the size — see the deduct test above.
221        assert_eq!(json, json!({"$type": "reduceOutput", "index": u16::MAX}));
222        let recovered = AddressFundsFeeStrategyStep::from_json(json).expect("from_json");
223        assert_eq!(
224            recovered,
225            AddressFundsFeeStrategyStep::ReduceOutput(u16::MAX)
226        );
227    }
228
229    #[test]
230    fn value_round_trip_deduct_from_input() {
231        use crate::serialization::ValueConvertible;
232        let original = AddressFundsFeeStrategyStep::DeductFromInput(7);
233        let value = original.to_object().expect("to_object");
234        // `7u16`: explicit suffix forces `Value::U16` in the expected, matching
235        // the field's actual u16 type. A bare `7` would expand via
236        // `to_value(&7i32)` and produce `Value::I32`, which would fail — that
237        // distinction is exactly what JSON can't preserve but `platform_value`
238        // does, and what we want this test to lock in.
239        assert_eq!(
240            value,
241            platform_value!({"$type": "deductFromInput", "index": 7u16})
242        );
243        let recovered = AddressFundsFeeStrategyStep::from_object(value).expect("from_object");
244        assert_eq!(recovered, AddressFundsFeeStrategyStep::DeductFromInput(7));
245    }
246
247    #[test]
248    fn value_round_trip_reduce_output() {
249        use crate::serialization::ValueConvertible;
250        let original = AddressFundsFeeStrategyStep::ReduceOutput(u16::MAX);
251        let value = original.to_object().expect("to_object");
252        assert_eq!(
253            value,
254            platform_value!({"$type": "reduceOutput", "index": u16::MAX})
255        );
256        let recovered = AddressFundsFeeStrategyStep::from_object(value).expect("from_object");
257        assert_eq!(
258            recovered,
259            AddressFundsFeeStrategyStep::ReduceOutput(u16::MAX)
260        );
261    }
262}