darkfi_contract_test_harness/
money_transfer.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    Result,
22};
23use darkfi_money_contract::{
24    client::{transfer_v1::make_transfer_call, MoneyNote, OwnCoin},
25    model::{MoneyFeeParamsV1, MoneyTransferParamsV1, TokenId},
26    MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
27};
28use darkfi_sdk::{
29    crypto::{contract_id::MONEY_CONTRACT_ID, MerkleNode},
30    ContractCall,
31};
32use darkfi_serial::AsyncEncodable;
33use log::debug;
34
35use super::{Holder, TestHarness};
36
37impl TestHarness {
38    /// Create a `Money::Transfer` transaction.
39    #[allow(clippy::too_many_arguments)]
40    pub async fn transfer(
41        &mut self,
42        amount: u64,
43        holder: &Holder,
44        recipient: &Holder,
45        owncoins: &[OwnCoin],
46        token_id: TokenId,
47        block_height: u32,
48        half_split: bool,
49    ) -> Result<(Transaction, (MoneyTransferParamsV1, Option<MoneyFeeParamsV1>), Vec<OwnCoin>)>
50    {
51        let wallet = self.holders.get(holder).unwrap();
52        let rcpt = self.holders.get(recipient).unwrap().keypair.public;
53
54        let (mint_pk, mint_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_MINT_NS_V1).unwrap();
55        let (burn_pk, burn_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_BURN_NS_V1).unwrap();
56
57        // Create the transfer call
58        let (params, secrets, mut spent_coins) = make_transfer_call(
59            wallet.keypair,
60            rcpt,
61            amount,
62            token_id,
63            owncoins.to_owned(),
64            wallet.money_merkle_tree.clone(),
65            None,
66            None,
67            mint_zkbin.clone(),
68            mint_pk.clone(),
69            burn_zkbin.clone(),
70            burn_pk.clone(),
71            half_split,
72        )?;
73
74        // Encode the call
75        let mut data = vec![MoneyFunction::TransferV1 as u8];
76        params.encode_async(&mut data).await?;
77        let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
78
79        // Create the TransactionBuilder containing the `Transfer` call
80        let mut tx_builder =
81            TransactionBuilder::new(ContractCallLeaf { call, proofs: secrets.proofs }, vec![])?;
82
83        // If we have tx fees enabled, we first have to execute the fee-less tx to gather its
84        // used gas, and then we feed it into the fee-creating function.
85        // We also tell it about any spent coins so we don't accidentally reuse them in the
86        // fee call.
87        // TODO: We have to build a proper coin selection algorithm so that we can utilize
88        // the Money::Transfer to merge any coins which would give us a coin with enough
89        // value for paying the transaction fee.
90        let mut fee_params = None;
91        let mut fee_signature_secrets = None;
92        if self.verify_fees {
93            let mut tx = tx_builder.build()?;
94            let sigs = tx.create_sigs(&secrets.signature_secrets)?;
95            tx.signatures = vec![sigs];
96
97            let (fee_call, fee_proofs, fee_secrets, spent_fee_coins, fee_call_params) =
98                self.append_fee_call(holder, tx, block_height, &spent_coins).await?;
99
100            // Append the fee call to the transaction
101            tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
102            fee_signature_secrets = Some(fee_secrets);
103            spent_coins.extend_from_slice(&spent_fee_coins);
104            fee_params = Some(fee_call_params);
105        }
106
107        // Now build the actual transaction and sign it with all necessary keys.
108        let mut tx = tx_builder.build()?;
109        let sigs = tx.create_sigs(&secrets.signature_secrets)?;
110        tx.signatures = vec![sigs];
111        if let Some(fee_signature_secrets) = fee_signature_secrets {
112            let sigs = tx.create_sigs(&fee_signature_secrets)?;
113            tx.signatures.push(sigs);
114        }
115
116        Ok((tx, (params, fee_params), spent_coins))
117    }
118
119    /// Execute a `Money::Transfer` transaction for a given [`Holder`].
120    ///
121    /// Returns any found [`OwnCoin`]s.
122    pub async fn execute_transfer_tx(
123        &mut self,
124        holder: &Holder,
125        tx: Transaction,
126        call_params: &MoneyTransferParamsV1,
127        fee_params: &Option<MoneyFeeParamsV1>,
128        block_height: u32,
129        append: bool,
130    ) -> Result<Vec<OwnCoin>> {
131        let wallet = self.holders.get_mut(holder).unwrap();
132
133        // Execute the transaction
134        wallet.add_transaction("money::transfer", tx, block_height).await?;
135
136        // Iterate over call inputs to mark any spent coins
137        let nullifiers =
138            call_params.inputs.iter().map(|i| i.nullifier.inner()).map(|l| (l, l)).collect();
139        wallet.money_null_smt.insert_batch(nullifiers).expect("smt.insert_batch()");
140
141        let mut found_owncoins = vec![];
142        if append {
143            for input in &call_params.inputs {
144                if let Some(spent_coin) = wallet
145                    .unspent_money_coins
146                    .iter()
147                    .find(|x| x.nullifier() == input.nullifier)
148                    .cloned()
149                {
150                    debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
151                    wallet.unspent_money_coins.retain(|x| x.nullifier() != input.nullifier);
152                    wallet.spent_money_coins.push(spent_coin.clone());
153                }
154            }
155
156            // Iterate over call outputs to find any new OwnCoins
157            for output in &call_params.outputs {
158                wallet.money_merkle_tree.append(MerkleNode::from(output.coin.inner()));
159
160                // Attempt to decrypt the output note to see if this is a coin for the holder.
161                let Ok(note) = output.note.decrypt::<MoneyNote>(&wallet.keypair.secret) else {
162                    continue
163                };
164
165                let owncoin = OwnCoin {
166                    coin: output.coin,
167                    note: note.clone(),
168                    secret: wallet.keypair.secret,
169                    leaf_position: wallet.money_merkle_tree.mark().unwrap(),
170                };
171
172                debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
173                wallet.unspent_money_coins.push(owncoin.clone());
174                found_owncoins.push(owncoin);
175            }
176        }
177
178        // Handle fee call
179        if let Some(ref fee_params) = fee_params {
180            // Process call input to mark any spent coins
181            let nullifier = fee_params.input.nullifier.inner();
182            wallet
183                .money_null_smt
184                .insert_batch(vec![(nullifier, nullifier)])
185                .expect("smt.insert_batch()");
186
187            if append {
188                if let Some(spent_coin) = wallet
189                    .unspent_money_coins
190                    .iter()
191                    .find(|x| x.nullifier() == fee_params.input.nullifier)
192                    .cloned()
193                {
194                    debug!("Found spent OwnCoin({}) for {:?}", spent_coin.coin, holder);
195                    wallet
196                        .unspent_money_coins
197                        .retain(|x| x.nullifier() != fee_params.input.nullifier);
198                    wallet.spent_money_coins.push(spent_coin.clone());
199                }
200
201                // Process call output to find any new OwnCoins
202                wallet.money_merkle_tree.append(MerkleNode::from(fee_params.output.coin.inner()));
203
204                // Attempt to decrypt the output note to see if this is a coin for the holder.
205                if let Ok(note) =
206                    fee_params.output.note.decrypt::<MoneyNote>(&wallet.keypair.secret)
207                {
208                    let owncoin = OwnCoin {
209                        coin: fee_params.output.coin,
210                        note: note.clone(),
211                        secret: wallet.keypair.secret,
212                        leaf_position: wallet.money_merkle_tree.mark().unwrap(),
213                    };
214
215                    debug!("Found new OwnCoin({}) for {:?}", owncoin.coin, holder);
216                    wallet.unspent_money_coins.push(owncoin.clone());
217                    found_owncoins.push(owncoin);
218                };
219            }
220        }
221
222        Ok(found_owncoins)
223    }
224}