use super::{CreatedDbc, Error, Inputs, Result, SpendRequest, TransferOutputs};
use sn_dbc::{
rng, Dbc, DbcIdSource, DerivedKey, Hash, InputHistory, PublicAddress, RevealedInput, Token,
TransactionBuilder,
};
use std::collections::BTreeMap;
pub fn create_transfer(
available_dbcs: Vec<(Dbc, DerivedKey)>,
recipients: Vec<(Token, DbcIdSource)>,
change_to: PublicAddress,
reason_hash: Hash,
) -> Result<TransferOutputs> {
let selected_inputs = select_inputs(available_dbcs, recipients, change_to)?;
create_transfer_with(selected_inputs, reason_hash)
}
fn select_inputs(
available_dbcs: Vec<(Dbc, DerivedKey)>,
recipients: Vec<(Token, DbcIdSource)>,
change_to: PublicAddress,
) -> Result<Inputs> {
let mut dbcs_to_spend = Vec::new();
let mut total_input_amount = Token::zero();
let total_output_amount = recipients
.iter()
.fold(Some(Token::zero()), |total, (amount, _)| {
total.and_then(|t| t.checked_add(*amount))
})
.ok_or_else(|| {
Error::DbcReissueFailed(
"Overflow occurred while summing the amounts for the recipients.".to_string(),
)
})?;
let mut change_amount = total_output_amount;
for (dbc, derived_key) in available_dbcs {
let input_key = dbc.id();
let dbc_balance = match dbc.revealed_amount(&derived_key) {
Ok(revealed_amount) => Token::from_nano(revealed_amount.value()),
Err(err) => {
warn!("Ignoring input Dbc (id: {input_key:?}) due to not having correct derived key: {err:?}");
continue;
}
};
dbcs_to_spend.push((dbc, derived_key));
total_input_amount = total_input_amount.checked_add(dbc_balance)
.ok_or_else(|| {
Error::DbcReissueFailed(
"Overflow occurred while increasing total input amount while trying to cover the output DBCs."
.to_string(),
)
})?;
match change_amount.checked_sub(dbc_balance) {
Some(pending_output) => {
change_amount = pending_output;
if change_amount.as_nano() == 0 {
break;
}
}
None => {
change_amount = Token::from_nano(dbc_balance.as_nano() - change_amount.as_nano());
break;
}
}
}
verify_amounts(total_input_amount, total_output_amount)?;
Ok(Inputs {
dbcs_to_spend,
recipients,
change: (change_amount, change_to),
})
}
fn verify_amounts(total_input_amount: Token, total_output_amount: Token) -> Result<()> {
if total_output_amount > total_input_amount {
return Err(Error::NotEnoughBalance(total_input_amount.to_string()));
}
Ok(())
}
fn create_transfer_with(selected_inputs: Inputs, reason_hash: Hash) -> Result<TransferOutputs> {
let Inputs {
dbcs_to_spend,
recipients,
change: (change, change_to),
..
} = selected_inputs;
let mut inputs = vec![];
let mut src_txs = BTreeMap::new();
for (dbc, derived_key) in dbcs_to_spend {
let revealed_amount = match dbc.revealed_amount(&derived_key) {
Ok(amount) => amount,
Err(err) => {
warn!("Ignoring dbc, as it didn't have the correct derived key: {err}");
continue;
}
};
let input = InputHistory {
input: RevealedInput::new(derived_key, revealed_amount),
input_src_tx: dbc.src_tx.clone(),
};
inputs.push(input);
let _ = src_txs.insert(dbc.id(), dbc.src_tx);
}
let mut tx_builder = TransactionBuilder::default()
.add_inputs(inputs)
.add_outputs(recipients);
let mut rng = rng::thread_rng();
let dbc_id_src = change_to.random_dbc_id_src(&mut rng);
let change_id = dbc_id_src.dbc_id();
if change.as_nano() > 0 {
tx_builder = tx_builder.add_output(change, dbc_id_src);
}
let dbc_builder = tx_builder
.build(reason_hash, &mut rng)
.map_err(Box::new)
.map_err(Error::Dbcs)?;
let tx_hash = dbc_builder.spent_tx.hash();
let signed_spends: BTreeMap<_, _> = dbc_builder
.signed_spends()
.into_iter()
.map(|spend| (spend.dbc_id(), spend))
.collect();
if !signed_spends
.iter()
.all(|(dbc_id, _)| src_txs.contains_key(*dbc_id))
{
return Err(Error::DbcReissueFailed(
"Not all signed spends could be matched to a source dbc transaction.".to_string(),
));
}
let mut all_spend_requests = vec![];
for (dbc_id, signed_spend) in signed_spends.into_iter() {
let parent_tx = src_txs.get(dbc_id).ok_or(Error::DbcReissueFailed(format!(
"Missing source dbc tx of {dbc_id:?}!"
)))?;
let spend_requests = SpendRequest {
signed_spend: signed_spend.clone(),
parent_tx: parent_tx.clone(),
};
all_spend_requests.push(spend_requests);
}
let mut created_dbcs: Vec<_> = dbc_builder
.build()
.map_err(Box::new)
.map_err(Error::Dbcs)?
.into_iter()
.map(|(dbc, amount)| CreatedDbc { dbc, amount })
.collect();
let mut change_dbc = None;
created_dbcs.retain(|created| {
if created.dbc.id() == change_id {
change_dbc = Some(created.dbc.clone());
false
} else {
true
}
});
Ok(TransferOutputs {
tx_hash,
created_dbcs,
change_dbc,
all_spend_requests,
})
}