dg_xch_cli_lib/wallets/
memory_wallet.rs

1use crate::wallets::common::{sign_coin_spends, DerivationRecord};
2use crate::wallets::{SecretKeyStore, Wallet, WalletInfo, WalletStore};
3use async_trait::async_trait;
4use blst::min_pk::SecretKey;
5use dashmap::DashMap;
6use dg_xch_clients::api::full_node::FullnodeAPI;
7use dg_xch_clients::rpc::full_node::FullnodeClient;
8use dg_xch_clients::ClientSSLConfig;
9use dg_xch_core::blockchain::coin_record::{CatCoinRecord, CoinRecord};
10use dg_xch_core::blockchain::coin_spend::CoinSpend;
11use dg_xch_core::blockchain::sized_bytes::{Bytes32, Bytes48};
12use dg_xch_core::blockchain::spend_bundle::SpendBundle;
13use dg_xch_core::blockchain::wallet_type::{AmountWithPuzzleHash, WalletType};
14use dg_xch_core::clvm::program::{Program, SerializedProgram};
15// use dg_xch_core::clvm::sexp::IntoSExp;
16use dg_xch_core::consensus::constants::ConsensusConstants;
17// use dg_xch_puzzles::cats::{CAT_1_PROGRAM, CAT_2_PROGRAM};
18use dg_xch_puzzles::p2_delegated_puzzle_or_hidden_puzzle::{
19    calculate_synthetic_secret_key, DEFAULT_HIDDEN_PUZZLE_HASH,
20};
21use log::{error, info};
22use num_traits::ToPrimitive;
23use std::collections::HashMap;
24use std::io::{Error, ErrorKind};
25use std::sync::atomic::{AtomicU32, Ordering};
26use std::sync::Arc;
27use tokio::sync::Mutex;
28
29pub struct MemoryWalletConfig {
30    pub fullnode_host: String,
31    pub fullnode_port: u16,
32    pub fullnode_ssl_path: Option<ClientSSLConfig>,
33    pub additional_headers: Option<HashMap<String, String>>,
34}
35
36pub struct MemoryWalletStore {
37    pub master_sk: SecretKey,
38    pub current_index: AtomicU32,
39    standard_coins: Arc<Mutex<Vec<CoinRecord>>>,
40    cat_coins: Arc<Mutex<Vec<CatCoinRecord>>>,
41    derivation_records: DashMap<Bytes32, DerivationRecord>,
42    keys_for_ph: DashMap<Bytes32, (Bytes32, Bytes48)>,
43    secret_key_store: SecretKeyStore,
44}
45impl MemoryWalletStore {
46    #[must_use]
47    pub fn new(secret_key: SecretKey, starting_index: u32) -> Self {
48        Self {
49            master_sk: secret_key,
50            current_index: AtomicU32::new(starting_index),
51            standard_coins: Arc::default(),
52            cat_coins: Arc::default(),
53            derivation_records: DashMap::default(),
54            keys_for_ph: DashMap::default(),
55            secret_key_store: SecretKeyStore::default(),
56        }
57    }
58}
59#[async_trait]
60impl WalletStore for MemoryWalletStore {
61    fn get_master_sk(&self) -> &SecretKey {
62        &self.master_sk
63    }
64
65    fn standard_coins(&self) -> Arc<Mutex<Vec<CoinRecord>>> {
66        self.standard_coins.clone()
67    }
68
69    fn cat_coins(&self) -> Arc<Mutex<Vec<CatCoinRecord>>> {
70        self.cat_coins.clone()
71    }
72
73    fn secret_key_store(&self) -> &SecretKeyStore {
74        &self.secret_key_store
75    }
76
77    fn current_index(&self) -> u32 {
78        self.current_index.load(Ordering::Relaxed)
79    }
80
81    fn next_index(&self) -> u32 {
82        self.current_index.fetch_add(1, Ordering::Relaxed)
83    }
84
85    async fn get_confirmed_balance(&self) -> u128 {
86        let coins = self.standard_coins.lock().await;
87        coins.iter().map(|coin| coin.coin.amount as u128).sum()
88    }
89
90    async fn get_unconfirmed_balance(&self) -> u128 {
91        todo!()
92    }
93
94    async fn get_pending_change_balance(&self) -> u128 {
95        todo!()
96    }
97
98    async fn populate_secret_key_for_puzzle_hash(
99        &self,
100        puz_hash: &Bytes32,
101    ) -> Result<Bytes48, Error> {
102        if self.keys_for_ph.is_empty() || self.keys_for_ph.get(puz_hash).is_none() {
103            info!("Populating Initial PuzzleHashes");
104            for i in self.current_index.load(Ordering::Relaxed)
105                ..=(self.current_index.load(Ordering::Relaxed) + 100)
106            {
107                let hardened_record = self.get_derivation_record_at_index(i, true).await?;
108                self.derivation_records
109                    .insert(hardened_record.puzzle_hash, hardened_record);
110                let record = self.get_derivation_record_at_index(i, false).await?;
111                self.derivation_records.insert(record.puzzle_hash, record);
112            }
113        }
114        match self.keys_for_ph.get(puz_hash) {
115            None => {
116                error!("Failed to find keys for puzzle hash");
117                Err(Error::new(
118                    ErrorKind::NotFound,
119                    format!("Failed to find puzzle hash: {puz_hash})"),
120                ))
121            }
122            Some(v) => {
123                let secret_key = SecretKey::from_bytes(v.value().0.as_ref()).map_err(|e| {
124                    Error::new(ErrorKind::InvalidInput, format!("MasterKey: {e:?}"))
125                })?;
126                let synthetic_secret_key =
127                    calculate_synthetic_secret_key(&secret_key, *DEFAULT_HIDDEN_PUZZLE_HASH)?;
128                let _old_key = self.secret_key_store.save_secret_key(&synthetic_secret_key);
129                Ok(v.value().1)
130            }
131        }
132    }
133
134    async fn add_puzzle_hash_and_keys(
135        &self,
136        puzzle_hash: Bytes32,
137        keys: (Bytes32, Bytes48),
138    ) -> Option<(Bytes32, Bytes48)> {
139        self.keys_for_ph.insert(puzzle_hash, keys)
140    }
141
142    async fn secret_key_for_public_key(&self, public_key: &Bytes48) -> Result<SecretKey, Error> {
143        match self
144            .secret_key_store()
145            .secret_key_for_public_key(public_key)
146        {
147            None => Err(Error::new(
148                ErrorKind::NotFound,
149                format!("Failed to find secret_key for pub_key: {public_key})"),
150            )),
151            Some(v) => {
152                let secret_key = SecretKey::from_bytes(v.value().as_ref()).map_err(|e| {
153                    Error::new(ErrorKind::InvalidInput, format!("MasterKey: {e:?}"))
154                })?;
155                Ok(secret_key)
156            }
157        }
158    }
159}
160
161pub struct MemoryWallet {
162    //A wallet that is lost on restarts
163    info: WalletInfo<MemoryWalletStore>,
164    pub config: MemoryWalletConfig,
165    pub fullnode_client: FullnodeClient,
166}
167impl MemoryWallet {
168    pub fn new(
169        master_secret_key: SecretKey,
170        client: &FullnodeClient,
171        constants: Arc<ConsensusConstants>,
172    ) -> Result<Self, Error> {
173        Self::create(
174            WalletInfo {
175                id: 1,
176                name: "memory_wallet".to_string(),
177                wallet_type: WalletType::StandardWallet,
178                constants,
179                master_sk: master_secret_key.clone(),
180                wallet_store: Arc::new(Mutex::new(MemoryWalletStore::new(master_secret_key, 0))),
181                data: String::new(),
182            },
183            MemoryWalletConfig {
184                fullnode_host: client.host.clone(),
185                fullnode_port: client.port,
186                fullnode_ssl_path: client.ssl_path.clone(),
187                additional_headers: client.additional_headers.clone(),
188            },
189        )
190    }
191}
192#[async_trait]
193impl Wallet<MemoryWalletStore, MemoryWalletConfig> for MemoryWallet {
194    fn create(
195        info: WalletInfo<MemoryWalletStore>,
196        config: MemoryWalletConfig,
197    ) -> Result<Self, Error> {
198        let fullnode_client = FullnodeClient::new(
199            &config.fullnode_host.clone(),
200            config.fullnode_port,
201            60,
202            config.fullnode_ssl_path.clone(),
203            &config.additional_headers.clone(),
204        )?;
205        Ok(Self {
206            info,
207            config,
208            fullnode_client,
209        })
210    }
211    fn create_simulator(
212        info: WalletInfo<MemoryWalletStore>,
213        config: MemoryWalletConfig,
214    ) -> Result<Self, Error> {
215        let fullnode_client =
216            FullnodeClient::new_simulator(&config.fullnode_host.clone(), config.fullnode_port, 60)?;
217        Ok(Self {
218            info,
219            config,
220            fullnode_client,
221        })
222    }
223
224    fn name(&self) -> &str {
225        &self.info.name
226    }
227
228    #[allow(clippy::cast_possible_wrap)]
229    async fn sync(&self) -> Result<bool, Error> {
230        let standard_coins_arc = self.wallet_store().lock().await.standard_coins().clone();
231        // let cat_coins_arc = self.wallet_store().lock().await.cat_coins().clone();
232        let puzzle_hashes = self
233            .wallet_store()
234            .lock()
235            .await
236            .get_puzzle_hashes(0, 100, false)
237            .await?;
238        let standard_coins = self
239            .fullnode_client
240            .get_coin_records_by_puzzle_hashes(&puzzle_hashes, Some(true), None, None)
241            .await?;
242        {
243            let mut arc_mut = standard_coins_arc.lock().await;
244            arc_mut.clear();
245            arc_mut.extend(standard_coins);
246        }
247        // let hinted_coins = self
248        //     .fullnode_client
249        //     .get_coin_records_by_hints(&puzzle_hashes, Some(true), None, None)
250        //     .await?;
251        // let mut cat_records = vec![];
252        // for hinted_coin in hinted_coins {
253        //     if let Some(parent_coin) = self
254        //         .fullnode_client
255        //         .get_coin_record_by_name(&hinted_coin.coin.parent_coin_info)
256        //         .await?
257        //     {
258        //         if let Ok(parent_coin_spend) =
259        //             self.fullnode_client.get_coin_spend(&parent_coin).await
260        //         {
261        //             let (cat_program, args) =
262        //                 parent_coin_spend.puzzle_reveal.to_program().uncurry()?;
263        //             let is_cat_v1 = cat_program == *CAT_1_PROGRAM;
264        //             let is_cat_v2 = !is_cat_v1 && cat_program == *CAT_2_PROGRAM;
265        //             if is_cat_v1 || is_cat_v2 {
266        //                 let asset_id: Bytes32 = args.rest()?.first()?.try_into()?;
267        //                 let inner_puzzle: Bytes32 = args.rest()?.rest()?.first()?.try_into()?;
268        //                 let lineage_proof = Program::to(vec![
269        //                     parent_coin_spend.coin.parent_coin_info.to_sexp(),
270        //                     inner_puzzle.to_sexp(),
271        //                     parent_coin_spend.coin.amount.to_sexp(),
272        //                 ]);
273        //                 cat_records.push(CatCoinRecord {
274        //                     delegate: hinted_coin,
275        //                     version: if is_cat_v1 {
276        //                         CatVersion::V1
277        //                     } else {
278        //                         CatVersion::V2
279        //                     },
280        //                     asset_id,
281        //                     cat_program,
282        //                     lineage_proof,
283        //                     parent_coin_spend,
284        //                 });
285        //             } else {
286        //                 error!("Error Parsing Coin as CAT: {hinted_coin:?}");
287        //             }
288        //         }
289        //     }
290        // }
291        // {
292        //     let mut arc_mut = cat_coins_arc.lock().await;
293        //     arc_mut.clear();
294        //     arc_mut.extend(cat_records);
295        // }
296        Ok(true)
297    }
298
299    fn is_synced(&self) -> bool {
300        todo!()
301    }
302
303    fn wallet_info(&self) -> &WalletInfo<MemoryWalletStore> {
304        &self.info
305    }
306
307    fn wallet_store(&self) -> Arc<Mutex<MemoryWalletStore>> {
308        self.info.wallet_store.clone()
309    }
310
311    #[allow(clippy::too_many_lines)]
312    #[allow(clippy::cast_possible_wrap)]
313    #[allow(clippy::cast_possible_truncation)]
314    #[allow(clippy::cast_sign_loss)]
315    async fn create_spend_bundle(
316        &self,
317        mut payments: Vec<AmountWithPuzzleHash>,
318        input_coins: &[CoinRecord],
319        change_puzzle_hash: Option<Bytes32>,
320        allow_excess: bool,
321        fee: i64,
322        origin_id: Option<Bytes32>,
323        solution_transformer: Option<Box<dyn Fn(Program) -> Program + 'static + Send + Sync>>,
324    ) -> Result<SpendBundle, Error> {
325        let mut coins = input_coins.to_vec();
326        let total_coin_value: u64 = coins.iter().map(|c| c.coin.amount).sum();
327        let total_payment_value: u64 = payments.iter().map(|p| p.amount).sum();
328        let change = total_coin_value as i64 - total_payment_value as i64 - fee;
329        if change_puzzle_hash.is_none() && change > 0 && !allow_excess {
330            return Err(Error::new(
331                ErrorKind::InvalidInput,
332                "Found change but not Change Puzzle Hash was provided.",
333            ));
334        }
335        if let Some(change_puzzle_hash) = change_puzzle_hash {
336            if change > 0 {
337                payments.push(AmountWithPuzzleHash {
338                    puzzle_hash: change_puzzle_hash,
339                    amount: change as u64,
340                    memos: vec![],
341                })
342            }
343        }
344        let mut spends = vec![];
345        let origin_index = match origin_id {
346            Some(origin_id) => {
347                match coins
348                    .iter()
349                    .enumerate()
350                    .find(|(_, val)| val.coin.coin_id() == origin_id)
351                {
352                    Some((index, _)) => index as i64,
353                    None => -1i64,
354                }
355            }
356            None => 0i64,
357        };
358        if origin_index == -1 {
359            return Err(Error::new(
360                ErrorKind::InvalidInput,
361                "Origin ID Not in Coin List",
362            ));
363        }
364        if origin_index != 0 {
365            let origin_coin = coins.remove(origin_index as usize);
366            coins.insert(0, origin_coin);
367        }
368        for coin in &coins {
369            let mut solution =
370                self.make_solution(&payments, 0, None, None, None, None, fee as u64)?;
371            if let Some(solution_transformer) = &solution_transformer {
372                solution = solution_transformer(solution)
373            }
374            let puzzle = self.puzzle_for_puzzle_hash(&coin.coin.puzzle_hash).await?;
375            let coin_spend = CoinSpend {
376                coin: coin.coin,
377                puzzle_reveal: SerializedProgram::from(puzzle),
378                solution: SerializedProgram::from(solution),
379            };
380            spends.push(coin_spend);
381        }
382        info!("Signing Coin Spends");
383        let spend_bundle = sign_coin_spends(
384            spends,
385            |pub_key| {
386                let pub_key = *pub_key;
387                let wallet_store = self.wallet_store().clone();
388                async move {
389                    wallet_store
390                        .lock()
391                        .await
392                        .secret_key_for_public_key(&pub_key)
393                        .await
394                }
395            },
396            HashMap::with_capacity(0),
397            &self.wallet_info().constants.agg_sig_me_additional_data,
398            self.wallet_info()
399                .constants
400                .max_block_cost_clvm
401                .to_u64()
402                .unwrap(),
403        )
404        .await?;
405        Ok(spend_bundle)
406    }
407}