Skip to main content

dpp/shielded/builder/
token_shield.rs

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/// Builds a `TokenShield` batch transition: proves an outputs-only Orchard bundle paying
19/// `amount` of the token to `recipient` inside the token's own shielded pool, then wraps it in
20/// a batch transition signed by `owner_id`, the identity whose token balance funds the shield.
21///
22/// The identity pays the fee in credits; `amount` tokens leave its balance for the pool at
23/// execution. The bundle has no spends, so its anchor is never checked against a pool, and the
24/// identity signature over the batch only binds it inside *this* batch — the authorized bundle
25/// bytes on their own would verify in any pool. The extra sighash data pins them to this token,
26/// to the shield kind and to `owner_id`. `sender_ovk` lets the sending wallet recover the note
27/// it created (recipient, value, memo) from chain data.
28#[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        // Orchard pads an outputs-only bundle to its two-action minimum.
179        assert_eq!(shield.actions().len(), 2);
180        assert!(!shield.proof().is_empty());
181    }
182}