mirror of
https://github.com/mimblewimble/grin.git
synced 2025-01-20 19:11:08 +03:00
wallet can now optionally spend zero-confirmation txs (#188)
* wallet can now optionally spend zero-confirmation txs * add rule to get_mineable_transactions based on total pool size
This commit is contained in:
parent
bab7bd7060
commit
4d7b46b0b9
8 changed files with 162 additions and 58 deletions
|
@ -260,6 +260,20 @@ impl DirectedGraph {
|
|||
pub fn get_roots(&self) -> Vec<core::hash::Hash> {
|
||||
self.roots.iter().map(|x| x.transaction_hash).collect()
|
||||
}
|
||||
|
||||
/// Get list of all vertices in this graph including the roots
|
||||
pub fn get_vertices(&self) -> Vec<core::hash::Hash> {
|
||||
let mut hashes = self.roots
|
||||
.iter()
|
||||
.map(|x| x.transaction_hash)
|
||||
.collect::<Vec<_>>();
|
||||
let non_root_hashes = self.vertices
|
||||
.iter()
|
||||
.map(|x| x.transaction_hash)
|
||||
.collect::<Vec<_>>();
|
||||
hashes.extend(&non_root_hashes);
|
||||
return hashes
|
||||
}
|
||||
}
|
||||
|
||||
/// Using transaction merkle_inputs_outputs to calculate a deterministic hash;
|
||||
|
|
|
@ -304,11 +304,17 @@ impl Pool {
|
|||
}
|
||||
}
|
||||
|
||||
/// Simplest possible implementation: just return the roots
|
||||
/// Currently a single rule for miner preference -
|
||||
/// return all txs if less than num_to_fetch txs in the entire pool
|
||||
/// otherwise return num_to_fetch of just the roots
|
||||
pub fn get_mineable_transactions(&self, num_to_fetch: u32) -> Vec<hash::Hash> {
|
||||
let mut roots = self.graph.get_roots();
|
||||
roots.truncate(num_to_fetch as usize);
|
||||
roots
|
||||
if self.graph.len_vertices() <= num_to_fetch as usize {
|
||||
self.graph.get_vertices()
|
||||
} else {
|
||||
let mut roots = self.graph.get_roots();
|
||||
roots.truncate(num_to_fetch as usize);
|
||||
roots
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -200,6 +200,12 @@ fn main() {
|
|||
.arg(Arg::with_name("amount")
|
||||
.help("Amount to send in the smallest denomination")
|
||||
.index(1))
|
||||
.arg(Arg::with_name("minimum_confirmations")
|
||||
.help("Minimum number of confirmations required for an output to be spendable.")
|
||||
.short("c")
|
||||
.long("min_conf")
|
||||
.default_value("1")
|
||||
.takes_value(true))
|
||||
.arg(Arg::with_name("dest")
|
||||
.help("Send the transaction to the provided server")
|
||||
.short("d")
|
||||
|
@ -212,7 +218,13 @@ fn main() {
|
|||
transactions.")
|
||||
.arg(Arg::with_name("amount")
|
||||
.help("Amount to burn in the smallest denomination")
|
||||
.index(1)))
|
||||
.index(1))
|
||||
.arg(Arg::with_name("minimum_confirmations")
|
||||
.help("Minimum number of confirmations required for an output to be spendable.")
|
||||
.short("c")
|
||||
.long("min_conf")
|
||||
.default_value("1")
|
||||
.takes_value(true)))
|
||||
|
||||
.subcommand(SubCommand::with_name("info")
|
||||
.about("basic wallet info (outputs)")))
|
||||
|
@ -380,11 +392,22 @@ fn wallet_command(wallet_args: &ArgMatches) {
|
|||
.expect("Amount to send required")
|
||||
.parse()
|
||||
.expect("Could not parse amount as a whole number.");
|
||||
let minimum_confirmations: u64 = send_args
|
||||
.value_of("minimum_confirmations")
|
||||
.unwrap_or("1")
|
||||
.parse()
|
||||
.expect("Could not parse minimum_confirmations as a whole number.");
|
||||
let mut dest = "stdout";
|
||||
if let Some(d) = send_args.value_of("dest") {
|
||||
dest = d;
|
||||
}
|
||||
wallet::issue_send_tx(&wallet_config, &keychain, amount, dest.to_string()).unwrap();
|
||||
wallet::issue_send_tx(
|
||||
&wallet_config,
|
||||
&keychain,
|
||||
amount,
|
||||
minimum_confirmations,
|
||||
dest.to_string()
|
||||
).unwrap();
|
||||
}
|
||||
("burn", Some(send_args)) => {
|
||||
let amount = send_args
|
||||
|
@ -392,7 +415,17 @@ fn wallet_command(wallet_args: &ArgMatches) {
|
|||
.expect("Amount to burn required")
|
||||
.parse()
|
||||
.expect("Could not parse amount as a whole number.");
|
||||
wallet::issue_burn_tx(&wallet_config, &keychain, amount).unwrap();
|
||||
let minimum_confirmations: u64 = send_args
|
||||
.value_of("minimum_confirmations")
|
||||
.unwrap_or("1")
|
||||
.parse()
|
||||
.expect("Could not parse minimum_confirmations as a whole number.");
|
||||
wallet::issue_burn_tx(
|
||||
&wallet_config,
|
||||
&keychain,
|
||||
amount,
|
||||
minimum_confirmations,
|
||||
).unwrap();
|
||||
}
|
||||
("info", Some(_)) => {
|
||||
wallet::show_info(&wallet_config, &keychain);
|
||||
|
|
|
@ -20,16 +20,12 @@ use types::*;
|
|||
use keychain::Keychain;
|
||||
use util;
|
||||
|
||||
fn refresh_output(out: &mut OutputData, api_out: Option<api::Output>, tip: &api::Tip) {
|
||||
fn refresh_output(out: &mut OutputData, api_out: Option<api::Output>) {
|
||||
if let Some(api_out) = api_out {
|
||||
out.height = api_out.height;
|
||||
out.lock_height = api_out.lock_height;
|
||||
|
||||
if out.status == OutputStatus::Locked {
|
||||
// leave it Locked locally for now
|
||||
} else if api_out.lock_height > tip.height {
|
||||
out.status = OutputStatus::Immature;
|
||||
} else {
|
||||
if out.status != OutputStatus::Locked {
|
||||
out.status = OutputStatus::Unspent;
|
||||
}
|
||||
} else if vec![OutputStatus::Unspent, OutputStatus::Locked].contains(&out.status) {
|
||||
|
@ -39,9 +35,10 @@ fn refresh_output(out: &mut OutputData, api_out: Option<api::Output>, tip: &api:
|
|||
|
||||
/// Goes through the list of outputs that haven't been spent yet and check
|
||||
/// with a node whether their status has changed.
|
||||
pub fn refresh_outputs(config: &WalletConfig, keychain: &Keychain) -> Result<(), Error> {
|
||||
let tip = get_tip_from_node(config)?;
|
||||
|
||||
pub fn refresh_outputs(
|
||||
config: &WalletConfig,
|
||||
keychain: &Keychain,
|
||||
) -> Result<(), Error> {
|
||||
WalletData::with_wallet(&config.data_file_dir, |wallet_data| {
|
||||
// check each output that's not spent
|
||||
for mut out in wallet_data.outputs.values_mut().filter(|out| {
|
||||
|
@ -50,7 +47,7 @@ pub fn refresh_outputs(config: &WalletConfig, keychain: &Keychain) -> Result<(),
|
|||
{
|
||||
// TODO check the pool for unconfirmed
|
||||
match get_output_from_node(config, keychain, out.value, out.n_child) {
|
||||
Ok(api_out) => refresh_output(&mut out, api_out, &tip),
|
||||
Ok(api_out) => refresh_output(&mut out, api_out),
|
||||
Err(_) => {
|
||||
// TODO find error with connection and return
|
||||
// error!(LOGGER, "Error contacting server node at {}. Is it running?",
|
||||
|
|
|
@ -23,8 +23,23 @@ pub fn show_info(config: &WalletConfig, keychain: &Keychain) {
|
|||
// operate within a lock on wallet data
|
||||
let _ = WalletData::with_wallet(&config.data_file_dir, |wallet_data| {
|
||||
|
||||
// get the current height via the api
|
||||
// if we cannot get the current height use the max height known to the wallet
|
||||
let current_height = match checker::get_tip_from_node(config) {
|
||||
Ok(tip) => tip.height,
|
||||
Err(_) => {
|
||||
match wallet_data.outputs.values().map(|out| out.height).max() {
|
||||
Some(height) => height,
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// need to specify a default value here somehow
|
||||
let minimum_confirmations = 1;
|
||||
|
||||
println!("Outputs - ");
|
||||
println!("key_id, height, lock_height, status, zero_ok, value");
|
||||
println!("key_id, height, lock_height, status, spendable?, coinbase?, value");
|
||||
println!("----------------------------------");
|
||||
|
||||
let mut outputs = wallet_data
|
||||
|
@ -35,13 +50,14 @@ pub fn show_info(config: &WalletConfig, keychain: &Keychain) {
|
|||
outputs.sort_by_key(|out| out.n_child);
|
||||
for out in outputs {
|
||||
println!(
|
||||
"{}, {}, {}, {:?}, {}, {}",
|
||||
"{}, {}, {}, {:?}, {}, {}, {}",
|
||||
out.key_id,
|
||||
out.height,
|
||||
out.lock_height,
|
||||
out.status,
|
||||
out.zero_ok,
|
||||
out.value
|
||||
out.eligible_to_spend(current_height, minimum_confirmations),
|
||||
out.is_coinbase,
|
||||
out.value,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -172,10 +172,11 @@ impl ApiEndpoint for WalletReceiver {
|
|||
}
|
||||
|
||||
/// Build a coinbase output and the corresponding kernel
|
||||
fn receive_coinbase(config: &WalletConfig,
|
||||
keychain: &Keychain,
|
||||
block_fees: &BlockFees)
|
||||
-> Result<(Output, TxKernel, BlockFees), Error> {
|
||||
fn receive_coinbase(
|
||||
config: &WalletConfig,
|
||||
keychain: &Keychain,
|
||||
block_fees: &BlockFees
|
||||
) -> Result<(Output, TxKernel, BlockFees), Error> {
|
||||
let root_key_id = keychain.root_key_id();
|
||||
|
||||
// operate within a lock on wallet data
|
||||
|
@ -205,7 +206,7 @@ fn receive_coinbase(config: &WalletConfig,
|
|||
status: OutputStatus::Unconfirmed,
|
||||
height: 0,
|
||||
lock_height: 0,
|
||||
zero_ok: false,
|
||||
is_coinbase: true,
|
||||
});
|
||||
|
||||
debug!(
|
||||
|
@ -280,7 +281,7 @@ fn receive_transaction(
|
|||
status: OutputStatus::Unconfirmed,
|
||||
height: 0,
|
||||
lock_height: 0,
|
||||
zero_ok: false,
|
||||
is_coinbase: false,
|
||||
});
|
||||
debug!(
|
||||
LOGGER,
|
||||
|
|
|
@ -31,14 +31,25 @@ pub fn issue_send_tx(
|
|||
config: &WalletConfig,
|
||||
keychain: &Keychain,
|
||||
amount: u64,
|
||||
minimum_confirmations: u64,
|
||||
dest: String,
|
||||
) -> Result<(), Error> {
|
||||
checker::refresh_outputs(config, keychain)?;
|
||||
|
||||
let chain_tip = checker::get_tip_from_node(config)?;
|
||||
let current_height = chain_tip.height;
|
||||
|
||||
// proof of concept - set lock_height on the tx
|
||||
let lock_height = chain_tip.height;
|
||||
|
||||
let (tx, blind_sum) = build_send_tx(config, keychain, amount, lock_height)?;
|
||||
let (tx, blind_sum) = build_send_tx(
|
||||
config,
|
||||
keychain,
|
||||
amount,
|
||||
current_height,
|
||||
minimum_confirmations,
|
||||
lock_height,
|
||||
)?;
|
||||
let json_tx = partial_tx_to_json(amount, blind_sum, tx);
|
||||
|
||||
if dest == "stdout" {
|
||||
|
@ -64,6 +75,8 @@ fn build_send_tx(
|
|||
config: &WalletConfig,
|
||||
keychain: &Keychain,
|
||||
amount: u64,
|
||||
current_height: u64,
|
||||
minimum_confirmations: u64,
|
||||
lock_height: u64,
|
||||
) -> Result<(Transaction, BlindingFactor), Error> {
|
||||
let key_id = keychain.clone().root_key_id();
|
||||
|
@ -71,8 +84,8 @@ fn build_send_tx(
|
|||
// operate within a lock on wallet data
|
||||
WalletData::with_wallet(&config.data_file_dir, |wallet_data| {
|
||||
|
||||
// select some suitable outputs to spend from our local wallet
|
||||
let (coins, _) = wallet_data.select(key_id.clone(), u64::max_value());
|
||||
// select some spendable coins from our local wallet
|
||||
let coins = wallet_data.select(key_id.clone(), current_height, minimum_confirmations);
|
||||
|
||||
// build transaction skeleton with inputs and change
|
||||
// TODO - should probably also check we are sending enough to cover the fees + non-zero output
|
||||
|
@ -89,9 +102,17 @@ fn build_send_tx(
|
|||
})?
|
||||
}
|
||||
|
||||
pub fn issue_burn_tx(config: &WalletConfig, keychain: &Keychain, amount: u64) -> Result<(), Error> {
|
||||
pub fn issue_burn_tx(
|
||||
config: &WalletConfig,
|
||||
keychain: &Keychain,
|
||||
amount: u64,
|
||||
minimum_confirmations: u64,
|
||||
) -> Result<(), Error> {
|
||||
let keychain = &Keychain::burn_enabled(keychain, &Identifier::zero());
|
||||
|
||||
let chain_tip = checker::get_tip_from_node(config)?;
|
||||
let current_height = chain_tip.height;
|
||||
|
||||
let _ = checker::refresh_outputs(config, keychain);
|
||||
|
||||
let key_id = keychain.root_key_id();
|
||||
|
@ -99,8 +120,8 @@ pub fn issue_burn_tx(config: &WalletConfig, keychain: &Keychain, amount: u64) ->
|
|||
// operate within a lock on wallet data
|
||||
WalletData::with_wallet(&config.data_file_dir, |mut wallet_data| {
|
||||
|
||||
// select all suitable outputs by passing largest amount
|
||||
let (coins, _) = wallet_data.select(key_id.clone(), u64::max_value());
|
||||
// select some spendable coins from the wallet
|
||||
let coins = wallet_data.select(key_id.clone(), current_height, minimum_confirmations);
|
||||
|
||||
// build transaction skeleton with inputs and change
|
||||
let mut parts = inputs_and_change(&coins, keychain, key_id, &mut wallet_data, amount)?;
|
||||
|
@ -170,7 +191,7 @@ fn inputs_and_change(
|
|||
status: OutputStatus::Unconfirmed,
|
||||
height: 0,
|
||||
lock_height: 0,
|
||||
zero_ok: true,
|
||||
is_coinbase: false,
|
||||
});
|
||||
|
||||
// now lock the ouputs we're spending so we avoid accidental double spend attempt
|
||||
|
|
|
@ -132,7 +132,6 @@ impl Default for WalletConfig {
|
|||
pub enum OutputStatus {
|
||||
Unconfirmed,
|
||||
Unspent,
|
||||
Immature,
|
||||
Locked,
|
||||
Spent,
|
||||
}
|
||||
|
@ -142,7 +141,6 @@ impl fmt::Display for OutputStatus {
|
|||
match *self {
|
||||
OutputStatus::Unconfirmed => write!(f, "Unconfirmed"),
|
||||
OutputStatus::Unspent => write!(f, "Unspent"),
|
||||
OutputStatus::Immature => write!(f, "Immature"),
|
||||
OutputStatus::Locked => write!(f, "Locked"),
|
||||
OutputStatus::Spent => write!(f, "Spent"),
|
||||
}
|
||||
|
@ -168,8 +166,8 @@ pub struct OutputData {
|
|||
pub height: u64,
|
||||
/// Height we are locked until
|
||||
pub lock_height: u64,
|
||||
/// Can we spend with zero confirmations? (Did it originate from us, change output etc.)
|
||||
pub zero_ok: bool,
|
||||
/// Is this a coinbase output? Is it subject to coinbase locktime?
|
||||
pub is_coinbase: bool,
|
||||
}
|
||||
|
||||
impl OutputData {
|
||||
|
@ -177,6 +175,29 @@ impl OutputData {
|
|||
fn lock(&mut self) {
|
||||
self.status = OutputStatus::Locked;
|
||||
}
|
||||
|
||||
pub fn eligible_to_spend(
|
||||
&self,
|
||||
current_height: u64,
|
||||
minimum_confirmations: u64
|
||||
) -> bool {
|
||||
if [
|
||||
OutputStatus::Spent,
|
||||
OutputStatus::Locked,
|
||||
].contains(&self.status) {
|
||||
return false;
|
||||
} else if self.status == OutputStatus::Unconfirmed && self.is_coinbase {
|
||||
return false;
|
||||
} else if self.lock_height > current_height {
|
||||
return false;
|
||||
} else if self.status == OutputStatus::Unspent && self.height + minimum_confirmations <= current_height {
|
||||
return true;
|
||||
} else if self.status == OutputStatus::Unconfirmed && minimum_confirmations == 0 {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wallet information tracking all our outputs. Based on HD derivation and
|
||||
|
@ -313,27 +334,22 @@ impl WalletData {
|
|||
self.outputs.get(&key_id.to_hex())
|
||||
}
|
||||
|
||||
/// Select a subset of unspent outputs to spend in a transaction
|
||||
/// transferring the provided amount.
|
||||
pub fn select(&self, root_key_id: keychain::Identifier, amount: u64) -> (Vec<OutputData>, i64) {
|
||||
let mut to_spend = vec![];
|
||||
let mut input_total = 0;
|
||||
/// Select spendable coins from the wallet
|
||||
pub fn select(
|
||||
&self,
|
||||
root_key_id: keychain::Identifier,
|
||||
current_height: u64,
|
||||
minimum_confirmations: u64,
|
||||
) -> Vec<OutputData> {
|
||||
|
||||
for out in self.outputs.values() {
|
||||
if out.root_key_id == root_key_id
|
||||
&& (out.status == OutputStatus::Unspent)
|
||||
// the following will let us spend zero confirmation change outputs
|
||||
// || (out.status == OutputStatus::Unconfirmed && out.zero_ok))
|
||||
{
|
||||
to_spend.push(out.clone());
|
||||
input_total += out.value;
|
||||
if input_total >= amount {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO - clean up our handling of i64 vs u64 so we are consistent
|
||||
(to_spend, (input_total as i64) - (amount as i64))
|
||||
self.outputs
|
||||
.values()
|
||||
.filter(|out| {
|
||||
out.root_key_id == root_key_id
|
||||
&& out.eligible_to_spend(current_height, minimum_confirmations)
|
||||
})
|
||||
.map(|out| out.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Next child index when we want to create a new output.
|
||||
|
|
Loading…
Reference in a new issue