use crate::address;
use crate::epic_core::core::amount_to_hr_string;
use crate::epic_core::libtx::{
build,
proof::{ProofBuild, ProofBuilder},
tx_fee,
};
use crate::epic_keychain::{Identifier, Keychain};
use crate::epic_util::secp::key::SecretKey;
use crate::error::{Error, ErrorKind};
use crate::internal::keys;
use crate::slate::Slate;
use crate::types::*;
use std::collections::HashMap;
pub fn build_send_tx<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain: &K,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
minimum_confirmations: u64,
max_outputs: usize,
change_outputs: usize,
selection_strategy_is_use_all: bool,
parent_key_id: Identifier,
use_test_nonce: bool,
) -> Result<Context, Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let (elems, inputs, change_amounts_derivations, fee) = select_send_tx(
wallet,
keychain_mask,
slate.amount,
slate.height,
minimum_confirmations,
max_outputs,
change_outputs,
selection_strategy_is_use_all,
&parent_key_id,
)?;
slate.fee = fee;
let blinding = slate.add_transaction_elements(keychain, &ProofBuilder::new(keychain), elems)?;
let mut context = Context::new(
keychain.secp(),
blinding.secret_key(&keychain.secp()).unwrap(),
&parent_key_id,
use_test_nonce,
0,
);
context.fee = fee;
for input in inputs {
context.add_input(&input.key_id, &input.mmr_index, input.value);
}
let mut commits: HashMap<Identifier, Option<String>> = HashMap::new();
for (change_amount, id, mmr_index) in &change_amounts_derivations {
context.add_output(&id, &mmr_index, *change_amount);
commits.insert(
id.clone(),
wallet.calc_commit_for_cache(keychain_mask, *change_amount, &id)?,
);
}
Ok(context)
}
pub fn lock_tx_context<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &Slate,
context: &Context,
) -> Result<(), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let mut output_commits: HashMap<Identifier, (Option<String>, u64)> = HashMap::new();
let mut total_change = 0;
for (id, _, change_amount) in &context.get_outputs() {
output_commits.insert(
id.clone(),
(
wallet.calc_commit_for_cache(keychain_mask, *change_amount, &id)?,
*change_amount,
),
);
total_change += change_amount;
}
debug!("Change amount is: {}", total_change);
let keychain = wallet.keychain(keychain_mask)?;
let tx_entry = {
let lock_inputs = context.get_inputs().clone();
let messages = Some(slate.participant_messages());
let slate_id = slate.id;
let height = slate.height;
let parent_key_id = context.parent_key_id.clone();
let mut batch = wallet.batch(keychain_mask)?;
let log_id = batch.next_tx_log_id(&parent_key_id)?;
let mut t = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxSent, log_id);
t.tx_slate_id = Some(slate_id.clone());
let filename = format!("{}.epictx", slate_id);
t.stored_tx = Some(filename);
t.fee = Some(slate.fee);
t.ttl_cutoff_height = slate.ttl_cutoff_height;
match slate.calc_excess(&keychain) {
Ok(e) => t.kernel_excess = Some(e),
Err(_) => {}
}
t.kernel_lookup_min_height = Some(slate.height);
let mut amount_debited = 0;
t.num_inputs = lock_inputs.len();
for id in lock_inputs {
let mut coin = batch.get(&id.0, &id.1).unwrap();
coin.tx_log_entry = Some(log_id);
amount_debited = amount_debited + coin.value;
batch.lock_output(&mut coin)?;
}
t.amount_debited = amount_debited;
t.messages = messages;
if let Some(ref p) = slate.payment_proof {
let sender_address_path = match context.payment_proof_derivation_index {
Some(p) => p,
None => {
return Err(ErrorKind::PaymentProof(
"Payment proof derivation index required".to_owned(),
))?;
}
};
let sender_key = address::address_from_derivation_path(
&keychain,
&parent_key_id,
sender_address_path,
)?;
let sender_address = address::ed25519_keypair(&sender_key)?.1;
t.payment_proof = Some(StoredProofInfo {
receiver_address: p.receiver_address.clone(),
receiver_signature: p.receiver_signature.clone(),
sender_address,
sender_address_path,
sender_signature: None,
});
};
for (id, _, _) in &context.get_outputs() {
t.num_outputs += 1;
let (commit, change_amount) = output_commits.get(&id).unwrap().clone();
t.amount_credited += change_amount;
batch.save(OutputData {
root_key_id: parent_key_id.clone(),
key_id: id.clone(),
n_child: id.to_path().last_path_index(),
commit: commit,
mmr_index: None,
value: change_amount.clone(),
status: OutputStatus::Unconfirmed,
height: height,
lock_height: 0,
is_coinbase: false,
tx_log_entry: Some(log_id),
})?;
}
batch.save_tx_log_entry(t.clone(), &parent_key_id)?;
batch.commit()?;
t
};
wallet.store_tx(&format!("{}", tx_entry.tx_slate_id.unwrap()), &slate.tx)?;
Ok(())
}
pub fn build_recipient_output<'a, T: ?Sized, C, K>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
slate: &mut Slate,
parent_key_id: Identifier,
use_test_rng: bool,
) -> Result<(Identifier, Context), Error>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let key_id = keys::next_available_key(wallet, keychain_mask).unwrap();
let keychain = wallet.keychain(keychain_mask)?;
let key_id_inner = key_id.clone();
let amount = slate.amount;
let height = slate.height;
let slate_id = slate.id.clone();
let blinding = slate.add_transaction_elements(
&keychain,
&ProofBuilder::new(&keychain),
vec![build::output(amount, key_id.clone())],
)?;
let mut context = Context::new(
keychain.secp(),
blinding
.secret_key(wallet.keychain(keychain_mask)?.secp())
.unwrap(),
&parent_key_id,
use_test_rng,
1,
);
context.add_output(&key_id, &None, amount);
let messages = Some(slate.participant_messages());
let commit = wallet.calc_commit_for_cache(keychain_mask, amount, &key_id_inner)?;
let mut batch = wallet.batch(keychain_mask)?;
let log_id = batch.next_tx_log_id(&parent_key_id)?;
let mut t = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxReceived, log_id);
t.tx_slate_id = Some(slate_id);
t.amount_credited = amount;
t.num_outputs = 1;
t.messages = messages;
t.ttl_cutoff_height = slate.ttl_cutoff_height;
match slate.calc_excess(&keychain) {
Ok(e) => t.kernel_excess = Some(e),
Err(_) => {}
}
t.kernel_lookup_min_height = Some(slate.height);
batch.save(OutputData {
root_key_id: parent_key_id.clone(),
key_id: key_id_inner.clone(),
mmr_index: None,
n_child: key_id_inner.to_path().last_path_index(),
commit: commit,
value: amount,
status: OutputStatus::Unconfirmed,
height: height,
lock_height: 0,
is_coinbase: false,
tx_log_entry: Some(log_id),
})?;
batch.save_tx_log_entry(t, &parent_key_id)?;
batch.commit()?;
Ok((key_id, context))
}
pub fn select_send_tx<'a, T: ?Sized, C, K, B>(
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
amount: u64,
current_height: u64,
minimum_confirmations: u64,
max_outputs: usize,
change_outputs: usize,
selection_strategy_is_use_all: bool,
parent_key_id: &Identifier,
) -> Result<
(
Vec<Box<build::Append<K, B>>>,
Vec<OutputData>,
Vec<(u64, Identifier, Option<u64>)>, // change amounts and derivations
u64, // fee
),
Error,
>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
B: ProofBuild,
{
let (coins, _total, amount, fee) = select_coins_and_fee(
wallet,
amount,
current_height,
minimum_confirmations,
max_outputs,
change_outputs,
selection_strategy_is_use_all,
&parent_key_id,
)?;
let (parts, change_amounts_derivations) =
inputs_and_change(&coins, wallet, keychain_mask, amount, fee, change_outputs)?;
Ok((parts, coins, change_amounts_derivations, fee))
}
pub fn select_coins_and_fee<'a, T: ?Sized, C, K>(
wallet: &mut T,
amount: u64,
current_height: u64,
minimum_confirmations: u64,
max_outputs: usize,
change_outputs: usize,
selection_strategy_is_use_all: bool,
parent_key_id: &Identifier,
) -> Result<
(
Vec<OutputData>,
u64, // total
u64, // amount
u64, // fee
),
Error,
>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let (max_outputs, mut coins) = select_coins(
wallet,
amount,
current_height,
minimum_confirmations,
max_outputs,
selection_strategy_is_use_all,
parent_key_id,
);
let mut fee = tx_fee(coins.len(), 1, 1, None);
let mut total: u64 = coins.iter().map(|c| c.value).sum();
let mut amount_with_fee = amount + fee;
if total == 0 {
return Err(ErrorKind::NotEnoughFunds {
available: 0,
available_disp: amount_to_hr_string(0, false),
needed: amount_with_fee as u64,
needed_disp: amount_to_hr_string(amount_with_fee as u64, false),
})?;
}
if total < amount_with_fee && coins.len() == max_outputs {
return Err(ErrorKind::NotEnoughFunds {
available: total,
available_disp: amount_to_hr_string(total, false),
needed: amount_with_fee as u64,
needed_disp: amount_to_hr_string(amount_with_fee as u64, false),
})?;
}
let num_outputs = change_outputs + 1;
if total != amount_with_fee {
fee = tx_fee(coins.len(), num_outputs, 1, None);
amount_with_fee = amount + fee;
while total < amount_with_fee {
if coins.len() == max_outputs {
return Err(ErrorKind::NotEnoughFunds {
available: total as u64,
available_disp: amount_to_hr_string(total, false),
needed: amount_with_fee as u64,
needed_disp: amount_to_hr_string(amount_with_fee as u64, false),
})?;
}
coins = select_coins(
wallet,
amount_with_fee,
current_height,
minimum_confirmations,
max_outputs,
selection_strategy_is_use_all,
parent_key_id,
)
.1;
fee = tx_fee(coins.len(), num_outputs, 1, None);
total = coins.iter().map(|c| c.value).sum();
amount_with_fee = amount + fee;
}
}
Ok((coins, total, amount, fee))
}
pub fn inputs_and_change<'a, T: ?Sized, C, K, B>(
coins: &Vec<OutputData>,
wallet: &mut T,
keychain_mask: Option<&SecretKey>,
amount: u64,
fee: u64,
num_change_outputs: usize,
) -> Result<
(
Vec<Box<build::Append<K, B>>>,
Vec<(u64, Identifier, Option<u64>)>,
),
Error,
>
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
B: ProofBuild,
{
let mut parts = vec![];
let total: u64 = coins.iter().map(|c| c.value).sum();
let change = total - amount - fee;
for coin in coins {
if coin.is_coinbase {
parts.push(build::coinbase_input(coin.value, coin.key_id.clone()));
} else {
parts.push(build::input(coin.value, coin.key_id.clone()));
}
}
let mut change_amounts_derivations = vec![];
if change == 0 {
debug!("No change (sending exactly amount + fee), no change outputs to build");
} else {
debug!(
"Building change outputs: total change: {} ({} outputs)",
change, num_change_outputs
);
let part_change = change / num_change_outputs as u64;
let remainder_change = change % part_change;
for x in 0..num_change_outputs {
let change_amount = if x == (num_change_outputs - 1) {
part_change + remainder_change
} else {
part_change
};
let change_key = wallet.next_child(keychain_mask).unwrap();
change_amounts_derivations.push((change_amount, change_key.clone(), None));
parts.push(build::output(change_amount, change_key));
}
}
Ok((parts, change_amounts_derivations))
}
pub fn select_coins<'a, T: ?Sized, C, K>(
wallet: &mut T,
amount: u64,
current_height: u64,
minimum_confirmations: u64,
max_outputs: usize,
select_all: bool,
parent_key_id: &Identifier,
) -> (usize, Vec<OutputData>)
where
T: WalletBackend<'a, C, K>,
C: NodeClient + 'a,
K: Keychain + 'a,
{
let mut eligible = wallet
.iter()
.filter(|out| {
out.root_key_id == *parent_key_id
&& out.eligible_to_spend(current_height, minimum_confirmations)
})
.collect::<Vec<OutputData>>();
let max_available = eligible.len();
eligible.sort_by_key(|out| out.value);
if eligible.len() > max_outputs {
for window in eligible.windows(max_outputs) {
let windowed_eligibles = window.iter().cloned().collect::<Vec<_>>();
if let Some(outputs) = select_from(amount, select_all, windowed_eligibles) {
return (max_available, outputs);
}
}
if let Some(outputs) = select_from(amount, false, eligible.clone()) {
debug!(
"Extending maximum number of outputs. {} outputs selected.",
outputs.len()
);
return (max_available, outputs);
}
} else {
if let Some(outputs) = select_from(amount, select_all, eligible.clone()) {
return (max_available, outputs);
}
}
eligible.reverse();
(
max_available,
eligible.iter().take(max_outputs).cloned().collect(),
)
}
fn select_from(amount: u64, select_all: bool, outputs: Vec<OutputData>) -> Option<Vec<OutputData>> {
let total = outputs.iter().fold(0, |acc, x| acc + x.value);
if total >= amount {
if select_all {
return Some(outputs.iter().cloned().collect());
} else {
let mut selected_amount = 0;
return Some(
outputs
.iter()
.take_while(|out| {
let res = selected_amount < amount;
selected_amount += out.value;
res
})
.cloned()
.collect(),
);
}
} else {
None
}
}