diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index cfb9e12ada..9acd7a9584 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -80,6 +80,8 @@ impl Preview { no_backup: true, satpoint: None, dry_run: false, + commit_change: None, + inscription_destination: None, }, )), } diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 58cba3aa82..3bed4e76e9 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -1,3 +1,4 @@ +use crate::subcommand::wallet::transaction_builder::ChangeAddresses; use { super::*, bitcoin::{ @@ -41,10 +42,19 @@ pub(crate) struct Inscribe { pub(crate) no_backup: bool, #[clap(long, help = "Don't sign or broadcast transactions.")] pub(crate) dry_run: bool, + #[clap(long, help = "Send commit change to ")] + pub(crate) commit_change: Option
, + #[clap(long, help = "Send reveal output to ")] + pub(crate) inscription_destination: Option
, } impl Inscribe { pub(crate) fn run(self, options: Options) -> Result { + if self.satpoint.is_some() && self.commit_change.is_some() { + // inscribing a specific satspoint might require splitting a UTXO. don't allow specifying a single + // change address if a specific satspoint is being inscribed. + bail!("Can not specify satspoint and commit-change in the same command.") + } let client = options.bitcoin_rpc_client_for_wallet_command(false)?; let inscription = Inscription::from_file(options.chain(), &self.file)?; @@ -56,9 +66,14 @@ impl Inscribe { let inscriptions = index.get_inscriptions(None)?; - let commit_tx_change = [get_change_address(&client)?, get_change_address(&client)?]; + let commit_tx_change = match self.commit_change { + Some(address) => ChangeAddresses::Single(address), + None => ChangeAddresses::Double([get_change_address(&client)?, get_change_address(&client)?]), + }; - let reveal_tx_destination = get_change_address(&client)?; + let reveal_tx_destination = self + .inscription_destination + .unwrap_or(get_change_address(&client)?); let (unsigned_commit_tx, reveal_tx, recovery_key_pair) = Inscribe::create_inscription_transactions( @@ -129,10 +144,15 @@ impl Inscribe { inscriptions: BTreeMap, network: Network, utxos: BTreeMap, - change: [Address; 2], + change: ChangeAddresses, destination: Address, fee_rate: FeeRate, ) -> Result<(Transaction, Transaction, TweakedKeyPair)> { + if let ChangeAddresses::Single(_) = change { + if satpoint.is_some() { + bail!("Can not specify satpoint and only provide a single change address") + } + } let satpoint = if let Some(satpoint) = satpoint { satpoint } else { @@ -365,7 +385,7 @@ mod tests { BTreeMap::new(), Network::Bitcoin, utxos.into_iter().collect(), - [commit_address, change(1)], + [commit_address, change(1)].into(), reveal_address, FeeRate::try_from(1.0).unwrap(), ) @@ -394,7 +414,7 @@ mod tests { BTreeMap::new(), Network::Bitcoin, utxos.into_iter().collect(), - [commit_address, change(1)], + [commit_address, change(1)].into(), reveal_address, FeeRate::try_from(1.0).unwrap(), ) @@ -427,7 +447,7 @@ mod tests { inscriptions, Network::Bitcoin, utxos.into_iter().collect(), - [commit_address, change(1)], + [commit_address, change(1)].into(), reveal_address, FeeRate::try_from(1.0).unwrap(), ) @@ -467,7 +487,7 @@ mod tests { inscriptions, Network::Bitcoin, utxos.into_iter().collect(), - [commit_address, change(1)], + [commit_address, change(1)].into(), reveal_address, FeeRate::try_from(1.0).unwrap(), ) @@ -501,7 +521,7 @@ mod tests { inscriptions, bitcoin::Network::Signet, utxos.into_iter().collect(), - [commit_address, change(1)], + [commit_address, change(1)].into(), reveal_address, FeeRate::try_from(fee_rate).unwrap(), ) @@ -533,7 +553,7 @@ mod tests { BTreeMap::new(), Network::Bitcoin, utxos.into_iter().collect(), - [commit_address, change(1)], + [commit_address, change(1)].into(), reveal_address, FeeRate::try_from(1.0).unwrap(), ) @@ -546,4 +566,77 @@ mod tests { error ); } + + #[test] + fn inscribe_with_specific_destination() { + let utxos = vec![(outpoint(1), Amount::from_sat(20000))]; + let inscription = inscription("text/plain", "ord"); + let commit_address = change(0); + let reveal_address = recipient(); + + let (_, reveal_tx, _) = Inscribe::create_inscription_transactions( + Some(satpoint(1, 0)), + inscription, + BTreeMap::new(), + Network::Bitcoin, + utxos.into_iter().collect(), + [commit_address, change(1)].into(), + reveal_address, + FeeRate::try_from(1.0).unwrap(), + ) + .unwrap(); + + assert_eq!( + reveal_tx.output.first().unwrap().script_pubkey, + recipient().script_pubkey() + ); + } + + #[test] + fn inscribe_with_specific_commit_change_address() { + let utxos = vec![(outpoint(1), Amount::from_sat(20000))]; + let inscription = inscription("text/plain", "ord"); + let change_address = change(1); + let reveal_address = recipient(); + + let (commit_tx, _, _) = Inscribe::create_inscription_transactions( + None, + inscription, + BTreeMap::new(), + Network::Bitcoin, + utxos.into_iter().collect(), + change_address.clone().into(), + reveal_address, + FeeRate::try_from(1.0).unwrap(), + ) + .unwrap(); + assert_eq!( + commit_tx.output.get(1).unwrap().script_pubkey, + change_address.script_pubkey() + ); + } + + #[test] + fn cant_inscribe_satpoint_with_single_change_address() { + let utxos = vec![(outpoint(1), Amount::from_sat(50 * COIN_VALUE))]; + + let inscription = inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize]); + let change_address = change(0); + let reveal_address = recipient(); + + let error = Inscribe::create_inscription_transactions( + Some(satpoint(1, 0)), + inscription, + BTreeMap::new(), + Network::Bitcoin, + utxos.into_iter().collect(), + change_address.into(), + reveal_address, + FeeRate::try_from(1.0).unwrap(), + ) + .unwrap_err() + .to_string(); + + assert!(error.contains("Can not specify satpoint and only provide a single change address")); + } } diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 1c18340546..7f69fe6a5e 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -1,4 +1,5 @@ use super::*; +use crate::subcommand::wallet::transaction_builder::ChangeAddresses; #[derive(Debug, Parser)] pub(crate) struct Send { @@ -73,7 +74,8 @@ impl Send { } }; - let change = [get_change_address(&client)?, get_change_address(&client)?]; + let change = + ChangeAddresses::Double([get_change_address(&client)?, get_change_address(&client)?]); let unsigned_transaction = TransactionBuilder::build_transaction_with_postage( satpoint, diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index 24b496c924..0e8175ad33 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -64,6 +64,23 @@ enum Target { Postage, } +pub enum ChangeAddresses { + Single(Address), + Double([Address; 2]), +} + +impl From
for ChangeAddresses { + fn from(value: Address) -> Self { + ChangeAddresses::Single(value) + } +} + +impl From<[Address; 2]> for ChangeAddresses { + fn from(value: [Address; 2]) -> Self { + ChangeAddresses::Double(value) + } +} + impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -121,7 +138,7 @@ impl TransactionBuilder { inscriptions: BTreeMap, amounts: BTreeMap, recipient: Address, - change: [Address; 2], + change: ChangeAddresses, fee_rate: FeeRate, ) -> Result { Self::new( @@ -141,7 +158,7 @@ impl TransactionBuilder { inscriptions: BTreeMap, amounts: BTreeMap, recipient: Address, - change: [Address; 2], + change: ChangeAddresses, fee_rate: FeeRate, output_value: Amount, ) -> Result { @@ -182,29 +199,39 @@ impl TransactionBuilder { inscriptions: BTreeMap, amounts: BTreeMap, recipient: Address, - change: [Address; 2], + change: ChangeAddresses, fee_rate: FeeRate, target: Target, ) -> Result { - if change.contains(&recipient) { - return Err(Error::DuplicateAddress(recipient)); - } - - if change[0] == change[1] { - return Err(Error::DuplicateAddress(change[0].clone())); - } + let change_addresses = match change { + ChangeAddresses::Single(address) => { + if address == recipient { + return Err(Error::DuplicateAddress(recipient)); + } + BTreeSet::from([address]) + } + ChangeAddresses::Double(addresses) => { + if addresses.contains(&recipient) { + return Err(Error::DuplicateAddress(recipient)); + } + if addresses[0] == addresses[1] { + return Err(Error::DuplicateAddress(addresses[0].clone())); + } + BTreeSet::from(addresses) + } + }; Ok(Self { utxos: amounts.keys().cloned().collect(), amounts, - change_addresses: change.iter().cloned().collect(), + change_addresses: change_addresses.clone(), fee_rate, inputs: Vec::new(), inscriptions, outgoing, outputs: Vec::new(), recipient, - unused_change_addresses: change.to_vec(), + unused_change_addresses: change_addresses.into_iter().collect(), target, }) } @@ -673,7 +700,7 @@ mod tests { BTreeMap::new(), utxos.clone().into_iter().collect(), recipient(), - [change(0), change(1)], + ChangeAddresses::Double([change(0), change(1)]), FeeRate::try_from(1.0).unwrap(), Target::Postage, ) @@ -745,7 +772,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), ) .unwrap() @@ -762,7 +789,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), ), Ok(Transaction { @@ -784,7 +811,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), Target::Postage, ) @@ -809,7 +836,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), ), Ok(Transaction { @@ -831,7 +858,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), ), Err(Error::NotEnoughCardinalUtxos), @@ -851,7 +878,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), ), Err(Error::NotEnoughCardinalUtxos), @@ -871,7 +898,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), ), Ok(Transaction { @@ -897,7 +924,7 @@ mod tests { .into_iter() .collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), Target::Postage, ) @@ -916,7 +943,7 @@ mod tests { .into_iter() .collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), Target::Postage, ) @@ -935,7 +962,7 @@ mod tests { .into_iter() .collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), Target::Postage, ) @@ -954,7 +981,7 @@ mod tests { .into_iter() .collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), Target::Postage, ) @@ -979,7 +1006,7 @@ mod tests { .into_iter() .collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), Target::Postage, ) @@ -1002,7 +1029,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), ), Ok(Transaction { @@ -1027,7 +1054,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), Target::Postage, ) @@ -1048,7 +1075,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), ), Ok(Transaction { @@ -1073,7 +1100,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), ), Ok(Transaction { @@ -1095,7 +1122,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), Target::Postage, ) @@ -1123,7 +1150,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), Target::Postage, ) @@ -1149,7 +1176,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), Target::Postage, ) @@ -1172,7 +1199,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), Target::Postage, ) @@ -1255,7 +1282,7 @@ mod tests { BTreeMap::from([(satpoint(2, 10 * COIN_VALUE), inscription_id(1))]), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), ), Err(Error::NotEnoughCardinalUtxos) @@ -1272,7 +1299,7 @@ mod tests { BTreeMap::from([(satpoint(1, 500), inscription_id(1))]), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), ), Err(Error::UtxoContainsAdditionalInscription { @@ -1294,7 +1321,7 @@ mod tests { BTreeMap::from([(satpoint(1, 0), inscription_id(1))]), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), fee_rate, ) .unwrap(); @@ -1323,7 +1350,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), Amount::from_sat(1000) ), @@ -1349,7 +1376,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), Amount::from_sat(1500) ), @@ -1372,7 +1399,7 @@ mod tests { BTreeMap::from([(satpoint(1, 500), inscription_id(1))]), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), Amount::from_sat(1) ), @@ -1396,7 +1423,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), Amount::from_sat(1000) ), @@ -1417,7 +1444,7 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(4.0).unwrap(), Amount::from_sat(1000) ), @@ -1456,7 +1483,7 @@ mod tests { .into_iter() .collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), Amount::from_sat(707) ), @@ -1479,7 +1506,7 @@ mod tests { .into_iter() .collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(1.0).unwrap(), ), Ok(Transaction { @@ -1501,7 +1528,7 @@ mod tests { .into_iter() .collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(5.0).unwrap(), Amount::from_sat(1000) ), @@ -1524,7 +1551,7 @@ mod tests { .into_iter() .collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(6.0).unwrap(), Amount::from_sat(1000) ), @@ -1542,7 +1569,7 @@ mod tests { .into_iter() .collect(), recipient(), - [recipient(), change(1)], + [recipient(), change(1)].into(), FeeRate::try_from(0.0).unwrap(), Amount::from_sat(1000) ), @@ -1560,7 +1587,7 @@ mod tests { .into_iter() .collect(), recipient(), - [change(0), change(0)], + [change(0), change(0)].into(), FeeRate::try_from(0.0).unwrap(), Amount::from_sat(1000) ), @@ -1578,7 +1605,7 @@ mod tests { .into_iter() .collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(2.0).unwrap(), Amount::from_sat(1500) ), @@ -1601,7 +1628,7 @@ mod tests { .into_iter() .collect(), recipient(), - [change(0), change(1)], + [change(0), change(1)].into(), FeeRate::try_from(250.0).unwrap(), ), Ok(Transaction {