diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index 462f762952..915fb136f2 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -87,6 +87,7 @@ impl Preview { dry_run: false, no_limit: false, destination: None, + postage: Some(TransactionBuilder::TARGET_POSTAGE), }, )), } diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index 00c1d8da97..875ed5200f 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -24,7 +24,7 @@ pub mod receive; mod restore; pub mod sats; pub mod send; -pub(crate) mod transaction_builder; +pub mod transaction_builder; pub mod transactions; #[derive(Debug, Parser)] diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index c3b8aac51b..f712890656 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -1,6 +1,6 @@ use { super::*, - crate::wallet::Wallet, + crate::{subcommand::wallet::transaction_builder::Target, wallet::Wallet}, bitcoin::{ blockdata::{opcodes, script}, key::PrivateKey, @@ -51,6 +51,11 @@ pub(crate) struct Inscribe { pub(crate) dry_run: bool, #[clap(long, help = "Send inscription to .")] pub(crate) destination: Option>, + #[clap( + long, + help = "Amount of postage to include in the inscription. Default `10000sat`" + )] + pub(crate) postage: Option, } impl Inscribe { @@ -88,6 +93,10 @@ impl Inscribe { self.commit_fee_rate.unwrap_or(self.fee_rate), self.fee_rate, self.no_limit, + match self.postage { + Some(postage) => postage, + _ => TransactionBuilder::TARGET_POSTAGE, + }, )?; utxos.insert( @@ -155,6 +164,7 @@ impl Inscribe { commit_fee_rate: FeeRate, reveal_fee_rate: FeeRate, no_limit: bool, + postage: Amount, ) -> Result<(Transaction, Transaction, TweakedKeyPair)> { let satpoint = if let Some(satpoint) = satpoint { satpoint @@ -220,15 +230,16 @@ impl Inscribe { &reveal_script, ); - let unsigned_commit_tx = TransactionBuilder::build_transaction_with_value( + let unsigned_commit_tx = TransactionBuilder::new( satpoint, inscriptions, utxos, commit_tx_address.clone(), change, commit_fee_rate, - reveal_fee + TransactionBuilder::TARGET_POSTAGE, - )?; + Target::Value(reveal_fee + postage), + ) + .build_transaction()?; let (vout, output) = unsigned_commit_tx .output @@ -393,6 +404,7 @@ mod tests { FeeRate::try_from(1.0).unwrap(), FeeRate::try_from(1.0).unwrap(), false, + TransactionBuilder::TARGET_POSTAGE, ) .unwrap(); @@ -424,6 +436,7 @@ mod tests { FeeRate::try_from(1.0).unwrap(), FeeRate::try_from(1.0).unwrap(), false, + TransactionBuilder::TARGET_POSTAGE, ) .unwrap(); @@ -459,6 +472,7 @@ mod tests { FeeRate::try_from(1.0).unwrap(), FeeRate::try_from(1.0).unwrap(), false, + TransactionBuilder::TARGET_POSTAGE, ) .unwrap_err() .to_string(); @@ -501,6 +515,7 @@ mod tests { FeeRate::try_from(1.0).unwrap(), FeeRate::try_from(1.0).unwrap(), false, + TransactionBuilder::TARGET_POSTAGE, ) .is_ok()) } @@ -537,6 +552,7 @@ mod tests { FeeRate::try_from(fee_rate).unwrap(), FeeRate::try_from(fee_rate).unwrap(), false, + TransactionBuilder::TARGET_POSTAGE, ) .unwrap(); @@ -599,6 +615,7 @@ mod tests { FeeRate::try_from(commit_fee_rate).unwrap(), FeeRate::try_from(fee_rate).unwrap(), false, + TransactionBuilder::TARGET_POSTAGE, ) .unwrap(); @@ -648,6 +665,7 @@ mod tests { FeeRate::try_from(1.0).unwrap(), FeeRate::try_from(1.0).unwrap(), false, + TransactionBuilder::TARGET_POSTAGE, ) .unwrap_err() .to_string(); @@ -679,6 +697,7 @@ mod tests { FeeRate::try_from(1.0).unwrap(), FeeRate::try_from(1.0).unwrap(), true, + TransactionBuilder::TARGET_POSTAGE, ) .unwrap(); diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index f55c4601e5..2c6c9fd574 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -1,4 +1,4 @@ -use {super::*, crate::wallet::Wallet}; +use {super::*, crate::subcommand::wallet::transaction_builder::Target, crate::wallet::Wallet}; #[derive(Debug, Parser)] pub(crate) struct Send { @@ -6,6 +6,11 @@ pub(crate) struct Send { outgoing: Outgoing, #[clap(long, help = "Use fee rate of sats/vB")] fee_rate: FeeRate, + #[clap( + long, + help = "Target amount of postage to include with sent inscriptions. Default `10000sat`" + )] + pub(crate) postage: Option, } #[derive(Serialize, Deserialize)] @@ -67,14 +72,22 @@ impl Send { get_change_address(&client, &options)?, ]; - let unsigned_transaction = TransactionBuilder::build_transaction_with_postage( + let postage = if let Some(postage) = self.postage { + Target::ExactPostage(postage) + } else { + Target::Postage + }; + + let unsigned_transaction = TransactionBuilder::new( satpoint, inscriptions, unspent_outputs, address, change, self.fee_rate, - )?; + postage, + ) + .build_transaction()?; let signed_tx = client .sign_raw_transaction_with_wallet(&unsigned_transaction, None, None)? diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index 5738e0c8ee..77003bc53a 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -63,9 +63,10 @@ pub enum Error { } #[derive(Debug, PartialEq)] -enum Target { +pub enum Target { Value(Amount), Postage, + ExactPostage(Amount), } impl fmt::Display for Error { @@ -97,7 +98,7 @@ impl fmt::Display for Error { impl std::error::Error for Error {} -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct TransactionBuilder { amounts: BTreeMap, change_addresses: BTreeSet
, @@ -117,61 +118,59 @@ type Result = std::result::Result; impl TransactionBuilder { const ADDITIONAL_INPUT_VBYTES: usize = 58; const ADDITIONAL_OUTPUT_VBYTES: usize = 43; - const MAX_POSTAGE: Amount = Amount::from_sat(2 * 10_000); const SCHNORR_SIGNATURE_SIZE: usize = 64; pub(crate) const TARGET_POSTAGE: Amount = Amount::from_sat(10_000); + pub(crate) const MAX_POSTAGE: Amount = Amount::from_sat(2 * 10_000); - pub fn build_transaction_with_postage( + pub(crate) fn new( outgoing: SatPoint, inscriptions: BTreeMap, amounts: BTreeMap, recipient: Address, change: [Address; 2], fee_rate: FeeRate, - ) -> Result { - Self::new( - outgoing, - inscriptions, + target: Target, + ) -> Self { + Self { + utxos: amounts.keys().cloned().collect(), amounts, - recipient, - change, + change_addresses: change.iter().cloned().collect(), fee_rate, - Target::Postage, - )? - .build_transaction() + inputs: Vec::new(), + inscriptions, + outgoing, + outputs: Vec::new(), + recipient, + unused_change_addresses: change.to_vec(), + target, + } } - pub fn build_transaction_with_value( - outgoing: SatPoint, - inscriptions: BTreeMap, - amounts: BTreeMap, - recipient: Address, - change: [Address; 2], - fee_rate: FeeRate, - output_value: Amount, - ) -> Result { - let dust_value = recipient.script_pubkey().dust_value(); + pub(crate) fn build_transaction(self) -> Result { + if self.change_addresses.len() < 2 { + return Err(Error::DuplicateAddress( + self.change_addresses.first().unwrap().clone(), + )); + } - if output_value < dust_value { - return Err(Error::Dust { - output_value, - dust_value, - }); + if self.change_addresses.contains(&self.recipient) { + return Err(Error::DuplicateAddress(self.recipient)); } - Self::new( - outgoing, - inscriptions, - amounts, - recipient, - change, - fee_rate, - Target::Value(output_value), - )? - .build_transaction() - } + match self.target { + Target::Value(output_value) | Target::ExactPostage(output_value) => { + let dust_value = self.recipient.script_pubkey().dust_value(); + + if output_value < dust_value { + return Err(Error::Dust { + output_value, + dust_value, + }); + } + } + _ => (), + } - fn build_transaction(self) -> Result { self .select_outgoing()? .align_outgoing() @@ -182,38 +181,6 @@ impl TransactionBuilder { .build() } - fn new( - outgoing: SatPoint, - inscriptions: BTreeMap, - amounts: BTreeMap, - recipient: Address, - change: [Address; 2], - 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())); - } - - Ok(Self { - utxos: amounts.keys().cloned().collect(), - amounts, - change_addresses: change.iter().cloned().collect(), - fee_rate, - inputs: Vec::new(), - inscriptions, - outgoing, - outputs: Vec::new(), - recipient, - unused_change_addresses: change.to_vec(), - target, - }) - } - fn select_outgoing(mut self) -> Result { for (inscribed_satpoint, inscription_id) in &self.inscriptions { if self.outgoing.outpoint == inscribed_satpoint.outpoint @@ -315,7 +282,7 @@ impl TransactionBuilder { let min_value = match self.target { Target::Postage => self.outputs.last().unwrap().0.script_pubkey().dust_value(), - Target::Value(value) => value, + Target::Value(value) | Target::ExactPostage(value) => value, }; let total = min_value @@ -372,6 +339,7 @@ impl TransactionBuilder { if let Some(excess) = value.checked_sub(self.fee_rate.fee(self.estimate_vbytes())) { let (max, target) = match self.target { + Target::ExactPostage(postage) => (postage, postage), Target::Postage => (Self::MAX_POSTAGE, Self::TARGET_POSTAGE), Target::Value(value) => (value, value), }; @@ -461,7 +429,7 @@ impl TransactionBuilder { previous_output: OutPoint::null(), script_sig: ScriptBuf::new(), sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - witness: Witness::from_slice(&[&[0; TransactionBuilder::SCHNORR_SIGNATURE_SIZE]]), + witness: Witness::from_slice(&[&[0; Self::SCHNORR_SIGNATURE_SIZE]]), }) .collect(), output: outputs @@ -588,6 +556,12 @@ impl TransactionBuilder { "invariant: excess postage is stripped" ); } + Target::ExactPostage(postage) => { + assert!( + Amount::from_sat(output.value) <= postage + slop, + "invariant: excess postage is stripped" + ); + } Target::Value(value) => { assert!( Amount::from_sat(output.value).checked_sub(value).unwrap() @@ -747,7 +721,6 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .unwrap() .select_outgoing() .unwrap(); @@ -810,14 +783,16 @@ mod tests { fn transactions_are_rbf() { let utxos = vec![(outpoint(1), Amount::from_sat(5_000))]; - assert!(TransactionBuilder::build_transaction_with_postage( + assert!(TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), utxos.into_iter().collect(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), + Target::Postage, ) + .build_transaction() .unwrap() .is_explicitly_rbf()) } @@ -827,14 +802,16 @@ mod tests { let utxos = vec![(outpoint(1), Amount::from_sat(5_000))]; pretty_assert_eq!( - TransactionBuilder::build_transaction_with_postage( + TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), utxos.into_iter().collect(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - ), + Target::Postage, + ) + .build_transaction(), Ok(Transaction { version: 1, lock_time: LockTime::ZERO, @@ -858,7 +835,6 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .unwrap() .select_outgoing() .unwrap() .align_outgoing() @@ -874,14 +850,16 @@ mod tests { ]; pretty_assert_eq!( - TransactionBuilder::build_transaction_with_postage( + TransactionBuilder::new( satpoint(1, 4_950), BTreeMap::new(), utxos.into_iter().collect(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - ), + Target::Postage, + ) + .build_transaction(), Ok(Transaction { version: 1, lock_time: LockTime::ZERO, @@ -896,14 +874,16 @@ mod tests { let utxos = vec![(outpoint(1), Amount::from_sat(5_000))]; pretty_assert_eq!( - TransactionBuilder::build_transaction_with_postage( + TransactionBuilder::new( satpoint(1, 4_950), BTreeMap::new(), utxos.into_iter().collect(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - ), + Target::Postage, + ) + .build_transaction(), Err(Error::NotEnoughCardinalUtxos), ) } @@ -916,14 +896,16 @@ mod tests { ]; pretty_assert_eq!( - TransactionBuilder::build_transaction_with_postage( + TransactionBuilder::new( satpoint(1, 4_950), BTreeMap::new(), utxos.into_iter().collect(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - ), + Target::Postage + ) + .build_transaction(), Err(Error::NotEnoughCardinalUtxos), ) } @@ -936,14 +918,16 @@ mod tests { ]; pretty_assert_eq!( - TransactionBuilder::build_transaction_with_postage( + TransactionBuilder::new( satpoint(1, 4_950), BTreeMap::new(), utxos.into_iter().collect(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - ), + Target::Postage, + ) + .build_transaction(), Ok(Transaction { version: 1, lock_time: LockTime::ZERO, @@ -971,7 +955,6 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .unwrap() .build() .unwrap(); } @@ -990,7 +973,6 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .unwrap() .build() .unwrap(); } @@ -1009,7 +991,6 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .unwrap() .build() .unwrap(); } @@ -1028,7 +1009,6 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .unwrap() .select_outgoing() .unwrap(); @@ -1054,7 +1034,6 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .unwrap() .select_outgoing() .unwrap(); @@ -1068,14 +1047,16 @@ mod tests { let utxos = vec![(outpoint(1), Amount::from_sat(1_000_000))]; pretty_assert_eq!( - TransactionBuilder::build_transaction_with_postage( + TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), utxos.into_iter().collect(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - ), + Target::Postage, + ) + .build_transaction(), Ok(Transaction { version: 1, lock_time: LockTime::ZERO, @@ -1102,7 +1083,6 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .unwrap() .select_outgoing() .unwrap() .build() @@ -1114,14 +1094,16 @@ mod tests { let utxos = vec![(outpoint(1), Amount::from_sat(10_000))]; pretty_assert_eq!( - TransactionBuilder::build_transaction_with_postage( + TransactionBuilder::new( satpoint(1, 3_333), BTreeMap::new(), utxos.into_iter().collect(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - ), + Target::Postage, + ) + .build_transaction(), Ok(Transaction { version: 1, lock_time: LockTime::ZERO, @@ -1139,14 +1121,16 @@ mod tests { ]; pretty_assert_eq!( - TransactionBuilder::build_transaction_with_postage( + TransactionBuilder::new( satpoint(1, 1), BTreeMap::new(), utxos.into_iter().collect(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - ), + Target::Postage, + ) + .build_transaction(), Ok(Transaction { version: 1, lock_time: LockTime::ZERO, @@ -1170,7 +1154,6 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .unwrap() .select_outgoing() .unwrap() .align_outgoing() @@ -1198,7 +1181,6 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .unwrap() .select_outgoing() .unwrap() .align_outgoing() @@ -1224,7 +1206,6 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .unwrap() .select_outgoing() .unwrap() .strip_value() @@ -1247,7 +1228,6 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .unwrap() .select_outgoing() .unwrap() .strip_value() @@ -1321,14 +1301,16 @@ mod tests { ]; pretty_assert_eq!( - TransactionBuilder::build_transaction_with_postage( + TransactionBuilder::new( satpoint(1, 0), BTreeMap::from([(satpoint(2, 10 * COIN_VALUE), inscription_id(1))]), utxos.into_iter().collect(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - ), + Target::Postage, + ) + .build_transaction(), Err(Error::NotEnoughCardinalUtxos) ) } @@ -1338,14 +1320,16 @@ mod tests { let utxos = vec![(outpoint(1), Amount::from_sat(1_000))]; pretty_assert_eq!( - TransactionBuilder::build_transaction_with_postage( + TransactionBuilder::new( satpoint(1, 0), BTreeMap::from([(satpoint(1, 500), inscription_id(1))]), utxos.into_iter().collect(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - ), + Target::Postage, + ) + .build_transaction(), Err(Error::UtxoContainsAdditionalInscription { outgoing_satpoint: satpoint(1, 0), inscribed_satpoint: satpoint(1, 500), @@ -1360,14 +1344,16 @@ mod tests { let fee_rate = FeeRate::try_from(17.3).unwrap(); - let transaction = TransactionBuilder::build_transaction_with_postage( + let transaction = TransactionBuilder::new( satpoint(1, 0), BTreeMap::from([(satpoint(1, 0), inscription_id(1))]), utxos.into_iter().collect(), recipient(), [change(0), change(1)], fee_rate, + Target::Postage, ) + .build_transaction() .unwrap(); let fee = @@ -1389,15 +1375,16 @@ mod tests { let utxos = vec![(outpoint(1), Amount::from_sat(5_000))]; pretty_assert_eq!( - TransactionBuilder::build_transaction_with_value( + TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), utxos.into_iter().collect(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - Amount::from_sat(1000) - ), + Target::Value(Amount::from_sat(1000)) + ) + .build_transaction(), Ok(Transaction { version: 1, lock_time: LockTime::ZERO, @@ -1415,15 +1402,16 @@ mod tests { ]; pretty_assert_eq!( - TransactionBuilder::build_transaction_with_value( + TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), utxos.into_iter().collect(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - Amount::from_sat(1500) - ), + Target::Value(Amount::from_sat(1500)) + ) + .build_transaction(), Ok(Transaction { version: 1, lock_time: LockTime::ZERO, @@ -1438,15 +1426,16 @@ mod tests { let utxos = vec![(outpoint(1), Amount::from_sat(1_000))]; pretty_assert_eq!( - TransactionBuilder::build_transaction_with_value( + TransactionBuilder::new( satpoint(1, 0), BTreeMap::from([(satpoint(1, 500), inscription_id(1))]), utxos.into_iter().collect(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - Amount::from_sat(1) - ), + Target::Value(Amount::from_sat(1)) + ) + .build_transaction(), Err(Error::Dust { output_value: Amount::from_sat(1), dust_value: Amount::from_sat(294) @@ -1462,15 +1451,16 @@ mod tests { ]; pretty_assert_eq!( - TransactionBuilder::build_transaction_with_value( + TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), utxos.into_iter().collect(), recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - Amount::from_sat(1000) - ), + Target::Value(Amount::from_sat(1000)) + ) + .build_transaction(), Err(Error::NotEnoughCardinalUtxos), ) } @@ -1483,15 +1473,16 @@ mod tests { ]; pretty_assert_eq!( - TransactionBuilder::build_transaction_with_value( + TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), utxos.into_iter().collect(), recipient(), [change(0), change(1)], FeeRate::try_from(4.0).unwrap(), - Amount::from_sat(1000) - ), + Target::Value(Amount::from_sat(1000)) + ) + .build_transaction(), Err(Error::NotEnoughCardinalUtxos), ) } @@ -1521,7 +1512,7 @@ mod tests { #[test] fn do_not_strip_excess_value_if_it_would_create_dust() { pretty_assert_eq!( - TransactionBuilder::build_transaction_with_value( + TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), vec![(outpoint(1), Amount::from_sat(1_000))] @@ -1530,8 +1521,9 @@ mod tests { recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - Amount::from_sat(707) - ), + Target::Value(Amount::from_sat(707)) + ) + .build_transaction(), Ok(Transaction { version: 1, lock_time: LockTime::ZERO, @@ -1544,7 +1536,7 @@ mod tests { #[test] fn possible_to_create_output_of_exactly_max_postage() { pretty_assert_eq!( - TransactionBuilder::build_transaction_with_postage( + TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), vec![(outpoint(1), Amount::from_sat(20_099))] @@ -1553,7 +1545,9 @@ mod tests { recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - ), + Target::Postage, + ) + .build_transaction(), Ok(Transaction { version: 1, lock_time: LockTime::ZERO, @@ -1566,7 +1560,7 @@ mod tests { #[test] fn do_not_strip_excess_value_if_additional_output_cannot_pay_fee() { pretty_assert_eq!( - TransactionBuilder::build_transaction_with_value( + TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), vec![(outpoint(1), Amount::from_sat(1_500))] @@ -1575,8 +1569,9 @@ mod tests { recipient(), [change(0), change(1)], FeeRate::try_from(5.0).unwrap(), - Amount::from_sat(1000) - ), + Target::Value(Amount::from_sat(1000)) + ) + .build_transaction(), Ok(Transaction { version: 1, lock_time: LockTime::ZERO, @@ -1589,7 +1584,7 @@ mod tests { #[test] fn correct_error_is_returned_when_fee_cannot_be_paid() { pretty_assert_eq!( - TransactionBuilder::build_transaction_with_value( + TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), vec![(outpoint(1), Amount::from_sat(1_500))] @@ -1598,8 +1593,9 @@ mod tests { recipient(), [change(0), change(1)], FeeRate::try_from(6.0).unwrap(), - Amount::from_sat(1000) - ), + Target::Value(Amount::from_sat(1000)) + ) + .build_transaction(), Err(Error::NotEnoughCardinalUtxos) ); } @@ -1607,7 +1603,7 @@ mod tests { #[test] fn recipient_address_must_be_unique() { pretty_assert_eq!( - TransactionBuilder::build_transaction_with_value( + TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), vec![(outpoint(1), Amount::from_sat(1000))] @@ -1616,8 +1612,9 @@ mod tests { recipient(), [recipient(), change(1)], FeeRate::try_from(0.0).unwrap(), - Amount::from_sat(1000) - ), + Target::Value(Amount::from_sat(1000)) + ) + .build_transaction(), Err(Error::DuplicateAddress(recipient())) ); } @@ -1625,7 +1622,7 @@ mod tests { #[test] fn change_addresses_must_be_unique() { pretty_assert_eq!( - TransactionBuilder::build_transaction_with_value( + TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), vec![(outpoint(1), Amount::from_sat(1000))] @@ -1634,8 +1631,9 @@ mod tests { recipient(), [change(0), change(0)], FeeRate::try_from(0.0).unwrap(), - Amount::from_sat(1000) - ), + Target::Value(Amount::from_sat(1000)) + ) + .build_transaction(), Err(Error::DuplicateAddress(change(0))) ); } @@ -1643,7 +1641,7 @@ mod tests { #[test] fn output_over_value_because_fees_prevent_excess_value_stripping() { pretty_assert_eq!( - TransactionBuilder::build_transaction_with_value( + TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), vec![(outpoint(1), Amount::from_sat(2000))] @@ -1652,8 +1650,9 @@ mod tests { recipient(), [change(0), change(1)], FeeRate::try_from(2.0).unwrap(), - Amount::from_sat(1500) - ), + Target::Value(Amount::from_sat(1500)) + ) + .build_transaction(), Ok(Transaction { version: 1, lock_time: LockTime::ZERO, @@ -1666,7 +1665,7 @@ mod tests { #[test] fn output_over_max_postage_because_fees_prevent_excess_value_stripping() { pretty_assert_eq!( - TransactionBuilder::build_transaction_with_postage( + TransactionBuilder::new( satpoint(1, 0), BTreeMap::new(), vec![(outpoint(1), Amount::from_sat(45000))] @@ -1675,7 +1674,9 @@ mod tests { recipient(), [change(0), change(1)], FeeRate::try_from(250.0).unwrap(), - ), + Target::Postage, + ) + .build_transaction(), Ok(Transaction { version: 1, lock_time: LockTime::ZERO, @@ -1705,7 +1706,6 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), ) - .unwrap() .select_outgoing() .unwrap() .add_value() @@ -1750,7 +1750,6 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), ) - .unwrap() .select_outgoing() .unwrap() .align_outgoing() @@ -1801,8 +1800,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), - ) - .unwrap(); + ); assert_eq!( tx_builder @@ -1841,4 +1839,39 @@ mod tests { Amount::from_sat(20_000), ); } + + #[test] + fn build_transaction_with_custom_postage() { + let utxos = vec![(outpoint(1), Amount::from_sat(1_000_000))]; + + let fee_rate = FeeRate::try_from(17.3).unwrap(); + + let transaction = TransactionBuilder::new( + satpoint(1, 0), + BTreeMap::from([(satpoint(1, 0), inscription_id(1))]), + utxos.into_iter().collect(), + recipient(), + [change(0), change(1)], + fee_rate, + Target::ExactPostage(Amount::from_sat(66_000)), + ) + .build_transaction() + .unwrap(); + + let fee = + fee_rate.fee(transaction.vsize() + TransactionBuilder::SCHNORR_SIGNATURE_SIZE / 4 + 1); + + pretty_assert_eq!( + transaction, + Transaction { + version: 1, + lock_time: LockTime::ZERO, + input: vec![tx_in(outpoint(1))], + output: vec![ + tx_out(66_000, recipient()), + tx_out(1_000_000 - 66_000 - fee.to_sat(), change(1)) + ], + } + ) + } } diff --git a/tests/wallet/inscribe.rs b/tests/wallet/inscribe.rs index 03a20ee03a..47527a8284 100644 --- a/tests/wallet/inscribe.rs +++ b/tests/wallet/inscribe.rs @@ -417,3 +417,24 @@ fn inscribe_with_no_limit() { .write("degenerate.png", four_megger) .rpc_server(&rpc_server); } + +#[test] +fn inscribe_works_with_postage() { + let rpc_server = test_bitcoincore_rpc::spawn(); + create_wallet(&rpc_server); + rpc_server.mine_blocks(1); + + CommandBuilder::new("wallet inscribe foo.txt --postage 5btc --fee-rate 10".to_string()) + .write("foo.txt", [0; 350]) + .rpc_server(&rpc_server) + .run_and_check_output::(); + + rpc_server.mine_blocks(1); + + let inscriptions = CommandBuilder::new("wallet inscriptions".to_string()) + .write("foo.txt", [0; 350]) + .rpc_server(&rpc_server) + .run_and_check_output::>(); + + pretty_assert_eq!(inscriptions[0].postage, 5 * COIN_VALUE); +} diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index 5ecf6122c8..0e26079877 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -346,3 +346,36 @@ fn user_must_provide_fee_rate_to_send() { ) .run_and_extract_stdout(); } + +#[test] +fn wallet_send_with_fee_rate_and_target_postage() { + let rpc_server = test_bitcoincore_rpc::spawn(); + create_wallet(&rpc_server); + rpc_server.mine_blocks(1); + + let Inscribe { inscription, .. } = inscribe(&rpc_server); + + CommandBuilder::new(format!( + "wallet send bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {inscription} --fee-rate 2.0 --postage 77000sat" + )) + .rpc_server(&rpc_server) + .stdout_regex("[[:xdigit:]]{64}\n") + .run_and_extract_stdout(); + + let tx = &rpc_server.mempool()[0]; + let mut fee = 0; + for input in &tx.input { + fee += rpc_server + .get_utxo_amount(&input.previous_output) + .unwrap() + .to_sat(); + } + for output in &tx.output { + fee -= output.value; + } + + let fee_rate = fee as f64 / tx.vsize() as f64; + + pretty_assert_eq!(fee_rate, 2.0); + pretty_assert_eq!(tx.output[0].value, 77_000); +}