From c01279c654e2813a7ab96190183847dfc60da432 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 8 Nov 2024 20:38:31 -0800 Subject: [PATCH] Allow including metadata when burning inscriptions (#4045) --- crates/ordinals/src/charm.rs | 2 +- docs/src/inscriptions/burning.md | 24 ++++ src/lib.rs | 3 +- src/subcommand.rs | 2 +- src/subcommand/server.rs | 2 +- src/subcommand/server/server_config.rs | 16 +-- src/subcommand/wallet.rs | 22 +++ src/subcommand/wallet/burn.rs | 56 +++++++- src/subcommand/wallet/inscribe.rs | 21 +-- src/subcommand/wallet/mint.rs | 7 +- src/subcommand/wallet/split.rs | 2 - src/templates.rs | 11 +- src/templates/inscription.rs | 94 +++++++++--- src/wallet/batch/plan.rs | 7 +- templates/inscription.html | 10 +- tests/lib.rs | 6 +- tests/test_server.rs | 31 ++++ tests/wallet/batch_command.rs | 2 +- tests/wallet/burn.rs | 189 +++++++++++++++++++++++++ 19 files changed, 432 insertions(+), 75 deletions(-) create mode 100644 docs/src/inscriptions/burning.md diff --git a/crates/ordinals/src/charm.rs b/crates/ordinals/src/charm.rs index 53e4aae770..38d86de35f 100644 --- a/crates/ordinals/src/charm.rs +++ b/crates/ordinals/src/charm.rs @@ -34,7 +34,7 @@ impl Charm { Self::Burned, ]; - fn flag(self) -> u16 { + pub fn flag(self) -> u16 { 1 << self as u16 } diff --git a/docs/src/inscriptions/burning.md b/docs/src/inscriptions/burning.md new file mode 100644 index 0000000000..e45edb1916 --- /dev/null +++ b/docs/src/inscriptions/burning.md @@ -0,0 +1,24 @@ +Burning +======= + +Inscriptions may be burned by constructing a transaction that spends them to a +script pubkey beginning with `OP_RETURN`. + +Sending inscriptions to a so-called "burn address" is not recognized by `ord`. + +Burned inscriptions receive the "burned" charm, recognized with 🔥 on the +inscription's `/inscription` page. + +When burning inscriptions, CBOR metadata may be included in a data push +immediately following the `OP_RETURN`. + +Burn metadata is unstructured, having no meaning to the underlying protocol, +and should be human readable. It is displayed on the burned inscription's +`/inscription` page, in the same manner as inscription metadata, under the +heading "burn metadata". + +Use it, if you feel like it, to commemorate the inscription, celebrate the +closing of a collection, or for whatever other purposes you so desire. + +Data pushes after the first are currently ignored by `ord`. However, they may +be given future meaning by the protocol, and should not be used. diff --git a/src/lib.rs b/src/lib.rs index 60bc129e51..acc628e8d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,6 +133,7 @@ pub mod wallet; type Result = std::result::Result; type SnafuResult = std::result::Result; +const MAX_STANDARD_OP_RETURN_SIZE: usize = 83; const TARGET_POSTAGE: Amount = Amount::from_sat(10_000); static SHUTTING_DOWN: AtomicBool = AtomicBool::new(false); @@ -196,7 +197,7 @@ fn target_as_block_hash(target: bitcoin::Target) -> BlockHash { BlockHash::from_raw_hash(Hash::from_byte_array(target.to_le_bytes())) } -fn unbound_outpoint() -> OutPoint { +pub fn unbound_outpoint() -> OutPoint { OutPoint { txid: Hash::all_zeros(), vout: 0, diff --git a/src/subcommand.rs b/src/subcommand.rs index 3b607ceee3..d3213303a7 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -9,7 +9,7 @@ pub mod index; pub mod list; pub mod parse; pub mod runes; -pub(crate) mod server; +pub mod server; mod settings; pub mod subsidy; pub mod supply; diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 15cfc4ab9a..8f44de2284 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -39,7 +39,7 @@ use { }, }; -pub(crate) use server_config::ServerConfig; +pub use server_config::ServerConfig; mod accept_encoding; mod accept_json; diff --git a/src/subcommand/server/server_config.rs b/src/subcommand/server/server_config.rs index 8a426888ba..b0406027b5 100644 --- a/src/subcommand/server/server_config.rs +++ b/src/subcommand/server/server_config.rs @@ -1,14 +1,14 @@ use {super::*, axum::http::HeaderName}; #[derive(Default)] -pub(crate) struct ServerConfig { - pub(crate) chain: Chain, - pub(crate) csp_origin: Option, - pub(crate) decompress: bool, - pub(crate) domain: Option, - pub(crate) index_sats: bool, - pub(crate) json_api_enabled: bool, - pub(crate) proxy: Option, +pub struct ServerConfig { + pub chain: Chain, + pub csp_origin: Option, + pub decompress: bool, + pub domain: Option, + pub index_sats: bool, + pub json_api_enabled: bool, + pub proxy: Option, } impl ServerConfig { diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index 8e38038d06..be2c459f1c 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -138,4 +138,26 @@ impl WalletCommand { Subcommand::Transactions(transactions) => transactions.run(wallet), } } + + fn parse_metadata(cbor: Option, json: Option) -> Result>> { + match (cbor, json) { + (None, None) => Ok(None), + (Some(path), None) => { + let cbor = fs::read(path)?; + let _value: Value = ciborium::from_reader(Cursor::new(cbor.clone())) + .context("failed to parse CBOR metadata")?; + + Ok(Some(cbor)) + } + (None, Some(path)) => { + let value: serde_json::Value = + serde_json::from_reader(File::open(path)?).context("failed to parse JSON metadata")?; + let mut cbor = Vec::new(); + ciborium::into_writer(&value, &mut cbor)?; + + Ok(Some(cbor)) + } + (Some(_), Some(_)) => panic!(), + } + } } diff --git a/src/subcommand/wallet/burn.rs b/src/subcommand/wallet/burn.rs index 9c05c752eb..4312987e1d 100644 --- a/src/subcommand/wallet/burn.rs +++ b/src/subcommand/wallet/burn.rs @@ -2,10 +2,32 @@ use {super::*, bitcoin::opcodes}; #[derive(Debug, Parser)] pub struct Burn { + #[arg( + long, + conflicts_with = "json_metadata", + help = "Include CBOR from in OP_RETURN.", + value_name = "PATH" + )] + cbor_metadata: Option, #[arg(long, help = "Don't sign or broadcast transaction.")] dry_run: bool, #[arg(long, help = "Use fee rate of sats/vB.")] fee_rate: FeeRate, + #[arg( + long, + help = "Include JSON from converted to CBOR in OP_RETURN.", + conflicts_with = "cbor_metadata", + value_name = "PATH" + )] + json_metadata: Option, + #[arg( + long, + alias = "nolimit", + help = "Allow OP_RETURN greater than 83 bytes. Transactions over this limit are nonstandard \ + and will not be relayed by bitcoind in its default configuration. Do not use this flag unless \ + you understand the implications." + )] + no_limit: bool, #[arg( long, help = "Target postage with sent inscriptions. [default: 10000 sat]", @@ -23,12 +45,16 @@ impl Burn { .ok_or_else(|| anyhow!("inscription {} not found", self.inscription))? .clone(); + let metadata = WalletCommand::parse_metadata(self.cbor_metadata, self.json_metadata)?; + let Some(value) = inscription_info.value else { bail!("Cannot burn unbound inscription"); }; + let value = Amount::from_sat(value); + ensure! { - value <= TARGET_POSTAGE.to_sat(), + value <= TARGET_POSTAGE, "Cannot burn inscription contained in UTXO exceeding {TARGET_POSTAGE}", } @@ -37,11 +63,34 @@ impl Burn { "Postage may not exceed {TARGET_POSTAGE}", } + 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 + ) + })?; + builder = builder.push_slice(push); + } + + let script_pubkey = builder.into_script(); + + ensure!( + self.no_limit || script_pubkey.len() <= MAX_STANDARD_OP_RETURN_SIZE, + "OP_RETURN with metadata larger than maximum: {} > {}", + script_pubkey.len(), + MAX_STANDARD_OP_RETURN_SIZE, + ); + let unsigned_transaction = Self::create_unsigned_burn_transaction( &wallet, inscription_info.satpoint, self.postage, self.fee_rate, + script_pubkey, )?; let (txid, psbt, fee) = @@ -60,6 +109,7 @@ impl Burn { satpoint: SatPoint, postage: Option, fee_rate: FeeRate, + script_pubkey: ScriptBuf, ) -> Result { let runic_outputs = wallet.get_runic_outputs()?; @@ -72,10 +122,6 @@ impl Burn { let postage = postage.map(Target::ExactPostage).unwrap_or(Target::Postage); - let script_pubkey = script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .into_script(); - Ok( TransactionBuilder::new( satpoint, diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index f02643d361..c71c652318 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -72,7 +72,7 @@ impl Inscribe { chain, self.shared.compress, self.delegate, - Inscribe::parse_metadata(self.cbor_metadata, self.json_metadata)?, + WalletCommand::parse_metadata(self.cbor_metadata, self.json_metadata)?, self.metaprotocol, self.parent.into_iter().collect(), self.file, @@ -100,25 +100,6 @@ impl Inscribe { &wallet, ) } - - fn parse_metadata(cbor: Option, json: Option) -> Result>> { - if let Some(path) = cbor { - let cbor = fs::read(path)?; - let _value: Value = ciborium::from_reader(Cursor::new(cbor.clone())) - .context("failed to parse CBOR metadata")?; - - Ok(Some(cbor)) - } else if let Some(path) = json { - let value: serde_json::Value = - serde_json::from_reader(File::open(path)?).context("failed to parse JSON metadata")?; - let mut cbor = Vec::new(); - ciborium::into_writer(&value, &mut cbor)?; - - Ok(Some(cbor)) - } else { - Ok(None) - } - } } #[cfg(test)] diff --git a/src/subcommand/wallet/mint.rs b/src/subcommand/wallet/mint.rs index 99fb51f3fe..2715dda891 100644 --- a/src/subcommand/wallet/mint.rs +++ b/src/subcommand/wallet/mint.rs @@ -66,9 +66,10 @@ impl Mint { let script_pubkey = runestone.encipher(); ensure!( - script_pubkey.len() <= 82, - "runestone greater than maximum OP_RETURN size: {} > 82", - script_pubkey.len() + script_pubkey.len() <= MAX_STANDARD_OP_RETURN_SIZE, + "runestone greater than maximum OP_RETURN size: {} > {}", + script_pubkey.len(), + MAX_STANDARD_OP_RETURN_SIZE, ); let unfunded_transaction = Transaction { diff --git a/src/subcommand/wallet/split.rs b/src/subcommand/wallet/split.rs index 333a909ec1..92348a155f 100644 --- a/src/subcommand/wallet/split.rs +++ b/src/subcommand/wallet/split.rs @@ -2,8 +2,6 @@ use {super::*, splitfile::Splitfile}; mod splitfile; -const MAX_STANDARD_OP_RETURN_SIZE: usize = 83; - #[derive(Debug, PartialEq)] enum Error { DustOutput { diff --git a/src/templates.rs b/src/templates.rs index 29aec83d71..7898d2a5f8 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -10,7 +10,6 @@ pub(crate) use { home::HomeHtml, iframe::Iframe, input::InputHtml, - inscription::InscriptionHtml, inscriptions::InscriptionsHtml, inscriptions_block::InscriptionsBlockHtml, metadata::MetadataHtml, @@ -25,8 +24,8 @@ pub(crate) use { }; pub use { - blocks::BlocksHtml, rune::RuneHtml, runes::RunesHtml, status::StatusHtml, - transaction::TransactionHtml, + blocks::BlocksHtml, inscription::InscriptionHtml, rune::RuneHtml, runes::RunesHtml, + status::StatusHtml, transaction::TransactionHtml, }; pub mod address; @@ -53,7 +52,7 @@ pub mod status; pub mod transaction; #[derive(Boilerplate)] -pub(crate) struct PageHtml { +pub struct PageHtml { content: T, config: Arc, } @@ -62,7 +61,7 @@ impl PageHtml where T: PageContent, { - pub(crate) fn new(content: T, config: Arc) -> Self { + pub fn new(content: T, config: Arc) -> Self { Self { content, config } } @@ -83,7 +82,7 @@ where } } -pub(crate) trait PageContent: Display + 'static { +pub trait PageContent: Display + 'static { fn title(&self) -> String; fn page(self, server_config: Arc) -> PageHtml diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index e5e5c1bda0..2cfe83d836 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -1,24 +1,24 @@ use super::*; #[derive(Boilerplate, Default)] -pub(crate) struct InscriptionHtml { - pub(crate) chain: Chain, - pub(crate) charms: u16, - pub(crate) child_count: u64, - pub(crate) children: Vec, - pub(crate) fee: u64, - pub(crate) height: u32, - pub(crate) inscription: Inscription, - pub(crate) id: InscriptionId, - pub(crate) number: i32, - pub(crate) next: Option, - pub(crate) output: Option, - pub(crate) parents: Vec, - pub(crate) previous: Option, - pub(crate) rune: Option, - pub(crate) sat: Option, - pub(crate) satpoint: SatPoint, - pub(crate) timestamp: DateTime, +pub struct InscriptionHtml { + pub chain: Chain, + pub charms: u16, + pub child_count: u64, + pub children: Vec, + pub fee: u64, + pub height: u32, + pub inscription: Inscription, + pub id: InscriptionId, + pub number: i32, + pub next: Option, + pub output: Option, + pub parents: Vec, + pub previous: Option, + pub rune: Option, + pub sat: Option, + pub satpoint: SatPoint, + pub timestamp: DateTime, } impl PageContent for InscriptionHtml { @@ -27,6 +27,23 @@ impl PageContent for InscriptionHtml { } } +impl InscriptionHtml { + pub fn burn_metadata(&self) -> Option { + let script_pubkey = &self.output.as_ref()?.script_pubkey; + + if !script_pubkey.is_op_return() { + return None; + } + + let script::Instruction::PushBytes(metadata) = script_pubkey.instructions().nth(1)?.ok()? + else { + return None; + }; + + ciborium::from_reader(Cursor::new(metadata)).ok() + } +} + #[cfg(test)] mod tests { use super::*; @@ -445,4 +462,45 @@ mod tests { .unindent() ); } + + #[test] + fn with_burn_metadata() { + let script_pubkey = script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_slice([ + 0xA2, 0x63, b'f', b'o', b'o', 0x63, b'b', b'a', b'r', 0x63, b'b', b'a', b'z', 0x01, + ]) + .into_script(); + + assert_regex_match!( + InscriptionHtml { + fee: 1, + inscription: Inscription { + content_encoding: Some("br".into()), + ..inscription("text/plain;charset=utf-8", "HELLOWORLD") + }, + id: inscription_id(1), + number: 1, + satpoint: satpoint(1, 0), + output: Some(TxOut { + value: Amount::from_sat(1), + script_pubkey, + }), + ..default() + }, + " +

Inscription 1

+ .* +
+ .* +
burn metadata
+
+
foo
bar
baz
1
+
+ .* +
+ " + .unindent() + ); + } } diff --git a/src/wallet/batch/plan.rs b/src/wallet/batch/plan.rs index 9f844ca845..7639903fbf 100644 --- a/src/wallet/batch/plan.rs +++ b/src/wallet/batch/plan.rs @@ -483,9 +483,10 @@ impl Plan { runestone = Some(inner); ensure!( - self.no_limit || script_pubkey.len() <= 82, - "runestone greater than maximum OP_RETURN size: {} > 82", - script_pubkey.len() + self.no_limit || script_pubkey.len() <= MAX_STANDARD_OP_RETURN_SIZE, + "runestone greater than maximum OP_RETURN size: {} > {}", + script_pubkey.len(), + MAX_STANDARD_OP_RETURN_SIZE, ); reveal_outputs.push(TxOut { diff --git a/templates/inscription.html b/templates/inscription.html index 9a907055e9..fa50f66e4b 100644 --- a/templates/inscription.html +++ b/templates/inscription.html @@ -61,11 +61,17 @@

Inscription {{ self.number }}

{{ Trusted(MetadataHtml(&metadata)) }} %% } +%% if let Some(burn_metadata) = self.burn_metadata() { +
burn metadata
+
+ {{ Trusted(MetadataHtml(&burn_metadata)) }} +
+%% } %% if let Some(output) = &self.output { -%% if let Ok(address) = self.chain.address_from_script(&output.script_pubkey ) { +%% if let Ok(address) = self.chain.address_from_script(&output.script_pubkey ) {
address
{{ address }}
-%% } +%% }
value
{{ output.value.to_sat() }}
%% } diff --git a/tests/lib.rs b/tests/lib.rs index b9f4f5dd09..cedafcaf2e 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -4,14 +4,14 @@ use { self::{command_builder::CommandBuilder, expected::Expected, test_server::TestServer}, bitcoin::{ address::{Address, NetworkUnchecked}, - Amount, Network, OutPoint, Sequence, Txid, Witness, + opcodes, script, Amount, Network, OutPoint, Sequence, TxOut, Txid, Witness, }, chrono::{DateTime, Utc}, executable_path::executable_path, mockcore::TransactionTemplate, ord::{ - api, chain::Chain, outgoing::Outgoing, subcommand::runes::RuneInfo, wallet::batch, - wallet::ListDescriptorsResult, InscriptionId, RuneEntry, + api, chain::Chain, outgoing::Outgoing, subcommand::runes::RuneInfo, templates::InscriptionHtml, + wallet::batch, wallet::ListDescriptorsResult, Inscription, InscriptionId, RuneEntry, }, ordinals::{ Artifact, Charm, Edict, Pile, Rarity, Rune, RuneId, Runestone, Sat, SatPoint, SpacedRune, diff --git a/tests/test_server.rs b/tests/test_server.rs index e351418f9c..57bdce2a8b 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -4,6 +4,7 @@ use { bitcoincore_rpc::{Auth, Client, RpcApi}, ord::{parse_ord_server_args, Index}, reqwest::blocking::Response, + sysinfo::System, }; pub(crate) struct TestServer { @@ -108,6 +109,36 @@ impl TestServer { pretty_assert_eq!(response.text().unwrap(), expected_response); } + #[track_caller] + pub(crate) fn assert_html( + &self, + path: impl AsRef, + chain: Chain, + content: impl ord::templates::PageContent, + ) { + self.sync_server(); + let response = reqwest::blocking::get(self.url().join(path.as_ref()).unwrap()).unwrap(); + + assert_eq!( + response.status(), + StatusCode::OK, + "{}", + response.text().unwrap() + ); + + let expected_response = ord::templates::PageHtml::new( + content, + Arc::new(ord::subcommand::server::ServerConfig { + chain, + domain: Some(System::host_name().unwrap()), + ..Default::default() + }), + ) + .to_string(); + + pretty_assert_eq!(response.text().unwrap(), expected_response); + } + pub(crate) fn request(&self, path: impl AsRef) -> Response { self.sync_server(); diff --git a/tests/wallet/batch_command.rs b/tests/wallet/batch_command.rs index 57e16f2cbd..38904db6d9 100644 --- a/tests/wallet/batch_command.rs +++ b/tests/wallet/batch_command.rs @@ -2616,7 +2616,7 @@ fn oversize_runestone_error() { ) .core(&core) .ord(&ord) - .expected_stderr("error: runestone greater than maximum OP_RETURN size: 104 > 82\n") + .expected_stderr("error: runestone greater than maximum OP_RETURN size: 104 > 83\n") .expected_exit_code(1) .run_and_extract_stdout(); } diff --git a/tests/wallet/burn.rs b/tests/wallet/burn.rs index 140a27dc21..07f355d3e6 100644 --- a/tests/wallet/burn.rs +++ b/tests/wallet/burn.rs @@ -32,6 +32,8 @@ fn inscriptions_can_be_burned() {
🔥
+
value
+
9922
.*
content length
3 bytes
@@ -212,3 +214,190 @@ fn cannot_burn_with_excess_postage() { .expected_exit_code(1) .run_and_extract_stdout(); } + +#[test] +fn json_metadata_can_be_included_when_burning() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let (inscription, _) = inscribe(&core, &ord); + + core.mine_blocks(1); + + let output = CommandBuilder::new(format!( + "wallet burn --fee-rate 1 {inscription} --json-metadata metadata.json" + )) + .core(&core) + .ord(&ord) + .write("metadata.json", r#"{"foo": "bar", "baz": 1}"#) + .stdout_regex(r".*") + .run_and_deserialize_output::(); + + let txid = core.mempool()[0].compute_txid(); + assert_eq!(txid, output.txid); + + core.mine_blocks(1); + + let script_pubkey = script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_slice([ + 0xA2, 0x63, b'f', b'o', b'o', 0x63, b'b', b'a', b'r', 0x63, b'b', b'a', b'z', 0x01, + ]) + .into_script(); + + ord.assert_html( + format!("/inscription/{inscription}"), + Chain::Mainnet, + InscriptionHtml { + charms: Charm::Burned.flag(), + fee: 138, + id: inscription, + output: Some(TxOut { + value: Amount::from_sat(9907), + script_pubkey, + }), + height: 3, + inscription: Inscription { + content_type: Some("text/plain;charset=utf-8".as_bytes().into()), + body: Some("foo".as_bytes().into()), + ..default() + }, + satpoint: SatPoint { + outpoint: OutPoint { + txid: output.txid, + vout: 0, + }, + offset: 0, + }, + timestamp: "1970-01-01 00:00:03+00:00" + .parse::>() + .unwrap(), + ..default() + }, + ); +} + +#[test] +fn cbor_metadata_can_be_included_when_burning() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let (inscription, _) = inscribe(&core, &ord); + + core.mine_blocks(1); + + let metadata = [ + 0xA2, 0x63, b'f', b'o', b'o', 0x63, b'b', b'a', b'r', 0x63, b'b', b'a', b'z', 0x01, + ]; + + let output = CommandBuilder::new(format!( + "wallet burn --fee-rate 1 {inscription} --cbor-metadata metadata.cbor" + )) + .core(&core) + .ord(&ord) + .write("metadata.cbor", metadata) + .stdout_regex(r".*") + .run_and_deserialize_output::(); + + let txid = core.mempool()[0].compute_txid(); + assert_eq!(txid, output.txid); + + core.mine_blocks(1); + + let script_pubkey = script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_slice(metadata) + .into_script(); + + ord.assert_html( + format!("/inscription/{inscription}"), + Chain::Mainnet, + InscriptionHtml { + charms: Charm::Burned.flag(), + fee: 138, + id: inscription, + output: Some(TxOut { + value: Amount::from_sat(9907), + script_pubkey, + }), + height: 3, + inscription: Inscription { + content_type: Some("text/plain;charset=utf-8".as_bytes().into()), + body: Some("foo".as_bytes().into()), + ..default() + }, + satpoint: SatPoint { + outpoint: OutPoint { + txid: output.txid, + vout: 0, + }, + offset: 0, + }, + timestamp: "1970-01-01 00:00:03+00:00" + .parse::>() + .unwrap(), + ..default() + }, + ); +} + +#[test] +fn cbor_and_json_metadata_flags_conflict() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let (inscription, _) = inscribe(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new(format!( + "wallet burn --fee-rate 1 {inscription} --cbor-metadata foo --json-metadata bar" + )) + .core(&core) + .ord(&ord) + .stderr_regex( + "error: the argument '--cbor-metadata ' cannot be used with '--json-metadata '.*", + ) + .expected_exit_code(2) + .run_and_extract_stdout(); +} + +#[test] +fn oversize_metadata_requires_no_limit_flag() { + let core = mockcore::spawn(); + + let ord = TestServer::spawn_with_server_args(&core, &[], &[]); + + create_wallet(&core, &ord); + + core.mine_blocks(1); + + let (inscription, _) = inscribe(&core, &ord); + + core.mine_blocks(1); + + CommandBuilder::new(format!( + "wallet burn --fee-rate 1 {inscription} --json-metadata metadata.json" + )) + .core(&core) + .ord(&ord) + .write("metadata.json", format!("\"{}\"", "0".repeat(79))) + .stderr_regex("error: OP_RETURN with metadata larger than maximum: 84 > 83\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +}