Skip to main content

dpp/tokens/token_payment_info/
mod.rs

1//! Token payment metadata and helpers.
2//!
3//! This module defines the versioned `TokenPaymentInfo` wrapper used to describe how a
4//! client intends to pay with tokens for an operation (for example, creating,
5//! transferring, purchasing, or updating the price of a document/NFT).
6//! It captures which token to use, optional price bounds, and who covers gas fees.
7//!
8//! The enum is versioned to allow future evolution without breaking callers. The
9//! current implementation is [`v0::TokenPaymentInfoV0`]. Accessors are provided via
10//! [`v0::v0_accessors::TokenPaymentInfoAccessorsV0`], and convenience methods (such
11//! as `token_id()` and `is_valid_for_required_cost()`) are available through
12//! [`methods::v0::TokenPaymentInfoMethodsV0`].
13//!
14//! Typical usage:
15//!
16//! ```ignore
17//! use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy;
18//! use dpp::data_contract::TokenContractPosition;
19//! use dpp::tokens::token_payment_info::{TokenPaymentInfo, v0::TokenPaymentInfoV0};
20//!
21//! // Client indicates payment preferences for a transition
22//! let info: TokenPaymentInfo = TokenPaymentInfoV0 {
23//!     // `None` => use a token defined on the current contract
24//!     payment_token_contract_id: None,
25//!     // Which token (by position/index) on the contract to use
26//!     token_contract_position: 0u16,
27//!     // Optional bounds to guard against unexpected price changes
28//!     minimum_token_cost: None,
29//!     maximum_token_cost: Some(1_000u64.into()),
30//!     // Who pays gas: user, contract owner, or prefer contract owner
31//!     gas_fees_paid_by: GasFeesPaidBy::DocumentOwner,
32//! }.into();
33//! ```
34//!
35//! Deserialization from a platform `BTreeMap<String, Value>` requires a
36//! `$formatVersion` key. For V0 the map may contain:
37//! - `paymentTokenContractId` (`Identifier` as bytes)
38//! - `tokenContractPosition` (`u16`)
39//! - `minimumTokenCost` (`u64`)
40//! - `maximumTokenCost` (`u64`)
41//! - `gasFeesPaidBy` (one of: `"DocumentOwner"`, `"ContractOwner"`, `"PreferContractOwner"`)
42//!
43//! Unknown `$formatVersion` values yield an `UnknownVersionMismatch` error.
44//!
45use crate::balances::credits::TokenAmount;
46use crate::data_contract::TokenContractPosition;
47use crate::tokens::gas_fees_paid_by::GasFeesPaidBy;
48use crate::tokens::token_payment_info::methods::v0::TokenPaymentInfoMethodsV0;
49use crate::tokens::token_payment_info::v0::v0_accessors::TokenPaymentInfoAccessorsV0;
50use crate::tokens::token_payment_info::v0::TokenPaymentInfoV0;
51use crate::ProtocolError;
52use bincode::{Decode, DecodeUntrusted, Encode};
53use derive_more::{Display, From};
54use platform_serialization_derive::{
55    PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize,
56};
57use platform_value::btreemap_extensions::BTreeValueMapHelper;
58#[cfg(feature = "value-conversion")]
59use platform_value::Error;
60use platform_value::{Identifier, Value};
61#[cfg(feature = "serde-conversion")]
62use serde::{Deserialize, Serialize};
63use std::collections::BTreeMap;
64
65pub mod methods;
66pub mod v0;
67
68#[derive(
69    Debug,
70    Clone,
71    Copy,
72    Encode,
73    Decode,
74    PlatformDeserializeTrusted,
75    PlatformDeserializeUntrusted,
76    PlatformSerialize,
77    PartialEq,
78    Display,
79    From,
80    DecodeUntrusted,
81)]
82#[cfg_attr(
83    feature = "serde-conversion",
84    derive(Serialize, Deserialize),
85    serde(tag = "$formatVersion")
86)]
87/// Versioned container describing how a client intends to pay with tokens.
88///
89/// The `TokenPaymentInfo` enum allows the protocol to evolve the underlying structure
90/// across versions while keeping a stable API for callers. Use the accessor trait
91/// [`v0::v0_accessors::TokenPaymentInfoAccessorsV0`] to read or update fields, and
92/// [`methods::v0::TokenPaymentInfoMethodsV0`] for helpers like `token_id()` and
93/// `is_valid_for_required_cost()`.
94///
95/// See [`v0::TokenPaymentInfoV0`] for the current set of fields and semantics.
96pub enum TokenPaymentInfo {
97    #[display("V0({})", "_0")]
98    #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))]
99    V0(TokenPaymentInfoV0),
100}
101
102#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
103impl crate::serialization::JsonConvertible for TokenPaymentInfo {}
104
105#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
106impl crate::serialization::ValueConvertible for TokenPaymentInfo {}
107
108impl TokenPaymentInfoMethodsV0 for TokenPaymentInfo {}
109
110impl TokenPaymentInfoAccessorsV0 for TokenPaymentInfo {
111    // Getters
112    fn payment_token_contract_id(&self) -> Option<Identifier> {
113        match self {
114            TokenPaymentInfo::V0(v0) => v0.payment_token_contract_id(),
115        }
116    }
117
118    fn payment_token_contract_id_ref(&self) -> &Option<Identifier> {
119        match self {
120            TokenPaymentInfo::V0(v0) => v0.payment_token_contract_id_ref(),
121        }
122    }
123
124    fn token_contract_position(&self) -> TokenContractPosition {
125        match self {
126            TokenPaymentInfo::V0(v0) => v0.token_contract_position(),
127        }
128    }
129
130    fn minimum_token_cost(&self) -> Option<TokenAmount> {
131        match self {
132            TokenPaymentInfo::V0(v0) => v0.minimum_token_cost(),
133        }
134    }
135
136    fn maximum_token_cost(&self) -> Option<TokenAmount> {
137        match self {
138            TokenPaymentInfo::V0(v0) => v0.maximum_token_cost(),
139        }
140    }
141
142    fn gas_fees_paid_by(&self) -> GasFeesPaidBy {
143        match self {
144            TokenPaymentInfo::V0(v0) => v0.gas_fees_paid_by(),
145        }
146    }
147
148    // Setters
149    fn set_payment_token_contract_id(&mut self, id: Option<Identifier>) {
150        match self {
151            TokenPaymentInfo::V0(v0) => v0.set_payment_token_contract_id(id),
152        }
153    }
154
155    fn set_token_contract_position(&mut self, position: TokenContractPosition) {
156        match self {
157            TokenPaymentInfo::V0(v0) => v0.set_token_contract_position(position),
158        }
159    }
160
161    fn set_minimum_token_cost(&mut self, cost: Option<TokenAmount>) {
162        match self {
163            TokenPaymentInfo::V0(v0) => v0.set_minimum_token_cost(cost),
164        }
165    }
166
167    fn set_maximum_token_cost(&mut self, cost: Option<TokenAmount>) {
168        match self {
169            TokenPaymentInfo::V0(v0) => v0.set_maximum_token_cost(cost),
170        }
171    }
172
173    fn set_gas_fees_paid_by(&mut self, payer: GasFeesPaidBy) {
174        match self {
175            TokenPaymentInfo::V0(v0) => v0.set_gas_fees_paid_by(payer),
176        }
177    }
178}
179
180impl TryFrom<BTreeMap<String, Value>> for TokenPaymentInfo {
181    type Error = ProtocolError;
182
183    fn try_from(map: BTreeMap<String, Value>) -> Result<Self, Self::Error> {
184        // Expect a `$formatVersion` discriminator and dispatch to the
185        // corresponding versioned structure. This allows backward-compatible
186        // support for older serialized payloads.
187        let format_version = map.get_str("$formatVersion")?;
188        match format_version {
189            "0" => {
190                let token_payment_info: TokenPaymentInfoV0 = map.try_into()?;
191
192                Ok(token_payment_info.into())
193            }
194            version => Err(ProtocolError::UnknownVersionMismatch {
195                method: "TokenPaymentInfo::from_value".to_string(),
196                known_versions: vec![0],
197                received: version
198                    .parse()
199                    .map_err(|_| ProtocolError::Generic("Conversion error".to_string()))?,
200            }),
201        }
202    }
203}
204
205#[cfg(feature = "value-conversion")]
206impl TryFrom<TokenPaymentInfo> for Value {
207    type Error = Error;
208    /// Serialize the versioned token payment info into a platform `Value`.
209    ///
210    /// This mirrors the map format accepted by `TryFrom<BTreeMap<String, Value>>`,
211    /// including the `$formatVersion` discriminator.
212    fn try_from(value: TokenPaymentInfo) -> Result<Self, Self::Error> {
213        platform_value::to_value(value)
214    }
215}
216
217#[cfg(all(
218    test,
219    feature = "json-conversion",
220    feature = "value-conversion",
221    feature = "serde-conversion"
222))]
223mod json_convertible_tests {
224    use super::*;
225    use platform_value::platform_value;
226    use serde_json::json;
227
228    fn fixture() -> TokenPaymentInfo {
229        TokenPaymentInfo::V0(TokenPaymentInfoV0 {
230            payment_token_contract_id: Some(Identifier::new([0x99; 32])),
231            token_contract_position: 3,
232            minimum_token_cost: Some(100),
233            maximum_token_cost: Some(1_000),
234            gas_fees_paid_by: GasFeesPaidBy::ContractOwner,
235        })
236    }
237
238    #[test]
239    fn json_round_trip_with_full_wire_shape() {
240        use crate::serialization::JsonConvertible;
241        let original = fixture();
242        let json = original.to_json().expect("to_json");
243        // Internally-tagged enum (`tag = "$formatVersion"`); inner V0 has
244        // `rename_all = "camelCase"`. `Identifier` -> base58 in JSON.
245        // `token_contract_position` is `TokenContractPosition` (= u16) and
246        // `minimum_token_cost` / `maximum_token_cost` are `TokenAmount` (= u64);
247        // JSON erases the size — see the value-path assertion for typed locks.
248        // `gas_fees_paid_by` is the unit enum `GasFeesPaidBy` and serializes
249        // as `"ContractOwner"` (no `rename_all`).
250        assert_eq!(
251            json,
252            json!({
253                "$formatVersion": "0",
254                "paymentTokenContractId": "BLbDu5FZUdSfLrGejhuaWw5iMJBo3j3TVRyPv9rfJyMA",
255                "tokenContractPosition": 3,
256                "minimumTokenCost": 100,
257                "maximumTokenCost": 1_000,
258                "gasFeesPaidBy": "ContractOwner",
259            })
260        );
261        let recovered = TokenPaymentInfo::from_json(json).expect("from_json");
262        assert_eq!(original, recovered);
263    }
264
265    #[test]
266    fn value_round_trip_with_full_wire_shape() {
267        use crate::serialization::ValueConvertible;
268        let original = fixture();
269        let value = original.to_object().expect("to_object");
270        // `Identifier` flows as `Value::Identifier` when interpolated.
271        // `3u16` locks `Value::U16`; `100u64` / `1_000u64` lock `Value::U64`.
272        let payment_token_contract_id = Identifier::new([0x99; 32]);
273        assert_eq!(
274            value,
275            platform_value!({
276                "$formatVersion": "0",
277                "paymentTokenContractId": payment_token_contract_id,
278                "tokenContractPosition": 3u16,
279                "minimumTokenCost": 100u64,
280                "maximumTokenCost": 1_000u64,
281                "gasFeesPaidBy": "ContractOwner",
282            })
283        );
284        let recovered = TokenPaymentInfo::from_object(value).expect("from_object");
285        assert_eq!(original, recovered);
286    }
287}