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 #2766

Closed
Closed
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
/target
/test-times.txt
/tmp
/.fleet/
6 changes: 5 additions & 1 deletion src/charm.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#[derive(Copy, Clone)]
pub(crate) enum Charm {
Burned,
Coin,
Cursed,
Epic,
Expand All @@ -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,
Expand All @@ -24,6 +25,7 @@ impl Charm {
Self::Cursed,
Self::Unbound,
Self::Lost,
Self::Burned,
];

fn flag(self) -> u16 {
Expand All @@ -40,6 +42,7 @@ impl Charm {

pub(crate) fn icon(self) -> &'static str {
match self {
Self::Burned => "🔥",
Self::Coin => "🪙",
Self::Cursed => "👹",
Self::Epic => "🪻",
Expand All @@ -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",
Expand Down
36 changes: 32 additions & 4 deletions src/index/updater/inscription_updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub(super) struct Flotsam {
inscription_id: InscriptionId,
offset: u64,
origin: Origin,
burned: bool,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -83,6 +84,12 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> {
continue;
}

// if the inscription was sent to an OP_RETURN it was burned
let is_burned = tx.output
.get(input_index)
.map(|output| output.script_pubkey.is_op_return())
.unwrap_or(false);

// find existing inscriptions on input (transfers of inscriptions)
for (old_satpoint, inscription_id) in Index::inscriptions_on_output(
self.satpoint_to_sequence_number,
Expand All @@ -94,6 +101,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> {
offset,
inscription_id,
origin: Origin::Old { old_satpoint },
burned: is_burned,
});

inscribed_offsets
Expand Down Expand Up @@ -189,6 +197,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> {
.unwrap_or(offset);

floating_inscriptions.push(Flotsam {
burned: is_burned,
inscription_id,
offset,
origin: Origin::New {
Expand Down Expand Up @@ -367,13 +376,32 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> {
.satpoint_to_sequence_number
.remove_all(&old_satpoint.store())?;

(
false,
self
let sequence_number = self
.id_to_sequence_number
.get(&inscription_id.store())?
.unwrap()
.value(),
.value();

if flotsam.burned {
let mut inscription_entry = InscriptionEntry::load(
self
.sequence_number_to_entry
.get(sequence_number)?
.unwrap()
.value(),
);

Charm::Burned.set(&mut inscription_entry.charms);

self.sequence_number_to_entry.insert(
sequence_number,
&inscription_entry.store(),
)?;
}

(
false,
sequence_number
)
}
Origin::New {
Expand Down
34 changes: 34 additions & 0 deletions src/subcommand/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ use {
},
axum_server::Handle,
brotli::Decompressor,
bitcoin::blockdata::script::Instruction::{
Op, PushBytes
},
bitcoin::blockdata::opcodes::all::{OP_PUSHNUM_NEG1,OP_RETURN},
rust_embed::RustEmbed,
rustls_acme::{
acme::{LETS_ENCRYPT_PRODUCTION_DIRECTORY, LETS_ENCRYPT_STAGING_DIRECTORY},
Expand Down Expand Up @@ -1242,11 +1246,37 @@ impl Server {
Charm::Lost.set(&mut charms);
}

let mut is_burned = false;
let burn_payload = output.as_ref().and_then(|o| {
let mut instructions = o.script_pubkey.instructions();

// Check if the first instruction is OP_RETURN
if let Some(Ok(Op(OP_RETURN))) = instructions.next() {
// Check if the second instruction is OP_1NEGATE
if let Some(Ok(Op(OP_PUSHNUM_NEG1))) = instructions.next() {
is_burned = true;
// Extract the payload if it exists
instructions.filter_map(|instr| {
if let Ok(PushBytes(data)) = instr {
String::from_utf8(data.as_bytes().to_vec()).ok()
} else {
None
}
}).next()
} else {
None
}
} else {
None
}
});

Ok(if accept_json.0 {
Json(InscriptionJson {
inscription_id,
children,
inscription_number: entry.inscription_number,
is_burned: Some(is_burned),
genesis_height: entry.height,
parent,
genesis_fee: entry.fee,
Expand All @@ -1268,10 +1298,13 @@ impl Server {
previous,
next,
rune,
burn_payload,
charms: Some(charms),
})
.into_response()
} else {
InscriptionHtml {
burn_payload,
chain: server_config.chain,
charms,
children,
Expand All @@ -1280,6 +1313,7 @@ impl Server {
inscription,
inscription_id,
inscription_number: entry.inscription_number,
is_burned: Some(is_burned),
next,
output,
parent,
Expand Down
2 changes: 1 addition & 1 deletion src/subcommand/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions src/subcommand/wallet/inscribe/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ impl Batch {
change,
self.commit_fee_rate,
Target::Value(reveal_fee + total_postage),
chain,
)
.build_transaction()?;

Expand Down
79 changes: 69 additions & 10 deletions src/subcommand/wallet/send.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
use {super::*, crate::subcommand::wallet::transaction_builder::Target, crate::wallet::Wallet};
use bitcoin::script::Builder;
use {
super::*,
bitcoin::{
blockdata::opcodes::all::{OP_PUSHNUM_NEG1,OP_RETURN},
script::PushBytesBuf,
},
crate::{
subcommand::wallet::transaction_builder::{Target},
wallet::Wallet,
},
};

#[derive(Debug, Parser, Clone)]
#[clap(
group = ArgGroup::new("output")
.required(true)
.args(&["address", "burn"]),
)]
pub(crate) struct Send {
address: Address<NetworkUnchecked>,
outgoing: Outgoing,
#[arg(long, conflicts_with = "burn", help = "Recipient address")]
address: Option<Address<NetworkUnchecked>>,
#[arg(
long,
conflicts_with = "address",
help = "Message to append when burning sats"
)]
burn: Option<String>,
#[arg(long, help = "Use fee rate of <FEE_RATE> sats/vB")]
fee_rate: FeeRate,
#[arg(
Expand All @@ -20,10 +43,7 @@ pub struct Output {

impl Send {
pub(crate) fn run(self, options: Options) -> SubcommandResult {
let address = self
.address
.clone()
.require_network(options.chain().network())?;
let output = self.get_output(&options)?;

let index = Index::open(&options)?;
index.update()?;
Expand All @@ -45,14 +65,33 @@ impl Send {

let satpoint = match self.outgoing {
Outgoing::Amount(amount) => {
// let script = output.get_script(); // Replace with the actual method to get the Script from output

if output.is_op_return() {
bail!("refusing to burn amount");
}

let address = match chain.address_from_script(output.as_script()) {
Ok(addr) => addr,
Err(e) => {
bail!("failed to get address from script: {:?}", e);
}
};

Self::lock_non_cardinal_outputs(&client, &inscriptions, &runic_outputs, unspent_outputs)?;
let transaction = Self::send_amount(&client, amount, address, self.fee_rate)?;
return Ok(Box::new(Output { transaction }));
}
let txid = Self::send_amount(&client, amount, address, self.fee_rate)?;
return Ok(Box::new(Output { transaction: txid }));
},
Outgoing::InscriptionId(id) => index
.get_inscription_satpoint_by_id(id)?
.ok_or_else(|| anyhow!("inscription {id} not found"))?,
Outgoing::Rune { decimal, rune } => {
let address = self
.address
.unwrap()
.clone()
.require_network(options.chain().network())?;

let transaction = Self::send_runes(
address,
chain,
Expand Down Expand Up @@ -100,10 +139,11 @@ impl Send {
unspent_outputs,
locked_outputs,
runic_outputs,
address.clone(),
output,
change,
self.fee_rate,
postage,
chain,
)
.build_transaction()?;

Expand All @@ -116,6 +156,25 @@ impl Send {
Ok(Box::new(Output { transaction: txid }))
}

fn get_output(&self, options: &Options) -> Result<ScriptBuf, Error> {
if let Some(address) = &self.address {
let address = address.clone().require_network(options.chain().network())?;
Ok(ScriptBuf::from(address))
} else if let Some(msg) = &self.burn {
let push_data_buf = PushBytesBuf::try_from(Vec::from(msg.clone()))
.expect("burn payload too large");

Ok(Builder::new()
.push_opcode(OP_RETURN)
.push_opcode(OP_PUSHNUM_NEG1)
.push_slice(&push_data_buf)
.into_script()
)
} else {
bail!("no valid output given")
}
}

fn lock_non_cardinal_outputs(
client: &Client,
inscriptions: &BTreeMap<SatPoint, InscriptionId>,
Expand Down
Loading