dg_xch_cli_lib/wallets/
mod.rs

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, //JSON String to Store Extra Data for Wallets
78}
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; //coins smaller than target.
230                let mut all_sum = 0; //coins smaller than target.
231                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 me { //This exists in chia's code but I cant find a usage
471        //     condition_list.push(make_assert_my_coin_id_condition(me["id"]));
472        // }
473        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                                //We cannot create two coins has same id, create a new puzhash for the change:
888                                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        //Process the non-origin coins now that we have the primary announcement hash
974        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            //If only 3 elements (opcode + 2 args), there is no memo, this is ph, amount
1018            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}