Skip to main content

dpp/shielded/builder/
token_claim_to_pool.rs

1use grovedb_commitment_tree::OutgoingViewingKey;
2
3use crate::address_funds::OrchardAddress;
4use crate::balances::credits::TokenAmount;
5use crate::data_contract::associated_token::token_distribution_key::TokenDistributionType;
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/// Builds a `TokenClaimToPool` batch transition: proves an outputs-only Orchard bundle creating
20/// `amount` of the token as a note for `recipient` inside the token's shielded pool, then wraps
21/// it in a batch transition signed by `owner_id`, the claimant, who pays the fee in credits.
22///
23/// `amount` must be exactly what consensus will pay out: the next pre-programmed release, or for
24/// a perpetual distribution the rewards from the last claim up to `claim_up_to` (a cycle-aligned
25/// moment in the distribution's own unit, not after the current interval). A mismatch is a paid
26/// failure, so the client should compute the amount from the same state it read the moment from.
27#[allow(clippy::too_many_arguments)]
28pub async fn build_token_claim_to_pool_transition<
29    S: Signer<IdentityPublicKey>,
30    P: OrchardProver,
31>(
32    token_id: Identifier,
33    owner_id: Identifier,
34    data_contract_id: Identifier,
35    token_contract_position: u16,
36    recipient: &OrchardAddress,
37    amount: TokenAmount,
38    distribution_type: TokenDistributionType,
39    claim_up_to: Option<u64>,
40    memo: [u8; 36],
41    sender_ovk: Option<OutgoingViewingKey>,
42    public_note: Option<String>,
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 claim 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 claim to pool amount {} exceeds maximum allowed value {}",
58            amount,
59            i64::MAX as u64
60        )));
61    }
62    if distribution_type == TokenDistributionType::Perpetual && claim_up_to.is_none() {
63        return Err(ProtocolError::ShieldedBuildError(
64            "a perpetual claim into the pool must name the moment it claims up to".to_string(),
65        ));
66    }
67
68    let extra_sighash_data = token_pool_output_only_extra_sighash_data(
69        TokenTransitionActionType::ClaimToPool,
70        token_id.as_bytes(),
71        owner_id.as_bytes(),
72        platform_version,
73    )?;
74    let bundle = build_output_only_bundle(
75        recipient,
76        amount,
77        memo,
78        sender_ovk,
79        0,
80        &extra_sighash_data,
81        prover,
82    )?;
83    let sb = serialize_authorized_bundle(&bundle);
84
85    if sb.value_balance != -(amount as i64) {
86        return Err(ProtocolError::ShieldedBuildError(format!(
87            "token claim to pool amount bundle value balance {} does not equal -{}",
88            sb.value_balance, amount
89        )));
90    }
91
92    BatchTransition::new_token_claim_to_pool_transition(
93        token_id,
94        owner_id,
95        data_contract_id,
96        token_contract_position,
97        distribution_type,
98        claim_up_to,
99        OrchardBundleParams {
100            actions: sb.actions,
101            anchor: sb.anchor,
102            proof: sb.proof,
103            binding_signature: sb.binding_signature,
104        },
105        public_note,
106        identity_public_key,
107        identity_contract_nonce,
108        user_fee_increase,
109        signer,
110        platform_version,
111        None,
112    )
113    .await
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::shielded::builder::test_helpers::{
120        test_identity_key, test_orchard_address, DummyIdentitySigner, TestProver,
121    };
122
123    #[tokio::test]
124    async fn perpetual_claim_needs_a_moment() {
125        let key = test_identity_key();
126        let err = build_token_claim_to_pool_transition(
127            Identifier::from([1u8; 32]),
128            Identifier::from([2u8; 32]),
129            Identifier::from([3u8; 32]),
130            0,
131            &test_orchard_address(),
132            100,
133            TokenDistributionType::Perpetual,
134            None,
135            [0u8; 36],
136            None,
137            None,
138            &key,
139            1,
140            0,
141            &DummyIdentitySigner,
142            &TestProver,
143            PlatformVersion::latest(),
144        )
145        .await
146        .expect_err("perpetual claim without a moment must be rejected")
147        .to_string();
148        assert!(err.contains("claims up to"), "unexpected error: {err}");
149    }
150}