From b9400f17024dd7eb527243cc76b6361ea6b3d5b6 Mon Sep 17 00:00:00 2001 From: onchainguy-eth <1436535+onchainguy-eth@users.noreply.github.com> Date: Mon, 1 Apr 2024 22:43:38 +0200 Subject: [PATCH] Implement burning --- crates/ordinals/src/charm.rs | 7 +- src/index/updater/inscription_updater.rs | 30 ++- src/subcommand/wallet/send.rs | 64 ++++-- src/test.rs | 16 +- src/wallet/batch/plan.rs | 3 +- src/wallet/recipient.rs | 56 +++++ src/wallet/transaction_builder.rs | 275 ++++++++++++++++------- tests/wallet/send.rs | 52 +++++ 8 files changed, 393 insertions(+), 110 deletions(-) create mode 100644 src/wallet/recipient.rs diff --git a/crates/ordinals/src/charm.rs b/crates/ordinals/src/charm.rs index 0331be9306..d40359d289 100644 --- a/crates/ordinals/src/charm.rs +++ b/crates/ordinals/src/charm.rs @@ -14,10 +14,11 @@ pub enum Charm { Uncommon = 9, Vindicated = 10, Mythic = 11, + Burned = 12, } impl Charm { - pub const ALL: [Self; 12] = [ + pub const ALL: [Self; 13] = [ Self::Coin, Self::Uncommon, Self::Rare, @@ -30,6 +31,7 @@ impl Charm { Self::Unbound, Self::Lost, Self::Vindicated, + Self::Burned, ]; fn flag(self) -> u16 { @@ -62,6 +64,7 @@ impl Charm { Self::Unbound => "🔓", Self::Uncommon => "🌱", Self::Vindicated => "❤️‍🔥", + Self::Burned => "💀🔥", } } @@ -91,6 +94,7 @@ impl Display for Charm { Self::Unbound => "unbound", Self::Uncommon => "uncommon", Self::Vindicated => "vindicated", + Self::Burned => "burned", } ) } @@ -113,6 +117,7 @@ impl FromStr for Charm { "unbound" => Self::Unbound, "uncommon" => Self::Uncommon, "vindicated" => Self::Vindicated, + "burned" => Self::Burned, _ => return Err(format!("invalid charm `{s}`")), }) } diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 41dd42c538..f6d3ff822f 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -291,6 +291,7 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { let mut range_to_vout = BTreeMap::new(); let mut new_locations = Vec::new(); let mut output_value = 0; + let mut is_burned = false; for (vout, tx_out) in tx.output.iter().enumerate() { let end = output_value + tx_out.value; @@ -307,6 +308,7 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { offset: flotsam.offset - output_value, }; + is_burned = tx_out.script_pubkey.is_op_return(); new_locations.push((new_satpoint, inscriptions.next().unwrap())); } @@ -345,7 +347,7 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { _ => new_satpoint, }; - self.update_inscription_location(input_sat_ranges, flotsam, new_satpoint)?; + self.update_inscription_location(input_sat_ranges, flotsam, new_satpoint, is_burned)?; } if is_coinbase { @@ -354,7 +356,7 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { outpoint: OutPoint::null(), offset: self.lost_sats + flotsam.offset - output_value, }; - self.update_inscription_location(input_sat_ranges, flotsam, new_satpoint)?; + self.update_inscription_location(input_sat_ranges, flotsam, new_satpoint, false)?; } self.lost_sats += self.reward - output_value; Ok(()) @@ -392,6 +394,7 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { input_sat_ranges: Option<&VecDeque<(u64, u64)>>, flotsam: Flotsam, new_satpoint: SatPoint, + is_burned: bool, ) -> Result { let inscription_id = flotsam.inscription_id; let (unbound, sequence_number) = match flotsam.origin { @@ -406,6 +409,29 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> { .unwrap() .value(); + println!("is_burned {}", is_burned); + + if is_burned { + let entry = InscriptionEntry::load( + self.sequence_number_to_entry + .get(&sequence_number)? + .unwrap() + .value(), + ); + + let mut charms = entry.charms.clone(); + Charm::Burned.set(&mut charms); + + self.sequence_number_to_entry.insert( + sequence_number, + &InscriptionEntry { + charms, + ..entry + } + .store(), + )?; + } + if let Some(sender) = self.event_sender { sender.blocking_send(Event::InscriptionTransferred { block_height: self.height, diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 7471b13f31..22b0ef00a1 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -1,4 +1,23 @@ -use {super::*, crate::outgoing::Outgoing, base64::Engine, bitcoin::psbt::Psbt}; +use {super::*, crate::outgoing::Outgoing, base64::Engine, bitcoin::{opcodes,psbt::Psbt}}; + +#[derive(Clone, Copy)] +struct AddressParser; + +#[derive(Clone, Debug, PartialEq)] +enum ParsedAddress { + Address(Address), + ScriptBuf(ScriptBuf), +} + +fn parse_address(arg: &str) -> Result { + if arg == "burn" { + let builder = script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN); + Ok(ParsedAddress::ScriptBuf(builder.into_script())) + } else { + Ok(ParsedAddress::Address(Address::from_str(arg)?)) + } +} #[derive(Debug, Parser)] pub(crate) struct Send { @@ -11,7 +30,8 @@ pub(crate) struct Send { help = "Target postage with sent inscriptions. [default: 10000 sat]" )] pub(crate) postage: Option, - address: Address, + #[arg(value_parser = parse_address)] + address: ParsedAddress, outgoing: Outgoing, } @@ -25,25 +45,32 @@ pub struct Output { impl Send { pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult { - let address = self - .address - .clone() - .require_network(wallet.chain().network())?; + let recipient = match self.address { + ParsedAddress::Address(address) => { + address + .clone() + .require_network(wallet.chain().network())? + .script_pubkey() + } + ParsedAddress::ScriptBuf(script_buf) => { + script_buf.clone() + } + }; let unsigned_transaction = match self.outgoing { Outgoing::Amount(amount) => { - Self::create_unsigned_send_amount_transaction(&wallet, address, amount, self.fee_rate)? + Self::create_unsigned_send_amount_transaction(&wallet, recipient, amount, self.fee_rate)? } Outgoing::Rune { decimal, rune } => Self::create_unsigned_send_runes_transaction( &wallet, - address, + recipient, rune, decimal, self.fee_rate, )?, Outgoing::InscriptionId(id) => Self::create_unsigned_send_satpoint_transaction( &wallet, - address, + recipient, wallet .inscription_info() .get(&id) @@ -55,7 +82,7 @@ impl Send { )?, Outgoing::SatPoint(satpoint) => Self::create_unsigned_send_satpoint_transaction( &wallet, - address, + recipient, satpoint, self.postage, self.fee_rate, @@ -63,7 +90,7 @@ impl Send { )?, Outgoing::Sat(sat) => Self::create_unsigned_send_satpoint_transaction( &wallet, - address, + recipient, wallet.find_sat_in_outputs(sat)?, self.postage, self.fee_rate, @@ -132,7 +159,7 @@ impl Send { fn create_unsigned_send_amount_transaction( wallet: &Wallet, - destination: Address, + destination: ScriptBuf, amount: Amount, fee_rate: FeeRate, ) -> Result { @@ -143,7 +170,7 @@ impl Send { lock_time: LockTime::ZERO, input: Vec::new(), output: vec![TxOut { - script_pubkey: destination.script_pubkey(), + script_pubkey: destination, value: amount.to_sat(), }], }; @@ -159,7 +186,7 @@ impl Send { fn create_unsigned_send_satpoint_transaction( wallet: &Wallet, - destination: Address, + destination: ScriptBuf, satpoint: SatPoint, postage: Option, fee_rate: FeeRate, @@ -195,18 +222,19 @@ impl Send { wallet.utxos().clone(), wallet.locked_utxos().clone().into_keys().collect(), runic_outputs, - destination.clone(), + destination, change, fee_rate, postage, + wallet.chain().network() ) - .build_transaction()?, + .build_transaction()?, ) } fn create_unsigned_send_runes_transaction( wallet: &Wallet, - destination: Address, + destination: ScriptBuf, spaced_rune: SpacedRune, decimal: Decimal, fee_rate: FeeRate, @@ -295,7 +323,7 @@ impl Send { value: TARGET_POSTAGE.to_sat(), }, TxOut { - script_pubkey: destination.script_pubkey(), + script_pubkey: destination, value: TARGET_POSTAGE.to_sat(), }, ], diff --git a/src/test.rs b/src/test.rs index dcf9127cfc..effab4bfb3 100644 --- a/src/test.rs +++ b/src/test.rs @@ -10,6 +10,7 @@ pub(crate) use { std::iter, tempfile::TempDir, unindent::Unindent, + wallet::transaction_builder::OutputType }; pub(crate) fn txid(n: u64) -> Txid { @@ -40,7 +41,16 @@ pub(crate) fn address() -> Address { .assume_checked() } -pub(crate) fn recipient() -> Address { +pub(crate) fn recipient() -> ScriptBuf { + recipient_as_address() + .script_pubkey() +} + +pub(crate) fn recipient_address_as_output_type() -> OutputType { + OutputType::Address(recipient_as_address()) +} + +pub(crate) fn recipient_as_address() -> Address { "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz" .parse::>() .unwrap() @@ -60,6 +70,10 @@ pub(crate) fn change(n: u64) -> Address { .assume_checked() } +pub(crate) fn change_as_output_type(n: u64) -> OutputType { + OutputType::Address(change(n)) +} + pub(crate) fn tx_in(previous_output: OutPoint) -> TxIn { TxIn { previous_output, diff --git a/src/wallet/batch/plan.rs b/src/wallet/batch/plan.rs index ed37d47adf..38ffb7a7d1 100644 --- a/src/wallet/batch/plan.rs +++ b/src/wallet/batch/plan.rs @@ -523,10 +523,11 @@ impl Plan { utxos.clone(), locked_utxos.clone(), runic_utxos, - commit_tx_address.clone(), + commit_tx_address.script_pubkey(), commit_change, self.commit_fee_rate, Target::Value(target_value), + chain.network() ) .build_transaction()?; diff --git a/src/wallet/recipient.rs b/src/wallet/recipient.rs new file mode 100644 index 0000000000..2a960ae810 --- /dev/null +++ b/src/wallet/recipient.rs @@ -0,0 +1,56 @@ +use rustls_acme::acme::AuthStatus::Invalid; +use super::*; + +#[derive(Debug, PartialEq)] +pub(crate) enum RecipientError { + InvalidRecipient +} + +impl Display for RecipientError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + RecipientError::InvalidRecipient => { + write!(f, "Invalid recipient") + } + } + } +} + +impl std::error::Error for RecipientError {} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum Recipient { + Address(Address), + ScriptBuf(ScriptBuf), +} + +trait ScriptPubkey { + fn script_pubkey(&self) -> ScriptBuf; +} + +impl ScriptPubkey for Address { + fn script_pubkey(&self) -> ScriptBuf { + self.script_pubkey() + } +} + +impl ScriptPubkey for ScriptBuf { + fn script_pubkey(&self) -> ScriptBuf { + self.clone() + } +} + +impl Recipient { + pub fn to_address(&self) -> Result
{ + match self { + Recipient::Address(address) => Ok(address.clone()), + _ => Err(anyhow!(RecipientError::InvalidRecipient)), + } + } + pub fn script_pubkey(&self) -> ScriptBuf { + match self { + Recipient::Address(address) => address.script_pubkey(), + Recipient::ScriptBuf(script_buf) => script_buf.script_pubkey(), + } + } +} \ No newline at end of file diff --git a/src/wallet/transaction_builder.rs b/src/wallet/transaction_builder.rs index 8250589535..2a71376008 100644 --- a/src/wallet/transaction_builder.rs +++ b/src/wallet/transaction_builder.rs @@ -43,6 +43,7 @@ pub enum Error { output_value: Amount, dust_value: Amount, }, + InvalidAddress, NotEnoughCardinalUtxos, NotInWallet(SatPoint), OutOfRange(SatPoint, u64), @@ -68,6 +69,7 @@ impl Display for Error { output_value, dust_value, } => write!(f, "output value is below dust value: {output_value} < {dust_value}"), + Error::InvalidAddress => write!(f, "invalid address"), Error::NotInWallet(outgoing_satpoint) => write!(f, "outgoing satpoint {outgoing_satpoint} not in wallet"), Error::OutOfRange(outgoing_satpoint, maximum) => write!(f, "outgoing satpoint {outgoing_satpoint} offset higher than maximum {maximum}"), Error::NotEnoughCardinalUtxos => write!( @@ -90,6 +92,26 @@ impl Display for Error { } impl std::error::Error for Error {} +impl From for Error { + fn from(_: bitcoin::address::Error) -> Self { + Self::InvalidAddress + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum OutputType { + Address(Address), + ScriptBuf(ScriptBuf), +} + +impl OutputType { + fn script_pubkey(&self) -> ScriptBuf { + match self { + OutputType::Address(addr) => addr.script_pubkey(), + OutputType::ScriptBuf(script) => script.clone(), + } + } +} #[derive(Debug, PartialEq)] pub struct TransactionBuilder { @@ -99,9 +121,10 @@ pub struct TransactionBuilder { inputs: Vec, inscriptions: BTreeMap>, locked_utxos: BTreeSet, + network: Network, outgoing: SatPoint, - outputs: Vec<(Address, Amount)>, - recipient: Address, + outputs: Vec<(OutputType, Amount)>, + recipient: ScriptBuf, runic_utxos: BTreeSet, target: Target, unused_change_addresses: Vec
, @@ -122,10 +145,11 @@ impl TransactionBuilder { amounts: BTreeMap, locked_utxos: BTreeSet, runic_utxos: BTreeSet, - recipient: Address, + recipient: ScriptBuf, change: [Address; 2], fee_rate: FeeRate, target: Target, + network: Network ) -> Self { Self { utxos: amounts.keys().cloned().collect(), @@ -141,6 +165,7 @@ impl TransactionBuilder { runic_utxos, target, unused_change_addresses: change.to_vec(), + network } } @@ -151,22 +176,29 @@ impl TransactionBuilder { )); } - if self.change_addresses.contains(&self.recipient) { - return Err(Error::DuplicateAddress(self.recipient)); - } + if !self.recipient.is_op_return() { + let recipient_as_address = Address::from_script( + &self.recipient.as_script(), + self.network + )?; - match self.target { - Target::Value(output_value) | Target::ExactPostage(output_value) => { - let dust_value = self.recipient.script_pubkey().dust_value(); + if self.change_addresses.contains(&recipient_as_address) { + return Err(Error::DuplicateAddress(recipient_as_address)); + } - if output_value < dust_value { - return Err(Error::Dust { - output_value, - dust_value, - }); + match self.target { + Target::Value(output_value) | Target::ExactPostage(output_value) => { + let dust_value = self.recipient.dust_value(); + + if output_value < dust_value { + return Err(Error::Dust { + output_value, + dust_value, + }); + } } + _ => (), } - _ => (), } self @@ -215,7 +247,14 @@ impl TransactionBuilder { self.inputs.push(self.outgoing.outpoint); self .outputs - .push((self.recipient.clone(), Amount::from_sat(amount))); + .push( + if self.recipient.is_op_return() { + (OutputType::ScriptBuf(self.recipient.clone()), Amount::from_sat(amount)) + } else { + (OutputType::Address(Address::from_script(&self.recipient.as_script(), self.network)?), + Amount::from_sat(amount)) + } + ); tprintln!( "selected outgoing outpoint {} with value {}", @@ -230,7 +269,7 @@ impl TransactionBuilder { assert_eq!(self.outputs.len(), 1, "invariant: only one output"); assert_eq!( - self.outputs[0].0, self.recipient, + self.outputs[0].0.script_pubkey(), self.recipient, "invariant: first output is recipient" ); @@ -243,10 +282,12 @@ impl TransactionBuilder { self.outputs.insert( 0, ( - self - .unused_change_addresses - .pop() - .expect("not enough change addresses"), + OutputType::Address( + self + .unused_change_addresses + .pop() + .expect("not enough change addresses") + ), Amount::from_sat(sat_offset), ), ); @@ -257,7 +298,7 @@ impl TransactionBuilder { } fn pad_alignment_output(mut self) -> Result { - if self.outputs[0].0 == self.recipient { + if self.outputs[0].0.script_pubkey() == self.recipient { tprintln!("no alignment output"); } else { let dust_limit = self @@ -343,7 +384,7 @@ impl TransactionBuilder { self .outputs .iter() - .find(|(address, _amount)| address == &self.recipient) + .find(|(address, _amount)| &address.script_pubkey() == &self.recipient) .expect("couldn't find output that contains the index"); let value = total_output_amount - Amount::from_sat(sat_offset); @@ -370,10 +411,12 @@ impl TransactionBuilder { tprintln!("stripped {} sats", (value - target).to_sat()); self.outputs.last_mut().expect("no outputs found").1 = target; self.outputs.push(( - self - .unused_change_addresses - .pop() - .expect("not enough change addresses"), + OutputType::Address( + self + .unused_change_addresses + .pop() + .expect("not enough change addresses") + ), value - target, )); } @@ -425,13 +468,13 @@ impl TransactionBuilder { self .outputs .iter() - .map(|(address, _amount)| address) + .map(|(output, _amount)| output) .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, @@ -445,9 +488,15 @@ impl TransactionBuilder { .collect(), output: outputs .into_iter() - .map(|address| TxOut { - value: 0, - script_pubkey: address.script_pubkey(), + .map(|output_type| match output_type { + OutputType::Address(addr) => TxOut { + value: 0, + script_pubkey: addr.script_pubkey(), + }, + OutputType::ScriptBuf(script) => TxOut { + value: 0, + script_pubkey: script, + }, }) .collect(), } @@ -459,7 +508,6 @@ impl TransactionBuilder { } fn build(self) -> Result { - let recipient = self.recipient.script_pubkey(); let transaction = Transaction { version: 2, lock_time: LockTime::ZERO, @@ -523,7 +571,7 @@ impl TransactionBuilder { output_end += tx_out.value; if output_end > sat_offset { assert_eq!( - tx_out.script_pubkey, recipient, + tx_out.script_pubkey, self.recipient, "invariant: outgoing sat is sent to recipient" ); found = true; @@ -536,7 +584,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", @@ -557,7 +605,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 { @@ -743,6 +791,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .select_outgoing() .unwrap(); @@ -756,7 +805,7 @@ mod tests { assert_eq!( tx_builder.outputs, [( - recipient(), + recipient_address_as_output_type(), Amount::from_sat(100 * COIN_VALUE - 51 * COIN_VALUE) )] ) @@ -782,11 +831,12 @@ mod tests { 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_address_as_output_type(), Amount::from_sat(5_000)), + (change_as_output_type(0), Amount::from_sat(5_000)), + (change_as_output_type(1), Amount::from_sat(1_724)), ], target: Target::Postage, + network: Network::Testnet, }; pretty_assert_eq!( @@ -796,7 +846,7 @@ mod tests { lock_time: LockTime::ZERO, input: vec![tx_in(outpoint(1)), tx_in(outpoint(2)), tx_in(outpoint(3))], output: vec![ - tx_out(5_000, recipient()), + tx_out(5_000, recipient_as_address()), tx_out(5_000, change(0)), tx_out(1_724, change(1)) ], @@ -818,6 +868,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .build_transaction() .unwrap() @@ -839,13 +890,14 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .build_transaction(), Ok(Transaction { version: 2, lock_time: LockTime::ZERO, input: vec![tx_in(outpoint(1))], - output: vec![tx_out(4901, recipient())], + output: vec![tx_out(4901, recipient_as_address())], }) ) } @@ -865,6 +917,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .select_outgoing() .unwrap() @@ -891,13 +944,14 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .build_transaction(), Ok(Transaction { version: 2, lock_time: LockTime::ZERO, input: vec![tx_in(outpoint(1)), tx_in(outpoint(2))], - output: vec![tx_out(4_950, change(1)), tx_out(4_862, recipient())], + output: vec![tx_out(4_950, change(1)), tx_out(4_862, recipient_as_address())], }) ) } @@ -917,6 +971,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .build_transaction(), Err(Error::NotEnoughCardinalUtxos), @@ -940,7 +995,8 @@ mod tests { recipient(), [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), - Target::Postage + Target::Postage, + Network::Testnet, ) .build_transaction(), Err(Error::NotEnoughCardinalUtxos), @@ -965,6 +1021,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .build_transaction(), Ok(Transaction { @@ -973,7 +1030,7 @@ mod tests { input: vec![tx_in(outpoint(1)), tx_in(outpoint(2))], output: vec![ tx_out(4_950, change(1)), - tx_out(TARGET_POSTAGE.to_sat(), recipient()), + tx_out(TARGET_POSTAGE.to_sat(), recipient_as_address()), tx_out(14_831, change(0)), ], }) @@ -995,6 +1052,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .build() .unwrap(); @@ -1015,6 +1073,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .build() .unwrap(); @@ -1035,6 +1094,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .build() .unwrap(); @@ -1055,14 +1115,17 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .select_outgoing() .unwrap(); - builder.outputs[0].0 = "tb1qx4gf3ya0cxfcwydpq8vr2lhrysneuj5d7lqatw" - .parse::>() - .unwrap() - .assume_checked(); + builder.outputs[0].0 = OutputType::Address( + "tb1qx4gf3ya0cxfcwydpq8vr2lhrysneuj5d7lqatw" + .parse::>() + .unwrap() + .assume_checked() + ); builder.build().unwrap(); } @@ -1082,6 +1145,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .select_outgoing() .unwrap(); @@ -1106,6 +1170,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .build_transaction(), Ok(Transaction { @@ -1113,7 +1178,7 @@ mod tests { lock_time: LockTime::ZERO, input: vec![tx_in(outpoint(1))], output: vec![ - tx_out(TARGET_POSTAGE.to_sat(), recipient()), + tx_out(TARGET_POSTAGE.to_sat(), recipient_as_address()), tx_out(989_870, change(1)) ], }) @@ -1135,6 +1200,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .select_outgoing() .unwrap() @@ -1157,13 +1223,14 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .build_transaction(), Ok(Transaction { version: 2, lock_time: LockTime::ZERO, input: vec![tx_in(outpoint(1))], - output: vec![tx_out(3_333, change(1)), tx_out(6_537, recipient())], + output: vec![tx_out(3_333, change(1)), tx_out(6_537, recipient_as_address())], }) ) } @@ -1186,13 +1253,14 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .build_transaction(), Ok(Transaction { version: 2, lock_time: LockTime::ZERO, input: vec![tx_in(outpoint(2)), tx_in(outpoint(1))], - output: vec![tx_out(10_001, change(1)), tx_out(9_811, recipient())], + output: vec![tx_out(10_001, change(1)), tx_out(9_811, recipient_as_address())], }) ) } @@ -1212,6 +1280,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .select_outgoing() .unwrap() @@ -1241,6 +1310,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .select_outgoing() .unwrap() @@ -1268,6 +1338,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .select_outgoing() .unwrap() @@ -1292,6 +1363,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet, ) .select_outgoing() .unwrap() @@ -1321,11 +1393,12 @@ mod tests { 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_address_as_output_type(), Amount::from_sat(5_000)), + (recipient_address_as_output_type(), Amount::from_sat(5_000)), + (change_as_output_type(1), Amount::from_sat(1_774)), ], target: Target::Postage, + network: Network::Testnet, } .build() .unwrap(); @@ -1352,11 +1425,12 @@ mod tests { 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_address_as_output_type(), Amount::from_sat(5_000)), + (change_as_output_type(0), Amount::from_sat(5_000)), + (change_as_output_type(0), Amount::from_sat(1_774)), ], target: Target::Postage, + network: Network::Testnet, } .build() .unwrap(); @@ -1380,6 +1454,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet ) .build_transaction(), Err(Error::NotEnoughCardinalUtxos) @@ -1404,6 +1479,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet ) .build_transaction(), Err(Error::NotEnoughCardinalUtxos) @@ -1425,6 +1501,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet ) .build_transaction(), Err(Error::UtxoContainsAdditionalInscriptions { @@ -1451,6 +1528,7 @@ mod tests { [change(0), change(1)], fee_rate, Target::Postage, + Network::Testnet, ) .build_transaction() .unwrap(); @@ -1464,7 +1542,7 @@ mod tests { version: 2, lock_time: LockTime::ZERO, input: vec![tx_in(outpoint(1))], - output: vec![tx_out(10_000 - fee.to_sat(), recipient())], + output: vec![tx_out(10_000 - fee.to_sat(), recipient_as_address())], } ) } @@ -1483,14 +1561,15 @@ 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)), + Network::Testnet, ) .build_transaction(), Ok(Transaction { version: 2, lock_time: LockTime::ZERO, input: vec![tx_in(outpoint(1))], - output: vec![tx_out(1000, recipient()), tx_out(3870, change(1))], + output: vec![tx_out(1000, recipient_as_address()), tx_out(3870, change(1))], }) ) } @@ -1512,14 +1591,15 @@ 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)), + Network::Testnet ) .build_transaction(), Ok(Transaction { version: 2, lock_time: LockTime::ZERO, input: vec![tx_in(outpoint(1)), tx_in(outpoint(2))], - output: vec![tx_out(1500, recipient()), tx_out(312, change(1))], + output: vec![tx_out(1500, recipient_as_address()), tx_out(312, change(1))], }) ) } @@ -1538,7 +1618,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)), + Network::Testnet ) .build_transaction(), Err(Error::Dust { @@ -1565,7 +1646,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)), + Network::Testnet ) .build_transaction(), Err(Error::NotEnoughCardinalUtxos), @@ -1589,7 +1671,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)), + Network::Testnet, ) .build_transaction(), Err(Error::NotEnoughCardinalUtxos), @@ -1609,10 +1692,12 @@ mod tests { let after = TransactionBuilder::estimate_vbytes_with( 0, vec![ - "bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k" - .parse::>() - .unwrap() - .assume_checked(), + OutputType::Address( + "bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k" + .parse::>() + .unwrap() + .assume_checked() + ), ], ); assert_eq!(after - before, TransactionBuilder::ADDITIONAL_OUTPUT_VBYTES); @@ -1632,14 +1717,15 @@ 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)), + Network::Testnet ) .build_transaction(), Ok(Transaction { version: 2, lock_time: LockTime::ZERO, input: vec![tx_in(outpoint(1))], - output: vec![tx_out(901, recipient())], + output: vec![tx_out(901, recipient_as_address())], }), ); } @@ -1659,13 +1745,14 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Network::Testnet ) .build_transaction(), Ok(Transaction { version: 2, lock_time: LockTime::ZERO, input: vec![tx_in(outpoint(1))], - output: vec![tx_out(20_000, recipient())], + output: vec![tx_out(20_000, recipient_as_address())], }), ); } @@ -1684,14 +1771,15 @@ 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)), + Network::Testnet ) .build_transaction(), Ok(Transaction { version: 2, lock_time: LockTime::ZERO, input: vec![tx_in(outpoint(1))], - output: vec![tx_out(1005, recipient())], + output: vec![tx_out(1005, recipient_as_address())], }), ); } @@ -1710,7 +1798,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)), + Network::Testnet ) .build_transaction(), Err(Error::NotEnoughCardinalUtxos) @@ -1729,12 +1818,13 @@ mod tests { BTreeSet::new(), BTreeSet::new(), recipient(), - [recipient(), change(1)], + [recipient_as_address(), change(1)], FeeRate::try_from(0.0).unwrap(), - Target::Value(Amount::from_sat(1000)) + Target::Value(Amount::from_sat(1000)), + Network::Testnet ) .build_transaction(), - Err(Error::DuplicateAddress(recipient())) + Err(Error::DuplicateAddress(recipient_as_address())) ); } @@ -1752,7 +1842,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)), + Network::Testnet ) .build_transaction(), Err(Error::DuplicateAddress(change(0))) @@ -1773,14 +1864,15 @@ 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)), + Network::Testnet ) .build_transaction(), Ok(Transaction { version: 2, lock_time: LockTime::ZERO, input: vec![tx_in(outpoint(1))], - output: vec![tx_out(1802, recipient())], + output: vec![tx_out(1802, recipient_as_address())], }), ); } @@ -1800,13 +1892,14 @@ mod tests { [change(0), change(1)], FeeRate::try_from(250.0).unwrap(), Target::Postage, + Network::Testnet ) .build_transaction(), Ok(Transaction { version: 2, lock_time: LockTime::ZERO, input: vec![tx_in(outpoint(1))], - output: vec![tx_out(20250, recipient())], + output: vec![tx_out(20250, recipient_as_address())], }), ); } @@ -1832,6 +1925,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), + Network::Testnet ) .select_outgoing() .unwrap() @@ -1852,7 +1946,7 @@ 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_address_as_output_type(), Amount::from_sat(3_003 + 3_006 + 3_005 + 3_001))] ) } @@ -1878,6 +1972,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), + Network::Testnet ) .select_outgoing() .unwrap() @@ -1900,8 +1995,8 @@ mod tests { assert_eq!( tx_builder.outputs, [ - (change(1), Amount::from_sat(101 + 104 + 105 + 1)), - (recipient(), Amount::from_sat(19_999)) + (change_as_output_type(1), Amount::from_sat(101 + 104 + 105 + 1)), + (recipient_address_as_output_type(), Amount::from_sat(19_999)) ] ) } @@ -1931,6 +2026,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), + Network::Testnet ); assert_eq!( @@ -1987,6 +2083,7 @@ mod tests { [change(0), change(1)], fee_rate, Target::ExactPostage(Amount::from_sat(66_000)), + Network::Testnet ) .build_transaction() .unwrap(); @@ -2001,7 +2098,7 @@ mod tests { lock_time: LockTime::ZERO, input: vec![tx_in(outpoint(1))], output: vec![ - tx_out(66_000, recipient()), + tx_out(66_000, recipient_as_address()), tx_out(1_000_000 - 66_000 - fee.to_sat(), change(1)) ], } @@ -2023,6 +2120,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), + Network::Testnet ); assert_eq!( @@ -2049,6 +2147,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), + Network::Testnet ); assert_eq!( @@ -2074,6 +2173,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), + Network::Testnet ); assert_eq!( @@ -2099,6 +2199,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), + Network::Testnet ); assert_eq!( diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index 45b76caab2..5a0147a240 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -47,6 +47,58 @@ fn inscriptions_can_be_sent() { ); } +#[test] +fn inscriptions_can_be_burned() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &[], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let (inscription, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let output = CommandBuilder::new(format!( + "wallet send --fee-rate 1 burn {inscription}", + )) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .stdout_regex(r".*") + .run_and_deserialize_output::(); + + let txid = bitcoin_rpc_server.mempool()[0].txid(); + assert_eq!(txid, output.txid); + + bitcoin_rpc_server.mine_blocks(1); + + let send_txid = output.txid; + + ord_rpc_server.assert_response_regex( + format!("/inscription/{inscription}"), + format!( + ".*

Inscription 0

.*
.* +
charms
+
+ 💀🔥 +
+ .* +
content length
+
3 bytes
+
content type
+
text/plain;charset=utf-8
+ .* +
location
+
{send_txid}:0:0
+ .* +
+.*", + ), + ); +} + #[test] fn send_unknown_inscription() { let core = mockcore::spawn();