From 3a44161d57b7d49d5bdf1456cc15d47228af0d5a Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 28 Mar 2024 13:48:33 -0700 Subject: [PATCH] Move runes types into ordinals crate (#3391) --- Cargo.lock | 1 + Cargo.toml | 4 - benches/server.rs | 16 - crates/ordinals/Cargo.toml | 1 + crates/ordinals/src/cenotaph.rs | 63 +++ crates/ordinals/src/charm.rs | 5 +- {src/runes => crates/ordinals/src}/edict.rs | 7 +- crates/ordinals/src/etching.rs | 39 ++ crates/ordinals/src/lib.rs | 35 +- {src/runes => crates/ordinals/src}/pile.rs | 2 +- {src/runes => crates/ordinals/src}/rune.rs | 99 +++-- {src/runes => crates/ordinals/src}/rune_id.rs | 70 ++-- .../ordinals/src}/runestone.rs | 362 +++++++++--------- .../ordinals/src/runestone}/flag.rs | 0 crates/ordinals/src/runestone/message.rs | 56 +++ .../ordinals/src/runestone}/tag.rs | 0 .../ordinals/src}/spaced_rune.rs | 49 ++- {src/runes => crates/ordinals/src}/terms.rs | 0 {src/runes => crates/ordinals/src}/varint.rs | 29 +- src/chain.rs | 26 +- src/index.rs | 27 +- src/index/updater.rs | 5 +- src/index/updater/rune_updater.rs | 13 +- src/lib.rs | 13 +- src/runes.rs | 56 +-- src/runes/etching.rs | 11 - src/subcommand/server.rs | 7 +- src/subcommand/wallet/inscribe.rs | 4 +- src/templates/rune.rs | 2 +- src/test.rs | 6 +- src/wallet/batch/plan.rs | 8 +- tests/lib.rs | 6 +- tests/wallet/inscribe.rs | 2 +- tests/wallet/mint.rs | 5 +- tests/wallet/send.rs | 2 +- 35 files changed, 624 insertions(+), 407 deletions(-) delete mode 100644 benches/server.rs create mode 100644 crates/ordinals/src/cenotaph.rs rename {src/runes => crates/ordinals/src}/edict.rs (75%) create mode 100644 crates/ordinals/src/etching.rs rename {src/runes => crates/ordinals/src}/pile.rs (98%) rename {src/runes => crates/ordinals/src}/rune.rs (77%) rename {src/runes => crates/ordinals/src}/rune_id.rs (64%) rename {src/runes => crates/ordinals/src}/runestone.rs (87%) rename {src/runes => crates/ordinals/src/runestone}/flag.rs (100%) create mode 100644 crates/ordinals/src/runestone/message.rs rename {src/runes => crates/ordinals/src/runestone}/tag.rs (100%) rename {src/runes => crates/ordinals/src}/spaced_rune.rs (66%) rename {src/runes => crates/ordinals/src}/terms.rs (100%) rename {src/runes => crates/ordinals/src}/varint.rs (92%) delete mode 100644 src/runes/etching.rs diff --git a/Cargo.lock b/Cargo.lock index 7b326ff2bf..81414c6963 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2327,6 +2327,7 @@ version = "0.0.4" dependencies = [ "bitcoin", "derive_more", + "pretty_assertions", "serde", "serde_json", "serde_with", diff --git a/Cargo.toml b/Cargo.toml index f57afa0706..926638fcd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,10 +79,6 @@ reqwest = { version = "0.11.10", features = ["blocking", "brotli", "json"] } test-bitcoincore-rpc = { path = "crates/test-bitcoincore-rpc" } unindent = "0.2.1" -[[bench]] -name = "server" -harness = false - [[bin]] name = "ord" path = "src/bin/main.rs" diff --git a/benches/server.rs b/benches/server.rs deleted file mode 100644 index b6cc2fdace..0000000000 --- a/benches/server.rs +++ /dev/null @@ -1,16 +0,0 @@ -use {criterion::Criterion, ord::Index}; - -fn main() { - let mut criterion = Criterion::default().configure_from_args(); - let index = Index::open(&Default::default()).unwrap(); - let mut i = 0; - - criterion.bench_function("inscription", |b| { - b.iter(|| { - Index::inscription_info_benchmark(&index, i); - i += 1; - }); - }); - - Criterion::default().configure_from_args().final_summary(); -} diff --git a/crates/ordinals/Cargo.toml b/crates/ordinals/Cargo.toml index adc439385e..3f839c7fd4 100644 --- a/crates/ordinals/Cargo.toml +++ b/crates/ordinals/Cargo.toml @@ -17,3 +17,4 @@ thiserror = "1.0.56" [dev-dependencies] serde_json = { version = "1.0.81", features = ["preserve_order"] } +pretty_assertions = "1.2.1" diff --git a/crates/ordinals/src/cenotaph.rs b/crates/ordinals/src/cenotaph.rs new file mode 100644 index 0000000000..4dbc76a9e2 --- /dev/null +++ b/crates/ordinals/src/cenotaph.rs @@ -0,0 +1,63 @@ +use super::*; + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Cenotaph { + EdictOutput, + EdictRuneId, + Opcode, + SupplyOverflow, + TrailingIntegers, + TruncatedField, + UnrecognizedEvenTag, + UnrecognizedFlag, + Varint, +} + +impl Cenotaph { + pub const ALL: [Self; 9] = [ + Self::EdictOutput, + Self::EdictRuneId, + Self::Opcode, + Self::SupplyOverflow, + Self::TrailingIntegers, + Self::TruncatedField, + Self::UnrecognizedEvenTag, + Self::UnrecognizedFlag, + Self::Varint, + ]; + + pub fn flag(self) -> u32 { + 1 << (self as u32) + } +} + +impl Display for Cenotaph { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::EdictOutput => write!(f, "edict output greater than transaction output count"), + Self::EdictRuneId => write!(f, "invalid rune ID in edict"), + Self::Opcode => write!(f, "non-pushdata opcode in OP_RETURN"), + Self::SupplyOverflow => write!(f, "supply overflows u128"), + Self::TrailingIntegers => write!(f, "trailing integers in body"), + Self::TruncatedField => write!(f, "field with missing value"), + Self::UnrecognizedEvenTag => write!(f, "unrecognized even tag"), + Self::UnrecognizedFlag => write!(f, "unrecognized field"), + Self::Varint => write!(f, "invalid varint"), + } + } +} + +impl From for Runestone { + fn from(cenotaph: Cenotaph) -> Self { + Self { + cenotaph: cenotaph.flag(), + ..default() + } + } +} + +impl From for u32 { + fn from(cenotaph: Cenotaph) -> Self { + cenotaph.flag() + } +} diff --git a/crates/ordinals/src/charm.rs b/crates/ordinals/src/charm.rs index 6755db36e5..0331be9306 100644 --- a/crates/ordinals/src/charm.rs +++ b/crates/ordinals/src/charm.rs @@ -17,7 +17,7 @@ pub enum Charm { } impl Charm { - pub const ALL: [Charm; 12] = [ + pub const ALL: [Self; 12] = [ Self::Coin, Self::Uncommon, Self::Rare, @@ -67,9 +67,8 @@ impl Charm { pub fn charms(charms: u16) -> Vec { Self::ALL - .iter() + .into_iter() .filter(|charm| charm.is_set(charms)) - .copied() .collect() } } diff --git a/src/runes/edict.rs b/crates/ordinals/src/edict.rs similarity index 75% rename from src/runes/edict.rs rename to crates/ordinals/src/edict.rs index 9fc753a820..3bc6113884 100644 --- a/src/runes/edict.rs +++ b/crates/ordinals/src/edict.rs @@ -8,12 +8,7 @@ pub struct Edict { } impl Edict { - pub(crate) fn from_integers( - tx: &Transaction, - id: RuneId, - amount: u128, - output: u128, - ) -> Option { + pub fn from_integers(tx: &Transaction, id: RuneId, amount: u128, output: u128) -> Option { let Ok(output) = u32::try_from(output) else { return None; }; diff --git a/crates/ordinals/src/etching.rs b/crates/ordinals/src/etching.rs new file mode 100644 index 0000000000..3317462948 --- /dev/null +++ b/crates/ordinals/src/etching.rs @@ -0,0 +1,39 @@ +use super::*; + +#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Copy, Clone, Eq)] +pub struct Etching { + pub divisibility: Option, + pub premine: Option, + pub rune: Option, + pub spacers: Option, + pub symbol: Option, + pub terms: Option, +} + +impl Etching { + pub const MAX_DIVISIBILITY: u8 = 38; + pub const MAX_SPACERS: u32 = 0b00000111_11111111_11111111_11111111; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn max_spacers() { + let mut rune = String::new(); + + for (i, c) in Rune(u128::MAX).to_string().chars().enumerate() { + if i > 0 { + rune.push('•'); + } + + rune.push(c); + } + + assert_eq!( + Etching::MAX_SPACERS, + rune.parse::().unwrap().spacers + ); + } +} diff --git a/crates/ordinals/src/lib.rs b/crates/ordinals/src/lib.rs index 9566f60019..afd2aa8f1d 100644 --- a/crates/ordinals/src/lib.rs +++ b/crates/ordinals/src/lib.rs @@ -1,16 +1,21 @@ -//! Types for interoperating with ordinals and inscriptions. +//! Types for interoperating with ordinals, inscriptions, and runes. use { - bitcoin::constants::{COIN_VALUE, DIFFCHANGE_INTERVAL, SUBSIDY_HALVING_INTERVAL}, bitcoin::{ consensus::{Decodable, Encodable}, - OutPoint, + constants::{ + COIN_VALUE, DIFFCHANGE_INTERVAL, MAX_SCRIPT_ELEMENT_SIZE, SUBSIDY_HALVING_INTERVAL, + }, + opcodes, + script::{self, Instruction}, + Network, OutPoint, ScriptBuf, Transaction, }, derive_more::{Display, FromStr}, serde::{Deserialize, Serialize}, serde_with::{DeserializeFromStr, SerializeDisplay}, std::{ cmp, + collections::{HashMap, VecDeque}, fmt::{self, Display, Formatter}, io, num::ParseIntError, @@ -20,18 +25,34 @@ use { thiserror::Error, }; -pub const CYCLE_EPOCHS: u32 = 6; - pub use { - charm::Charm, decimal_sat::DecimalSat, degree::Degree, epoch::Epoch, height::Height, - rarity::Rarity, sat::Sat, sat_point::SatPoint, + cenotaph::Cenotaph, charm::Charm, decimal_sat::DecimalSat, degree::Degree, edict::Edict, + epoch::Epoch, etching::Etching, height::Height, pile::Pile, rarity::Rarity, rune::Rune, + rune_id::RuneId, runestone::Runestone, sat::Sat, sat_point::SatPoint, spaced_rune::SpacedRune, + terms::Terms, }; +pub const CYCLE_EPOCHS: u32 = 6; + +fn default() -> T { + Default::default() +} + +mod cenotaph; mod charm; mod decimal_sat; mod degree; +mod edict; mod epoch; +mod etching; mod height; +mod pile; mod rarity; +mod rune; +mod rune_id; +mod runestone; mod sat; mod sat_point; +mod spaced_rune; +mod terms; +pub mod varint; diff --git a/src/runes/pile.rs b/crates/ordinals/src/pile.rs similarity index 98% rename from src/runes/pile.rs rename to crates/ordinals/src/pile.rs index cf5d7b90f3..d93cba20ba 100644 --- a/src/runes/pile.rs +++ b/crates/ordinals/src/pile.rs @@ -131,7 +131,7 @@ mod tests { assert_eq!( Pile { amount: u128::MAX, - divisibility: MAX_DIVISIBILITY, + divisibility: 38, symbol: None, } .to_string(), diff --git a/src/runes/rune.rs b/crates/ordinals/src/rune.rs similarity index 77% rename from src/runes/rune.rs rename to crates/ordinals/src/rune.rs index bd49af12d6..0c144188a7 100644 --- a/src/runes/rune.rs +++ b/crates/ordinals/src/rune.rs @@ -6,6 +6,8 @@ use super::*; pub struct Rune(pub u128); impl Rune { + const RESERVED: u128 = 6402364363415443603228541259936211926; + const STEPS: &'static [u128] = &[ 0, 26, @@ -37,12 +39,27 @@ impl Rune { 166461473448801533683942072758341510102, ]; - pub(crate) fn minimum_at_height(chain: Chain, height: Height) -> Self { + pub fn n(self) -> u128 { + self.0 + } + + pub fn first_rune_height(network: Network) -> u32 { + SUBSIDY_HALVING_INTERVAL + * match network { + Network::Bitcoin => 4, + Network::Regtest => 0, + Network::Signet => 0, + Network::Testnet => 12, + _ => 0, + } + } + + pub fn minimum_at_height(chain: Network, height: Height) -> Self { let offset = height.0.saturating_add(1); const INTERVAL: u32 = SUBSIDY_HALVING_INTERVAL / 12; - let start = chain.first_rune_height(); + let start = Self::first_rune_height(chain); let end = start + SUBSIDY_HALVING_INTERVAL; @@ -67,15 +84,15 @@ impl Rune { Rune(start - ((start - end) * remainder / u128::from(INTERVAL))) } - pub(crate) fn is_reserved(self) -> bool { - self.0 >= RESERVED + pub fn is_reserved(self) -> bool { + self.0 >= Self::RESERVED } - pub fn reserved(n: u128) -> Self { - Rune(RESERVED.checked_add(n).unwrap()) + pub fn reserved(n: u128) -> Option { + Some(Rune(Self::RESERVED.checked_add(n)?)) } - pub(crate) fn commitment(self) -> Vec { + pub fn commitment(self) -> Vec { let bytes = self.0.to_le_bytes(); let mut end = bytes.len(); @@ -118,26 +135,41 @@ impl Display for Rune { impl FromStr for Rune { type Err = Error; - fn from_str(s: &str) -> crate::Result { + fn from_str(s: &str) -> Result { let mut x = 0u128; for (i, c) in s.chars().enumerate() { if i > 0 { x += 1; } - x = x.checked_mul(26).ok_or_else(|| anyhow!("out of range"))?; + x = x.checked_mul(26).ok_or(Error::Range)?; match c { 'A'..='Z' => { - x = x - .checked_add(c as u128 - 'A' as u128) - .ok_or_else(|| anyhow!("out of range"))?; + x = x.checked_add(c as u128 - 'A' as u128).ok_or(Error::Range)?; } - _ => bail!("invalid character in rune name: {c}"), + _ => return Err(Error::Character(c)), } } Ok(Rune(x)) } } +#[derive(Debug, PartialEq)] +pub enum Error { + Character(char), + Range, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Character(c) => write!(f, "invalid character `{c}`"), + Self::Range => write!(f, "name out of range"), + } + } +} + +impl std::error::Error for Error {} + #[cfg(test)] mod tests { use super::*; @@ -185,8 +217,12 @@ mod tests { } #[test] - fn from_str_out_of_range() { - "BCGDENLQRQWDSLRUGSNLBTMFIJAW".parse::().unwrap_err(); + fn from_str_error() { + assert_eq!( + "BCGDENLQRQWDSLRUGSNLBTMFIJAW".parse::().unwrap_err(), + Error::Range + ); + assert_eq!("x".parse::().unwrap_err(), Error::Character('x')); } #[test] @@ -197,7 +233,7 @@ mod tests { #[track_caller] fn case(height: u32, minimum: &str) { assert_eq!( - Rune::minimum_at_height(Chain::Mainnet, Height(height)).to_string(), + Rune::minimum_at_height(Network::Bitcoin, Height(height)).to_string(), minimum, ); } @@ -278,35 +314,35 @@ mod tests { #[test] fn minimum_at_height() { #[track_caller] - fn case(chain: Chain, height: u32, minimum: &str) { + fn case(network: Network, height: u32, minimum: &str) { assert_eq!( - Rune::minimum_at_height(chain, Height(height)).to_string(), + Rune::minimum_at_height(network, Height(height)).to_string(), minimum, ); } - case(Chain::Testnet, 0, "AAAAAAAAAAAAA"); + case(Network::Testnet, 0, "AAAAAAAAAAAAA"); case( - Chain::Testnet, + Network::Testnet, SUBSIDY_HALVING_INTERVAL * 12 - 1, "AAAAAAAAAAAAA", ); case( - Chain::Testnet, + Network::Testnet, SUBSIDY_HALVING_INTERVAL * 12, "ZZYZXBRKWXVA", ); case( - Chain::Testnet, + Network::Testnet, SUBSIDY_HALVING_INTERVAL * 12 + 1, "ZZXZUDIVTVQA", ); - case(Chain::Signet, 0, "ZZYZXBRKWXVA"); - case(Chain::Signet, 1, "ZZXZUDIVTVQA"); + case(Network::Signet, 0, "ZZYZXBRKWXVA"); + case(Network::Signet, 1, "ZZXZUDIVTVQA"); - case(Chain::Regtest, 0, "ZZYZXBRKWXVA"); - case(Chain::Regtest, 1, "ZZXZUDIVTVQA"); + case(Network::Regtest, 0, "ZZYZXBRKWXVA"); + case(Network::Regtest, 1, "ZZXZUDIVTVQA"); } #[test] @@ -320,12 +356,17 @@ mod tests { #[test] fn reserved() { assert_eq!( - RESERVED, + Rune::RESERVED, "AAAAAAAAAAAAAAAAAAAAAAAAAAA".parse::().unwrap().0, ); - assert_eq!(Rune::reserved(0), Rune(RESERVED)); - assert_eq!(Rune::reserved(1), Rune(RESERVED + 1)); + assert_eq!(Rune::reserved(0), Some(Rune(Rune::RESERVED))); + assert_eq!(Rune::reserved(1), Some(Rune(Rune::RESERVED + 1))); + assert_eq!( + Rune::reserved(u128::MAX - Rune::RESERVED), + Some(Rune(u128::MAX)) + ); + assert_eq!(Rune::reserved(u128::MAX - Rune::RESERVED + 1), None); } #[test] diff --git a/src/runes/rune_id.rs b/crates/ordinals/src/rune_id.rs similarity index 64% rename from src/runes/rune_id.rs rename to crates/ordinals/src/rune_id.rs index d50546e5d0..0c82a0047a 100644 --- a/src/runes/rune_id.rs +++ b/crates/ordinals/src/rune_id.rs @@ -19,7 +19,7 @@ pub struct RuneId { } impl RuneId { - pub(crate) fn new(block: u64, tx: u32) -> Option { + pub fn new(block: u64, tx: u32) -> Option { let id = RuneId { block, tx }; if id.block == 0 && id.tx > 0 { @@ -29,7 +29,7 @@ impl RuneId { Some(id) } - pub(crate) fn delta(self, next: RuneId) -> Option<(u128, u128)> { + pub fn delta(self, next: RuneId) -> Option<(u128, u128)> { let block = next.block.checked_sub(self.block)?; let tx = if block == 0 { @@ -41,7 +41,7 @@ impl RuneId { Some((block.into(), tx.into())) } - pub(crate) fn next(self: RuneId, block: u128, tx: u128) -> Option { + pub fn next(self: RuneId, block: u128, tx: u128) -> Option { RuneId::new( self.block.checked_add(block.try_into().ok()?)?, if block == 0 { @@ -51,32 +51,11 @@ impl RuneId { }, ) } - - pub(crate) fn encode_balance(self, balance: u128, buffer: &mut Vec) { - varint::encode_to_vec(self.block.into(), buffer); - varint::encode_to_vec(self.tx.into(), buffer); - varint::encode_to_vec(balance, buffer); - } - - pub(crate) fn decode_balance(buffer: &[u8]) -> Option<((RuneId, u128), usize)> { - let mut len = 0; - let (block, block_len) = varint::decode(&buffer[len..])?; - len += block_len; - let (tx, tx_len) = varint::decode(&buffer[len..])?; - len += tx_len; - let id = RuneId { - block: block.try_into().ok()?, - tx: tx.try_into().ok()?, - }; - let (balance, balance_len) = varint::decode(&buffer[len..])?; - len += balance_len; - Some(((id, balance), len)) - } } impl Display for RuneId { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}:{}", self.block, self.tx,) + write!(f, "{}:{}", self.block, self.tx) } } @@ -84,17 +63,34 @@ impl FromStr for RuneId { type Err = Error; fn from_str(s: &str) -> Result { - let (height, index) = s - .split_once(':') - .ok_or_else(|| anyhow!("invalid rune ID: {s}"))?; + let (height, index) = s.split_once(':').ok_or(Error::Separator)?; Ok(Self { - block: height.parse()?, - tx: index.parse()?, + block: height.parse().map_err(Error::Block)?, + tx: index.parse().map_err(Error::Transaction)?, }) } } +#[derive(Debug, PartialEq)] +pub enum Error { + Separator, + Block(ParseIntError), + Transaction(ParseIntError), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Separator => write!(f, "missing separator"), + Self::Block(err) => write!(f, "invalid height: {err}"), + Self::Transaction(err) => write!(f, "invalid index: {err}"), + } + } +} + +impl std::error::Error for Error {} + #[cfg(test)] mod tests { use super::*; @@ -149,11 +145,15 @@ mod tests { #[test] fn from_str() { - assert!(":".parse::().is_err()); - assert!("1:".parse::().is_err()); - assert!(":2".parse::().is_err()); - assert!("a:2".parse::().is_err()); - assert!("1:a".parse::().is_err()); + assert!(matches!("123".parse::(), Err(Error::Separator))); + assert!(matches!(":".parse::(), Err(Error::Block(_)))); + assert!(matches!("1:".parse::(), Err(Error::Transaction(_)))); + assert!(matches!(":2".parse::(), Err(Error::Block(_)))); + assert!(matches!("a:2".parse::(), Err(Error::Block(_)))); + assert!(matches!( + "1:a".parse::(), + Err(Error::Transaction(_)), + )); assert_eq!("1:2".parse::().unwrap(), RuneId { block: 1, tx: 2 }); } diff --git a/src/runes/runestone.rs b/crates/ordinals/src/runestone.rs similarity index 87% rename from src/runes/runestone.rs rename to crates/ordinals/src/runestone.rs index dcd972e4fc..ed768b9e2c 100644 --- a/src/runes/runestone.rs +++ b/crates/ordinals/src/runestone.rs @@ -1,102 +1,55 @@ -use super::*; +use {super::*, flag::Flag, message::Message, tag::Tag}; -const MAX_SPACERS: u32 = 0b00000111_11111111_11111111_11111111; +mod flag; +mod message; +mod tag; #[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Runestone { - pub cenotaph: bool, + pub cenotaph: u32, pub edicts: Vec, pub etching: Option, pub mint: Option, pub pointer: Option, } -struct Message { - cenotaph: bool, - edicts: Vec, - fields: HashMap>, -} - #[derive(Debug, PartialEq)] enum Payload { Valid(Vec), - Invalid, -} - -impl Message { - fn from_integers(tx: &Transaction, payload: &[u128]) -> Self { - let mut edicts = Vec::new(); - let mut fields = HashMap::>::new(); - let mut cenotaph = false; - - for i in (0..payload.len()).step_by(2) { - let tag = payload[i]; - - if Tag::Body == tag { - let mut id = RuneId::default(); - for chunk in payload[i + 1..].chunks(4) { - if chunk.len() != 4 { - cenotaph = true; - break; - } - - let Some(next) = id.next(chunk[0], chunk[1]) else { - cenotaph = true; - break; - }; - - let Some(edict) = Edict::from_integers(tx, next, chunk[2], chunk[3]) else { - cenotaph = true; - break; - }; - - id = next; - edicts.push(edict); - } - break; - } - - let Some(&value) = payload.get(i + 1) else { - cenotaph = true; - break; - }; - - fields.entry(tag).or_default().push_back(value); - } - - Self { - cenotaph, - edicts, - fields, - } - } + Invalid(Cenotaph), } impl Runestone { + pub const MAGIC_NUMBER: opcodes::All = opcodes::all::OP_PUSHNUM_13; + pub fn from_transaction(transaction: &Transaction) -> Option { Self::decipher(transaction).ok().flatten() } - fn cenotaph() -> Self { - Self { - cenotaph: true, - ..default() - } + pub fn is_cenotaph(&self) -> bool { + self.cenotaph != 0 + } + + pub fn cenotaph_reasons(&self) -> Vec { + Cenotaph::ALL + .into_iter() + .filter(|cenotaph| self.cenotaph & cenotaph.flag() != 0) + .collect() } fn decipher(transaction: &Transaction) -> Result, script::Error> { let payload = match Runestone::payload(transaction)? { Some(Payload::Valid(payload)) => payload, - Some(Payload::Invalid) => return Ok(Some(Self::cenotaph())), + Some(Payload::Invalid(cenotaph)) => return Ok(Some(cenotaph.into())), None => return Ok(None), }; let Some(integers) = Runestone::integers(&payload) else { - return Ok(Some(Self::cenotaph())); + return Ok(Some(Cenotaph::Varint.into())); }; let Message { - cenotaph, + mut cenotaph, edicts, mut fields, } = Message::from_integers(transaction, &integers); @@ -107,12 +60,12 @@ impl Runestone { let pointer = Tag::Pointer.take(&mut fields, |[pointer]| { let pointer = u32::try_from(pointer).ok()?; - (pointer.into_usize() < transaction.output.len()).then_some(pointer) + (u64::from(pointer) < u64::try_from(transaction.output.len()).unwrap()).then_some(pointer) }); let divisibility = Tag::Divisibility.take(&mut fields, |[divisibility]| { let divisibility = u8::try_from(divisibility).ok()?; - (divisibility <= MAX_DIVISIBILITY).then_some(divisibility) + (divisibility <= Etching::MAX_DIVISIBILITY).then_some(divisibility) }); let amount = Tag::Amount.take(&mut fields, |[amount]| Some(amount)); @@ -125,7 +78,7 @@ impl Runestone { let spacers = Tag::Spacers.take(&mut fields, |[spacers]| { let spacers = u32::try_from(spacers).ok()?; - (spacers <= MAX_SPACERS).then_some(spacers) + (spacers <= Etching::MAX_SPACERS).then_some(spacers) }); let symbol = Tag::Symbol.take(&mut fields, |[symbol]| { @@ -178,8 +131,20 @@ impl Runestone { }), }); + if overflow { + cenotaph |= Cenotaph::SupplyOverflow.flag(); + } + + if flags != 0 { + cenotaph |= Cenotaph::UnrecognizedFlag.flag(); + } + + if fields.keys().any(|tag| tag % 2 == 0) { + cenotaph |= Cenotaph::UnrecognizedEvenTag.flag(); + } + Ok(Some(Self { - cenotaph: cenotaph || overflow || flags != 0 || fields.keys().any(|tag| tag % 2 == 0), + cenotaph, edicts, etching, mint, @@ -222,7 +187,7 @@ impl Runestone { Tag::Pointer.encode_option(self.pointer, &mut payload); - if self.cenotaph { + if self.is_cenotaph() { Tag::Cenotaph.encode([0], &mut payload); } @@ -245,7 +210,7 @@ impl Runestone { let mut builder = script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) - .push_opcode(MAGIC_NUMBER); + .push_opcode(Runestone::MAGIC_NUMBER); for chunk in payload.chunks(MAX_SCRIPT_ELEMENT_SIZE) { let push: &script::PushBytes = chunk.try_into().unwrap(); @@ -267,7 +232,9 @@ impl Runestone { // followed by the protocol identifier, ignoring errors, since OP_RETURN // scripts may be invalid - if instructions.next().transpose().ok().flatten() != Some(Instruction::Op(MAGIC_NUMBER)) { + if instructions.next().transpose().ok().flatten() + != Some(Instruction::Op(Runestone::MAGIC_NUMBER)) + { continue; } @@ -278,7 +245,7 @@ impl Runestone { if let Ok(Instruction::PushBytes(push)) = result { payload.extend_from_slice(push.as_bytes()); } else { - return Ok(Some(Payload::Invalid)); + return Ok(Some(Payload::Invalid(Cenotaph::Opcode))); } } @@ -304,7 +271,17 @@ impl Runestone { #[cfg(test)] mod tests { - use {super::*, bitcoin::script::PushBytes}; + use { + super::*, + bitcoin::{ + blockdata::locktime::absolute::LockTime, script::PushBytes, Sequence, TxIn, TxOut, Witness, + }, + pretty_assertions::assert_eq, + }; + + pub(crate) fn rune_id(tx: u32) -> RuneId { + RuneId { block: 1, tx } + } fn decipher(integers: &[u128]) -> Runestone { let payload = payload(integers); @@ -316,7 +293,7 @@ mod tests { output: vec![TxOut { script_pubkey: script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) - .push_opcode(MAGIC_NUMBER) + .push_opcode(Runestone::MAGIC_NUMBER) .push_slice(payload) .into_script(), value: 0, @@ -438,7 +415,7 @@ mod tests { fn deciphering_valid_runestone_with_invalid_script_postfix_returns_invalid_payload() { let mut script_pubkey = script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) - .push_opcode(MAGIC_NUMBER) + .push_opcode(Runestone::MAGIC_NUMBER) .into_script() .into_bytes(); @@ -454,7 +431,7 @@ mod tests { lock_time: LockTime::ZERO, version: 2, }), - Ok(Some(Payload::Invalid)) + Ok(Some(Payload::Invalid(Cenotaph::Opcode))) ); } @@ -465,7 +442,7 @@ mod tests { output: vec![TxOut { script_pubkey: script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) - .push_opcode(MAGIC_NUMBER) + .push_opcode(Runestone::MAGIC_NUMBER) .push_slice([128]) .into_script(), value: 0, @@ -485,7 +462,7 @@ mod tests { TxOut { script_pubkey: script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) - .push_opcode(MAGIC_NUMBER) + .push_opcode(Runestone::MAGIC_NUMBER) .push_opcode(opcodes::all::OP_VERIFY) .push_slice([0]) .push_slice::<&PushBytes>(varint::encode(1).as_slice().try_into().unwrap()) @@ -497,7 +474,7 @@ mod tests { TxOut { script_pubkey: script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) - .push_opcode(MAGIC_NUMBER) + .push_opcode(Runestone::MAGIC_NUMBER) .push_slice([0]) .push_slice::<&PushBytes>(varint::encode(1).as_slice().try_into().unwrap()) .push_slice::<&PushBytes>(varint::encode(2).as_slice().try_into().unwrap()) @@ -511,10 +488,7 @@ mod tests { }) .unwrap() .unwrap(), - Runestone { - cenotaph: true, - ..default() - } + Cenotaph::Opcode.into(), ); } @@ -526,7 +500,7 @@ mod tests { output: vec![TxOut { script_pubkey: script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) - .push_opcode(MAGIC_NUMBER) + .push_opcode(Runestone::MAGIC_NUMBER) .into_script(), value: 0 }], @@ -546,7 +520,7 @@ mod tests { let script_pubkey = vec![ opcodes::all::OP_RETURN.to_u8(), opcodes::all::OP_PUSHBYTES_9.to_u8(), - MAGIC_NUMBER.to_u8(), + Runestone::MAGIC_NUMBER.to_u8(), opcodes::all::OP_PUSHBYTES_4.to_u8(), ]; @@ -561,7 +535,7 @@ mod tests { TxOut { script_pubkey: script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) - .push_opcode(MAGIC_NUMBER) + .push_opcode(Runestone::MAGIC_NUMBER) .push_slice(payload) .into_script(), value: 0, @@ -620,7 +594,7 @@ mod tests { #[test] fn decipher_etching_with_rune() { - pretty_assert_eq!( + assert_eq!( decipher(&[ Tag::Flags.into(), Flag::Etching.mask(), @@ -649,7 +623,7 @@ mod tests { #[test] fn etch_flag_is_required_to_etch_rune_even_if_mint_is_set() { - pretty_assert_eq!( + assert_eq!( decipher(&[ Tag::Flags.into(), Flag::Terms.mask(), @@ -674,7 +648,7 @@ mod tests { #[test] fn decipher_etching_with_term() { - pretty_assert_eq!( + assert_eq!( decipher(&[ Tag::Flags.into(), Flag::Etching.mask() | Flag::Terms.mask(), @@ -738,13 +712,13 @@ mod tests { #[test] fn invalid_varint_produces_cenotaph() { - pretty_assert_eq!( + assert_eq!( Runestone::decipher(&Transaction { input: Vec::new(), output: vec![TxOut { script_pubkey: script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) - .push_opcode(MAGIC_NUMBER) + .push_opcode(Runestone::MAGIC_NUMBER) .push_slice([128]) .into_script(), value: 0, @@ -754,16 +728,13 @@ mod tests { }) .unwrap() .unwrap(), - Runestone { - cenotaph: true, - ..default() - } + Cenotaph::Varint.into(), ); } #[test] fn duplicate_even_tags_produce_cenotaph() { - pretty_assert_eq!( + assert_eq!( decipher(&[ Tag::Flags.into(), Flag::Etching.mask(), @@ -787,7 +758,7 @@ mod tests { rune: Some(Rune(4)), ..default() }), - cenotaph: true, + cenotaph: Cenotaph::UnrecognizedEvenTag.flag(), ..default() } ); @@ -795,7 +766,7 @@ mod tests { #[test] fn duplicate_odd_tags_are_ignored() { - pretty_assert_eq!( + assert_eq!( decipher(&[ Tag::Flags.into(), Flag::Etching.mask(), @@ -850,7 +821,7 @@ mod tests { amount: 2, output: 0, }], - cenotaph: true, + cenotaph: Cenotaph::UnrecognizedEvenTag.flag(), ..default() }, ); @@ -874,7 +845,7 @@ mod tests { amount: 2, output: 0, }], - cenotaph: true, + cenotaph: Cenotaph::UnrecognizedFlag.flag(), ..default() }, ); @@ -882,11 +853,40 @@ mod tests { #[test] fn runestone_with_edict_id_with_zero_block_and_nonzero_tx_is_cenotaph() { - pretty_assert_eq!( + assert_eq!( decipher(&[Tag::Body.into(), 0, 1, 2, 0]), Runestone { edicts: Vec::new(), - cenotaph: true, + cenotaph: Cenotaph::EdictRuneId.into(), + ..default() + }, + ); + } + + #[test] + fn runestone_with_overflowing_edict_id_delta_is_cenotaph() { + assert_eq!( + decipher(&[Tag::Body.into(), 1, 0, 0, 0, u64::MAX.into(), 0, 0, 0]), + Runestone { + edicts: vec![Edict { + id: RuneId::new(1, 0).unwrap(), + amount: 0, + output: 0, + }], + cenotaph: Cenotaph::EdictRuneId.into(), + ..default() + }, + ); + + assert_eq!( + decipher(&[Tag::Body.into(), 1, 1, 0, 0, 0, u64::MAX.into(), 0, 0]), + Runestone { + edicts: vec![Edict { + id: RuneId::new(1, 1).unwrap(), + amount: 0, + output: 0, + }], + cenotaph: Cenotaph::EdictRuneId.into(), ..default() }, ); @@ -894,11 +894,11 @@ mod tests { #[test] fn runestone_with_output_over_max_is_cenotaph() { - pretty_assert_eq!( + assert_eq!( decipher(&[Tag::Body.into(), 1, 1, 2, 2]), Runestone { edicts: Vec::new(), - cenotaph: true, + cenotaph: Cenotaph::EdictOutput.into(), ..default() }, ); @@ -910,7 +910,7 @@ mod tests { decipher(&[Tag::Flags.into(), 1, Tag::Flags.into()]), Runestone { etching: Some(Etching::default()), - cenotaph: true, + cenotaph: Cenotaph::TruncatedField.flag(), ..default() }, ); @@ -921,10 +921,14 @@ mod tests { let mut integers = vec![Tag::Body.into(), 1, 1, 2, 0]; for i in 0..4 { - pretty_assert_eq!( + assert_eq!( decipher(&integers), Runestone { - cenotaph: i > 0, + cenotaph: if i > 0 { + Cenotaph::TrailingIntegers.flag() + } else { + 0 + }, edicts: vec![Edict { id: rune_id(1), amount: 2, @@ -979,7 +983,7 @@ mod tests { Tag::Rune.into(), 4, Tag::Divisibility.into(), - (MAX_DIVISIBILITY + 1).into(), + (Etching::MAX_DIVISIBILITY + 1).into(), Tag::Body.into(), 1, 1, @@ -1029,7 +1033,7 @@ mod tests { #[test] fn decipher_etching_with_symbol() { - pretty_assert_eq!( + assert_eq!( decipher(&[ Tag::Flags.into(), Flag::Etching.mask(), @@ -1061,7 +1065,7 @@ mod tests { #[test] fn decipher_etching_with_all_etching_tags() { - pretty_assert_eq!( + assert_eq!( decipher(&[ Tag::Flags.into(), Flag::Etching.mask() | Flag::Terms.mask(), @@ -1112,7 +1116,7 @@ mod tests { symbol: Some('a'), spacers: Some(5), }), - cenotaph: false, + cenotaph: 0, pointer: Some(0), mint: Some(RuneId::new(1, 1).unwrap()), }, @@ -1187,7 +1191,7 @@ mod tests { #[test] fn tag_values_are_not_parsed_as_tags() { - pretty_assert_eq!( + assert_eq!( decipher(&[ Tag::Flags.into(), Flag::Etching.mask(), @@ -1216,7 +1220,7 @@ mod tests { #[test] fn runestone_may_contain_multiple_edicts() { - pretty_assert_eq!( + assert_eq!( decipher(&[Tag::Body.into(), 1, 1, 2, 0, 0, 3, 5, 0]), Runestone { edicts: vec![ @@ -1238,7 +1242,7 @@ mod tests { #[test] fn runestones_with_invalid_rune_id_blocks_are_cenotaph() { - pretty_assert_eq!( + assert_eq!( decipher(&[Tag::Body.into(), 1, 1, 2, 0, u128::MAX, 1, 0, 0,]), Runestone { edicts: vec![Edict { @@ -1246,7 +1250,7 @@ mod tests { amount: 2, output: 0, }], - cenotaph: true, + cenotaph: Cenotaph::EdictRuneId.flag(), ..default() }, ); @@ -1254,7 +1258,7 @@ mod tests { #[test] fn runestones_with_invalid_rune_id_txs_are_cenotaph() { - pretty_assert_eq!( + assert_eq!( decipher(&[Tag::Body.into(), 1, 1, 2, 0, 1, u128::MAX, 0, 0,]), Runestone { edicts: vec![Edict { @@ -1262,7 +1266,7 @@ mod tests { amount: 2, output: 0, }], - cenotaph: true, + cenotaph: Cenotaph::EdictRuneId.flag(), ..default() }, ); @@ -1276,7 +1280,7 @@ mod tests { output: vec![TxOut { script_pubkey: script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) - .push_opcode(MAGIC_NUMBER) + .push_opcode(Runestone::MAGIC_NUMBER) .push_slice::<&PushBytes>( varint::encode(Tag::Flags.into()) .as_slice() @@ -1344,7 +1348,7 @@ mod tests { TxOut { script_pubkey: script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) - .push_opcode(MAGIC_NUMBER) + .push_opcode(Runestone::MAGIC_NUMBER) .push_slice(payload) .into_script(), value: 0 @@ -1384,7 +1388,7 @@ mod tests { TxOut { script_pubkey: script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) - .push_opcode(MAGIC_NUMBER) + .push_opcode(Runestone::MAGIC_NUMBER) .push_slice(payload) .into_script(), value: 0 @@ -1434,7 +1438,7 @@ mod tests { case( Vec::new(), Some(Etching { - divisibility: Some(MAX_DIVISIBILITY), + divisibility: Some(Etching::MAX_DIVISIBILITY), rune: Some(Rune(0)), ..default() }), @@ -1444,7 +1448,7 @@ mod tests { case( Vec::new(), Some(Etching { - divisibility: Some(MAX_DIVISIBILITY), + divisibility: Some(Etching::MAX_DIVISIBILITY), terms: Some(Terms { cap: Some(u32::MAX.into()), amount: Some(u64::MAX.into()), @@ -1454,7 +1458,7 @@ mod tests { premine: Some(u64::MAX.into()), rune: Some(Rune(u128::MAX)), symbol: Some('\u{10FFFF}'), - spacers: Some(MAX_SPACERS), + spacers: Some(Etching::MAX_SPACERS), }), 89, ); @@ -1475,7 +1479,7 @@ mod tests { output: 0, }], Some(Etching { - divisibility: Some(MAX_DIVISIBILITY), + divisibility: Some(Etching::MAX_DIVISIBILITY), rune: Some(Rune(u128::MAX)), ..default() }), @@ -1489,7 +1493,7 @@ mod tests { output: 0, }], Some(Etching { - divisibility: Some(MAX_DIVISIBILITY), + divisibility: Some(Etching::MAX_DIVISIBILITY), rune: Some(Rune(u128::MAX)), ..default() }), @@ -1651,8 +1655,8 @@ mod tests { u128::from(u64::MAX) + 1, ]), Runestone { - etching: Some(Etching::default()), - cenotaph: true, + etching: Some(default()), + cenotaph: Cenotaph::UnrecognizedEvenTag.into(), ..default() }, ); @@ -1678,7 +1682,7 @@ mod tests { panic!("invalid payload") }; - pretty_assert_eq!(Runestone::integers(&payload).unwrap(), expected); + assert_eq!(Runestone::integers(&payload).unwrap(), expected); let runestone = { let mut edicts = runestone.edicts; @@ -1689,7 +1693,7 @@ mod tests { } }; - pretty_assert_eq!( + assert_eq!( Runestone::from_transaction(&transaction).unwrap(), runestone ); @@ -1699,7 +1703,7 @@ mod tests { case( Runestone { - cenotaph: true, + cenotaph: Cenotaph::UnrecognizedEvenTag.into(), edicts: vec![ Edict { id: RuneId::new(2, 3).unwrap(), @@ -1783,7 +1787,7 @@ mod tests { rune: Some(Rune(3)), spacers: None, }), - cenotaph: false, + cenotaph: 0, ..default() }, &[Tag::Flags.into(), Flag::Etching.mask(), Tag::Rune.into(), 3], @@ -1799,7 +1803,7 @@ mod tests { rune: None, spacers: None, }), - cenotaph: false, + cenotaph: 0, ..default() }, &[Tag::Flags.into(), Flag::Etching.mask()], @@ -1807,7 +1811,7 @@ mod tests { case( Runestone { - cenotaph: true, + cenotaph: Cenotaph::UnrecognizedEvenTag.into(), ..default() }, &[Tag::Cenotaph.into(), 0], @@ -1847,77 +1851,83 @@ mod tests { assert_eq!(script.instructions().count(), 4); } - #[test] - fn max_spacers() { - let mut rune = String::new(); - - for (i, c) in Rune(u128::MAX).to_string().chars().enumerate() { - if i > 0 { - rune.push('•'); - } - - rune.push(c); - } - - assert_eq!(MAX_SPACERS, rune.parse::().unwrap().spacers); - } - #[test] fn edict_output_greater_than_32_max_produces_cenotaph() { - assert!(decipher(&[Tag::Body.into(), 1, 1, 1, u128::from(u32::MAX) + 1]).cenotaph); + assert_eq!( + decipher(&[Tag::Body.into(), 1, 1, 1, u128::from(u32::MAX) + 1]).cenotaph, + Cenotaph::EdictOutput.flag() + ); } #[test] fn partial_mint_produces_cenotaph() { - assert!(decipher(&[Tag::Mint.into(), 1]).cenotaph); + assert_eq!( + decipher(&[Tag::Mint.into(), 1]).cenotaph, + Cenotaph::UnrecognizedEvenTag.flag() + ); } #[test] fn invalid_mint_produces_cenotaph() { - assert!(decipher(&[Tag::Mint.into(), 0, Tag::Mint.into(), 1]).cenotaph); + assert_eq!( + decipher(&[Tag::Mint.into(), 0, Tag::Mint.into(), 1]).cenotaph, + Cenotaph::UnrecognizedEvenTag.flag() + ); } #[test] fn invalid_deadline_produces_cenotaph() { - assert!(decipher(&[Tag::OffsetEnd.into(), u128::MAX]).cenotaph); + assert_eq!( + decipher(&[Tag::OffsetEnd.into(), u128::MAX]).cenotaph, + Cenotaph::UnrecognizedEvenTag.flag() + ); } #[test] fn invalid_default_output_produces_cenotaph() { - assert!(decipher(&[Tag::Pointer.into(), 1]).cenotaph); - assert!(decipher(&[Tag::Pointer.into(), u128::MAX]).cenotaph); + assert_eq!( + decipher(&[Tag::Pointer.into(), 1]).cenotaph, + Cenotaph::UnrecognizedEvenTag.flag() + ); + assert_eq!( + decipher(&[Tag::Pointer.into(), u128::MAX]).cenotaph, + Cenotaph::UnrecognizedEvenTag.flag() + ); } #[test] fn invalid_divisibility_does_not_produce_cenotaph() { - assert!(!decipher(&[Tag::Divisibility.into(), u128::MAX]).cenotaph); + assert!(!decipher(&[Tag::Divisibility.into(), u128::MAX]).is_cenotaph()); } #[test] fn min_and_max_runes_are_not_cenotaphs() { - assert!(!decipher(&[Tag::Rune.into(), 0]).cenotaph); - assert!(!decipher(&[Tag::Rune.into(), u128::MAX]).cenotaph); + assert!(!decipher(&[Tag::Rune.into(), 0]).is_cenotaph()); + assert!(!decipher(&[Tag::Rune.into(), u128::MAX]).is_cenotaph()); } #[test] fn invalid_spacers_does_not_produce_cenotaph() { - assert!(!decipher(&[Tag::Spacers.into(), u128::MAX]).cenotaph); + assert!(!decipher(&[Tag::Spacers.into(), u128::MAX]).is_cenotaph()); } #[test] fn invalid_symbol_does_not_produce_cenotaph() { - assert!(!decipher(&[Tag::Symbol.into(), u128::MAX]).cenotaph); + assert!(!decipher(&[Tag::Symbol.into(), u128::MAX]).is_cenotaph()); } #[test] fn invalid_term_produces_cenotaph() { - assert!(decipher(&[Tag::OffsetEnd.into(), u128::MAX]).cenotaph); + assert_eq!( + decipher(&[Tag::OffsetEnd.into(), u128::MAX]).cenotaph, + Cenotaph::UnrecognizedEvenTag.flag() + ); } #[test] fn invalid_supply_produces_cenotaph() { - assert!( - !decipher(&[ + assert_eq!( + decipher(&[ Tag::Flags.into(), Flag::Etching.mask() | Flag::Terms.mask(), Tag::Cap.into(), @@ -1925,10 +1935,11 @@ mod tests { Tag::Amount.into(), u128::MAX ]) - .cenotaph + .cenotaph, + 0, ); - assert!( + assert_eq!( decipher(&[ Tag::Flags.into(), Flag::Etching.mask() | Flag::Terms.mask(), @@ -1937,10 +1948,11 @@ mod tests { Tag::Amount.into(), u128::MAX ]) - .cenotaph + .cenotaph, + Cenotaph::SupplyOverflow.flag(), ); - assert!( + assert_eq!( decipher(&[ Tag::Flags.into(), Flag::Etching.mask() | Flag::Terms.mask(), @@ -1949,10 +1961,11 @@ mod tests { Tag::Amount.into(), u128::MAX / 2 + 1 ]) - .cenotaph + .cenotaph, + Cenotaph::SupplyOverflow.flag(), ); - assert!( + assert_eq!( decipher(&[ Tag::Flags.into(), Flag::Etching.mask() | Flag::Terms.mask(), @@ -1963,7 +1976,8 @@ mod tests { Tag::Amount.into(), u128::MAX ]) - .cenotaph + .cenotaph, + Cenotaph::SupplyOverflow.flag(), ); } @@ -2001,7 +2015,7 @@ mod tests { output: vec![TxOut { script_pubkey: ScriptBuf::from(vec![ opcodes::all::OP_RETURN.to_u8(), - MAGIC_NUMBER.to_u8(), + Runestone::MAGIC_NUMBER.to_u8(), opcodes::all::OP_PUSHBYTES_4.to_u8(), ]), value: 0, @@ -2011,7 +2025,7 @@ mod tests { assert_eq!( Runestone::decipher(&transaction).unwrap(), Some(Runestone { - cenotaph: true, + cenotaph: Cenotaph::Opcode.into(), ..default() }) ); diff --git a/src/runes/flag.rs b/crates/ordinals/src/runestone/flag.rs similarity index 100% rename from src/runes/flag.rs rename to crates/ordinals/src/runestone/flag.rs diff --git a/crates/ordinals/src/runestone/message.rs b/crates/ordinals/src/runestone/message.rs new file mode 100644 index 0000000000..f31b03d7eb --- /dev/null +++ b/crates/ordinals/src/runestone/message.rs @@ -0,0 +1,56 @@ +use super::*; + +pub(super) struct Message { + pub(super) cenotaph: u32, + pub(super) edicts: Vec, + pub(super) fields: HashMap>, +} + +impl Message { + pub(super) fn from_integers(tx: &Transaction, payload: &[u128]) -> Self { + let mut edicts = Vec::new(); + let mut fields = HashMap::>::new(); + let mut cenotaph = 0; + + for i in (0..payload.len()).step_by(2) { + let tag = payload[i]; + + if Tag::Body == tag { + let mut id = RuneId::default(); + for chunk in payload[i + 1..].chunks(4) { + if chunk.len() != 4 { + cenotaph |= Cenotaph::TrailingIntegers.flag(); + break; + } + + let Some(next) = id.next(chunk[0], chunk[1]) else { + cenotaph |= Cenotaph::EdictRuneId.flag(); + break; + }; + + let Some(edict) = Edict::from_integers(tx, next, chunk[2], chunk[3]) else { + cenotaph |= Cenotaph::EdictOutput.flag(); + break; + }; + + id = next; + edicts.push(edict); + } + break; + } + + let Some(&value) = payload.get(i + 1) else { + cenotaph |= Cenotaph::TruncatedField.flag(); + break; + }; + + fields.entry(tag).or_default().push_back(value); + } + + Self { + cenotaph, + edicts, + fields, + } + } +} diff --git a/src/runes/tag.rs b/crates/ordinals/src/runestone/tag.rs similarity index 100% rename from src/runes/tag.rs rename to crates/ordinals/src/runestone/tag.rs diff --git a/src/runes/spaced_rune.rs b/crates/ordinals/src/spaced_rune.rs similarity index 66% rename from src/runes/spaced_rune.rs rename to crates/ordinals/src/spaced_rune.rs index e609952389..72c19125c8 100644 --- a/src/runes/spaced_rune.rs +++ b/crates/ordinals/src/spaced_rune.rs @@ -25,22 +25,22 @@ impl FromStr for SpacedRune { match c { 'A'..='Z' => rune.push(c), '.' | '•' => { - let flag = 1 << rune.len().checked_sub(1).context("leading spacer")?; + let flag = 1 << rune.len().checked_sub(1).ok_or(Error::LeadingSpacer)?; if spacers & flag != 0 { - bail!("double spacer"); + return Err(Error::DoubleSpacer); } spacers |= flag; } - _ => bail!("invalid character"), + _ => return Err(Error::Character(c)), } } if 32 - spacers.leading_zeros() >= rune.len().try_into().unwrap() { - bail!("trailing spacer") + return Err(Error::TrailingSpacer); } Ok(SpacedRune { - rune: rune.parse()?, + rune: rune.parse().map_err(Error::Rune)?, spacers, }) } @@ -62,6 +62,29 @@ impl Display for SpacedRune { } } +#[derive(Debug, PartialEq)] +pub enum Error { + LeadingSpacer, + TrailingSpacer, + DoubleSpacer, + Character(char), + Rune(rune::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Character(c) => write!(f, "invalid character `{c}`"), + Self::DoubleSpacer => write!(f, "double spacer"), + Self::LeadingSpacer => write!(f, "leading spacer"), + Self::TrailingSpacer => write!(f, "trailing spacer"), + Self::Rune(err) => write!(f, "{err}"), + } + } +} + +impl std::error::Error for Error {} + #[cfg(test)] mod tests { use super::*; @@ -94,23 +117,23 @@ mod tests { } assert_eq!( - ".A".parse::().unwrap_err().to_string(), - "leading spacer", + ".A".parse::().unwrap_err(), + Error::LeadingSpacer, ); assert_eq!( - "A..B".parse::().unwrap_err().to_string(), - "double spacer", + "A..B".parse::().unwrap_err(), + Error::DoubleSpacer, ); assert_eq!( - "A.".parse::().unwrap_err().to_string(), - "trailing spacer", + "A.".parse::().unwrap_err(), + Error::TrailingSpacer, ); assert_eq!( - "Ax".parse::().unwrap_err().to_string(), - "invalid character", + "Ax".parse::().unwrap_err(), + Error::Character('x') ); case("A.B", "AB", 0b1); diff --git a/src/runes/terms.rs b/crates/ordinals/src/terms.rs similarity index 100% rename from src/runes/terms.rs rename to crates/ordinals/src/terms.rs diff --git a/src/runes/varint.rs b/crates/ordinals/src/varint.rs similarity index 92% rename from src/runes/varint.rs rename to crates/ordinals/src/varint.rs index 3c2d8cad9c..cef2086a5e 100644 --- a/src/runes/varint.rs +++ b/crates/ordinals/src/varint.rs @@ -1,3 +1,5 @@ +use super::*; + pub fn encode_to_vec(mut n: u128, v: &mut Vec) { while n >> 7 > 0 { v.push(n.to_le_bytes()[0] | 0b1000_0000); @@ -10,13 +12,6 @@ pub fn decode(buffer: &[u8]) -> Option<(u128, usize)> { try_decode(buffer).ok() } -#[derive(PartialEq, Debug)] -enum Error { - Overlong, - Overflow, - Unterminated, -} - fn try_decode(buffer: &[u8]) -> Result<(u128, usize), Error> { let mut n = 0u128; @@ -41,13 +36,31 @@ fn try_decode(buffer: &[u8]) -> Result<(u128, usize), Error> { Err(Error::Unterminated) } -#[cfg(test)] pub fn encode(n: u128) -> Vec { let mut v = Vec::new(); encode_to_vec(n, &mut v); v } +#[derive(PartialEq, Debug)] +enum Error { + Overlong, + Overflow, + Unterminated, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Overlong => write!(f, "too long"), + Self::Overflow => write!(f, "overflow"), + Self::Unterminated => write!(f, "unterminated"), + } + } +} + +impl std::error::Error for Error {} + #[cfg(test)] mod tests { use super::*; diff --git a/src/chain.rs b/src/chain.rs index fe925a2d63..d21b05aa61 100644 --- a/src/chain.rs +++ b/src/chain.rs @@ -14,12 +14,7 @@ pub enum Chain { impl Chain { pub(crate) fn network(self) -> Network { - match self { - Self::Mainnet => Network::Bitcoin, - Self::Testnet => Network::Testnet, - Self::Signet => Network::Signet, - Self::Regtest => Network::Regtest, - } + self.into() } pub(crate) fn default_rpc_port(self) -> u16 { @@ -48,13 +43,7 @@ impl Chain { } pub(crate) fn first_rune_height(self) -> u32 { - SUBSIDY_HALVING_INTERVAL - * match self { - Self::Mainnet => 4, - Self::Regtest => 0, - Self::Signet => 0, - Self::Testnet => 12, - } + Rune::first_rune_height(self.into()) } pub(crate) fn jubilee_height(self) -> u32 { @@ -94,6 +83,17 @@ impl Chain { } } +impl From for Network { + fn from(chain: Chain) -> Network { + match chain { + Chain::Mainnet => Network::Bitcoin, + Chain::Testnet => Network::Testnet, + Chain::Signet => Network::Signet, + Chain::Regtest => Network::Regtest, + } + } +} + impl Display for Chain { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!( diff --git a/src/index.rs b/src/index.rs index 582b7519be..d599bd2a7f 100644 --- a/src/index.rs +++ b/src/index.rs @@ -490,7 +490,7 @@ impl Index { inscriptions: blessed_inscriptions + cursed_inscriptions, lost_sats: statistic(Statistic::LostSats)?, minimum_rune_for_next_block: Rune::minimum_at_height( - self.settings.chain(), + self.settings.chain().network(), Height(next_height), ), rune_index: statistic(Statistic::IndexRunes)? != 0, @@ -887,6 +887,27 @@ impl Index { Ok(entries) } + pub(crate) fn encode_rune_balance(id: RuneId, balance: u128, buffer: &mut Vec) { + varint::encode_to_vec(id.block.into(), buffer); + varint::encode_to_vec(id.tx.into(), buffer); + varint::encode_to_vec(balance, buffer); + } + + pub(crate) fn decode_rune_balance(buffer: &[u8]) -> Option<((RuneId, u128), usize)> { + let mut len = 0; + let (block, block_len) = varint::decode(&buffer[len..])?; + len += block_len; + let (tx, tx_len) = varint::decode(&buffer[len..])?; + len += tx_len; + let id = RuneId { + block: block.try_into().ok()?, + tx: tx.try_into().ok()?, + }; + let (balance, balance_len) = varint::decode(&buffer[len..])?; + len += balance_len; + Some(((id, balance), len)) + } + pub(crate) fn get_rune_balances_for_outpoint( &self, outpoint: OutPoint, @@ -906,7 +927,7 @@ impl Index { let mut balances = Vec::new(); let mut i = 0; while i < balances_buffer.len() { - let ((id, amount), length) = RuneId::decode_balance(&balances_buffer[i..]).unwrap(); + let ((id, amount), length) = Index::decode_rune_balance(&balances_buffer[i..]).unwrap(); i += length; let entry = RuneEntry::load(id_to_rune_entries.get(id.store())?.unwrap().value()); @@ -997,7 +1018,7 @@ impl Index { let mut balances = Vec::new(); let mut i = 0; while i < balances_buffer.len() { - let ((id, balance), length) = RuneId::decode_balance(&balances_buffer[i..]).unwrap(); + let ((id, balance), length) = Index::decode_rune_balance(&balances_buffer[i..]).unwrap(); i += length; balances.push((id, balance)); } diff --git a/src/index/updater.rs b/src/index/updater.rs index bf1507b0ca..694934b04d 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -597,7 +597,10 @@ impl<'index> Updater<'index> { height: self.height, id_to_entry: &mut rune_id_to_rune_entry, inscription_id_to_sequence_number: &mut inscription_id_to_sequence_number, - minimum: Rune::minimum_at_height(self.index.settings.chain(), Height(self.height)), + minimum: Rune::minimum_at_height( + self.index.settings.chain().network(), + Height(self.height), + ), outpoint_to_balances: &mut outpoint_to_rune_balances, rune_to_id: &mut rune_to_rune_id, runes, diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index 45bdecbda7..a1eff1e6a5 100644 --- a/src/index/updater/rune_updater.rs +++ b/src/index/updater/rune_updater.rs @@ -1,7 +1,4 @@ -use { - super::*, - crate::runes::{Edict, Runestone}, -}; +use super::*; struct Mint { id: RuneId, @@ -41,7 +38,7 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { let cenotaph = runestone .as_ref() - .map(|runestone| runestone.cenotaph) + .map(|runestone| runestone.is_cenotaph()) .unwrap_or_default(); let pointer = runestone.as_ref().and_then(|runestone| runestone.pointer); @@ -196,7 +193,7 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { balances.sort(); for (id, balance) in balances { - id.encode_balance(balance, &mut buffer); + Index::encode_rune_balance(id, balance, &mut buffer); } self.outpoint_to_balances.insert( @@ -311,7 +308,7 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { .statistic_to_count .insert(&Statistic::ReservedRunes.into(), reserved_runes + 1)?; - Rune::reserved(reserved_runes.into()) + Rune::reserved(reserved_runes.into()).unwrap() }; Ok(Some(Etched { @@ -415,7 +412,7 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { let buffer = guard.value(); let mut i = 0; while i < buffer.len() { - let ((id, balance), len) = RuneId::decode_balance(&buffer[i..]).unwrap(); + let ((id, balance), len) = Index::decode_rune_balance(&buffer[i..]).unwrap(); i += len; *unallocated.entry(id).or_default() += balance; } diff --git a/src/lib.rs b/src/lib.rs index 5d8be3a777..b741df41d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,6 @@ use { }, into_usize::IntoUsize, representation::Representation, - runes::Terms, settings::Settings, subcommand::{Subcommand, SubcommandResult}, tally::Tally, @@ -41,10 +40,8 @@ use { consensus::{self, Decodable, Encodable}, hash_types::{BlockHash, TxMerkleNode}, hashes::Hash, - opcodes, - script::{self, Instruction}, - Amount, Block, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, - Witness, + script, Amount, Block, Network, OutPoint, Script, ScriptBuf, Sequence, Transaction, TxIn, + TxOut, Txid, Witness, }, bitcoincore_rpc::{Client, RpcApi}, chrono::{DateTime, TimeZone, Utc}, @@ -53,7 +50,10 @@ use { html_escaper::{Escape, Trusted}, http::HeaderMap, lazy_static::lazy_static, - ordinals::{Charm, Epoch, Height, Rarity, Sat, SatPoint}, + ordinals::{ + varint, Charm, Edict, Epoch, Etching, Height, Pile, Rarity, Rune, RuneId, Runestone, Sat, + SatPoint, SpacedRune, Terms, + }, regex::Regex, reqwest::Url, serde::{Deserialize, Deserializer, Serialize}, @@ -88,7 +88,6 @@ pub use self::{ inscriptions::{Envelope, Inscription, InscriptionId}, object::Object, options::Options, - runes::{Edict, Pile, Rune, RuneId, Runestone, SpacedRune}, wallet::transaction_builder::{Target, TransactionBuilder}, }; diff --git a/src/runes.rs b/src/runes.rs index 1c9628d4d5..5352e8b36f 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -1,31 +1,4 @@ -use { - self::{flag::Flag, tag::Tag}, - super::*, -}; - -pub use { - edict::Edict, etching::Etching, pile::Pile, rune::Rune, rune_id::RuneId, runestone::Runestone, - spaced_rune::SpacedRune, terms::Terms, -}; - -pub const MAX_DIVISIBILITY: u8 = 38; - -const MAGIC_NUMBER: opcodes::All = opcodes::all::OP_PUSHNUM_13; -const RESERVED: u128 = 6402364363415443603228541259936211926; - -mod edict; -mod etching; -mod flag; -mod pile; -mod rune; -mod rune_id; -mod runestone; -mod spaced_rune; -mod tag; -mod terms; -pub mod varint; - -type Result = std::result::Result; +use super::*; #[derive(Debug, PartialEq)] pub enum MintError { @@ -172,7 +145,8 @@ mod tests { #[test] fn runes_must_be_greater_than_or_equal_to_minimum_for_height() { - let minimum = Rune::minimum_at_height(Chain::Regtest, Height(RUNE_COMMIT_INTERVAL + 2)).0; + let minimum = + Rune::minimum_at_height(Chain::Regtest.network(), Height(RUNE_COMMIT_INTERVAL + 2)).0; { let context = Context::builder() @@ -256,7 +230,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Some(Rune(RESERVED)), + rune: Some(Rune::reserved(0).unwrap()), ..default() }), ..default() @@ -278,7 +252,7 @@ mod tests { output: 0, }], etching: Some(Etching { - rune: Some(Rune(RESERVED - 1)), + rune: Some(Rune(Rune::reserved(0).unwrap().n() - 1)), premine: Some(u128::MAX), ..default() }), @@ -294,7 +268,7 @@ mod tests { block: id.block, etching: txid, spaced_rune: SpacedRune { - rune: Rune(RESERVED - 1), + rune: Rune(Rune::reserved(0).unwrap().n() - 1), spacers: 0, }, premine: u128::MAX, @@ -346,7 +320,7 @@ mod tests { block: id0.block, etching: txid0, spaced_rune: SpacedRune { - rune: Rune(RESERVED), + rune: Rune::reserved(0).unwrap(), spacers: 0, }, premine: u128::MAX, @@ -398,7 +372,7 @@ mod tests { block: id0.block, etching: txid0, spaced_rune: SpacedRune { - rune: Rune(RESERVED), + rune: Rune::reserved(0).unwrap(), spacers: 0, }, premine: u128::MAX, @@ -412,7 +386,7 @@ mod tests { block: id1.block, etching: txid1, spaced_rune: SpacedRune { - rune: Rune(RESERVED + 1), + rune: Rune::reserved(1).unwrap(), spacers: 0, }, premine: u128::MAX, @@ -820,7 +794,7 @@ mod tests { ..default() }), pointer: None, - cenotaph: true, + cenotaph: 1, ..default() }, 1, @@ -868,7 +842,7 @@ mod tests { symbol: Some('$'), spacers: Some(1), }), - cenotaph: true, + cenotaph: 1, ..default() }, 1, @@ -914,7 +888,7 @@ mod tests { output: 0, }], etching: Some(Etching::default()), - cenotaph: true, + cenotaph: 1, ..default() } .encipher(), @@ -933,7 +907,7 @@ mod tests { block: id.block, etching: txid0, spaced_rune: SpacedRune { - rune: Rune(RESERVED), + rune: Rune::reserved(0).unwrap(), spacers: 0, }, timestamp: id.block, @@ -993,7 +967,7 @@ mod tests { inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some( Runestone { - cenotaph: true, + cenotaph: 1, ..default() } .encipher(), @@ -4105,7 +4079,7 @@ mod tests { inputs: &[(5, 0, 0, Witness::new())], op_return: Some( Runestone { - cenotaph: true, + cenotaph: 1, mint: Some(id), edicts: vec![Edict { id, diff --git a/src/runes/etching.rs b/src/runes/etching.rs deleted file mode 100644 index 79ee7908aa..0000000000 --- a/src/runes/etching.rs +++ /dev/null @@ -1,11 +0,0 @@ -use super::*; - -#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Copy, Clone, Eq)] -pub struct Etching { - pub divisibility: Option, - pub premine: Option, - pub rune: Option, - pub spacers: Option, - pub symbol: Option, - pub terms: Option, -} diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 92de3206c3..1540089afd 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1893,12 +1893,7 @@ impl Server { #[cfg(test)] mod tests { use { - super::*, - crate::runes::{Edict, Etching, Rune, Runestone}, - reqwest::Url, - serde::de::DeserializeOwned, - std::net::TcpListener, - tempfile::TempDir, + super::*, reqwest::Url, serde::de::DeserializeOwned, std::net::TcpListener, tempfile::TempDir, }; const RUNE: u128 = 99246114928149462; diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index b963bac0ea..d5975277ac 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -221,7 +221,7 @@ impl Inscribe { ensure!(!rune.is_reserved(), "rune `{rune}` is reserved"); ensure!( - etching.divisibility <= crate::runes::MAX_DIVISIBILITY, + etching.divisibility <= Etching::MAX_DIVISIBILITY, " must be less than or equal 38" ); @@ -303,7 +303,7 @@ impl Inscribe { ); } - let minimum = Rune::minimum_at_height(wallet.chain(), Height(reveal_height)); + let minimum = Rune::minimum_at_height(wallet.chain().into(), Height(reveal_height)); ensure!( rune >= minimum, diff --git a/src/templates/rune.rs b/src/templates/rune.rs index fbcfbbd0a8..e772ff83f1 100644 --- a/src/templates/rune.rs +++ b/src/templates/rune.rs @@ -16,7 +16,7 @@ impl PageContent for RuneHtml { #[cfg(test)] mod tests { - use {super::*, crate::runes::Rune}; + use super::*; #[test] fn display() { diff --git a/src/test.rs b/src/test.rs index ace0269a28..a9d89bb6a3 100644 --- a/src/test.rs +++ b/src/test.rs @@ -3,7 +3,7 @@ pub(crate) use { bitcoin::{ blockdata::script::{PushBytes, PushBytesBuf}, constants::COIN_VALUE, - WPubkeyHash, + opcodes, WPubkeyHash, }, pretty_assertions::assert_eq as pretty_assert_eq, std::iter, @@ -131,10 +131,6 @@ pub(crate) fn inscription_id(n: u32) -> InscriptionId { format!("{}i{n}", hex.repeat(64)).parse().unwrap() } -pub(crate) fn rune_id(tx: u32) -> RuneId { - RuneId { block: 1, tx } -} - pub(crate) fn envelope(payload: &[&[u8]]) -> Witness { let mut builder = script::Builder::new() .push_opcode(opcodes::OP_FALSE) diff --git a/src/wallet/batch/plan.rs b/src/wallet/batch/plan.rs index 680ebd3d1f..700a8bc4c1 100644 --- a/src/wallet/batch/plan.rs +++ b/src/wallet/batch/plan.rs @@ -457,14 +457,14 @@ impl Plan { } let inner = Runestone { - cenotaph: false, + cenotaph: 0, edicts, - etching: Some(runes::Etching { + etching: Some(ordinals::Etching { divisibility: (etching.divisibility > 0).then_some(etching.divisibility), terms: etching .terms - .map(|terms| -> Result { - Ok(runes::Terms { + .map(|terms| -> Result { + Ok(ordinals::Terms { cap: (terms.cap > 0).then_some(terms.cap), height: ( terms.height.and_then(|range| (range.start)), diff --git a/tests/lib.rs b/tests/lib.rs index 92d60e0a76..9722833306 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -11,10 +11,10 @@ use { chrono::{DateTime, Utc}, executable_path::executable_path, ord::{ - api, chain::Chain, outgoing::Outgoing, subcommand::runes::RuneInfo, wallet::batch, Edict, - InscriptionId, Pile, Rune, RuneEntry, RuneId, Runestone, SpacedRune, + api, chain::Chain, outgoing::Outgoing, subcommand::runes::RuneInfo, wallet::batch, + InscriptionId, RuneEntry, }, - ordinals::{Charm, Rarity, Sat, SatPoint}, + ordinals::{Charm, Edict, Pile, Rarity, Rune, RuneId, Runestone, Sat, SatPoint, SpacedRune}, pretty_assertions::assert_eq as pretty_assert_eq, regex::Regex, reqwest::{StatusCode, Url}, diff --git a/tests/wallet/inscribe.rs b/tests/wallet/inscribe.rs index 755e98c9ac..acc73106fb 100644 --- a/tests/wallet/inscribe.rs +++ b/tests/wallet/inscribe.rs @@ -2935,7 +2935,7 @@ fn etch_reserved_rune_error() { etching: Some(batch::Etching { divisibility: 0, rune: SpacedRune { - rune: Rune::reserved(0), + rune: Rune::reserved(0).unwrap(), spacers: 0, }, premine: "1000".parse().unwrap(), diff --git a/tests/wallet/mint.rs b/tests/wallet/mint.rs index ba3a4a0102..89df32f2ec 100644 --- a/tests/wallet/mint.rs +++ b/tests/wallet/mint.rs @@ -1,7 +1,4 @@ -use { - super::*, - ord::{runes::Pile, subcommand::wallet::mint}, -}; +use {super::*, ord::subcommand::wallet::mint}; #[test] fn minting_rune_and_fails_if_after_end() { diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index bbdb93bfb6..6bd13c7435 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -1110,7 +1110,7 @@ fn sending_rune_creates_transaction_with_expected_runestone() { amount: 750, output: 2 }], - cenotaph: false, + cenotaph: 0, mint: None, }, );