1use grovedb_commitment_tree::{Anchor, FullViewingKey, SpendAuthorizingKey};
2
3use crate::address_funds::OrchardAddress;
4use crate::balances::credits::TokenAmount;
5use crate::identity::signer::Signer;
6use crate::identity::IdentityPublicKey;
7use crate::prelude::{Identifier, IdentityNonce, UserFeeIncrease};
8use crate::shielded::{token_unshield_extra_sighash_data, OrchardBundleParams};
9use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1;
10use crate::state_transition::batch_transition::BatchTransition;
11use crate::state_transition::StateTransition;
12use crate::ProtocolError;
13use platform_version::version::PlatformVersion;
14
15use super::{build_spend_bundle, serialize_authorized_bundle, OrchardProver, SpendableNote};
16
17#[allow(clippy::too_many_arguments)]
26pub async fn build_token_unshield_transition<S: Signer<IdentityPublicKey>, P: OrchardProver>(
27 token_id: Identifier,
28 owner_id: Identifier,
29 data_contract_id: Identifier,
30 token_contract_position: u16,
31 spends: Vec<SpendableNote>,
32 recipient_id: Identifier,
33 amount: TokenAmount,
34 change_address: &OrchardAddress,
35 fvk: &FullViewingKey,
36 ask: &SpendAuthorizingKey,
37 anchor: Anchor,
38 memo: [u8; 36],
39 identity_public_key: &IdentityPublicKey,
40 identity_contract_nonce: IdentityNonce,
41 user_fee_increase: UserFeeIncrease,
42 signer: &S,
43 prover: &P,
44 platform_version: &PlatformVersion,
45) -> Result<StateTransition, ProtocolError> {
46 if amount == 0 {
47 return Err(ProtocolError::ShieldedBuildError(
48 "token unshield amount must be greater than zero".to_string(),
49 ));
50 }
51 if amount > i64::MAX as u64 {
52 return Err(ProtocolError::ShieldedBuildError(format!(
53 "token unshield amount {} exceeds maximum allowed value {}",
54 amount,
55 i64::MAX as u64
56 )));
57 }
58
59 let total_spent: u64 = spends
60 .iter()
61 .try_fold(0u64, |total, spend| {
62 total.checked_add(spend.note.value().inner())
63 })
64 .ok_or_else(|| {
65 ProtocolError::ShieldedBuildError("total spendable value overflows u64".to_string())
66 })?;
67 if amount > total_spent {
68 return Err(ProtocolError::ShieldedBuildError(format!(
69 "token unshield amount {} exceeds total spendable value {}",
70 amount, total_spent
71 )));
72 }
73 let change_amount = total_spent - amount;
74
75 let extra_sighash_data = token_unshield_extra_sighash_data(
76 &token_id.to_buffer(),
77 &owner_id.to_buffer(),
78 &recipient_id.to_buffer(),
79 amount,
80 platform_version,
81 )?;
82
83 let bundle = build_spend_bundle(
84 spends,
85 change_address,
86 change_amount,
87 memo,
88 fvk,
89 ask,
90 anchor,
91 prover,
92 &extra_sighash_data,
93 )?;
94 let sb = serialize_authorized_bundle(&bundle);
95
96 if sb.value_balance != amount as i64 {
97 return Err(ProtocolError::ShieldedBuildError(format!(
98 "token unshield bundle value balance {} does not equal the amount {}",
99 sb.value_balance, amount
100 )));
101 }
102
103 BatchTransition::new_token_unshield_transition(
104 token_id,
105 owner_id,
106 data_contract_id,
107 token_contract_position,
108 amount,
109 recipient_id,
110 OrchardBundleParams {
111 actions: sb.actions,
112 anchor: sb.anchor,
113 proof: sb.proof,
114 binding_signature: sb.binding_signature,
115 },
116 identity_public_key,
117 identity_contract_nonce,
118 user_fee_increase,
119 signer,
120 platform_version,
121 None,
122 )
123 .await
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::shielded::builder::test_helpers::{
130 test_identity_key, test_orchard_address, test_spendable_note, DummyIdentitySigner,
131 TestProver,
132 };
133 use grovedb_commitment_tree::SpendingKey;
134
135 fn keys() -> (FullViewingKey, SpendAuthorizingKey) {
136 let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes");
137 (FullViewingKey::from(&sk), SpendAuthorizingKey::from(&sk))
138 }
139
140 #[tokio::test]
141 async fn rejects_amount_above_spendable_value() {
142 let (fvk, ask) = keys();
143 let key = test_identity_key();
144 let err = build_token_unshield_transition(
145 Identifier::from([1u8; 32]),
146 Identifier::from([2u8; 32]),
147 Identifier::from([3u8; 32]),
148 0,
149 vec![test_spendable_note(100)],
150 Identifier::from([4u8; 32]),
151 1_000,
152 &test_orchard_address(),
153 &fvk,
154 &ask,
155 Anchor::empty_tree(),
156 [0u8; 36],
157 &key,
158 1,
159 0,
160 &DummyIdentitySigner,
161 &TestProver,
162 PlatformVersion::latest(),
163 )
164 .await
165 .expect_err("overspend must be rejected")
166 .to_string();
167 assert!(
168 err.contains("exceeds total spendable value"),
169 "unexpected error: {err}"
170 );
171 }
172
173 #[tokio::test]
174 async fn rejects_zero_amount() {
175 let (fvk, ask) = keys();
176 let key = test_identity_key();
177 let err = build_token_unshield_transition(
178 Identifier::from([1u8; 32]),
179 Identifier::from([2u8; 32]),
180 Identifier::from([3u8; 32]),
181 0,
182 vec![test_spendable_note(100)],
183 Identifier::from([4u8; 32]),
184 0,
185 &test_orchard_address(),
186 &fvk,
187 &ask,
188 Anchor::empty_tree(),
189 [0u8; 36],
190 &key,
191 1,
192 0,
193 &DummyIdentitySigner,
194 &TestProver,
195 PlatformVersion::latest(),
196 )
197 .await
198 .expect_err("zero amount must be rejected")
199 .to_string();
200 assert!(err.contains("greater than zero"), "unexpected error: {err}");
201 }
202}