diff --git a/src/fee_rate.rs b/src/fee_rate.rs new file mode 100644 index 0000000000..8046288c6c --- /dev/null +++ b/src/fee_rate.rs @@ -0,0 +1,66 @@ +use super::*; + +#[derive(Debug, PartialEq, Clone, Copy)] +pub(crate) struct FeeRate(f64); + +impl FromStr for FeeRate { + type Err = Error; + + fn from_str(s: &str) -> Result { + Self::try_from(f64::from_str(s)?) + } +} + +impl TryFrom for FeeRate { + type Error = Error; + + fn try_from(rate: f64) -> Result { + if rate.is_sign_negative() | rate.is_nan() | rate.is_infinite() { + bail!("invalid fee rate: {rate}") + } + Ok(Self(rate)) + } +} + +impl FeeRate { + pub(crate) fn fee(&self, vsize: usize) -> Amount { + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_sign_loss)] + Amount::from_sat((self.0 * vsize as f64).round() as u64) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse() { + assert_eq!("1.1".parse::().unwrap().0, 1.1); + assert_eq!("11.19".parse::().unwrap().0, 11.19); + assert_eq!("11.1111".parse::().unwrap().0, 11.1111); + assert!("-4.2".parse::().is_err()); + assert!(FeeRate::try_from(f64::INFINITY).is_err()); + assert!(FeeRate::try_from(f64::NAN).is_err()); + } + + #[test] + fn fee() { + assert_eq!( + "2.5".parse::().unwrap().fee(100), + Amount::from_sat(250) + ); + assert_eq!( + "2.0".parse::().unwrap().fee(1024), + Amount::from_sat(2048) + ); + assert_eq!( + "1.1".parse::().unwrap().fee(100), + Amount::from_sat(110) + ); + assert_eq!( + "1.0".parse::().unwrap().fee(123456789), + Amount::from_sat(123456789) + ); + } +} diff --git a/src/main.rs b/src/main.rs index 24ee4955ac..6a7af69afa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -85,6 +85,7 @@ mod content; mod decimal; mod degree; mod epoch; +mod fee_rate; mod height; mod index; mod inscription; diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index f94e68528e..b2bfa37609 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -1,4 +1,4 @@ -use super::*; +use {super::*, fee_rate::FeeRate}; #[derive(Debug, Parser)] pub(crate) struct Preview { @@ -77,6 +77,7 @@ impl Preview { options: options.clone(), subcommand: Subcommand::Wallet(super::wallet::Wallet::Inscribe( super::wallet::inscribe::Inscribe { + fee_rate: FeeRate::try_from(1.0).unwrap(), file, no_backup: true, satpoint: None, diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index eac564bd68..76802beb34 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -1,4 +1,4 @@ -use {super::*, transaction_builder::TransactionBuilder}; +use {super::*, fee_rate::FeeRate, transaction_builder::TransactionBuilder}; mod balance; pub(crate) mod create; diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index a88ba6bcc6..bd40f97d23 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -20,6 +20,12 @@ use { pub(crate) struct Inscribe { #[clap(long, help = "Inscribe ")] pub(crate) satpoint: Option, + #[clap( + long, + default_value = "1.0", + help = "Use fee rate of sats/vB" + )] + pub(crate) fee_rate: FeeRate, #[clap(help = "Inscribe sat with contents of ")] pub(crate) file: PathBuf, #[clap(long, help = "Do not back up recovery key.")] @@ -52,6 +58,7 @@ impl Inscribe { utxos, commit_tx_change, reveal_tx_destination, + self.fee_rate, )?; if !self.no_backup { @@ -83,6 +90,7 @@ impl Inscribe { utxos: BTreeMap, change: Vec
, destination: Address, + fee_rate: FeeRate, ) -> Result<(Transaction, Transaction, TweakedKeyPair)> { let satpoint = if let Some(satpoint) = satpoint { satpoint @@ -143,6 +151,7 @@ impl Inscribe { utxos, commit_tx_address.clone(), change, + fee_rate, )?; let (vout, output) = unsigned_commit_tx @@ -181,7 +190,7 @@ impl Inscribe { reveal_tx.input[0].witness.push(&reveal_script); reveal_tx.input[0].witness.push(&control_block.serialize()); - TransactionBuilder::TARGET_FEE_RATE * reveal_tx.vsize().try_into().unwrap() + fee_rate.fee(reveal_tx.vsize()) }; reveal_tx.output[0].value = reveal_tx.output[0] @@ -279,10 +288,13 @@ mod tests { utxos.into_iter().collect(), vec![commit_address, change(1)], reveal_address, + FeeRate::try_from(1.0).unwrap(), ) .unwrap(); - let fee = TransactionBuilder::TARGET_FEE_RATE * reveal_tx.vsize().try_into().unwrap(); + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_sign_loss)] + let fee = Amount::from_sat((1.0 * (reveal_tx.vsize() as f64)).ceil() as u64); assert_eq!( reveal_tx.output[0].value, @@ -306,6 +318,7 @@ mod tests { utxos.into_iter().collect(), vec![commit_address, change(1)], reveal_address, + FeeRate::try_from(1.0).unwrap(), ) .unwrap_err() .to_string() @@ -314,7 +327,7 @@ mod tests { #[test] fn reveal_transaction_would_create_dust() { - let utxos = vec![(outpoint(1), Amount::from_sat(600))]; + let utxos = vec![(outpoint(1), Amount::from_sat(500))]; let inscription = inscription("text/plain", "ord"); let satpoint = Some(satpoint(1, 0)); let commit_address = change(0); @@ -328,6 +341,7 @@ mod tests { utxos.into_iter().collect(), vec![commit_address, change(1)], reveal_address, + FeeRate::try_from(1.0).unwrap(), ) .unwrap_err() .to_string(); @@ -354,6 +368,7 @@ mod tests { utxos.into_iter().collect(), vec![commit_address, change(1)], reveal_address, + FeeRate::try_from(1.0).unwrap(), ) .unwrap(); @@ -386,6 +401,7 @@ mod tests { utxos.into_iter().collect(), vec![commit_address, change(1)], reveal_address, + FeeRate::try_from(1.0).unwrap(), ) .unwrap_err() .to_string(); @@ -425,7 +441,52 @@ mod tests { utxos.into_iter().collect(), vec![commit_address, change(1)], reveal_address, + FeeRate::try_from(1.0).unwrap(), ) .is_ok()) } + + #[test] + fn inscribe_with_custom_fee_rate() { + let utxos = vec![ + (outpoint(1), Amount::from_sat(10_000)), + (outpoint(2), Amount::from_sat(10_000)), + ]; + let mut inscriptions = BTreeMap::new(); + inscriptions.insert( + SatPoint { + outpoint: outpoint(1), + offset: 0, + }, + Txid::from_str("06413a3ef4232f0485df2bc7c912c13c05c69f967c19639344753e05edb64bd5").unwrap(), + ); + + let inscription = inscription("text/plain", "ord"); + let satpoint = None; + let commit_address = change(0); + let reveal_address = recipient(); + let fee_rate = 3.3; + + let (commit_tx, reveal_tx, _private_key) = Inscribe::create_inscription_transactions( + satpoint, + inscription, + inscriptions, + bitcoin::Network::Signet, + utxos.into_iter().collect(), + vec![commit_address, change(1)], + reveal_address, + FeeRate::try_from(fee_rate).unwrap(), + ) + .unwrap(); + + let fee = FeeRate::try_from(fee_rate) + .unwrap() + .fee(reveal_tx.vsize()) + .to_sat(); + + assert_eq!( + reveal_tx.output[0].value, + 10_000 - fee - (10_000 - commit_tx.output[0].value), + ); + } } diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 5759f4bd1a..4a40e18d6f 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -4,6 +4,12 @@ use super::*; pub(crate) struct Send { address: Address, outgoing: Outgoing, + #[clap( + long, + default_value = "1.0", + help = "Use fee rate of sats/vB" + )] + fee_rate: FeeRate, } impl Send { @@ -70,6 +76,7 @@ impl Send { unspent_outputs, self.address, change, + self.fee_rate, )?; let signed_tx = client diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index 40ab1d9925..c4842f82f4 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -26,7 +26,7 @@ use { super::*, bitcoin::{ - blockdata::{locktime::PackedLockTime, script, witness::Witness}, + blockdata::{locktime::PackedLockTime, witness::Witness}, util::amount::Amount, }, std::collections::{BTreeMap, BTreeSet}, @@ -69,11 +69,12 @@ impl std::error::Error for Error {} pub(crate) struct TransactionBuilder { amounts: BTreeMap, change_addresses: BTreeSet
, + fee_rate: FeeRate, inputs: Vec, inscriptions: BTreeMap, + outgoing: SatPoint, outputs: Vec<(Address, Amount)>, recipient: Address, - outgoing: SatPoint, unused_change_addresses: Vec
, utxos: BTreeSet, } @@ -81,9 +82,9 @@ pub(crate) struct TransactionBuilder { type Result = std::result::Result; impl TransactionBuilder { - pub(crate) const TARGET_FEE_RATE: Amount = Amount::from_sat(1); const MAX_POSTAGE: Amount = Amount::from_sat(2 * 10_000); const TARGET_POSTAGE: Amount = Amount::from_sat(10_000); + const SCHNORR_SIGNATURE_SIZE: usize = 64; pub(crate) fn build_transaction( outgoing: SatPoint, @@ -91,8 +92,9 @@ impl TransactionBuilder { amounts: BTreeMap, recipient: Address, change: Vec
, + fee_rate: FeeRate, ) -> Result { - Self::new(outgoing, inscriptions, amounts, recipient, change) + Self::new(outgoing, inscriptions, amounts, recipient, change, fee_rate) .select_outgoing()? .align_outgoing() .pad_alignment_output()? @@ -108,16 +110,18 @@ impl TransactionBuilder { amounts: BTreeMap, recipient: Address, change: Vec
, + fee_rate: FeeRate, ) -> Self { Self { utxos: amounts.keys().cloned().collect(), amounts, change_addresses: change.iter().cloned().collect(), + fee_rate, inputs: Vec::new(), inscriptions, + outgoing, outputs: Vec::new(), recipient, - outgoing, unused_change_addresses: change, } } @@ -188,7 +192,7 @@ impl TransactionBuilder { } fn add_postage(mut self) -> Result { - let estimated_fee = self.estimate_fee(); + let estimated_fee = self.fee_rate.fee(self.estimate_vsize()); let dust_limit = self.outputs.last().unwrap().0.script_pubkey().dust_value(); if self.outputs.last().unwrap().1 < dust_limit + estimated_fee { @@ -232,7 +236,7 @@ impl TransactionBuilder { fn deduct_fee(mut self) -> Self { let sat_offset = self.calculate_sat_offset(); - let fee = self.estimate_fee(); + let fee = self.fee_rate.fee(self.estimate_vsize()); let total_output_amount = self .outputs @@ -255,12 +259,10 @@ impl TransactionBuilder { self } - /// Estimate the size in virtual bytes of the transaction being built. Since - /// we don't know the size of the input script sigs and witnesses, assume - /// they are P2PKH, so that we get a worst case estimate, since it's probably - /// better to pay too overestimate and pay too much in fees than to - /// underestimate and never get the transaction confirmed, or, even worse, be - /// under the minimum relay fee and never even get relayed. + /// Estimate the size in virtual bytes of the transaction under construction. + /// We initialize wallets with taproot descriptors only, so we know that all + /// inputs are taproot key path spends, which allows us to know that witnesses + /// will all consist of single Schnorr signatures. fn estimate_vsize(&self) -> usize { Transaction { version: 1, @@ -270,12 +272,9 @@ impl TransactionBuilder { .iter() .map(|_| TxIn { previous_output: OutPoint::null(), - script_sig: script::Builder::new() - .push_slice(&[0; 71]) - .push_slice(&[0; 65]) - .into_script(), + script_sig: Script::new(), sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - witness: Witness::new(), + witness: Witness::from_vec(vec![vec![0; TransactionBuilder::SCHNORR_SIGNATURE_SIZE]]), }) .collect(), output: self @@ -290,10 +289,6 @@ impl TransactionBuilder { .vsize() } - fn estimate_fee(&self) -> Amount { - Self::TARGET_FEE_RATE * self.estimate_vsize().try_into().unwrap() - } - fn build(self) -> Result { let recipient = self.recipient.script_pubkey(); let transaction = Transaction { @@ -415,21 +410,23 @@ impl TransactionBuilder { offset += output.value; } - let mut fee = Amount::ZERO; + let mut actual_fee = Amount::ZERO; for input in &transaction.input { - fee += self.amounts[&input.previous_output]; + actual_fee += self.amounts[&input.previous_output]; } for output in &transaction.output { - fee -= Amount::from_sat(output.value); + actual_fee -= Amount::from_sat(output.value); } - let fee_rate = fee.to_sat() as f64 / self.estimate_vsize() as f64; - let target_fee_rate = Self::TARGET_FEE_RATE.to_sat() as f64; - assert!( - fee_rate == target_fee_rate, - "invariant: fee rate is equal to target fee rate: actual fee rate: {} target_fee rate: {}", - fee_rate, - target_fee_rate, + let mut modified_tx = transaction.clone(); + for input in &mut modified_tx.input { + input.witness = Witness::from_vec(vec![vec![0; 64]]); + } + let expected_fee = self.fee_rate.fee(modified_tx.vsize()); + + assert_eq!( + actual_fee, expected_fee, + "invariant: fee estimation is correct", ); for tx_out in &transaction.output { @@ -503,6 +500,7 @@ mod tests { utxos.clone().into_iter().collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ) .select_outgoing() .unwrap(); @@ -531,6 +529,7 @@ mod tests { let tx_builder = TransactionBuilder { amounts, + fee_rate: FeeRate::try_from(1.0).unwrap(), utxos: BTreeSet::new(), outgoing: satpoint(1, 0), inscriptions: BTreeMap::new(), @@ -541,7 +540,7 @@ mod tests { outputs: vec![ (recipient(), Amount::from_sat(5_000)), (change(0), Amount::from_sat(5_000)), - (change(1), Amount::from_sat(1_360)), + (change(1), Amount::from_sat(1_724)), ], }; @@ -554,7 +553,7 @@ mod tests { output: vec![ tx_out(5_000, recipient()), tx_out(5_000, change(0)), - tx_out(1_360, change(1)) + tx_out(1_724, change(1)) ], }) ) @@ -570,6 +569,7 @@ mod tests { utxos.into_iter().collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ) .unwrap() .is_explicitly_rbf()) @@ -586,12 +586,13 @@ mod tests { utxos.into_iter().collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ), Ok(Transaction { version: 1, lock_time: PackedLockTime::ZERO, input: vec![tx_in(outpoint(1))], - output: vec![tx_out(4780, recipient())], + output: vec![tx_out(4901, recipient())], }) ) } @@ -607,6 +608,7 @@ mod tests { utxos.into_iter().collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ) .select_outgoing() .unwrap() @@ -629,12 +631,13 @@ mod tests { utxos.into_iter().collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ), Ok(Transaction { version: 1, lock_time: PackedLockTime::ZERO, input: vec![tx_in(outpoint(1)), tx_in(outpoint(2))], - output: vec![tx_out(4_950, change(1)), tx_out(4_620, recipient())], + output: vec![tx_out(4_950, change(1)), tx_out(4_862, recipient())], }) ) } @@ -650,6 +653,7 @@ mod tests { utxos.into_iter().collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ), Err(Error::NotEnoughCardinalUtxos), ) @@ -669,6 +673,7 @@ mod tests { utxos.into_iter().collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ), Err(Error::NotEnoughCardinalUtxos), ) @@ -688,6 +693,7 @@ mod tests { utxos.into_iter().collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ), Ok(Transaction { version: 1, @@ -696,7 +702,7 @@ mod tests { output: vec![ tx_out(4_950, change(1)), tx_out(TransactionBuilder::TARGET_POSTAGE.to_sat(), recipient()), - tx_out(9_589, change(0)), + tx_out(9_831, change(0)), ], }) ) @@ -713,6 +719,7 @@ mod tests { .collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ) .build() .unwrap(); @@ -729,6 +736,7 @@ mod tests { .collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ) .build() .unwrap(); @@ -745,6 +753,7 @@ mod tests { .collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ) .build() .unwrap(); @@ -761,6 +770,7 @@ mod tests { .collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ) .select_outgoing() .unwrap(); @@ -783,6 +793,7 @@ mod tests { .collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ) .select_outgoing() .unwrap(); @@ -802,7 +813,8 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - vec![change(0), change(1)] + vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ), Ok(Transaction { version: 1, @@ -810,7 +822,7 @@ mod tests { input: vec![tx_in(outpoint(1))], output: vec![ tx_out(TransactionBuilder::TARGET_POSTAGE.to_sat(), recipient()), - tx_out(989_749, change(1)) + tx_out(989_870, change(1)) ], }) ) @@ -827,6 +839,7 @@ mod tests { utxos.into_iter().collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ) .select_outgoing() .unwrap() @@ -844,13 +857,14 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - vec![change(0), change(1)] + vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ), Ok(Transaction { version: 1, lock_time: PackedLockTime::ZERO, input: vec![tx_in(outpoint(1))], - output: vec![tx_out(3_333, change(1)), tx_out(6_416, recipient())], + output: vec![tx_out(3_333, change(1)), tx_out(6_537, recipient())], }) ) } @@ -868,13 +882,14 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), recipient(), - vec![change(0), change(1)] + vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ), Ok(Transaction { version: 1, lock_time: PackedLockTime::ZERO, input: vec![tx_in(outpoint(2)), tx_in(outpoint(1))], - output: vec![tx_out(10_001, change(1)), tx_out(9_569, recipient())], + output: vec![tx_out(10_001, change(1)), tx_out(9_811, recipient())], }) ) } @@ -890,6 +905,7 @@ mod tests { utxos.into_iter().collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ) .select_outgoing() .unwrap() @@ -915,6 +931,7 @@ mod tests { utxos.into_iter().collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ) .select_outgoing() .unwrap() @@ -938,6 +955,7 @@ mod tests { utxos.into_iter().collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ) .select_outgoing() .unwrap() @@ -948,7 +966,7 @@ mod tests { } #[test] - #[should_panic(expected = "invariant: fee rate is equal to target fee rate")] + #[should_panic(expected = "invariant: fee estimation is correct")] fn invariant_fee_is_at_least_target_fee_rate() { let utxos = vec![(outpoint(1), Amount::from_sat(10_000))]; @@ -958,6 +976,7 @@ mod tests { utxos.into_iter().collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ) .select_outgoing() .unwrap() @@ -976,6 +995,7 @@ mod tests { TransactionBuilder { amounts, + fee_rate: FeeRate::try_from(1.0).unwrap(), utxos: BTreeSet::new(), outgoing: satpoint(1, 0), inscriptions: BTreeMap::new(), @@ -1003,6 +1023,7 @@ mod tests { TransactionBuilder { amounts, + fee_rate: FeeRate::try_from(1.0).unwrap(), utxos: BTreeSet::new(), outgoing: satpoint(1, 0), inscriptions: BTreeMap::new(), @@ -1039,6 +1060,7 @@ mod tests { utxos.into_iter().collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ), Err(Error::NotEnoughCardinalUtxos) ) @@ -1060,6 +1082,7 @@ mod tests { utxos.into_iter().collect(), recipient(), vec![change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), ), Err(Error::UtxoContainsAdditionalInscription { outgoing_satpoint: satpoint(1, 0), @@ -1070,4 +1093,39 @@ mod tests { }) ) } + + #[test] + fn build_transaction_with_custom_fee_rate() { + let utxos = vec![(outpoint(1), Amount::from_sat(10_000))]; + + let fee_rate = FeeRate::try_from(17.3).unwrap(); + + let transaction = TransactionBuilder::build_transaction( + satpoint(1, 0), + BTreeMap::from([( + satpoint(1, 0), + "bed200b55adcf20e359bbb762392d5106cafbafc48e55f77c94d3041de3521da" + .parse() + .unwrap(), + )]), + utxos.into_iter().collect(), + recipient(), + vec![change(0), change(1)], + fee_rate, + ) + .unwrap(); + + let fee = + fee_rate.fee(transaction.vsize() + TransactionBuilder::SCHNORR_SIGNATURE_SIZE / 4 + 1); + + pretty_assert_eq!( + transaction, + Transaction { + version: 1, + lock_time: PackedLockTime::ZERO, + input: vec![tx_in(outpoint(1))], + output: vec![tx_out(10_000 - fee.to_sat(), recipient())], + } + ) + } } diff --git a/test-bitcoincore-rpc/src/lib.rs b/test-bitcoincore-rpc/src/lib.rs index b70b2ae98e..a1b549ef56 100644 --- a/test-bitcoincore-rpc/src/lib.rs +++ b/test-bitcoincore-rpc/src/lib.rs @@ -203,6 +203,10 @@ impl Handle { self.state().pop_block() } + pub fn get_utxo_amount(&self, outpoint: &OutPoint) -> Option { + self.state().utxos.get(outpoint).cloned() + } + pub fn tx(&self, bi: usize, ti: usize) -> Transaction { let state = self.state(); state.blocks[&state.hashes[bi]].txdata[ti].clone() diff --git a/test-bitcoincore-rpc/src/server.rs b/test-bitcoincore-rpc/src/server.rs index 22f51d495a..6b45309faa 100644 --- a/test-bitcoincore-rpc/src/server.rs +++ b/test-bitcoincore-rpc/src/server.rs @@ -1,9 +1,11 @@ use { super::*, bitcoin::{ + psbt::serialize::Deserialize, secp256k1::{rand, KeyPair, Secp256k1, XOnlyPublicKey}, Address, Witness, }, + bitcoincore_rpc::RawTx, }; pub(crate) struct Server { @@ -241,9 +243,14 @@ impl Api for Server { assert_eq!(utxos, None, "utxos param not supported"); assert_eq!(sighash_type, None, "sighash_type param not supported"); + let mut transaction = Transaction::deserialize(&hex::decode(tx).unwrap()).unwrap(); + for input in &mut transaction.input { + input.witness = Witness::from_vec(vec![vec![0; 64]]); + } + Ok( serde_json::to_value(SignRawTransactionResult { - hex: hex::decode(tx).unwrap(), + hex: hex::decode(transaction.raw_hex()).unwrap(), complete: true, errors: None, }) diff --git a/tests/wallet/inscribe.rs b/tests/wallet/inscribe.rs index 3b94cf5f39..c0634d44f6 100644 --- a/tests/wallet/inscribe.rs +++ b/tests/wallet/inscribe.rs @@ -265,3 +265,32 @@ fn inscribe_with_optional_satpoint_arg() { TestServer::spawn_with_args(&rpc_server, &[]) .assert_response_regex(format!("/content/{inscription_id}",), ".*HELLOWORLD.*"); } + +#[test] +fn inscribe_with_fee_rate() { + let rpc_server = test_bitcoincore_rpc::spawn(); + create_wallet(&rpc_server); + rpc_server.mine_blocks(1); + + CommandBuilder::new("--index-sats wallet inscribe degenerate.png --fee-rate 2.0") + .write("degenerate.png", [1; 520]) + .rpc_server(&rpc_server) + .stdout_regex("commit\t[[:xdigit:]]{64}\nreveal\t[[:xdigit:]]{64}\n") + .run(); + + 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); +} diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index 0567cdcbf0..561c1e7163 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -331,3 +331,43 @@ fn send_btc_fails_if_lock_unspent_fails() { .expected_exit_code(1) .run(); } + +#[test] +fn wallet_send_with_fee_rate() { + let rpc_server = test_bitcoincore_rpc::spawn(); + create_wallet(&rpc_server); + rpc_server.mine_blocks(1); + + let stdout = CommandBuilder::new("--index-sats wallet inscribe degenerate.png") + .write("degenerate.png", [1; 520]) + .rpc_server(&rpc_server) + .stdout_regex("commit\t[[:xdigit:]]{64}\nreveal\t[[:xdigit:]]{64}\n") + .run(); + + rpc_server.mine_blocks(1); + + let reveal_txid = reveal_txid_from_inscribe_stdout(&stdout); + + CommandBuilder::new(format!( + "wallet send bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {reveal_txid} --fee-rate 2.0" + )) + .rpc_server(&rpc_server) + .stdout_regex("[[:xdigit:]]{64}\n") + .run(); + + 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); +}