Skip to main content

dpp/tokens/
gas_fees_paid_by.rs

1use crate::consensus::basic::data_contract::UnknownGasFeesPaidByError;
2use crate::consensus::basic::BasicError;
3use crate::consensus::ConsensusError;
4#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
5use crate::serialization::JsonConvertible;
6#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
7use crate::serialization::ValueConvertible;
8use crate::ProtocolError;
9use bincode::{Decode, DecodeUntrusted, Encode};
10use derive_more::Display;
11#[cfg(feature = "serde-conversion")]
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Copy, Encode, Decode, Default, PartialEq, Display, DecodeUntrusted)]
15#[cfg_attr(feature = "serde-conversion", derive(Serialize, Deserialize))]
16/// Who pays the gas (the storage and processing fee) of a document action that is paid for
17/// with a token.
18///
19/// The same enum is used on both sides of a token payment. On a document type's token cost it
20/// is what the contract owner offers; on a transition's token payment info it is what the
21/// document owner asks for. [`GasFeesPaidBy::resolve`] combines the two into the effective payer
22/// from protocol version 14 on: before it the field was carried but never acted on, and the
23/// signer always paid.
24///
25/// The contract owner can only be asked to pay when the action actually charges a token, so
26/// every sponsored transition is backed by a token the contract owner chose to hand out.
27pub enum GasFeesPaidBy {
28    /// The document owner pays the gas fees.
29    ///
30    /// On the contract side this is the default and means the contract owner never pays. On the
31    /// transition side it opts out of any sponsorship the contract offers.
32    #[default]
33    DocumentOwner = 0,
34    /// The contract owner pays the gas fees.
35    ///
36    /// On the contract side this offers to pay, and accepts every request. On the transition
37    /// side this insists that the contract owner pays: the transition is refused, and nobody is
38    /// charged, when the contract owner cannot cover the fee, and it is rejected outright when
39    /// the contract offers less than that.
40    ContractOwner = 1,
41    /// The contract owner pays the gas fees when their balance covers them; otherwise the
42    /// document owner does.
43    ///
44    /// On the contract side this offers to pay but refuses to be the reason a transition fails,
45    /// so a transition insisting on `ContractOwner` is rejected. On the transition side it is
46    /// the document owner stating their willingness to pay the fee themselves when the contract
47    /// owner's balance is insufficient, and it is accepted by every contract.
48    PreferContractOwner = 2,
49}
50
51impl GasFeesPaidBy {
52    /// The effective gas payer of a document action, given what the document type's token
53    /// cost offers (`offered_by_contract`) and what the transition's token payment info asks
54    /// for (`requested`), or `None` when the request cannot be honoured.
55    ///
56    /// | offered \ requested   | `DocumentOwner` | `PreferContractOwner` | `ContractOwner` |
57    /// |-----------------------|-----------------|-----------------------|-----------------|
58    /// | `DocumentOwner`       | document owner  | document owner        | refused         |
59    /// | `PreferContractOwner` | document owner  | prefer contract owner | refused         |
60    /// | `ContractOwner`       | document owner  | prefer contract owner | contract owner  |
61    ///
62    /// An action without a token cost offers `DocumentOwner`, and a transition without token
63    /// payment info requests it.
64    pub fn resolve(offered_by_contract: Self, requested: Self) -> Option<Self> {
65        match (offered_by_contract, requested) {
66            (_, GasFeesPaidBy::DocumentOwner) => Some(GasFeesPaidBy::DocumentOwner),
67            (GasFeesPaidBy::DocumentOwner, GasFeesPaidBy::PreferContractOwner) => {
68                Some(GasFeesPaidBy::DocumentOwner)
69            }
70            (GasFeesPaidBy::DocumentOwner, GasFeesPaidBy::ContractOwner) => None,
71            (GasFeesPaidBy::PreferContractOwner, GasFeesPaidBy::PreferContractOwner) => {
72                Some(GasFeesPaidBy::PreferContractOwner)
73            }
74            (GasFeesPaidBy::PreferContractOwner, GasFeesPaidBy::ContractOwner) => None,
75            (GasFeesPaidBy::ContractOwner, GasFeesPaidBy::PreferContractOwner) => {
76                Some(GasFeesPaidBy::PreferContractOwner)
77            }
78            (GasFeesPaidBy::ContractOwner, GasFeesPaidBy::ContractOwner) => {
79                Some(GasFeesPaidBy::ContractOwner)
80            }
81        }
82    }
83}
84
85#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
86impl JsonConvertible for GasFeesPaidBy {}
87
88#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
89impl ValueConvertible for GasFeesPaidBy {}
90
91impl From<GasFeesPaidBy> for u8 {
92    fn from(value: GasFeesPaidBy) -> Self {
93        match value {
94            GasFeesPaidBy::DocumentOwner => 0,
95            GasFeesPaidBy::ContractOwner => 1,
96            GasFeesPaidBy::PreferContractOwner => 2,
97        }
98    }
99}
100
101impl TryFrom<u8> for GasFeesPaidBy {
102    type Error = ProtocolError;
103
104    fn try_from(value: u8) -> Result<Self, Self::Error> {
105        match value {
106            0 => Ok(GasFeesPaidBy::DocumentOwner),
107            1 => Ok(GasFeesPaidBy::ContractOwner),
108            2 => Ok(GasFeesPaidBy::PreferContractOwner),
109            value => Err(ProtocolError::ConsensusError(
110                ConsensusError::BasicError(BasicError::UnknownGasFeesPaidByError(
111                    UnknownGasFeesPaidByError::new(vec![0, 1, 2], value as u64),
112                ))
113                .into(),
114            )),
115        }
116    }
117}
118
119impl TryFrom<u64> for GasFeesPaidBy {
120    type Error = ProtocolError;
121
122    fn try_from(value: u64) -> Result<Self, Self::Error> {
123        u8::try_from(value)
124            .map_err(|_| {
125                ProtocolError::ConsensusError(
126                    ConsensusError::BasicError(BasicError::UnknownGasFeesPaidByError(
127                        UnknownGasFeesPaidByError::new(vec![0, 1, 2], value),
128                    ))
129                    .into(),
130                )
131            })?
132            .try_into()
133    }
134}
135
136#[cfg(test)]
137mod resolve_tests {
138    use super::GasFeesPaidBy::{ContractOwner, DocumentOwner, PreferContractOwner};
139    use super::*;
140
141    #[test]
142    fn should_let_the_document_owner_opt_out_of_any_offer() {
143        for offered in [DocumentOwner, PreferContractOwner, ContractOwner] {
144            assert_eq!(
145                GasFeesPaidBy::resolve(offered, DocumentOwner),
146                Some(DocumentOwner)
147            );
148        }
149    }
150
151    #[test]
152    fn should_accept_a_preference_from_every_contract() {
153        assert_eq!(
154            GasFeesPaidBy::resolve(DocumentOwner, PreferContractOwner),
155            Some(DocumentOwner)
156        );
157        assert_eq!(
158            GasFeesPaidBy::resolve(PreferContractOwner, PreferContractOwner),
159            Some(PreferContractOwner)
160        );
161        assert_eq!(
162            GasFeesPaidBy::resolve(ContractOwner, PreferContractOwner),
163            Some(PreferContractOwner)
164        );
165    }
166
167    #[test]
168    fn should_honour_an_insistence_only_when_the_contract_commits_to_paying() {
169        assert_eq!(GasFeesPaidBy::resolve(DocumentOwner, ContractOwner), None);
170        assert_eq!(
171            GasFeesPaidBy::resolve(PreferContractOwner, ContractOwner),
172            None
173        );
174        assert_eq!(
175            GasFeesPaidBy::resolve(ContractOwner, ContractOwner),
176            Some(ContractOwner)
177        );
178    }
179}
180
181#[cfg(all(
182    test,
183    feature = "json-conversion",
184    feature = "value-conversion",
185    feature = "serde-conversion"
186))]
187mod json_convertible_tests {
188    use super::*;
189    use platform_value::platform_value;
190    use serde_json::json;
191
192    // `GasFeesPaidBy` is a unit-only enum without `rename_all`, so each variant
193    // (de)serializes as its PascalCase Rust name in both JSON and platform_value.
194
195    #[test]
196    fn json_round_trip_document_owner() {
197        use crate::serialization::JsonConvertible;
198        let original = GasFeesPaidBy::DocumentOwner;
199        let json = original.to_json().expect("to_json");
200        assert_eq!(json, json!("DocumentOwner"));
201        let recovered = GasFeesPaidBy::from_json(json).expect("from_json");
202        assert_eq!(original, recovered);
203    }
204
205    #[test]
206    fn json_round_trip_contract_owner() {
207        use crate::serialization::JsonConvertible;
208        let original = GasFeesPaidBy::ContractOwner;
209        let json = original.to_json().expect("to_json");
210        assert_eq!(json, json!("ContractOwner"));
211        let recovered = GasFeesPaidBy::from_json(json).expect("from_json");
212        assert_eq!(original, recovered);
213    }
214
215    #[test]
216    fn json_round_trip_prefer_contract_owner() {
217        use crate::serialization::JsonConvertible;
218        let original = GasFeesPaidBy::PreferContractOwner;
219        let json = original.to_json().expect("to_json");
220        assert_eq!(json, json!("PreferContractOwner"));
221        let recovered = GasFeesPaidBy::from_json(json).expect("from_json");
222        assert_eq!(original, recovered);
223    }
224
225    #[test]
226    fn value_round_trip_document_owner() {
227        use crate::serialization::ValueConvertible;
228        let original = GasFeesPaidBy::DocumentOwner;
229        let value = original.to_object().expect("to_object");
230        assert_eq!(value, platform_value!("DocumentOwner"));
231        let recovered = GasFeesPaidBy::from_object(value).expect("from_object");
232        assert_eq!(original, recovered);
233    }
234
235    #[test]
236    fn value_round_trip_contract_owner() {
237        use crate::serialization::ValueConvertible;
238        let original = GasFeesPaidBy::ContractOwner;
239        let value = original.to_object().expect("to_object");
240        assert_eq!(value, platform_value!("ContractOwner"));
241        let recovered = GasFeesPaidBy::from_object(value).expect("from_object");
242        assert_eq!(original, recovered);
243    }
244
245    #[test]
246    fn value_round_trip_prefer_contract_owner() {
247        use crate::serialization::ValueConvertible;
248        let original = GasFeesPaidBy::PreferContractOwner;
249        let value = original.to_object().expect("to_object");
250        assert_eq!(value, platform_value!("PreferContractOwner"));
251        let recovered = GasFeesPaidBy::from_object(value).expect("from_object");
252        assert_eq!(original, recovered);
253    }
254}