diff --git a/crates/mockcore/src/api.rs b/crates/mockcore/src/api.rs index 8571ec3ea3..be6fcecf2b 100644 --- a/crates/mockcore/src/api.rs +++ b/crates/mockcore/src/api.rs @@ -87,7 +87,12 @@ pub trait Api { ) -> Result; #[rpc(name = "sendrawtransaction")] - fn send_raw_transaction(&self, tx: String) -> Result; + fn send_raw_transaction( + &self, + tx: String, + maxfeerate: Option<()>, + maxburnamount: Option, + ) -> Result; #[rpc(name = "sendtoaddress")] fn send_to_address( diff --git a/crates/mockcore/src/lib.rs b/crates/mockcore/src/lib.rs index 8bb2c43891..c9b781e63e 100644 --- a/crates/mockcore/src/lib.rs +++ b/crates/mockcore/src/lib.rs @@ -74,7 +74,7 @@ pub fn builder() -> Builder { Builder { fail_lock_unspent: false, network: Network::Bitcoin, - version: 240000, + version: 250000, } } diff --git a/crates/mockcore/src/server.rs b/crates/mockcore/src/server.rs index 0fe3176b60..bd8916e783 100644 --- a/crates/mockcore/src/server.rs +++ b/crates/mockcore/src/server.rs @@ -1,7 +1,7 @@ use { super::*, base64::Engine, - bitcoin::{consensus::Decodable, psbt::Psbt, Witness}, + bitcoin::{consensus::Decodable, opcodes, psbt::Psbt, script::Instruction, Witness}, bitcoincore_rpc::json::StringOrStringArray, }; @@ -506,9 +506,36 @@ impl Api for Server { ) } - fn send_raw_transaction(&self, tx: String) -> Result { + fn send_raw_transaction( + &self, + tx: String, + maxfeerate: Option<()>, + maxburnamount: Option, + ) -> Result { + assert!( + maxfeerate.is_none(), + "sendrawtransaction: maxfeerate is not supported" + ); + let tx: Transaction = deserialize(&hex::decode(tx).unwrap()).unwrap(); + let burnt = tx + .output + .iter() + .filter(|tx_out| { + tx_out.script_pubkey.instructions().next() + == Some(Ok(Instruction::Op(opcodes::all::OP_RETURN))) + }) + .map(|tx_out| tx_out.value) + .sum::(); + + let maxburnamount = Amount::from_btc(maxburnamount.unwrap_or_default()).unwrap(); + + assert!( + burnt <= maxburnamount, + "burnt amount greater than maxburnamount: {burnt} > {maxburnamount}", + ); + let mut state = self.state.lock().unwrap(); for tx_in in &tx.input { diff --git a/src/subcommand/wallet/burn.rs b/src/subcommand/wallet/burn.rs index 53e6a5adb2..fb9c7d73cf 100644 --- a/src/subcommand/wallet/burn.rs +++ b/src/subcommand/wallet/burn.rs @@ -65,14 +65,25 @@ impl Burn { let mut builder = script::Builder::new().push_opcode(opcodes::all::OP_RETURN); - if let Some(metadata) = metadata { - let push: &script::PushBytes = metadata.as_slice().try_into().with_context(|| { - format!( - "metadata length {} over maximum {}", - metadata.len(), - u32::MAX - ) - })?; + // add empty metadata if none is supplied so we can add padding + let metadata = metadata.unwrap_or_default(); + + let push: &script::PushBytes = metadata.as_slice().try_into().with_context(|| { + format!( + "metadata length {} over maximum {}", + metadata.len(), + u32::MAX + ) + })?; + builder = builder.push_slice(push); + + // pad OP_RETURN script to least five bytes to ensure transaction base size + // is greater than 64 bytes + let padding = 5usize.saturating_sub(builder.as_script().len()); + if padding > 0 { + // subtract one byte push opcode from padding length + let padding = vec![0; padding - 1]; + let push: &script::PushBytes = padding.as_slice().try_into().unwrap(); builder = builder.push_slice(push); } @@ -93,8 +104,14 @@ impl Burn { script_pubkey, )?; + let base_size = unsigned_transaction.base_size(); + assert!( + base_size >= 65, + "transaction base size less than minimum standard tx nonwitness size: {base_size} < 65", + ); + let (txid, psbt, fee) = - wallet.sign_and_broadcast_transaction(unsigned_transaction, self.dry_run)?; + wallet.sign_and_broadcast_transaction(unsigned_transaction, self.dry_run, Some(value))?; Ok(Some(Box::new(send::Output { txid, diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 89abbf6a77..ace88b13ce 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -81,7 +81,7 @@ impl Send { }; let (txid, psbt, fee) = - wallet.sign_and_broadcast_transaction(unsigned_transaction, self.dry_run)?; + wallet.sign_and_broadcast_transaction(unsigned_transaction, self.dry_run, None)?; Ok(Some(Box::new(Output { txid, diff --git a/src/subcommand/wallet/split.rs b/src/subcommand/wallet/split.rs index 92348a155f..1342a34fea 100644 --- a/src/subcommand/wallet/split.rs +++ b/src/subcommand/wallet/split.rs @@ -145,7 +145,7 @@ impl Split { let unsigned_transaction = consensus::encode::deserialize(&unsigned_transaction)?; let (txid, psbt, fee) = - wallet.sign_and_broadcast_transaction(unsigned_transaction, self.dry_run)?; + wallet.sign_and_broadcast_transaction(unsigned_transaction, self.dry_run, None)?; Ok(Some(Box::new(Output { txid, psbt, fee }))) } diff --git a/src/wallet.rs b/src/wallet.rs index 2387d45838..f050d35dc6 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -563,7 +563,7 @@ impl Wallet { } pub(crate) fn check_version(client: Client) -> Result { - const MIN_VERSION: usize = 240000; + const MIN_VERSION: usize = 250000; let bitcoin_version = client.version()?; if bitcoin_version < MIN_VERSION { @@ -743,6 +743,7 @@ impl Wallet { &self, unsigned_transaction: Transaction, dry_run: bool, + burn_amount: Option, ) -> Result<(Txid, String, u64)> { let unspent_outputs = self.utxos(); @@ -777,10 +778,7 @@ impl Wallet { .hex .ok_or_else(|| anyhow!("unable to sign transaction"))?; - ( - self.bitcoin_client().send_raw_transaction(&signed_tx)?, - psbt, - ) + (self.send_raw_transaction(&signed_tx, burn_amount)?, psbt) }; let mut fee = 0; @@ -797,4 +795,23 @@ impl Wallet { Ok((txid, psbt, fee)) } + + fn send_raw_transaction( + &self, + tx: R, + burn_amount: Option, + ) -> Result { + let mut arguments = vec![tx.raw_hex().into()]; + + if let Some(burn_amount) = burn_amount { + arguments.push(serde_json::Value::Null); + arguments.push(burn_amount.to_btc().into()); + } + + Ok( + self + .bitcoin_client() + .call("sendrawtransaction", &arguments)?, + ) + } } diff --git a/tests/wallet/burn.rs b/tests/wallet/burn.rs index 07f355d3e6..a3670523b6 100644 --- a/tests/wallet/burn.rs +++ b/tests/wallet/burn.rs @@ -33,7 +33,7 @@ fn inscriptions_can_be_burned() { 🔥
value
-
9922
+
9918
.*
content length
3 bytes
diff --git a/tests/wallet/inscribe.rs b/tests/wallet/inscribe.rs index d4adccfddd..0b328f8af7 100644 --- a/tests/wallet/inscribe.rs +++ b/tests/wallet/inscribe.rs @@ -74,13 +74,13 @@ fn metaprotocol_appears_on_inscription_page() { #[test] fn inscribe_fails_if_bitcoin_core_is_too_old() { - let core = mockcore::builder().version(230000).build(); + let core = mockcore::builder().version(240000).build(); let ord = TestServer::spawn(&core); CommandBuilder::new("wallet inscribe --file hello.txt --fee-rate 1") .write("hello.txt", "HELLOWORLD") .expected_exit_code(1) - .expected_stderr("error: Bitcoin Core 24.0.0 or newer required, current version is 23.0.0\n") + .expected_stderr("error: Bitcoin Core 25.0.0 or newer required, current version is 24.0.0\n") .core(&core) .ord(&ord) .run_and_extract_stdout();