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};
15use dg_xch_core::consensus::constants::ConsensusConstants;
17use 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 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 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 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}