dg_xch_cli_lib/wallets/
plotnft_utils.rs

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; //Error?
652                    }
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}