darkfi_money_contract/client/transfer_v1/
mod.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::{zk::ProvingKey, zkas::ZkBinary, ClientFailed, Result};
20use darkfi_sdk::{
21    crypto::{pasta_prelude::*, Blind, FuncId, Keypair, MerkleTree, PublicKey},
22    pasta::pallas,
23};
24use log::{debug, error};
25use rand::{prelude::SliceRandom, rngs::OsRng};
26
27use crate::{
28    client::OwnCoin,
29    error::MoneyError,
30    model::{MoneyTransferParamsV1, TokenId},
31};
32
33mod builder;
34pub use builder::{
35    TransferCallBuilder, TransferCallClearInput, TransferCallInput, TransferCallOutput,
36    TransferCallSecrets,
37};
38
39pub(crate) mod proof;
40
41/// Select coins from `coins` of at least `min_value` in total.
42/// Different strategies can be used. This function uses the dumb strategy
43/// of selecting coins until we reach `min_value`.
44pub fn select_coins(coins: Vec<OwnCoin>, min_value: u64) -> Result<(Vec<OwnCoin>, u64)> {
45    let mut total_value = 0;
46    let mut selected = vec![];
47
48    for coin in coins {
49        if total_value >= min_value {
50            break
51        }
52
53        total_value += coin.note.value;
54        selected.push(coin);
55    }
56
57    if total_value < min_value {
58        error!(target: "contract::money::client::transfer::select_coins", "Not enough value to build tx inputs");
59        return Err(ClientFailed::NotEnoughValue(total_value).into())
60    }
61
62    let change_value = total_value - min_value;
63
64    Ok((selected, change_value))
65}
66
67/// Make a simple anonymous transfer call.
68///
69/// * `keypair`: Caller's keypair
70/// * `recipient`: Recipient's public key
71/// * `value`: Amount that we want to send to the recipient
72/// * `token_id`: Token ID that we want to send to the recipient
73/// * `coins`: Set of `OwnCoin` we're given to use in this builder
74/// * `tree`: Merkle tree of coins used to create inclusion proofs
75/// * `output_spend_hook: Optional contract spend hook to use in
76///   the output, not applicable to the change
77/// * `output_user_data: Optional user data to use in the output,
78///   not applicable to the change
79/// * `mint_zkbin`: `Mint_V1` zkas circuit ZkBinary
80/// * `mint_pk`: Proving key for the `Mint_V1` zk circuit
81/// * `burn_zkbin`: `Burn_V1` zkas circuit ZkBinary
82/// * `burn_pk`: Proving key for the `Burn_V1` zk circuit
83/// * `half_split`: Flag indicating to split the output coin into
84///   two equal halves.
85///
86/// Returns a tuple of:
87///
88/// * The actual call data
89/// * Secret values such as blinds
90/// * A list of the spent coins
91#[allow(clippy::too_many_arguments)]
92pub fn make_transfer_call(
93    keypair: Keypair,
94    recipient: PublicKey,
95    value: u64,
96    token_id: TokenId,
97    coins: Vec<OwnCoin>,
98    tree: MerkleTree,
99    output_spend_hook: Option<FuncId>,
100    output_user_data: Option<pallas::Base>,
101    mint_zkbin: ZkBinary,
102    mint_pk: ProvingKey,
103    burn_zkbin: ZkBinary,
104    burn_pk: ProvingKey,
105    half_split: bool,
106) -> Result<(MoneyTransferParamsV1, TransferCallSecrets, Vec<OwnCoin>)> {
107    debug!(target: "contract::money::client::transfer", "Building Money::TransferV1 contract call");
108    if value == 0 {
109        return Err(ClientFailed::InvalidAmount(value).into())
110    }
111
112    // Using integer division via `half_split` causes the evaluation of `1 / 2` which is equal to
113    // 0. This would cause us to send two outputs of 0 value which is not what we want.
114    if half_split && value == 1 {
115        return Err(ClientFailed::InvalidAmount(value).into())
116    }
117
118    if token_id.inner() == pallas::Base::ZERO {
119        return Err(ClientFailed::InvalidTokenId(token_id.to_string()).into())
120    }
121
122    if coins.is_empty() {
123        return Err(ClientFailed::VerifyError(MoneyError::TransferMissingInputs.to_string()).into())
124    }
125
126    // Ensure the coins given to us are all of the same token ID.
127    // The money contract base transfer doesn't allow conversions.
128    for coin in &coins {
129        if coin.note.token_id != token_id {
130            return Err(ClientFailed::InvalidTokenId(coin.note.token_id.to_string()).into())
131        }
132    }
133
134    let mut inputs = vec![];
135    let mut outputs = vec![];
136
137    let (spent_coins, change_value) = select_coins(coins, value)?;
138    if spent_coins.is_empty() {
139        error!(target: "contract::money::client::transfer", "Error: No coins selected");
140        return Err(ClientFailed::VerifyError(MoneyError::TransferMissingInputs.to_string()).into())
141    }
142
143    for coin in spent_coins.iter() {
144        let input = TransferCallInput {
145            coin: coin.clone(),
146            merkle_path: tree.witness(coin.leaf_position, 0).unwrap(),
147            user_data_blind: Blind::random(&mut OsRng),
148        };
149
150        inputs.push(input);
151    }
152
153    // Check if we should split the output into two equal halves
154    if half_split {
155        // Cumpute each half value. If the value is odd,
156        // the remainder(1) will be appended to the second half.
157        let mut half = value / 2;
158
159        // Add the first half, if its not zero
160        if half != 0 {
161            outputs.push(TransferCallOutput {
162                public_key: recipient,
163                value: half,
164                token_id,
165                spend_hook: output_spend_hook.unwrap_or(FuncId::none()),
166                user_data: output_user_data.unwrap_or(pallas::Base::ZERO),
167                blind: Blind::random(&mut OsRng),
168            });
169        }
170
171        // Append the remainder and add the second half
172        half += value % 2;
173        outputs.push(TransferCallOutput {
174            public_key: recipient,
175            value: half,
176            token_id,
177            spend_hook: output_spend_hook.unwrap_or(FuncId::none()),
178            user_data: output_user_data.unwrap_or(pallas::Base::ZERO),
179            blind: Blind::random(&mut OsRng),
180        });
181    } else {
182        outputs.push(TransferCallOutput {
183            public_key: recipient,
184            value,
185            token_id,
186            spend_hook: output_spend_hook.unwrap_or(FuncId::none()),
187            user_data: output_user_data.unwrap_or(pallas::Base::ZERO),
188            blind: Blind::random(&mut OsRng),
189        });
190    }
191
192    if change_value > 0 {
193        outputs.push(TransferCallOutput {
194            public_key: keypair.public,
195            value: change_value,
196            token_id,
197            spend_hook: FuncId::none(),
198            user_data: pallas::Base::ZERO,
199            blind: Blind::random(&mut OsRng),
200        });
201    }
202
203    // Shuffle the outputs
204    outputs.shuffle(&mut OsRng);
205
206    let xfer_builder = TransferCallBuilder {
207        clear_inputs: vec![],
208        inputs,
209        outputs,
210        mint_zkbin,
211        mint_pk,
212        burn_zkbin,
213        burn_pk,
214    };
215
216    let (params, secrets) = xfer_builder.build()?;
217
218    Ok((params, secrets, spent_coins))
219}