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

feat: burn rune through forcing Cenotaph #3683

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
7 changes: 7 additions & 0 deletions crates/ordinals/src/runestone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ impl Runestone {
pub const MAGIC_NUMBER: opcodes::All = opcodes::all::OP_PUSHNUM_13;
pub const COMMIT_CONFIRMATIONS: u16 = 6;

// satoshi first block address
pub const MAINNET_BURN_ADDRESS: &'static str = "1FvzCLoTPGANNjWoUo6jUGuAG3wg1w4YjR";
// just a random testnet address
pub const TESTNET_BURN_ADDRESS: &'static str = "tb1prqr6lpxeqcyfff8n7c748ad7szyh8hwmf9nly598r5uqewkm4kms0nevay";
// just a random regnet address
pub const REGNET_BURN_ADDRESS: &'static str = "bcrt1pyf99p0qrt8dsx8a8hnd7edzpnnpsndf9ce6hhx55s9f9v84e48eqdlyd6n";

pub fn decipher(transaction: &Transaction) -> Option<Artifact> {
let payload = match Runestone::payload(transaction) {
Some(Payload::Valid(payload)) => payload,
Expand Down
4 changes: 4 additions & 0 deletions src/subcommand/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub mod resume;
pub mod runics;
pub mod sats;
pub mod send;
pub mod burn;
mod shared_args;
pub mod transactions;

Expand Down Expand Up @@ -78,6 +79,8 @@ pub(crate) enum Subcommand {
#[command(about = "Send sat or inscription")]
Send(send::Send),
#[command(about = "See wallet transactions")]
Burn(burn::Burn),
#[command(about = "Warning: burn all specific rune in your wallet. Defining amount has no use")]
Transactions(transactions::Transactions),
}

Expand Down Expand Up @@ -120,6 +123,7 @@ impl WalletCommand {
Subcommand::Runics => runics::run(wallet),
Subcommand::Sats(sats) => sats.run(wallet),
Subcommand::Send(send) => send.run(wallet),
Subcommand::Burn(burn) => burn.run(wallet),
Subcommand::Transactions(transactions) => transactions.run(wallet),
}
}
Expand Down
221 changes: 221 additions & 0 deletions src/subcommand/wallet/burn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use {super::*, crate::outgoing::Outgoing, base64::Engine, bitcoin::psbt::Psbt, core::panic, ordinals::Flaw};

#[derive(Debug, Parser)]
pub(crate) 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,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Output {
pub txid: Txid,
pub psbt: String,
pub outgoing: Outgoing,
pub fee: u64,
}

impl Burn {
pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult {
let unsigned_transaction = match self.outgoing {
Outgoing::Amount(_) => unimplemented!(),
Outgoing::Rune { decimal, rune } => Self::create_unsigned_burn_runes_transaction(
&wallet,
rune,
decimal,
self.fee_rate
)?,
Outgoing::InscriptionId(_) => unimplemented!(),
Outgoing::SatPoint(_) => unimplemented!(),
Outgoing::Sat(_) => unimplemented!()
};

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();
}

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

fn create_unsigned_burn_runes_transaction(
wallet: &Wallet,
spaced_rune: SpacedRune,
decimal: Decimal,
fee_rate: FeeRate,
) -> Result<Transaction> {
ensure!(
wallet.has_rune_index(),
"sending runes with `ord send` requires index created with `--index-runes` flag",
);

let inscriptions = wallet.inscriptions();
let runic_outputs = wallet.get_runic_outputs()?;
let bitcoin_client = wallet.bitcoin_client();

wallet.lock_non_cardinal_outputs()?;

let (id, entry, _parent) = wallet
.get_rune(spaced_rune.rune)?
.with_context(|| format!("rune `{}` has not been etched", spaced_rune.rune))?;

let amount = decimal.to_integer(entry.divisibility)?;

let inscribed_outputs = inscriptions
.keys()
.map(|satpoint| satpoint.outpoint)
.collect::<HashSet<OutPoint>>();

let mut input_runes = 0;
let mut input = Vec::new();

for output in runic_outputs {
if inscribed_outputs.contains(&output) {
continue;
}

let balance = wallet.get_rune_balance_in_output(&output, entry.spaced_rune.rune)?;

if balance > 0 {
input_runes += balance;
input.push(output);
}

if input_runes >= amount {
break;
}
}

ensure! {
input_runes >= amount,
"insufficient `{}` balance, only {} in wallet",
spaced_rune,
Pile {
amount: input_runes,
divisibility: entry.divisibility,
symbol: entry.symbol
},
}

let runestone = Runestone {
edicts: vec![Edict {
amount,
id,
output: 999999,
}],
..default()
};

let burn_address = match wallet.chain().network() {
Network::Bitcoin => Runestone::MAINNET_BURN_ADDRESS,
Network::Testnet => Runestone::TESTNET_BURN_ADDRESS,
Network::Signet => Runestone::TESTNET_BURN_ADDRESS,
Network::Regtest => Runestone::REGNET_BURN_ADDRESS,
_ => panic!("no such network")
};

let burn_destination = Address::from_str(burn_address)
.unwrap()
.require_network(wallet.chain().network())?;

let unfunded_transaction = Transaction {
version: 2,
lock_time: LockTime::ZERO,
input: input
.into_iter()
.map(|previous_output| TxIn {
previous_output,
script_sig: ScriptBuf::new(),
sequence: Sequence::MAX,
witness: Witness::new(),
})
.collect(),
output: vec![
TxOut {
script_pubkey: runestone.encipher(),
value: 0,
},
TxOut {
script_pubkey: wallet.get_change_address()?.script_pubkey(),
value: TARGET_POSTAGE.to_sat(),
},
TxOut {
script_pubkey: burn_destination.payload.script_pubkey(),
value: TARGET_POSTAGE.to_sat(),
},
],
};

let unsigned_transaction =
fund_raw_transaction(bitcoin_client, fee_rate, &unfunded_transaction)?;

let unsigned_transaction = consensus::encode::deserialize(&unsigned_transaction)?;

assert_eq!(
Runestone::decipher(&unsigned_transaction),
Some(Artifact::Cenotaph(ordinals::Cenotaph
{flaw:Some(Flaw::EdictOutput), ..default()}
)),
);

Ok(unsigned_transaction)
}
}
1 change: 1 addition & 0 deletions tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ type Batch = ord::wallet::batch::Output;
type Create = ord::subcommand::wallet::create::Output;
type Inscriptions = Vec<ord::subcommand::wallet::inscriptions::Output>;
type Send = ord::subcommand::wallet::send::Output;
type Burn = ord::subcommand::wallet::burn::Output;
type Supply = ord::subcommand::supply::Output;

fn create_wallet(core: &mockcore::Handle, ord: &TestServer) {
Expand Down
1 change: 1 addition & 0 deletions tests/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ mod runics;
mod sats;
mod selection;
mod send;
mod burn;
mod transactions;
113 changes: 113 additions & 0 deletions tests/wallet/burn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use super::*;

#[test]
fn burning_rune_works() {
let core = mockcore::builder().network(Network::Regtest).build();

let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]);

create_wallet(&core, &ord);

etch(&core, &ord, Rune(RUNE));

let output = CommandBuilder::new(format!(
"--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 100:{}",
Rune(RUNE)
))
.core(&core)
.ord(&ord)
.run_and_deserialize_output::<Send>();

core.mine_blocks(1);

let balances = CommandBuilder::new("--regtest --index-runes balances")
.core(&core)
.ord(&ord)
.run_and_deserialize_output::<ord::subcommand::balances::Output>();

assert_eq!(
balances,
ord::subcommand::balances::Output {
runes: vec![(
SpacedRune::new(Rune(RUNE), 0),
vec![(
OutPoint {
txid: output.txid,
vout: 1
},
Pile {
amount: 900,
divisibility: 0,
symbol: Some('¢')
},
),(
OutPoint {
txid: output.txid,
vout: 2
},
Pile {
amount: 100,
divisibility: 0,
symbol: Some('¢')
},
)]
.into_iter()
.collect()
),]
.into_iter()
.collect(),
}
);

// burn all runes left in wallet
// defining amount has no use
let _ = CommandBuilder::new(format!("--chain regtest --index-runes wallet burn --fee-rate 1 500:{}",
Rune(RUNE)))
.core(&core)
.ord(&ord)
.run_and_deserialize_output::<Burn>();

core.mine_blocks(1);

// burned amount should not be picked up by updater
let balances = CommandBuilder::new("--regtest --index-runes balances")
.core(&core)
.ord(&ord)
.run_and_deserialize_output::<ord::subcommand::balances::Output>();

pretty_assert_eq!(
balances,
ord::subcommand::balances::Output {
runes: vec![(
SpacedRune::new(Rune(RUNE), 0),
vec![(
OutPoint {
txid: output.txid,
vout: 2
},
Pile {
amount: 100,
divisibility: 0,
symbol: Some('¢')
},
)]
.into_iter()
.collect()
),]
.into_iter()
.collect(),
}
);

let rune = CommandBuilder::new("--regtest --index-runes runes")
.core(&core)
.ord(&ord)
.run_and_deserialize_output::<ord::subcommand::runes::Output>();

let burned = rune.runes[&Rune(RUNE)].burned;

assert_eq!(
burned,
900
);
}
Loading