use crate::{
rng, CashNote, DerivationIndex, DerivedSecretKey, Hash, Input, MainPubkey, NanoTokens,
SignedSpend, Transaction, TransactionBuilder, UniquePubkey,
};
use crate::{Error, Result};
use std::collections::BTreeMap;
use xor_name::XorName;
#[derive(custom_debug::Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct OfflineTransfer {
pub tx: Transaction,
#[debug(skip)]
pub created_cash_notes: Vec<CashNote>,
#[debug(skip)]
pub change_cash_note: Option<CashNote>,
pub all_spend_requests: Vec<SignedSpend>,
}
pub type PaymentDetails = (UniquePubkey, MainPubkey, NanoTokens);
pub type ContentPaymentsIdMap = BTreeMap<XorName, Vec<PaymentDetails>>;
#[derive(Debug)]
struct TranferInputs {
pub cash_notes_to_spend: Vec<(CashNote, DerivedSecretKey)>,
pub recipients: Vec<(NanoTokens, MainPubkey, DerivationIndex)>,
pub change: (NanoTokens, MainPubkey),
}
pub fn create_offline_transfer(
available_cash_notes: Vec<(CashNote, DerivedSecretKey)>,
recipients: Vec<(NanoTokens, MainPubkey, DerivationIndex)>,
change_to: MainPubkey,
reason_hash: Hash,
) -> Result<OfflineTransfer> {
let total_output_amount = recipients
.iter()
.try_fold(NanoTokens::zero(), |total, (amount, _, _)| {
total.checked_add(*amount)
})
.ok_or_else(|| {
Error::CashNoteReissueFailed(
"Overflow occurred while summing the amounts for the recipients.".to_string(),
)
})?;
let (cash_notes_to_spend, change_amount) =
select_inputs(available_cash_notes, total_output_amount)?;
let selected_inputs = TranferInputs {
cash_notes_to_spend,
recipients,
change: (change_amount, change_to),
};
create_offline_transfer_with(selected_inputs, reason_hash)
}
fn select_inputs(
available_cash_notes: Vec<(CashNote, DerivedSecretKey)>,
total_output_amount: NanoTokens,
) -> Result<(Vec<(CashNote, DerivedSecretKey)>, NanoTokens)> {
let mut cash_notes_to_spend = Vec::new();
let mut total_input_amount = NanoTokens::zero();
let mut change_amount = total_output_amount;
for (cash_note, derived_key) in available_cash_notes {
let input_key = cash_note.unique_pubkey();
let cash_note_balance = match cash_note.value() {
Ok(token) => token,
Err(err) => {
warn!(
"Ignoring input CashNote (id: {input_key:?}) due to missing an output: {err:?}"
);
continue;
}
};
cash_notes_to_spend.push((cash_note, derived_key));
total_input_amount = total_input_amount.checked_add(cash_note_balance)
.ok_or_else(|| {
Error::CashNoteReissueFailed(
"Overflow occurred while increasing total input amount while trying to cover the output CashNotes."
.to_string(),
)
})?;
match change_amount.checked_sub(cash_note_balance) {
Some(pending_output) => {
change_amount = pending_output;
if change_amount.as_nano() == 0 {
break;
}
}
None => {
change_amount =
NanoTokens::from(cash_note_balance.as_nano() - change_amount.as_nano());
break;
}
}
}
if total_output_amount > total_input_amount {
return Err(Error::NotEnoughBalance(
total_input_amount,
total_output_amount,
));
}
Ok((cash_notes_to_spend, change_amount))
}
fn create_offline_transfer_with(
selected_inputs: TranferInputs,
reason_hash: Hash,
) -> Result<OfflineTransfer> {
let TranferInputs {
change: (change, change_to),
..
} = selected_inputs;
let mut inputs = vec![];
let mut src_txs = BTreeMap::new();
for (cash_note, derived_key) in selected_inputs.cash_notes_to_spend {
let token = match cash_note.value() {
Ok(token) => token,
Err(err) => {
warn!("Ignoring cash_note, as it didn't have the correct derived key: {err}");
continue;
}
};
let input = Input {
unique_pubkey: cash_note.unique_pubkey(),
amount: token,
};
inputs.push((input, derived_key, cash_note.src_tx.clone()));
let _ = src_txs.insert(cash_note.unique_pubkey(), cash_note.src_tx);
}
let mut tx_builder = TransactionBuilder::default()
.add_inputs(inputs)
.add_outputs(selected_inputs.recipients);
let mut rng = rng::thread_rng();
let derivation_index = UniquePubkey::random_derivation_index(&mut rng);
let change_id = change_to.new_unique_pubkey(&derivation_index);
if !change.is_zero() {
tx_builder = tx_builder.add_output(change, change_to, derivation_index);
}
let cash_note_builder = tx_builder.build(reason_hash)?;
let tx = cash_note_builder.spent_tx.clone();
let signed_spends: BTreeMap<_, _> = cash_note_builder
.signed_spends()
.into_iter()
.map(|spend| (spend.unique_pubkey(), spend))
.collect();
if !signed_spends
.iter()
.all(|(unique_pubkey, _)| src_txs.contains_key(*unique_pubkey))
{
return Err(Error::CashNoteReissueFailed(
"Not all signed spends could be matched to a source cash_note transaction.".to_string(),
));
}
let mut all_spend_requests = vec![];
for (_, signed_spend) in signed_spends.into_iter() {
all_spend_requests.push(signed_spend.to_owned());
}
let mut created_cash_notes: Vec<_> = cash_note_builder
.build()?
.into_iter()
.map(|(cash_note, _)| cash_note)
.collect();
let mut change_cash_note = None;
created_cash_notes.retain(|created| {
if created.unique_pubkey() == change_id {
change_cash_note = Some(created.clone());
false
} else {
true
}
});
Ok(OfflineTransfer {
tx,
created_cash_notes,
change_cash_note,
all_spend_requests,
})
}