Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Implement burn #3437

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion crates/ordinals/src/charm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -30,6 +31,7 @@ impl Charm {
Self::Unbound,
Self::Lost,
Self::Vindicated,
Self::Burned,
];

fn flag(self) -> u16 {
Expand Down Expand Up @@ -62,6 +64,7 @@ impl Charm {
Self::Unbound => "🔓",
Self::Uncommon => "🌱",
Self::Vindicated => "❤️‍🔥",
Self::Burned => "🔥",
}
}

Expand Down Expand Up @@ -91,6 +94,7 @@ impl Display for Charm {
Self::Unbound => "unbound",
Self::Uncommon => "uncommon",
Self::Vindicated => "vindicated",
Self::Burned => "burned",
}
)
}
Expand All @@ -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}`")),
})
}
Expand Down
28 changes: 26 additions & 2 deletions src/index/updater/inscription_updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,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, txout) in tx.output.iter().enumerate() {
let end = output_value + txout.value;

Expand All @@ -306,6 +307,7 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> {
offset: flotsam.offset - output_value,
};

is_burned = txout.script_pubkey.is_op_return();
new_locations.push((new_satpoint, inscriptions.next().unwrap()));
}

Expand Down Expand Up @@ -344,7 +346,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 {
Expand All @@ -353,7 +355,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(())
Expand Down Expand Up @@ -391,6 +393,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 {
Expand All @@ -405,6 +408,27 @@ impl<'a, 'tx> InscriptionUpdater<'a, 'tx> {
.unwrap()
.value();

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,
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ pub mod wallet;

type Result<T = (), E = Error> = std::result::Result<T, E>;

const TARGET_POSTAGE: Amount = Amount::from_sat(10_000);
pub const TARGET_POSTAGE: Amount = Amount::from_sat(10_000);

static SHUTTING_DOWN: AtomicBool = AtomicBool::new(false);
static LISTENERS: Mutex<Vec<axum_server::Handle>> = Mutex::new(Vec::new());
Expand Down
6 changes: 6 additions & 0 deletions src/subcommand/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use {
crate::wallet::{batch, wallet_constructor::WalletConstructor, Wallet},
bitcoincore_rpc::bitcoincore_rpc_json::ListDescriptorsResult,
shared_args::SharedArgs,
sign_transaction::*
};

pub mod balance;
Expand All @@ -24,6 +25,8 @@ pub mod sats;
pub mod send;
mod shared_args;
pub mod transactions;
pub mod burn;
mod sign_transaction;

#[derive(Debug, Parser)]
pub(crate) struct WalletCommand {
Expand All @@ -47,6 +50,8 @@ pub(crate) enum Subcommand {
Balance,
#[command(about = "Create inscriptions and runes")]
Batch(batch_command::Batch),
#[command(about = "Burn sat or inscription")]
Burn(burn::Burn),
#[command(about = "List unspent cardinal outputs in wallet")]
Cardinals,
#[command(about = "Create new wallet")]
Expand Down Expand Up @@ -106,6 +111,7 @@ impl WalletCommand {
match self.subcommand {
Subcommand::Balance => balance::run(wallet),
Subcommand::Batch(batch) => batch.run(wallet),
Subcommand::Burn(burn) => burn.run(wallet),
Subcommand::Cardinals => cardinals::run(wallet),
Subcommand::Create(_) | Subcommand::Restore(_) => unreachable!(),
Subcommand::Dump => dump::run(wallet),
Expand Down
89 changes: 89 additions & 0 deletions src/subcommand/wallet/burn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use {super::*, crate::outgoing::Outgoing, bitcoin::{opcodes}};

#[derive(Debug, Parser)]
pub struct Burn {
#[arg(long, help = "Don't sign or broadcast transaction")]
pub(crate) dry_run: bool,
#[arg(long, help = "Use fee rate of <FEE_RATE> sats/vB")]
fee_rate: FeeRate,
#[arg(
long,
help = "Target <AMOUNT> postage with sent inscriptions. [default: 10000 sat]"
)]
pub(crate) postage: Option<Amount>,
outgoing: Outgoing,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
outgoing: Outgoing,
inscription_id: InscriptionId,

For now we only want to enable burning inscriptions, so let's make sure only that is possible. We'll add burning functionality to the other types (sats, btc, runes) later.

}

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

if inscription_info.value.unwrap() > 10000 {
return Err(anyhow!("The amount of sats where the inscription is on exceeds 10000"));
}

Self::create_unsigned_burn_transaction(
&wallet,
inscription_info.satpoint,
self.postage,
self.fee_rate,
)?
}
_ => panic!("Outgoing type is not an inscription"),
};

let (txid, psbt, fee) = sign_transaction(&wallet, unsigned_transaction, self.dry_run)?;

Ok(Some(Box::new(crate::subcommand::wallet::send::Output {
txid,
psbt,
outgoing: self.outgoing,
fee,
})))
}

fn create_unsigned_burn_transaction(
wallet: &Wallet,
satpoint: SatPoint,
postage: Option<Amount>,
fee_rate: FeeRate
) -> Result<Transaction> {
let runic_outputs = wallet.get_runic_outputs()?;

ensure!(
!runic_outputs.contains(&satpoint.outpoint),
"runic outpoints may not be burned"
);

let change = [wallet.get_change_address()?, wallet.get_change_address()?];

let postage = if let Some(postage) = postage {
Target::ExactPostage(postage)
} else {
Target::Postage
};

let burn_script = script::Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script();

Ok(
TransactionBuilder::new(
satpoint,
wallet.inscriptions().clone(),
wallet.utxos().clone(),
wallet.locked_utxos().clone().into_keys().collect(),
runic_outputs,
burn_script,
change,
fee_rate,
postage,
wallet.chain().network()
)
.build_transaction()?,
)
}
}
56 changes: 4 additions & 52 deletions src/subcommand/wallet/send.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use {super::*, crate::outgoing::Outgoing, base64::Engine, bitcoin::psbt::Psbt};
use {super::*, crate::outgoing::Outgoing};

#[derive(Debug, Parser)]
pub(crate) struct Send {
Expand Down Expand Up @@ -72,56 +72,7 @@ impl Send {
)?,
};

let unspent_outputs = wallet.utxos();

let (txid, psbt) = if self.dry_run {
let psbt = wallet
.bitcoin_client()
.wallet_process_psbt(
&base64::engine::general_purpose::STANDARD
.encode(Psbt::from_unsigned_tx(unsigned_transaction.clone())?.serialize()),
Some(false),
None,
None,
)?
.psbt;

(unsigned_transaction.txid(), psbt)
} else {
let psbt = wallet
.bitcoin_client()
.wallet_process_psbt(
&base64::engine::general_purpose::STANDARD
.encode(Psbt::from_unsigned_tx(unsigned_transaction.clone())?.serialize()),
Some(true),
None,
None,
)?
.psbt;

let signed_tx = wallet
.bitcoin_client()
.finalize_psbt(&psbt, None)?
.hex
.ok_or_else(|| anyhow!("unable to sign transaction"))?;

(
wallet.bitcoin_client().send_raw_transaction(&signed_tx)?,
psbt,
)
};

let mut fee = 0;
for txin in unsigned_transaction.input.iter() {
let Some(txout) = unspent_outputs.get(&txin.previous_output) else {
panic!("input {} not found in utxos", txin.previous_output);
};
fee += txout.value;
}

for txout in unsigned_transaction.output.iter() {
fee = fee.checked_sub(txout.value).unwrap();
}
let (txid, psbt, fee) = sign_transaction(&wallet, unsigned_transaction, self.dry_run)?;

Ok(Some(Box::new(Output {
txid,
Expand Down Expand Up @@ -196,10 +147,11 @@ impl Send {
wallet.utxos().clone(),
wallet.locked_utxos().clone().into_keys().collect(),
runic_outputs,
destination.clone(),
destination.script_pubkey(),
change,
fee_rate,
postage,
wallet.chain().network()
)
.build_transaction()?,
)
Expand Down
64 changes: 64 additions & 0 deletions src/subcommand/wallet/sign_transaction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use {
super::*,
base64::Engine,
bitcoin::psbt::Psbt,
};

pub(super) fn sign_transaction(
wallet: &Wallet,
unsigned_transaction: Transaction,
dry_run: bool,
) -> Result<(Txid, String, u64)> {
let unspent_outputs = wallet.utxos();

let (txid, psbt) = if dry_run {
let psbt = wallet
.bitcoin_client()
.wallet_process_psbt(
&base64::engine::general_purpose::STANDARD
.encode(Psbt::from_unsigned_tx(unsigned_transaction.clone())?.serialize()),
Some(false),
None,
None,
)?
.psbt;

(unsigned_transaction.txid(), psbt)
} else {
let psbt = wallet
.bitcoin_client()
.wallet_process_psbt(
&base64::engine::general_purpose::STANDARD
.encode(Psbt::from_unsigned_tx(unsigned_transaction.clone())?.serialize()),
Some(true),
None,
None,
)?
.psbt;

let signed_tx = wallet
.bitcoin_client()
.finalize_psbt(&psbt, None)?
.hex
.ok_or_else(|| anyhow!("unable to sign transaction"))?;

(
wallet.bitcoin_client().send_raw_transaction(&signed_tx)?,
psbt,
)
};

let mut fee = 0;
for txin in unsigned_transaction.input.iter() {
let Some(txout) = unspent_outputs.get(&txin.previous_output) else {
panic!("input {} not found in utxos", txin.previous_output);
};
fee += txout.value;
}

for txout in unsigned_transaction.output.iter() {
fee = fee.checked_sub(txout.value).unwrap();
}

Ok((txid, psbt, fee))
}
Loading