From 70aa2c11987fde59ce84955a36bc8bb7426cf893 Mon Sep 17 00:00:00 2001 From: onchainguy-eth <1436535+onchainguy-eth@users.noreply.github.com> Date: Tue, 28 Nov 2023 23:45:10 +0100 Subject: [PATCH 1/9] implement burn --- src/subcommand/server.rs | 28 ++ src/subcommand/wallet.rs | 2 +- src/subcommand/wallet/send.rs | 65 +++- src/subcommand/wallet/transaction_builder.rs | 320 ++++++++++++------- src/templates/inscription.rs | 37 +++ templates/inscription.html | 10 + tests/json_api.rs | 2 + tests/server.rs | 2 +- tests/wallet/inscriptions.rs | 22 +- tests/wallet/send.rs | 223 ++++++++----- 10 files changed, 478 insertions(+), 233 deletions(-) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 4e1a028b0e..b0c72fe73f 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_RETURN, rust_embed::RustEmbed, rustls_acme::{ acme::{LETS_ENCRYPT_PRODUCTION_DIRECTORY, LETS_ENCRYPT_STAGING_DIRECTORY}, @@ -1242,11 +1246,32 @@ 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() { + 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 + } + }); + 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 +1293,12 @@ impl Server { previous, next, rune, + burn_payload }) .into_response() } else { InscriptionHtml { + burn_payload, chain: server_config.chain, charms, children, @@ -1280,6 +1307,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/send.rs b/src/subcommand/wallet/send.rs index 80a60c6584..ab9be3d3db 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -1,14 +1,32 @@ -use {super::*, crate::subcommand::wallet::transaction_builder::Target, crate::wallet::Wallet}; +use { + super::*, + crate::{ + subcommand::wallet::transaction_builder::{OutputScript, 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( - long, - help = "Target amount of postage to include with sent inscriptions. Default `10000sat`" + long, + help = "Target amount of postage to include with sent inscriptions. Default `10000sat`" )] pub(crate) postage: Option, } @@ -20,10 +38,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()?; @@ -44,15 +59,24 @@ impl Send { index.get_runic_outputs(&unspent_outputs.keys().cloned().collect::>())?; let satpoint = match self.outgoing { - Outgoing::Amount(amount) => { - 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 })); - } + Outgoing::Amount(amount) => match output { + OutputScript::OpReturn(_) => bail!("refusing to burn amount"), + OutputScript::PubKey(address) => { + Self::lock_non_cardinal_outputs(&client, &inscriptions, &runic_outputs, unspent_outputs)?; + 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,12 +124,12 @@ impl Send { unspent_outputs, locked_outputs, runic_outputs, - address.clone(), + output, change, self.fee_rate, postage, ) - .build_transaction()?; + .build_transaction()?; let signed_tx = client .sign_raw_transaction_with_wallet(&unsigned_transaction, None, None)? @@ -116,6 +140,17 @@ 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(OutputScript::PubKey(address)) + } else if let Some(msg) = &self.burn { + Ok(OutputScript::OpReturn(Vec::from(msg.clone()))) + } 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..7cf6606104 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -33,6 +33,11 @@ use { super::*, + bitcoin::{ + blockdata::{locktime::absolute::LockTime, witness::Witness}, + script::PushBytesBuf, + Amount, ScriptBuf, + }, std::cmp::{max, min}, }; @@ -61,6 +66,33 @@ pub enum Target { ExactPostage(Amount), } +#[derive(Clone, Debug, PartialEq)] +pub enum OutputScript { + PubKey(Address), + OpReturn(Vec), +} + +impl OutputScript { + fn script(&self) -> ScriptBuf { + match self { + OutputScript::PubKey(address) => address.script_pubkey(), + OutputScript::OpReturn(data) => ScriptBuf::new_op_return( + &PushBytesBuf::try_from(data.clone()).expect("burn payload too large"), + ), + } + } + + fn dust_value(&self) -> Amount { + self.script().dust_value() + } +} + +impl From
for OutputScript { + fn from(value: Address) -> Self { + OutputScript::PubKey(value) + } +} + impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -99,8 +131,8 @@ pub struct TransactionBuilder { inscriptions: BTreeMap, locked_utxos: BTreeSet, outgoing: SatPoint, - outputs: Vec<(Address, Amount)>, - recipient: Address, + outputs: Vec<(OutputScript, Amount)>, + recipient: OutputScript, runic_utxos: BTreeSet, target: Target, unused_change_addresses: Vec
, @@ -115,13 +147,13 @@ 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, @@ -136,7 +168,7 @@ impl TransactionBuilder { locked_utxos, outgoing, outputs: Vec::new(), - recipient, + recipient: recipient.into(), runic_utxos, target, unused_change_addresses: change.to_vec(), @@ -150,13 +182,15 @@ impl TransactionBuilder { )); } - if self.change_addresses.contains(&self.recipient) { - return Err(Error::DuplicateAddress(self.recipient)); + if let OutputScript::PubKey(address) = &self.recipient { + if self.change_addresses.contains(address) { + return Err(Error::DuplicateAddress(address.clone())); + } } 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 +276,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 +322,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, }; @@ -352,15 +387,15 @@ impl TransactionBuilder { if excess > max && value.checked_sub(target).unwrap() - > self - .unused_change_addresses - .last() - .unwrap() - .script_pubkey() - .dust_value() - + self - .fee_rate - .fee(self.estimate_vbytes() + Self::ADDITIONAL_OUTPUT_VBYTES) + > self + .unused_change_addresses + .last() + .unwrap() + .script_pubkey() + .dust_value() + + self + .fee_rate + .fee(self.estimate_vbytes() + Self::ADDITIONAL_OUTPUT_VBYTES) { tprintln!("stripped {} sats", (value - target).to_sat()); self.outputs.last_mut().expect("no outputs found").1 = target; @@ -368,7 +403,8 @@ impl TransactionBuilder { self .unused_change_addresses .pop() - .expect("not enough change addresses"), + .expect("not enough change addresses") + .into(), value - target, )); } @@ -420,13 +456,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,13 +476,13 @@ impl TransactionBuilder { .collect(), output: outputs .into_iter() - .map(|address| TxOut { + .map(|recipient| TxOut { value: 0, - script_pubkey: address.script_pubkey(), + script_pubkey: recipient.script(), }) .collect(), } - .vsize() + .vsize() } fn estimate_fee(&self) -> Amount { @@ -454,7 +490,7 @@ impl TransactionBuilder { } fn build(self) -> Result { - let recipient = self.recipient.script_pubkey(); + let recipient = self.recipient.script(); let transaction = Transaction { version: 2, lock_time: LockTime::ZERO, @@ -471,9 +507,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.script(), }) .collect(), }; @@ -531,7 +567,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.script()) .count(), 1, "invariant: recipient address appears exactly once in outputs", @@ -552,7 +588,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.script() { let slop = self.fee_rate.fee(Self::ADDITIONAL_OUTPUT_VBYTES); match self.target { @@ -572,12 +608,12 @@ impl TransactionBuilder { assert!( Amount::from_sat(output.value).checked_sub(value).unwrap() <= self - .change_addresses - .iter() - .map(|address| address.script_pubkey().dust_value()) - .max() - .unwrap_or_default() - + slop, + .change_addresses + .iter() + .map(|address| address.script_pubkey().dust_value()) + .max() + .unwrap_or_default() + + slop, "invariant: output equals target value", ); } @@ -739,8 +775,8 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap(); + .select_outgoing() + .unwrap(); utxos.remove(1); assert_eq!( @@ -751,7 +787,7 @@ mod tests { assert_eq!( tx_builder.outputs, [( - recipient(), + recipient().into(), Amount::from_sat(100 * COIN_VALUE - 51 * COIN_VALUE) )] ) @@ -772,14 +808,14 @@ 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, }; @@ -814,9 +850,9 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .build_transaction() - .unwrap() - .is_explicitly_rbf()) + .build_transaction() + .unwrap() + .is_explicitly_rbf()) } #[test] @@ -861,11 +897,11 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap() - .align_outgoing() - .strip_value() - .deduct_fee(); + .select_outgoing() + .unwrap() + .align_outgoing() + .strip_value() + .deduct_fee(); } #[test] @@ -991,8 +1027,8 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .build() - .unwrap(); + .build() + .unwrap(); } #[test] @@ -1011,8 +1047,8 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .build() - .unwrap(); + .build() + .unwrap(); } #[test] @@ -1031,8 +1067,8 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .build() - .unwrap(); + .build() + .unwrap(); } #[test] @@ -1051,13 +1087,14 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap(); + .select_outgoing() + .unwrap(); builder.outputs[0].0 = "tb1qx4gf3ya0cxfcwydpq8vr2lhrysneuj5d7lqatw" .parse::>() .unwrap() - .assume_checked(); + .assume_checked() + .into(); builder.build().unwrap(); } @@ -1078,8 +1115,8 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap(); + .select_outgoing() + .unwrap(); builder.outputs[0].1 = Amount::from_sat(0); @@ -1131,10 +1168,10 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap() - .build() - .unwrap(); + .select_outgoing() + .unwrap() + .build() + .unwrap(); } #[test] @@ -1208,13 +1245,13 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap() - .align_outgoing() - .add_value() - .unwrap() - .strip_value() - .deduct_fee(); + .select_outgoing() + .unwrap() + .align_outgoing() + .add_value() + .unwrap() + .strip_value() + .deduct_fee(); builder.change_addresses = BTreeSet::new(); @@ -1237,15 +1274,15 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap() - .align_outgoing() - .add_value() - .unwrap() - .strip_value() - .deduct_fee() - .build() - .unwrap(); + .select_outgoing() + .unwrap() + .align_outgoing() + .add_value() + .unwrap() + .strip_value() + .deduct_fee() + .build() + .unwrap(); } #[test] @@ -1264,12 +1301,12 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap() - .strip_value() - .deduct_fee() - .build() - .unwrap(); + .select_outgoing() + .unwrap() + .strip_value() + .deduct_fee() + .build() + .unwrap(); } #[test] @@ -1288,11 +1325,11 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap() - .strip_value() - .build() - .unwrap(); + .select_outgoing() + .unwrap() + .strip_value() + .build() + .unwrap(); } #[test] @@ -1311,19 +1348,19 @@ 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, } - .build() - .unwrap(); + .build() + .unwrap(); } #[test] @@ -1342,19 +1379,19 @@ 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, } - .build() - .unwrap(); + .build() + .unwrap(); } #[test] @@ -1447,8 +1484,8 @@ mod tests { fee_rate, Target::Postage, ) - .build_transaction() - .unwrap(); + .build_transaction() + .unwrap(); let fee = fee_rate.fee(transaction.vsize() + TransactionBuilder::SCHNORR_SIGNATURE_SIZE / 4 + 1); @@ -1607,7 +1644,8 @@ mod tests { "bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k" .parse::>() .unwrap() - .assume_checked(), + .assume_checked() + .into(), ], ); assert_eq!(after - before, TransactionBuilder::ADDITIONAL_OUTPUT_VBYTES); @@ -1828,10 +1866,10 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), ) - .select_outgoing() - .unwrap() - .add_value() - .unwrap(); + .select_outgoing() + .unwrap() + .add_value() + .unwrap(); utxos.remove(4); utxos.remove(3); @@ -1847,7 +1885,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) + )] ) } @@ -1874,11 +1915,11 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), ) - .select_outgoing() - .unwrap() - .align_outgoing() - .pad_alignment_output() - .unwrap(); + .select_outgoing() + .unwrap() + .align_outgoing() + .pad_alignment_output() + .unwrap(); utxos.remove(5); utxos.remove(2); @@ -1895,8 +1936,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)) ] ) } @@ -1983,8 +2024,8 @@ mod tests { fee_rate, Target::ExactPostage(Amount::from_sat(66_000)), ) - .build_transaction() - .unwrap(); + .build_transaction() + .unwrap(); let fee = fee_rate.fee(transaction.vsize() + TransactionBuilder::SCHNORR_SIGNATURE_SIZE / 4 + 1); @@ -2110,4 +2151,39 @@ 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(); + + pretty_assert_eq!( + TransactionBuilder::new( + satpoint(1, 0), + BTreeMap::new(), + utxos.into_iter().collect(), + BTreeSet::new(), + OutputScript::OpReturn(msg.clone()), + [change(0), change(1)], + FeeRate::try_from(1.0).unwrap(), + Target::Value(Amount::from_sat(1000)) + ) + .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( + &PushBytesBuf::try_from(msg.clone()).expect("burn message should fit") + ) + }, + tx_out(3879, change(1)) + ], + }) + ) + } } diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index 4faff866ab..c5a24a954a 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,7 @@ pub(crate) struct InscriptionHtml { #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct InscriptionJson { pub address: Option, + pub burn_payload: Option, pub children: Vec, pub content_length: Option, pub content_type: Option, @@ -30,6 +33,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 +461,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..e3fc04b829 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/tests/json_api.rs b/tests/json_api.rs index ab3dfb8e8e..c379294d9b 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), 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..7232ba2e0b 100644 --- a/tests/wallet/inscriptions.rs +++ b/tests/wallet/inscriptions.rs @@ -29,14 +29,14 @@ 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) - .expected_exit_code(0) - .stdout_regex(".*") - .run_and_deserialize_output::() - .transaction; + .rpc_server(&rpc_server) + .expected_exit_code(0) + .stdout_regex(".*") + .run_and_deserialize_output::() + .transaction; rpc_server.mine_blocks(1); @@ -94,13 +94,13 @@ 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) - .expected_exit_code(0) - .stdout_regex(".*") - .run_and_extract_stdout(); + .rpc_server(&rpc_server) + .expected_exit_code(0) + .stdout_regex(".*") + .run_and_extract_stdout(); rpc_server.mine_blocks(1); diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index cdbe0c22b0..c04784f959 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -11,11 +11,11 @@ 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".*") - .run_and_deserialize_output::(); + .rpc_server(&rpc_server) + .stdout_regex(r".*") + .run_and_deserialize_output::(); let txid = rpc_server.mempool()[0].txid(); assert_eq!(txid, output.transaction); @@ -51,12 +51,12 @@ 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")) - .expected_exit_code(1) - .run_and_extract_stdout(); + .rpc_server(&rpc_server) + .expected_stderr(format!("error: inscription {txid}i0 not found\n")) + .expected_exit_code(1) + .run_and_extract_stdout(); } #[test] @@ -70,10 +70,10 @@ 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::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); rpc_server.mine_blocks(1); @@ -98,10 +98,10 @@ 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::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); } #[test] @@ -111,14 +111,14 @@ 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( - "error: address tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz belongs to network testnet which is different from required bitcoin\n", - ) - .expected_exit_code(1) - .run_and_extract_stdout(); + .rpc_server(&rpc_server) + .expected_stderr( + "error: address tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz belongs to network testnet which is different from required bitcoin\n", + ) + .expected_exit_code(1) + .run_and_extract_stdout(); } #[test] @@ -128,10 +128,10 @@ 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::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); assert_eq!(rpc_server.mempool()[0].txid(), output.transaction); } @@ -145,18 +145,18 @@ 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) - .expected_stderr("error: wallet does not contain enough cardinal UTXOs, please add additional funds to wallet.\n") - .run_and_extract_stdout(); + .rpc_server(&rpc_server) + .expected_exit_code(1) + .expected_stderr("error: wallet does not contain enough cardinal UTXOs, please add additional funds to wallet.\n") + .run_and_extract_stdout(); } #[test] @@ -174,14 +174,14 @@ fn do_not_send_within_dust_limit_of_an_inscription() { }; CommandBuilder::new(format!( - "wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {output}:329" - )) - .rpc_server(&rpc_server) - .expected_exit_code(1) - .expected_stderr(format!( - "error: cannot send {output}:329 without also sending inscription {inscription} at {output}:0\n" + "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {output}:329" )) - .run_and_extract_stdout(); + .rpc_server(&rpc_server) + .expected_exit_code(1) + .expected_stderr(format!( + "error: cannot send {output}:329 without also sending inscription {inscription} at {output}:0\n" + )) + .run_and_extract_stdout(); } #[test] @@ -199,10 +199,10 @@ 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::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); } #[test] @@ -266,43 +266,43 @@ 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) - .expected_exit_code(1) - .expected_stderr(format!( - "error: cannot send {reveal_txid}:0:0 without also sending inscription {reveal_txid}i2 at {reveal_txid}:0:{}\n", 100 * COIN_VALUE - )) - .run_and_extract_stdout(); + .rpc_server(&rpc_server) + .expected_exit_code(1) + .expected_stderr(format!( + "error: cannot send {reveal_txid}:0:0 without also sending inscription {reveal_txid}i2 at {reveal_txid}:0:{}\n", 100 * COIN_VALUE + )) + .run_and_extract_stdout(); // 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) - .run_and_deserialize_output::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); rpc_server.mine_blocks(1); // 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) - .run_and_deserialize_output::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); rpc_server.mine_blocks(1); // 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) - .run_and_deserialize_output::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); } #[test] @@ -315,12 +315,12 @@ 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") - .expected_exit_code(1) - .run_and_extract_stdout(); + .rpc_server(&rpc_server) + .expected_stderr("error: inscriptions must be sent by inscription ID\n") + .expected_exit_code(1) + .run_and_extract_stdout(); } #[test] @@ -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,7 +374,9 @@ fn send_btc_locks_inscriptions() { let (_, reveal) = inscribe(&rpc_server); - CommandBuilder::new("wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc") + CommandBuilder::new( + "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc", + ) .rpc_server(&rpc_server) .run_and_deserialize_output::(); @@ -403,7 +405,9 @@ fn send_btc_fails_if_lock_unspent_fails() { rpc_server.mine_blocks(1); - CommandBuilder::new("wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc") + 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) @@ -419,10 +423,10 @@ 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::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); let tx = &rpc_server.mempool()[0]; let mut fee = 0; @@ -450,15 +454,15 @@ 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) - .stderr_regex( - ".*error: the following required arguments were not provided: + .rpc_server(&rpc_server) + .expected_exit_code(2) + .stderr_regex( + ".*error: the following required arguments were not provided: .*--fee-rate .*", - ) - .run_and_extract_stdout(); + ) + .run_and_extract_stdout(); } #[test] @@ -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,7 +507,9 @@ fn send_btc_does_not_send_locked_utxos() { rpc_server.lock(outpoint); - CommandBuilder::new("wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc") + CommandBuilder::new( + "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc", + ) .rpc_server(&rpc_server) .expected_exit_code(1) .stderr_regex("error:.*") @@ -953,3 +959,54 @@ 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" + ); +} From 8c90acd6cdb8e29c5c26f89c4a5f251751d00c22 Mon Sep 17 00:00:00 2001 From: onchainguy-eth <1436535+onchainguy-eth@users.noreply.github.com> Date: Fri, 1 Dec 2023 08:41:24 +0100 Subject: [PATCH 2/9] Get rid of whitespace changes --- src/subcommand/wallet/send.rs | 18 +- src/subcommand/wallet/transaction_builder.rs | 164 +++++++++---------- tests/wallet/inscriptions.rs | 18 +- tests/wallet/send.rs | 142 ++++++++-------- 4 files changed, 171 insertions(+), 171 deletions(-) diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index ab9be3d3db..ab3c6d306f 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -8,25 +8,25 @@ use { #[derive(Debug, Parser, Clone)] #[clap( -group = ArgGroup::new("output") -.required(true) -.args(&["address", "burn"]), + group = ArgGroup::new("output") + .required(true) + .args(&["address", "burn"]), )] pub(crate) struct Send { outgoing: Outgoing, #[arg(long, conflicts_with = "burn", help = "Recipient address")] address: Option>, #[arg( - long, - conflicts_with = "address", - help = "Message to append when burning sats" + 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( - long, - help = "Target amount of postage to include with sent inscriptions. Default `10000sat`" + long, + help = "Target amount of postage to include with sent inscriptions. Default `10000sat`" )] pub(crate) postage: Option, } @@ -129,7 +129,7 @@ impl Send { self.fee_rate, postage, ) - .build_transaction()?; + .build_transaction()?; let signed_tx = client .sign_raw_transaction_with_wallet(&unsigned_transaction, None, None)? diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index 7cf6606104..edfed813bd 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -387,15 +387,15 @@ impl TransactionBuilder { if excess > max && value.checked_sub(target).unwrap() - > self - .unused_change_addresses - .last() - .unwrap() - .script_pubkey() - .dust_value() - + self - .fee_rate - .fee(self.estimate_vbytes() + Self::ADDITIONAL_OUTPUT_VBYTES) + > self + .unused_change_addresses + .last() + .unwrap() + .script_pubkey() + .dust_value() + + self + .fee_rate + .fee(self.estimate_vbytes() + Self::ADDITIONAL_OUTPUT_VBYTES) { tprintln!("stripped {} sats", (value - target).to_sat()); self.outputs.last_mut().expect("no outputs found").1 = target; @@ -482,7 +482,7 @@ impl TransactionBuilder { }) .collect(), } - .vsize() + .vsize() } fn estimate_fee(&self) -> Amount { @@ -608,12 +608,12 @@ impl TransactionBuilder { assert!( Amount::from_sat(output.value).checked_sub(value).unwrap() <= self - .change_addresses - .iter() - .map(|address| address.script_pubkey().dust_value()) - .max() - .unwrap_or_default() - + slop, + .change_addresses + .iter() + .map(|address| address.script_pubkey().dust_value()) + .max() + .unwrap_or_default() + + slop, "invariant: output equals target value", ); } @@ -775,8 +775,8 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap(); + .select_outgoing() + .unwrap(); utxos.remove(1); assert_eq!( @@ -850,9 +850,9 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .build_transaction() - .unwrap() - .is_explicitly_rbf()) + .build_transaction() + .unwrap() + .is_explicitly_rbf()) } #[test] @@ -897,11 +897,11 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap() - .align_outgoing() - .strip_value() - .deduct_fee(); + .select_outgoing() + .unwrap() + .align_outgoing() + .strip_value() + .deduct_fee(); } #[test] @@ -1027,8 +1027,8 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .build() - .unwrap(); + .build() + .unwrap(); } #[test] @@ -1047,8 +1047,8 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .build() - .unwrap(); + .build() + .unwrap(); } #[test] @@ -1067,8 +1067,8 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .build() - .unwrap(); + .build() + .unwrap(); } #[test] @@ -1087,8 +1087,8 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap(); + .select_outgoing() + .unwrap(); builder.outputs[0].0 = "tb1qx4gf3ya0cxfcwydpq8vr2lhrysneuj5d7lqatw" .parse::>() @@ -1115,8 +1115,8 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap(); + .select_outgoing() + .unwrap(); builder.outputs[0].1 = Amount::from_sat(0); @@ -1168,10 +1168,10 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap() - .build() - .unwrap(); + .select_outgoing() + .unwrap() + .build() + .unwrap(); } #[test] @@ -1245,13 +1245,13 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap() - .align_outgoing() - .add_value() - .unwrap() - .strip_value() - .deduct_fee(); + .select_outgoing() + .unwrap() + .align_outgoing() + .add_value() + .unwrap() + .strip_value() + .deduct_fee(); builder.change_addresses = BTreeSet::new(); @@ -1274,15 +1274,15 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap() - .align_outgoing() - .add_value() - .unwrap() - .strip_value() - .deduct_fee() - .build() - .unwrap(); + .select_outgoing() + .unwrap() + .align_outgoing() + .add_value() + .unwrap() + .strip_value() + .deduct_fee() + .build() + .unwrap(); } #[test] @@ -1301,12 +1301,12 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap() - .strip_value() - .deduct_fee() - .build() - .unwrap(); + .select_outgoing() + .unwrap() + .strip_value() + .deduct_fee() + .build() + .unwrap(); } #[test] @@ -1325,11 +1325,11 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Postage, ) - .select_outgoing() - .unwrap() - .strip_value() - .build() - .unwrap(); + .select_outgoing() + .unwrap() + .strip_value() + .build() + .unwrap(); } #[test] @@ -1359,8 +1359,8 @@ mod tests { ], target: Target::Postage, } - .build() - .unwrap(); + .build() + .unwrap(); } #[test] @@ -1390,8 +1390,8 @@ mod tests { ], target: Target::Postage, } - .build() - .unwrap(); + .build() + .unwrap(); } #[test] @@ -1484,8 +1484,8 @@ mod tests { fee_rate, Target::Postage, ) - .build_transaction() - .unwrap(); + .build_transaction() + .unwrap(); let fee = fee_rate.fee(transaction.vsize() + TransactionBuilder::SCHNORR_SIGNATURE_SIZE / 4 + 1); @@ -1866,10 +1866,10 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), ) - .select_outgoing() - .unwrap() - .add_value() - .unwrap(); + .select_outgoing() + .unwrap() + .add_value() + .unwrap(); utxos.remove(4); utxos.remove(3); @@ -1915,11 +1915,11 @@ mod tests { FeeRate::try_from(1.0).unwrap(), Target::Value(Amount::from_sat(10_000)), ) - .select_outgoing() - .unwrap() - .align_outgoing() - .pad_alignment_output() - .unwrap(); + .select_outgoing() + .unwrap() + .align_outgoing() + .pad_alignment_output() + .unwrap(); utxos.remove(5); utxos.remove(2); diff --git a/tests/wallet/inscriptions.rs b/tests/wallet/inscriptions.rs index 7232ba2e0b..4952e1ccc1 100644 --- a/tests/wallet/inscriptions.rs +++ b/tests/wallet/inscriptions.rs @@ -32,11 +32,11 @@ fn inscriptions() { "wallet send --fee-rate 1 --recipient {} {inscription}", address.assume_checked() )) - .rpc_server(&rpc_server) - .expected_exit_code(0) - .stdout_regex(".*") - .run_and_deserialize_output::() - .transaction; + .rpc_server(&rpc_server) + .expected_exit_code(0) + .stdout_regex(".*") + .run_and_deserialize_output::() + .transaction; rpc_server.mine_blocks(1); @@ -97,10 +97,10 @@ fn inscriptions_with_postage() { "wallet send --fee-rate 1 --recipient {} {inscription}", address.assume_checked() )) - .rpc_server(&rpc_server) - .expected_exit_code(0) - .stdout_regex(".*") - .run_and_extract_stdout(); + .rpc_server(&rpc_server) + .expected_exit_code(0) + .stdout_regex(".*") + .run_and_extract_stdout(); rpc_server.mine_blocks(1); diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index c04784f959..a008b93f36 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -13,9 +13,9 @@ fn inscriptions_can_be_sent() { let output = CommandBuilder::new(format!( "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {inscription}", )) - .rpc_server(&rpc_server) - .stdout_regex(r".*") - .run_and_deserialize_output::(); + .rpc_server(&rpc_server) + .stdout_regex(r".*") + .run_and_deserialize_output::(); let txid = rpc_server.mempool()[0].txid(); assert_eq!(txid, output.transaction); @@ -53,10 +53,10 @@ fn send_unknown_inscription() { CommandBuilder::new(format!( "wallet send --fee-rate 1 --address bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv {txid}i0" )) - .rpc_server(&rpc_server) - .expected_stderr(format!("error: inscription {txid}i0 not found\n")) - .expected_exit_code(1) - .run_and_extract_stdout(); + .rpc_server(&rpc_server) + .expected_stderr(format!("error: inscription {txid}i0 not found\n")) + .expected_exit_code(1) + .run_and_extract_stdout(); } #[test] @@ -72,8 +72,8 @@ fn send_inscribed_sat() { let output = CommandBuilder::new(format!( "wallet send --fee-rate 1 --address bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv {inscription}", )) - .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); rpc_server.mine_blocks(1); @@ -100,8 +100,8 @@ fn send_on_mainnnet_works_with_wallet_named_foo() { CommandBuilder::new(format!( "--wallet foo wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {txid}:0:0" )) - .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); } #[test] @@ -113,12 +113,12 @@ fn send_addresses_must_be_valid_for_network() { CommandBuilder::new(format!( "wallet send --fee-rate 1 --address tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz {txid}:0:0" )) - .rpc_server(&rpc_server) - .expected_stderr( - "error: address tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz belongs to network testnet which is different from required bitcoin\n", - ) - .expected_exit_code(1) - .run_and_extract_stdout(); + .rpc_server(&rpc_server) + .expected_stderr( + "error: address tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz belongs to network testnet which is different from required bitcoin\n", + ) + .expected_exit_code(1) + .run_and_extract_stdout(); } #[test] @@ -130,8 +130,8 @@ fn send_on_mainnnet_works_with_wallet_named_ord() { let output = CommandBuilder::new(format!( "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {txid}:0:0" )) - .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); assert_eq!(rpc_server.mempool()[0].txid(), output.transaction); } @@ -153,10 +153,10 @@ fn send_does_not_use_inscribed_sats_as_cardinal_utxos() { CommandBuilder::new(format!( "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {txid}:0:0" )) - .rpc_server(&rpc_server) - .expected_exit_code(1) - .expected_stderr("error: wallet does not contain enough cardinal UTXOs, please add additional funds to wallet.\n") - .run_and_extract_stdout(); + .rpc_server(&rpc_server) + .expected_exit_code(1) + .expected_stderr("error: wallet does not contain enough cardinal UTXOs, please add additional funds to wallet.\n") + .run_and_extract_stdout(); } #[test] @@ -176,12 +176,12 @@ fn do_not_send_within_dust_limit_of_an_inscription() { CommandBuilder::new(format!( "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {output}:329" )) - .rpc_server(&rpc_server) - .expected_exit_code(1) - .expected_stderr(format!( - "error: cannot send {output}:329 without also sending inscription {inscription} at {output}:0\n" - )) - .run_and_extract_stdout(); + .rpc_server(&rpc_server) + .expected_exit_code(1) + .expected_stderr(format!( + "error: cannot send {output}:329 without also sending inscription {inscription} at {output}:0\n" + )) + .run_and_extract_stdout(); } #[test] @@ -201,8 +201,8 @@ fn can_send_after_dust_limit_from_an_inscription() { CommandBuilder::new(format!( "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {output}:330" )) - .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); } #[test] @@ -269,20 +269,20 @@ fn splitting_merged_inscriptions_is_possible() { "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {}i0", reveal_txid, )) - .rpc_server(&rpc_server) - .expected_exit_code(1) - .expected_stderr(format!( - "error: cannot send {reveal_txid}:0:0 without also sending inscription {reveal_txid}i2 at {reveal_txid}:0:{}\n", 100 * COIN_VALUE - )) - .run_and_extract_stdout(); + .rpc_server(&rpc_server) + .expected_exit_code(1) + .expected_stderr(format!( + "error: cannot send {reveal_txid}:0:0 without also sending inscription {reveal_txid}i2 at {reveal_txid}:0:{}\n", 100 * COIN_VALUE + )) + .run_and_extract_stdout(); // splitting out last CommandBuilder::new(format!( "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {}i2", reveal_txid, )) - .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); rpc_server.mine_blocks(1); @@ -291,8 +291,8 @@ fn splitting_merged_inscriptions_is_possible() { "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {}i1", reveal_txid, )) - .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); rpc_server.mine_blocks(1); @@ -301,8 +301,8 @@ fn splitting_merged_inscriptions_is_possible() { "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {}i0", reveal_txid, )) - .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); } #[test] @@ -317,10 +317,10 @@ fn inscriptions_cannot_be_sent_by_satpoint() { CommandBuilder::new(format!( "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") - .expected_exit_code(1) - .run_and_extract_stdout(); + .rpc_server(&rpc_server) + .expected_stderr("error: inscriptions must be sent by inscription ID\n") + .expected_exit_code(1) + .run_and_extract_stdout(); } #[test] @@ -377,8 +377,8 @@ fn send_btc_locks_inscriptions() { CommandBuilder::new( "wallet send --fee-rate 1 --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc", ) - .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); assert_eq!( rpc_server.sent(), @@ -408,10 +408,10 @@ fn send_btc_fails_if_lock_unspent_fails() { 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(); + .rpc_server(&rpc_server) + .expected_stderr("error: failed to lock UTXOs\n") + .expected_exit_code(1) + .run_and_extract_stdout(); } #[test] @@ -425,8 +425,8 @@ fn wallet_send_with_fee_rate() { CommandBuilder::new(format!( "wallet send --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {inscription} --fee-rate 2.0" )) - .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; @@ -456,13 +456,13 @@ fn user_must_provide_fee_rate_to_send() { CommandBuilder::new(format!( "wallet send --address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {inscription}" )) - .rpc_server(&rpc_server) - .expected_exit_code(2) - .stderr_regex( - ".*error: the following required arguments were not provided: + .rpc_server(&rpc_server) + .expected_exit_code(2) + .stderr_regex( + ".*error: the following required arguments were not provided: .*--fee-rate .*", - ) - .run_and_extract_stdout(); + ) + .run_and_extract_stdout(); } #[test] @@ -510,10 +510,10 @@ fn send_btc_does_not_send_locked_utxos() { 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(); + .rpc_server(&rpc_server) + .expected_exit_code(1) + .stderr_regex("error:.*") + .run_and_extract_stdout(); } #[test] @@ -968,10 +968,10 @@ fn refuse_to_burn_amount() { 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(); + .rpc_server(&rpc_server) + .expected_stderr("error: refusing to burn amount\n") + .expected_exit_code(1) + .run_and_extract_stdout(); } #[test] @@ -996,8 +996,8 @@ fn burn_inscribed_sat() { CommandBuilder::new(format!( "wallet send --fee-rate 1 --burn begone {inscription}", )) - .rpc_server(&rpc_server) - .run_and_deserialize_output::(); + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); rpc_server.mine_blocks(1); From 911a76e18390a8fd282ab6f2d8faf8d4d732e830 Mon Sep 17 00:00:00 2001 From: onchainguy-eth <1436535+onchainguy-eth@users.noreply.github.com> Date: Fri, 1 Dec 2023 11:08:49 +0100 Subject: [PATCH 3/9] Use ScriptBuf instead of custom struct --- src/subcommand/wallet/inscribe/batch.rs | 1 + src/subcommand/wallet/send.rs | 35 ++++++++++-- src/subcommand/wallet/transaction_builder.rs | 60 +++++++------------- 3 files changed, 54 insertions(+), 42 deletions(-) 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 ab3c6d306f..be190d0f95 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -1,7 +1,10 @@ use { super::*, + bitcoin::{ + script::PushBytesBuf, + }, crate::{ - subcommand::wallet::transaction_builder::{OutputScript, Target}, + subcommand::wallet::transaction_builder::{Target}, wallet::Wallet, }, }; @@ -105,6 +108,27 @@ impl Send { satpoint } + Outgoing::InscriptionId(id) => index + .get_inscription_satpoint_by_id(id)? + .ok_or_else(|| anyhow!("Inscription {id} not found"))?, + 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_inscriptions(&client, inscriptions, runic_outputs, unspent_outputs)?; + let txid = Self::send_amount(&client, amount, address, self.fee_rate.n())?; + return Ok(Box::new(Output { transaction: txid })); + }, }; let change = [ @@ -128,6 +152,7 @@ impl Send { change, self.fee_rate, postage, + chain, ) .build_transaction()?; @@ -140,12 +165,14 @@ impl Send { Ok(Box::new(Output { transaction: txid })) } - fn get_output(&self, options: &Options) -> Result { + fn get_output(&self, options: &Options) -> Result { if let Some(address) = &self.address { let address = address.clone().require_network(options.chain().network())?; - Ok(OutputScript::PubKey(address)) + Ok(ScriptBuf::from(address)) } else if let Some(msg) = &self.burn { - Ok(OutputScript::OpReturn(Vec::from(msg.clone()))) + Ok(ScriptBuf::new_op_return( + &PushBytesBuf::try_from(Vec::from(msg.clone())).expect("burn payload too large") + )) } else { bail!("no valid output given") } diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index edfed813bd..31bbf78193 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -35,7 +35,6 @@ use { super::*, bitcoin::{ blockdata::{locktime::absolute::LockTime, witness::Witness}, - script::PushBytesBuf, Amount, ScriptBuf, }, std::cmp::{max, min}, @@ -43,6 +42,7 @@ use { #[derive(Debug, PartialEq)] pub enum Error { + BitcoinAddressError(String), DuplicateAddress(Address), Dust { output_value: Amount, @@ -66,36 +66,16 @@ pub enum Target { ExactPostage(Amount), } -#[derive(Clone, Debug, PartialEq)] -pub enum OutputScript { - PubKey(Address), - OpReturn(Vec), -} - -impl OutputScript { - fn script(&self) -> ScriptBuf { - match self { - OutputScript::PubKey(address) => address.script_pubkey(), - OutputScript::OpReturn(data) => ScriptBuf::new_op_return( - &PushBytesBuf::try_from(data.clone()).expect("burn payload too large"), - ), - } - } - - fn dust_value(&self) -> Amount { - self.script().dust_value() - } -} - -impl From
for OutputScript { - fn from(value: Address) -> Self { - OutputScript::PubKey(value) +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, @@ -125,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<(OutputScript, Amount)>, - recipient: OutputScript, + outputs: Vec<(ScriptBuf, Amount)>, + recipient: ScriptBuf, runic_utxos: BTreeSet, target: Target, unused_change_addresses: Vec
, @@ -147,7 +128,7 @@ 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, @@ -157,10 +138,12 @@ impl TransactionBuilder { 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(), @@ -182,10 +165,11 @@ impl TransactionBuilder { )); } - if let OutputScript::PubKey(address) = &self.recipient { - if self.change_addresses.contains(address) { - return Err(Error::DuplicateAddress(address.clone())); - } + 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 { @@ -462,7 +446,7 @@ impl TransactionBuilder { ) } - 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, @@ -478,7 +462,7 @@ impl TransactionBuilder { .into_iter() .map(|recipient| TxOut { value: 0, - script_pubkey: recipient.script(), + script_pubkey: recipient.clone(), }) .collect(), } @@ -490,7 +474,7 @@ impl TransactionBuilder { } fn build(self) -> Result { - let recipient = self.recipient.script(); + let recipient = self.recipient.clone(); let transaction = Transaction { version: 2, lock_time: LockTime::ZERO, @@ -509,7 +493,7 @@ impl TransactionBuilder { .iter() .map(|(output, amount)| TxOut { value: amount.to_sat(), - script_pubkey: output.script(), + script_pubkey: output.clone() }) .collect(), }; @@ -567,7 +551,7 @@ impl TransactionBuilder { transaction .output .iter() - .filter(|tx_out| tx_out.script_pubkey == self.recipient.script()) + .filter(|tx_out| tx_out.script_pubkey == self.recipient) .count(), 1, "invariant: recipient address appears exactly once in outputs", @@ -588,7 +572,7 @@ impl TransactionBuilder { let mut offset = 0; for output in &transaction.output { - if output.script_pubkey == self.recipient.script() { + if output.script_pubkey == self.recipient { let slop = self.fee_rate.fee(Self::ADDITIONAL_OUTPUT_VBYTES); match self.target { From 617f6c6904cc5b48abf076a286e92b86231d800e Mon Sep 17 00:00:00 2001 From: onchainguy-eth <1436535+onchainguy-eth@users.noreply.github.com> Date: Wed, 6 Dec 2023 00:34:38 +0100 Subject: [PATCH 4/9] Fix tests --- src/subcommand/wallet/transaction_builder.rs | 96 +++++++++++++++----- test-bitcoincore-rpc/src/lib.rs | 2 +- 2 files changed, 75 insertions(+), 23 deletions(-) diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index 31bbf78193..50b31235c2 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -165,11 +165,13 @@ impl TransactionBuilder { )); } - 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 - )); + 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 { @@ -758,6 +760,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet ) .select_outgoing() .unwrap(); @@ -802,6 +805,7 @@ mod tests { (change(1).into(), Amount::from_sat(1_724)), ], target: Target::Postage, + chain: Chain::Testnet }; pretty_assert_eq!( @@ -833,6 +837,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet ) .build_transaction() .unwrap() @@ -854,6 +859,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet ) .build_transaction(), Ok(Transaction { @@ -880,6 +886,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet ) .select_outgoing() .unwrap() @@ -906,6 +913,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet ) .build_transaction(), Ok(Transaction { @@ -932,6 +940,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet ) .build_transaction(), Err(Error::NotEnoughCardinalUtxos), @@ -955,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), @@ -980,6 +990,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Ok(Transaction { @@ -1010,6 +1021,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build() .unwrap(); @@ -1030,6 +1042,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build() .unwrap(); @@ -1050,6 +1063,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build() .unwrap(); @@ -1070,6 +1084,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .select_outgoing() .unwrap(); @@ -1098,6 +1113,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .select_outgoing() .unwrap(); @@ -1122,6 +1138,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Ok(Transaction { @@ -1151,6 +1168,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .select_outgoing() .unwrap() @@ -1173,6 +1191,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Ok(Transaction { @@ -1202,6 +1221,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Ok(Transaction { @@ -1228,6 +1248,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .select_outgoing() .unwrap() @@ -1257,6 +1278,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .select_outgoing() .unwrap() @@ -1284,6 +1306,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .select_outgoing() .unwrap() @@ -1308,6 +1331,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .select_outgoing() .unwrap() @@ -1342,6 +1366,7 @@ mod tests { (change(1).into(), Amount::from_sat(1_774)), ], target: Target::Postage, + chain: Chain::Testnet } .build() .unwrap(); @@ -1373,6 +1398,7 @@ mod tests { (change(0).into(), Amount::from_sat(1_774)), ], target: Target::Postage, + chain: Chain::Testnet, } .build() .unwrap(); @@ -1396,6 +1422,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Err(Error::NotEnoughCardinalUtxos) @@ -1420,6 +1447,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Err(Error::NotEnoughCardinalUtxos) @@ -1441,6 +1469,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Err(Error::UtxoContainsAdditionalInscription { @@ -1467,6 +1496,7 @@ mod tests { [change(0), change(1)], fee_rate, Target::Postage, + Chain::Testnet, ) .build_transaction() .unwrap(); @@ -1499,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 { @@ -1528,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 { @@ -1554,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 { @@ -1581,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), @@ -1605,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), @@ -1649,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 { @@ -1676,6 +1712,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(1.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Ok(Transaction { @@ -1701,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 { @@ -1727,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) @@ -1748,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())) @@ -1769,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))) @@ -1790,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 { @@ -1817,6 +1859,7 @@ mod tests { [change(0), change(1)], FeeRate::try_from(250.0).unwrap(), Target::Postage, + Chain::Testnet, ) .build_transaction(), Ok(Transaction { @@ -1849,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() @@ -1898,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() @@ -1951,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!( @@ -2007,6 +2053,7 @@ mod tests { [change(0), change(1)], fee_rate, Target::ExactPostage(Amount::from_sat(66_000)), + Chain::Testnet, ) .build_transaction() .unwrap(); @@ -2043,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!( @@ -2069,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!( @@ -2097,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!( @@ -2125,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!( @@ -2141,6 +2192,7 @@ mod tests { 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( @@ -2148,10 +2200,12 @@ mod tests { BTreeMap::new(), utxos.into_iter().collect(), BTreeSet::new(), - OutputScript::OpReturn(msg.clone()), + 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)) + Target::Value(Amount::from_sat(1000)), + Chain::Testnet ) .build_transaction(), Ok(Transaction { @@ -2161,9 +2215,7 @@ mod tests { output: vec![ TxOut { value: 1000, - script_pubkey: ScriptBuf::new_op_return( - &PushBytesBuf::try_from(msg.clone()).expect("burn message should fit") - ) + script_pubkey: ScriptBuf::new_op_return(&msg_push_bytes) }, tx_out(3879, change(1)) ], 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")] From 0a667b663340576ba55187b64be1bc448249eea2 Mon Sep 17 00:00:00 2001 From: onchainguy-eth <1436535+onchainguy-eth@users.noreply.github.com> Date: Wed, 6 Dec 2023 01:08:53 +0100 Subject: [PATCH 5/9] Tmp exclude test --- src/subcommand/wallet/balance.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/subcommand/wallet/balance.rs b/src/subcommand/wallet/balance.rs index cafc2814ef..7a708fc4e1 100644 --- a/src/subcommand/wallet/balance.rs +++ b/src/subcommand/wallet/balance.rs @@ -57,16 +57,17 @@ mod tests { #[test] fn runes_and_runic_fields_are_not_present_if_none() { - assert_eq!( - serde_json::to_string(&Output { - cardinal: 0, - ordinal: 0, - runes: None, - runic: None, - total: 0 - }) - .unwrap(), - r#"{"cardinal":0,"ordinal":0,"total":0}"# - ); + // @todo - somehow not compiling + // assert_eq!( + // serde_json::to_string(&Output { + // cardinal: 0, + // ordinal: 0, + // runes: None, + // runic: None, + // total: 0 + // }) + // .unwrap(), + // r#"{"cardinal":0,"ordinal":0,"total":0}"# + // ); } } From 5f07baf5800054b071d6d90dc6d9c8817dc9a212 Mon Sep 17 00:00:00 2001 From: onchainguy-eth <1436535+onchainguy-eth@users.noreply.github.com> Date: Sat, 9 Dec 2023 20:10:47 +0100 Subject: [PATCH 6/9] Implement burn charm --- .gitignore | 1 + src/charm.rs | 6 ++++- src/index/updater/inscription_updater.rs | 29 +++++++++++++++++++++--- src/subcommand/server.rs | 3 ++- src/templates/inscription.rs | 3 ++- templates/inscription.html | 2 +- tests/json_api.rs | 1 + tests/wallet/send.rs | 4 ++++ 8 files changed, 42 insertions(+), 7 deletions(-) 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..7811e81b9a 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,9 @@ 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).unwrap().script_pubkey.is_op_return(); + // 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 +98,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { offset, inscription_id, origin: Origin::Old { old_satpoint }, + burned: is_burned, }); inscribed_offsets @@ -189,6 +194,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 +373,30 @@ 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(); + + 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 b0c72fe73f..62a506b860 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1293,7 +1293,8 @@ impl Server { previous, next, rune, - burn_payload + burn_payload, + charms: Some(charms), }) .into_response() } else { diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index c5a24a954a..5b47140bbc 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -26,6 +26,7 @@ pub(crate) struct InscriptionHtml { pub struct InscriptionJson { pub address: Option, pub burn_payload: Option, + pub charms: Option, pub children: Vec, pub content_length: Option, pub content_type: Option, @@ -484,7 +485,7 @@ mod tests {
.*
address
-
burned 🔥
+
burned
.*
burn payload
0xdeadbeef
diff --git a/templates/inscription.html b/templates/inscription.html index e3fc04b829..4135d5399e 100644 --- a/templates/inscription.html +++ b/templates/inscription.html @@ -51,7 +51,7 @@

Inscription {{ self.inscription_number }}

%% if let Some(is_burned) = self.is_burned { %% if is_burned {
address
-
burned 🔥
+
burned
%% } %% } %% if let Some(output) = &self.output { diff --git a/tests/json_api.rs b/tests/json_api.rs index c379294d9b..1a5352c8ba 100644 --- a/tests/json_api.rs +++ b/tests/json_api.rs @@ -165,6 +165,7 @@ fn get_inscription() { previous: None, next: None, rune: None, + charms: None, } ) } diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index a008b93f36..0476722c1e 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -1009,4 +1009,8 @@ fn burn_inscribed_sat() { inscription_json.address.is_none(), "address should be missing after burn" ); + assert!( + inscription_json.charms.eq(&Some(1u16)), + "inscription should have burned charm" + ); } From fd90fa0c2a813046f8abc1d5cb9170e3a0f2be60 Mon Sep 17 00:00:00 2001 From: onchainguy-eth <1436535+onchainguy-eth@users.noreply.github.com> Date: Sat, 9 Dec 2023 21:15:15 +0100 Subject: [PATCH 7/9] Fix charms and add op code parsing --- src/index/updater/inscription_updater.rs | 31 ++++++++++++++---------- src/subcommand/server.rs | 25 +++++++++++-------- src/subcommand/wallet/send.rs | 14 ++++++++--- tests/wallet/send.rs | 4 +++ 4 files changed, 48 insertions(+), 26 deletions(-) diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 7811e81b9a..1cd8707b8e 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -85,7 +85,10 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { } // if the inscription was sent to an OP_RETURN it was burned - let is_burned = tx.output.get(input_index).unwrap().script_pubkey.is_op_return(); + 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( @@ -379,20 +382,22 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { .unwrap() .value(); - let mut inscription_entry = InscriptionEntry::load( - self - .sequence_number_to_entry - .get(sequence_number)? - .unwrap() - .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); + Charm::Burned.set(&mut inscription_entry.charms); - self.sequence_number_to_entry.insert( - sequence_number, - &inscription_entry.store(), - )?; + self.sequence_number_to_entry.insert( + sequence_number, + &inscription_entry.store(), + )?; + } ( false, diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 62a506b860..ff866a0c73 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -32,7 +32,7 @@ use { bitcoin::blockdata::script::Instruction::{ Op, PushBytes }, - bitcoin::blockdata::opcodes::all::OP_RETURN, + bitcoin::blockdata::opcodes::all::{OP_PUSHNUM_NEG1,OP_RETURN}, rust_embed::RustEmbed, rustls_acme::{ acme::{LETS_ENCRYPT_PRODUCTION_DIRECTORY, LETS_ENCRYPT_STAGING_DIRECTORY}, @@ -1252,15 +1252,20 @@ impl Server { // Check if the first instruction is OP_RETURN if let Some(Ok(Op(OP_RETURN))) = 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() + // 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 } diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index be190d0f95..929a655f55 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -1,6 +1,8 @@ +use bitcoin::script::Builder; use { super::*, bitcoin::{ + blockdata::opcodes::all::{OP_PUSHNUM_NEG1,OP_RETURN}, script::PushBytesBuf, }, crate::{ @@ -170,9 +172,15 @@ impl Send { let address = address.clone().require_network(options.chain().network())?; Ok(ScriptBuf::from(address)) } else if let Some(msg) = &self.burn { - Ok(ScriptBuf::new_op_return( - &PushBytesBuf::try_from(Vec::from(msg.clone())).expect("burn payload too large") - )) + 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") } diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index 0476722c1e..c0cf01cac5 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -1013,4 +1013,8 @@ fn burn_inscribed_sat() { 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" + ) } From af7cc24a279e9074c40e53a5032a5f2bf0cd2ba6 Mon Sep 17 00:00:00 2001 From: onchainguy-eth <1436535+onchainguy-eth@users.noreply.github.com> Date: Sat, 9 Dec 2023 21:17:16 +0100 Subject: [PATCH 8/9] Re-add test --- src/subcommand/wallet/balance.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/subcommand/wallet/balance.rs b/src/subcommand/wallet/balance.rs index 7a708fc4e1..cafc2814ef 100644 --- a/src/subcommand/wallet/balance.rs +++ b/src/subcommand/wallet/balance.rs @@ -57,17 +57,16 @@ mod tests { #[test] fn runes_and_runic_fields_are_not_present_if_none() { - // @todo - somehow not compiling - // assert_eq!( - // serde_json::to_string(&Output { - // cardinal: 0, - // ordinal: 0, - // runes: None, - // runic: None, - // total: 0 - // }) - // .unwrap(), - // r#"{"cardinal":0,"ordinal":0,"total":0}"# - // ); + assert_eq!( + serde_json::to_string(&Output { + cardinal: 0, + ordinal: 0, + runes: None, + runic: None, + total: 0 + }) + .unwrap(), + r#"{"cardinal":0,"ordinal":0,"total":0}"# + ); } } From 996949ce70a188760f01f2f402d4bb4d21d3cfa5 Mon Sep 17 00:00:00 2001 From: onchainguy-eth <1436535+onchainguy-eth@users.noreply.github.com> Date: Mon, 25 Dec 2023 13:47:40 +0100 Subject: [PATCH 9/9] Merge with rune changes --- src/subcommand/wallet/send.rs | 43 +++++++++++++---------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 929a655f55..8abc102668 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -64,13 +64,23 @@ impl Send { index.get_runic_outputs(&unspent_outputs.keys().cloned().collect::>())?; let satpoint = match self.outgoing { - Outgoing::Amount(amount) => match output { - OutputScript::OpReturn(_) => bail!("refusing to burn amount"), - OutputScript::PubKey(address) => { - Self::lock_non_cardinal_outputs(&client, &inscriptions, &runic_outputs, unspent_outputs)?; - let txid = Self::send_amount(&client, amount, address, self.fee_rate)?; - return Ok(Box::new(Output { transaction: txid })); + 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 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)? @@ -110,27 +120,6 @@ impl Send { satpoint } - Outgoing::InscriptionId(id) => index - .get_inscription_satpoint_by_id(id)? - .ok_or_else(|| anyhow!("Inscription {id} not found"))?, - 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_inscriptions(&client, inscriptions, runic_outputs, unspent_outputs)?; - let txid = Self::send_amount(&client, amount, address, self.fee_rate.n())?; - return Ok(Box::new(Output { transaction: txid })); - }, }; let change = [