1use crate::address_funds::OrchardAddress;
2use crate::identity::accessors::IdentityGettersV0;
3use crate::identity::signer::Signer;
4use crate::identity::{Identity, IdentityPublicKey};
5use crate::prelude::{IdentityNonce, UserFeeIncrease};
6use crate::shielded::shield_from_identity_extra_sighash_data;
7use crate::state_transition::shield_from_identity_transition::methods::ShieldFromIdentityTransitionMethodsV0;
8use crate::state_transition::shield_from_identity_transition::ShieldFromIdentityTransition;
9use crate::state_transition::StateTransition;
10use crate::ProtocolError;
11use platform_version::version::PlatformVersion;
12
13use super::{build_output_only_bundle, serialize_authorized_bundle, OrchardProver};
14
15#[allow(clippy::too_many_arguments)]
21pub async fn build_shield_from_identity_transition<
22 S: Signer<IdentityPublicKey>,
23 P: OrchardProver,
24>(
25 identity: &Identity,
26 recipient: &OrchardAddress,
27 shield_amount: u64,
28 nonce: IdentityNonce,
29 signer: &S,
30 signing_transfer_key_to_use: Option<&IdentityPublicKey>,
31 user_fee_increase: UserFeeIncrease,
32 prover: &P,
33 memo: [u8; 36],
34 sender_ovk: Option<grovedb_commitment_tree::OutgoingViewingKey>,
35 platform_version: &PlatformVersion,
36) -> Result<StateTransition, ProtocolError> {
37 if shield_amount == 0 {
38 return Err(ProtocolError::ShieldedBuildError(
39 "shield amount must be greater than zero".to_string(),
40 ));
41 }
42
43 let extra_sighash_data =
46 shield_from_identity_extra_sighash_data(&identity.id().to_buffer(), platform_version)?;
47 let bundle = build_output_only_bundle(
48 recipient,
49 shield_amount,
50 memo,
51 sender_ovk,
52 0,
53 &extra_sighash_data,
54 prover,
55 )?;
56 let sb = serialize_authorized_bundle(&bundle);
57
58 ShieldFromIdentityTransition::try_from_bundle_with_identity_signer(
59 identity,
60 sb.value_balance.unsigned_abs(),
61 sb.actions,
62 sb.anchor,
63 sb.proof,
64 sb.binding_signature,
65 user_fee_increase,
66 signer,
67 signing_transfer_key_to_use,
68 nonce,
69 platform_version,
70 )
71 .await
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use crate::address_funds::AddressWitness;
78 use crate::identity::accessors::IdentityGettersV0;
79 use crate::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0;
80 use crate::identity::{KeyType, Purpose, SecurityLevel};
81 use crate::shielded::builder::test_helpers::{test_orchard_address, TestProver};
82 use crate::state_transition::shield_from_identity_transition::accessors::ShieldFromIdentityTransitionAccessorsV0;
83 use crate::state_transition::StateTransitionIdentitySigned;
84 use platform_value::BinaryData;
85 use rand::rngs::StdRng;
86 use rand::SeedableRng;
87
88 #[derive(Debug)]
89 struct DummySigner;
90
91 #[async_trait::async_trait]
92 impl Signer<IdentityPublicKey> for DummySigner {
93 async fn sign(
94 &self,
95 _key: &IdentityPublicKey,
96 _data: &[u8],
97 ) -> Result<BinaryData, ProtocolError> {
98 Ok(BinaryData::new(vec![0u8; 65]))
99 }
100
101 async fn sign_create_witness(
102 &self,
103 _key: &IdentityPublicKey,
104 _data: &[u8],
105 ) -> Result<AddressWitness, ProtocolError> {
106 Err(ProtocolError::ShieldedBuildError(
107 "identity signer never creates address witnesses".to_string(),
108 ))
109 }
110
111 fn can_sign_with(&self, _key: &IdentityPublicKey) -> bool {
112 true
113 }
114 }
115
116 fn identity_with_transfer_key() -> Identity {
117 let platform_version = PlatformVersion::latest();
118 let mut identity =
119 Identity::random_identity(0, Some(1), platform_version).expect("identity");
120 let mut rng = StdRng::seed_from_u64(42);
121 let (key, _) = IdentityPublicKey::random_key_with_known_attributes(
122 0,
123 &mut rng,
124 Purpose::TRANSFER,
125 SecurityLevel::CRITICAL,
126 KeyType::ECDSA_SECP256K1,
127 None,
128 platform_version,
129 )
130 .expect("transfer key");
131 identity.add_public_key(key);
132 identity
133 }
134
135 #[tokio::test]
136 async fn test_build_shield_from_identity_rejects_zero_amount() {
137 let identity = identity_with_transfer_key();
138 let result = build_shield_from_identity_transition(
139 &identity,
140 &test_orchard_address(),
141 0,
142 1,
143 &DummySigner,
144 None,
145 0,
146 &TestProver,
147 [0u8; 36],
148 None,
149 PlatformVersion::latest(),
150 )
151 .await;
152 let err = result
153 .expect_err("zero amount must be rejected")
154 .to_string();
155 assert!(err.contains("greater than zero"), "unexpected error: {err}");
156 }
157
158 #[tokio::test]
159 async fn test_build_shield_from_identity_transition_valid() {
160 let identity = identity_with_transfer_key();
161 let result = build_shield_from_identity_transition(
162 &identity,
163 &test_orchard_address(),
164 50_000,
165 7,
166 &DummySigner,
167 None,
168 3,
169 &TestProver,
170 [0u8; 36],
171 None,
172 PlatformVersion::latest(),
173 )
174 .await;
175 let st = result.expect("expected a built transition");
176 let StateTransition::ShieldFromIdentity(t) = st else {
177 panic!("expected ShieldFromIdentity, got {st:?}");
178 };
179 assert_eq!(t.identity_id(), identity.id());
180 assert_eq!(t.amount(), 50_000);
181 assert_eq!(t.nonce(), 7);
182 assert!(!t.actions().is_empty());
183 let key = identity
184 .get_first_public_key_matching(
185 Purpose::TRANSFER,
186 SecurityLevel::full_range().into(),
187 KeyType::all_key_types().into(),
188 true,
189 )
190 .expect("transfer key");
191 assert_eq!(t.signature_public_key_id(), key.id());
192 }
193}