darkfi_money_contract/client/transfer_v1/
mod.rs1use 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
41pub 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#[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 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 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 if half_split {
155 let mut half = value / 2;
158
159 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 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 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}