dpp/shielded/builder/
document_token_payment.rs1use grovedb_commitment_tree::{Anchor, FullViewingKey, SpendAuthorizingKey};
2
3use crate::address_funds::OrchardAddress;
4use crate::balances::credits::TokenAmount;
5use crate::prelude::Identifier;
6use crate::shielded::document_token_payment_extra_sighash_data;
7use crate::tokens::token_payment_info::v1::TokenShieldedPayment;
8use crate::ProtocolError;
9use platform_version::version::PlatformVersion;
10
11use super::{build_spend_bundle, serialize_authorized_bundle, OrchardProver, SpendableNote};
12
13#[allow(clippy::too_many_arguments)]
25pub fn build_document_shielded_token_payment<P: OrchardProver>(
26 token_id: Identifier,
27 owner_id: Identifier,
28 data_contract_id: Identifier,
29 document_id: Identifier,
30 spends: Vec<SpendableNote>,
31 amount: TokenAmount,
32 change_address: &OrchardAddress,
33 fvk: &FullViewingKey,
34 ask: &SpendAuthorizingKey,
35 anchor: Anchor,
36 memo: [u8; 36],
37 prover: &P,
38 platform_version: &PlatformVersion,
39) -> Result<TokenShieldedPayment, ProtocolError> {
40 if amount == 0 {
41 return Err(ProtocolError::ShieldedBuildError(
42 "document token payment amount must be greater than zero".to_string(),
43 ));
44 }
45 if amount > i64::MAX as u64 {
46 return Err(ProtocolError::ShieldedBuildError(format!(
47 "document token payment amount {} exceeds maximum allowed value {}",
48 amount,
49 i64::MAX as u64
50 )));
51 }
52
53 let total_spent: u64 = spends
54 .iter()
55 .try_fold(0u64, |total, spend| {
56 total.checked_add(spend.note.value().inner())
57 })
58 .ok_or_else(|| {
59 ProtocolError::ShieldedBuildError("total spendable value overflows u64".to_string())
60 })?;
61 if amount > total_spent {
62 return Err(ProtocolError::ShieldedBuildError(format!(
63 "document token payment amount {} exceeds total spendable value {}",
64 amount, total_spent
65 )));
66 }
67 let change_amount = total_spent - amount;
68
69 let extra_sighash_data = document_token_payment_extra_sighash_data(
70 &token_id.to_buffer(),
71 &owner_id.to_buffer(),
72 &data_contract_id.to_buffer(),
73 &document_id.to_buffer(),
74 amount,
75 platform_version,
76 )?;
77
78 let bundle = build_spend_bundle(
79 spends,
80 change_address,
81 change_amount,
82 memo,
83 fvk,
84 ask,
85 anchor,
86 prover,
87 &extra_sighash_data,
88 )?;
89 let sb = serialize_authorized_bundle(&bundle);
90
91 if sb.value_balance != amount as i64 {
92 return Err(ProtocolError::ShieldedBuildError(format!(
93 "document token payment bundle value balance {} does not equal the amount {}",
94 sb.value_balance, amount
95 )));
96 }
97
98 Ok(TokenShieldedPayment {
99 amount,
100 actions: sb.actions,
101 anchor: sb.anchor,
102 proof: sb.proof,
103 binding_signature: sb.binding_signature,
104 })
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::shielded::builder::test_helpers::{
111 test_orchard_address, test_spendable_note, TestProver,
112 };
113 use grovedb_commitment_tree::SpendingKey;
114
115 fn keys() -> (FullViewingKey, SpendAuthorizingKey) {
116 let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes");
117 (FullViewingKey::from(&sk), SpendAuthorizingKey::from(&sk))
118 }
119
120 #[test]
121 fn rejects_amount_above_spendable_value() {
122 let (fvk, ask) = keys();
123 let err = build_document_shielded_token_payment(
124 Identifier::from([1u8; 32]),
125 Identifier::from([2u8; 32]),
126 Identifier::from([3u8; 32]),
127 Identifier::from([4u8; 32]),
128 vec![test_spendable_note(100)],
129 1_000,
130 &test_orchard_address(),
131 &fvk,
132 &ask,
133 Anchor::empty_tree(),
134 [0u8; 36],
135 &TestProver,
136 PlatformVersion::latest(),
137 )
138 .expect_err("overspend must be rejected")
139 .to_string();
140 assert!(
141 err.contains("exceeds total spendable value"),
142 "unexpected error: {err}"
143 );
144 }
145
146 #[test]
147 fn rejects_zero_amount() {
148 let (fvk, ask) = keys();
149 let err = build_document_shielded_token_payment(
150 Identifier::from([1u8; 32]),
151 Identifier::from([2u8; 32]),
152 Identifier::from([3u8; 32]),
153 Identifier::from([4u8; 32]),
154 vec![test_spendable_note(100)],
155 0,
156 &test_orchard_address(),
157 &fvk,
158 &ask,
159 Anchor::empty_tree(),
160 [0u8; 36],
161 &TestProver,
162 PlatformVersion::latest(),
163 )
164 .expect_err("zero amount must be rejected")
165 .to_string();
166 assert!(err.contains("greater than zero"), "unexpected error: {err}");
167 }
168}