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 */
1819use 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};
2627use crate::{
28 client::OwnCoin,
29 error::MoneyError,
30 model::{MoneyTransferParamsV1, TokenId},
31};
3233mod builder;
34pub use builder::{
35 TransferCallBuilder, TransferCallClearInput, TransferCallInput, TransferCallOutput,
36 TransferCallSecrets,
37};
3839pub(crate) mod proof;
4041/// 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)> {
45let mut total_value = 0;
46let mut selected = vec![];
4748for coin in coins {
49if total_value >= min_value {
50break
51}
5253 total_value += coin.note.value;
54 selected.push(coin);
55 }
5657if total_value < min_value {
58error!(target: "contract::money::client::transfer::select_coins", "Not enough value to build tx inputs");
59return Err(ClientFailed::NotEnoughValue(total_value).into())
60 }
6162let change_value = total_value - min_value;
6364Ok((selected, change_value))
65}
6667/// 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>)> {
107debug!(target: "contract::money::client::transfer", "Building Money::TransferV1 contract call");
108if value == 0 {
109return Err(ClientFailed::InvalidAmount(value).into())
110 }
111112// 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.
114if half_split && value == 1 {
115return Err(ClientFailed::InvalidAmount(value).into())
116 }
117118if token_id.inner() == pallas::Base::ZERO {
119return Err(ClientFailed::InvalidTokenId(token_id.to_string()).into())
120 }
121122if coins.is_empty() {
123return Err(ClientFailed::VerifyError(MoneyError::TransferMissingInputs.to_string()).into())
124 }
125126// Ensure the coins given to us are all of the same token ID.
127 // The money contract base transfer doesn't allow conversions.
128for coin in &coins {
129if coin.note.token_id != token_id {
130return Err(ClientFailed::InvalidTokenId(coin.note.token_id.to_string()).into())
131 }
132 }
133134let mut inputs = vec![];
135let mut outputs = vec![];
136137let (spent_coins, change_value) = select_coins(coins, value)?;
138if spent_coins.is_empty() {
139error!(target: "contract::money::client::transfer", "Error: No coins selected");
140return Err(ClientFailed::VerifyError(MoneyError::TransferMissingInputs.to_string()).into())
141 }
142143for coin in spent_coins.iter() {
144let 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 };
149150 inputs.push(input);
151 }
152153// Check if we should split the output into two equal halves
154if half_split {
155// Cumpute each half value. If the value is odd,
156 // the remainder(1) will be appended to the second half.
157let mut half = value / 2;
158159// Add the first half, if its not zero
160if 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 }
170171// Append the remainder and add the second half
172half += 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 }
191192if 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 }
202203// Shuffle the outputs
204outputs.shuffle(&mut OsRng);
205206let xfer_builder = TransferCallBuilder {
207 clear_inputs: vec![],
208 inputs,
209 outputs,
210 mint_zkbin,
211 mint_pk,
212 burn_zkbin,
213 burn_pk,
214 };
215216let (params, secrets) = xfer_builder.build()?;
217218Ok((params, secrets, spent_coins))
219}