1use crate::wallets::common::{sign_coin_spends, DerivationRecord};
2use async_trait::async_trait;
3use blst::min_pk::SecretKey;
4use dashmap::mapref::one::Ref;
5use dashmap::DashMap;
6use dg_xch_core::blockchain::announcement::Announcement;
7use dg_xch_core::blockchain::coin::Coin;
8use dg_xch_core::blockchain::coin_record::{CatCoinRecord, CoinRecord};
9use dg_xch_core::blockchain::coin_spend::CoinSpend;
10use dg_xch_core::blockchain::condition_opcode::ConditionOpcode;
11use dg_xch_core::blockchain::sized_bytes::{Bytes32, Bytes48};
12use dg_xch_core::blockchain::spend_bundle::SpendBundle;
13use dg_xch_core::blockchain::transaction_record::{TransactionRecord, TransactionType};
14use dg_xch_core::blockchain::wallet_type::{AmountWithPuzzleHash, WalletType};
15use dg_xch_core::clvm::program::{Program, SerializedProgram};
16use dg_xch_core::clvm::utils::INFINITE_COST;
17use dg_xch_core::consensus::constants::ConsensusConstants;
18use dg_xch_core::traits::SizedBytes;
19use dg_xch_core::utils::hash_256;
20use dg_xch_keys::{master_sk_to_wallet_sk, master_sk_to_wallet_sk_unhardened};
21use dg_xch_puzzles::p2_delegated_puzzle_or_hidden_puzzle::{
22 puzzle_for_pk, puzzle_hash_for_pk, solution_for_conditions,
23};
24use dg_xch_puzzles::utils::{
25 make_assert_absolute_seconds_exceeds_condition, make_assert_coin_announcement,
26 make_assert_puzzle_announcement, make_create_coin_announcement, make_create_coin_condition,
27 make_create_puzzle_announcement, make_reserve_fee_condition,
28};
29use dg_xch_serialize::{ChiaProtocolVersion, ChiaSerialize};
30use log::{debug, info};
31use num_traits::ToPrimitive;
32use rand::prelude::StdRng;
33use rand::{Rng, SeedableRng};
34use std::cmp::max;
35use std::collections::{HashMap, HashSet};
36use std::future::Future;
37use std::io::{Error, ErrorKind};
38use std::sync::Arc;
39use std::time::{SystemTime, UNIX_EPOCH};
40use tokio::sync::Mutex;
41
42pub mod common;
43pub mod memory_wallet;
44pub mod plotnft_utils;
45
46#[derive(Default)]
47pub struct SecretKeyStore {
48 keys: DashMap<Bytes48, Bytes32>,
49}
50impl SecretKeyStore {
51 #[must_use]
52 pub fn save_secret_key(&self, secret_key: &SecretKey) -> Option<Bytes32> {
53 self.keys.insert(
54 Bytes48::from(secret_key.sk_to_pk()),
55 Bytes32::from(secret_key.to_bytes()),
56 )
57 }
58 #[must_use]
59 pub fn secret_key_for_public_key(&self, pub_key: &Bytes48) -> Option<Ref<Bytes48, Bytes32>> {
60 self.keys.get(pub_key)
61 }
62}
63
64#[derive(Eq, PartialEq, Hash, Clone, Copy)]
65struct Primary {
66 puzzle_hash: Bytes32,
67 amount: u64,
68}
69
70pub struct WalletInfo<T: WalletStore> {
71 pub id: u32,
72 pub name: String,
73 pub wallet_type: WalletType,
74 pub constants: Arc<ConsensusConstants>,
75 pub master_sk: SecretKey,
76 pub wallet_store: Arc<Mutex<T>>,
77 pub data: String, }
79
80#[async_trait]
81pub trait WalletStore {
82 fn get_master_sk(&self) -> &SecretKey;
83 fn standard_coins(&self) -> Arc<Mutex<Vec<CoinRecord>>>;
84 fn cat_coins(&self) -> Arc<Mutex<Vec<CatCoinRecord>>>;
85 fn secret_key_store(&self) -> &SecretKeyStore;
86 fn current_index(&self) -> u32;
87 fn next_index(&self) -> u32;
88 async fn get_confirmed_balance(&self) -> u128;
89 async fn get_unconfirmed_balance(&self) -> u128;
90 async fn get_pending_change_balance(&self) -> u128;
91 async fn populate_secret_key_for_puzzle_hash(
92 &self,
93 puz_hash: &Bytes32,
94 ) -> Result<Bytes48, Error>;
95
96 async fn add_puzzle_hash_and_keys(
97 &self,
98 puzzle_hash: Bytes32,
99 keys: (Bytes32, Bytes48),
100 ) -> Option<(Bytes32, Bytes48)>;
101 async fn get_max_send_amount(&self) -> u128 {
102 let unspent: Vec<CoinRecord> = self
103 .standard_coins()
104 .lock()
105 .await
106 .iter()
107 .filter(|v| !v.spent)
108 .copied()
109 .collect();
110 if unspent.is_empty() {
111 0
112 } else {
113 unspent.iter().map(|v| v.coin.amount as u128).sum()
114 }
115 }
116
117 async fn get_spendable_balance(&self) -> u128 {
118 self.get_max_send_amount().await
119 }
120 async fn get_puzzle_hashes(
121 &self,
122 start: u32,
123 count: u32,
124 hardened: bool,
125 ) -> Result<Vec<Bytes32>, Error> {
126 let mut puz_hashes = vec![];
127 for i in start..start + count {
128 puz_hashes.push(
129 self.get_derivation_record_at_index(i, hardened)
130 .await?
131 .puzzle_hash,
132 );
133 }
134 Ok(puz_hashes)
135 }
136 fn wallet_sk(&self, index: u32, hardened: bool) -> Result<SecretKey, Error> {
137 if hardened {
138 master_sk_to_wallet_sk(self.get_master_sk(), index)
139 .map_err(|e| Error::new(ErrorKind::InvalidInput, format!("MasterKey: {e:?}")))
140 } else {
141 master_sk_to_wallet_sk_unhardened(self.get_master_sk(), index)
142 .map_err(|e| Error::new(ErrorKind::InvalidInput, format!("MasterKey: {e:?}")))
143 }
144 }
145 async fn get_derivation_record_at_index(
146 &self,
147 index: u32,
148 hardened: bool,
149 ) -> Result<DerivationRecord, Error> {
150 let wallet_sk = self.wallet_sk(index, hardened)?;
151 let _ = self.secret_key_store().save_secret_key(&wallet_sk);
152 let pubkey = Bytes48::from(wallet_sk.sk_to_pk().to_bytes());
153 let puzzle_hash = puzzle_hash_for_pk(pubkey)?;
154 self.add_puzzle_hash_and_keys(puzzle_hash, (Bytes32::from(wallet_sk), pubkey))
155 .await;
156 Ok(DerivationRecord {
157 index,
158 puzzle_hash,
159 pubkey,
160 wallet_type: WalletType::StandardWallet,
161 wallet_id: 1,
162 hardened,
163 })
164 }
165 async fn get_unused_derivation_record(
166 &self,
167 hardened: bool,
168 ) -> Result<DerivationRecord, Error> {
169 self.get_derivation_record_at_index(self.next_index(), hardened)
170 .await
171 }
172 async fn get_derivation_record(&self, hardened: bool) -> Result<DerivationRecord, Error> {
173 self.get_derivation_record_at_index(self.current_index(), hardened)
174 .await
175 }
176
177 #[allow(clippy::too_many_lines)]
178 async fn select_coins(
179 &self,
180 amount: u64,
181 exclude: Option<&[Coin]>,
182 min_coin_amount: Option<u64>,
183 max_coin_amount: u64,
184 exclude_coin_amounts: Option<&[u64]>,
185 ) -> Result<HashSet<Coin>, Error> {
186 let spendable_amount = self.get_spendable_balance().await;
187 let exclude = exclude.unwrap_or_default();
188 let min_coin_amount = min_coin_amount.unwrap_or(0);
189 let exclude_coin_amounts = exclude_coin_amounts.unwrap_or_default();
190 if amount as u128 > spendable_amount {
191 Err(Error::new(ErrorKind::InvalidInput, format!("Can't select amount higher than our spendable balance. Amount: {amount}, spendable: {spendable_amount}")))
192 } else {
193 debug!("About to select coins for amount {amount}");
194 let max_num_coins = 500;
195 let mut sum_spendable_coins = 0;
196 let mut valid_spendable_coins: Vec<Coin> = vec![];
197 for coin_record in self
198 .standard_coins()
199 .lock()
200 .await
201 .iter()
202 .filter(|v| !v.spent)
203 {
204 if exclude.contains(&coin_record.coin) {
205 continue;
206 }
207 if coin_record.coin.amount < min_coin_amount
208 || coin_record.coin.amount > max_coin_amount
209 {
210 continue;
211 }
212 if exclude_coin_amounts.contains(&coin_record.coin.amount) {
213 continue;
214 }
215 sum_spendable_coins += coin_record.coin.amount;
216 valid_spendable_coins.push(coin_record.coin);
217 }
218 if sum_spendable_coins < amount {
219 return Err(Error::new(ErrorKind::InvalidInput, format!("Transaction for {amount} is greater than spendable balance of {sum_spendable_coins}. There may be other transactions pending or our minimum coin amount is too high.")));
220 }
221 if amount == 0 && sum_spendable_coins == 0 {
222 return Err(Error::new(ErrorKind::InvalidInput, "No coins available to spend, you can not create a coin with an amount of 0, without already having coins."));
223 }
224 valid_spendable_coins.sort_by(|f, s| f.amount.cmp(&s.amount));
225 if let Some(c) = check_for_exact_match(&valid_spendable_coins, amount) {
226 info!("Selected coin with an exact match: {c:?}");
227 Ok(HashSet::from([c]))
228 } else {
229 let mut smaller_coin_sum = 0; let mut all_sum = 0; let mut smaller_coins = vec![];
232 for coin in &valid_spendable_coins {
233 if coin.amount < amount {
234 smaller_coin_sum += coin.amount;
235 smaller_coins.push(*coin);
236 }
237 all_sum += coin.amount;
238 }
239 if smaller_coin_sum == amount && smaller_coins.len() < max_num_coins && amount != 0
240 {
241 debug!("Selected all smaller coins because they equate to an exact match of the target: {smaller_coins:?}");
242 Ok(smaller_coins.iter().copied().collect())
243 } else if smaller_coin_sum < amount {
244 let smallest_coin =
245 select_smallest_coin_over_target(amount, &valid_spendable_coins);
246 if let Some(smallest_coin) = smallest_coin {
247 debug!("Selected closest greater coin: {}", smallest_coin.name());
248 Ok(HashSet::from([smallest_coin]))
249 } else {
250 return Err(Error::new(ErrorKind::InvalidInput, format!("Transaction of {amount} mojo is greater than available sum {all_sum} mojos.")));
251 }
252 } else if smaller_coin_sum > amount {
253 let mut coin_set = knapsack_coin_algorithm(
254 &smaller_coins,
255 amount,
256 max_coin_amount,
257 max_num_coins,
258 None,
259 );
260 debug!("Selected coins from knapsack algorithm: {coin_set:?}");
261 if coin_set.is_none() {
262 coin_set = sum_largest_coins(amount as u128, &smaller_coins);
263 if coin_set.is_none()
264 || coin_set.as_ref().map(HashSet::len).unwrap_or_default()
265 > max_num_coins
266 {
267 let greater_coin =
268 select_smallest_coin_over_target(amount, &valid_spendable_coins);
269 if let Some(greater_coin) = greater_coin {
270 coin_set = Some(HashSet::from([greater_coin]));
271 } else {
272 return Err(Error::new(ErrorKind::InvalidInput, format!("Transaction of {amount} mojo would use more than {max_num_coins} coins. Try sending a smaller amount")));
273 }
274 }
275 }
276 coin_set.ok_or_else(|| {
277 Error::new(
278 ErrorKind::InvalidInput,
279 "Failed to select coins for transaction",
280 )
281 })
282 } else {
283 match select_smallest_coin_over_target(amount, &valid_spendable_coins) {
284 Some(coin) => {
285 debug!("Resorted to selecting smallest coin over target due to dust.: {coin:?}");
286 Ok(HashSet::from([coin]))
287 }
288 None => Err(Error::new(
289 ErrorKind::InvalidInput,
290 "Too many coins are required to make this transaction",
291 )),
292 }
293 }
294 }
295 }
296 }
297
298 async fn populate_secret_keys_for_coin_spends(
299 &self,
300 coin_spends: &[CoinSpend],
301 ) -> Result<(), Error> {
302 for coin_spend in coin_spends {
303 self.populate_secret_key_for_puzzle_hash(&coin_spend.coin.puzzle_hash)
304 .await?;
305 }
306 Ok(())
307 }
308 async fn secret_key_for_public_key(&self, public_key: &Bytes48) -> Result<SecretKey, Error>;
309 fn mapping_function<'a, F>(
310 &'a self,
311 public_key: &'a Bytes48,
312 ) -> Box<dyn Future<Output = Result<SecretKey, Error>> + Send + 'a> {
313 Box::new(self.secret_key_for_public_key(public_key))
314 }
315}
316
317fn check_for_exact_match(coin_list: &[Coin], target: u64) -> Option<Coin> {
318 for coin in coin_list {
319 if coin.amount == target {
320 return Some(*coin);
321 }
322 }
323 None
324}
325
326fn select_smallest_coin_over_target(target: u64, sorted_coin_list: &[Coin]) -> Option<Coin> {
327 for coin in sorted_coin_list {
328 if coin.amount >= target {
329 return Some(*coin);
330 }
331 }
332 None
333}
334
335fn sum_largest_coins(target: u128, sorted_coins: &[Coin]) -> Option<HashSet<Coin>> {
336 let mut total_value = 0u128;
337 let mut selected_coins = HashSet::default();
338 for coin in sorted_coins {
339 total_value += coin.amount as u128;
340 selected_coins.insert(*coin);
341 if total_value >= target {
342 return Some(selected_coins);
343 }
344 }
345 None
346}
347
348fn knapsack_coin_algorithm(
349 smaller_coins: &[Coin],
350 target: u64,
351 max_coin_amount: u64,
352 max_num_coins: usize,
353 seed: Option<[u8; 32]>,
354) -> Option<HashSet<Coin>> {
355 let mut best_set_sum = max_coin_amount;
356 let mut best_set_of_coins: Option<HashSet<Coin>> = None;
357 let seed = Bytes32::new(seed.unwrap_or(*b"---------knapsack seed----------"));
358 let mut rand = StdRng::from_seed(seed.bytes());
359 for _ in 0..1000 {
360 let mut selected_coins = HashSet::default();
361 let mut selected_coins_sum = 0;
362 let mut n_pass = 0;
363 let mut target_reached = false;
364 while n_pass < 2 && !target_reached {
365 for coin in smaller_coins {
366 if (n_pass == 0 && rand.gen::<bool>())
367 || (n_pass == 1 && !selected_coins.contains(coin))
368 {
369 if selected_coins.len() > max_num_coins {
370 break;
371 }
372 selected_coins_sum += coin.amount;
373 selected_coins.insert(*coin);
374 match selected_coins_sum.cmp(&target) {
375 std::cmp::Ordering::Greater => {
376 target_reached = true;
377 if selected_coins_sum < best_set_sum {
378 best_set_of_coins = Some(selected_coins.clone());
379 best_set_sum = selected_coins_sum;
380 selected_coins_sum -= coin.amount;
381 selected_coins.remove(coin);
382 }
383 }
384 std::cmp::Ordering::Less => {}
385 std::cmp::Ordering::Equal => return Some(selected_coins),
386 }
387 }
388 }
389 n_pass += 1;
390 }
391 }
392 best_set_of_coins
393}
394
395#[async_trait]
396pub trait Wallet<T: WalletStore + Send + Sync, C> {
397 fn create(info: WalletInfo<T>, config: C) -> Result<Self, Error>
398 where
399 Self: Sized;
400 fn create_simulator(info: WalletInfo<T>, config: C) -> Result<Self, Error>
401 where
402 Self: Sized;
403 fn name(&self) -> &str;
404 async fn sync(&self) -> Result<bool, Error>;
405 fn is_synced(&self) -> bool;
406 fn wallet_info(&self) -> &WalletInfo<T>;
407 fn wallet_store(&self) -> Arc<Mutex<T>>;
408 fn require_derivation_paths(&self) -> bool {
409 true
410 }
411 #[allow(clippy::cast_possible_truncation)]
412 async fn puzzle_hashes(
413 &self,
414 start_index: usize,
415 count: usize,
416 hardened: bool,
417 ) -> Result<Vec<Bytes32>, Error> {
418 let mut hashes = vec![];
419 for i in start_index..start_index + count {
420 hashes.push(
421 self.wallet_store()
422 .lock()
423 .await
424 .get_derivation_record_at_index(i as u32, hardened)
425 .await?
426 .puzzle_hash,
427 );
428 }
429 Ok(hashes)
430 }
431 fn puzzle_for_pk(&self, public_key: Bytes48) -> Result<Program, Error> {
432 puzzle_for_pk(public_key)
433 }
434 fn puzzle_hash_for_pk(&self, public_key: Bytes48) -> Result<Bytes32, Error> {
435 puzzle_hash_for_pk(public_key)
436 }
437 #[allow(clippy::too_many_arguments)]
438 async fn create_spend_bundle(
439 &self,
440 payments: Vec<AmountWithPuzzleHash>,
441 input_coins: &[CoinRecord],
442 change_puzzle_hash: Option<Bytes32>,
443 allow_excess: bool,
444 fee: i64,
445 origin_id: Option<Bytes32>,
446 solution_transformer: Option<Box<dyn Fn(Program) -> Program + 'static + Send + Sync>>,
447 ) -> Result<SpendBundle, Error>;
448 #[allow(clippy::too_many_arguments)]
449 fn make_solution(
450 &self,
451 primaries: &[AmountWithPuzzleHash],
452 min_time: u64,
453 coin_announcements: Option<HashSet<Bytes32>>,
454 coin_announcements_to_assert: Option<HashSet<Bytes32>>,
455 puzzle_announcements: Option<HashSet<Vec<u8>>>,
456 puzzle_announcements_to_assert: Option<HashSet<Bytes32>>,
457 fee: u64,
458 ) -> Result<Program, Error> {
459 let mut condition_list = vec![];
460 for primary in primaries {
461 condition_list.push(make_create_coin_condition(
462 primary.puzzle_hash,
463 primary.amount,
464 &primary.memos,
465 ));
466 }
467 if min_time > 0 {
468 condition_list.push(make_assert_absolute_seconds_exceeds_condition(min_time));
469 }
470 if fee > 0 {
474 condition_list.push(make_reserve_fee_condition(fee));
475 }
476 if let Some(coin_announcements) = coin_announcements {
477 for announcement in coin_announcements {
478 condition_list.push(make_create_coin_announcement(&announcement.bytes()));
479 }
480 }
481 if let Some(coin_announcements_to_assert) = coin_announcements_to_assert {
482 for announcement_hash in coin_announcements_to_assert {
483 condition_list.push(make_assert_coin_announcement(&announcement_hash));
484 }
485 }
486 if let Some(puzzle_announcements) = puzzle_announcements {
487 for announcement in puzzle_announcements {
488 condition_list.push(make_create_puzzle_announcement(&announcement));
489 }
490 }
491 if let Some(puzzle_announcements_to_assert) = puzzle_announcements_to_assert {
492 for announcement_hash in puzzle_announcements_to_assert {
493 condition_list.push(make_assert_puzzle_announcement(&announcement_hash));
494 }
495 }
496 solution_for_conditions(condition_list)
497 }
498
499 fn compute_memos(
500 &self,
501 spend_bundle: &SpendBundle,
502 ) -> Result<HashMap<Bytes32, Vec<Vec<u8>>>, Error> {
503 let mut memos: HashMap<Bytes32, Vec<Vec<u8>>> = HashMap::default();
504 for coin_spend in &spend_bundle.coin_spends {
505 for (coin_name, coin_memos) in compute_memos_for_spend(coin_spend)? {
506 match memos.remove(&coin_name) {
507 Some(mut existing_memos) => {
508 existing_memos.extend(coin_memos);
509 memos.insert(coin_name, existing_memos);
510 }
511 None => {
512 memos.insert(coin_name, coin_memos);
513 }
514 }
515 }
516 }
517 Ok(memos)
518 }
519 async fn puzzle_for_puzzle_hash(&self, puz_hash: &Bytes32) -> Result<Program, Error> {
520 let public_key = self
521 .wallet_store()
522 .lock()
523 .await
524 .populate_secret_key_for_puzzle_hash(puz_hash)
525 .await?;
526 puzzle_for_pk(public_key)
527 }
528 async fn get_new_puzzle(&self) -> Result<Program, Error> {
529 let dr = self
530 .wallet_store()
531 .lock()
532 .await
533 .get_unused_derivation_record(false)
534 .await?;
535 let puzzle = puzzle_for_pk(dr.pubkey)?;
536 self.wallet_store()
537 .lock()
538 .await
539 .populate_secret_key_for_puzzle_hash(&puzzle.tree_hash())
540 .await?;
541 Ok(puzzle)
542 }
543 async fn get_puzzle_hash(&self, new: bool) -> Result<Bytes32, Error> {
544 Ok(if new {
545 self.get_new_puzzlehash().await?
546 } else {
547 let dr = self
548 .wallet_store()
549 .lock()
550 .await
551 .get_derivation_record(false)
552 .await?;
553 dr.puzzle_hash
554 })
555 }
556 async fn get_new_puzzlehash(&self) -> Result<Bytes32, Error> {
557 let dr = self
558 .wallet_store()
559 .lock()
560 .await
561 .get_unused_derivation_record(false)
562 .await?;
563 self.wallet_store()
564 .lock()
565 .await
566 .populate_secret_key_for_puzzle_hash(&dr.puzzle_hash)
567 .await?;
568 Ok(dr.puzzle_hash)
569 }
570 async fn convert_puzzle_hash(&self, puzzle_hash: Bytes32) -> Bytes32 {
571 puzzle_hash
572 }
573 async fn generate_simple_signed_transaction(
574 &self,
575 mojos: u64,
576 fee_mojos: u64,
577 to_address: Bytes32,
578 ) -> Result<TransactionRecord, Error> {
579 self.generate_signed_transaction(
580 mojos,
581 &to_address,
582 fee_mojos,
583 None,
584 None,
585 None,
586 false,
587 None,
588 None,
589 None,
590 false,
591 None,
592 None,
593 None,
594 None,
595 None,
596 )
597 .await
598 }
599 async fn generate_simple_unsigned_transaction(
600 &self,
601 mojos: u64,
602 fee_mojos: u64,
603 to_address: Bytes32,
604 ) -> Result<Vec<CoinSpend>, Error> {
605 self.generate_unsigned_transaction(
606 mojos,
607 &to_address,
608 fee_mojos,
609 None,
610 None,
611 None,
612 false,
613 None,
614 None,
615 None,
616 false,
617 None,
618 None,
619 None,
620 None,
621 None,
622 )
623 .await
624 }
625 #[allow(clippy::too_many_arguments)]
626 async fn generate_signed_transaction(
627 &self,
628 amount: u64,
629 puzzle_hash: &Bytes32,
630 fee: u64,
631 origin_id: Option<Bytes32>,
632 coins: Option<Vec<Coin>>,
633 primaries: Option<&[AmountWithPuzzleHash]>,
634 ignore_max_send_amount: bool,
635 coin_announcements_to_consume: Option<&[Announcement]>,
636 puzzle_announcements_to_consume: Option<&[Announcement]>,
637 memos: Option<Vec<Vec<u8>>>,
638 negative_change_allowed: bool,
639 min_coin_amount: Option<u64>,
640 max_coin_amount: Option<u64>,
641 exclude_coin_amounts: Option<&[u64]>,
642 exclude_coins: Option<&[Coin]>,
643 reuse_puzhash: Option<bool>,
644 ) -> Result<TransactionRecord, Error> {
645 let non_change_amount = if let Some(primaries) = primaries {
646 amount + primaries.iter().map(|a| a.amount).sum::<u64>()
647 } else {
648 amount
649 };
650 debug!(
651 "Generating transaction for: {} {} {:?}",
652 puzzle_hash, amount, coins
653 );
654 let transaction = self
655 .generate_unsigned_transaction(
656 amount,
657 puzzle_hash,
658 fee,
659 origin_id,
660 coins,
661 primaries,
662 ignore_max_send_amount,
663 coin_announcements_to_consume,
664 puzzle_announcements_to_consume,
665 memos,
666 negative_change_allowed,
667 min_coin_amount,
668 max_coin_amount,
669 exclude_coin_amounts,
670 exclude_coins,
671 reuse_puzhash,
672 )
673 .await?;
674 assert!(!transaction.is_empty());
675 info!("About to sign a transaction: {:?}", transaction);
676 let wallet_store = self.wallet_store().clone();
677 let spend_bundle = sign_coin_spends(
678 transaction,
679 |pub_key| {
680 let pub_key = *pub_key;
681 let wallet_store = wallet_store.clone();
682 async move {
683 wallet_store
684 .lock()
685 .await
686 .secret_key_for_public_key(&pub_key)
687 .await
688 }
689 },
690 HashMap::with_capacity(0),
691 &self.wallet_info().constants.agg_sig_me_additional_data,
692 self.wallet_info()
693 .constants
694 .max_block_cost_clvm
695 .to_u64()
696 .unwrap(),
697 )
698 .await?;
699 let now = SystemTime::now()
700 .duration_since(UNIX_EPOCH)
701 .unwrap()
702 .as_secs();
703 let add_list = spend_bundle.additions()?;
704 let rem_list = spend_bundle.removals();
705 let output_amount: u64 = add_list.iter().map(|a| a.amount).sum::<u64>() + fee;
706 let input_amount: u64 = rem_list.iter().map(|a| a.amount).sum::<u64>();
707 if negative_change_allowed {
708 assert!(output_amount >= input_amount);
709 } else {
710 assert_eq!(output_amount, input_amount);
711 }
712 let memos = self.compute_memos(&spend_bundle)?;
713 let memos = memos
714 .into_iter()
715 .map(|v| (v.0, v.1))
716 .collect::<Vec<(Bytes32, Vec<Vec<u8>>)>>();
717 let name = spend_bundle.name()?;
718 Ok(TransactionRecord {
719 confirmed_at_height: 0,
720 created_at_time: now,
721 to_puzzle_hash: *puzzle_hash,
722 amount: non_change_amount,
723 fee_amount: fee,
724 confirmed: false,
725 sent: 0,
726 spend_bundle: Some(spend_bundle),
727 additions: add_list,
728 removals: rem_list,
729 wallet_id: 1,
730 sent_to: vec![],
731 trade_id: None,
732 transaction_type: TransactionType::OutgoingTx as u32,
733 name,
734 memos,
735 })
736 }
737 #[allow(clippy::too_many_arguments)]
738 #[allow(clippy::too_many_lines)]
739 #[allow(clippy::cast_possible_truncation)]
740 #[allow(clippy::cast_possible_wrap)]
741 #[allow(clippy::cast_sign_loss)]
742 async fn generate_unsigned_transaction(
743 &self,
744 amount: u64,
745 puzzle_hash: &Bytes32,
746 fee: u64,
747 origin_id: Option<Bytes32>,
748 coins: Option<Vec<Coin>>,
749 primaries: Option<&[AmountWithPuzzleHash]>,
750 ignore_max_send_amount: bool,
751 coin_announcements_to_consume: Option<&[Announcement]>,
752 puzzle_announcements_to_consume: Option<&[Announcement]>,
753 memos: Option<Vec<Vec<u8>>>,
754 negative_change_allowed: bool,
755 min_coin_amount: Option<u64>,
756 max_coin_amount: Option<u64>,
757 exclude_coin_amounts: Option<&[u64]>,
758 exclude_coins: Option<&[Coin]>,
759 reuse_puzhash: Option<bool>,
760 ) -> Result<Vec<CoinSpend>, Error> {
761 let mut primaries_amount = 0u64;
762 let total_amount: u128;
763 if let Some(primaries) = primaries {
764 for primary in primaries {
765 primaries_amount += primary.amount;
766 }
767 total_amount = amount as u128 + fee as u128 + primaries_amount as u128;
768 } else {
769 total_amount = amount as u128 + fee as u128;
770 }
771 let reuse_puzhash = reuse_puzhash.unwrap_or(true);
772 let total_balance = self
773 .wallet_store()
774 .lock()
775 .await
776 .get_spendable_balance()
777 .await;
778 if !ignore_max_send_amount {
779 let max_send = self.wallet_store().lock().await.get_max_send_amount().await;
780 if total_amount > max_send {
781 return Err(Error::new(ErrorKind::InvalidInput, format!("Can't send more than {max_send} mojos in a single transaction, got {total_amount}")));
782 }
783 debug!("Max send amount: {max_send}");
784 }
785 let coins_set: HashSet<Coin>;
786 if coins.is_none() {
787 if total_amount > total_balance {
788 return Err(Error::new(ErrorKind::InvalidInput, format!("Can't spend more than wallet balance: {total_balance} mojos, tried to spend: {total_amount} mojos")));
789 }
790 coins_set = self
791 .wallet_store()
792 .lock()
793 .await
794 .select_coins(
795 total_amount as u64,
796 exclude_coins,
797 min_coin_amount,
798 max_coin_amount.unwrap_or(
799 self.wallet_info()
800 .constants
801 .max_coin_amount
802 .to_u64()
803 .unwrap_or_default(),
804 ),
805 exclude_coin_amounts,
806 )
807 .await?;
808 } else if exclude_coins.is_some() {
809 return Err(Error::new(
810 ErrorKind::InvalidInput,
811 "Can't exclude coins when also specifically including coins",
812 ));
813 } else {
814 coins_set = HashSet::from_iter(coins.unwrap_or_default());
815 }
816 assert!(!coins_set.is_empty());
817 info!("Found Coins to use: {:?}", coins_set);
818 let spend_value: i128 = coins_set.iter().map(|v| i128::from(v.amount)).sum::<i128>();
819 info!("spend_value is {spend_value} and total_amount is {total_amount}");
820 let mut change = spend_value - total_amount as i128;
821 if negative_change_allowed {
822 change = max(0, change);
823 }
824 assert!(change >= 0);
825 let coin_announcements_bytes = coin_announcements_to_consume
826 .unwrap_or_default()
827 .iter()
828 .map(Announcement::name)
829 .collect::<Vec<Bytes32>>();
830 let puzzle_announcements_bytes = puzzle_announcements_to_consume
831 .unwrap_or_default()
832 .iter()
833 .map(Announcement::name)
834 .collect::<Vec<Bytes32>>();
835 let mut spends: Vec<CoinSpend> = vec![];
836 let mut primary_announcement_hash = None;
837 if primaries.is_some() {
838 let mut all_primaries_list = primaries
839 .unwrap_or_default()
840 .iter()
841 .map(|a| Primary {
842 puzzle_hash: a.puzzle_hash,
843 amount: a.amount,
844 })
845 .collect::<Vec<Primary>>();
846 all_primaries_list.push(Primary {
847 puzzle_hash: *puzzle_hash,
848 amount,
849 });
850 let as_set: HashSet<Primary> = all_primaries_list.iter().copied().collect();
851 if all_primaries_list.len() != as_set.len() {
852 return Err(Error::new(
853 ErrorKind::InvalidInput,
854 "Cannot create two identical coins",
855 ));
856 }
857 }
858 let memos = memos.unwrap_or_default();
859 let mut origin_id = origin_id;
860 for coin in &coins_set {
861 if [None, Some(coin.name())].contains(&origin_id) {
862 origin_id = Some(coin.name());
863 let mut primaries = if let Some(primaries) = primaries {
864 let mut primaries = primaries.to_vec();
865 primaries.push(AmountWithPuzzleHash {
866 amount,
867 puzzle_hash: *puzzle_hash,
868 memos: memos.clone(),
869 });
870 primaries
871 } else if amount > 0 {
872 vec![AmountWithPuzzleHash {
873 amount,
874 puzzle_hash: *puzzle_hash,
875 memos: memos.clone(),
876 }]
877 } else {
878 vec![]
879 };
880 if change > 0 {
881 let change_puzzle_hash = if reuse_puzhash {
882 let mut change_puzzle_hash = coin.puzzle_hash;
883 for primary in &primaries {
884 if change_puzzle_hash == primary.puzzle_hash
885 && change == i128::from(primary.amount)
886 {
887 change_puzzle_hash = self.get_new_puzzlehash().await?;
889 break;
890 }
891 }
892 change_puzzle_hash
893 } else {
894 self.get_new_puzzlehash().await?
895 };
896 primaries.push(AmountWithPuzzleHash {
897 amount: change as u64,
898 puzzle_hash: change_puzzle_hash,
899 memos: vec![],
900 });
901 }
902 let mut message_list: Vec<Bytes32> = coins_set.iter().map(Coin::name).collect();
903 for primary in &primaries {
904 message_list.push(
905 Coin {
906 parent_coin_info: coin.name(),
907 puzzle_hash: primary.puzzle_hash,
908 amount: primary.amount,
909 }
910 .name(),
911 );
912 }
913 let message = hash_256(message_list.iter().fold(vec![], |mut v, e| {
914 v.extend(
915 e.to_bytes(ChiaProtocolVersion::default())
916 .expect("Bytes32 has Safe to_bytes"),
917 );
918 v
919 }));
920 let coin_announcements = HashSet::from([Bytes32::new(message)]);
921 let coin_announcements_to_assert = HashSet::from_iter(coin_announcements_bytes);
922 let puzzle_announcements_to_assert = HashSet::from_iter(puzzle_announcements_bytes);
923 info!("Primaries: {primaries:?}");
924 info!(
925 "coin_announcements: {:?}",
926 coin_announcements
927 .iter()
928 .map(|v| { hex::encode(v) })
929 .collect::<Vec<String>>()
930 );
931 info!("coin_announcements_to_assert: {coin_announcements_to_assert:?}");
932 info!("puzzle_announcements_to_assert: {puzzle_announcements_to_assert:?}");
933 let puzzle = self.puzzle_for_puzzle_hash(&coin.puzzle_hash).await?;
934 let solution = self.make_solution(
935 &primaries,
936 0,
937 if coin_announcements.is_empty() {
938 None
939 } else {
940 Some(coin_announcements)
941 },
942 if coin_announcements_to_assert.is_empty() {
943 None
944 } else {
945 Some(coin_announcements_to_assert)
946 },
947 None,
948 if puzzle_announcements_to_assert.is_empty() {
949 None
950 } else {
951 Some(puzzle_announcements_to_assert)
952 },
953 fee,
954 )?;
955 primary_announcement_hash = Some(
956 Announcement {
957 origin_info: coin.name(),
958 message: message.to_vec(),
959 morph_bytes: None,
960 }
961 .name(),
962 );
963 info!("Reveal: {} ", hex::encode(&puzzle.serialized));
964 info!("Solution: {} ", hex::encode(&solution.serialized));
965 spends.push(CoinSpend {
966 coin: *coin,
967 puzzle_reveal: SerializedProgram::from_bytes(&puzzle.serialized),
968 solution: SerializedProgram::from_bytes(&solution.serialized),
969 });
970 break;
971 }
972 }
973 for coin in coins_set {
975 if Some(coin.name()) == origin_id {
976 continue;
977 }
978 let puzzle = self.puzzle_for_puzzle_hash(&coin.puzzle_hash).await?;
979 let solution = self.make_solution(
980 &[],
981 0,
982 None,
983 Some(HashSet::from_iter(primary_announcement_hash)),
984 None,
985 None,
986 0,
987 )?;
988 info!("Reveal: {} ", hex::encode(&puzzle.serialized));
989 info!("Solution: {} ", hex::encode(&solution.serialized));
990 spends.push(CoinSpend {
991 coin,
992 puzzle_reveal: SerializedProgram::from_bytes(&puzzle.serialized),
993 solution: SerializedProgram::from_bytes(&solution.serialized),
994 });
995 }
996 info!("Spends is {:?}", spends);
997 Ok(spends)
998 }
999}
1000
1001pub fn compute_memos_for_spend(
1002 coin_spend: &CoinSpend,
1003) -> Result<HashMap<Bytes32, Vec<Vec<u8>>>, Error> {
1004 let (_, result) = coin_spend
1005 .puzzle_reveal
1006 .run_with_cost(INFINITE_COST, &coin_spend.solution.to_program())?;
1007 let mut memos = HashMap::default();
1008 let result_list = result.as_list();
1009 for condition in result_list {
1010 let mut conditions: Vec<Program> = condition.as_list();
1011 if ConditionOpcode::from(&conditions[0]) == ConditionOpcode::CreateCoin
1012 && conditions.len() >= 4
1013 {
1014 let memo_list = conditions.remove(3);
1015 let amount = conditions.remove(2);
1016 let puzzle_hash = conditions.remove(1);
1017 let coin_added = Coin {
1019 parent_coin_info: coin_spend.coin.name(),
1020 puzzle_hash: Bytes32::try_from(puzzle_hash)?,
1021 amount: amount
1022 .as_int()?
1023 .to_u64()
1024 .ok_or(Error::new(ErrorKind::InvalidInput, "invalid amount"))?,
1025 };
1026 let memo_list = memo_list
1027 .as_list()
1028 .into_iter()
1029 .map(|v| v.as_vec().unwrap_or_default())
1030 .collect::<Vec<Vec<u8>>>();
1031 memos.insert(coin_added.name(), memo_list);
1032 }
1033 }
1034 Ok(memos)
1035}