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