1use grovedb_commitment_tree::OutgoingViewingKey;
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_pool_output_only_extra_sighash_data, OrchardBundleParams};
10use crate::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType;
11use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1;
12use crate::state_transition::batch_transition::BatchTransition;
13use crate::state_transition::StateTransition;
14use crate::ProtocolError;
15use platform_version::version::PlatformVersion;
16
17use super::{build_output_only_bundle, serialize_authorized_bundle, OrchardProver};
18
19#[allow(clippy::too_many_arguments)]
32pub async fn build_token_mint_to_pool_transition<S: Signer<IdentityPublicKey>, P: OrchardProver>(
33 token_id: Identifier,
34 owner_id: Identifier,
35 data_contract_id: Identifier,
36 token_contract_position: u16,
37 recipient: &OrchardAddress,
38 amount: TokenAmount,
39 memo: [u8; 36],
40 sender_ovk: Option<OutgoingViewingKey>,
41 public_note: Option<String>,
42 using_group_info: Option<GroupStateTransitionInfoStatus>,
43 identity_public_key: &IdentityPublicKey,
44 identity_contract_nonce: IdentityNonce,
45 user_fee_increase: UserFeeIncrease,
46 signer: &S,
47 prover: &P,
48 platform_version: &PlatformVersion,
49) -> Result<StateTransition, ProtocolError> {
50 if amount == 0 {
51 return Err(ProtocolError::ShieldedBuildError(
52 "token mint to pool amount must be greater than zero".to_string(),
53 ));
54 }
55 if amount > i64::MAX as u64 {
56 return Err(ProtocolError::ShieldedBuildError(format!(
57 "token mint to pool amount {} exceeds maximum allowed value {}",
58 amount,
59 i64::MAX as u64
60 )));
61 }
62 if matches!(
63 using_group_info,
64 Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(_))
65 ) {
66 return Err(ProtocolError::ShieldedBuildError(
67 "a group action mint to pool is proven once by the proposer; another signer submits the proposer's bundle unchanged instead of building its own".to_string(),
68 ));
69 }
70
71 let extra_sighash_data = token_pool_output_only_extra_sighash_data(
72 TokenTransitionActionType::MintToPool,
73 token_id.as_bytes(),
74 owner_id.as_bytes(),
75 platform_version,
76 )?;
77 let bundle = build_output_only_bundle(
78 recipient,
79 amount,
80 memo,
81 sender_ovk,
82 0,
83 &extra_sighash_data,
84 prover,
85 )?;
86 let sb = serialize_authorized_bundle(&bundle);
87
88 if sb.value_balance != -(amount as i64) {
89 return Err(ProtocolError::ShieldedBuildError(format!(
90 "token mint to pool amount bundle value balance {} does not equal -{}",
91 sb.value_balance, amount
92 )));
93 }
94
95 BatchTransition::new_token_mint_to_pool_transition(
96 token_id,
97 owner_id,
98 data_contract_id,
99 token_contract_position,
100 amount,
101 OrchardBundleParams {
102 actions: sb.actions,
103 anchor: sb.anchor,
104 proof: sb.proof,
105 binding_signature: sb.binding_signature,
106 },
107 public_note,
108 using_group_info,
109 identity_public_key,
110 identity_contract_nonce,
111 user_fee_increase,
112 signer,
113 platform_version,
114 None,
115 )
116 .await
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::group::GroupStateTransitionInfo;
123 use crate::shielded::builder::test_helpers::{
124 test_identity_key, test_orchard_address, DummyIdentitySigner, TestProver,
125 };
126
127 #[tokio::test]
128 async fn rejects_zero_amount() {
129 let key = test_identity_key();
130 let err = build_token_mint_to_pool_transition(
131 Identifier::from([1u8; 32]),
132 Identifier::from([2u8; 32]),
133 Identifier::from([3u8; 32]),
134 0,
135 &test_orchard_address(),
136 0,
137 [0u8; 36],
138 None,
139 None,
140 None,
141 &key,
142 1,
143 0,
144 &DummyIdentitySigner,
145 &TestProver,
146 PlatformVersion::latest(),
147 )
148 .await
149 .expect_err("zero amount must be rejected")
150 .to_string();
151 assert!(err.contains("greater than zero"), "unexpected error: {err}");
152 }
153
154 #[tokio::test]
155 async fn rejects_a_fresh_bundle_for_another_group_signer() {
156 let key = test_identity_key();
157 let err = build_token_mint_to_pool_transition(
158 Identifier::from([1u8; 32]),
159 Identifier::from([2u8; 32]),
160 Identifier::from([3u8; 32]),
161 0,
162 &test_orchard_address(),
163 100,
164 [0u8; 36],
165 None,
166 None,
167 Some(
168 GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(
169 GroupStateTransitionInfo {
170 group_contract_position: 0,
171 action_id: Identifier::from([4u8; 32]),
172 action_is_proposer: false,
173 },
174 ),
175 ),
176 &key,
177 1,
178 0,
179 &DummyIdentitySigner,
180 &TestProver,
181 PlatformVersion::latest(),
182 )
183 .await
184 .expect_err("another signer must reuse the proposer's bundle")
185 .to_string();
186 assert!(
187 err.contains("proven once by the proposer"),
188 "unexpected error: {err}"
189 );
190 }
191}