1use grovedb_commitment_tree::OutgoingViewingKey;
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_pool_output_only_extra_sighash_data, OrchardBundleParams};
9use crate::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType;
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_output_only_bundle, serialize_authorized_bundle, OrchardProver};
17
18#[allow(clippy::too_many_arguments)]
29pub async fn build_token_shield_transition<S: Signer<IdentityPublicKey>, P: OrchardProver>(
30 token_id: Identifier,
31 owner_id: Identifier,
32 data_contract_id: Identifier,
33 token_contract_position: u16,
34 recipient: &OrchardAddress,
35 amount: TokenAmount,
36 memo: [u8; 36],
37 sender_ovk: Option<OutgoingViewingKey>,
38 identity_public_key: &IdentityPublicKey,
39 identity_contract_nonce: IdentityNonce,
40 user_fee_increase: UserFeeIncrease,
41 signer: &S,
42 prover: &P,
43 platform_version: &PlatformVersion,
44) -> Result<StateTransition, ProtocolError> {
45 if amount == 0 {
46 return Err(ProtocolError::ShieldedBuildError(
47 "token shield amount must be greater than zero".to_string(),
48 ));
49 }
50 if amount > i64::MAX as u64 {
51 return Err(ProtocolError::ShieldedBuildError(format!(
52 "token shield amount {} exceeds maximum allowed value {}",
53 amount,
54 i64::MAX as u64
55 )));
56 }
57
58 let extra_sighash_data = token_pool_output_only_extra_sighash_data(
59 TokenTransitionActionType::Shield,
60 token_id.as_bytes(),
61 owner_id.as_bytes(),
62 platform_version,
63 )?;
64 let bundle = build_output_only_bundle(
65 recipient,
66 amount,
67 memo,
68 sender_ovk,
69 0,
70 &extra_sighash_data,
71 prover,
72 )?;
73 let sb = serialize_authorized_bundle(&bundle);
74
75 if sb.value_balance != -(amount as i64) {
76 return Err(ProtocolError::ShieldedBuildError(format!(
77 "token shield amount bundle value balance {} does not equal -{}",
78 sb.value_balance, amount
79 )));
80 }
81
82 BatchTransition::new_token_shield_transition(
83 token_id,
84 owner_id,
85 data_contract_id,
86 token_contract_position,
87 amount,
88 OrchardBundleParams {
89 actions: sb.actions,
90 anchor: sb.anchor,
91 proof: sb.proof,
92 binding_signature: sb.binding_signature,
93 },
94 identity_public_key,
95 identity_contract_nonce,
96 user_fee_increase,
97 signer,
98 platform_version,
99 None,
100 )
101 .await
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::shielded::builder::test_helpers::{
108 test_identity_key, test_orchard_address, DummyIdentitySigner, TestProver,
109 };
110 use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0;
111 use crate::state_transition::batch_transition::batched_transition::token_transition::TokenTransition;
112 use crate::state_transition::batch_transition::batched_transition::BatchedTransitionRef;
113 use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors;
114 use crate::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods;
115 use crate::state_transition::batch_transition::token_shield_transition::v0::v0_methods::TokenShieldTransitionV0Methods;
116
117 #[tokio::test]
118 async fn rejects_zero_amount() {
119 let key = test_identity_key();
120 let err = build_token_shield_transition(
121 Identifier::from([1u8; 32]),
122 Identifier::from([2u8; 32]),
123 Identifier::from([3u8; 32]),
124 0,
125 &test_orchard_address(),
126 0,
127 [0u8; 36],
128 None,
129 &key,
130 1,
131 0,
132 &DummyIdentitySigner,
133 &TestProver,
134 PlatformVersion::latest(),
135 )
136 .await
137 .expect_err("zero amount must be rejected")
138 .to_string();
139 assert!(err.contains("greater than zero"), "unexpected error: {err}");
140 }
141
142 #[tokio::test]
143 async fn builds_a_signed_batch_carrying_the_proved_bundle() {
144 let key = test_identity_key();
145 let token_id = Identifier::from([1u8; 32]);
146 let owner_id = Identifier::from([2u8; 32]);
147 let state_transition = build_token_shield_transition(
148 token_id,
149 owner_id,
150 Identifier::from([3u8; 32]),
151 0,
152 &test_orchard_address(),
153 50_000,
154 [0u8; 36],
155 None,
156 &key,
157 7,
158 0,
159 &DummyIdentitySigner,
160 &TestProver,
161 PlatformVersion::latest(),
162 )
163 .await
164 .expect("shield transition");
165
166 assert_eq!(state_transition.owner_id(), Some(owner_id));
167 let StateTransition::Batch(batch) = state_transition else {
168 panic!("expected a batch transition");
169 };
170 let transitions: Vec<_> = batch.transitions_iter().collect();
171 assert_eq!(transitions.len(), 1);
172 let BatchedTransitionRef::Token(TokenTransition::Shield(shield)) = transitions[0] else {
173 panic!("expected a token shield transition");
174 };
175 assert_eq!(shield.amount(), 50_000);
176 assert_eq!(shield.base().token_id(), token_id);
177 assert_eq!(shield.base().identity_contract_nonce(), 7);
178 assert_eq!(shield.actions().len(), 2);
180 assert!(!shield.proof().is_empty());
181 }
182}