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