Skip to content

Commit

Permalink
Allow including metadata when burning inscriptions (ordinals#4045)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey authored Nov 9, 2024
1 parent 11adf7b commit c01279c
Show file tree
Hide file tree
Showing 19 changed files with 432 additions and 75 deletions.
2 changes: 1 addition & 1 deletion crates/ordinals/src/charm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ impl Charm {
Self::Burned,
];

fn flag(self) -> u16 {
pub fn flag(self) -> u16 {
1 << self as u16
}

Expand Down
24 changes: 24 additions & 0 deletions docs/src/inscriptions/burning.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ pub mod wallet;
type Result<T = (), E = Error> = std::result::Result<T, E>;
type SnafuResult<T = (), E = SnafuError> = std::result::Result<T, E>;

const MAX_STANDARD_OP_RETURN_SIZE: usize = 83;
const TARGET_POSTAGE: Amount = Amount::from_sat(10_000);

static SHUTTING_DOWN: AtomicBool = AtomicBool::new(false);
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/subcommand/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ use {
},
};

pub(crate) use server_config::ServerConfig;
pub use server_config::ServerConfig;

mod accept_encoding;
mod accept_json;
Expand Down
16 changes: 8 additions & 8 deletions src/subcommand/server/server_config.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use {super::*, axum::http::HeaderName};

#[derive(Default)]
pub(crate) struct ServerConfig {
pub(crate) chain: Chain,
pub(crate) csp_origin: Option<String>,
pub(crate) decompress: bool,
pub(crate) domain: Option<String>,
pub(crate) index_sats: bool,
pub(crate) json_api_enabled: bool,
pub(crate) proxy: Option<Url>,
pub struct ServerConfig {
pub chain: Chain,
pub csp_origin: Option<String>,
pub decompress: bool,
pub domain: Option<String>,
pub index_sats: bool,
pub json_api_enabled: bool,
pub proxy: Option<Url>,
}

impl ServerConfig {
Expand Down
22 changes: 22 additions & 0 deletions src/subcommand/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,26 @@ impl WalletCommand {
Subcommand::Transactions(transactions) => transactions.run(wallet),
}
}

fn parse_metadata(cbor: Option<PathBuf>, json: Option<PathBuf>) -> Result<Option<Vec<u8>>> {
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!(),
}
}
}
56 changes: 51 additions & 5 deletions src/subcommand/wallet/burn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,32 @@ use {super::*, bitcoin::opcodes};

#[derive(Debug, Parser)]
pub struct Burn {
#[arg(
long,
conflicts_with = "json_metadata",
help = "Include CBOR from <PATH> in OP_RETURN.",
value_name = "PATH"
)]
cbor_metadata: Option<PathBuf>,
#[arg(long, help = "Don't sign or broadcast transaction.")]
dry_run: bool,
#[arg(long, help = "Use fee rate of <FEE_RATE> sats/vB.")]
fee_rate: FeeRate,
#[arg(
long,
help = "Include JSON from <PATH> converted to CBOR in OP_RETURN.",
conflicts_with = "cbor_metadata",
value_name = "PATH"
)]
json_metadata: Option<PathBuf>,
#[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 <AMOUNT> postage with sent inscriptions. [default: 10000 sat]",
Expand All @@ -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}",
}

Expand All @@ -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) =
Expand All @@ -60,6 +109,7 @@ impl Burn {
satpoint: SatPoint,
postage: Option<Amount>,
fee_rate: FeeRate,
script_pubkey: ScriptBuf,
) -> Result<Transaction> {
let runic_outputs = wallet.get_runic_outputs()?;

Expand All @@ -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,
Expand Down
21 changes: 1 addition & 20 deletions src/subcommand/wallet/inscribe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -100,25 +100,6 @@ impl Inscribe {
&wallet,
)
}

fn parse_metadata(cbor: Option<PathBuf>, json: Option<PathBuf>) -> Result<Option<Vec<u8>>> {
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)]
Expand Down
7 changes: 4 additions & 3 deletions src/subcommand/wallet/mint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 0 additions & 2 deletions src/subcommand/wallet/split.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ use {super::*, splitfile::Splitfile};

mod splitfile;

const MAX_STANDARD_OP_RETURN_SIZE: usize = 83;

#[derive(Debug, PartialEq)]
enum Error {
DustOutput {
Expand Down
11 changes: 5 additions & 6 deletions src/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ pub(crate) use {
home::HomeHtml,
iframe::Iframe,
input::InputHtml,
inscription::InscriptionHtml,
inscriptions::InscriptionsHtml,
inscriptions_block::InscriptionsBlockHtml,
metadata::MetadataHtml,
Expand All @@ -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;
Expand All @@ -53,7 +52,7 @@ pub mod status;
pub mod transaction;

#[derive(Boilerplate)]
pub(crate) struct PageHtml<T: PageContent> {
pub struct PageHtml<T: PageContent> {
content: T,
config: Arc<ServerConfig>,
}
Expand All @@ -62,7 +61,7 @@ impl<T> PageHtml<T>
where
T: PageContent,
{
pub(crate) fn new(content: T, config: Arc<ServerConfig>) -> Self {
pub fn new(content: T, config: Arc<ServerConfig>) -> Self {
Self { content, config }
}

Expand All @@ -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<ServerConfig>) -> PageHtml<Self>
Expand Down
Loading

0 comments on commit c01279c

Please sign in to comment.