use crate::wallets::common::sign_coin_spend;
use crate::wallets::memory_wallet::{MemoryWalletConfig, MemoryWalletStore};
use crate::wallets::{Wallet, WalletInfo, WalletStore};
use async_trait::async_trait;
use blst::min_pk::SecretKey;
use dg_xch_clients::api::full_node::FullnodeAPI;
use dg_xch_clients::rpc::full_node::FullnodeClient;
use dg_xch_core::blockchain::announcement::Announcement;
use dg_xch_core::blockchain::coin_record::CoinRecord;
use dg_xch_core::blockchain::coin_spend::CoinSpend;
use dg_xch_core::blockchain::sized_bytes::{Bytes32, Bytes48};
use dg_xch_core::blockchain::spend_bundle::SpendBundle;
use dg_xch_core::blockchain::transaction_record::{TransactionRecord, TransactionType};
use dg_xch_core::blockchain::tx_status::TXStatus;
use dg_xch_core::blockchain::wallet_type::{AmountWithPuzzleHash, WalletType};
use dg_xch_core::clvm::program::Program;
use dg_xch_core::consensus::constants::ConsensusConstants;
use dg_xch_core::constants::{FARMING_TO_POOL, LEAVING_POOL, POOL_PROTOCOL_VERSION};
use dg_xch_core::plots::PlotNft;
use dg_xch_core::pool::PoolState;
use dg_xch_core::traits::SizedBytes;
use dg_xch_keys::{
master_sk_to_singleton_owner_sk, master_sk_to_wallet_sk, master_sk_to_wallet_sk_unhardened,
};
use dg_xch_puzzles::clvm_puzzles::{
create_full_puzzle, create_travel_spend, get_most_recent_singleton_coin_from_coin_spend,
launcher_coin_spend_to_extra_data, pool_state_to_inner_puzzle, solution_to_pool_state,
SINGLETON_LAUNCHER_HASH,
};
use dg_xch_puzzles::p2_delegated_puzzle_or_hidden_puzzle::puzzle_hash_for_pk;
use log::info;
use num_traits::cast::ToPrimitive;
use std::collections::HashMap;
use std::future::Future;
use std::io::{Error, ErrorKind};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::select;
use tokio::sync::Mutex;
use tokio::task::JoinSet;
pub struct PlotNFTWallet {
info: WalletInfo<MemoryWalletStore>,
pub config: MemoryWalletConfig,
fullnode_client: Arc<FullnodeClient>,
}
#[async_trait]
impl Wallet<MemoryWalletStore, MemoryWalletConfig> for PlotNFTWallet {
fn create(
info: WalletInfo<MemoryWalletStore>,
config: MemoryWalletConfig,
) -> Result<Self, Error> {
Ok(Self {
fullnode_client: Arc::new(FullnodeClient::new(
&config.fullnode_host,
config.fullnode_port,
60,
config.fullnode_ssl_path.clone(),
&config.additional_headers,
)?),
info,
config,
})
}
fn create_simulator(
info: WalletInfo<MemoryWalletStore>,
config: MemoryWalletConfig,
) -> Result<Self, Error> {
Ok(Self {
fullnode_client: Arc::new(FullnodeClient::new_simulator(
&config.fullnode_host,
config.fullnode_port,
60,
)?),
info,
config,
})
}
fn name(&self) -> &str {
&self.info.name
}
async fn sync(&self) -> Result<bool, Error> {
let mut puzzle_hashes = vec![];
for index in 0..50 {
let wallet_sk = master_sk_to_wallet_sk(&self.info.master_sk, index).map_err(|e| {
Error::new(
ErrorKind::InvalidInput,
format!("Failed to parse Wallet SK: {e:?}"),
)
})?;
let pub_key: Bytes48 = wallet_sk.sk_to_pk().to_bytes().into();
let ph = puzzle_hash_for_pk(pub_key)?;
puzzle_hashes.push(ph);
let wallet_sk = master_sk_to_wallet_sk_unhardened(&self.info.master_sk, index)
.map_err(|e| {
Error::new(
ErrorKind::InvalidInput,
format!("Failed to parse Wallet SK: {e:?}"),
)
})?;
let pub_key: Bytes48 = wallet_sk.sk_to_pk().to_bytes().into();
let ph = puzzle_hash_for_pk(pub_key)?;
puzzle_hashes.push(ph);
}
let (spend, unspent) =
scrounge_for_standard_coins(self.fullnode_client.clone(), &puzzle_hashes).await?;
let store = self.info.wallet_store.lock().await;
let coins = store.standard_coins();
coins.lock().await.extend(spend.into_iter());
coins.lock().await.extend(unspent.into_iter());
Ok(true)
}
fn is_synced(&self) -> bool {
todo!()
}
fn wallet_info(&self) -> &WalletInfo<MemoryWalletStore> {
&self.info
}
fn wallet_store(&self) -> Arc<Mutex<MemoryWalletStore>> {
self.info.wallet_store.clone()
}
async fn create_spend_bundle(
&self,
_payments: Vec<AmountWithPuzzleHash>,
_input_coins: &[CoinRecord],
_change_puzzle_hash: Option<Bytes32>,
_allow_excess: bool,
_fee: i64,
_origin_id: Option<Bytes32>,
_solution_transformer: Option<Box<dyn Fn(Program) -> Program + 'static + Send + Sync>>,
) -> Result<SpendBundle, Error> {
todo!()
}
}
impl PlotNFTWallet {
pub fn new(
master_secret_key: SecretKey,
client: &FullnodeClient,
constants: Arc<ConsensusConstants>,
) -> Result<Self, Error> {
Self::create(
WalletInfo {
id: 1,
name: "pooling_wallet".to_string(),
wallet_type: WalletType::PoolingWallet,
constants,
master_sk: master_secret_key.clone(),
wallet_store: Arc::new(Mutex::new(MemoryWalletStore::new(master_secret_key, 0))),
data: String::new(),
},
MemoryWalletConfig {
fullnode_host: client.host.clone(),
fullnode_port: client.port,
fullnode_ssl_path: client.ssl_path.clone(),
additional_headers: client.additional_headers.clone(),
},
)
}
pub fn find_owner_key(&self, key_to_find: &Bytes48, limit: u32) -> Result<SecretKey, Error> {
for i in 0..limit {
let key = master_sk_to_singleton_owner_sk(&self.wallet_info().master_sk, i)?;
if key.sk_to_pk().to_bytes() == key_to_find.bytes() {
return Ok(key);
}
}
Err(Error::new(ErrorKind::NotFound, "Failed to find Owner SK"))
}
pub async fn generate_fee_transaction(
&self,
fee: u64,
coin_announcements: Option<&[Announcement]>,
) -> Result<TransactionRecord, Error> {
self.generate_signed_transaction(
0,
&self.get_new_puzzlehash().await?,
fee,
None,
None,
None,
false,
coin_announcements,
None,
None,
false,
None,
None,
None,
None,
None,
)
.await
}
#[allow(clippy::too_many_lines)]
#[allow(clippy::cast_sign_loss)]
pub async fn generate_travel_transaction(
&self,
plot_nft: &PlotNft,
target_pool_state: &PoolState,
fee: u64,
constants: &ConsensusConstants,
) -> Result<(TransactionRecord, Option<TransactionRecord>), Error> {
let launcher_coin = self
.fullnode_client
.get_coin_record_by_name(&plot_nft.launcher_id)
.await?
.ok_or_else(|| Error::other("Failed to load launcher_coin"))?;
let last_record = self
.fullnode_client
.get_coin_record_by_name(&plot_nft.singleton_coin.coin.parent_coin_info)
.await?
.ok_or_else(|| Error::other("Failed to load launcher_coin"))?;
let last_coin_spend = self.fullnode_client.get_coin_spend(&last_record).await?;
let next_state = if plot_nft.pool_state.state == FARMING_TO_POOL {
PoolState {
version: POOL_PROTOCOL_VERSION,
state: LEAVING_POOL,
target_puzzle_hash: plot_nft.pool_state.target_puzzle_hash,
owner_pubkey: plot_nft.pool_state.owner_pubkey,
pool_url: plot_nft.pool_state.pool_url.clone(),
relative_lock_height: plot_nft.pool_state.relative_lock_height,
}
} else {
target_pool_state.clone()
};
let new_inner_puzzle = pool_state_to_inner_puzzle(
&next_state,
launcher_coin.coin.name(),
constants.genesis_challenge,
plot_nft.delay_time as u64,
plot_nft.delay_puzzle_hash,
)?;
let new_full_puzzle = create_full_puzzle(&new_inner_puzzle, launcher_coin.coin.name())?;
let (outgoing_coin_spend, inner_puzzle) = create_travel_spend(
&last_coin_spend,
launcher_coin.coin,
&plot_nft.pool_state,
&next_state,
constants.genesis_challenge,
plot_nft.delay_time as u64,
plot_nft.delay_puzzle_hash,
)?;
let (additions, _cost) = last_coin_spend
.compute_additions_with_cost(constants.max_block_cost_clvm.to_u64().unwrap())?;
let singleton = &additions[0];
let singleton_id = singleton.name();
assert_eq!(
outgoing_coin_spend.coin.parent_coin_info,
last_coin_spend.coin.name()
);
assert_eq!(
outgoing_coin_spend.coin.parent_coin_info,
last_coin_spend.coin.name()
);
assert_eq!(outgoing_coin_spend.coin.name(), singleton_id);
assert_ne!(new_inner_puzzle, inner_puzzle);
let mut signed_spend_bundle = sign_coin_spend(
outgoing_coin_spend,
|_| async { self.find_owner_key(&plot_nft.pool_state.owner_pubkey, 500) },
HashMap::with_capacity(0),
constants,
)
.await?;
assert_eq!(
signed_spend_bundle.removals()[0].puzzle_hash,
singleton.puzzle_hash
);
assert_eq!(signed_spend_bundle.removals()[0].name(), singleton.name());
let fee_tx: Option<TransactionRecord> = None;
if fee > 0 {
let fee_tx = self.generate_fee_transaction(fee, None).await?;
if let Some(fee_bundle) = fee_tx.spend_bundle {
signed_spend_bundle = SpendBundle::aggregate(vec![signed_spend_bundle, fee_bundle])
.map_err(|e| Error::other(format!("Failed to parse Public key: {e:?}")))?;
}
}
let additions = signed_spend_bundle.additions()?;
let removals = signed_spend_bundle.removals();
let name = signed_spend_bundle.name()?;
let tx_record = TransactionRecord {
confirmed_at_height: 0,
created_at_time: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
to_puzzle_hash: new_full_puzzle.tree_hash(),
amount: 1,
fee_amount: fee,
confirmed: false,
sent: 0,
spend_bundle: Some(signed_spend_bundle),
additions,
removals,
wallet_id: 1,
sent_to: vec![],
trade_id: None,
memos: vec![],
transaction_type: TransactionType::OutgoingTx as u32,
name,
};
Ok((tx_record, fee_tx))
}
}
#[allow(clippy::cast_sign_loss)]
pub async fn generate_travel_transaction_without_fee<F, Fut>(
client: Arc<FullnodeClient>,
key_fn: F,
plot_nft: &PlotNft,
target_pool_state: &PoolState,
constants: &ConsensusConstants,
) -> Result<(TransactionRecord, Option<TransactionRecord>), Error>
where
F: Fn(&Bytes48) -> Fut,
Fut: Future<Output = Result<SecretKey, Error>>,
{
let launcher_coin = client
.get_coin_record_by_name(&plot_nft.launcher_id)
.await?
.ok_or_else(|| Error::other("Failed to load launcher_coin"))?;
let last_record = client
.get_coin_record_by_name(&plot_nft.singleton_coin.coin.parent_coin_info)
.await?
.ok_or_else(|| Error::other("Failed to load launcher_coin"))?;
let last_coin_spend = client.get_coin_spend(&last_record).await?;
let next_state = if plot_nft.pool_state.state == FARMING_TO_POOL {
PoolState {
version: POOL_PROTOCOL_VERSION,
state: LEAVING_POOL,
target_puzzle_hash: plot_nft.pool_state.target_puzzle_hash,
owner_pubkey: plot_nft.pool_state.owner_pubkey,
pool_url: plot_nft.pool_state.pool_url.clone(),
relative_lock_height: plot_nft.pool_state.relative_lock_height,
}
} else {
target_pool_state.clone()
};
let new_inner_puzzle = pool_state_to_inner_puzzle(
&next_state,
launcher_coin.coin.name(),
constants.genesis_challenge,
plot_nft.delay_time as u64,
plot_nft.delay_puzzle_hash,
)?;
let new_full_puzzle = create_full_puzzle(&new_inner_puzzle, launcher_coin.coin.name())?;
let (outgoing_coin_spend, inner_puzzle) = create_travel_spend(
&last_coin_spend,
launcher_coin.coin,
&plot_nft.pool_state,
&next_state,
constants.genesis_challenge,
plot_nft.delay_time as u64,
plot_nft.delay_puzzle_hash,
)?;
let (additions, _cost) = last_coin_spend
.compute_additions_with_cost(constants.max_block_cost_clvm.to_u64().unwrap())?;
let singleton = &additions[0];
let singleton_id = singleton.name();
assert_eq!(
outgoing_coin_spend.coin.parent_coin_info,
last_coin_spend.coin.name()
);
assert_eq!(
outgoing_coin_spend.coin.parent_coin_info,
last_coin_spend.coin.name()
);
assert_eq!(outgoing_coin_spend.coin.name(), singleton_id);
assert_ne!(new_inner_puzzle, inner_puzzle);
let signed_spend_bundle = sign_coin_spend(
outgoing_coin_spend,
key_fn,
HashMap::with_capacity(0),
constants,
)
.await?;
assert_eq!(
signed_spend_bundle.removals()[0].puzzle_hash,
singleton.puzzle_hash
);
assert_eq!(signed_spend_bundle.removals()[0].name(), singleton.name());
let additions = signed_spend_bundle.additions()?;
let removals = signed_spend_bundle.removals();
let name = signed_spend_bundle.name()?;
let tx_record = TransactionRecord {
confirmed_at_height: 0,
created_at_time: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
to_puzzle_hash: new_full_puzzle.tree_hash(),
amount: 1,
fee_amount: 0,
confirmed: false,
sent: 0,
spend_bundle: Some(signed_spend_bundle),
additions,
removals,
wallet_id: 1,
sent_to: vec![],
trade_id: None,
memos: vec![],
transaction_type: TransactionType::OutgoingTx as u32,
name,
};
Ok((tx_record, None))
}
pub async fn get_current_pool_state(
client: Arc<FullnodeClient>,
launcher_id: &Bytes32,
) -> Result<(PoolState, CoinSpend), Error> {
let mut last_spend: CoinSpend;
let mut saved_state: PoolState;
match client.get_coin_record_by_name(launcher_id).await? {
Some(lc) if lc.spent => {
last_spend = client.get_coin_spend(&lc).await?;
match solution_to_pool_state(&last_spend)? {
Some(state) => {
saved_state = state;
}
None => {
return Err(Error::new(
ErrorKind::InvalidData,
"Failed to Read Pool State",
));
}
}
}
Some(_) => {
return Err(Error::new(
ErrorKind::InvalidData,
format!("Genesis coin {} not spent", &launcher_id.to_string()),
));
}
None => {
return Err(Error::new(
ErrorKind::NotFound,
format!("Can not find genesis coin {}", &launcher_id),
));
}
}
let mut saved_spend: CoinSpend = last_spend.clone();
let mut last_not_none_state: PoolState = saved_state.clone();
loop {
match get_most_recent_singleton_coin_from_coin_spend(&last_spend)? {
None => {
return Err(Error::new(
ErrorKind::NotFound,
"Failed to find recent singleton from coin Record",
));
}
Some(next_coin) => match client.get_coin_record_by_name(&next_coin.name()).await? {
None => {
return Err(Error::new(
ErrorKind::NotFound,
"Failed to find Coin Record",
));
}
Some(next_coin_record) => {
if !next_coin_record.spent {
break;
}
last_spend = client.get_coin_spend(&next_coin_record).await?;
if let Ok(Some(pool_state)) = solution_to_pool_state(&last_spend) {
last_not_none_state = pool_state;
}
saved_spend = last_spend.clone();
saved_state = last_not_none_state.clone();
}
},
}
}
Ok((saved_state, saved_spend))
}
pub async fn scrounge_for_plotnft_by_key(
client: Arc<FullnodeClient>,
master_secret_key: &SecretKey,
) -> Result<Vec<PlotNft>, Error> {
let mut page = 0;
let mut plotnfs = vec![];
while page < 15 && plotnfs.is_empty() {
let mut puzzle_hashes = vec![];
for index in page * 50..(page + 1) * 50 {
let wallet_sk =
master_sk_to_wallet_sk_unhardened(master_secret_key, index).map_err(|e| {
Error::new(
ErrorKind::InvalidInput,
format!("Failed to parse Wallet SK: {e:?}"),
)
})?;
let pub_key: Bytes48 = wallet_sk.sk_to_pk().to_bytes().into();
let ph = puzzle_hash_for_pk(pub_key)?;
puzzle_hashes.push(ph);
}
plotnfs.extend(scrounge_for_plotnfts(client.clone(), &puzzle_hashes).await?);
page += 1;
}
Ok(plotnfs)
}
pub async fn scrounge_for_plotnfts(
client: Arc<FullnodeClient>,
puzzle_hashes: &[Bytes32],
) -> Result<Vec<PlotNft>, Error> {
info!("Fetching Coins for {} Puzzle Hashes", puzzle_hashes.len());
let hashes = client
.get_coin_records_by_puzzle_hashes(puzzle_hashes, Some(true), None, None)
.await?;
let mut spent: Vec<CoinRecord> = hashes.into_iter().filter(|c| c.spent).collect();
let plotnfts = Arc::new(Mutex::new(vec![]));
let mut thread_pool: JoinSet<Result<(), Error>> = JoinSet::new();
let counter = Arc::new(Mutex::new(0usize));
let total = spent.len();
let first_10: Vec<CoinRecord> = (0..std::cmp::min(10, total))
.map(|_| spent.remove(0))
.collect();
info!("Loading {total} Coin Spends");
for spent_coin in first_10 {
let plotnfts = plotnfts.clone();
let client = client.clone();
let counter = counter.clone();
thread_pool.spawn(async move {
let coin_spend = client.get_coin_spend(&spent_coin).await?;
for child in coin_spend.additions()? {
if child.puzzle_hash == *SINGLETON_LAUNCHER_HASH {
let launcher_id = child.name();
if let Some(plotnft) =
get_plotnft_by_launcher_id(client.clone(), launcher_id, None).await?
{
plotnfts.lock().await.push(plotnft);
}
*counter.lock().await += 1;
}
}
Ok(())
});
}
loop {
select! {
val = thread_pool.join_next() => {
info!("Finished: {} / {total}", *counter.lock().await);
let plotnfts = plotnfts.clone();
let client = client.clone();
let counter = counter.clone();
if let Some(spent_coin) = spent.pop() {
thread_pool.spawn(async move {
let coin_spend = client.get_coin_spend(&spent_coin).await?;
for child in coin_spend.additions()? {
if child.puzzle_hash == *SINGLETON_LAUNCHER_HASH {
let launcher_id = child.name();
if let Some(plotnft) = get_plotnft_by_launcher_id(client.clone(), launcher_id, None).await? {
plotnfts.lock().await.push(plotnft);
}
}
}
*counter.lock().await += 1;
Ok(())
});
continue;
}
if val.is_none() {
break;
}
}
() = tokio::time::sleep(Duration::from_secs(1)) => {
info!("Finished: {} / {total}", *counter.lock().await);
}
}
}
Ok(Arc::try_unwrap(plotnfts).unwrap().into_inner())
}
pub async fn scrounge_for_standard_coins(
client: Arc<FullnodeClient>,
puzzle_hashes: &[Bytes32],
) -> Result<(Vec<CoinRecord>, Vec<CoinRecord>), Error> {
let records = client
.get_coin_records_by_puzzle_hashes(puzzle_hashes, Some(true), None, None)
.await?;
let mut spent = vec![];
let mut unspent = vec![];
for coin in records {
if coin.spent {
spent.push(coin);
} else {
unspent.push(coin);
}
}
Ok((spent, unspent))
}
pub async fn get_pool_state(
client: Arc<FullnodeClient>,
launcher_id: Bytes32,
last_known_coin_name: Option<Bytes32>,
) -> Result<PoolState, Error> {
if let Some(plotnft) =
get_plotnft_by_launcher_id(client, launcher_id, last_known_coin_name).await?
{
Ok(plotnft.pool_state)
} else {
Err(Error::new(
ErrorKind::NotFound,
format!("Failed to find pool state for launcher_id {launcher_id}"),
))
}
}
pub async fn get_plotnft_by_launcher_id(
client: Arc<FullnodeClient>,
launcher_id: Bytes32,
last_known_coin_name: Option<Bytes32>,
) -> Result<Option<PlotNft>, Error> {
if let Some(starting_coin) = client.get_coin_record_by_name(&launcher_id).await? {
let spend = client.get_coin_spend(&starting_coin).await?;
let initial_extra_data = launcher_coin_spend_to_extra_data(&spend)?;
let first_coin = get_most_recent_singleton_coin_from_coin_spend(&spend)?;
if let Some(coin) = first_coin {
info!("Found Launcher Coin, Starting to crawl Coin History");
let mut last_not_null_state = initial_extra_data.pool_state.clone();
let mut singleton_coin = if let Some(last_known_coin_name) = last_known_coin_name {
client
.get_coin_record_by_name(&last_known_coin_name)
.await?
} else {
client.get_coin_record_by_name(&coin.name()).await?
};
while let Some(sc) = &singleton_coin {
info!(
"Found Next Coin, {} at height {}",
sc.coin.name(),
sc.confirmed_block_index
);
if sc.spent {
let last_spend = client.get_coin_spend(sc).await?;
let next_coin = get_most_recent_singleton_coin_from_coin_spend(&last_spend)?;
if let Some(pool_state) = solution_to_pool_state(&last_spend)? {
last_not_null_state = pool_state;
}
if let Some(nc) = next_coin {
singleton_coin = client.get_coin_record_by_name(&nc.name()).await?;
} else {
break; }
} else {
break;
}
}
if let Some(singleton_coin) = singleton_coin {
Ok(Some(PlotNft {
launcher_id,
singleton_coin,
pool_state: last_not_null_state,
delay_time: initial_extra_data.delay_time,
delay_puzzle_hash: initial_extra_data.delay_puzzle_hash,
}))
} else {
Ok(None)
}
} else {
Ok(None)
}
} else {
Ok(None)
}
}
pub async fn submit_next_state_spend_bundle(
client: Arc<FullnodeClient>,
pool_wallet: &PlotNFTWallet,
plot_nft: &PlotNft,
target_pool_state: &PoolState,
fee: u64,
) -> Result<(), Error> {
let (travel_record, _) = pool_wallet
.generate_travel_transaction(
plot_nft,
target_pool_state,
fee,
&pool_wallet.info.constants,
)
.await?;
let coin_to_find = travel_record
.additions
.iter()
.find(|c| c.amount == 1)
.expect("Failed to find NFT coin");
match client
.push_tx(
&travel_record
.spend_bundle
.expect("Expected Transaction Record to have Spend bundle"),
)
.await?
{
TXStatus::SUCCESS => {
info!("Transaction Submitted Successfully. Waiting for coin to show as spent...");
loop {
if let Ok(Some(record)) = client.get_coin_record_by_name(&coin_to_find.name()).await
{
if let Ok(Some(record)) = client
.get_coin_record_by_name(&record.coin.parent_coin_info)
.await
{
info!(
"Found spent parent coin, Parent Coin was spent at {}",
record.spent_block_index
);
break;
}
}
tokio::time::sleep(Duration::from_secs(10)).await;
info!("Waiting for plot_nft spend to appear...");
}
Ok(())
}
TXStatus::PENDING => Err(Error::other("Transaction is pending")),
TXStatus::FAILED => Err(Error::other("Failed to submit transaction")),
}
}
pub async fn submit_next_state_spend_bundle_with_key(
client: Arc<FullnodeClient>,
secret_key: &SecretKey,
plot_nft: &PlotNft,
target_pool_state: &PoolState,
constants: &ConsensusConstants,
) -> Result<(), Error> {
let (travel_record, _) = generate_travel_transaction_without_fee(
client.clone(),
|_| async { Ok(secret_key.clone()) },
plot_nft,
target_pool_state,
constants,
)
.await?;
let coin_to_find = travel_record
.additions
.iter()
.find(|c| c.amount == 1)
.expect("Failed to find NFT coin");
match client
.push_tx(
&travel_record
.spend_bundle
.expect("Expected Transaction Record to have Spend bundle"),
)
.await?
{
TXStatus::SUCCESS => {
info!("Transaction Submitted Successfully. Waiting for coin to show as spent...");
loop {
if let Ok(Some(record)) = client.get_coin_record_by_name(&coin_to_find.name()).await
{
if let Ok(Some(record)) = client
.get_coin_record_by_name(&record.coin.parent_coin_info)
.await
{
info!(
"Found spent parent coin, Parent Coin was spent at {}",
record.spent_block_index
);
break;
}
}
tokio::time::sleep(Duration::from_secs(10)).await;
info!("Waiting for plot_nft spend to appear...");
}
Ok(())
}
TXStatus::PENDING => Err(Error::other("Transaction is pending")),
TXStatus::FAILED => Err(Error::other("Failed to submit transaction")),
}
}