use crate::{
cashnotes::{CashNoteBuilder, UnsignedTransfer, CASHNOTE_PURPOSE_OF_CHANGE},
rng, CashNote, CashNoteOutputDetails, DerivationIndex, DerivedSecretKey, Hash, Input,
MainPubkey, NanoTokens, Result, SignedSpend, Transaction, TransactionBuilder, TransferError,
UniquePubkey, NETWORK_ROYALTIES_PK,
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
pub type CashNotesAndSecretKey = Vec<(CashNote, Option<DerivedSecretKey>)>;
pub type TransferRecipientDetails = (NanoTokens, String, MainPubkey, DerivationIndex);
#[derive(custom_debug::Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OfflineTransfer {
pub tx: Transaction,
#[debug(skip)]
pub cash_notes_for_recipient: Vec<CashNote>,
#[debug(skip)]
pub change_cash_note: Option<CashNote>,
pub all_spend_requests: Vec<SignedSpend>,
}
impl OfflineTransfer {
pub fn from_transaction(
signed_spends: BTreeSet<SignedSpend>,
tx: Transaction,
change_id: UniquePubkey,
output_details: BTreeMap<UniquePubkey, CashNoteOutputDetails>,
) -> Result<Self> {
let cash_note_builder =
CashNoteBuilder::new(tx.clone(), output_details, signed_spends.clone());
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(Self {
tx,
cash_notes_for_recipient: created_cash_notes,
change_cash_note,
all_spend_requests: signed_spends.into_iter().collect(),
})
}
pub fn new(
available_cash_notes: CashNotesAndSecretKey,
recipients: Vec<(NanoTokens, String, MainPubkey, DerivationIndex)>,
change_to: MainPubkey,
input_reason_hash: Hash,
) -> Result<Self> {
let total_output_amount = recipients
.iter()
.try_fold(NanoTokens::zero(), |total, (amount, _, _, _)| {
total.checked_add(*amount)
})
.ok_or_else(|| {
TransferError::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 = TransferInputs {
cash_notes_to_spend,
recipients,
change: (change_amount, change_to),
};
create_offline_transfer_with(selected_inputs, input_reason_hash)
}
}
#[derive(Debug)]
struct TransferInputs {
pub cash_notes_to_spend: CashNotesAndSecretKey,
pub recipients: Vec<(NanoTokens, String, MainPubkey, DerivationIndex)>,
pub change: (NanoTokens, MainPubkey),
}
pub fn create_unsigned_transfer(
available_cash_notes: CashNotesAndSecretKey,
recipients: Vec<(NanoTokens, String, MainPubkey, DerivationIndex)>,
change_to: MainPubkey,
reason_hash: Hash,
) -> Result<UnsignedTransfer> {
let total_output_amount = recipients
.iter()
.try_fold(NanoTokens::zero(), |total, (amount, _, _, _)| {
total.checked_add(*amount)
})
.ok_or(TransferError::ExcessiveNanoValue)?;
let (cash_notes_to_spend, change_amount) =
select_inputs(available_cash_notes, total_output_amount)?;
let selected_inputs = TransferInputs {
cash_notes_to_spend,
recipients,
change: (change_amount, change_to),
};
let network_royalties: Vec<DerivationIndex> = selected_inputs
.recipients
.iter()
.filter(|(_, _, main_pubkey, _)| *main_pubkey == *NETWORK_ROYALTIES_PK)
.map(|(_, _, _, derivation_index)| *derivation_index)
.collect();
let (tx_builder, _src_txs, change_id) = create_transaction_builder_with(selected_inputs)?;
tx_builder.build_unsigned_transfer(reason_hash, network_royalties, change_id)
}
fn select_inputs(
available_cash_notes: CashNotesAndSecretKey,
total_output_amount: NanoTokens,
) -> Result<(CashNotesAndSecretKey, 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(|| {
TransferError::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(TransferError::NotEnoughBalance(
total_input_amount,
total_output_amount,
));
}
Ok((cash_notes_to_spend, change_amount))
}
fn create_transaction_builder_with(
selected_inputs: TransferInputs,
) -> Result<(
TransactionBuilder,
BTreeMap<crate::UniquePubkey, Transaction>,
crate::UniquePubkey,
)> {
let TransferInputs {
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.parent_tx.clone(),
cash_note.derivation_index,
));
let _ = src_txs.insert(cash_note.unique_pubkey(), cash_note.parent_tx);
}
let mut tx_builder = TransactionBuilder::default()
.add_inputs(inputs)
.add_outputs(selected_inputs.recipients);
let mut rng = rng::thread_rng();
let derivation_index = DerivationIndex::random(&mut rng);
let change_id = change_to.new_unique_pubkey(&derivation_index);
if !change.is_zero() {
tx_builder = tx_builder.add_output(
change,
CASHNOTE_PURPOSE_OF_CHANGE.to_string(),
change_to,
derivation_index,
);
}
Ok((tx_builder, src_txs, change_id))
}
fn create_offline_transfer_with(
selected_inputs: TransferInputs,
input_reason_hash: Hash,
) -> Result<OfflineTransfer> {
let network_royalties: Vec<DerivationIndex> = selected_inputs
.recipients
.iter()
.filter(|(_, _, main_pubkey, _)| *main_pubkey == *NETWORK_ROYALTIES_PK)
.map(|(_, _, _, derivation_index)| *derivation_index)
.collect();
let (tx_builder, src_txs, change_id) = create_transaction_builder_with(selected_inputs)?;
let cash_note_builder = tx_builder.build(input_reason_hash, network_royalties)?;
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(TransferError::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,
cash_notes_for_recipient: created_cash_notes,
change_cash_note,
all_spend_requests,
})
}