Skip to main content

dpp/shielded/builder/
token_mint_to_pool.rs

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/// Builds a `TokenMintToPool` 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`, who must be authorized to mint and pays the
22/// fee in credits. `using_group_info` sets up a group action exactly like a transparent mint.
23///
24/// A group action mint is proven once, by the proposer
25/// (`GroupStateTransitionInfoProposer`): the group action pins the digest of the actions and the
26/// sighash binds the proposer as the minter, so every other signer submits the proposer's bundle
27/// unchanged through `new_token_mint_to_pool_transition` with
28/// `GroupStateTransitionInfoOtherSigner`. Asking this builder for a fresh bundle on behalf of
29/// another signer is refused, since consensus would reject it as a modification of the group
30/// action.
31#[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}