Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable rune burning in wallet #4117

Merged
merged 15 commits into from
Dec 12, 2024
144 changes: 86 additions & 58 deletions src/subcommand/wallet/burn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,68 +28,96 @@ pub struct Burn {
you understand the implications."
)]
no_limit: bool,
inscription: InscriptionId,
asset: Outgoing,
}

impl Burn {
pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult {
let inscription_info = wallet
.inscription_info()
.get(&self.inscription)
.ok_or_else(|| anyhow!("inscription {} not found", self.inscription))?
.clone();

let metadata = WalletCommand::parse_metadata(self.cbor_metadata, self.json_metadata)?;

ensure!(
inscription_info.value.is_some(),
"Cannot burn unbound inscription"
);

let mut builder = script::Builder::new().push_opcode(opcodes::all::OP_RETURN);

// 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);
}

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 burn_amount = Amount::from_sat(1);

let unsigned_transaction = Self::create_unsigned_burn_transaction(
&wallet,
inscription_info.satpoint,
self.fee_rate,
script_pubkey,
burn_amount,
)?;
let (unsigned_transaction, burn_amount) = match self.asset {
Outgoing::InscriptionId(id) => {
let inscription_info = wallet
.inscription_info()
.get(&id)
.ok_or_else(|| anyhow!("inscription {id} not found"))?
.clone();

let metadata = WalletCommand::parse_metadata(self.cbor_metadata, self.json_metadata)?;

ensure!(
inscription_info.value.is_some(),
"Cannot burn unbound inscription"
);

let mut builder = script::Builder::new().push_opcode(opcodes::all::OP_RETURN);

// 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);
}

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 burn_amount = Amount::from_sat(1);

(
Self::create_unsigned_burn_satpoint_transaction(
&wallet,
inscription_info.satpoint,
self.fee_rate,
script_pubkey,
burn_amount,
)?,
burn_amount,
)
}
Outgoing::Rune { decimal, rune } => {
ensure!(
self.cbor_metadata.is_none() && self.json_metadata.is_none(),
"metadata not supported when burning runes"
);

(
wallet.create_unsigned_send_or_burn_runes_transaction(
None,
rune,
decimal,
None,
self.fee_rate,
)?,
Amount::ZERO,
)
}
Outgoing::Amount(_) => bail!("burning bitcoin not supported"),
Outgoing::Sat(_) => bail!("burning sat not supported"),
Outgoing::SatPoint(_) => bail!("burning satpoint not supported"),
};

let base_size = unsigned_transaction.base_size();

assert!(
base_size >= 65,
"transaction base size less than minimum standard tx nonwitness size: {base_size} < 65",
Expand All @@ -104,12 +132,12 @@ impl Burn {
Ok(Some(Box::new(send::Output {
txid,
psbt,
asset: Outgoing::InscriptionId(self.inscription),
asset: self.asset,
fee,
})))
}

fn create_unsigned_burn_transaction(
fn create_unsigned_burn_satpoint_transaction(
wallet: &Wallet,
satpoint: SatPoint,
fee_rate: FeeRate,
Expand Down
Loading
Loading