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, Encode};
53use derive_more::{Display, From};
54use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize};
55use platform_value::btreemap_extensions::BTreeValueMapHelper;
56#[cfg(feature = "value-conversion")]
57use platform_value::Error;
58use platform_value::{Identifier, Value};
59#[cfg(feature = "serde-conversion")]
60use serde::{Deserialize, Serialize};
61use std::collections::BTreeMap;
62
63pub mod methods;
64pub mod v0;
65
66#[derive(
67    Debug,
68    Clone,
69    Copy,
70    Encode,
71    Decode,
72    PlatformDeserialize,
73    PlatformSerialize,
74    PartialEq,
75    Display,
76    From,
77)]
78#[cfg_attr(
79    feature = "serde-conversion",
80    derive(Serialize, Deserialize),
81    serde(tag = "$formatVersion")
82)]
83/// Versioned container describing how a client intends to pay with tokens.
84///
85/// The `TokenPaymentInfo` enum allows the protocol to evolve the underlying structure
86/// across versions while keeping a stable API for callers. Use the accessor trait
87/// [`v0::v0_accessors::TokenPaymentInfoAccessorsV0`] to read or update fields, and
88/// [`methods::v0::TokenPaymentInfoMethodsV0`] for helpers like `token_id()` and
89/// `is_valid_for_required_cost()`.
90///
91/// See [`v0::TokenPaymentInfoV0`] for the current set of fields and semantics.
92pub enum TokenPaymentInfo {
93    #[display("V0({})", "_0")]
94    #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))]
95    V0(TokenPaymentInfoV0),
96}
97
98#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
99impl crate::serialization::JsonConvertible for TokenPaymentInfo {}
100
101#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
102impl crate::serialization::ValueConvertible for TokenPaymentInfo {}
103
104impl TokenPaymentInfoMethodsV0 for TokenPaymentInfo {}
105
106impl TokenPaymentInfoAccessorsV0 for TokenPaymentInfo {
107    // Getters
108    fn payment_token_contract_id(&self) -> Option<Identifier> {
109        match self {
110            TokenPaymentInfo::V0(v0) => v0.payment_token_contract_id(),
111        }
112    }
113
114    fn payment_token_contract_id_ref(&self) -> &Option<Identifier> {
115        match self {
116            TokenPaymentInfo::V0(v0) => v0.payment_token_contract_id_ref(),
117        }
118    }
119
120    fn token_contract_position(&self) -> TokenContractPosition {
121        match self {
122            TokenPaymentInfo::V0(v0) => v0.token_contract_position(),
123        }
124    }
125
126    fn minimum_token_cost(&self) -> Option<TokenAmount> {
127        match self {
128            TokenPaymentInfo::V0(v0) => v0.minimum_token_cost(),
129        }
130    }
131
132    fn maximum_token_cost(&self) -> Option<TokenAmount> {
133        match self {
134            TokenPaymentInfo::V0(v0) => v0.maximum_token_cost(),
135        }
136    }
137
138    fn gas_fees_paid_by(&self) -> GasFeesPaidBy {
139        match self {
140            TokenPaymentInfo::V0(v0) => v0.gas_fees_paid_by(),
141        }
142    }
143
144    // Setters
145    fn set_payment_token_contract_id(&mut self, id: Option<Identifier>) {
146        match self {
147            TokenPaymentInfo::V0(v0) => v0.set_payment_token_contract_id(id),
148        }
149    }
150
151    fn set_token_contract_position(&mut self, position: TokenContractPosition) {
152        match self {
153            TokenPaymentInfo::V0(v0) => v0.set_token_contract_position(position),
154        }
155    }
156
157    fn set_minimum_token_cost(&mut self, cost: Option<TokenAmount>) {
158        match self {
159            TokenPaymentInfo::V0(v0) => v0.set_minimum_token_cost(cost),
160        }
161    }
162
163    fn set_maximum_token_cost(&mut self, cost: Option<TokenAmount>) {
164        match self {
165            TokenPaymentInfo::V0(v0) => v0.set_maximum_token_cost(cost),
166        }
167    }
168
169    fn set_gas_fees_paid_by(&mut self, payer: GasFeesPaidBy) {
170        match self {
171            TokenPaymentInfo::V0(v0) => v0.set_gas_fees_paid_by(payer),
172        }
173    }
174}
175
176impl TryFrom<BTreeMap<String, Value>> for TokenPaymentInfo {
177    type Error = ProtocolError;
178
179    fn try_from(map: BTreeMap<String, Value>) -> Result<Self, Self::Error> {
180        // Expect a `$formatVersion` discriminator and dispatch to the
181        // corresponding versioned structure. This allows backward-compatible
182        // support for older serialized payloads.
183        let format_version = map.get_str("$formatVersion")?;
184        match format_version {
185            "0" => {
186                let token_payment_info: TokenPaymentInfoV0 = map.try_into()?;
187
188                Ok(token_payment_info.into())
189            }
190            version => Err(ProtocolError::UnknownVersionMismatch {
191                method: "TokenPaymentInfo::from_value".to_string(),
192                known_versions: vec![0],
193                received: version
194                    .parse()
195                    .map_err(|_| ProtocolError::Generic("Conversion error".to_string()))?,
196            }),
197        }
198    }
199}
200
201#[cfg(feature = "value-conversion")]
202impl TryFrom<TokenPaymentInfo> for Value {
203    type Error = Error;
204    /// Serialize the versioned token payment info into a platform `Value`.
205    ///
206    /// This mirrors the map format accepted by `TryFrom<BTreeMap<String, Value>>`,
207    /// including the `$formatVersion` discriminator.
208    fn try_from(value: TokenPaymentInfo) -> Result<Self, Self::Error> {
209        platform_value::to_value(value)
210    }
211}
212
213#[cfg(all(
214    test,
215    feature = "json-conversion",
216    feature = "value-conversion",
217    feature = "serde-conversion"
218))]
219mod json_convertible_tests {
220    use super::*;
221    use platform_value::platform_value;
222    use serde_json::json;
223
224    fn fixture() -> TokenPaymentInfo {
225        TokenPaymentInfo::V0(TokenPaymentInfoV0 {
226            payment_token_contract_id: Some(Identifier::new([0x99; 32])),
227            token_contract_position: 3,
228            minimum_token_cost: Some(100),
229            maximum_token_cost: Some(1_000),
230            gas_fees_paid_by: GasFeesPaidBy::ContractOwner,
231        })
232    }
233
234    #[test]
235    fn json_round_trip_with_full_wire_shape() {
236        use crate::serialization::JsonConvertible;
237        let original = fixture();
238        let json = original.to_json().expect("to_json");
239        // Internally-tagged enum (`tag = "$formatVersion"`); inner V0 has
240        // `rename_all = "camelCase"`. `Identifier` -> base58 in JSON.
241        // `token_contract_position` is `TokenContractPosition` (= u16) and
242        // `minimum_token_cost` / `maximum_token_cost` are `TokenAmount` (= u64);
243        // JSON erases the size — see the value-path assertion for typed locks.
244        // `gas_fees_paid_by` is the unit enum `GasFeesPaidBy` and serializes
245        // as `"ContractOwner"` (no `rename_all`).
246        assert_eq!(
247            json,
248            json!({
249                "$formatVersion": "0",
250                "paymentTokenContractId": "BLbDu5FZUdSfLrGejhuaWw5iMJBo3j3TVRyPv9rfJyMA",
251                "tokenContractPosition": 3,
252                "minimumTokenCost": 100,
253                "maximumTokenCost": 1_000,
254                "gasFeesPaidBy": "ContractOwner",
255            })
256        );
257        let recovered = TokenPaymentInfo::from_json(json).expect("from_json");
258        assert_eq!(original, recovered);
259    }
260
261    #[test]
262    fn value_round_trip_with_full_wire_shape() {
263        use crate::serialization::ValueConvertible;
264        let original = fixture();
265        let value = original.to_object().expect("to_object");
266        // `Identifier` flows as `Value::Identifier` when interpolated.
267        // `3u16` locks `Value::U16`; `100u64` / `1_000u64` lock `Value::U64`.
268        let payment_token_contract_id = Identifier::new([0x99; 32]);
269        assert_eq!(
270            value,
271            platform_value!({
272                "$formatVersion": "0",
273                "paymentTokenContractId": payment_token_contract_id,
274                "tokenContractPosition": 3u16,
275                "minimumTokenCost": 100u64,
276                "maximumTokenCost": 1_000u64,
277                "gasFeesPaidBy": "ContractOwner",
278            })
279        );
280        let recovered = TokenPaymentInfo::from_object(value).expect("from_object");
281        assert_eq!(original, recovered);
282    }
283}