Skip to content

Commit

Permalink
Implement direct-send main subroutine
Browse files Browse the repository at this point in the history
Can be used to spend certain coins, in the manner of coin control. Does not
yet support having the wallet choose the coins to spend. So coin control is
non-optional for now.

Does not yet support spending timelocked utxos.
  • Loading branch information
chris-belcher committed Jan 16, 2022
1 parent 99b2111 commit 9b256b1
Show file tree
Hide file tree
Showing 4 changed files with 404 additions and 74 deletions.
50 changes: 47 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ use bitcoincore_rpc::{Auth, Client, Error, RpcApi};
pub mod wallet_sync;
use wallet_sync::{Wallet, WalletSyncAddressAmount};

pub mod wallet_direct_send;
use wallet_direct_send::{CoinToSpend, Destination, SendAmount};

pub mod contracts;
use contracts::{read_locktime_from_contract, SwapCoin};

Expand Down Expand Up @@ -158,15 +161,15 @@ pub fn display_wallet_balance(wallet_file_name: &PathBuf, long_form: Option<bool
let long_form = long_form.unwrap_or(false);

let mut utxos = wallet.list_unspent_from_wallet(&rpc).unwrap();
utxos.sort_by(|a, b| b.confirmations.cmp(&a.confirmations));
utxos.sort_by(|(a, _), (b, _)| b.confirmations.cmp(&a.confirmations));
let utxo_count = utxos.len();
let balance: Amount = utxos.iter().fold(Amount::ZERO, |acc, u| acc + u.amount);
let balance: Amount = utxos.iter().fold(Amount::ZERO, |acc, (u, _)| acc + u.amount);
println!("= spendable wallet balance =");
println!(
"{:16} {:24} {:^8} {:<7} value",
"coin", "address", "type", "conf",
);
for utxo in utxos {
for (utxo, _) in utxos {
let txid = utxo.txid.to_hex();
let addr = utxo.address.unwrap().to_string();
#[rustfmt::skip]
Expand Down Expand Up @@ -428,6 +431,47 @@ pub fn recover_from_incomplete_coinswap(
}
}

pub fn direct_send(
wallet_file_name: &PathBuf,
send_amount: SendAmount,
destination: Destination,
coins_to_spend: &[CoinToSpend],
dont_broadcast: bool,
) {
let rpc = match get_bitcoin_rpc() {
Ok(rpc) => rpc,
Err(error) => {
log::error!(target: "main", "error connecting to bitcoin node: {:?}", error);
return;
}
};
let mut wallet =
match Wallet::load_wallet_from_file(wallet_file_name, WalletSyncAddressAmount::Normal) {
Ok(w) => w,
Err(error) => {
log::error!(target: "main", "error loading wallet file: {:?}", error);
return;
}
};
wallet.startup_sync(&rpc).unwrap();
let tx = wallet
.create_direct_send(&rpc, send_amount, destination, coins_to_spend)
.unwrap();
if dont_broadcast {
let txhex = bitcoin::consensus::encode::serialize_hex(&tx);
let accepted = rpc
.test_mempool_accept(&[txhex.clone()])
.unwrap()
.iter()
.any(|tma| tma.allowed);
assert!(accepted);
println!("tx = \n{}", txhex);
} else {
let txid = rpc.send_raw_transaction(&tx).unwrap();
println!("broadcasted {}", txid);
}
}

pub fn run_watchtower(kill_flag: Option<Arc<RwLock<bool>>>) {
let rpc = match get_bitcoin_rpc() {
Ok(rpc) => rpc,
Expand Down
39 changes: 32 additions & 7 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use structopt::StructOpt;

use teleport;
use teleport::maker_protocol::MakerBehavior;
use teleport::wallet_direct_send::{CoinToSpend, Destination, SendAmount};
use teleport::wallet_sync::WalletSyncAddressAmount;
use teleport::watchtower_client::ContractInfo;

Expand All @@ -17,6 +18,11 @@ struct ArgsWithWalletFile {
#[structopt(default_value = "wallet.teleport", parse(from_os_str), long)]
wallet_file_name: PathBuf,

/// Dont broadcast transactions, only output their transaction hex string
/// Only for commands which involve sending transactions e.g. recover-from-incomplete-coinswap
#[structopt(short, long)]
dont_broadcast: bool,

/// Subcommand
#[structopt(flatten)]
subcommand: Subcommand,
Expand Down Expand Up @@ -59,8 +65,17 @@ enum Subcommand {
RecoverFromIncompleteCoinswap {
/// Hashvalue as hex string which uniquely identifies the coinswap
hashvalue: Hash160,
/// Dont broadcast transactions, only output their transaction hex string
dont_broadcast: Option<bool>,
},

/// Send a transaction from the wallet
DirectSend {
/// Amount to send (in sats), or "sweep" for sweep
send_amount: SendAmount,
/// Address to send coins to, or "wallet" to send back to own wallet
destination: Destination,
/// Coins to spend as inputs, either in long form "<txid>:vout" or short
/// form "txid-prefix..txid-suffix:vout"
coins_to_spend: Vec<CoinToSpend>,
},

/// Run watchtower
Expand Down Expand Up @@ -111,14 +126,24 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Subcommand::CoinswapSend => {
teleport::run_taker(&args.wallet_file_name, WalletSyncAddressAmount::Normal);
}
Subcommand::RecoverFromIncompleteCoinswap {
hashvalue,
dont_broadcast,
} => {
Subcommand::RecoverFromIncompleteCoinswap { hashvalue } => {
teleport::recover_from_incomplete_coinswap(
&args.wallet_file_name,
hashvalue,
dont_broadcast.unwrap_or(false),
args.dont_broadcast,
);
}
Subcommand::DirectSend {
send_amount,
destination,
coins_to_spend,
} => {
teleport::direct_send(
&args.wallet_file_name,
send_amount,
destination,
&coins_to_spend,
args.dont_broadcast,
);
}
Subcommand::RunWatchtower => {
Expand Down
213 changes: 213 additions & 0 deletions src/wallet_direct_send.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
use std::num::ParseIntError;
use std::str::FromStr;

use bitcoin::{Address, Amount, OutPoint, Script, Transaction, TxIn, TxOut};

use bitcoincore_rpc::json::ListUnspentResultEntry;
use bitcoincore_rpc::Client;

use crate::error::Error;
use crate::wallet_sync::{UTXOSpendInfo, Wallet, NETWORK, SignTransactionInputInfo};

#[derive(Debug)]
pub enum SendAmount {
Sweep,
Amount(Amount),
}

impl FromStr for SendAmount {
type Err = ParseIntError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(if s == "sweep" {
SendAmount::Sweep
} else {
SendAmount::Amount(Amount::from_sat(String::from(s).parse::<u64>()?))
})
}
}

#[derive(Debug)]
pub enum Destination {
Wallet,
Address(Address),
}

impl FromStr for Destination {
type Err = bitcoin::util::address::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(if s == "wallet" {
Destination::Wallet
} else {
Destination::Address(Address::from_str(s)?)
})
}
}

#[derive(Debug)]
pub enum CoinToSpend {
LongForm(OutPoint),
ShortForm {
prefix: String,
suffix: String,
vout: u32,
},
}

fn parse_short_form_coin(s: &str) -> Option<CoinToSpend> {
//example short form: 568a4e..83a2e8:0
if s.len() < 15 {
return None;
}
let dots = &s[6..8];
if dots != ".." {
return None;
}
let colon = s.chars().nth(14).unwrap();
if colon != ':' {
return None;
}
let prefix = String::from(&s[0..6]);
let suffix = String::from(&s[8..14]);
let vout = *(&s[15..].parse::<u32>().ok()?);
Some(CoinToSpend::ShortForm {
prefix,
suffix,
vout,
})
}

impl FromStr for CoinToSpend {
type Err = bitcoin::blockdata::transaction::ParseOutPointError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let parsed_outpoint = OutPoint::from_str(s);
if parsed_outpoint.is_ok() {
Ok(CoinToSpend::LongForm(parsed_outpoint.unwrap()))
} else {
let short_form = parse_short_form_coin(s);
if short_form.is_some() {
Ok(short_form.unwrap())
} else {
Err(parsed_outpoint.err().unwrap())
}
}
}
}

impl Wallet {
pub fn create_direct_send(
&mut self,
rpc: &Client,
send_amount: SendAmount,
destination: Destination,
coins_to_spend: &[CoinToSpend],
) -> Result<Transaction, Error> {
let mut tx_inputs = Vec::<TxIn>::new();
let mut unspent_inputs = Vec::<(ListUnspentResultEntry, UTXOSpendInfo)>::new();
//TODO this search within a search could get very slow
let list_unspent_result = self.list_unspent_from_wallet(rpc)?;
for (list_unspent_entry, spend_info) in list_unspent_result {
for cts in coins_to_spend {
let previous_output = match cts {
CoinToSpend::LongForm(outpoint) => {
if list_unspent_entry.txid == outpoint.txid
&& list_unspent_entry.vout == outpoint.vout
{
*outpoint
} else {
continue;
}
}
CoinToSpend::ShortForm {
prefix,
suffix,
vout,
} => {
let txid_hex = list_unspent_entry.txid.to_string();
if txid_hex.starts_with(prefix)
&& txid_hex.ends_with(suffix)
&& list_unspent_entry.vout == *vout
{
OutPoint {
txid: list_unspent_entry.txid,
vout: list_unspent_entry.vout,
}
} else {
continue;
}
}
};
tx_inputs.push(TxIn {
previous_output,
sequence: 0,
witness: Vec::new(),
script_sig: Script::new(),
});
unspent_inputs.push((list_unspent_entry.clone(), spend_info.clone()));
}
}
if tx_inputs.len() != coins_to_spend.len() {
panic!("unable to find all given inputs, only found = {:?}", tx_inputs);
}
let dest_addr = match destination {
Destination::Wallet => self.get_next_external_address(rpc)?,
Destination::Address(a) => a,
};
if dest_addr.network != NETWORK {
panic!("wrong address network type (e.g. testnet, regtest)");
}
let miner_fee = 500; //TODO do this calculation properly

let mut output = Vec::<TxOut>::new();
let total_input_value = unspent_inputs
.iter()
.fold(Amount::ZERO, |acc, u| acc + u.0.amount)
.as_sat();
output.push(TxOut {
script_pubkey: dest_addr.script_pubkey(),
value: match send_amount {
SendAmount::Sweep => total_input_value - miner_fee,
SendAmount::Amount(a) => a.as_sat(),
},
});
if let SendAmount::Amount(amount) = send_amount {
output.push(TxOut {
script_pubkey: self.get_next_internal_addresses(rpc, 1)?[0].script_pubkey(),
value: total_input_value - amount.as_sat() - miner_fee,
});
}

let mut tx = Transaction {
input: tx_inputs,
output,
lock_time: 0,
version: 2,
};
self.sign_transaction(&mut tx, &mut unspent_inputs.iter()
.map(|(lure, usi)|
match usi {
UTXOSpendInfo::SeedUTXO { path } => {
SignTransactionInputInfo::SeedCoin {
path: path.clone(),
input_value: lure.amount.as_sat()
}
}
UTXOSpendInfo::SwapCoin { multisig_redeemscript } => {
SignTransactionInputInfo::SwapCoin {
multisig_redeemscript: multisig_redeemscript.clone()
}
}
UTXOSpendInfo::TimelockContract { swapcoin_multisig_redeemscript } => {
panic!("not implemented yet");
}
}
)
);

println!("tx = {:?}", tx);

Ok(tx)
}
}
Loading

0 comments on commit 9b256b1

Please sign in to comment.