1use crate::wallets::common::sign_coin_spend;
2use crate::wallets::memory_wallet::{MemoryWalletConfig, MemoryWalletStore};
3use crate::wallets::{Wallet, WalletInfo, WalletStore};
4use async_trait::async_trait;
5use blst::min_pk::SecretKey;
6use dg_xch_clients::api::full_node::FullnodeAPI;
7use dg_xch_clients::rpc::full_node::FullnodeClient;
8use dg_xch_core::blockchain::announcement::Announcement;
9use dg_xch_core::blockchain::coin_record::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::transaction_record::{TransactionRecord, TransactionType};
14use dg_xch_core::blockchain::tx_status::TXStatus;
15use dg_xch_core::blockchain::wallet_type::{AmountWithPuzzleHash, WalletType};
16use dg_xch_core::clvm::program::Program;
17use dg_xch_core::consensus::constants::ConsensusConstants;
18use dg_xch_core::constants::{FARMING_TO_POOL, LEAVING_POOL, POOL_PROTOCOL_VERSION};
19use dg_xch_core::plots::PlotNft;
20use dg_xch_core::pool::PoolState;
21use dg_xch_core::traits::SizedBytes;
22use dg_xch_keys::{
23 master_sk_to_singleton_owner_sk, master_sk_to_wallet_sk, master_sk_to_wallet_sk_unhardened,
24};
25use dg_xch_puzzles::clvm_puzzles::{
26 create_full_puzzle, create_travel_spend, get_most_recent_singleton_coin_from_coin_spend,
27 launcher_coin_spend_to_extra_data, pool_state_to_inner_puzzle, solution_to_pool_state,
28 SINGLETON_LAUNCHER_HASH,
29};
30use dg_xch_puzzles::p2_delegated_puzzle_or_hidden_puzzle::puzzle_hash_for_pk;
31use log::info;
32use num_traits::cast::ToPrimitive;
33use std::collections::HashMap;
34use std::future::Future;
35use std::io::{Error, ErrorKind};
36use std::sync::Arc;
37use std::time::{Duration, SystemTime, UNIX_EPOCH};
38use tokio::select;
39use tokio::sync::Mutex;
40use tokio::task::JoinSet;
41
42pub struct PlotNFTWallet {
43 info: WalletInfo<MemoryWalletStore>,
44 pub config: MemoryWalletConfig,
45 fullnode_client: Arc<FullnodeClient>,
46}
47#[async_trait]
48impl Wallet<MemoryWalletStore, MemoryWalletConfig> for PlotNFTWallet {
49 fn create(
50 info: WalletInfo<MemoryWalletStore>,
51 config: MemoryWalletConfig,
52 ) -> Result<Self, Error> {
53 Ok(Self {
54 fullnode_client: Arc::new(FullnodeClient::new(
55 &config.fullnode_host,
56 config.fullnode_port,
57 60,
58 config.fullnode_ssl_path.clone(),
59 &config.additional_headers,
60 )?),
61 info,
62 config,
63 })
64 }
65 fn create_simulator(
66 info: WalletInfo<MemoryWalletStore>,
67 config: MemoryWalletConfig,
68 ) -> Result<Self, Error> {
69 Ok(Self {
70 fullnode_client: Arc::new(FullnodeClient::new_simulator(
71 &config.fullnode_host,
72 config.fullnode_port,
73 60,
74 )?),
75 info,
76 config,
77 })
78 }
79
80 fn name(&self) -> &str {
81 &self.info.name
82 }
83
84 async fn sync(&self) -> Result<bool, Error> {
85 let mut puzzle_hashes = vec![];
86 for index in 0..50 {
87 let wallet_sk = master_sk_to_wallet_sk(&self.info.master_sk, index).map_err(|e| {
88 Error::new(
89 ErrorKind::InvalidInput,
90 format!("Failed to parse Wallet SK: {e:?}"),
91 )
92 })?;
93 let pub_key: Bytes48 = wallet_sk.sk_to_pk().to_bytes().into();
94 let ph = puzzle_hash_for_pk(pub_key)?;
95 puzzle_hashes.push(ph);
96 let wallet_sk = master_sk_to_wallet_sk_unhardened(&self.info.master_sk, index)
97 .map_err(|e| {
98 Error::new(
99 ErrorKind::InvalidInput,
100 format!("Failed to parse Wallet SK: {e:?}"),
101 )
102 })?;
103 let pub_key: Bytes48 = wallet_sk.sk_to_pk().to_bytes().into();
104 let ph = puzzle_hash_for_pk(pub_key)?;
105 puzzle_hashes.push(ph);
106 }
107 let (spend, unspent) =
108 scrounge_for_standard_coins(self.fullnode_client.clone(), &puzzle_hashes).await?;
109 let store = self.info.wallet_store.lock().await;
110 let coins = store.standard_coins();
111 coins.lock().await.extend(spend.into_iter());
112 coins.lock().await.extend(unspent.into_iter());
113 Ok(true)
114 }
115
116 fn is_synced(&self) -> bool {
117 todo!()
118 }
119
120 fn wallet_info(&self) -> &WalletInfo<MemoryWalletStore> {
121 &self.info
122 }
123
124 fn wallet_store(&self) -> Arc<Mutex<MemoryWalletStore>> {
125 self.info.wallet_store.clone()
126 }
127
128 async fn create_spend_bundle(
129 &self,
130 _payments: Vec<AmountWithPuzzleHash>,
131 _input_coins: &[CoinRecord],
132 _change_puzzle_hash: Option<Bytes32>,
133 _allow_excess: bool,
134 _fee: i64,
135 _origin_id: Option<Bytes32>,
136 _solution_transformer: Option<Box<dyn Fn(Program) -> Program + 'static + Send + Sync>>,
137 ) -> Result<SpendBundle, Error> {
138 todo!()
139 }
140}
141impl PlotNFTWallet {
142 pub fn new(
143 master_secret_key: SecretKey,
144 client: &FullnodeClient,
145 constants: Arc<ConsensusConstants>,
146 ) -> Result<Self, Error> {
147 Self::create(
148 WalletInfo {
149 id: 1,
150 name: "pooling_wallet".to_string(),
151 wallet_type: WalletType::PoolingWallet,
152 constants,
153 master_sk: master_secret_key.clone(),
154 wallet_store: Arc::new(Mutex::new(MemoryWalletStore::new(master_secret_key, 0))),
155 data: String::new(),
156 },
157 MemoryWalletConfig {
158 fullnode_host: client.host.clone(),
159 fullnode_port: client.port,
160 fullnode_ssl_path: client.ssl_path.clone(),
161 additional_headers: client.additional_headers.clone(),
162 },
163 )
164 }
165 pub fn find_owner_key(&self, key_to_find: &Bytes48, limit: u32) -> Result<SecretKey, Error> {
166 for i in 0..limit {
167 let key = master_sk_to_singleton_owner_sk(&self.wallet_info().master_sk, i)?;
168 if key.sk_to_pk().to_bytes() == key_to_find.bytes() {
169 return Ok(key);
170 }
171 }
172 Err(Error::new(ErrorKind::NotFound, "Failed to find Owner SK"))
173 }
174
175 pub async fn generate_fee_transaction(
176 &self,
177 fee: u64,
178 coin_announcements: Option<&[Announcement]>,
179 ) -> Result<TransactionRecord, Error> {
180 self.generate_signed_transaction(
181 0,
182 &self.get_new_puzzlehash().await?,
183 fee,
184 None,
185 None,
186 None,
187 false,
188 coin_announcements,
189 None,
190 None,
191 false,
192 None,
193 None,
194 None,
195 None,
196 None,
197 )
198 .await
199 }
200
201 #[allow(clippy::too_many_lines)]
202 #[allow(clippy::cast_sign_loss)]
203 pub async fn generate_travel_transaction(
204 &self,
205 plot_nft: &PlotNft,
206 target_pool_state: &PoolState,
207 fee: u64,
208 constants: &ConsensusConstants,
209 ) -> Result<(TransactionRecord, Option<TransactionRecord>), Error> {
210 let launcher_coin = self
211 .fullnode_client
212 .get_coin_record_by_name(&plot_nft.launcher_id)
213 .await?
214 .ok_or_else(|| Error::other("Failed to load launcher_coin"))?;
215 let last_record = self
216 .fullnode_client
217 .get_coin_record_by_name(&plot_nft.singleton_coin.coin.parent_coin_info)
218 .await?
219 .ok_or_else(|| Error::other("Failed to load launcher_coin"))?;
220 let last_coin_spend = self.fullnode_client.get_coin_spend(&last_record).await?;
221 let next_state = if plot_nft.pool_state.state == FARMING_TO_POOL {
222 PoolState {
223 version: POOL_PROTOCOL_VERSION,
224 state: LEAVING_POOL,
225 target_puzzle_hash: plot_nft.pool_state.target_puzzle_hash,
226 owner_pubkey: plot_nft.pool_state.owner_pubkey,
227 pool_url: plot_nft.pool_state.pool_url.clone(),
228 relative_lock_height: plot_nft.pool_state.relative_lock_height,
229 }
230 } else {
231 target_pool_state.clone()
232 };
233 let new_inner_puzzle = pool_state_to_inner_puzzle(
234 &next_state,
235 launcher_coin.coin.name(),
236 constants.genesis_challenge,
237 plot_nft.delay_time as u64,
238 plot_nft.delay_puzzle_hash,
239 )?;
240 let new_full_puzzle = create_full_puzzle(&new_inner_puzzle, launcher_coin.coin.name())?;
241 let (outgoing_coin_spend, inner_puzzle) = create_travel_spend(
242 &last_coin_spend,
243 launcher_coin.coin,
244 &plot_nft.pool_state,
245 &next_state,
246 constants.genesis_challenge,
247 plot_nft.delay_time as u64,
248 plot_nft.delay_puzzle_hash,
249 )?;
250 let (additions, _cost) = last_coin_spend
251 .compute_additions_with_cost(constants.max_block_cost_clvm.to_u64().unwrap())?;
252 let singleton = &additions[0];
253 let singleton_id = singleton.name();
254 assert_eq!(
255 outgoing_coin_spend.coin.parent_coin_info,
256 last_coin_spend.coin.name()
257 );
258 assert_eq!(
259 outgoing_coin_spend.coin.parent_coin_info,
260 last_coin_spend.coin.name()
261 );
262 assert_eq!(outgoing_coin_spend.coin.name(), singleton_id);
263 assert_ne!(new_inner_puzzle, inner_puzzle);
264 let mut signed_spend_bundle = sign_coin_spend(
265 outgoing_coin_spend,
266 |_| async { self.find_owner_key(&plot_nft.pool_state.owner_pubkey, 500) },
267 HashMap::with_capacity(0),
268 constants,
269 )
270 .await?;
271 assert_eq!(
272 signed_spend_bundle.removals()[0].puzzle_hash,
273 singleton.puzzle_hash
274 );
275 assert_eq!(signed_spend_bundle.removals()[0].name(), singleton.name());
276 let fee_tx: Option<TransactionRecord> = None;
277 if fee > 0 {
278 let fee_tx = self.generate_fee_transaction(fee, None).await?;
279 if let Some(fee_bundle) = fee_tx.spend_bundle {
280 signed_spend_bundle = SpendBundle::aggregate(vec![signed_spend_bundle, fee_bundle])
281 .map_err(|e| Error::other(format!("Failed to parse Public key: {e:?}")))?;
282 }
283 }
284 let additions = signed_spend_bundle.additions()?;
285 let removals = signed_spend_bundle.removals();
286 let name = signed_spend_bundle.name()?;
287 let tx_record = TransactionRecord {
288 confirmed_at_height: 0,
289 created_at_time: SystemTime::now()
290 .duration_since(UNIX_EPOCH)
291 .unwrap()
292 .as_secs(),
293 to_puzzle_hash: new_full_puzzle.tree_hash(),
294 amount: 1,
295 fee_amount: fee,
296 confirmed: false,
297 sent: 0,
298 spend_bundle: Some(signed_spend_bundle),
299 additions,
300 removals,
301 wallet_id: 1,
302 sent_to: vec![],
303 trade_id: None,
304 memos: vec![],
305 transaction_type: TransactionType::OutgoingTx as u32,
306 name,
307 };
308 Ok((tx_record, fee_tx))
309 }
310}
311
312#[allow(clippy::cast_sign_loss)]
313pub async fn generate_travel_transaction_without_fee<F, Fut>(
314 client: Arc<FullnodeClient>,
315 key_fn: F,
316 plot_nft: &PlotNft,
317 target_pool_state: &PoolState,
318 constants: &ConsensusConstants,
319) -> Result<(TransactionRecord, Option<TransactionRecord>), Error>
320where
321 F: Fn(&Bytes48) -> Fut,
322 Fut: Future<Output = Result<SecretKey, Error>>,
323{
324 let launcher_coin = client
325 .get_coin_record_by_name(&plot_nft.launcher_id)
326 .await?
327 .ok_or_else(|| Error::other("Failed to load launcher_coin"))?;
328 let last_record = client
329 .get_coin_record_by_name(&plot_nft.singleton_coin.coin.parent_coin_info)
330 .await?
331 .ok_or_else(|| Error::other("Failed to load launcher_coin"))?;
332 let last_coin_spend = client.get_coin_spend(&last_record).await?;
333 let next_state = if plot_nft.pool_state.state == FARMING_TO_POOL {
334 PoolState {
335 version: POOL_PROTOCOL_VERSION,
336 state: LEAVING_POOL,
337 target_puzzle_hash: plot_nft.pool_state.target_puzzle_hash,
338 owner_pubkey: plot_nft.pool_state.owner_pubkey,
339 pool_url: plot_nft.pool_state.pool_url.clone(),
340 relative_lock_height: plot_nft.pool_state.relative_lock_height,
341 }
342 } else {
343 target_pool_state.clone()
344 };
345 let new_inner_puzzle = pool_state_to_inner_puzzle(
346 &next_state,
347 launcher_coin.coin.name(),
348 constants.genesis_challenge,
349 plot_nft.delay_time as u64,
350 plot_nft.delay_puzzle_hash,
351 )?;
352 let new_full_puzzle = create_full_puzzle(&new_inner_puzzle, launcher_coin.coin.name())?;
353 let (outgoing_coin_spend, inner_puzzle) = create_travel_spend(
354 &last_coin_spend,
355 launcher_coin.coin,
356 &plot_nft.pool_state,
357 &next_state,
358 constants.genesis_challenge,
359 plot_nft.delay_time as u64,
360 plot_nft.delay_puzzle_hash,
361 )?;
362 let (additions, _cost) = last_coin_spend
363 .compute_additions_with_cost(constants.max_block_cost_clvm.to_u64().unwrap())?;
364 let singleton = &additions[0];
365 let singleton_id = singleton.name();
366 assert_eq!(
367 outgoing_coin_spend.coin.parent_coin_info,
368 last_coin_spend.coin.name()
369 );
370 assert_eq!(
371 outgoing_coin_spend.coin.parent_coin_info,
372 last_coin_spend.coin.name()
373 );
374 assert_eq!(outgoing_coin_spend.coin.name(), singleton_id);
375 assert_ne!(new_inner_puzzle, inner_puzzle);
376 let signed_spend_bundle = sign_coin_spend(
377 outgoing_coin_spend,
378 key_fn,
379 HashMap::with_capacity(0),
380 constants,
381 )
382 .await?;
383 assert_eq!(
384 signed_spend_bundle.removals()[0].puzzle_hash,
385 singleton.puzzle_hash
386 );
387 assert_eq!(signed_spend_bundle.removals()[0].name(), singleton.name());
388 let additions = signed_spend_bundle.additions()?;
389 let removals = signed_spend_bundle.removals();
390 let name = signed_spend_bundle.name()?;
391 let tx_record = TransactionRecord {
392 confirmed_at_height: 0,
393 created_at_time: SystemTime::now()
394 .duration_since(UNIX_EPOCH)
395 .unwrap()
396 .as_secs(),
397 to_puzzle_hash: new_full_puzzle.tree_hash(),
398 amount: 1,
399 fee_amount: 0,
400 confirmed: false,
401 sent: 0,
402 spend_bundle: Some(signed_spend_bundle),
403 additions,
404 removals,
405 wallet_id: 1,
406 sent_to: vec![],
407 trade_id: None,
408 memos: vec![],
409 transaction_type: TransactionType::OutgoingTx as u32,
410 name,
411 };
412 Ok((tx_record, None))
413}
414
415pub async fn get_current_pool_state(
416 client: Arc<FullnodeClient>,
417 launcher_id: &Bytes32,
418) -> Result<(PoolState, CoinSpend), Error> {
419 let mut last_spend: CoinSpend;
420 let mut saved_state: PoolState;
421 match client.get_coin_record_by_name(launcher_id).await? {
422 Some(lc) if lc.spent => {
423 last_spend = client.get_coin_spend(&lc).await?;
424 match solution_to_pool_state(&last_spend)? {
425 Some(state) => {
426 saved_state = state;
427 }
428 None => {
429 return Err(Error::new(
430 ErrorKind::InvalidData,
431 "Failed to Read Pool State",
432 ));
433 }
434 }
435 }
436 Some(_) => {
437 return Err(Error::new(
438 ErrorKind::InvalidData,
439 format!("Genesis coin {} not spent", &launcher_id.to_string()),
440 ));
441 }
442 None => {
443 return Err(Error::new(
444 ErrorKind::NotFound,
445 format!("Can not find genesis coin {}", &launcher_id),
446 ));
447 }
448 }
449 let mut saved_spend: CoinSpend = last_spend.clone();
450 let mut last_not_none_state: PoolState = saved_state.clone();
451 loop {
452 match get_most_recent_singleton_coin_from_coin_spend(&last_spend)? {
453 None => {
454 return Err(Error::new(
455 ErrorKind::NotFound,
456 "Failed to find recent singleton from coin Record",
457 ));
458 }
459 Some(next_coin) => match client.get_coin_record_by_name(&next_coin.name()).await? {
460 None => {
461 return Err(Error::new(
462 ErrorKind::NotFound,
463 "Failed to find Coin Record",
464 ));
465 }
466 Some(next_coin_record) => {
467 if !next_coin_record.spent {
468 break;
469 }
470 last_spend = client.get_coin_spend(&next_coin_record).await?;
471 if let Ok(Some(pool_state)) = solution_to_pool_state(&last_spend) {
472 last_not_none_state = pool_state;
473 }
474 saved_spend = last_spend.clone();
475 saved_state = last_not_none_state.clone();
476 }
477 },
478 }
479 }
480 Ok((saved_state, saved_spend))
481}
482
483pub async fn scrounge_for_plotnft_by_key(
484 client: Arc<FullnodeClient>,
485 master_secret_key: &SecretKey,
486) -> Result<Vec<PlotNft>, Error> {
487 let mut page = 0;
488 let mut plotnfs = vec![];
489 while page < 15 && plotnfs.is_empty() {
490 let mut puzzle_hashes = vec![];
491 for index in page * 50..(page + 1) * 50 {
492 let wallet_sk =
493 master_sk_to_wallet_sk_unhardened(master_secret_key, index).map_err(|e| {
494 Error::new(
495 ErrorKind::InvalidInput,
496 format!("Failed to parse Wallet SK: {e:?}"),
497 )
498 })?;
499 let pub_key: Bytes48 = wallet_sk.sk_to_pk().to_bytes().into();
500 let ph = puzzle_hash_for_pk(pub_key)?;
501 puzzle_hashes.push(ph);
502 }
503 plotnfs.extend(scrounge_for_plotnfts(client.clone(), &puzzle_hashes).await?);
504 page += 1;
505 }
506 Ok(plotnfs)
507}
508
509pub async fn scrounge_for_plotnfts(
510 client: Arc<FullnodeClient>,
511 puzzle_hashes: &[Bytes32],
512) -> Result<Vec<PlotNft>, Error> {
513 info!("Fetching Coins for {} Puzzle Hashes", puzzle_hashes.len());
514 let hashes = client
515 .get_coin_records_by_puzzle_hashes(puzzle_hashes, Some(true), None, None)
516 .await?;
517 let mut spent: Vec<CoinRecord> = hashes.into_iter().filter(|c| c.spent).collect();
518 let plotnfts = Arc::new(Mutex::new(vec![]));
519 let mut thread_pool: JoinSet<Result<(), Error>> = JoinSet::new();
520 let counter = Arc::new(Mutex::new(0usize));
521 let total = spent.len();
522 let first_10: Vec<CoinRecord> = (0..std::cmp::min(10, total))
523 .map(|_| spent.remove(0))
524 .collect();
525 info!("Loading {total} Coin Spends");
526 for spent_coin in first_10 {
527 let plotnfts = plotnfts.clone();
528 let client = client.clone();
529 let counter = counter.clone();
530 thread_pool.spawn(async move {
531 let coin_spend = client.get_coin_spend(&spent_coin).await?;
532 for child in coin_spend.additions()? {
533 if child.puzzle_hash == *SINGLETON_LAUNCHER_HASH {
534 let launcher_id = child.name();
535 if let Some(plotnft) =
536 get_plotnft_by_launcher_id(client.clone(), launcher_id, None).await?
537 {
538 plotnfts.lock().await.push(plotnft);
539 }
540 *counter.lock().await += 1;
541 }
542 }
543 Ok(())
544 });
545 }
546 loop {
547 select! {
548 val = thread_pool.join_next() => {
549 info!("Finished: {} / {total}", *counter.lock().await);
550 let plotnfts = plotnfts.clone();
551 let client = client.clone();
552 let counter = counter.clone();
553 if let Some(spent_coin) = spent.pop() {
554 thread_pool.spawn(async move {
555 let coin_spend = client.get_coin_spend(&spent_coin).await?;
556 for child in coin_spend.additions()? {
557 if child.puzzle_hash == *SINGLETON_LAUNCHER_HASH {
558 let launcher_id = child.name();
559 if let Some(plotnft) = get_plotnft_by_launcher_id(client.clone(), launcher_id, None).await? {
560 plotnfts.lock().await.push(plotnft);
561 }
562 }
563 }
564 *counter.lock().await += 1;
565 Ok(())
566 });
567 continue;
568 }
569 if val.is_none() {
570 break;
571 }
572 }
573 () = tokio::time::sleep(Duration::from_secs(1)) => {
574 info!("Finished: {} / {total}", *counter.lock().await);
575 }
576 }
577 }
578 Ok(Arc::try_unwrap(plotnfts).unwrap().into_inner())
579}
580
581pub async fn scrounge_for_standard_coins(
582 client: Arc<FullnodeClient>,
583 puzzle_hashes: &[Bytes32],
584) -> Result<(Vec<CoinRecord>, Vec<CoinRecord>), Error> {
585 let records = client
586 .get_coin_records_by_puzzle_hashes(puzzle_hashes, Some(true), None, None)
587 .await?;
588 let mut spent = vec![];
589 let mut unspent = vec![];
590 for coin in records {
591 if coin.spent {
592 spent.push(coin);
593 } else {
594 unspent.push(coin);
595 }
596 }
597 Ok((spent, unspent))
598}
599
600pub async fn get_pool_state(
601 client: Arc<FullnodeClient>,
602 launcher_id: Bytes32,
603 last_known_coin_name: Option<Bytes32>,
604) -> Result<PoolState, Error> {
605 if let Some(plotnft) =
606 get_plotnft_by_launcher_id(client, launcher_id, last_known_coin_name).await?
607 {
608 Ok(plotnft.pool_state)
609 } else {
610 Err(Error::new(
611 ErrorKind::NotFound,
612 format!("Failed to find pool state for launcher_id {launcher_id}"),
613 ))
614 }
615}
616
617pub async fn get_plotnft_by_launcher_id(
618 client: Arc<FullnodeClient>,
619 launcher_id: Bytes32,
620 last_known_coin_name: Option<Bytes32>,
621) -> Result<Option<PlotNft>, Error> {
622 if let Some(starting_coin) = client.get_coin_record_by_name(&launcher_id).await? {
623 let spend = client.get_coin_spend(&starting_coin).await?;
624 let initial_extra_data = launcher_coin_spend_to_extra_data(&spend)?;
625 let first_coin = get_most_recent_singleton_coin_from_coin_spend(&spend)?;
626 if let Some(coin) = first_coin {
627 info!("Found Launcher Coin, Starting to crawl Coin History");
628 let mut last_not_null_state = initial_extra_data.pool_state.clone();
629 let mut singleton_coin = if let Some(last_known_coin_name) = last_known_coin_name {
630 client
631 .get_coin_record_by_name(&last_known_coin_name)
632 .await?
633 } else {
634 client.get_coin_record_by_name(&coin.name()).await?
635 };
636 while let Some(sc) = &singleton_coin {
637 info!(
638 "Found Next Coin, {} at height {}",
639 sc.coin.name(),
640 sc.confirmed_block_index
641 );
642 if sc.spent {
643 let last_spend = client.get_coin_spend(sc).await?;
644 let next_coin = get_most_recent_singleton_coin_from_coin_spend(&last_spend)?;
645 if let Some(pool_state) = solution_to_pool_state(&last_spend)? {
646 last_not_null_state = pool_state;
647 }
648 if let Some(nc) = next_coin {
649 singleton_coin = client.get_coin_record_by_name(&nc.name()).await?;
650 } else {
651 break; }
653 } else {
654 break;
655 }
656 }
657 if let Some(singleton_coin) = singleton_coin {
658 Ok(Some(PlotNft {
659 launcher_id,
660 singleton_coin,
661 pool_state: last_not_null_state,
662 delay_time: initial_extra_data.delay_time,
663 delay_puzzle_hash: initial_extra_data.delay_puzzle_hash,
664 }))
665 } else {
666 Ok(None)
667 }
668 } else {
669 Ok(None)
670 }
671 } else {
672 Ok(None)
673 }
674}
675
676pub async fn submit_next_state_spend_bundle(
677 client: Arc<FullnodeClient>,
678 pool_wallet: &PlotNFTWallet,
679 plot_nft: &PlotNft,
680 target_pool_state: &PoolState,
681 fee: u64,
682) -> Result<(), Error> {
683 let (travel_record, _) = pool_wallet
684 .generate_travel_transaction(
685 plot_nft,
686 target_pool_state,
687 fee,
688 &pool_wallet.info.constants,
689 )
690 .await?;
691 let coin_to_find = travel_record
692 .additions
693 .iter()
694 .find(|c| c.amount == 1)
695 .expect("Failed to find NFT coin");
696 match client
697 .push_tx(
698 &travel_record
699 .spend_bundle
700 .expect("Expected Transaction Record to have Spend bundle"),
701 )
702 .await?
703 {
704 TXStatus::SUCCESS => {
705 info!("Transaction Submitted Successfully. Waiting for coin to show as spent...");
706 loop {
707 if let Ok(Some(record)) = client.get_coin_record_by_name(&coin_to_find.name()).await
708 {
709 if let Ok(Some(record)) = client
710 .get_coin_record_by_name(&record.coin.parent_coin_info)
711 .await
712 {
713 info!(
714 "Found spent parent coin, Parent Coin was spent at {}",
715 record.spent_block_index
716 );
717 break;
718 }
719 }
720 tokio::time::sleep(Duration::from_secs(10)).await;
721 info!("Waiting for plot_nft spend to appear...");
722 }
723 Ok(())
724 }
725 TXStatus::PENDING => Err(Error::other("Transaction is pending")),
726 TXStatus::FAILED => Err(Error::other("Failed to submit transaction")),
727 }
728}
729
730pub async fn submit_next_state_spend_bundle_with_key(
731 client: Arc<FullnodeClient>,
732 secret_key: &SecretKey,
733 plot_nft: &PlotNft,
734 target_pool_state: &PoolState,
735 constants: &ConsensusConstants,
736) -> Result<(), Error> {
737 let (travel_record, _) = generate_travel_transaction_without_fee(
738 client.clone(),
739 |_| async { Ok(secret_key.clone()) },
740 plot_nft,
741 target_pool_state,
742 constants,
743 )
744 .await?;
745 let coin_to_find = travel_record
746 .additions
747 .iter()
748 .find(|c| c.amount == 1)
749 .expect("Failed to find NFT coin");
750 match client
751 .push_tx(
752 &travel_record
753 .spend_bundle
754 .expect("Expected Transaction Record to have Spend bundle"),
755 )
756 .await?
757 {
758 TXStatus::SUCCESS => {
759 info!("Transaction Submitted Successfully. Waiting for coin to show as spent...");
760 loop {
761 if let Ok(Some(record)) = client.get_coin_record_by_name(&coin_to_find.name()).await
762 {
763 if let Ok(Some(record)) = client
764 .get_coin_record_by_name(&record.coin.parent_coin_info)
765 .await
766 {
767 info!(
768 "Found spent parent coin, Parent Coin was spent at {}",
769 record.spent_block_index
770 );
771 break;
772 }
773 }
774 tokio::time::sleep(Duration::from_secs(10)).await;
775 info!("Waiting for plot_nft spend to appear...");
776 }
777 Ok(())
778 }
779 TXStatus::PENDING => Err(Error::other("Transaction is pending")),
780 TXStatus::FAILED => Err(Error::other("Failed to submit transaction")),
781 }
782}