1use grovedb_commitment_tree::{
2 Anchor, Builder, BundleType, DashMemo, FullViewingKey, NoteValue, PaymentAddress,
3 SpendAuthorizingKey,
4};
5
6use crate::address_funds::OrchardAddress;
7use crate::fee::Credits;
8use crate::shielded::compute_minimum_shielded_fee;
9use crate::state_transition::shielded_transfer_transition::methods::ShieldedTransferTransitionMethodsV0;
10use crate::state_transition::shielded_transfer_transition::ShieldedTransferTransition;
11use crate::state_transition::StateTransition;
12use crate::ProtocolError;
13use platform_version::version::PlatformVersion;
14
15use super::{prove_and_sign_bundle, serialize_authorized_bundle, OrchardProver, SpendableNote};
16
17#[allow(clippy::too_many_arguments)]
37pub fn build_shielded_transfer_transition<P: OrchardProver>(
38 spends: Vec<SpendableNote>,
39 recipient: &OrchardAddress,
40 transfer_amount: u64,
41 change_address: &OrchardAddress,
42 fvk: &FullViewingKey,
43 ask: &SpendAuthorizingKey,
44 anchor: Anchor,
45 prover: &P,
46 memo: [u8; 36],
47 fee: Option<Credits>,
48 platform_version: &PlatformVersion,
49) -> Result<StateTransition, ProtocolError> {
50 let total_spent: u64 = spends.iter().map(|s| s.note.value().inner()).sum();
51
52 let num_actions = spends.len().max(2);
55 let min_fee = compute_minimum_shielded_fee(num_actions, platform_version);
56 let effective_fee = match fee {
57 Some(f) if f < min_fee => {
58 return Err(ProtocolError::ShieldedBuildError(format!(
59 "fee {} is below minimum required fee {}",
60 f, min_fee
61 )));
62 }
63 Some(f) if f > min_fee.saturating_mul(1000) => {
64 return Err(ProtocolError::ShieldedBuildError(format!(
65 "fee {} exceeds 1000x the minimum fee {}",
66 f, min_fee
67 )));
68 }
69 Some(f) => f,
70 None => min_fee,
71 };
72
73 let required = transfer_amount.checked_add(effective_fee).ok_or_else(|| {
74 ProtocolError::ShieldedBuildError("fee + transfer_amount overflows u64".to_string())
75 })?;
76 if required > total_spent {
77 return Err(ProtocolError::ShieldedBuildError(format!(
78 "transfer amount {} + fee {} = {} exceeds total spendable value {}",
79 transfer_amount, effective_fee, required, total_spent
80 )));
81 }
82
83 let change_amount = total_spent - required;
84
85 let recipient_payment = PaymentAddress::from(recipient);
86
87 let mut builder = Builder::<DashMemo>::new(BundleType::DEFAULT, anchor);
88
89 for spend in spends {
90 builder
91 .add_spend(fvk.clone(), spend.note, spend.merkle_path)
92 .map_err(|e| {
93 ProtocolError::ShieldedBuildError(format!("failed to add spend: {:?}", e))
94 })?;
95 }
96
97 builder
99 .add_output(
100 None,
101 recipient_payment,
102 NoteValue::from_raw(transfer_amount),
103 memo,
104 )
105 .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?;
106
107 if change_amount > 0 {
109 let change_payment = PaymentAddress::from(change_address);
110 builder
111 .add_output(
112 None,
113 change_payment,
114 NoteValue::from_raw(change_amount),
115 [0u8; 36],
116 )
117 .map_err(|e| {
118 ProtocolError::ShieldedBuildError(format!("failed to add change output: {:?}", e))
119 })?;
120 }
121
122 let bundle = prove_and_sign_bundle(builder, prover, std::slice::from_ref(ask), &[])?;
124 let sb = serialize_authorized_bundle(&bundle);
125
126 ShieldedTransferTransition::try_from_bundle(
128 sb.actions,
129 sb.value_balance as u64,
130 sb.anchor,
131 sb.proof,
132 sb.binding_signature,
133 platform_version,
134 )
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::shielded::builder::test_helpers::{
141 test_orchard_address, test_spendable_note, TestProver,
142 };
143
144 #[test]
145 fn test_shielded_transfer_fee_below_minimum() {
146 let platform_version = PlatformVersion::latest();
147 let recipient = test_orchard_address();
148 let change_address = test_orchard_address();
149
150 let note = test_spendable_note(1_000_000);
151 let spends = vec![note];
152
153 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32])
154 .expect("valid spending key bytes");
155 let fvk = FullViewingKey::from(&sk);
156 let ask = SpendAuthorizingKey::from(&sk);
157
158 let result = build_shielded_transfer_transition(
159 spends,
160 &recipient,
161 100,
162 &change_address,
163 &fvk,
164 &ask,
165 Anchor::empty_tree(),
166 &TestProver,
167 [0u8; 36],
168 Some(1), platform_version,
170 );
171
172 assert!(result.is_err());
173 let err = result.unwrap_err().to_string();
174 assert!(
175 err.contains("below minimum required fee"),
176 "unexpected error: {}",
177 err
178 );
179 }
180
181 #[test]
182 fn test_shielded_transfer_insufficient_funds() {
183 let platform_version = PlatformVersion::latest();
184 let recipient = test_orchard_address();
185 let change_address = test_orchard_address();
186
187 let note = test_spendable_note(100);
189 let spends = vec![note];
190
191 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32])
192 .expect("valid spending key bytes");
193 let fvk = FullViewingKey::from(&sk);
194 let ask = SpendAuthorizingKey::from(&sk);
195
196 let result = build_shielded_transfer_transition(
197 spends,
198 &recipient,
199 1_000_000,
200 &change_address,
201 &fvk,
202 &ask,
203 Anchor::empty_tree(),
204 &TestProver,
205 [0u8; 36],
206 None,
207 platform_version,
208 );
209
210 assert!(result.is_err());
211 let err = result.unwrap_err().to_string();
212 assert!(
213 err.contains("exceeds total spendable value"),
214 "unexpected error: {}",
215 err
216 );
217 }
218}