// Copyright 2023 MaidSafe.net limited.
//
// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. Please review the Licences for the specific language governing
// permissions and limitations relating to use of the SAFE Network Software.
use crate::Error;
use super::{error::Result, Client};
use backoff::{backoff::Backoff, ExponentialBackoff};
use futures::{future::join_all, TryFutureExt};
use libp2p::PeerId;
use sn_networking::{GetRecordError, PayeeQuote};
use sn_protocol::NetworkAddress;
use sn_transfers::{
CashNote, DerivationIndex, LocalWallet, MainPubkey, NanoTokens, Payment, PaymentQuote,
SignedSpend, SpendAddress, Transaction, Transfer, UniquePubkey, WalletError, WalletResult,
};
use std::{
collections::{BTreeMap, BTreeSet},
iter::Iterator,
time::{Duration, Instant},
};
use tokio::{task::JoinSet, time::sleep};
use xor_name::XorName;
/// A wallet client can be used to send and receive tokens to and from other wallets.
pub struct WalletClient {
client: Client,
wallet: LocalWallet,
}
impl WalletClient {
/// Create a new wallet client.
///
/// # Arguments
/// * `client` - A instance of the struct [`sn_client::Client`](Client)
/// * `wallet` - An instance of the struct [`sn_transfers::LocalWallet`](LocalWallet)
///
/// # Example
/// ```no_run
/// use sn_client::{Client, WalletClient, Error};
/// use tempfile::TempDir;
/// use bls::SecretKey;
/// use sn_transfers::{LocalWallet, MainSecretKey};
/// # #[tokio::main]
/// # async fn main() -> Result<(),Error>{
/// let client = Client::new(SecretKey::random(), None, false, None, None).await?;
/// let tmp_path = TempDir::new()?.path().to_owned();
/// let mut wallet = LocalWallet::load_from_path(&tmp_path,Some(MainSecretKey::new(SecretKey::random())))?;
/// let mut wallet_client = WalletClient::new(client, wallet);
/// # Ok(())
/// # }
/// ```
pub fn new(client: Client, wallet: LocalWallet) -> Self {
Self { client, wallet }
}
/// Stores the wallet to the local wallet directory.
/// # Example
/// ```no_run
/// # use sn_client::{Client, WalletClient, Error};
/// # use tempfile::TempDir;
/// # use bls::SecretKey;
/// # use sn_transfers::{LocalWallet, MainSecretKey};
/// # #[tokio::main]
/// # async fn main() -> Result<(),Error>{
/// # let client = Client::new(SecretKey::random(), None, false, None, None).await?;
/// # let tmp_path = TempDir::new()?.path().to_owned();
/// # let mut wallet = LocalWallet::load_from_path(&tmp_path,Some(MainSecretKey::new(SecretKey::random())))?;
/// let mut wallet_client = WalletClient::new(client, wallet);
/// wallet_client.store_local_wallet()?;
/// # Ok(())
/// # }
/// ```
pub fn store_local_wallet(&mut self) -> WalletResult<()> {
self.wallet.deposit_and_store_to_disk(&vec![])
}
/// Display the wallet balance
/// # Example
/// ```no_run
/// // Display the wallet balance in the terminal
/// # use sn_client::{Client, WalletClient, Error};
/// # use tempfile::TempDir;
/// # use bls::SecretKey;
/// # use sn_transfers::{LocalWallet, MainSecretKey};
/// # #[tokio::main]
/// # async fn main() -> Result<(),Error>{
/// # let client = Client::new(SecretKey::random(), None, false, None, None).await?;
/// # let tmp_path = TempDir::new()?.path().to_owned();
/// # let mut wallet = LocalWallet::load_from_path(&tmp_path,Some(MainSecretKey::new(SecretKey::random())))?;
/// let mut wallet_client = WalletClient::new(client, wallet);
/// println!("{}" ,wallet_client.balance());
/// # Ok(())
/// # }
pub fn balance(&self) -> NanoTokens {
self.wallet.balance()
}
/// See if any unconfirmed transactions exist.
/// # Example
/// ```no_run
/// // Print unconfirmed spends to the terminal
/// # use sn_client::{Client, WalletClient, Error};
/// # use tempfile::TempDir;
/// # use bls::SecretKey;
/// # use sn_transfers::{LocalWallet, MainSecretKey};
/// # #[tokio::main]
/// # async fn main() -> Result<(),Error>{
/// # let client = Client::new(SecretKey::random(), None, false, None, None).await?;
/// # let tmp_path = TempDir::new()?.path().to_owned();
/// # let mut wallet = LocalWallet::load_from_path(&tmp_path,Some(MainSecretKey::new(SecretKey::random())))?;
/// let mut wallet_client = WalletClient::new(client, wallet);
/// if wallet_client.unconfirmed_spend_requests_exist() {println!("Unconfirmed spends exist!")};
/// # Ok(())
/// # }
pub fn unconfirmed_spend_requests_exist(&self) -> bool {
self.wallet.unconfirmed_spend_requests_exist()
}
/// Get unconfirmed transactions
//TODO: Unused
pub fn unconfirmed_spend_requests(&self) -> &BTreeSet<SignedSpend> {
self.wallet.unconfirmed_spend_requests()
}
/// Returns the Cached Payment for a provided NetworkAddress.
///
/// # Arguments
/// * `address` - The [`NetworkAddress`](NetworkAddress).
///
/// # Example
/// ```no_run
/// // Getting the payment for an address using a random PeerId
/// # use sn_client::{Client, WalletClient, Error};
/// # use tempfile::TempDir;
/// # use bls::SecretKey;
/// # use sn_transfers::{LocalWallet, MainSecretKey};
/// # #[tokio::main]
/// # async fn main() -> Result<(),Error>{
/// # use std::io::Bytes;
/// # let client = Client::new(SecretKey::random(), None, false, None, None).await?;
/// # let tmp_path = TempDir::new()?.path().to_owned();
/// # let mut wallet = LocalWallet::load_from_path(&tmp_path,Some(MainSecretKey::new(SecretKey::random())))?;
/// use libp2p_identity::PeerId;
/// use sn_protocol::NetworkAddress;
///
/// let mut wallet_client = WalletClient::new(client, wallet);
/// let network_address = NetworkAddress::from_peer(PeerId::random());
/// let payment = wallet_client.get_payment_for_addr(&network_address)?;
/// # Ok(())
/// # }
/// ```
pub fn get_payment_for_addr(&self, address: &NetworkAddress) -> WalletResult<Payment> {
match &address.as_xorname() {
Some(xorname) => {
let payment_details = self
.wallet
.get_cached_payment_for_xorname(xorname)
.ok_or(WalletError::NoPaymentForAddress)?;
let payment = payment_details.to_payment();
debug!("Payment retrieved for {xorname:?} from wallet: {payment:?}");
info!("Payment retrieved for {xorname:?} from wallet");
Ok(payment)
}
None => Err(WalletError::InvalidAddressType),
}
}
/// Remove the payment for a given network address from disk.
///
/// # Arguments
/// * `address` - The [`NetworkAddress`](NetworkAddress).
///
/// # Example
/// ```no_run
/// // Removing a payment address using a random PeerId
/// # use sn_client::{Client, WalletClient, Error};
/// # use tempfile::TempDir;
/// # use bls::SecretKey;
/// # use sn_transfers::{LocalWallet, MainSecretKey};
/// # #[tokio::main]
/// # async fn main() -> Result<(),Error>{
/// # use std::io::Bytes;
/// # let client = Client::new(SecretKey::random(), None, false, None, None).await?;
/// # let tmp_path = TempDir::new()?.path().to_owned();
/// # let mut wallet = LocalWallet::load_from_path(&tmp_path,Some(MainSecretKey::new(SecretKey::random())))?;
/// use libp2p_identity::PeerId;
/// use sn_protocol::NetworkAddress;
///
/// let mut wallet_client = WalletClient::new(client, wallet);
/// let network_address = NetworkAddress::from_peer(PeerId::random());
/// let payment = wallet_client.remove_payment_for_addr(&network_address)?;
/// # Ok(())
/// # }
/// ```
pub fn remove_payment_for_addr(&self, address: &NetworkAddress) -> WalletResult<()> {
match &address.as_xorname() {
Some(xorname) => {
self.wallet.remove_payment_for_xorname(xorname);
Ok(())
}
None => Err(WalletError::InvalidAddressType),
}
}
/// Remove CashNote from available_cash_notes
//TODO: Unused
pub fn mark_note_as_spent(&mut self, cash_note_key: UniquePubkey) {
self.wallet.mark_note_as_spent(cash_note_key);
}
/// Send tokens to another wallet. Can also verify the store has been successful.
/// Verification will be attempted via GET request through a Spend on the network.
///
/// # Arguments
/// * `amount` - [`NanoTokens`](NanoTokens).
/// * `to` - [`MainPubkey`](MainPubkey).
/// * `verify_store` - A boolean to verify store. Set this to true for mandatory verification.
///
/// # Example
/// ```no_run
/// # use sn_client::{Client, WalletClient, Error};
/// # use tempfile::TempDir;
/// # use bls::SecretKey;
/// # use sn_transfers::{LocalWallet, MainSecretKey};
/// # #[tokio::main]
/// # async fn main() -> Result<(),Error>{
/// # use std::io::Bytes;
/// # let client = Client::new(SecretKey::random(), None, false, None, None).await?;
/// # let tmp_path = TempDir::new()?.path().to_owned();
/// # let mut wallet = LocalWallet::load_from_path(&tmp_path,Some(MainSecretKey::new(SecretKey::random())))?;
/// use sn_transfers::NanoTokens;
/// let mut wallet_client = WalletClient::new(client, wallet);
/// let nano = NanoTokens::from(10);
/// let main_pub_key = MainSecretKey::random().main_pubkey();
/// let payment = wallet_client.send_cash_note(nano,main_pub_key, true);
/// # Ok(())
/// # }
/// ```
pub async fn send_cash_note(
&mut self,
amount: NanoTokens,
to: MainPubkey,
verify_store: bool,
) -> WalletResult<CashNote> {
let created_cash_notes = self.wallet.local_send(vec![(amount, to)], None)?;
// send to network
if let Err(error) = self
.client
.send_spends(
self.wallet.unconfirmed_spend_requests().iter(),
verify_store,
)
.await
{
return Err(WalletError::CouldNotSendMoney(format!(
"The transfer was not successfully registered in the network: {error:?}"
)));
} else {
// clear unconfirmed txs
self.wallet.clear_confirmed_spend_requests();
}
// return the first CashNote (assuming there is only one because we only sent to one recipient)
match &created_cash_notes[..] {
[cashnote] => Ok(cashnote.clone()),
[_multiple, ..] => Err(WalletError::CouldNotSendMoney(
"Multiple CashNotes were returned from the transaction when only one was expected. This is a BUG."
.into(),
)),
[] => Err(WalletError::CouldNotSendMoney(
"No CashNotes were returned from the wallet.".into(),
)),
}
}
/// Send signed spends to another wallet.
/// Can optionally verify if the store has been successful.
/// Verification will be attempted via GET request through a Spend on the network.
// TODO: Unused
async fn send_signed_spends(
&mut self,
signed_spends: BTreeSet<SignedSpend>,
tx: Transaction,
change_id: UniquePubkey,
output_details: BTreeMap<UniquePubkey, (MainPubkey, DerivationIndex)>,
verify_store: bool,
) -> WalletResult<CashNote> {
let created_cash_notes =
self.wallet
.prepare_signed_transfer(signed_spends, tx, change_id, output_details)?;
// send to network
if let Err(error) = self
.client
.send_spends(
self.wallet.unconfirmed_spend_requests().iter(),
verify_store,
)
.await
{
return Err(WalletError::CouldNotSendMoney(format!(
"The transfer was not successfully registered in the network: {error:?}"
)));
} else {
// clear unconfirmed txs
self.wallet.clear_confirmed_spend_requests();
}
// return the first CashNote (assuming there is only one because we only sent to one recipient)
match &created_cash_notes[..] {
[cashnote] => Ok(cashnote.clone()),
[_multiple, ..] => Err(WalletError::CouldNotSendMoney(
"Multiple CashNotes were returned from the transaction when only one was expected. This is a BUG."
.into(),
)),
[] => Err(WalletError::CouldNotSendMoney(
"No CashNotes were returned from the wallet.".into(),
)),
}
}
/// Get storecost from the network
/// Returns the MainPubkey of the node to pay and the price in NanoTokens
pub async fn get_store_cost_at_address(
&self,
address: NetworkAddress,
) -> WalletResult<PayeeQuote> {
self.client
.network
.get_store_costs_from_network(address)
.await
.map_err(|error| WalletError::CouldNotSendMoney(error.to_string()))
}
/// Send tokens to nodes closest to the data we want to make storage payment for.
///
/// The returned result is: ((storage_cost, royalties_fees), (payee_map, skipped_chunks))
/// Where:
/// `storage_cost` is the total cost for the all contents
/// `royalties_fees` is the total royalty fess for the all contents
/// `payee_map` is the payees selected for each content
/// `skipped_chunks` is the list of content already exists in network and no need to upload
///
/// Note storage cost is _per record_, and it's zero if not required for this operation.
/// This can optionally verify the store has been successful.
/// * verify_store - A boolean to verify store. Set this to true for mandatory verification.
pub async fn pay_for_storage(
&mut self,
content_addrs: impl Iterator<Item = NetworkAddress>,
) -> WalletResult<(
(NanoTokens, NanoTokens),
(Vec<(XorName, PeerId)>, Vec<XorName>),
)> {
let verify_store = true;
let c: Vec<_> = content_addrs.collect();
// Using default ExponentialBackoff doesn't make sense,
// as it will just fail after the first payment failure.
let mut backoff = ExponentialBackoff::default();
let mut last_err = "No retries".to_string();
while let Some(delay) = backoff.next_backoff() {
trace!("Paying for storage (w/backoff retries) for: {:?}", c);
match self
.pay_for_storage_once(c.clone().into_iter(), verify_store)
.await
{
Ok(cost) => return Ok(cost),
Err(WalletError::CouldNotSendMoney(err)) => {
warn!("Attempt to pay for data failed: {err:?}");
last_err = err;
sleep(delay).await;
}
Err(err) => return Err(err),
}
}
Err(WalletError::CouldNotSendMoney(last_err))
}
/// Existing chunks will have the store cost set to Zero.
/// The payment procedure shall be skipped, and the chunk upload as well.
/// Hence the list of existing chunks will be returned.
async fn pay_for_storage_once(
&mut self,
content_addrs: impl Iterator<Item = NetworkAddress>,
verify_store: bool,
) -> WalletResult<(
(NanoTokens, NanoTokens),
(Vec<(XorName, PeerId)>, Vec<XorName>),
)> {
// get store cost from network in parrallel
let mut tasks = JoinSet::new();
for content_addr in content_addrs {
let client = self.client.clone();
tasks.spawn(async move {
let cost = client
.network
.get_store_costs_from_network(content_addr.clone())
.await
.map_err(|error| WalletError::CouldNotSendMoney(error.to_string()));
debug!("Storecosts retrieved for {content_addr:?} {cost:?}");
(content_addr, cost)
});
}
debug!("Pending store cost tasks: {:?}", tasks.len());
// collect store costs
let mut cost_map = BTreeMap::default();
let mut skipped_chunks = vec![];
let mut payee_map = vec![];
while let Some(res) = tasks.join_next().await {
match res {
Ok((content_addr, Ok(cost))) => {
if let Some(xorname) = content_addr.as_xorname() {
if cost.2.cost == NanoTokens::zero() {
skipped_chunks.push(xorname);
debug!("Skipped existing chunk {content_addr:?}");
} else {
let _ = cost_map.insert(xorname, (cost.1, cost.2));
payee_map.push((xorname, cost.0));
debug!("Storecost inserted into payment map for {content_addr:?}");
}
} else {
warn!("Cannot get store cost for a content that is not a data type: {content_addr:?}");
}
}
Ok((content_addr, Err(err))) => {
warn!("Cannot get store cost for {content_addr:?} with error {err:?}");
return Err(err);
}
Err(e) => {
return Err(WalletError::CouldNotSendMoney(format!(
"Storecost get task failed: {e:?}"
)));
}
}
}
info!("Storecosts retrieved");
// pay for records
Ok((
self.pay_for_records(&cost_map, verify_store).await?,
(payee_map, skipped_chunks),
))
}
/// Send tokens to nodes closest to the data we want to make storage payment for.
/// Returns the amount paid for storage, including the network royalties fee paid.
/// This can optionally verify the store has been successful.
/// This will attempt to GET the cash_note from the network.
pub async fn pay_for_records(
&mut self,
cost_map: &BTreeMap<XorName, (MainPubkey, PaymentQuote)>,
verify_store: bool,
) -> WalletResult<(NanoTokens, NanoTokens)> {
// Before wallet progress, there shall be no `unconfirmed_spend_requests`
// Here, just re-upload again. The caller shall carry out a re-try later on.
if self.wallet.unconfirmed_spend_requests_exist() {
info!("Pre-Unconfirmed transactions exist. Resending in 1 second...");
sleep(Duration::from_secs(1)).await;
self.resend_pending_txs(verify_store).await;
return Err(WalletError::CouldNotSendMoney(
"Wallet has pre-unconfirmed transactions. Resend, and try again.".to_string(),
));
}
let start = Instant::now();
let total_cost = self.wallet.local_send_storage_payment(cost_map)?;
trace!(
"local_send_storage_payment of {} chunks completed in {:?}",
cost_map.len(),
start.elapsed()
);
// send to network
trace!("Sending storage payment transfer to the network");
let start = Instant::now();
let spend_attempt_result = self
.client
.send_spends(
self.wallet.unconfirmed_spend_requests().iter(),
verify_store,
)
.await;
trace!(
"send_spends of {} chunks completed in {:?}",
cost_map.len(),
start.elapsed()
);
// Here is bit risky that for the whole bunch of spends to the chunks' store_costs and royalty_fee
// they will get re-paid again for ALL, if any one of the payment failed to be put.
let start = Instant::now();
if let Err(error) = spend_attempt_result {
warn!("The storage payment transfer was not successfully registered in the network: {error:?}. It will be retried later.");
// if we have a DoubleSpend error, lets remove the CashNote from the wallet
if let WalletError::DoubleSpendAttemptedForCashNotes(spent_cash_notes) = &error {
for cash_note_key in spent_cash_notes {
warn!("Removing double spends CashNote from wallet: {cash_note_key:?}");
self.wallet.mark_note_as_spent(*cash_note_key);
self.wallet.clear_specific_spend_request(*cash_note_key);
}
}
self.wallet.store_unconfirmed_spend_requests()?;
return Err(WalletError::CouldNotSendMoney(format!(
"The storage payment transfer was not successfully registered in the network: {error:?}"
)));
} else {
info!("Spend has completed: {:?}", spend_attempt_result);
self.wallet.clear_confirmed_spend_requests();
}
trace!(
"clear up spends of {} chunks completed in {:?}",
cost_map.len(),
start.elapsed()
);
Ok(total_cost)
}
/// Resend failed transactions. This can optionally verify the store has been successful.
/// This will attempt to GET the cash_note from the network.
pub async fn resend_pending_txs(&mut self, verify_store: bool) {
if self
.client
.send_spends(
self.wallet.unconfirmed_spend_requests().iter(),
verify_store,
)
.await
.is_ok()
{
self.wallet.clear_confirmed_spend_requests();
// We might want to be _really_ sure and do the below
// as well, but it's not necessary.
// use crate::domain::wallet::VerifyingClient;
// client.verify(tx_hash).await.ok();
}
}
/// Return the wallet.
pub fn into_wallet(self) -> LocalWallet {
self.wallet
}
/// Return ref to inner waller
pub fn mut_wallet(&mut self) -> &mut LocalWallet {
&mut self.wallet
}
}
impl Client {
/// Send spend requests to the network.
/// This can optionally verify the spends have been correctly stored before returning
pub async fn send_spends(
&self,
spend_requests: impl Iterator<Item = &SignedSpend>,
verify_store: bool,
) -> WalletResult<()> {
let mut tasks = Vec::new();
for spend_request in spend_requests {
debug!(
"sending spend request to the network: {:?}: {spend_request:#?}",
spend_request.unique_pubkey()
);
let the_task = async move {
let cash_note_key = spend_request.unique_pubkey();
let result = self
.network_store_spend(spend_request.clone(), verify_store)
.await;
(cash_note_key, result)
};
tasks.push(the_task);
}
let mut spent_cash_notes = BTreeSet::default();
for (cash_note_key, spend_attempt_result) in join_all(tasks).await {
// This is a record mismatch on spend, we need to clean up and remove the spent CashNote from the wallet
// This only happens if we're verifying the store
if let Err(Error::Network(sn_networking::Error::GetRecordError(
GetRecordError::RecordDoesNotMatch(record_key),
))) = spend_attempt_result
{
warn!("Record mismatch on spend, removing CashNote from wallet: {record_key:?}");
spent_cash_notes.insert(*cash_note_key);
} else {
return spend_attempt_result
.map_err(|err| WalletError::CouldNotSendMoney(err.to_string()));
}
}
if spent_cash_notes.is_empty() {
Ok(())
} else {
Err(WalletError::DoubleSpendAttemptedForCashNotes(
spent_cash_notes,
))
}
}
/// Receive a Transfer, verify and redeem CashNotes from the Network.
pub async fn receive(
&self,
transfer: &Transfer,
wallet: &LocalWallet,
) -> WalletResult<Vec<CashNote>> {
let cashnotes = self
.network
.verify_and_unpack_transfer(transfer, wallet)
.map_err(|e| WalletError::CouldNotReceiveMoney(format!("{e:?}")))
.await?;
Ok(cashnotes)
}
/// Verify that the spends referred to (in the CashNote) exist on the network.
pub async fn verify_cashnote(&self, cash_note: &CashNote) -> WalletResult<()> {
// We need to get all the spends in the cash_note from the network,
// and compare them to the spends in the cash_note, to know if the
// transfer is considered valid in the network.
let mut tasks = Vec::new();
for spend in &cash_note.signed_spends {
let address = SpendAddress::from_unique_pubkey(spend.unique_pubkey());
debug!(
"Getting spend for pubkey {:?} from network at {address:?}",
spend.unique_pubkey()
);
tasks.push(self.get_spend_from_network(address));
}
let mut received_spends = std::collections::BTreeSet::new();
for result in join_all(tasks).await {
let network_valid_spend =
result.map_err(|err| WalletError::CouldNotVerifyTransfer(err.to_string()))?;
let _ = received_spends.insert(network_valid_spend);
}
// If all the spends in the cash_note are the same as the ones in the network,
// we have successfully verified that the cash_note is globally recognised and therefor valid.
if received_spends == cash_note.signed_spends {
return Ok(());
}
Err(WalletError::CouldNotVerifyTransfer(
"The spends in network were not the same as the ones in the CashNote. The parents of this CashNote are probably double spends.".into(),
))
}
}
/// Use the client to send a CashNote from a local wallet to an address.
/// This marks the spent CashNote as spent in the Network
pub async fn send(
from: LocalWallet,
amount: NanoTokens,
to: MainPubkey,
client: &Client,
verify_store: bool,
) -> Result<CashNote> {
if amount.is_zero() {
return Err(Error::AmountIsZero);
}
let mut wallet_client = WalletClient::new(client.clone(), from);
let mut did_error = false;
// Wallet shall be all clear to progress forward.
let mut attempts = 0;
while wallet_client.unconfirmed_spend_requests_exist() {
info!("Pre-Unconfirmed transactions exist, sending again after 1 second...");
sleep(Duration::from_secs(1)).await;
wallet_client.resend_pending_txs(verify_store).await;
if attempts > 10 {
// save the error state, but break out of the loop so we can save
did_error = true;
break;
}
attempts += 1;
}
if did_error {
error!("Wallet has pre-unconfirmed transactions, can't progress further.");
println!("Wallet has pre-unconfirmed transactions, can't progress further.");
return Err(WalletError::UnconfirmedTxAfterRetries.into());
}
let new_cash_note = wallet_client
.send_cash_note(amount, to, verify_store)
.await
.map_err(|err| {
error!("Could not send cash note, err: {err:?}");
err
})?;
if verify_store {
attempts = 0;
while wallet_client.unconfirmed_spend_requests_exist() {
info!("Unconfirmed txs exist, sending again after 1 second...");
sleep(Duration::from_secs(1)).await;
wallet_client.resend_pending_txs(verify_store).await;
if attempts > 10 {
// save the error state, but break out of the loop so we can save
did_error = true;
break;
}
attempts += 1;
}
}
if did_error {
wallet_client
.into_wallet()
.store_unconfirmed_spend_requests()?;
return Err(WalletError::UnconfirmedTxAfterRetries.into());
}
wallet_client
.into_wallet()
.deposit_and_store_to_disk(&vec![new_cash_note.clone()])?;
Ok(new_cash_note)
}
/// Send tokens to another wallet. Can optionally verify the store has been successful.
/// Verification will be attempted via GET request through a Spend on the network.
pub async fn broadcast_signed_spends(
from: LocalWallet,
client: &Client,
signed_spends: BTreeSet<SignedSpend>,
tx: Transaction,
change_id: UniquePubkey,
output_details: BTreeMap<UniquePubkey, (MainPubkey, DerivationIndex)>,
verify_store: bool,
) -> WalletResult<CashNote> {
let mut wallet_client = WalletClient::new(client.clone(), from);
let mut did_error = false;
// Wallet shall be all clear to progress forward.
let mut attempts = 0;
while wallet_client.unconfirmed_spend_requests_exist() {
info!("Pre-Unconfirmed txs exist, sending again after 1 second...");
sleep(Duration::from_secs(1)).await;
wallet_client.resend_pending_txs(verify_store).await;
if attempts > 10 {
// save the error state, but break out of the loop so we can save
did_error = true;
break;
}
attempts += 1;
}
if did_error {
error!("Wallet has pre-unconfirmed txs, cann't progress further.");
println!("Wallet has pre-unconfirmed txs, cann't progress further.");
return Err(WalletError::UnconfirmedTxAfterRetries);
}
let new_cash_note = wallet_client
.send_signed_spends(signed_spends, tx, change_id, output_details, verify_store)
.await
.map_err(|err| {
error!("Could not send signed spends, err: {err:?}");
err
})?;
if verify_store {
attempts = 0;
while wallet_client.unconfirmed_spend_requests_exist() {
info!("Unconfirmed txs exist, sending again after 1 second...");
sleep(Duration::from_secs(1)).await;
wallet_client.resend_pending_txs(verify_store).await;
if attempts > 10 {
// save the error state, but break out of the loop so we can save
did_error = true;
break;
}
attempts += 1;
}
}
if did_error {
wallet_client
.into_wallet()
.store_unconfirmed_spend_requests()?;
return Err(WalletError::UnconfirmedTxAfterRetries);
}
wallet_client
.into_wallet()
.deposit_and_store_to_disk(&vec![new_cash_note.clone()])?;
Ok(new_cash_note)
}