1use grovedb_commitment_tree::{
2 Anchor, Builder, BundleType, DashMemo, FullViewingKey, NoteValue, PaymentAddress, Scope,
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)]
43pub fn build_shielded_transfer_transition<P: OrchardProver>(
44 spends: Vec<SpendableNote>,
45 recipient: &OrchardAddress,
46 transfer_amount: u64,
47 change_address: &OrchardAddress,
48 fvk: &FullViewingKey,
49 ask: &SpendAuthorizingKey,
50 anchor: Anchor,
51 prover: &P,
52 memo: [u8; 36],
53 platform_version: &PlatformVersion,
54) -> Result<(StateTransition, Credits), ProtocolError> {
55 let total_spent: u64 = spends.iter().map(|s| s.note.value().inner()).sum();
56
57 let num_actions = spends.len().max(2);
60 let fee = compute_minimum_shielded_fee(num_actions, platform_version)?;
64
65 let required = transfer_amount.checked_add(fee).ok_or_else(|| {
66 ProtocolError::ShieldedBuildError("fee + transfer_amount overflows u64".to_string())
67 })?;
68 if required > total_spent {
69 return Err(ProtocolError::ShieldedBuildError(format!(
70 "transfer amount {} + fee {} = {} exceeds total spendable value {}",
71 transfer_amount, fee, required, total_spent
72 )));
73 }
74
75 let change_amount = total_spent - required;
76
77 let recipient_payment = PaymentAddress::from(recipient);
78
79 let mut builder = Builder::<DashMemo>::new(BundleType::DEFAULT, anchor);
80
81 for spend in spends {
82 builder
83 .add_spend(fvk.clone(), spend.note, spend.merkle_path)
84 .map_err(|e| {
85 ProtocolError::ShieldedBuildError(format!("failed to add spend: {:?}", e))
86 })?;
87 }
88
89 let sender_ovk = fvk.to_ovk(Scope::External);
95
96 builder
98 .add_output(
99 Some(sender_ovk.clone()),
100 recipient_payment,
101 NoteValue::from_raw(transfer_amount),
102 memo,
103 )
104 .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?;
105
106 if change_amount > 0 {
108 let change_payment = PaymentAddress::from(change_address);
109 builder
110 .add_output(
111 Some(sender_ovk),
112 change_payment,
113 NoteValue::from_raw(change_amount),
114 [0u8; 36],
115 )
116 .map_err(|e| {
117 ProtocolError::ShieldedBuildError(format!("failed to add change output: {:?}", e))
118 })?;
119 }
120
121 let bundle = prove_and_sign_bundle(builder, prover, std::slice::from_ref(ask), &[])?;
123 let sb = serialize_authorized_bundle(&bundle);
124
125 let state_transition = ShieldedTransferTransition::try_from_bundle(
127 sb.actions,
128 sb.value_balance as u64,
129 sb.anchor,
130 sb.proof,
131 sb.binding_signature,
132 platform_version,
133 )?;
134 Ok((state_transition, fee))
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_insufficient_funds() {
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(100);
152 let spends = vec![note];
153
154 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32])
155 .expect("valid spending key bytes");
156 let fvk = FullViewingKey::from(&sk);
157 let ask = SpendAuthorizingKey::from(&sk);
158
159 let result = build_shielded_transfer_transition(
160 spends,
161 &recipient,
162 1_000_000,
163 &change_address,
164 &fvk,
165 &ask,
166 Anchor::empty_tree(),
167 &TestProver,
168 [0u8; 36],
169 platform_version,
170 );
171
172 assert!(result.is_err());
173 let err = result.unwrap_err().to_string();
174 assert!(
175 err.contains("exceeds total spendable value"),
176 "unexpected error: {}",
177 err
178 );
179 }
180
181 #[test]
186 fn test_shielded_transfer_fee_plus_amount_overflow_errors() {
187 let platform_version = PlatformVersion::latest();
189 let recipient = test_orchard_address();
190 let change_address = test_orchard_address();
191
192 let note = test_spendable_note(u64::MAX);
193 let spends = vec![note];
194
195 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32])
196 .expect("valid spending key bytes");
197 let fvk = FullViewingKey::from(&sk);
198 let ask = SpendAuthorizingKey::from(&sk);
199
200 let result = build_shielded_transfer_transition(
203 spends,
204 &recipient,
205 u64::MAX,
206 &change_address,
207 &fvk,
208 &ask,
209 Anchor::empty_tree(),
210 &TestProver,
211 [0u8; 36],
212 platform_version,
213 );
214
215 assert!(result.is_err(), "overflow case should error");
216 let err = result.unwrap_err().to_string();
217 assert!(
218 err.contains("fee + transfer_amount overflows u64"),
219 "expected checked_add overflow branch, got: {}",
220 err
221 );
222 }
223
224 #[test]
225 fn test_shielded_transfer_zero_spends_total_is_zero_errors() {
226 let platform_version = PlatformVersion::latest();
230 let recipient = test_orchard_address();
231 let change_address = test_orchard_address();
232
233 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32]).expect("valid sk");
234 let fvk = FullViewingKey::from(&sk);
235 let ask = SpendAuthorizingKey::from(&sk);
236
237 let result = build_shielded_transfer_transition(
238 vec![],
239 &recipient,
240 1,
241 &change_address,
242 &fvk,
243 &ask,
244 Anchor::empty_tree(),
245 &TestProver,
246 [0u8; 36],
247 platform_version,
248 );
249 assert!(result.is_err());
250 let err = result.unwrap_err().to_string();
251 assert!(
252 err.contains("exceeds total spendable value"),
253 "unexpected error: {}",
254 err
255 );
256 }
257
258 #[test]
259 fn test_shielded_transfer_uses_min_fee() {
260 let platform_version = PlatformVersion::latest();
264 let recipient = test_orchard_address();
265 let change_address = test_orchard_address();
266
267 let min_fee = crate::shielded::compute_minimum_shielded_fee(2, platform_version)
268 .expect("fee computation should not overflow");
269 let transfer_amount = 10u64;
270 let note = test_spendable_note(transfer_amount + min_fee);
271 let spends = vec![note];
272
273 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32]).expect("valid sk");
274 let fvk = FullViewingKey::from(&sk);
275 let ask = SpendAuthorizingKey::from(&sk);
276
277 let result = build_shielded_transfer_transition(
278 spends,
279 &recipient,
280 transfer_amount,
281 &change_address,
282 &fvk,
283 &ask,
284 Anchor::empty_tree(),
285 &TestProver,
286 [0u8; 36],
287 platform_version,
288 );
289
290 let err_msg = result.unwrap_err().to_string();
293 assert!(
294 err_msg.contains("failed to add spend")
295 || err_msg.contains("anchor")
296 || err_msg.contains("AnchorMismatch"),
297 "expected downstream add_spend error, got: {}",
298 err_msg
299 );
300 }
301}