darkfi_contract_test_harness/
money_token.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2025 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use darkfi::{
20    tx::{ContractCallLeaf, Transaction, TransactionBuilder},
21    zk::halo2::Field,
22    Result,
23};
24use darkfi_money_contract::{
25    client::{
26        auth_token_freeze_v1::AuthTokenFreezeCallBuilder,
27        auth_token_mint_v1::AuthTokenMintCallBuilder, token_mint_v1::TokenMintCallBuilder,
28        MoneyNote, OwnCoin,
29    },
30    model::{
31        CoinAttributes, MoneyAuthTokenFreezeParamsV1, MoneyAuthTokenMintParamsV1, MoneyFeeParamsV1,
32        MoneyTokenMintParamsV1, TokenAttributes,
33    },
34    MoneyFunction, MONEY_CONTRACT_ZKAS_AUTH_TOKEN_MINT_NS_V1, MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1,
35};
36use darkfi_sdk::{
37    crypto::{poseidon_hash, BaseBlind, Blind, FuncId, FuncRef, MerkleNode, MONEY_CONTRACT_ID},
38    dark_tree::DarkTree,
39    pasta::pallas,
40    ContractCall,
41};
42use darkfi_serial::AsyncEncodable;
43use log::debug;
44use rand::rngs::OsRng;
45
46use super::{Holder, TestHarness};
47
48impl TestHarness {
49    /// Mint an arbitrary token for a given recipient using `Money::TokenMint`
50    #[allow(clippy::too_many_arguments)]
51    pub async fn token_mint(
52        &mut self,
53        amount: u64,
54        holder: &Holder,
55        recipient: &Holder,
56        token_blind: BaseBlind,
57        spend_hook: Option<FuncId>,
58        user_data: Option<pallas::Base>,
59        block_height: u32,
60    ) -> Result<(
61        Transaction,
62        MoneyTokenMintParamsV1,
63        MoneyAuthTokenMintParamsV1,
64        Option<MoneyFeeParamsV1>,
65    )> {
66        let wallet = self.holders.get(holder).unwrap();
67        let mint_authority = wallet.token_mint_authority;
68        let rcpt = self.holders.get(recipient).unwrap().keypair.public;
69
70        let (token_mint_pk, token_mint_zkbin) =
71            self.proving_keys.get(MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1).unwrap();
72
73        let (auth_mint_pk, auth_mint_zkbin) =
74            self.proving_keys.get(MONEY_CONTRACT_ZKAS_AUTH_TOKEN_MINT_NS_V1).unwrap();
75
76        // Create the Auth FuncID
77        let auth_func_id = FuncRef {
78            contract_id: *MONEY_CONTRACT_ID,
79            func_code: MoneyFunction::AuthTokenMintV1 as u8,
80        }
81        .to_func_id();
82
83        let (mint_auth_x, mint_auth_y) = mint_authority.public.xy();
84
85        let token_attrs = TokenAttributes {
86            auth_parent: auth_func_id,
87            user_data: poseidon_hash([mint_auth_x, mint_auth_y]),
88            blind: token_blind,
89        };
90
91        let token_id = token_attrs.to_token_id();
92
93        let coin_attrs = CoinAttributes {
94            public_key: rcpt,
95            value: amount,
96            token_id,
97            spend_hook: spend_hook.unwrap_or(FuncId::none()),
98            user_data: user_data.unwrap_or(pallas::Base::ZERO),
99            blind: Blind::random(&mut OsRng),
100        };
101
102        // Create the auth call
103        let builder = AuthTokenMintCallBuilder {
104            coin_attrs: coin_attrs.clone(),
105            token_attrs: token_attrs.clone(),
106            mint_keypair: mint_authority,
107            auth_mint_zkbin: auth_mint_zkbin.clone(),
108            auth_mint_pk: auth_mint_pk.clone(),
109        };
110        let auth_debris = builder.build()?;
111        let mut data = vec![MoneyFunction::AuthTokenMintV1 as u8];
112        auth_debris.params.encode_async(&mut data).await?;
113        let auth_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
114
115        // Create the minting call
116        let builder = TokenMintCallBuilder {
117            coin_attrs,
118            token_attrs,
119            mint_zkbin: token_mint_zkbin.clone(),
120            mint_pk: token_mint_pk.clone(),
121        };
122        let mint_debris = builder.build()?;
123        let mut data = vec![MoneyFunction::TokenMintV1 as u8];
124        mint_debris.params.encode_async(&mut data).await?;
125        let mint_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
126
127        // Create the TransactionBuilder containing above calls
128        let mut tx_builder = TransactionBuilder::new(
129            ContractCallLeaf { call: mint_call, proofs: mint_debris.proofs },
130            vec![DarkTree::new(
131                ContractCallLeaf { call: auth_call, proofs: auth_debris.proofs },
132                vec![],
133                None,
134                None,
135            )],
136        )?;
137
138        // If we have tx fees enabled, make an offering
139        let mut fee_params = None;
140        let mut fee_signature_secrets = None;
141        if self.verify_fees {
142            let mut tx = tx_builder.build()?;
143            let auth_sigs = tx.create_sigs(&[mint_authority.secret])?;
144            let mint_sigs = tx.create_sigs(&[])?;
145            tx.signatures = vec![auth_sigs, mint_sigs];
146
147            let (fee_call, fee_proofs, fee_secrets, _spent_fee_coins, fee_call_params) =
148                self.append_fee_call(holder, tx, block_height, &[]).await?;
149
150            // Append the fee call to the transaction
151            tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
152            fee_signature_secrets = Some(fee_secrets);
153            fee_params = Some(fee_call_params);
154        }
155
156        // Now build the actual transaction and sign it with necessary keys.
157        let mut tx = tx_builder.build()?;
158        let auth_sigs = tx.create_sigs(&[mint_authority.secret])?;
159        let mint_sigs = tx.create_sigs(&[])?;
160        tx.signatures = vec![auth_sigs, mint_sigs];
161        if let Some(fee_signature_secrets) = fee_signature_secrets {
162            let sigs = tx.create_sigs(&fee_signature_secrets)?;
163            tx.signatures.push(sigs);
164        }
165
166        Ok((tx, mint_debris.params, auth_debris.params, fee_params))
167    }
168
169    /// Execute the transaction created by `token_mint()` for a given [`Holder`].
170    ///
171    /// Returns any found [`OwnCoin`]s.
172    #[allow(clippy::too_many_arguments)]
173    pub async fn execute_token_mint_tx(
174        &mut self,
175        holder: &Holder,
176        tx: Transaction,
177        mint_params: &MoneyTokenMintParamsV1,
178        auth_params: &MoneyAuthTokenMintParamsV1,
179        fee_params: &Option<MoneyFeeParamsV1>,
180        block_height: u32,
181        append: bool,
182    ) -> Result<Vec<OwnCoin>> {
183        let wallet = self.holders.get_mut(holder).unwrap();
184
185        // Execute the transaction
186        wallet.add_transaction("money::token_mint", tx, block_height).await?;
187
188        // Iterate over all inputs to mark any spent coins
189        if let Some(ref fee_params) = fee_params {
190            if append {
191                if let Some(spent_coin) = wallet
192                    .unspent_money_coins
193                    .iter()
194                    .find(|x| x.nullifier() == fee_params.input.nullifier)
195                    .cloned()
196                {
197                    debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
198                    wallet
199                        .unspent_money_coins
200                        .retain(|x| x.nullifier() != fee_params.input.nullifier);
201                    wallet.spent_money_coins.push(spent_coin.clone());
202                }
203            }
204        }
205
206        let mut found_owncoins = vec![];
207
208        if append {
209            wallet.money_merkle_tree.append(MerkleNode::from(mint_params.coin.inner()));
210
211            // Attempt to decrypt the encrypted note of the minted token
212            if let Ok(note) = auth_params.enc_note.decrypt::<MoneyNote>(&wallet.keypair.secret) {
213                let owncoin = OwnCoin {
214                    coin: mint_params.coin,
215                    note: note.clone(),
216                    secret: wallet.keypair.secret,
217                    leaf_position: wallet.money_merkle_tree.mark().unwrap(),
218                };
219
220                debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
221                wallet.unspent_money_coins.push(owncoin.clone());
222                found_owncoins.push(owncoin);
223            };
224
225            if let Some(ref fee_params) = fee_params {
226                wallet.money_merkle_tree.append(MerkleNode::from(fee_params.output.coin.inner()));
227
228                // Attempt to decrypt the encrypted note in the fee output
229                if let Ok(note) =
230                    fee_params.output.note.decrypt::<MoneyNote>(&wallet.keypair.secret)
231                {
232                    let owncoin = OwnCoin {
233                        coin: fee_params.output.coin,
234                        note: note.clone(),
235                        secret: wallet.keypair.secret,
236                        leaf_position: wallet.money_merkle_tree.mark().unwrap(),
237                    };
238
239                    debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
240                    wallet.unspent_money_coins.push(owncoin.clone());
241                    found_owncoins.push(owncoin);
242                }
243            }
244        }
245
246        Ok(found_owncoins)
247    }
248
249    /// Freeze the supply of a minted token
250    pub async fn token_freeze(
251        &mut self,
252        holder: &Holder,
253        block_height: u32,
254    ) -> Result<(Transaction, MoneyAuthTokenFreezeParamsV1, Option<MoneyFeeParamsV1>)> {
255        let wallet = self.holders.get(holder).unwrap();
256        let mint_authority = wallet.token_mint_authority;
257
258        let (auth_mint_pk, auth_mint_zkbin) =
259            self.proving_keys.get(MONEY_CONTRACT_ZKAS_AUTH_TOKEN_MINT_NS_V1).unwrap();
260
261        let auth_func_id = FuncRef {
262            contract_id: *MONEY_CONTRACT_ID,
263            func_code: MoneyFunction::AuthTokenMintV1 as u8,
264        }
265        .to_func_id();
266
267        let (mint_auth_x, mint_auth_y) = mint_authority.public.xy();
268        let token_blind = BaseBlind::random(&mut OsRng);
269
270        let token_attrs = TokenAttributes {
271            auth_parent: auth_func_id,
272            user_data: poseidon_hash([mint_auth_x, mint_auth_y]),
273            blind: token_blind,
274        };
275
276        // Create the freeze call
277        let builder = AuthTokenFreezeCallBuilder {
278            mint_keypair: mint_authority,
279            token_attrs,
280            auth_mint_pk: auth_mint_pk.clone(),
281            auth_mint_zkbin: auth_mint_zkbin.clone(),
282        };
283        let freeze_debris = builder.build()?;
284        let mut data = vec![MoneyFunction::AuthTokenFreezeV1 as u8];
285        freeze_debris.params.encode_async(&mut data).await?;
286        let freeze_call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
287
288        // Create the TransactionBuilder containing the above call
289        let mut tx_builder = TransactionBuilder::new(
290            ContractCallLeaf { call: freeze_call, proofs: freeze_debris.proofs },
291            vec![],
292        )?;
293
294        // If we have tx fees enabled, make an offering
295        let mut fee_params = None;
296        let mut fee_signature_secrets = None;
297        if self.verify_fees {
298            let mut tx = tx_builder.build()?;
299            let freeze_sigs = tx.create_sigs(&[mint_authority.secret])?;
300            tx.signatures = vec![freeze_sigs];
301
302            let (fee_call, fee_proofs, fee_secrets, _spent_fee_coins, fee_call_params) =
303                self.append_fee_call(holder, tx, block_height, &[]).await?;
304
305            // Append the fee call to the transaction
306            tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
307            fee_signature_secrets = Some(fee_secrets);
308            fee_params = Some(fee_call_params);
309        }
310
311        // Now build the actual transaction and sign it with necessary keys.
312        let mut tx = tx_builder.build()?;
313        let freeze_sigs = tx.create_sigs(&[mint_authority.secret])?;
314        tx.signatures = vec![freeze_sigs];
315        if let Some(fee_signature_secrets) = fee_signature_secrets {
316            let sigs = tx.create_sigs(&fee_signature_secrets)?;
317            tx.signatures.push(sigs);
318        }
319
320        Ok((tx, freeze_debris.params, fee_params))
321    }
322
323    /// Execute the transaction created by `token_freeze()` for a given [`Holder`].
324    ///
325    /// Returns any found [`OwnCoin`]s.
326    pub async fn execute_token_freeze_tx(
327        &mut self,
328        holder: &Holder,
329        tx: Transaction,
330        _freeze_params: &MoneyAuthTokenFreezeParamsV1,
331        fee_params: &Option<MoneyFeeParamsV1>,
332        block_height: u32,
333        append: bool,
334    ) -> Result<Vec<OwnCoin>> {
335        let wallet = self.holders.get_mut(holder).unwrap();
336
337        // Execute the transaction
338        wallet.add_transaction("money::token_freeze", tx, block_height).await?;
339
340        let mut found_owncoins = vec![];
341        if let Some(ref fee_params) = fee_params {
342            if append {
343                let nullifier = fee_params.input.nullifier.inner();
344                wallet
345                    .money_null_smt
346                    .insert_batch(vec![(nullifier, nullifier)])
347                    .expect("smt.insert_batch()");
348
349                if let Some(spent_coin) = wallet
350                    .unspent_money_coins
351                    .iter()
352                    .find(|x| x.nullifier() == fee_params.input.nullifier)
353                    .cloned()
354                {
355                    debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
356                    wallet
357                        .unspent_money_coins
358                        .retain(|x| x.nullifier() != fee_params.input.nullifier);
359                    wallet.spent_money_coins.push(spent_coin.clone());
360                }
361
362                wallet.money_merkle_tree.append(MerkleNode::from(fee_params.output.coin.inner()));
363
364                // Attempt to decrypt the encrypted note
365                if let Ok(note) =
366                    fee_params.output.note.decrypt::<MoneyNote>(&wallet.keypair.secret)
367                {
368                    let owncoin = OwnCoin {
369                        coin: fee_params.output.coin,
370                        note: note.clone(),
371                        secret: wallet.keypair.secret,
372                        leaf_position: wallet.money_merkle_tree.mark().unwrap(),
373                    };
374
375                    debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
376                    wallet.unspent_money_coins.push(owncoin.clone());
377                    found_owncoins.push(owncoin);
378                }
379            }
380        }
381
382        Ok(found_owncoins)
383    }
384}