diff --git a/src/subcommand/wallet/sendmany.rs b/src/subcommand/wallet/sendmany.rs index 1200516826..3f5ec00899 100644 --- a/src/subcommand/wallet/sendmany.rs +++ b/src/subcommand/wallet/sendmany.rs @@ -1,8 +1,16 @@ use { super::*, crate::wallet::Wallet, - std::io::{BufRead, BufReader}, - std::fs::File, + bitcoin::{ + locktime::absolute::LockTime, + Witness, + }, + bitcoincore_rpc::RawTx, + std::{ + collections::BTreeSet, + fs::File, + io::{BufRead, BufReader}, + }, }; #[derive(Debug, Parser, Clone)] @@ -11,14 +19,18 @@ pub(crate) struct SendMany { fee_rate: FeeRate, #[clap(long, help = "Location of a CSV file containing `inscriptionid`,`destination` pairs.")] pub(crate) csv: PathBuf, + #[clap(long, help = "Broadcast the transaction; the default is to output the raw tranasction hex so you can check it before broadcasting.")] + pub(crate) broadcast: bool, } #[derive(Serialize, Deserialize)] pub struct Output { - pub transaction: Txid, + pub tx: String, } impl SendMany { + const SCHNORR_SIGNATURE_SIZE: usize = 64; + pub(crate) fn run(self, options: Options) -> SubcommandResult { let file = File::open(&self.csv)?; let reader = BufReader::new(file); @@ -63,19 +75,21 @@ impl SendMany { let index = Index::open(&options)?; index.update()?; - let _client = options.bitcoin_rpc_client_for_wallet_command(false)?; - + let client = options.bitcoin_rpc_client_for_wallet_command(false)?; let unspent_outputs = index.get_unspent_outputs(Wallet::load(&options)?)?; + let locked_outputs = index.get_locked_outputs(Wallet::load(&options)?)?; - let _locked_outputs = index.get_locked_outputs(Wallet::load(&options)?)?; - + // we get a tree , and turn it into + // a tree let mut inscriptions = BTreeMap::new(); for (satpoint, inscriptionid) in index.get_inscriptions(&unspent_outputs)? { inscriptions.insert(inscriptionid, satpoint); } let mut ordered_inscriptions = Vec::new(); - let mut total_value = Amount::from_sat(0); + let mut total_value = 0; + let mut inputs = Vec::new(); + let mut outputs = Vec::new(); while !requested.is_empty() { let mut inscriptions_on_outpoint = Vec::new(); @@ -99,30 +113,152 @@ impl SendMany { let (first_satpoint, _first_inscription) = inscriptions_on_outpoint[0]; let first_offset = first_satpoint.offset; let first_outpoint = first_satpoint.outpoint; - let utxo_value = unspent_outputs[&first_outpoint]; + let utxo_value = unspent_outputs[&first_outpoint].to_sat(); if first_offset != 0 { bail!("the first inscription in {} is at non-zero offset {}", first_outpoint, first_offset); } - println!("using output {} worth {}", first_outpoint, utxo_value.to_sat()); + eprintln!("\noutput {}, worth {}:", first_outpoint, utxo_value); total_value += utxo_value; - for (_satpoint, inscriptionid) in &inscriptions_on_outpoint { + inputs.push(first_outpoint); + + for (i, (satpoint, inscriptionid)) in inscriptions_on_outpoint.iter().enumerate() { + let destination = &requested[inscriptionid]; + let offset = satpoint.offset; + let value = if i == inscriptions_on_outpoint.len() - 1 { + utxo_value - offset + } else { + inscriptions_on_outpoint[i + 1].0.offset - offset + }; + let script_pubkey = destination.script_pubkey(); + let dust_limit = script_pubkey.dust_value().to_sat(); + if value < dust_limit { + bail!("inscription {} at {} is only followed by {} sats, less than dust limit {} for address {}", + inscriptionid, satpoint.to_string(), value, dust_limit, destination); + } + + eprintln!(" {} : offset: {}, value: {}\n id: {}\n dest: {}", i, offset, value, inscriptionid, destination); + outputs.push(TxOut{script_pubkey, value}); requested.remove(&inscriptionid); } ordered_inscriptions.extend(inscriptions_on_outpoint); } - println!("\ntotal inputs before cardinal: {}", total_value); + let cardinals = Self::get_cardinals(unspent_outputs, locked_outputs, inscriptions); - println!("\nsending these inscriptions, in this order:"); - for (satpoint, inscriptionid) in &ordered_inscriptions { - println!(" inscriptionid {}, satpoint {}", inscriptionid.to_string(), satpoint.to_string()) + if cardinals.is_empty() { + bail!("wallet has no cardinals"); } - println!(""); - let txid = Txid::from_str("0000000000000000000000000000000000000000000000000000000000000000")?; - Ok(Box::new(Output { transaction: txid })) + // select the biggest cardinal - this could be improved by figuring out what size we need, and picking the next biggest for example + let (cardinal_outpoint, cardinal_value) = cardinals[0]; + eprintln!("\ncardinal:\n {}, worth {}", cardinal_outpoint.to_string(), cardinal_value); + + eprintln!("\ninputs without cardinal: {}", total_value); + total_value += cardinal_value; + eprintln!("inputs with cardinal: {}", total_value); + + inputs.push(cardinal_outpoint); + + let change_address = get_change_address(&client, chain)?; + let script_pubkey = change_address.script_pubkey(); + let dust_limit = script_pubkey.dust_value().to_sat(); + let value = 0; + outputs.push(TxOut{script_pubkey: script_pubkey.clone(), value}); + + // calculate the size of the tx once it is signed + let vsize = Self::estimate_transaction_vsize(inputs.len(), outputs.clone()); + let fee = self.fee_rate.fee(vsize).to_sat(); + let needed = fee + dust_limit; + if cardinal_value < needed { + bail!("cardinal ({}) is too small: we need enough for fee {} plus dust limit {} = {}", cardinal_value, fee, dust_limit, needed); + } + let value = cardinal_value - fee; + eprintln!("vsize: {}, fee: {}, change: {}\n", vsize, fee, value); + let last = outputs.len() - 1; + outputs[last] = TxOut{script_pubkey, value}; + + let tx = Self::build_transaction(inputs, outputs); + + let signed_tx = client.sign_raw_transaction_with_wallet(&tx, None, None)?; + let signed_tx = signed_tx.hex; + + if self.broadcast { + let txid = client.send_raw_transaction(&signed_tx)?.to_string(); + Ok(Box::new(Output { tx: txid })) + } else { + Ok(Box::new(Output { tx: signed_tx.raw_hex() })) + } + } + + fn get_cardinals( + unspent_outputs: BTreeMap, + locked_outputs: BTreeSet, + inscriptions: BTreeMap, + ) -> Vec<(OutPoint, u64)> { + let inscribed_utxos = + inscriptions // get a tree of the inscriptions we own + .values() // just the SatPoints + .map(|satpoint| satpoint.outpoint) // just the OutPoints of those SatPoints + .collect::>(); // as a set of OutPoints + + let mut cardinal_utxos = unspent_outputs + .iter() + .filter_map(|(output, amount)| { + if inscribed_utxos.contains(output) || locked_outputs.contains(output) { + None + } else { + Some(( + *output, + amount.to_sat(), + )) + } + }) + .collect::>(); + + cardinal_utxos.sort_by_key(|x| x.1); + cardinal_utxos.reverse(); + cardinal_utxos + } + + fn build_transaction( + inputs: Vec, + outputs: Vec, + ) -> Transaction { + Transaction { + input: inputs + .iter() + .map(|outpoint| TxIn { + previous_output: *outpoint, + script_sig: script::Builder::new().into_script(), + witness: Witness::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + }) + .collect(), + output: outputs, + lock_time: LockTime::ZERO, + version: 1, + } + } + + fn estimate_transaction_vsize( + inputs: usize, + outputs: Vec, + ) -> usize { + Transaction { + input: (0..inputs) + .map(|_| TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::from_slice(&[&[0; Self::SCHNORR_SIGNATURE_SIZE]]), + }) + .collect(), + output: outputs, + lock_time: LockTime::ZERO, + version: 1, + }.vsize() } }