diff --git a/crates/mockcore/src/lib.rs b/crates/mockcore/src/lib.rs
index 219bd79593..cf74318f4b 100644
--- a/crates/mockcore/src/lib.rs
+++ b/crates/mockcore/src/lib.rs
@@ -137,6 +137,7 @@ pub struct TransactionTemplate<'a> {
pub output_values: &'a [u64],
pub outputs: usize,
pub p2tr: bool,
+ pub receiver: Option
,
}
#[derive(Serialize, Deserialize, Debug)]
@@ -185,6 +186,7 @@ impl<'a> Default for TransactionTemplate<'a> {
output_values: &[],
outputs: 1,
p2tr: false,
+ receiver: None,
}
}
}
diff --git a/crates/mockcore/src/state.rs b/crates/mockcore/src/state.rs
index be3a9dce5e..f007a7c8ea 100644
--- a/crates/mockcore/src/state.rs
+++ b/crates/mockcore/src/state.rs
@@ -232,7 +232,9 @@ impl State {
.get(i)
.cloned()
.unwrap_or(value_per_output),
- script_pubkey: if template.p2tr {
+ script_pubkey: if template.receiver.is_some() {
+ template.receiver.as_ref().unwrap().script_pubkey()
+ } else if template.p2tr {
let secp = Secp256k1::new();
let keypair = KeyPair::new(&secp, &mut rand::thread_rng());
let internal_key = XOnlyPublicKey::from_keypair(&keypair);
diff --git a/tests/wallet/burn.rs b/tests/wallet/burn.rs
index 6f491bd320..9cfef4b04a 100644
--- a/tests/wallet/burn.rs
+++ b/tests/wallet/burn.rs
@@ -50,6 +50,95 @@ fn inscriptions_can_be_burned() {
);
}
+#[test]
+fn runes_cannot_be_burned() {
+ let core = mockcore::builder().network(Network::Regtest).build();
+
+ let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[""]);
+
+ create_wallet(&core, &ord);
+
+ etch(&core, &ord, Rune(RUNE));
+ let rune_id = RuneId { block: 7, tx: 1 };
+
+ CommandBuilder::new(format!("--regtest wallet burn --fee-rate 1 {rune_id}",))
+ .core(&core)
+ .ord(&ord)
+ .stderr_regex(r"error: invalid value '7:1' for '.*")
+ .expected_exit_code(2)
+ .run_and_extract_stdout();
+}
+
+#[test]
+fn runic_outputs_are_protected() {
+ let core = mockcore::builder().network(Network::Regtest).build();
+
+ let ord = TestServer::spawn_with_server_args(&core, &["--regtest", "--index-runes"], &[""]);
+
+ create_wallet(&core, &ord);
+
+ let (inscription, _) = inscribe_with_custom_postage(&core, &ord, Some(1000));
+ let height = core.height();
+
+ let rune = Rune(RUNE);
+ etch(&core, &ord, rune);
+
+ let address = CommandBuilder::new("--regtest wallet receive")
+ .core(&core)
+ .ord(&ord)
+ .run_and_deserialize_output::()
+ .addresses
+ .into_iter()
+ .next()
+ .unwrap();
+
+ CommandBuilder::new(format!(
+ "--regtest --index-runes wallet send --fee-rate 1 {} 1000:{} --postage 1000sat",
+ address.clone().require_network(Network::Regtest).unwrap(),
+ Rune(RUNE)
+ ))
+ .core(&core)
+ .ord(&ord)
+ .run_and_deserialize_output::();
+
+ core.mine_blocks(2);
+
+ let txid = core.broadcast_tx(TransactionTemplate {
+ inputs: &[
+ // send rune and inscription to the same output
+ (height as usize, 2, 0, Witness::new()),
+ ((core.height() - 1) as usize, 1, 0, Witness::new()),
+ // fees
+ (core.height() as usize, 0, 0, Witness::new()),
+ ],
+ outputs: 2,
+ output_values: &[2000, 50 * COIN_VALUE],
+ receiver: Some(address.require_network(Network::Regtest).unwrap()),
+ ..default()
+ });
+
+ core.mine_blocks(1);
+
+ ord.assert_response_regex(
+ format!("/output/{}:0", txid),
+ format!(r".*.*.*", inscription),
+ );
+
+ ord.assert_response_regex(
+ format!("/output/{}:0", txid),
+ format!(r".*{rune}.*"),
+ );
+
+ CommandBuilder::new(format!(
+ "--regtest --index-runes wallet burn --fee-rate 1 {inscription}",
+ ))
+ .core(&core)
+ .ord(&ord)
+ .expected_stderr("error: runic outpoints may not be burned\n")
+ .expected_exit_code(1)
+ .run_and_extract_stdout();
+}
+
#[test]
fn inscriptions_on_large_utxos_are_protected() {
let core = mockcore::spawn();
@@ -62,17 +151,70 @@ fn inscriptions_on_large_utxos_are_protected() {
let (inscription, _) = inscribe_with_custom_postage(&core, &ord, Some(10_001));
- core.mine_blocks(1);
-
CommandBuilder::new(format!("wallet burn --fee-rate 1 {inscription}",))
.core(&core)
.ord(&ord)
- .stdout_regex(r".*")
.expected_stderr("error: The amount of sats where the inscription is on exceeds 10000\n")
.expected_exit_code(1)
.run_and_extract_stdout();
}
+#[test]
+fn multiple_inscriptions_on_same_utxo_are_protected() {
+ let core = mockcore::builder().network(Network::Regtest).build();
+
+ let ord = TestServer::spawn_with_server_args(&core, &["--regtest"], &[]);
+
+ create_wallet(&core, &ord);
+
+ let address = CommandBuilder::new("--regtest wallet receive")
+ .core(&core)
+ .ord(&ord)
+ .run_and_deserialize_output::()
+ .addresses
+ .into_iter()
+ .next()
+ .unwrap();
+
+ let (inscription0, _) = inscribe_with_custom_postage(&core, &ord, Some(1000));
+ let height0 = core.height();
+ let (inscription1, _) = inscribe_with_custom_postage(&core, &ord, Some(1000));
+ let height1 = core.height();
+ let (inscription2, _) = inscribe_with_custom_postage(&core, &ord, Some(1000));
+ let height2 = core.height();
+
+ let txid = core.broadcast_tx(TransactionTemplate {
+ inputs: &[
+ // send all 3 inscriptions on a single output
+ (height0 as usize, 2, 0, Witness::new()),
+ (height1 as usize, 2, 0, Witness::new()),
+ (height2 as usize, 2, 0, Witness::new()),
+ // fees
+ (core.height() as usize, 0, 0, Witness::new()),
+ ],
+ outputs: 2,
+ output_values: &[3000, 50 * COIN_VALUE],
+ receiver: Some(address.require_network(Network::Regtest).unwrap()),
+ ..default()
+ });
+
+ core.mine_blocks(1);
+
+ ord.assert_response_regex(
+ format!("/output/{}:0", txid),
+ format!(r".*.*.*.*.*.*.*", inscription0, inscription1, inscription2),
+ );
+
+ CommandBuilder::new(format!("--regtest wallet burn --fee-rate 1 {inscription0}",))
+ .core(&core)
+ .ord(&ord)
+ .expected_stderr(format!(
+ "error: cannot send {txid}:0:0 without also sending inscription {inscription2} at {txid}:0:2000\n"
+ ))
+ .expected_exit_code(1)
+ .run_and_extract_stdout();
+}
+
#[test]
fn large_postage_is_protected() {
let core = mockcore::spawn();
@@ -92,7 +234,6 @@ fn large_postage_is_protected() {
))
.core(&core)
.ord(&ord)
- .stdout_regex(r".*")
.expected_stderr("error: Target postage exceeds 10000\n")
.expected_exit_code(1)
.run_and_extract_stdout();