diff --git a/.gitignore b/.gitignore index cb0a311337..7048059a32 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ /target /test-times.txt /tmp +/.fleet/ diff --git a/src/charm.rs b/src/charm.rs index b80c5c6616..485ef56cdf 100644 --- a/src/charm.rs +++ b/src/charm.rs @@ -1,5 +1,6 @@ #[derive(Copy, Clone)] pub(crate) enum Charm { + Burned, Coin, Cursed, Epic, @@ -13,7 +14,7 @@ pub(crate) enum Charm { } impl Charm { - pub(crate) const ALL: [Charm; 10] = [ + pub(crate) const ALL: [Charm; 11] = [ Self::Coin, Self::Uncommon, Self::Rare, @@ -24,6 +25,7 @@ impl Charm { Self::Cursed, Self::Unbound, Self::Lost, + Self::Burned, ]; fn flag(self) -> u16 { @@ -40,6 +42,7 @@ impl Charm { pub(crate) fn icon(self) -> &'static str { match self { + Self::Burned => "🔥", Self::Coin => "🪙", Self::Cursed => "👹", Self::Epic => "🪻", @@ -55,6 +58,7 @@ impl Charm { pub(crate) fn title(self) -> &'static str { match self { + Self::Burned => "burned", Self::Coin => "coin", Self::Cursed => "cursed", Self::Epic => "epic", diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 49a7da6f07..1cd8707b8e 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -18,6 +18,7 @@ pub(super) struct Flotsam { inscription_id: InscriptionId, offset: u64, origin: Origin, + burned: bool, } #[derive(Debug, Clone)] @@ -83,6 +84,12 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { continue; } + // if the inscription was sent to an OP_RETURN it was burned + let is_burned = tx.output + .get(input_index) + .map(|output| output.script_pubkey.is_op_return()) + .unwrap_or(false); + // find existing inscriptions on input (transfers of inscriptions) for (old_satpoint, inscription_id) in Index::inscriptions_on_output( self.satpoint_to_sequence_number, @@ -94,6 +101,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { offset, inscription_id, origin: Origin::Old { old_satpoint }, + burned: is_burned, }); inscribed_offsets @@ -189,6 +197,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { .unwrap_or(offset); floating_inscriptions.push(Flotsam { + burned: is_burned, inscription_id, offset, origin: Origin::New { @@ -367,13 +376,32 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { .satpoint_to_sequence_number .remove_all(&old_satpoint.store())?; - ( - false, - self + let sequence_number = self .id_to_sequence_number .get(&inscription_id.store())? .unwrap() - .value(), + .value(); + + if flotsam.burned { + let mut inscription_entry = InscriptionEntry::load( + self + .sequence_number_to_entry + .get(sequence_number)? + .unwrap() + .value(), + ); + + Charm::Burned.set(&mut inscription_entry.charms); + + self.sequence_number_to_entry.insert( + sequence_number, + &inscription_entry.store(), + )?; + } + + ( + false, + sequence_number ) } Origin::New { diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 4e1a028b0e..ff866a0c73 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -29,6 +29,10 @@ use { }, axum_server::Handle, brotli::Decompressor, + bitcoin::blockdata::script::Instruction::{ + Op, PushBytes + }, + bitcoin::blockdata::opcodes::all::{OP_PUSHNUM_NEG1,OP_RETURN}, rust_embed::RustEmbed, rustls_acme::{ acme::{LETS_ENCRYPT_PRODUCTION_DIRECTORY, LETS_ENCRYPT_STAGING_DIRECTORY}, @@ -1242,11 +1246,37 @@ impl Server { Charm::Lost.set(&mut charms); } + let mut is_burned = false; + let burn_payload = output.as_ref().and_then(|o| { + let mut instructions = o.script_pubkey.instructions(); + + // Check if the first instruction is OP_RETURN + if let Some(Ok(Op(OP_RETURN))) = instructions.next() { + // Check if the second instruction is OP_1NEGATE + if let Some(Ok(Op(OP_PUSHNUM_NEG1))) = instructions.next() { + is_burned = true; + // Extract the payload if it exists + instructions.filter_map(|instr| { + if let Ok(PushBytes(data)) = instr { + String::from_utf8(data.as_bytes().to_vec()).ok() + } else { + None + } + }).next() + } else { + None + } + } else { + None + } + }); + Ok(if accept_json.0 { Json(InscriptionJson { inscription_id, children, inscription_number: entry.inscription_number, + is_burned: Some(is_burned), genesis_height: entry.height, parent, genesis_fee: entry.fee, @@ -1268,10 +1298,13 @@ impl Server { previous, next, rune, + burn_payload, + charms: Some(charms), }) .into_response() } else { InscriptionHtml { + burn_payload, chain: server_config.chain, charms, children, @@ -1280,6 +1313,7 @@ impl Server { inscription, inscription_id, inscription_number: entry.inscription_number, + is_burned: Some(is_burned), next, output, parent, diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index 1923791829..efc87a22f3 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -46,7 +46,7 @@ pub(crate) enum Wallet { Restore(restore::Restore), #[command(about = "List wallet satoshis")] Sats(sats::Sats), - #[command(about = "Send sat or inscription")] + #[command(about = "Send sat or inscription (option to burn)")] Send(send::Send), #[command(about = "See wallet transactions")] Transactions(transactions::Transactions), diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index 0a618832cf..87883f27b6 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -340,6 +340,7 @@ impl Batch { change, self.commit_fee_rate, Target::Value(reveal_fee + total_postage), + chain, ) .build_transaction()?; diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 80a60c6584..8abc102668 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -1,9 +1,32 @@ -use {super::*, crate::subcommand::wallet::transaction_builder::Target, crate::wallet::Wallet}; +use bitcoin::script::Builder; +use { + super::*, + bitcoin::{ + blockdata::opcodes::all::{OP_PUSHNUM_NEG1,OP_RETURN}, + script::PushBytesBuf, + }, + crate::{ + subcommand::wallet::transaction_builder::{Target}, + wallet::Wallet, + }, +}; #[derive(Debug, Parser, Clone)] +#[clap( + group = ArgGroup::new("output") + .required(true) + .args(&["address", "burn"]), +)] pub(crate) struct Send { - address: Address, outgoing: Outgoing, + #[arg(long, conflicts_with = "burn", help = "Recipient address")] + address: Option>, + #[arg( + long, + conflicts_with = "address", + help = "Message to append when burning sats" + )] + burn: Option, #[arg(long, help = "Use fee rate of sats/vB")] fee_rate: FeeRate, #[arg( @@ -20,10 +43,7 @@ pub struct Output { impl Send { pub(crate) fn run(self, options: Options) -> SubcommandResult { - let address = self - .address - .clone() - .require_network(options.chain().network())?; + let output = self.get_output(&options)?; let index = Index::open(&options)?; index.update()?; @@ -45,14 +65,33 @@ impl Send { let satpoint = match self.outgoing { Outgoing::Amount(amount) => { + // let script = output.get_script(); // Replace with the actual method to get the Script from output + + if output.is_op_return() { + bail!("refusing to burn amount"); + } + + let address = match chain.address_from_script(output.as_script()) { + Ok(addr) => addr, + Err(e) => { + bail!("failed to get address from script: {:?}", e); + } + }; + Self::lock_non_cardinal_outputs(&client, &inscriptions, &runic_outputs, unspent_outputs)?; - let transaction = Self::send_amount(&client, amount, address, self.fee_rate)?; - return Ok(Box::new(Output { transaction })); - } + let txid = Self::send_amount(&client, amount, address, self.fee_rate)?; + return Ok(Box::new(Output { transaction: txid })); + }, Outgoing::InscriptionId(id) => index .get_inscription_satpoint_by_id(id)? .ok_or_else(|| anyhow!("inscription {id} not found"))?, Outgoing::Rune { decimal, rune } => { + let address = self + .address + .unwrap() + .clone() + .require_network(options.chain().network())?; + let transaction = Self::send_runes( address, chain, @@ -100,10 +139,11 @@ impl Send { unspent_outputs, locked_outputs, runic_outputs, - address.clone(), + output, change, self.fee_rate, postage, + chain, ) .build_transaction()?; @@ -116,6 +156,25 @@ impl Send { Ok(Box::new(Output { transaction: txid })) } + fn get_output(&self, options: &Options) -> Result { + if let Some(address) = &self.address { + let address = address.clone().require_network(options.chain().network())?; + Ok(ScriptBuf::from(address)) + } else if let Some(msg) = &self.burn { + let push_data_buf = PushBytesBuf::try_from(Vec::from(msg.clone())) + .expect("burn payload too large"); + + Ok(Builder::new() + .push_opcode(OP_RETURN) + .push_opcode(OP_PUSHNUM_NEG1) + .push_slice(&push_data_buf) + .into_script() + ) + } else { + bail!("no valid output given") + } + } + fn lock_non_cardinal_outputs( client: &Client, inscriptions: &BTreeMap, diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index 12699e7a2e..50b31235c2 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -33,11 +33,16 @@ use { super::*, + bitcoin::{ + blockdata::{locktime::absolute::LockTime, witness::Witness}, + Amount, ScriptBuf, + }, std::cmp::{max, min}, }; #[derive(Debug, PartialEq)] pub enum Error { + BitcoinAddressError(String), DuplicateAddress(Address), Dust { output_value: Amount, @@ -61,9 +66,16 @@ pub enum Target { ExactPostage(Amount), } +impl From for Error { + fn from(err: bitcoin::address::Error) -> Self { + Error::BitcoinAddressError(err.to_string()) + } +} + impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Error::BitcoinAddressError(error) => write!(f, "error parsing address: {error}"), Error::Dust { output_value, dust_value, @@ -93,14 +105,15 @@ impl std::error::Error for Error {} #[derive(Debug, PartialEq)] pub struct TransactionBuilder { amounts: BTreeMap, + chain: Chain, change_addresses: BTreeSet
, fee_rate: FeeRate, inputs: Vec, inscriptions: BTreeMap, locked_utxos: BTreeSet, outgoing: SatPoint, - outputs: Vec<(Address, Amount)>, - recipient: Address, + outputs: Vec<(ScriptBuf, Amount)>, + recipient: ScriptBuf, runic_utxos: BTreeSet, target: Target, unused_change_addresses: Vec
, @@ -115,20 +128,22 @@ impl TransactionBuilder { const SCHNORR_SIGNATURE_SIZE: usize = 64; pub(crate) const MAX_POSTAGE: Amount = Amount::from_sat(2 * 10_000); - pub fn new( + pub fn new>( outgoing: SatPoint, inscriptions: BTreeMap, amounts: BTreeMap, locked_utxos: BTreeSet, runic_utxos: BTreeSet, - recipient: Address, + recipient: U, change: [Address; 2], fee_rate: FeeRate, target: Target, + chain: Chain, ) -> Self { Self { utxos: amounts.keys().cloned().collect(), amounts, + chain, change_addresses: change.iter().cloned().collect(), fee_rate, inputs: Vec::new(), @@ -136,7 +151,7 @@ impl TransactionBuilder { locked_utxos, outgoing, outputs: Vec::new(), - recipient, + recipient: recipient.into(), runic_utxos, target, unused_change_addresses: change.to_vec(), @@ -150,13 +165,18 @@ impl TransactionBuilder { )); } - if self.change_addresses.contains(&self.recipient) { - return Err(Error::DuplicateAddress(self.recipient)); + if !self.recipient.is_op_return() { + let recipient_address = self.chain.address_from_script(&self.recipient)?; + if self.change_addresses.contains(&recipient_address) { + return Err(Error::DuplicateAddress( + recipient_address.clone() // Clone the duplicate address + )); + } } match self.target { Target::Value(output_value) | Target::ExactPostage(output_value) => { - let dust_value = self.recipient.script_pubkey().dust_value(); + let dust_value = self.recipient.dust_value(); if output_value < dust_value { return Err(Error::Dust { @@ -242,7 +262,8 @@ impl TransactionBuilder { self .unused_change_addresses .pop() - .expect("not enough change addresses"), + .expect("not enough change addresses") + .into(), Amount::from_sat(sat_offset), ), ); @@ -287,7 +308,7 @@ impl TransactionBuilder { let estimated_fee = self.estimate_fee(); let min_value = match self.target { - Target::Postage => self.outputs.last().unwrap().0.script_pubkey().dust_value(), + Target::Postage => self.outputs.last().unwrap().0.dust_value(), Target::Value(value) | Target::ExactPostage(value) => value, }; @@ -368,7 +389,8 @@ impl TransactionBuilder { self .unused_change_addresses .pop() - .expect("not enough change addresses"), + .expect("not enough change addresses") + .into(), value - target, )); } @@ -420,13 +442,13 @@ impl TransactionBuilder { self .outputs .iter() - .map(|(address, _amount)| address) + .map(|(recipient, _amount)| recipient) .cloned() .collect(), ) } - fn estimate_vbytes_with(inputs: usize, outputs: Vec
) -> usize { + fn estimate_vbytes_with(inputs: usize, outputs: Vec) -> usize { Transaction { version: 2, lock_time: LockTime::ZERO, @@ -440,9 +462,9 @@ impl TransactionBuilder { .collect(), output: outputs .into_iter() - .map(|address| TxOut { + .map(|recipient| TxOut { value: 0, - script_pubkey: address.script_pubkey(), + script_pubkey: recipient.clone(), }) .collect(), } @@ -454,7 +476,7 @@ impl TransactionBuilder { } fn build(self) -> Result { - let recipient = self.recipient.script_pubkey(); + let recipient = self.recipient.clone(); let transaction = Transaction { version: 2, lock_time: LockTime::ZERO, @@ -471,9 +493,9 @@ impl TransactionBuilder { output: self .outputs .iter() - .map(|(address, amount)| TxOut { + .map(|(output, amount)| TxOut { value: amount.to_sat(), - script_pubkey: address.script_pubkey(), + script_pubkey: output.clone() }) .collect(), }; @@ -531,7 +553,7 @@ impl TransactionBuilder { transaction .output .iter() - .filter(|tx_out| tx_out.script_pubkey == self.recipient.script_pubkey()) + .filter(|tx_out| tx_out.script_pubkey == self.recipient) .count(), 1, "invariant: recipient address appears exactly once in outputs", @@ -552,7 +574,7 @@ impl TransactionBuilder { let mut offset = 0; for output in &transaction.output { - if output.script_pubkey == self.recipient.script_pubkey() { + if output.script_pubkey == self.recipient { let slop = self.fee_rate.fee(Self::ADDITIONAL_OUTPUT_VBYTES); match self.target { @@ -738,6 +760,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet ) .select_outgoing() .unwrap(); @@ -751,7 +774,7 @@ mod tests { assert_eq!( tx_builder.outputs, [( - recipient(), + recipient().into(), Amount::from_sat(100 * COIN_VALUE - 51 * COIN_VALUE) )] ) @@ -772,16 +795,17 @@ mod tests { inscriptions: BTreeMap::new(), locked_utxos: BTreeSet::new(), runic_utxos: BTreeSet::new(), - recipient: recipient(), + recipient: recipient().into(), unused_change_addresses: vec![change(0), change(1)], change_addresses: vec![change(0), change(1)].into_iter().collect(), inputs: vec![outpoint(1), outpoint(2), outpoint(3)], outputs: vec![ - (recipient(), Amount::from_sat(5_000)), - (change(0), Amount::from_sat(5_000)), - (change(1), Amount::from_sat(1_724)), + (recipient().into(), Amount::from_sat(5_000)), + (change(0).into(), Amount::from_sat(5_000)), + (change(1).into(), Amount::from_sat(1_724)), ], target: Target::Postage, + chain: Chain::Testnet }; pretty_assert_eq!( @@ -813,6 +837,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet ) .build_transaction() .unwrap() @@ -834,6 +859,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet ) .build_transaction(), Ok(Transaction { @@ -860,6 +886,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet ) .select_outgoing() .unwrap() @@ -886,6 +913,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet ) .build_transaction(), Ok(Transaction { @@ -912,6 +940,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet ) .build_transaction(), Err(Error::NotEnoughCardinalUtxos), @@ -935,7 +964,8 @@ mod tests { recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - Target::Postage + Target::Postage, + Chain::Testnet ) .build_transaction(), Err(Error::NotEnoughCardinalUtxos), @@ -960,6 +990,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Ok(Transaction { @@ -990,6 +1021,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build() .unwrap(); @@ -1010,6 +1042,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build() .unwrap(); @@ -1030,6 +1063,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build() .unwrap(); @@ -1050,6 +1084,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .select_outgoing() .unwrap(); @@ -1057,7 +1092,8 @@ mod tests { builder.outputs[0].0 = "tb1qx4gf3ya0cxfcwydpq8vr2lhrysneuj5d7lqatw" .parse::>() .unwrap() - .assume_checked(); + .assume_checked() + .into(); builder.build().unwrap(); } @@ -1077,6 +1113,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .select_outgoing() .unwrap(); @@ -1101,6 +1138,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Ok(Transaction { @@ -1130,6 +1168,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .select_outgoing() .unwrap() @@ -1152,6 +1191,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Ok(Transaction { @@ -1181,6 +1221,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Ok(Transaction { @@ -1207,6 +1248,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .select_outgoing() .unwrap() @@ -1236,6 +1278,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .select_outgoing() .unwrap() @@ -1263,6 +1306,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .select_outgoing() .unwrap() @@ -1287,6 +1331,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .select_outgoing() .unwrap() @@ -1311,16 +1356,17 @@ mod tests { runic_utxos: BTreeSet::new(), outgoing: satpoint(1, 0), inscriptions: BTreeMap::new(), - recipient: recipient(), + recipient: recipient().into(), unused_change_addresses: vec![change(0), change(1)], change_addresses: vec![change(0), change(1)].into_iter().collect(), inputs: vec![outpoint(1), outpoint(2), outpoint(3)], outputs: vec![ - (recipient(), Amount::from_sat(5_000)), - (recipient(), Amount::from_sat(5_000)), - (change(1), Amount::from_sat(1_774)), + (recipient().into(), Amount::from_sat(5_000)), + (recipient().into(), Amount::from_sat(5_000)), + (change(1).into(), Amount::from_sat(1_774)), ], target: Target::Postage, + chain: Chain::Testnet } .build() .unwrap(); @@ -1342,16 +1388,17 @@ mod tests { runic_utxos: BTreeSet::new(), outgoing: satpoint(1, 0), inscriptions: BTreeMap::new(), - recipient: recipient(), + recipient: recipient().into(), unused_change_addresses: vec![change(0), change(1)], change_addresses: vec![change(0), change(1)].into_iter().collect(), inputs: vec![outpoint(1), outpoint(2), outpoint(3)], outputs: vec![ - (recipient(), Amount::from_sat(5_000)), - (change(0), Amount::from_sat(5_000)), - (change(0), Amount::from_sat(1_774)), + (recipient().into(), Amount::from_sat(5_000)), + (change(0).into(), Amount::from_sat(5_000)), + (change(0).into(), Amount::from_sat(1_774)), ], target: Target::Postage, + chain: Chain::Testnet, } .build() .unwrap(); @@ -1375,6 +1422,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Err(Error::NotEnoughCardinalUtxos) @@ -1399,6 +1447,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Err(Error::NotEnoughCardinalUtxos) @@ -1420,6 +1469,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Err(Error::UtxoContainsAdditionalInscription { @@ -1446,6 +1496,7 @@ mod tests { [change(0), change(1)], fee_rate, Target::Postage, + Chain::Testnet, ) .build_transaction() .unwrap(); @@ -1478,7 +1529,8 @@ mod tests { recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - Target::Value(Amount::from_sat(1000)) + Target::Value(Amount::from_sat(1000)), + Chain::Testnet, ) .build_transaction(), Ok(Transaction { @@ -1507,7 +1559,8 @@ mod tests { recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - Target::Value(Amount::from_sat(1500)) + Target::Value(Amount::from_sat(1500)), + Chain::Testnet, ) .build_transaction(), Ok(Transaction { @@ -1533,7 +1586,8 @@ mod tests { recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - Target::Value(Amount::from_sat(1)) + Target::Value(Amount::from_sat(1)), + Chain::Testnet, ) .build_transaction(), Err(Error::Dust { @@ -1560,7 +1614,8 @@ mod tests { recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - Target::Value(Amount::from_sat(1000)) + Target::Value(Amount::from_sat(1000)), + Chain::Testnet, ) .build_transaction(), Err(Error::NotEnoughCardinalUtxos), @@ -1584,7 +1639,8 @@ mod tests { recipient(), [change(0), change(1)], FeeRate::try_from(4.0).unwrap(), - Target::Value(Amount::from_sat(1000)) + Target::Value(Amount::from_sat(1000)), + Chain::Testnet, ) .build_transaction(), Err(Error::NotEnoughCardinalUtxos), @@ -1607,7 +1663,8 @@ mod tests { "bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k" .parse::>() .unwrap() - .assume_checked(), + .assume_checked() + .into(), ], ); assert_eq!(after - before, TransactionBuilder::ADDITIONAL_OUTPUT_VBYTES); @@ -1627,7 +1684,8 @@ mod tests { recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - Target::Value(Amount::from_sat(707)) + Target::Value(Amount::from_sat(707)), + Chain::Testnet, ) .build_transaction(), Ok(Transaction { @@ -1654,6 +1712,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Ok(Transaction { @@ -1679,7 +1738,8 @@ mod tests { recipient(), [change(0), change(1)], FeeRate::try_from(5.0).unwrap(), - Target::Value(Amount::from_sat(1000)) + Target::Value(Amount::from_sat(1000)), + Chain::Testnet, ) .build_transaction(), Ok(Transaction { @@ -1705,7 +1765,8 @@ mod tests { recipient(), [change(0), change(1)], FeeRate::try_from(6.0).unwrap(), - Target::Value(Amount::from_sat(1000)) + Target::Value(Amount::from_sat(1000)), + Chain::Testnet, ) .build_transaction(), Err(Error::NotEnoughCardinalUtxos) @@ -1726,7 +1787,8 @@ mod tests { recipient(), [recipient(), change(1)], FeeRate::try_from(0.0).unwrap(), - Target::Value(Amount::from_sat(1000)) + Target::Value(Amount::from_sat(1000)), + Chain::Testnet, ) .build_transaction(), Err(Error::DuplicateAddress(recipient())) @@ -1747,7 +1809,8 @@ mod tests { recipient(), [change(0), change(0)], FeeRate::try_from(0.0).unwrap(), - Target::Value(Amount::from_sat(1000)) + Target::Value(Amount::from_sat(1000)), + Chain::Testnet, ) .build_transaction(), Err(Error::DuplicateAddress(change(0))) @@ -1768,7 +1831,8 @@ mod tests { recipient(), [change(0), change(1)], FeeRate::try_from(2.0).unwrap(), - Target::Value(Amount::from_sat(1500)) + Target::Value(Amount::from_sat(1500)), + Chain::Testnet, ) .build_transaction(), Ok(Transaction { @@ -1795,6 +1859,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(250.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Ok(Transaction { @@ -1827,6 +1892,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), + Chain::Testnet, ) .select_outgoing() .unwrap() @@ -1847,7 +1913,10 @@ mod tests { ); // value inputs are pushed at the end assert_eq!( tx_builder.outputs, - [(recipient(), Amount::from_sat(3_003 + 3_006 + 3_005 + 3_001))] + [( + recipient().into(), + Amount::from_sat(3_003 + 3_006 + 3_005 + 3_001) + )] ) } @@ -1873,6 +1942,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), + Chain::Testnet, ) .select_outgoing() .unwrap() @@ -1895,8 +1965,8 @@ mod tests { assert_eq!( tx_builder.outputs, [ - (change(1), Amount::from_sat(101 + 104 + 105 + 1)), - (recipient(), Amount::from_sat(19_999)) + (change(1).into(), Amount::from_sat(101 + 104 + 105 + 1)), + (recipient().into(), Amount::from_sat(19_999)) ] ) } @@ -1926,6 +1996,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), + Chain::Testnet, ); assert_eq!( @@ -1982,9 +2053,10 @@ mod tests { [change(0), change(1)], fee_rate, Target::ExactPostage(Amount::from_sat(66_000)), + Chain::Testnet, ) - .build_transaction() - .unwrap(); + .build_transaction() + .unwrap(); let fee = fee_rate.fee(transaction.vsize() + TransactionBuilder::SCHNORR_SIGNATURE_SIZE / 4 + 1); @@ -2018,6 +2090,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), + Chain::Testnet, ); assert_eq!( @@ -2044,6 +2117,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), + Chain::Testnet, ); assert_eq!( @@ -2072,6 +2146,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), + Chain::Testnet, ); assert_eq!( @@ -2100,6 +2175,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), + Chain::Testnet, ); assert_eq!( @@ -2110,4 +2186,40 @@ mod tests { outpoint(2), ); } + + #[test] + fn burn_has_op_return_script() { + let utxos = vec![(outpoint(1), Amount::from_sat(5_000))]; + + let msg = b"let it burn".to_vec(); + let msg_push_bytes = PushBytesBuf::try_from(msg.clone()).expect("burn message should fit"); + + pretty_assert_eq!( + TransactionBuilder::new( + satpoint(1, 0), + BTreeMap::new(), + utxos.into_iter().collect(), + BTreeSet::new(), + BTreeSet::new(), + ScriptBuf::new_op_return(&msg_push_bytes), + [change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), + Target::Value(Amount::from_sat(1000)), + Chain::Testnet + ) + .build_transaction(), + Ok(Transaction { + version: 1, + lock_time: LockTime::ZERO, + input: vec![tx_in(outpoint(1))], + output: vec![ + TxOut { + value: 1000, + script_pubkey: ScriptBuf::new_op_return(&msg_push_bytes) + }, + tx_out(3879, change(1)) + ], + }) + ) + } } diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index 4faff866ab..5b47140bbc 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -2,6 +2,7 @@ use super::*; #[derive(Boilerplate, Default)] pub(crate) struct InscriptionHtml { + pub(crate) burn_payload: Option, pub(crate) chain: Chain, pub(crate) children: Vec, pub(crate) genesis_fee: u64, @@ -9,6 +10,7 @@ pub(crate) struct InscriptionHtml { pub(crate) inscription: Inscription, pub(crate) inscription_id: InscriptionId, pub(crate) inscription_number: i32, + pub(crate) is_burned: Option, pub(crate) next: Option, pub(crate) output: Option, pub(crate) parent: Option, @@ -23,6 +25,8 @@ pub(crate) struct InscriptionHtml { #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct InscriptionJson { pub address: Option, + pub burn_payload: Option, + pub charms: Option, pub children: Vec, pub content_length: Option, pub content_type: Option, @@ -30,6 +34,7 @@ pub struct InscriptionJson { pub genesis_height: u32, pub inscription_id: InscriptionId, pub inscription_number: i32, + pub is_burned: Option, pub next: Option, pub output_value: Option, pub parent: Option, @@ -457,4 +462,37 @@ mod tests { .unindent() ); } + + #[test] + fn burned() { + assert_regex_match!( + InscriptionHtml { + genesis_fee: 1, + inscription: Inscription { + content_encoding: Some("br".into()), + ..inscription("text/plain;charset=utf-8", "HELLOWORLD") + }, + inscription_id: inscription_id(1), + inscription_number: 1, + satpoint: satpoint(1, 0), + is_burned: Some(true.into()), + burn_payload: Some("0xdeadbeef".into()), + ..Default::default() + }, + " +

Inscription 1

+ .* +
+ .* +
address
+
burned
+ .* +
burn payload
+
0xdeadbeef
+ .* +
+ " + .unindent() + ); + } } diff --git a/templates/inscription.html b/templates/inscription.html index 72917c36a4..4135d5399e 100644 --- a/templates/inscription.html +++ b/templates/inscription.html @@ -48,6 +48,12 @@

Inscription {{ self.inscription_number }}

%% } %% } +%% if let Some(is_burned) = self.is_burned { +%% if is_burned { +
address
+
burned
+%% } +%% } %% if let Some(output) = &self.output { %% if let Ok(address) = self.chain.address_from_script(&output.script_pubkey ) {
address
@@ -92,6 +98,10 @@

Inscription {{ self.inscription_number }}

{{ self.satpoint }}
output
{{ self.satpoint.outpoint }}
+%% if let Some(burn_payload) = &self.burn_payload { +
burn payload
+
{{ burn_payload }}
+%% }
offset
{{ self.satpoint.offset }}
ethereum teleburn address
diff --git a/test-bitcoincore-rpc/src/lib.rs b/test-bitcoincore-rpc/src/lib.rs index b12cf61fbf..aad7652637 100644 --- a/test-bitcoincore-rpc/src/lib.rs +++ b/test-bitcoincore-rpc/src/lib.rs @@ -159,7 +159,7 @@ impl From for JsonOutPoint { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -struct FundRawTransactionOptions { +pub struct FundRawTransactionOptions { #[serde(with = "bitcoin::amount::serde::as_btc::opt")] fee_rate: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/tests/json_api.rs b/tests/json_api.rs index ab3dfb8e8e..1a5352c8ba 100644 --- a/tests/json_api.rs +++ b/tests/json_api.rs @@ -149,8 +149,10 @@ fn get_inscription() { InscriptionJson { parent: None, children: Vec::new(), + burn_payload: None, inscription_id, inscription_number: 0, + is_burned: Some(false), genesis_height: 2, genesis_fee: 138, output_value: Some(10000), @@ -163,6 +165,7 @@ fn get_inscription() { previous: None, next: None, rune: None, + charms: None, } ) } diff --git a/tests/server.rs b/tests/server.rs index 595263d652..b676ea4458 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -141,7 +141,7 @@ fn inscription_page_after_send() { ); let txid = CommandBuilder::new(format!( - "wallet send --fee-rate 1 bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv {inscription}" + "wallet send --fee-rate 1 --recipient bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv {inscription}" )) .rpc_server(&rpc_server) .stdout_regex(".*") diff --git a/tests/wallet/inscriptions.rs b/tests/wallet/inscriptions.rs index 4ff430510c..4952e1ccc1 100644 --- a/tests/wallet/inscriptions.rs +++ b/tests/wallet/inscriptions.rs @@ -29,7 +29,7 @@ fn inscriptions() { .address; let txid = CommandBuilder::new(format!( - "wallet send --fee-rate 1 {} {inscription}", + "wallet send --fee-rate 1 --recipient {} {inscription}", address.assume_checked() )) .rpc_server(&rpc_server) @@ -94,7 +94,7 @@ fn inscriptions_with_postage() { .address; CommandBuilder::new(format!( - "wallet send --fee-rate 1 {} {inscription}", + "wallet send --fee-rate 1 --recipient {} {inscription}", address.assume_checked() )) .rpc_server(&rpc_server) diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index cdbe0c22b0..c0cf01cac5 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -11,7 +11,7 @@ fn inscriptions_can_be_sent() { rpc_server.mine_blocks(1); let output = CommandBuilder::new(format!( - "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {inscription}", + "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {inscription}", )) .rpc_server(&rpc_server) .stdout_regex(r".*") @@ -51,7 +51,7 @@ fn send_unknown_inscription() { let txid = rpc_server.mine_blocks(1)[0].txdata[0].txid(); CommandBuilder::new(format!( - "wallet send --fee-rate 1 bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv {txid}i0" + "wallet send --fee-rate 1 --address bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv {txid}i0" )) .rpc_server(&rpc_server) .expected_stderr(format!("error: inscription {txid}i0 not found\n")) @@ -70,7 +70,7 @@ fn send_inscribed_sat() { rpc_server.mine_blocks(1); let output = CommandBuilder::new(format!( - "wallet send --fee-rate 1 bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv {inscription}", + "wallet send --fee-rate 1 --address bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv {inscription}", )) .rpc_server(&rpc_server) .run_and_deserialize_output::(); @@ -98,7 +98,7 @@ fn send_on_mainnnet_works_with_wallet_named_foo() { .run_and_deserialize_output::(); CommandBuilder::new(format!( - "--wallet foo wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {txid}:0:0" + "--wallet foo wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {txid}:0:0" )) .rpc_server(&rpc_server) .run_and_deserialize_output::(); @@ -111,7 +111,7 @@ fn send_addresses_must_be_valid_for_network() { create_wallet(&rpc_server); CommandBuilder::new(format!( - "wallet send --fee-rate 1 tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz {txid}:0:0" + "wallet send --fee-rate 1 --address tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz {txid}:0:0" )) .rpc_server(&rpc_server) .expected_stderr( @@ -128,7 +128,7 @@ fn send_on_mainnnet_works_with_wallet_named_ord() { create_wallet(&rpc_server); let output = CommandBuilder::new(format!( - "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {txid}:0:0" + "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {txid}:0:0" )) .rpc_server(&rpc_server) .run_and_deserialize_output::(); @@ -145,13 +145,13 @@ fn send_does_not_use_inscribed_sats_as_cardinal_utxos() { CommandBuilder::new(format!( "wallet inscribe --satpoint {txid}:0:0 --file degenerate.png --fee-rate 0" )) - .write("degenerate.png", [1; 100]) - .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .write("degenerate.png", [1; 100]) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); let txid = rpc_server.mine_blocks_with_subsidy(1, 100)[0].txdata[0].txid(); CommandBuilder::new(format!( - "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {txid}:0:0" + "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {txid}:0:0" )) .rpc_server(&rpc_server) .expected_exit_code(1) @@ -174,7 +174,7 @@ fn do_not_send_within_dust_limit_of_an_inscription() { }; CommandBuilder::new(format!( - "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {output}:329" + "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {output}:329" )) .rpc_server(&rpc_server) .expected_exit_code(1) @@ -199,7 +199,7 @@ fn can_send_after_dust_limit_from_an_inscription() { }; CommandBuilder::new(format!( - "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {output}:330" + "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {output}:330" )) .rpc_server(&rpc_server) .run_and_deserialize_output::(); @@ -266,7 +266,7 @@ fn splitting_merged_inscriptions_is_possible() { // try and fail to send first CommandBuilder::new(format!( - "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {}i0", + "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {}i0", reveal_txid, )) .rpc_server(&rpc_server) @@ -278,7 +278,7 @@ fn splitting_merged_inscriptions_is_possible() { // splitting out last CommandBuilder::new(format!( - "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {}i2", + "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {}i2", reveal_txid, )) .rpc_server(&rpc_server) @@ -288,7 +288,7 @@ fn splitting_merged_inscriptions_is_possible() { // splitting second to last CommandBuilder::new(format!( - "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {}i1", + "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {}i1", reveal_txid, )) .rpc_server(&rpc_server) @@ -298,7 +298,7 @@ fn splitting_merged_inscriptions_is_possible() { // splitting send first CommandBuilder::new(format!( - "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {}i0", + "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {}i0", reveal_txid, )) .rpc_server(&rpc_server) @@ -315,7 +315,7 @@ fn inscriptions_cannot_be_sent_by_satpoint() { rpc_server.mine_blocks(1); CommandBuilder::new(format!( - "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {reveal}:0:0" + "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {reveal}:0:0" )) .rpc_server(&rpc_server) .expected_stderr("error: inscriptions must be sent by inscription ID\n") @@ -331,10 +331,10 @@ fn send_btc_with_fee_rate() { rpc_server.mine_blocks(1); CommandBuilder::new( - "wallet send --fee-rate 13.3 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc", + "wallet send --fee-rate 13.3 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc", ) - .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); let tx = &rpc_server.mempool()[0]; let mut fee = 0; @@ -374,9 +374,11 @@ fn send_btc_locks_inscriptions() { let (_, reveal) = inscribe(&rpc_server); - CommandBuilder::new("wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc") - .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + CommandBuilder::new( + "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc", + ) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); assert_eq!( rpc_server.sent(), @@ -403,11 +405,13 @@ fn send_btc_fails_if_lock_unspent_fails() { rpc_server.mine_blocks(1); - CommandBuilder::new("wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc") - .rpc_server(&rpc_server) - .expected_stderr("error: failed to lock UTXOs\n") - .expected_exit_code(1) - .run_and_extract_stdout(); + CommandBuilder::new( + "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc", + ) + .rpc_server(&rpc_server) + .expected_stderr("error: failed to lock UTXOs\n") + .expected_exit_code(1) + .run_and_extract_stdout(); } #[test] @@ -419,7 +423,7 @@ fn wallet_send_with_fee_rate() { let (inscription, _) = inscribe(&rpc_server); CommandBuilder::new(format!( - "wallet send bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {inscription} --fee-rate 2.0" + "wallet send --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {inscription} --fee-rate 2.0" )) .rpc_server(&rpc_server) .run_and_deserialize_output::(); @@ -450,7 +454,7 @@ fn user_must_provide_fee_rate_to_send() { let (inscription, _) = inscribe(&rpc_server); CommandBuilder::new(format!( - "wallet send bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {inscription}" + "wallet send --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {inscription}" )) .rpc_server(&rpc_server) .expected_exit_code(2) @@ -470,10 +474,10 @@ fn wallet_send_with_fee_rate_and_target_postage() { let (inscription, _) = inscribe(&rpc_server); CommandBuilder::new(format!( - "wallet send bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {inscription} --fee-rate 2.0 --postage 77000sat" + "wallet send --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {inscription} --fee-rate 2.0 --postage 77000sat" )) - .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); let tx = &rpc_server.mempool()[0]; let mut fee = 0; @@ -503,11 +507,13 @@ fn send_btc_does_not_send_locked_utxos() { rpc_server.lock(outpoint); - CommandBuilder::new("wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc") - .rpc_server(&rpc_server) - .expected_exit_code(1) - .stderr_regex("error:.*") - .run_and_extract_stdout(); + CommandBuilder::new( + "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc", + ) + .rpc_server(&rpc_server) + .expected_exit_code(1) + .stderr_regex("error:.*") + .run_and_extract_stdout(); } #[test] @@ -953,3 +959,62 @@ fn sending_rune_does_not_send_inscription() { .stderr_regex("error:.*") .run_and_extract_stdout(); } + +#[test] +fn refuse_to_burn_amount() { + let rpc_server = test_bitcoincore_rpc::builder().build(); + rpc_server.mine_blocks_with_subsidy(1, 1_000); + + create_wallet(&rpc_server); + + CommandBuilder::new("wallet send --fee-rate 1 --burn ciao 1btc") + .rpc_server(&rpc_server) + .expected_stderr("error: refusing to burn amount\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn burn_inscribed_sat() { + let rpc_server = test_bitcoincore_rpc::spawn(); + create_wallet(&rpc_server); + rpc_server.mine_blocks(1); + + let (inscription, _) = inscribe(&rpc_server); + + rpc_server.mine_blocks(1); + + let ord_server = TestServer::spawn_with_server_args(&rpc_server, &[], &["--enable-json-api"]); + let response = ord_server.json_request(format!("/inscription/{inscription}")); + assert_eq!(response.status(), StatusCode::OK); + let inscription_json: InscriptionJson = serde_json::from_str(&response.text().unwrap()).unwrap(); + assert!( + inscription_json.address.is_some(), + "address should be shown before burn" + ); + + CommandBuilder::new(format!( + "wallet send --fee-rate 1 --burn begone {inscription}", + )) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + rpc_server.mine_blocks(1); + + let ord_server = TestServer::spawn_with_server_args(&rpc_server, &[], &["--enable-json-api"]); + let response = ord_server.json_request(format!("/inscription/{inscription}")); + assert_eq!(response.status(), StatusCode::OK); + let inscription_json: InscriptionJson = serde_json::from_str(&response.text().unwrap()).unwrap(); + assert!( + inscription_json.address.is_none(), + "address should be missing after burn" + ); + assert!( + inscription_json.charms.eq(&Some(1u16)), + "inscription should have burned charm" + ); + assert!( + inscription_json.burn_payload.eq(&Some("begone".into())), + "inscription should have burn payload" + ) +}