use crate::online::get_address::GetAddressOptions;
use crate::*;
use bitcoin::{Address, Amount, OutPoint};
use bitcoincore_rpc::RpcApi;
use bitcoincore_rpc_json::{
CreateRawTransactionInput, GetTransactionResultDetailCategory, WalletCreateFundedPsbtOptions,
};
use log::{debug, info};
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use structopt::StructOpt;
#[derive(StructOpt, Debug)]
pub struct CreateTxOptions {
#[structopt(long = "wallet-name")]
pub wallet_name: String,
#[structopt(long, long = "recipient")]
pub recipients: Vec<AddressAmount>,
#[structopt(long, long = "coin")]
pub coins: Vec<OutPoint>,
#[structopt(short, long)]
pub psbt_name: String,
}
#[derive(StructOpt, Debug)]
pub struct AddressAmount {
pub address: Address,
pub amount: Amount,
}
impl CreateTxOptions {
fn validate(&self) -> Result<()> {
if self.recipients.is_empty() {
return Err("At least one recipient is mandatory (--recipient)".into());
}
Ok(())
}
fn recipients_as_outputs(&self) -> HashMap<String, Amount> {
self.recipients
.iter()
.map(|r| (r.address.to_string(), r.amount))
.collect()
}
fn coins_as_inputs(&self) -> Vec<CreateRawTransactionInput> {
let mut vec = vec![];
for coin in self.coins.iter() {
vec.push(CreateRawTransactionInput {
txid: coin.txid,
vout: coin.vout,
sequence: None,
});
}
vec
}
}
impl FromStr for AddressAmount {
type Err = std::io::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let parts: Vec<_> = s.split(':').collect();
if parts.len() != 2 {
Err(io_err("recipient is not in the format address:amount"))
} else {
let amount = match parts[1].parse::<u64>() {
Ok(unsigned) => Amount::from_sat(unsigned),
Err(_) => Amount::from_str(parts[1])
.map_err(|_| io_err("Amount in recipient is invalid, should be satoshi"))?,
};
let address = Address::from_str(parts[0])
.map_err(|_| io_err("Address in recipient is not valid"))?;
Ok(AddressAmount { address, amount })
}
}
}
impl OnlineContext {
pub fn create_tx(&self, opt: &CreateTxOptions) -> Result<CreateTxOutput> {
opt.validate()?;
let client = self.make_client(&opt.wallet_name)?;
let outputs = opt.recipients_as_outputs();
debug!("{:?}", outputs);
let inputs = opt.coins_as_inputs();
debug!("{:?}", inputs);
let get_addr_opts = GetAddressOptions {
wallet_name: opt.wallet_name.to_string(),
index: None,
verify_wallet: false,
};
let change_address = self.get_address(&get_addr_opts)?.address;
let options = WalletCreateFundedPsbtOptions {
include_watching: Some(true),
change_address: Some(change_address),
..Default::default()
};
let result =
client.wallet_create_funded_psbt(&inputs, &outputs, None, Some(options), Some(true));
info!("wallet_create_funded_psbt {:#?}", result);
let funded_psbt = result?;
let mut psbt = psbt_from_rpc(&funded_psbt, &opt.psbt_name)?;
let psbt_name = self.save_psbt(&mut psbt)?;
let transactions = client
.list_transactions(None, Some(1000), None, Some(true))
.unwrap();
let mut address_reused = HashSet::new();
for recipient in opt.recipients.iter() {
for tx in transactions.iter() {
if tx.detail.address.as_ref() == Some(&recipient.address)
&& tx.detail.category == GetTransactionResultDetailCategory::Send
{
address_reused.insert(recipient.address.clone());
}
}
}
let create_tx = CreateTxOutput {
funded_psbt: (&psbt, self.network).into(),
psbt_name,
address_reused,
};
Ok(create_tx)
}
}