Skip to main content

dpp/tokens/token_payment_info/v1/
mod.rs

1pub mod v1_accessors;
2
3use crate::balances::credits::TokenAmount;
4use crate::data_contract::TokenContractPosition;
5use crate::shielded::SerializedAction;
6use crate::tokens::gas_fees_paid_by::GasFeesPaidBy;
7use crate::tokens::token_payment_info::v0::v0_accessors::TokenPaymentInfoAccessorsV0;
8use crate::tokens::token_payment_info::v0::TokenPaymentInfoV0;
9use crate::ProtocolError;
10use bincode::{Decode, DecodeUntrusted, Encode};
11use platform_value::btreemap_extensions::{
12    BTreeValueRemoveFromMapHelper, BTreeValueRemoveInnerValueFromMapHelper,
13};
14use platform_value::{Identifier, Value};
15#[cfg(feature = "serde-conversion")]
16use serde::{Deserialize, Serialize};
17use std::collections::BTreeMap;
18use std::fmt;
19
20/// A spend bundle in the payment token's shielded pool that pays a document action's token
21/// cost. The notes it spends leave the pool: `amount` of them go where the document type's
22/// token cost effect sends them (the contract owner's balance, or out of the supply) and the
23/// change returns to the pool as new notes. The note owner authorizes the spend; the batch
24/// owner still signs the batch and pays its fee in credits.
25#[derive(Debug, Clone, Encode, Decode, PartialEq, DecodeUntrusted)]
26// Auto-injects `json_safe_u64` on `amount` and `serde_bytes` on the fixed byte arrays /
27// `serde_bytes_var` on `proof` (base64 strings in JSON, raw bytes in Value).
28#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)]
29#[cfg_attr(
30    feature = "serde-conversion",
31    derive(Serialize, Deserialize),
32    serde(rename_all = "camelCase")
33)]
34pub struct TokenShieldedPayment {
35    /// The tokens the bundle pays: its value balance. Must equal the token cost of the document
36    /// action it pays for.
37    pub amount: TokenAmount,
38    /// Orchard actions (spend-output pairs).
39    pub actions: Vec<SerializedAction>,
40    /// Sinsemilla root of the token pool's note commitment tree the bundle was built against.
41    pub anchor: [u8; 32],
42    /// Halo 2 proof bytes.
43    pub proof: Vec<u8>,
44    /// RedPallas binding signature.
45    // `#[json_safe_fields]` gives this field `serde_bytes` when `json-conversion` is on; serde
46    // has no impl for arrays longer than 32, so a serde build without it needs the same here.
47    #[cfg_attr(
48        all(feature = "serde-conversion", not(feature = "json-conversion")),
49        serde(with = "crate::serialization::serde_bytes")
50    )]
51    pub binding_signature: [u8; 64],
52}
53
54#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
55impl crate::serialization::JsonConvertible for TokenShieldedPayment {}
56
57#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
58impl crate::serialization::ValueConvertible for TokenShieldedPayment {}
59
60impl Default for TokenShieldedPayment {
61    fn default() -> Self {
62        Self {
63            amount: 0,
64            actions: vec![],
65            anchor: [0u8; 32],
66            proof: vec![],
67            binding_signature: [0u8; 64],
68        }
69    }
70}
71
72impl fmt::Display for TokenShieldedPayment {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        write!(
75            f,
76            "Shielded Payment: amount {}, actions {}",
77            self.amount,
78            self.actions.len()
79        )
80    }
81}
82
83impl TryFrom<BTreeMap<String, Value>> for TokenShieldedPayment {
84    type Error = ProtocolError;
85
86    fn try_from(mut map: BTreeMap<String, Value>) -> Result<Self, Self::Error> {
87        // These are raw cryptographic bytes, so they cross the JSON boundary as
88        // base64 — the encoding this type's own JSON form writes, and the one
89        // `Bytes32` / `BinaryData` use. `remove_bytes_32` and
90        // `remove_binary_data` decode base64 text and pass byte-valued
91        // `Value`s through, so both the JSON and the binary map shape parse.
92        // Base58 is this repo's identifier encoding and does not apply here.
93        let actions = map
94            .remove_inner_value_array::<Vec<Value>>("actions")?
95            .into_iter()
96            .map(|value| {
97                let mut action = value.into_btree_string_map()?;
98                Ok(SerializedAction {
99                    nullifier: action.remove_bytes_32("nullifier")?.to_buffer(),
100                    rk: action.remove_bytes_32("rk")?.to_buffer(),
101                    cmx: action.remove_bytes_32("cmx")?.to_buffer(),
102                    encrypted_note: action.remove_binary_data("encryptedNote")?.0,
103                    cv_net: action.remove_bytes_32("cvNet")?.to_buffer(),
104                    spend_auth_sig: fixed_64(
105                        action.remove_binary_data("spendAuthSig")?.0,
106                        "spendAuthSig",
107                    )?,
108                })
109            })
110            .collect::<Result<Vec<_>, ProtocolError>>()?;
111        Ok(TokenShieldedPayment {
112            amount: map.remove_integer("amount")?,
113            actions,
114            anchor: map.remove_bytes_32("anchor")?.to_buffer(),
115            proof: map.remove_binary_data("proof")?.0,
116            binding_signature: fixed_64(
117                map.remove_binary_data("bindingSignature")?.0,
118                "bindingSignature",
119            )?,
120        })
121    }
122}
123
124fn fixed_64(bytes: Vec<u8>, field: &str) -> Result<[u8; 64], ProtocolError> {
125    let len = bytes.len();
126    bytes
127        .try_into()
128        .map_err(|_| ProtocolError::DecodingError(format!("{field} must be 64 bytes, got {len}")))
129}
130
131/// Format version 1 of the token payment info: the `V0` fields plus a shielded payment. The
132/// document action's token cost is paid out of the token's shielded pool by `shielded_payment`
133/// instead of the document owner's token balance; the identity signing the batch still pays
134/// the credit fee and receives nothing.
135#[derive(Debug, Clone, Encode, Decode, Default, PartialEq, DecodeUntrusted)]
136#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)]
137#[cfg_attr(
138    feature = "serde-conversion",
139    derive(Serialize, Deserialize),
140    serde(rename_all = "camelCase")
141)]
142pub struct TokenPaymentInfoV1 {
143    /// By default, we use a token in the same contract, this field must be set if the document
144    /// requires payment using another contracts token.
145    pub payment_token_contract_id: Option<Identifier>,
146    /// Which token (by position) on the contract the payment uses.
147    pub token_contract_position: TokenContractPosition,
148    /// Minimum token cost, this most often should not be set.
149    pub minimum_token_cost: Option<TokenAmount>,
150    /// Maximum token cost, this most often should be set.
151    pub maximum_token_cost: Option<TokenAmount>,
152    /// Who pays the gas fees, this needs to match what the contract allows.
153    pub gas_fees_paid_by: GasFeesPaidBy,
154    /// The spend bundle paying the token cost from the token's shielded pool. Boxed so a
155    /// payment info without one stays small.
156    pub shielded_payment: Box<TokenShieldedPayment>,
157}
158
159impl fmt::Display for TokenPaymentInfoV1 {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        write!(
162            f,
163            "Contract ID: {:?}, Token Position: {:?}, Min Cost: {:?}, Max Cost: {:?}, Gas Fees Paid By: {}, {}",
164            self.payment_token_contract_id,
165            self.token_contract_position,
166            self.minimum_token_cost,
167            self.maximum_token_cost,
168            self.gas_fees_paid_by,
169            self.shielded_payment
170        )
171    }
172}
173
174impl TokenPaymentInfoV1 {
175    /// Builds a `V1` from a `V0` and the shielded payment that pays its cost.
176    pub fn from_v0(v0: TokenPaymentInfoV0, shielded_payment: TokenShieldedPayment) -> Self {
177        Self {
178            payment_token_contract_id: v0.payment_token_contract_id,
179            token_contract_position: v0.token_contract_position,
180            minimum_token_cost: v0.minimum_token_cost,
181            maximum_token_cost: v0.maximum_token_cost,
182            gas_fees_paid_by: v0.gas_fees_paid_by,
183            shielded_payment: Box::new(shielded_payment),
184        }
185    }
186
187    /// Drops the shielded payment, leaving the `V0` fields.
188    pub fn into_v0(self) -> TokenPaymentInfoV0 {
189        TokenPaymentInfoV0 {
190            payment_token_contract_id: self.payment_token_contract_id,
191            token_contract_position: self.token_contract_position,
192            minimum_token_cost: self.minimum_token_cost,
193            maximum_token_cost: self.maximum_token_cost,
194            gas_fees_paid_by: self.gas_fees_paid_by,
195        }
196    }
197
198    /// The spend bundle paying the token cost from the token's shielded pool.
199    pub fn shielded_payment(&self) -> &TokenShieldedPayment {
200        &self.shielded_payment
201    }
202}
203
204impl TokenPaymentInfoAccessorsV0 for TokenPaymentInfoV1 {
205    fn payment_token_contract_id(&self) -> Option<Identifier> {
206        self.payment_token_contract_id
207    }
208
209    fn payment_token_contract_id_ref(&self) -> &Option<Identifier> {
210        &self.payment_token_contract_id
211    }
212
213    fn token_contract_position(&self) -> TokenContractPosition {
214        self.token_contract_position
215    }
216
217    fn minimum_token_cost(&self) -> Option<TokenAmount> {
218        self.minimum_token_cost
219    }
220
221    fn maximum_token_cost(&self) -> Option<TokenAmount> {
222        self.maximum_token_cost
223    }
224
225    fn set_payment_token_contract_id(&mut self, id: Option<Identifier>) {
226        self.payment_token_contract_id = id;
227    }
228
229    fn set_token_contract_position(&mut self, position: TokenContractPosition) {
230        self.token_contract_position = position;
231    }
232
233    fn set_minimum_token_cost(&mut self, cost: Option<TokenAmount>) {
234        self.minimum_token_cost = cost;
235    }
236
237    fn set_maximum_token_cost(&mut self, cost: Option<TokenAmount>) {
238        self.maximum_token_cost = cost;
239    }
240
241    fn gas_fees_paid_by(&self) -> GasFeesPaidBy {
242        self.gas_fees_paid_by
243    }
244
245    fn set_gas_fees_paid_by(&mut self, payer: GasFeesPaidBy) {
246        self.gas_fees_paid_by = payer;
247    }
248}
249
250impl TryFrom<BTreeMap<String, Value>> for TokenPaymentInfoV1 {
251    type Error = ProtocolError;
252
253    fn try_from(mut map: BTreeMap<String, Value>) -> Result<Self, Self::Error> {
254        let shielded_map: BTreeMap<String, Value> = map
255            .remove_map_as_btree_map_keep_values_as_platform_value::<String, Value>(
256                "shieldedPayment",
257            )?;
258        let shielded_payment: TokenShieldedPayment = shielded_map.try_into()?;
259        let v0: TokenPaymentInfoV0 = map.try_into()?;
260        Ok(TokenPaymentInfoV1::from_v0(v0, shielded_payment))
261    }
262}