dpp/tokens/token_payment_info/
mod.rs1use crate::balances::credits::TokenAmount;
51use crate::data_contract::TokenContractPosition;
52#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
53use crate::serialization::JsonConvertible;
54#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
55use crate::serialization::ValueConvertible;
56use crate::tokens::gas_fees_paid_by::GasFeesPaidBy;
57use crate::tokens::token_payment_info::methods::v0::TokenPaymentInfoMethodsV0;
58use crate::tokens::token_payment_info::v0::v0_accessors::TokenPaymentInfoAccessorsV0;
59use crate::tokens::token_payment_info::v0::TokenPaymentInfoV0;
60use crate::tokens::token_payment_info::v1::v1_accessors::TokenPaymentInfoAccessorsV1;
61use crate::tokens::token_payment_info::v1::{TokenPaymentInfoV1, TokenShieldedPayment};
62use crate::ProtocolError;
63use bincode::{Decode, DecodeUntrusted, Encode};
64use derive_more::{Display, From};
65use platform_serialization_derive::{
66 PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize,
67};
68use platform_value::btreemap_extensions::BTreeValueMapHelper;
69#[cfg(feature = "value-conversion")]
70use platform_value::Error;
71use platform_value::{Identifier, Value};
72#[cfg(feature = "serde-conversion")]
73use serde::{Deserialize, Serialize};
74use std::collections::BTreeMap;
75
76pub mod methods;
77pub mod v0;
78pub mod v1;
79
80#[derive(
81 Debug,
82 Clone,
83 Encode,
84 Decode,
85 PlatformDeserializeTrusted,
86 PlatformDeserializeUntrusted,
87 PlatformSerialize,
88 PartialEq,
89 Display,
90 From,
91 DecodeUntrusted,
92)]
93#[cfg_attr(
94 feature = "serde-conversion",
95 derive(Serialize, Deserialize),
96 serde(tag = "$formatVersion")
97)]
98pub enum TokenPaymentInfo {
108 #[display("V0({})", "_0")]
109 #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))]
110 V0(TokenPaymentInfoV0),
111 #[display("V1({})", "_0")]
113 #[cfg_attr(feature = "serde-conversion", serde(rename = "1"))]
114 V1(TokenPaymentInfoV1),
115}
116
117#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
118impl JsonConvertible for TokenPaymentInfo {}
119
120#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
121impl ValueConvertible for TokenPaymentInfo {}
122
123impl TokenPaymentInfoMethodsV0 for TokenPaymentInfo {}
124
125impl TokenPaymentInfoAccessorsV1 for TokenPaymentInfo {
126 fn shielded_payment(&self) -> Option<&TokenShieldedPayment> {
127 match self {
128 TokenPaymentInfo::V0(_) => None,
129 TokenPaymentInfo::V1(v1) => Some(v1.shielded_payment()),
130 }
131 }
132
133 fn set_shielded_payment(&mut self, shielded_payment: Option<TokenShieldedPayment>) {
134 let current = std::mem::replace(self, TokenPaymentInfo::V0(TokenPaymentInfoV0::default()));
135 *self = match (current, shielded_payment) {
136 (TokenPaymentInfo::V0(v0), Some(payment)) => {
137 TokenPaymentInfo::V1(TokenPaymentInfoV1::from_v0(v0, payment))
138 }
139 (TokenPaymentInfo::V1(v1), Some(payment)) => TokenPaymentInfo::V1(TokenPaymentInfoV1 {
140 shielded_payment: Box::new(payment),
141 ..v1
142 }),
143 (TokenPaymentInfo::V1(v1), None) => TokenPaymentInfo::V0(v1.into_v0()),
144 (v0 @ TokenPaymentInfo::V0(_), None) => v0,
145 };
146 }
147}
148
149impl TokenPaymentInfoAccessorsV0 for TokenPaymentInfo {
150 fn payment_token_contract_id(&self) -> Option<Identifier> {
152 match self {
153 TokenPaymentInfo::V0(v0) => v0.payment_token_contract_id(),
154 TokenPaymentInfo::V1(v1) => v1.payment_token_contract_id(),
155 }
156 }
157
158 fn payment_token_contract_id_ref(&self) -> &Option<Identifier> {
159 match self {
160 TokenPaymentInfo::V0(v0) => v0.payment_token_contract_id_ref(),
161 TokenPaymentInfo::V1(v1) => v1.payment_token_contract_id_ref(),
162 }
163 }
164
165 fn token_contract_position(&self) -> TokenContractPosition {
166 match self {
167 TokenPaymentInfo::V0(v0) => v0.token_contract_position(),
168 TokenPaymentInfo::V1(v1) => v1.token_contract_position(),
169 }
170 }
171
172 fn minimum_token_cost(&self) -> Option<TokenAmount> {
173 match self {
174 TokenPaymentInfo::V0(v0) => v0.minimum_token_cost(),
175 TokenPaymentInfo::V1(v1) => v1.minimum_token_cost(),
176 }
177 }
178
179 fn maximum_token_cost(&self) -> Option<TokenAmount> {
180 match self {
181 TokenPaymentInfo::V0(v0) => v0.maximum_token_cost(),
182 TokenPaymentInfo::V1(v1) => v1.maximum_token_cost(),
183 }
184 }
185
186 fn gas_fees_paid_by(&self) -> GasFeesPaidBy {
187 match self {
188 TokenPaymentInfo::V0(v0) => v0.gas_fees_paid_by(),
189 TokenPaymentInfo::V1(v1) => v1.gas_fees_paid_by(),
190 }
191 }
192
193 fn set_payment_token_contract_id(&mut self, id: Option<Identifier>) {
195 match self {
196 TokenPaymentInfo::V0(v0) => v0.set_payment_token_contract_id(id),
197 TokenPaymentInfo::V1(v1) => v1.set_payment_token_contract_id(id),
198 }
199 }
200
201 fn set_token_contract_position(&mut self, position: TokenContractPosition) {
202 match self {
203 TokenPaymentInfo::V0(v0) => v0.set_token_contract_position(position),
204 TokenPaymentInfo::V1(v1) => v1.set_token_contract_position(position),
205 }
206 }
207
208 fn set_minimum_token_cost(&mut self, cost: Option<TokenAmount>) {
209 match self {
210 TokenPaymentInfo::V0(v0) => v0.set_minimum_token_cost(cost),
211 TokenPaymentInfo::V1(v1) => v1.set_minimum_token_cost(cost),
212 }
213 }
214
215 fn set_maximum_token_cost(&mut self, cost: Option<TokenAmount>) {
216 match self {
217 TokenPaymentInfo::V0(v0) => v0.set_maximum_token_cost(cost),
218 TokenPaymentInfo::V1(v1) => v1.set_maximum_token_cost(cost),
219 }
220 }
221
222 fn set_gas_fees_paid_by(&mut self, payer: GasFeesPaidBy) {
223 match self {
224 TokenPaymentInfo::V0(v0) => v0.set_gas_fees_paid_by(payer),
225 TokenPaymentInfo::V1(v1) => v1.set_gas_fees_paid_by(payer),
226 }
227 }
228}
229
230impl TryFrom<BTreeMap<String, Value>> for TokenPaymentInfo {
231 type Error = ProtocolError;
232
233 fn try_from(map: BTreeMap<String, Value>) -> Result<Self, Self::Error> {
234 let format_version = map.get_str("$formatVersion")?;
238 match format_version {
239 "0" => {
240 let token_payment_info: TokenPaymentInfoV0 = map.try_into()?;
241
242 Ok(token_payment_info.into())
243 }
244 "1" => {
245 let token_payment_info: TokenPaymentInfoV1 = map.try_into()?;
246
247 Ok(token_payment_info.into())
248 }
249 version => Err(ProtocolError::UnknownVersionMismatch {
250 method: "TokenPaymentInfo::from_value".to_string(),
251 known_versions: vec![0, 1],
252 received: version
253 .parse()
254 .map_err(|_| ProtocolError::Generic("Conversion error".to_string()))?,
255 }),
256 }
257 }
258}
259
260#[cfg(feature = "value-conversion")]
261impl TryFrom<TokenPaymentInfo> for Value {
262 type Error = Error;
263 fn try_from(value: TokenPaymentInfo) -> Result<Self, Self::Error> {
268 platform_value::to_value(value)
269 }
270}
271
272#[cfg(all(
273 test,
274 feature = "json-conversion",
275 feature = "value-conversion",
276 feature = "serde-conversion"
277))]
278mod json_convertible_tests {
279 use super::*;
280 use platform_value::platform_value;
281 use serde_json::json;
282
283 fn fixture() -> TokenPaymentInfo {
284 TokenPaymentInfo::V0(TokenPaymentInfoV0 {
285 payment_token_contract_id: Some(Identifier::new([0x99; 32])),
286 token_contract_position: 3,
287 minimum_token_cost: Some(100),
288 maximum_token_cost: Some(1_000),
289 gas_fees_paid_by: GasFeesPaidBy::ContractOwner,
290 })
291 }
292
293 #[test]
294 fn json_round_trip_with_full_wire_shape() {
295 use crate::serialization::JsonConvertible;
296 let original = fixture();
297 let json = original.to_json().expect("to_json");
298 assert_eq!(
306 json,
307 json!({
308 "$formatVersion": "0",
309 "paymentTokenContractId": "BLbDu5FZUdSfLrGejhuaWw5iMJBo3j3TVRyPv9rfJyMA",
310 "tokenContractPosition": 3,
311 "minimumTokenCost": 100,
312 "maximumTokenCost": 1_000,
313 "gasFeesPaidBy": "ContractOwner",
314 })
315 );
316 let recovered = TokenPaymentInfo::from_json(json).expect("from_json");
317 assert_eq!(original, recovered);
318 }
319
320 fn v1_fixture() -> TokenPaymentInfo {
321 use crate::shielded::SerializedAction;
322 use crate::tokens::token_payment_info::v1::{TokenPaymentInfoV1, TokenShieldedPayment};
323 TokenPaymentInfo::V1(TokenPaymentInfoV1 {
324 payment_token_contract_id: None,
325 token_contract_position: 1,
326 minimum_token_cost: None,
327 maximum_token_cost: Some(10),
328 gas_fees_paid_by: GasFeesPaidBy::DocumentOwner,
329 shielded_payment: Box::new(TokenShieldedPayment {
330 amount: 10,
331 actions: vec![SerializedAction {
332 nullifier: [1u8; 32],
333 rk: [2u8; 32],
334 cmx: [3u8; 32],
335 encrypted_note: vec![4u8; 216],
336 cv_net: [5u8; 32],
337 spend_auth_sig: [6u8; 64],
338 }],
339 anchor: [7u8; 32],
340 proof: vec![8u8; 10],
341 binding_signature: [9u8; 64],
342 }),
343 })
344 }
345
346 #[test]
347 fn v1_json_and_value_round_trip() {
348 use crate::serialization::{JsonConvertible, ValueConvertible};
349 let original = v1_fixture();
350 let json = original.to_json().expect("to_json");
351 assert_eq!(json["$formatVersion"], "1");
352 assert_eq!(json["shieldedPayment"]["amount"], 10);
353 let recovered = TokenPaymentInfo::from_json(json).expect("from_json");
354 assert_eq!(original, recovered);
355 let value = original.to_object().expect("to_object");
356 let recovered = TokenPaymentInfo::from_object(value.clone()).expect("from_object");
357 assert_eq!(original, recovered);
358 let map = value.into_btree_string_map().expect("map");
360 let recovered: TokenPaymentInfo = map.try_into().expect("try_into");
361 assert_eq!(original, recovered);
362 }
363
364 #[test]
371 fn v1_parses_a_map_whose_byte_fields_arrived_as_base64_json_text() {
372 use crate::serialization::JsonConvertible;
373 let original = v1_fixture();
374 let json = original.to_json().expect("to_json");
375 assert_eq!(
377 json["shieldedPayment"]["anchor"],
378 json!("BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=")
379 );
380 assert_eq!(json["shieldedPayment"]["proof"], json!("CAgICAgICAgICA=="));
381 assert_eq!(
382 json["shieldedPayment"]["actions"][0]["nullifier"],
383 json!("AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=")
384 );
385 let map = Value::from(json)
386 .into_btree_string_map()
387 .expect("the JSON object becomes a string-keyed map");
388 let recovered: TokenPaymentInfo = map.try_into().expect("the JSON map parses");
389 assert_eq!(original, recovered);
390 }
391
392 #[test]
393 fn setting_and_clearing_the_shielded_payment_switches_the_format_version() {
394 let mut info = fixture();
395 let TokenPaymentInfo::V1(v1) = v1_fixture() else {
396 unreachable!()
397 };
398 info.set_shielded_payment(Some((*v1.shielded_payment).clone()));
399 assert!(matches!(info, TokenPaymentInfo::V1(_)));
400 assert_eq!(info.shielded_payment(), Some(&*v1.shielded_payment));
401 assert_eq!(info.maximum_token_cost(), Some(1_000));
402 info.set_shielded_payment(None);
403 assert_eq!(info, fixture());
404 }
405
406 #[test]
407 fn value_round_trip_with_full_wire_shape() {
408 use crate::serialization::ValueConvertible;
409 let original = fixture();
410 let value = original.to_object().expect("to_object");
411 let payment_token_contract_id = Identifier::new([0x99; 32]);
414 assert_eq!(
415 value,
416 platform_value!({
417 "$formatVersion": "0",
418 "paymentTokenContractId": payment_token_contract_id,
419 "tokenContractPosition": 3u16,
420 "minimumTokenCost": 100u64,
421 "maximumTokenCost": 1_000u64,
422 "gasFeesPaidBy": "ContractOwner",
423 })
424 );
425 let recovered = TokenPaymentInfo::from_object(value).expect("from_object");
426 assert_eq!(original, recovered);
427 }
428}