1use std::collections::BTreeMap;
2
3use crate::address_funds::AddressFundsFeeStrategy;
4use crate::address_funds::{OrchardAddress, PlatformAddress};
5use crate::fee::Credits;
6use crate::identity::signer::Signer;
7use crate::prelude::{AddressNonce, UserFeeIncrease};
8use crate::shielded::shield_extra_sighash_data;
9use crate::state_transition::shield_transition::methods::ShieldTransitionMethodsV0;
10use crate::state_transition::shield_transition::ShieldTransition;
11use crate::state_transition::StateTransition;
12use crate::ProtocolError;
13use platform_version::version::PlatformVersion;
14
15use super::{build_output_only_bundle, serialize_authorized_bundle, OrchardProver};
16
17#[allow(clippy::too_many_arguments)]
38pub async fn build_shield_transition<S: Signer<PlatformAddress>, P: OrchardProver>(
39 recipient: &OrchardAddress,
40 shield_amount: u64,
41 inputs: BTreeMap<PlatformAddress, (AddressNonce, Credits)>,
42 fee_strategy: AddressFundsFeeStrategy,
43 signer: &S,
44 user_fee_increase: UserFeeIncrease,
45 prover: &P,
46 memo: [u8; 36],
47 sender_ovk: Option<grovedb_commitment_tree::OutgoingViewingKey>,
48 platform_version: &PlatformVersion,
49) -> Result<StateTransition, ProtocolError> {
50 if fee_strategy.is_empty() {
51 return Err(ProtocolError::ShieldedBuildError(
52 "fee_strategy must have at least one step".to_string(),
53 ));
54 }
55
56 let extra_sighash_data = shield_extra_sighash_data(&inputs, platform_version)?;
59
60 let bundle = build_output_only_bundle(
63 recipient,
64 shield_amount,
65 memo,
66 sender_ovk,
67 0,
68 &extra_sighash_data,
69 prover,
70 )?;
71 let sb = serialize_authorized_bundle(&bundle);
72
73 ShieldTransition::try_from_bundle_with_signer(
74 inputs,
75 sb.actions,
76 sb.value_balance.unsigned_abs(),
77 sb.anchor,
78 sb.proof,
79 sb.binding_signature,
80 fee_strategy,
81 signer,
82 user_fee_increase,
83 platform_version,
84 )
85 .await
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::address_funds::AddressFundsFeeStrategyStep;
92 use crate::address_funds::AddressWitness;
93 use crate::shielded::builder::test_helpers::{test_orchard_address, TestProver};
94 use platform_value::BinaryData;
95
96 #[derive(Debug)]
99 struct DummySigner;
100
101 #[async_trait::async_trait]
102 impl Signer<PlatformAddress> for DummySigner {
103 async fn sign(
104 &self,
105 _key: &PlatformAddress,
106 _data: &[u8],
107 ) -> Result<BinaryData, ProtocolError> {
108 Ok(BinaryData::new(vec![0u8; 65]))
109 }
110
111 async fn sign_create_witness(
112 &self,
113 _key: &PlatformAddress,
114 _data: &[u8],
115 ) -> Result<AddressWitness, ProtocolError> {
116 Ok(AddressWitness::P2pkh {
117 signature: BinaryData::new(vec![0u8; 65]),
118 })
119 }
120
121 fn can_sign_with(&self, _key: &PlatformAddress) -> bool {
122 true
123 }
124 }
125
126 #[tokio::test]
127 async fn test_build_shield_empty_fee_strategy() {
128 let recipient = test_orchard_address();
129 let platform_version = PlatformVersion::latest();
130 let result = build_shield_transition(
131 &recipient,
132 1000,
133 BTreeMap::new(),
134 vec![], &DummySigner,
136 0,
137 &TestProver,
138 [0u8; 36],
139 None,
140 platform_version,
141 )
142 .await;
143
144 assert!(result.is_err());
145 let err = result.unwrap_err().to_string();
146 assert!(
147 err.contains("fee_strategy must have at least one step"),
148 "unexpected error: {}",
149 err
150 );
151 }
152
153 #[tokio::test]
154 async fn test_build_shield_transition_valid() {
155 let recipient = test_orchard_address();
156 let platform_version = PlatformVersion::latest();
157 let input_address = PlatformAddress::P2pkh([1u8; 20]);
159 let mut inputs = BTreeMap::new();
160 inputs.insert(input_address, (0u32, 100_000u64));
161
162 let fee_strategy = vec![AddressFundsFeeStrategyStep::DeductFromInput(0)];
163
164 let result = build_shield_transition(
165 &recipient,
166 50_000,
167 inputs,
168 fee_strategy,
169 &DummySigner,
170 0,
171 &TestProver,
172 [0u8; 36],
173 None,
174 platform_version,
175 )
176 .await;
177
178 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
179 match result.unwrap() {
180 StateTransition::Shield(_) => {} other => panic!("expected Shield variant, got {:?}", other),
182 }
183 }
184
185 #[tokio::test]
190 async fn test_build_shield_multiple_inputs_all_plumbed() {
191 let recipient = test_orchard_address();
194 let platform_version = PlatformVersion::latest();
195
196 let mut inputs = BTreeMap::new();
197 inputs.insert(PlatformAddress::P2pkh([1u8; 20]), (0u32, 100_000u64));
198 inputs.insert(PlatformAddress::P2pkh([2u8; 20]), (0u32, 200_000u64));
199 inputs.insert(PlatformAddress::P2pkh([3u8; 20]), (0u32, 300_000u64));
200
201 let fee_strategy = vec![AddressFundsFeeStrategyStep::DeductFromInput(0)];
202
203 let result = build_shield_transition(
204 &recipient,
205 50_000,
206 inputs,
207 fee_strategy,
208 &DummySigner,
209 0,
210 &TestProver,
211 [0u8; 36],
212 None,
213 platform_version,
214 )
215 .await;
216 assert!(
217 result.is_ok(),
218 "multi-input shield should succeed: {:?}",
219 result.err()
220 );
221 }
222
223 #[tokio::test]
224 async fn test_build_shield_user_fee_increase_non_zero_succeeds() {
225 let recipient = test_orchard_address();
228 let platform_version = PlatformVersion::latest();
229 let input_address = PlatformAddress::P2pkh([5u8; 20]);
230 let mut inputs = BTreeMap::new();
231 inputs.insert(input_address, (0u32, 500_000u64));
232
233 let fee_strategy = vec![AddressFundsFeeStrategyStep::DeductFromInput(0)];
234
235 let result = build_shield_transition(
236 &recipient,
237 100_000,
238 inputs,
239 fee_strategy,
240 &DummySigner,
241 42, &TestProver,
243 [9u8; 36],
244 None,
245 platform_version,
246 )
247 .await;
248 assert!(
249 result.is_ok(),
250 "non-zero user_fee_increase should succeed: {:?}",
251 result.err()
252 );
253 }
254
255 #[tokio::test]
256 async fn test_build_shield_memo_is_fully_plumbed() {
257 let recipient = test_orchard_address();
260 let platform_version = PlatformVersion::latest();
261 let input_address = PlatformAddress::P2pkh([9u8; 20]);
262 let mut inputs = BTreeMap::new();
263 inputs.insert(input_address, (5u32, 200_000u64));
264
265 let fee_strategy = vec![AddressFundsFeeStrategyStep::DeductFromInput(0)];
266 let mut memo = [0u8; 36];
267 for (i, b) in memo.iter_mut().enumerate() {
268 *b = i as u8;
269 }
270
271 let result = build_shield_transition(
272 &recipient,
273 80_000,
274 inputs,
275 fee_strategy,
276 &DummySigner,
277 0,
278 &TestProver,
279 memo,
280 None,
281 platform_version,
282 )
283 .await;
284 assert!(
285 result.is_ok(),
286 "varied memo should succeed: {:?}",
287 result.err()
288 );
289 }
290}