1use grovedb_commitment_tree::{
2 Anchor, Builder, BundleType, DashMemo, FullViewingKey, NoteValue, PaymentAddress, Scope,
3 SpendAuthorizingKey,
4};
5
6use crate::address_funds::OrchardAddress;
7use crate::balances::credits::TokenAmount;
8use crate::identity::signer::Signer;
9use crate::identity::IdentityPublicKey;
10use crate::prelude::{Identifier, IdentityNonce, UserFeeIncrease};
11use crate::shielded::{token_shielded_transfer_extra_sighash_data, OrchardBundleParams};
12use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1;
13use crate::state_transition::batch_transition::BatchTransition;
14use crate::state_transition::StateTransition;
15use crate::ProtocolError;
16use platform_version::version::PlatformVersion;
17
18use super::{prove_and_sign_bundle, serialize_authorized_bundle, OrchardProver, SpendableNote};
19
20#[allow(clippy::too_many_arguments)]
30pub async fn build_token_shielded_transfer_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 recipient: &OrchardAddress,
40 transfer_amount: TokenAmount,
41 change_address: &OrchardAddress,
42 fvk: &FullViewingKey,
43 ask: &SpendAuthorizingKey,
44 anchor: Anchor,
45 memo: [u8; 36],
46 identity_public_key: &IdentityPublicKey,
47 identity_contract_nonce: IdentityNonce,
48 user_fee_increase: UserFeeIncrease,
49 signer: &S,
50 prover: &P,
51 platform_version: &PlatformVersion,
52) -> Result<StateTransition, ProtocolError> {
53 if transfer_amount == 0 {
54 return Err(ProtocolError::ShieldedBuildError(
55 "token shielded transfer amount must be greater than zero".to_string(),
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 transfer_amount > total_spent {
68 return Err(ProtocolError::ShieldedBuildError(format!(
69 "token shielded transfer amount {} exceeds total spendable value {}",
70 transfer_amount, total_spent
71 )));
72 }
73 let change_amount = total_spent - transfer_amount;
74
75 let mut builder = Builder::<DashMemo>::new(BundleType::DEFAULT, anchor);
76
77 for spend in spends {
78 builder
79 .add_spend(fvk.clone(), spend.note, spend.merkle_path)
80 .map_err(|e| {
81 ProtocolError::ShieldedBuildError(format!("failed to add spend: {:?}", e))
82 })?;
83 }
84
85 let sender_ovk = fvk.to_ovk(Scope::External);
86
87 builder
88 .add_output(
89 Some(sender_ovk.clone()),
90 PaymentAddress::from(recipient),
91 NoteValue::from_raw(transfer_amount),
92 memo,
93 )
94 .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?;
95
96 if change_amount > 0 {
97 builder
98 .add_output(
99 Some(sender_ovk),
100 PaymentAddress::from(change_address),
101 NoteValue::from_raw(change_amount),
102 [0u8; 36],
103 )
104 .map_err(|e| {
105 ProtocolError::ShieldedBuildError(format!("failed to add change output: {:?}", e))
106 })?;
107 }
108
109 let extra_sighash_data = token_shielded_transfer_extra_sighash_data(
110 &token_id.to_buffer(),
111 &owner_id.to_buffer(),
112 platform_version,
113 )?;
114
115 let bundle = prove_and_sign_bundle(
116 builder,
117 prover,
118 std::slice::from_ref(ask),
119 &extra_sighash_data,
120 )?;
121 let sb = serialize_authorized_bundle(&bundle);
122
123 if sb.value_balance != 0 {
124 return Err(ProtocolError::ShieldedBuildError(format!(
125 "token shielded transfer bundle value balance must be zero, got {}",
126 sb.value_balance
127 )));
128 }
129
130 BatchTransition::new_token_shielded_transfer_transition(
131 token_id,
132 owner_id,
133 data_contract_id,
134 token_contract_position,
135 OrchardBundleParams {
136 actions: sb.actions,
137 anchor: sb.anchor,
138 proof: sb.proof,
139 binding_signature: sb.binding_signature,
140 },
141 identity_public_key,
142 identity_contract_nonce,
143 user_fee_increase,
144 signer,
145 platform_version,
146 None,
147 )
148 .await
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::shielded::builder::test_helpers::{
155 test_identity_key, test_orchard_address, test_spendable_note, DummyIdentitySigner,
156 TestProver,
157 };
158 use grovedb_commitment_tree::SpendingKey;
159
160 #[tokio::test]
161 async fn rejects_amount_above_spendable_value() {
162 let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes");
163 let fvk = FullViewingKey::from(&sk);
164 let ask = SpendAuthorizingKey::from(&sk);
165 let key = test_identity_key();
166 let err = build_token_shielded_transfer_transition(
167 Identifier::from([1u8; 32]),
168 Identifier::from([2u8; 32]),
169 Identifier::from([3u8; 32]),
170 0,
171 vec![test_spendable_note(100)],
172 &test_orchard_address(),
173 1_000,
174 &test_orchard_address(),
175 &fvk,
176 &ask,
177 Anchor::empty_tree(),
178 [0u8; 36],
179 &key,
180 1,
181 0,
182 &DummyIdentitySigner,
183 &TestProver,
184 PlatformVersion::latest(),
185 )
186 .await
187 .expect_err("overspend must be rejected")
188 .to_string();
189 assert!(
190 err.contains("exceeds total spendable value"),
191 "unexpected error: {err}"
192 );
193 }
194}