From 0d521d0cb51e7701493185e1a690f52a2875bf94 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 26 Mar 2024 18:30:49 -0700 Subject: [PATCH] Mint terms (#3375) --- src/blocktime.rs | 2 +- src/index.rs | 5 +- src/index/entry.rs | 345 ++++++++--- src/index/testing.rs | 2 +- src/index/updater.rs | 2 +- src/index/updater/rune_updater.rs | 44 +- src/lib.rs | 9 +- src/runes.rs | 755 +++++++++++-------------- src/runes/etching.rs | 2 +- src/runes/flag.rs | 12 +- src/runes/mint.rs | 9 - src/runes/rune_id.rs | 4 +- src/runes/runestone.rs | 333 ++++++----- src/runes/tag.rs | 22 +- src/runes/terms.rs | 9 + src/subcommand/runes.rs | 11 +- src/subcommand/server.rs | 23 +- src/subcommand/wallet/inscribe.rs | 152 +++-- src/subcommand/wallet/mint.rs | 11 +- src/templates/rune.rs | 23 +- src/wallet/batch.rs | 7 +- src/wallet/batch/etching.rs | 2 +- src/wallet/batch/plan.rs | 26 +- src/wallet/batch/range.rs | 8 + src/wallet/batch/{mint.rs => terms.rs} | 8 +- templates/block.html | 2 +- templates/rune.html | 16 +- tests/json_api.rs | 12 +- tests/lib.rs | 75 ++- tests/runes.rs | 6 +- tests/wallet/balance.rs | 4 +- tests/wallet/inscribe.rs | 608 +++++++++++++++++++- tests/wallet/mint.rs | 78 +-- tests/wallet/selection.rs | 6 +- tests/wallet/send.rs | 6 +- 35 files changed, 1647 insertions(+), 992 deletions(-) delete mode 100644 src/runes/mint.rs create mode 100644 src/runes/terms.rs create mode 100644 src/wallet/batch/range.rs rename src/wallet/batch/{mint.rs => terms.rs} (67%) diff --git a/src/blocktime.rs b/src/blocktime.rs index 0908bed309..c65eb0bd96 100644 --- a/src/blocktime.rs +++ b/src/blocktime.rs @@ -8,7 +8,7 @@ pub(crate) enum Blocktime { impl Blocktime { pub(crate) fn confirmed(seconds: u32) -> Self { - Self::Confirmed(timestamp(seconds)) + Self::Confirmed(timestamp(seconds.into())) } pub(crate) fn timestamp(self) -> DateTime { diff --git a/src/index.rs b/src/index.rs index b87ff832f8..582b7519be 100644 --- a/src/index.rs +++ b/src/index.rs @@ -6,7 +6,6 @@ use { }, event::Event, reorg::*, - runes::{Rune, RuneId}, updater::Updater, }, super::*, @@ -35,7 +34,7 @@ use { }, }; -pub use {self::entry::RuneEntry, entry::MintEntry}; +pub use self::entry::RuneEntry; pub(crate) mod entry; pub mod event; @@ -47,7 +46,7 @@ mod updater; #[cfg(test)] pub(crate) mod testing; -const SCHEMA_VERSION: u64 = 23; +const SCHEMA_VERSION: u64 = 24; macro_rules! define_table { ($name:ident, $key:ty, $value:ty) => { diff --git a/src/index/entry.rs b/src/index/entry.rs index e32d107f5b..9d655c28d4 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -30,47 +30,48 @@ impl Entry for Header { #[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)] pub struct RuneEntry { + pub block: u64, pub burned: u128, pub divisibility: u8, pub etching: Txid, - pub mint: Option, pub mints: u128, pub number: u64, pub premine: u128, pub spaced_rune: SpacedRune, pub symbol: Option, - pub timestamp: u32, + pub terms: Option, + pub timestamp: u64, } impl RuneEntry { - pub fn mintable(&self, block_height: Height, block_time: u32) -> Result { - let Some(mint) = self.mint else { + pub fn mintable(&self, height: u64) -> Result { + let Some(terms) = self.terms else { return Err(MintError::Unmintable); }; - if let Some(end) = mint.end { - if block_height.0 >= end { - return Err(MintError::End(end)); + if let Some(start) = self.start() { + if height < start { + return Err(MintError::Start(start)); } } - if let Some(deadline) = mint.deadline { - if block_time >= deadline { - return Err(MintError::Deadline(deadline)); + if let Some(end) = self.end() { + if height >= end { + return Err(MintError::End(end)); } } - let cap = mint.cap.unwrap_or_default(); + let cap = terms.cap.unwrap_or_default(); if self.mints >= cap { return Err(MintError::Cap(cap)); } - Ok(mint.limit.unwrap_or_default()) + Ok(terms.limit.unwrap_or_default()) } pub fn supply(&self) -> u128 { - self.premine + self.mints * self.mint.and_then(|mint| mint.limit).unwrap_or_default() + self.premine + self.mints * self.terms.and_then(|terms| terms.limit).unwrap_or_default() } pub fn pile(&self, amount: u128) -> Pile { @@ -80,48 +81,76 @@ impl RuneEntry { symbol: self.symbol, } } -} -pub(super) type RuneEntryValue = ( - u128, // burned - u8, // divisibility - (u128, u128), // etching - Option, // mint parameters - u128, // mints - u64, // number - u128, // premine - (u128, u32), // spaced rune - Option, // symbol - u32, // timestamp -); + pub fn start(&self) -> Option { + let terms = self.terms?; + + let relative = terms + .offset + .0 + .map(|offset| self.block.saturating_add(offset)); -#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize, Default)] -pub struct MintEntry { - pub cap: Option, // mint cap - pub deadline: Option, // unix timestamp - pub end: Option, // block height - pub limit: Option, // claim amount + let absolute = terms.height.0; + + relative + .zip(absolute) + .map(|(relative, absolute)| relative.max(absolute)) + .or(relative) + .or(absolute) + } + + pub fn end(&self) -> Option { + let terms = self.terms?; + + let relative = terms + .offset + .1 + .map(|offset| self.block.saturating_add(offset)); + + let absolute = terms.height.1; + + relative + .zip(absolute) + .map(|(relative, absolute)| relative.min(absolute)) + .or(relative) + .or(absolute) + } } -type MintEntryValue = ( - Option, // cap - Option, // deadline - Option, // end - Option, // limit +type TermsEntryValue = ( + Option, // cap + (Option, Option), // height + Option, // limit + (Option, Option), // offset +); + +pub(super) type RuneEntryValue = ( + u64, // block + u128, // burned + u8, // divisibility + (u128, u128), // etching + u128, // mints + u64, // number + u128, // premine + (u128, u32), // spaced rune + Option, // symbol + Option, // terms + u64, // timestamp ); impl Default for RuneEntry { fn default() -> Self { Self { + block: 0, burned: 0, divisibility: 0, etching: Txid::all_zeros(), - mint: None, mints: 0, number: 0, premine: 0, spaced_rune: SpacedRune::default(), symbol: None, + terms: None, timestamp: 0, } } @@ -132,19 +161,21 @@ impl Entry for RuneEntry { fn load( ( + block, burned, divisibility, etching, - mint, mints, number, premine, (rune, spacers), symbol, + terms, timestamp, ): RuneEntryValue, ) -> Self { Self { + block, burned, divisibility, etching: { @@ -157,12 +188,6 @@ impl Entry for RuneEntry { high[14], high[15], ]) }, - mint: mint.map(|(cap, deadline, end, limit)| MintEntry { - cap, - deadline, - end, - limit, - }), mints, number, premine, @@ -171,12 +196,19 @@ impl Entry for RuneEntry { spacers, }, symbol, + terms: terms.map(|(cap, height, limit, offset)| Terms { + cap, + height, + limit, + offset, + }), timestamp, } } fn store(self) -> Self::Value { ( + self.block, self.burned, self.divisibility, { @@ -192,25 +224,25 @@ impl Entry for RuneEntry { ]), ) }, - self.mint.map( - |MintEntry { - cap, - deadline, - end, - limit, - }| (cap, deadline, end, limit), - ), self.mints, self.number, self.premine, (self.spaced_rune.rune.0, self.spaced_rune.spacers), self.symbol, + self.terms.map( + |Terms { + cap, + height, + limit, + offset, + }| (cap, height, limit, offset), + ), self.timestamp, ) } } -pub(super) type RuneIdValue = (u32, u32); +pub(super) type RuneIdValue = (u64, u32); impl Entry for RuneId { type Value = RuneIdValue; @@ -500,6 +532,7 @@ mod tests { #[test] fn rune_entry() { let entry = RuneEntry { + block: 12, burned: 1, divisibility: 3, etching: Txid::from_byte_array([ @@ -507,11 +540,11 @@ mod tests { 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, ]), - mint: Some(MintEntry { + terms: Some(Terms { cap: Some(1), - deadline: Some(2), - end: Some(4), - limit: Some(5), + height: (Some(2), Some(3)), + limit: Some(4), + offset: (Some(5), Some(6)), }), mints: 11, number: 6, @@ -525,18 +558,19 @@ mod tests { }; let value = ( + 12, 1, 3, ( 0x0F0E0D0C0B0A09080706050403020100, 0x1F1E1D1C1B1A19181716151413121110, ), - Some((Some(1), Some(2), Some(4), Some(5))), 11, 6, 12, (7, 8), Some('a'), + Some((Some(1), (Some(2), Some(3)), Some(4), (Some(5), Some(6)))), 10, ); @@ -567,116 +601,243 @@ mod tests { } #[test] - fn mintable() { + fn mintable_default() { + assert_eq!(RuneEntry::default().mintable(0), Err(MintError::Unmintable)); + } + + #[test] + fn mintable_cap() { assert_eq!( - RuneEntry::default().mintable(Height(0), 0), - Err(MintError::Unmintable) + RuneEntry { + terms: Some(Terms { + cap: Some(1), + limit: Some(1000), + ..default() + }), + mints: 0, + ..default() + } + .mintable(0), + Ok(1000), ); assert_eq!( RuneEntry { - mint: Some(MintEntry { - end: Some(1), - limit: Some(1000), + terms: Some(Terms { cap: Some(1), + limit: Some(1000), ..default() }), + mints: 1, ..default() } - .mintable(Height(0), 0), - Ok(1000), + .mintable(0), + Err(MintError::Cap(1)), ); assert_eq!( RuneEntry { - mint: Some(MintEntry { - end: Some(1), + terms: Some(Terms { + cap: None, limit: Some(1000), + ..default() + }), + mints: 0, + ..default() + } + .mintable(0), + Err(MintError::Cap(0)), + ); + } + + #[test] + fn mintable_offset_start() { + assert_eq!( + RuneEntry { + block: 1, + terms: Some(Terms { cap: Some(1), + limit: Some(1000), + offset: (Some(1), None), ..default() }), + mints: 0, ..default() } - .mintable(Height(1), 0), - Err(MintError::End(1)), + .mintable(1), + Err(MintError::Start(2)), ); assert_eq!( RuneEntry { - mint: Some(MintEntry { - deadline: Some(1), + block: 1, + terms: Some(Terms { + cap: Some(1), limit: Some(1000), + offset: (Some(1), None), + ..default() + }), + mints: 0, + ..default() + } + .mintable(2), + Ok(1000), + ); + } + + #[test] + fn mintable_offset_end() { + assert_eq!( + RuneEntry { + block: 1, + terms: Some(Terms { cap: Some(1), + limit: Some(1000), + offset: (None, Some(1)), ..default() }), + mints: 0, ..default() } - .mintable(Height(0), 0), + .mintable(1), Ok(1000), ); assert_eq!( RuneEntry { - mint: Some(MintEntry { - deadline: Some(1), + block: 1, + terms: Some(Terms { + cap: Some(1), limit: Some(1000), + offset: (None, Some(1)), + ..default() + }), + mints: 0, + ..default() + } + .mintable(2), + Err(MintError::End(2)), + ); + } + + #[test] + fn mintable_height_start() { + assert_eq!( + RuneEntry { + terms: Some(Terms { cap: Some(1), + limit: Some(1000), + height: (Some(1), None), ..default() }), + mints: 0, ..default() } - .mintable(Height(0), 1), - Err(MintError::Deadline(1)), + .mintable(0), + Err(MintError::Start(1)), ); assert_eq!( RuneEntry { - mint: Some(MintEntry { + terms: Some(Terms { cap: Some(1), limit: Some(1000), + height: (Some(1), None), ..default() }), mints: 0, ..default() } - .mintable(Height(0), 0), + .mintable(1), Ok(1000), ); + } + #[test] + fn mintable_height_end() { assert_eq!( RuneEntry { - mint: Some(MintEntry { + terms: Some(Terms { cap: Some(1), limit: Some(1000), + height: (None, Some(1)), ..default() }), - mints: 1, + mints: 0, ..default() } - .mintable(Height(0), 0), - Err(MintError::Cap(1)), + .mintable(0), + Ok(1000), ); assert_eq!( RuneEntry { - mint: Some(MintEntry { - cap: None, + terms: Some(Terms { + cap: Some(1), limit: Some(1000), + height: (None, Some(1)), ..default() }), mints: 0, ..default() } - .mintable(Height(0), 0), - Err(MintError::Cap(0)), + .mintable(1), + Err(MintError::End(1)), ); } + #[test] + fn mintable_multiple_terms() { + let entry = RuneEntry { + terms: Some(Terms { + cap: Some(1), + limit: Some(1000), + height: (Some(10), Some(20)), + offset: (Some(0), Some(10)), + }), + block: 10, + mints: 0, + ..default() + }; + + assert_eq!(entry.mintable(10), Ok(1000)); + + { + let mut entry = entry; + entry.terms.as_mut().unwrap().cap = None; + assert_eq!(entry.mintable(10), Err(MintError::Cap(0))); + } + + { + let mut entry = entry; + entry.terms.as_mut().unwrap().height.0 = Some(11); + assert_eq!(entry.mintable(10), Err(MintError::Start(11))); + } + + { + let mut entry = entry; + entry.terms.as_mut().unwrap().height.1 = Some(10); + assert_eq!(entry.mintable(10), Err(MintError::End(10))); + } + + { + let mut entry = entry; + entry.terms.as_mut().unwrap().offset.0 = Some(1); + assert_eq!(entry.mintable(10), Err(MintError::Start(11))); + } + + { + let mut entry = entry; + entry.terms.as_mut().unwrap().offset.1 = Some(0); + assert_eq!(entry.mintable(10), Err(MintError::End(10))); + } + } + #[test] fn supply() { assert_eq!( RuneEntry { - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), ..default() }), @@ -689,7 +850,7 @@ mod tests { assert_eq!( RuneEntry { - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), ..default() }), @@ -702,7 +863,7 @@ mod tests { assert_eq!( RuneEntry { - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), ..default() }), @@ -716,7 +877,7 @@ mod tests { assert_eq!( RuneEntry { - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), ..default() }), diff --git a/src/index/testing.rs b/src/index/testing.rs index dd0c5ed8ab..8d8fed3d6e 100644 --- a/src/index/testing.rs +++ b/src/index/testing.rs @@ -199,7 +199,7 @@ impl Context { ( txid, RuneId { - block: u32::try_from(block_count + usize::try_from(RUNE_COMMIT_INTERVAL).unwrap() + 1) + block: u64::try_from(block_count + usize::try_from(RUNE_COMMIT_INTERVAL).unwrap() + 1) .unwrap(), tx: 1, }, diff --git a/src/index/updater.rs b/src/index/updater.rs index 5e21331cc3..bf1507b0ca 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -321,7 +321,7 @@ impl<'index> Updater<'index> { log::info!( "Block {} at {} with {} transactions…", self.height, - timestamp(block.header.time), + timestamp(block.header.time.into()), block.txdata.len() ); diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index 5f2b6e8a57..58a5fdc13c 100644 --- a/src/index/updater/rune_updater.rs +++ b/src/index/updater/rune_updater.rs @@ -3,7 +3,7 @@ use { crate::runes::{Edict, Runestone}, }; -struct Claim { +struct Mint { id: RuneId, limit: u128, } @@ -11,10 +11,10 @@ struct Claim { struct Etched { divisibility: u8, id: RuneId, - mint: Option, premine: u128, spaced_rune: SpacedRune, symbol: Option, + terms: Option, } pub(super) struct RuneUpdater<'a, 'tx, 'client> { @@ -44,19 +44,17 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { .map(|runestone| runestone.cenotaph) .unwrap_or_default(); - let default_output = runestone - .as_ref() - .and_then(|runestone| runestone.default_output); + let pointer = runestone.as_ref().and_then(|runestone| runestone.pointer); let mut allocated: Vec> = vec![HashMap::new(); tx.output.len()]; if let Some(runestone) = runestone { - if let Some(claim) = runestone - .claim - .and_then(|id| self.claim(id).transpose()) + if let Some(mint) = runestone + .mint + .and_then(|id| self.mint(id).transpose()) .transpose()? { - *unallocated.entry(claim.id).or_default() += claim.limit; + *unallocated.entry(mint.id).or_default() += mint.limit; } let etched = self.etched(tx_index, tx, &runestone)?; @@ -150,9 +148,9 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { // assign all un-allocated runes to the default output, or the first non // OP_RETURN output if there is no default, or if the default output is // too large - if let Some(vout) = default_output - .map(|vout| vout.into_usize()) - .filter(|vout| *vout < allocated.len()) + if let Some(vout) = pointer + .map(|pointer| pointer.into_usize()) + .inspect(|&pointer| assert!(pointer < allocated.len())) .or_else(|| { tx.output .iter() @@ -233,10 +231,10 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { let Etched { divisibility, id, - mint, premine, spaced_rune, symbol, + terms, } = etched; self.rune_to_id.insert(spaced_rune.rune.0, id.store())?; @@ -254,16 +252,17 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { self.id_to_entry.insert( id.store(), RuneEntry { + block: id.block, burned: 0, divisibility, etching: txid, - mint: mint.and_then(|mint| (!burn).then_some(mint)), + terms: terms.and_then(|terms| (!burn).then_some(terms)), mints: 0, number, premine, spaced_rune, symbol, - timestamp: self.block_time, + timestamp: self.block_time.into(), } .store(), )?; @@ -318,32 +317,27 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { Ok(Some(Etched { divisibility: etching.divisibility.unwrap_or_default(), id: RuneId { - block: self.height, + block: self.height.into(), tx: tx_index, }, - mint: etching.mint.map(|mint| MintEntry { - cap: mint.cap, - deadline: mint.deadline, - end: mint.term.map(|term| term + self.height), - limit: mint.limit, - }), premine: etching.premine.unwrap_or_default(), spaced_rune: SpacedRune { rune, spacers: etching.spacers.unwrap_or_default(), }, symbol: etching.symbol, + terms: etching.terms, })) } - fn claim(&mut self, id: RuneId) -> Result> { + fn mint(&mut self, id: RuneId) -> Result> { let Some(entry) = self.id_to_entry.get(&id.store())? else { return Ok(None); }; let mut rune_entry = RuneEntry::load(entry.value()); - let Ok(limit) = rune_entry.mintable(Height(self.height), self.block_time) else { + let Ok(limit) = rune_entry.mintable(self.height.into()) else { return Ok(None); }; @@ -353,7 +347,7 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { self.id_to_entry.insert(&id.store(), rune_entry.store())?; - Ok(Some(Claim { id, limit })) + Ok(Some(Mint { id, limit })) } fn tx_commits_to_rune(&self, tx: &Transaction, rune: Rune) -> Result { diff --git a/src/lib.rs b/src/lib.rs index 64248d4bec..5d8be3a777 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ use { }, into_usize::IntoUsize, representation::Representation, + runes::Terms, settings::Settings, subcommand::{Subcommand, SubcommandResult}, tally::Tally, @@ -83,7 +84,7 @@ use { pub use self::{ chain::Chain, fee_rate::FeeRate, - index::{Index, MintEntry, RuneEntry}, + index::{Index, RuneEntry}, inscriptions::{Envelope, Inscription, InscriptionId}, object::Object, options::Options, @@ -185,8 +186,10 @@ fn fund_raw_transaction( ) } -pub fn timestamp(seconds: u32) -> DateTime { - Utc.timestamp_opt(seconds.into(), 0).unwrap() +pub fn timestamp(seconds: u64) -> DateTime { + Utc + .timestamp_opt(seconds.try_into().unwrap_or(i64::MAX), 0) + .unwrap() } fn target_as_block_hash(target: bitcoin::Target) -> BlockHash { diff --git a/src/runes.rs b/src/runes.rs index 6d03e69b5a..048653f3d8 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -4,8 +4,8 @@ use { }; pub use { - edict::Edict, etching::Etching, mint::Mint, pile::Pile, rune::Rune, rune_id::RuneId, - runestone::Runestone, spaced_rune::SpacedRune, + edict::Edict, etching::Etching, pile::Pile, rune::Rune, rune_id::RuneId, runestone::Runestone, + spaced_rune::SpacedRune, terms::Terms, }; pub const MAX_DIVISIBILITY: u8 = 38; @@ -16,13 +16,13 @@ const RESERVED: u128 = 6402364363415443603228541259936211926; mod edict; mod etching; mod flag; -mod mint; mod pile; mod rune; mod rune_id; mod runestone; mod spaced_rune; mod tag; +mod terms; pub mod varint; type Result = std::result::Result; @@ -30,8 +30,8 @@ type Result = std::result::Result; #[derive(Debug, PartialEq)] pub enum MintError { Cap(u128), - Deadline(u32), - End(u32), + End(u64), + Start(u64), Unmintable, } @@ -39,8 +39,8 @@ impl Display for MintError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { MintError::Cap(cap) => write!(f, "limited to {cap} mints"), - MintError::Deadline(deadline) => write!(f, "mint ended at {deadline}"), MintError::End(end) => write!(f, "mint ended on block {end}"), + MintError::Start(start) => write!(f, "mint starts on block {start}"), MintError::Unmintable => write!(f, "not mintable"), } } @@ -116,6 +116,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -154,6 +155,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -225,6 +227,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(minimum), @@ -288,6 +291,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RESERVED - 1), @@ -339,6 +343,7 @@ mod tests { [( id0, RuneEntry { + block: id0.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RESERVED), @@ -390,6 +395,7 @@ mod tests { ( id0, RuneEntry { + block: id0.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RESERVED), @@ -403,6 +409,7 @@ mod tests { ( id1, RuneEntry { + block: id1.block, etching: txid1, spaced_rune: SpacedRune { rune: Rune(RESERVED + 1), @@ -462,6 +469,7 @@ mod tests { [( id, RuneEntry { + block: id.block, spaced_rune: SpacedRune { rune: Rune(RUNE), spacers: 0, @@ -509,6 +517,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -555,6 +564,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -597,6 +607,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -643,6 +654,7 @@ mod tests { [( id, RuneEntry { + block: id.block, burned: 100, etching: txid, spaced_rune: SpacedRune { @@ -659,7 +671,7 @@ mod tests { } #[test] - fn allocations_to_invalid_outputs_burn_runestone() { + fn allocations_to_invalid_outputs_produce_cenotaph() { let context = Context::builder().arg("--index-runes").build(); let (txid, id) = context.etch( @@ -689,6 +701,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -728,6 +741,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -769,6 +783,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -804,7 +819,7 @@ mod tests { rune: Some(Rune(RUNE)), ..default() }), - default_output: None, + pointer: None, cenotaph: true, ..default() }, @@ -815,6 +830,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -842,11 +858,11 @@ mod tests { etching: Some(Etching { premine: Some(u128::MAX), rune: Some(Rune(RUNE)), - mint: Some(Mint { + terms: Some(Terms { cap: Some(1), - deadline: Some(1), limit: Some(1), - term: Some(1), + offset: (Some(1), Some(1)), + height: (None, None), }), divisibility: Some(1), symbol: Some('$'), @@ -862,10 +878,11 @@ mod tests { [( id, RuneEntry { + block: id.block, burned: u128::MAX, divisibility: 1, etching: txid0, - mint: None, + terms: None, mints: 0, number: 0, premine: u128::MAX, @@ -913,12 +930,13 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RESERVED), spacers: 0, }, - timestamp: 2, + timestamp: id.block, ..default() }, )], @@ -951,6 +969,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -988,6 +1007,7 @@ mod tests { [( id, RuneEntry { + block: id.block, burned: u128::MAX, etching: txid0, spaced_rune: SpacedRune { @@ -1028,6 +1048,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1059,6 +1080,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1104,6 +1126,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1136,6 +1159,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1176,6 +1200,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1200,7 +1225,7 @@ mod tests { outputs: 2, op_return: Some( Runestone { - default_output: Some(1), + pointer: Some(1), ..default() } .encipher(), @@ -1214,6 +1239,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1259,6 +1285,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1283,7 +1310,7 @@ mod tests { outputs: 2, op_return: Some( Runestone { - default_output: Some(2), + pointer: Some(2), ..default() } .encipher(), @@ -1297,6 +1324,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1338,6 +1366,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1369,6 +1398,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1414,6 +1444,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1449,6 +1480,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1488,6 +1520,7 @@ mod tests { [( id0, RuneEntry { + block: id0.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1529,6 +1562,7 @@ mod tests { ( id0, RuneEntry { + block: id0.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1542,6 +1576,7 @@ mod tests { ( id1, RuneEntry { + block: id1.block, etching: txid1, spaced_rune: SpacedRune { rune: Rune(RUNE + 1), @@ -1587,6 +1622,7 @@ mod tests { ( id0, RuneEntry { + block: id0.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1600,6 +1636,7 @@ mod tests { ( id1, RuneEntry { + block: id1.block, etching: txid1, spaced_rune: SpacedRune { rune: Rune(RUNE + 1), @@ -1647,6 +1684,7 @@ mod tests { [( id0, RuneEntry { + block: id0.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1688,6 +1726,7 @@ mod tests { ( id0, RuneEntry { + block: id0.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1701,6 +1740,7 @@ mod tests { ( id1, RuneEntry { + block: id1.block, etching: txid1, spaced_rune: SpacedRune { rune: Rune(RUNE + 1), @@ -1746,6 +1786,7 @@ mod tests { ( id0, RuneEntry { + block: id0.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1759,6 +1800,7 @@ mod tests { ( id1, RuneEntry { + block: id1.block, etching: txid1, spaced_rune: SpacedRune { rune: Rune(RUNE + 1), @@ -1811,6 +1853,7 @@ mod tests { ( id0, RuneEntry { + block: id0.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1824,6 +1867,7 @@ mod tests { ( id1, RuneEntry { + block: id1.block, etching: txid1, spaced_rune: SpacedRune { rune: Rune(RUNE + 1), @@ -1880,6 +1924,7 @@ mod tests { [( id0, RuneEntry { + block: id0.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1921,6 +1966,7 @@ mod tests { ( id0, RuneEntry { + block: id0.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -1934,6 +1980,7 @@ mod tests { ( id1, RuneEntry { + block: id1.block, etching: txid1, spaced_rune: SpacedRune { rune: Rune(RUNE + 1), @@ -1997,6 +2044,7 @@ mod tests { ( id0, RuneEntry { + block: id0.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2010,6 +2058,7 @@ mod tests { ( id1, RuneEntry { + block: id1.block, etching: txid1, spaced_rune: SpacedRune { rune: Rune(RUNE + 1), @@ -2058,6 +2107,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2094,6 +2144,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2151,6 +2202,7 @@ mod tests { ( id0, RuneEntry { + block: id0.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2164,6 +2216,7 @@ mod tests { ( id1, RuneEntry { + block: id1.block, etching: txid1, spaced_rune: SpacedRune { rune: Rune(RUNE + 1), @@ -2199,8 +2252,6 @@ mod tests { fn edicts_with_id_zero_are_skipped() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - let (txid0, id) = context.etch( Runestone { edicts: vec![Edict { @@ -2222,6 +2273,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2270,6 +2322,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2315,6 +2368,7 @@ mod tests { [( id0, RuneEntry { + block: id0.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2356,6 +2410,7 @@ mod tests { ( id0, RuneEntry { + block: id0.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2369,6 +2424,7 @@ mod tests { ( id1, RuneEntry { + block: id1.block, etching: txid1, spaced_rune: SpacedRune { rune: Rune(RUNE + 1), @@ -2429,6 +2485,7 @@ mod tests { ( id0, RuneEntry { + block: id0.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2442,6 +2499,7 @@ mod tests { ( id1, RuneEntry { + block: id1.block, etching: txid1, spaced_rune: SpacedRune { rune: Rune(RUNE + 1), @@ -2498,6 +2556,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2539,6 +2598,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2584,6 +2644,7 @@ mod tests { [( id, RuneEntry { + block: id.block, burned: u128::MAX, etching: txid, spaced_rune: SpacedRune { @@ -2624,6 +2685,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2670,6 +2732,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2709,6 +2772,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2760,6 +2824,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2823,6 +2888,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2867,6 +2933,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2918,6 +2985,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2968,6 +3036,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3015,6 +3084,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3057,6 +3127,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3111,6 +3182,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3160,6 +3232,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3214,6 +3287,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3263,6 +3337,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3317,6 +3392,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3359,6 +3435,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3413,6 +3490,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3462,6 +3540,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3516,6 +3595,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3565,6 +3645,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3634,6 +3715,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3674,6 +3756,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3713,6 +3796,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3755,6 +3839,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3783,7 +3868,7 @@ mod tests { Runestone { etching: Some(Etching { rune: Some(Rune(RUNE)), - mint: Some(Mint { + terms: Some(Terms { limit: Some(1000), cap: Some(100), ..default() @@ -3799,6 +3884,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3806,7 +3892,7 @@ mod tests { }, timestamp: id.block, mints: 0, - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), cap: Some(100), ..default() @@ -3821,7 +3907,7 @@ mod tests { inputs: &[(2, 0, 0, Witness::new())], op_return: Some( Runestone { - claim: Some(id), + mint: Some(id), ..default() } .encipher(), @@ -3835,8 +3921,9 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), cap: Some(100), ..default() @@ -3869,7 +3956,7 @@ mod tests { Runestone { etching: Some(Etching { rune: Some(Rune(RUNE)), - mint: Some(Mint { + terms: Some(Terms { cap: Some(100), limit: Some(1000), ..default() @@ -3885,6 +3972,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3893,7 +3981,7 @@ mod tests { timestamp: id.block, premine: 0, mints: 0, - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), cap: Some(100), ..default() @@ -3913,7 +4001,7 @@ mod tests { amount: 1000, output: 0, }], - claim: Some(id), + mint: Some(id), ..default() } .encipher(), @@ -3927,8 +4015,9 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), cap: Some(100), ..default() @@ -3962,7 +4051,7 @@ mod tests { amount: 1000, output: 0, }], - claim: Some(id), + mint: Some(id), ..default() } .encipher(), @@ -3976,8 +4065,9 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), cap: Some(100), ..default() @@ -4016,7 +4106,7 @@ mod tests { op_return: Some( Runestone { cenotaph: true, - claim: Some(id), + mint: Some(id), edicts: vec![Edict { id, amount: 1000, @@ -4035,9 +4125,10 @@ mod tests { [( id, RuneEntry { + block: id.block, burned: 1000, etching: txid0, - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), cap: Some(100), ..default() @@ -4072,17 +4163,17 @@ mod tests { } #[test] - fn open_mints_can_be_limited_to_term() { + fn open_mints_can_be_limited_with_offset_end() { let context = Context::builder().arg("--index-runes").build(); let (txid0, id) = context.etch( Runestone { etching: Some(Etching { rune: Some(Rune(RUNE)), - mint: Some(Mint { + terms: Some(Terms { limit: Some(1000), cap: Some(100), - term: Some(2), + offset: (None, Some(2)), ..default() }), ..default() @@ -4092,38 +4183,30 @@ mod tests { 1, ); - context.assert_runes( - [( - id, - RuneEntry { - etching: txid0, - spaced_rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - mint: Some(MintEntry { - limit: Some(1000), - end: Some(id.block + 2), - cap: Some(100), - ..default() - }), - timestamp: id.block, - ..default() - }, - )], - [], - ); + let mut entry = RuneEntry { + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + terms: Some(Terms { + limit: Some(1000), + offset: (None, Some(2)), + cap: Some(100), + ..default() + }), + timestamp: id.block, + ..default() + }; + + context.assert_runes([(id, entry)], []); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, Witness::new())], op_return: Some( Runestone { - edicts: vec![Edict { - id, - amount: 1000, - output: 0, - }], - claim: Some(id), + mint: Some(id), ..default() } .encipher(), @@ -4133,27 +4216,10 @@ mod tests { context.mine_blocks(1); + entry.mints += 1; + context.assert_runes( - [( - id, - RuneEntry { - etching: txid0, - spaced_rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - mint: Some(MintEntry { - limit: Some(1000), - end: Some(id.block + 2), - cap: Some(100), - ..default() - }), - premine: 0, - timestamp: id.block, - mints: 1, - ..default() - }, - )], + [(id, entry)], [( OutPoint { txid: txid1, @@ -4167,12 +4233,7 @@ mod tests { inputs: &[(3, 0, 0, Witness::new())], op_return: Some( Runestone { - edicts: vec![Edict { - id, - amount: 1000, - output: 0, - }], - claim: Some(id), + mint: Some(id), ..default() } .encipher(), @@ -4183,26 +4244,7 @@ mod tests { context.mine_blocks(1); context.assert_runes( - [( - id, - RuneEntry { - etching: txid0, - spaced_rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - premine: 0, - timestamp: id.block, - mint: Some(MintEntry { - limit: Some(1000), - end: Some(id.block + 2), - cap: Some(100), - ..default() - }), - mints: 1, - ..default() - }, - )], + [(id, entry)], [( OutPoint { txid: txid1, @@ -4214,22 +4256,17 @@ mod tests { } #[test] - fn open_mints_with_term_zero_can_be_premined() { + fn open_mints_can_be_limited_with_offset_start() { let context = Context::builder().arg("--index-runes").build(); - let (txid, id) = context.etch( + let (txid0, id) = context.etch( Runestone { - edicts: vec![Edict { - id: RuneId::default(), - amount: 1111, - output: 0, - }], etching: Some(Etching { rune: Some(Rune(RUNE)), - premine: Some(1111), - mint: Some(Mint { + terms: Some(Terms { limit: Some(1000), - term: Some(0), + cap: Some(100), + offset: (Some(2), None), ..default() }), ..default() @@ -4239,34 +4276,46 @@ mod tests { 1, ); - context.assert_runes( - [( - id, - RuneEntry { - etching: txid, - spaced_rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - mint: Some(MintEntry { - limit: Some(1000), - end: Some(id.block), - ..default() - }), - timestamp: id.block, - premine: 1111, - ..default() - }, - )], - [(OutPoint { txid, vout: 0 }, vec![(id, 1111)])], - ); + let mut entry = RuneEntry { + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + terms: Some(Terms { + limit: Some(1000), + offset: (Some(2), None), + cap: Some(100), + ..default() + }), + timestamp: id.block, + ..default() + }; + + context.assert_runes([(id, entry)], []); context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, Witness::new())], - outputs: 2, op_return: Some( Runestone { - claim: Some(id), + mint: Some(id), + ..default() + } + .encipher(), + ), + ..default() + }); + + context.mine_blocks(1); + + context.assert_runes([(id, entry)], []); + + let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(3, 0, 0, Witness::new())], + op_return: Some( + Runestone { + mint: Some(id), ..default() } .encipher(), @@ -4276,44 +4325,33 @@ mod tests { context.mine_blocks(1); + entry.mints += 1; + context.assert_runes( + [(id, entry)], [( - id, - RuneEntry { - etching: txid, - spaced_rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - timestamp: id.block, - mint: Some(MintEntry { - limit: Some(1000), - end: Some(id.block), - ..default() - }), - premine: 1111, - ..default() + OutPoint { + txid: txid1, + vout: 0, }, + vec![(id, 1000)], )], - [(OutPoint { txid, vout: 0 }, vec![(id, 1111)])], ); } #[test] - fn open_mints_with_end_before_deadline() { + fn open_mints_can_be_limited_with_height_start() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - let (txid0, id) = context.etch( Runestone { etching: Some(Etching { rune: Some(Rune(RUNE)), - mint: Some(Mint { - cap: Some(2), + terms: Some(Terms { limit: Some(1000), - deadline: Some(12), - term: Some(2), + cap: Some(100), + height: (Some(10), None), + ..default() }), ..default() }), @@ -4322,33 +4360,30 @@ mod tests { 1, ); - context.assert_runes( - [( - id, - RuneEntry { - etching: txid0, - spaced_rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - timestamp: 9, - mint: Some(MintEntry { - cap: Some(2), - deadline: Some(12), - end: Some(11), - limit: Some(1000), - }), - ..default() - }, - )], - [], - ); + let mut entry = RuneEntry { + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + terms: Some(Terms { + limit: Some(1000), + height: (Some(10), None), + cap: Some(100), + ..default() + }), + timestamp: id.block, + ..default() + }; - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(id.block.try_into().unwrap(), 0, 0, Witness::new())], + context.assert_runes([(id, entry)], []); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, Witness::new())], op_return: Some( Runestone { - claim: Some(id), + mint: Some(id), ..default() } .encipher(), @@ -4358,41 +4393,13 @@ mod tests { context.mine_blocks(1); - context.assert_runes( - [( - id, - RuneEntry { - spaced_rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - premine: 0, - timestamp: 9, - mints: 1, - etching: txid0, - mint: Some(MintEntry { - cap: Some(2), - deadline: Some(12), - end: Some(11), - limit: Some(1000), - }), - ..default() - }, - )], - [( - OutPoint { - txid: txid1, - vout: 0, - }, - vec![(id, 1000)], - )], - ); + context.assert_runes([(id, entry)], []); - context.rpc_server.broadcast_tx(TransactionTemplate { + let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(3, 0, 0, Witness::new())], op_return: Some( Runestone { - claim: Some(id), + mint: Some(id), ..default() } .encipher(), @@ -4402,27 +4409,10 @@ mod tests { context.mine_blocks(1); + entry.mints += 1; + context.assert_runes( - [( - id, - RuneEntry { - etching: txid0, - spaced_rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - premine: 0, - timestamp: 9, - mint: Some(MintEntry { - cap: Some(2), - limit: Some(1000), - deadline: Some(12), - end: Some(11), - }), - mints: 1, - ..default() - }, - )], + [(id, entry)], [( OutPoint { txid: txid1, @@ -4434,20 +4424,18 @@ mod tests { } #[test] - fn open_mints_with_deadline_before_end() { + fn open_mints_can_be_limited_with_height_end() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - let (txid0, id) = context.etch( Runestone { etching: Some(Etching { rune: Some(Rune(RUNE)), - mint: Some(Mint { - cap: Some(2), + terms: Some(Terms { limit: Some(1000), - deadline: Some(11), - term: Some(3), + cap: Some(100), + height: (None, Some(10)), + ..default() }), ..default() }), @@ -4456,33 +4444,30 @@ mod tests { 1, ); - context.assert_runes( - [( - id, - RuneEntry { - etching: txid0, - spaced_rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - timestamp: id.block, - mint: Some(MintEntry { - cap: Some(2), - deadline: Some(11), - end: Some(12), - limit: Some(1000), - }), - ..default() - }, - )], - [], - ); + let mut entry = RuneEntry { + block: id.block, + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + terms: Some(Terms { + limit: Some(1000), + height: (None, Some(10)), + cap: Some(100), + ..default() + }), + timestamp: id.block, + ..default() + }; + + context.assert_runes([(id, entry)], []); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(3, 0, 0, Witness::new())], + inputs: &[(2, 0, 0, Witness::new())], op_return: Some( Runestone { - claim: Some(id), + mint: Some(id), ..default() } .encipher(), @@ -4492,27 +4477,10 @@ mod tests { context.mine_blocks(1); + entry.mints += 1; + context.assert_runes( - [( - id, - RuneEntry { - spaced_rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - premine: 0, - timestamp: id.block, - mints: 1, - etching: txid0, - mint: Some(MintEntry { - cap: Some(2), - deadline: Some(11), - end: Some(12), - limit: Some(1000), - }), - ..default() - }, - )], + [(id, entry)], [( OutPoint { txid: txid1, @@ -4523,10 +4491,10 @@ mod tests { ); context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(4, 0, 0, Witness::new())], + inputs: &[(3, 0, 0, Witness::new())], op_return: Some( Runestone { - claim: Some(id), + mint: Some(id), ..default() } .encipher(), @@ -4537,26 +4505,7 @@ mod tests { context.mine_blocks(1); context.assert_runes( - [( - id, - RuneEntry { - etching: txid0, - spaced_rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, - premine: 0, - timestamp: id.block, - mint: Some(MintEntry { - cap: Some(2), - limit: Some(1000), - deadline: Some(11), - end: Some(12), - }), - mints: 1, - ..default() - }, - )], + [(id, entry)], [( OutPoint { txid: txid1, @@ -4568,17 +4517,22 @@ mod tests { } #[test] - fn open_mints_can_be_limited_to_deadline() { + fn open_mints_with_offset_end_zero_can_be_premined() { let context = Context::builder().arg("--index-runes").build(); - let (txid0, id) = context.etch( + let (txid, id) = context.etch( Runestone { + edicts: vec![Edict { + id: RuneId::default(), + amount: 1111, + output: 0, + }], etching: Some(Etching { rune: Some(Rune(RUNE)), - mint: Some(Mint { + premine: Some(1111), + terms: Some(Terms { limit: Some(1000), - cap: Some(100), - deadline: Some(RUNE_COMMIT_INTERVAL + 4), + offset: (None, Some(0)), ..default() }), ..default() @@ -4592,82 +4546,31 @@ mod tests { [( id, RuneEntry { - etching: txid0, + block: id.block, + etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), spacers: 0, }, - timestamp: id.block, - mint: Some(MintEntry { - deadline: Some(id.block + 2), + terms: Some(Terms { limit: Some(1000), - cap: Some(100), + offset: (None, Some(0)), ..default() }), - ..default() - }, - )], - [], - ); - - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id, - amount: 1000, - output: 0, - }], - claim: Some(id), - ..default() - } - .encipher(), - ), - ..default() - }); - - context.mine_blocks(1); - - context.assert_runes( - [( - id, - RuneEntry { - spaced_rune: SpacedRune { - rune: Rune(RUNE), - spacers: 0, - }, timestamp: id.block, - mints: 1, - etching: txid0, - mint: Some(MintEntry { - deadline: Some(id.block + 2), - limit: Some(1000), - cap: Some(100), - ..default() - }), + premine: 1111, ..default() }, )], - [( - OutPoint { - txid: txid1, - vout: 0, - }, - vec![(id, 1000)], - )], + [(OutPoint { txid, vout: 0 }, vec![(id, 1111)])], ); context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(3, 0, 0, Witness::new())], + inputs: &[(2, 0, 0, Witness::new())], + outputs: 2, op_return: Some( Runestone { - edicts: vec![Edict { - id, - amount: 1000, - output: 0, - }], - claim: Some(id), + mint: Some(id), ..default() } .encipher(), @@ -4681,29 +4584,23 @@ mod tests { [( id, RuneEntry { - etching: txid0, + block: id.block, + etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), spacers: 0, }, timestamp: id.block, - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), - deadline: Some(id.block + 2), - cap: Some(100), + offset: (None, Some(0)), ..default() }), - mints: 1, + premine: 1111, ..default() }, )], - [( - OutPoint { - txid: txid1, - vout: 0, - }, - vec![(id, 1000)], - )], + [(OutPoint { txid, vout: 0 }, vec![(id, 1111)])], ); } @@ -4715,7 +4612,7 @@ mod tests { Runestone { etching: Some(Etching { rune: Some(Rune(RUNE)), - mint: Some(Mint { + terms: Some(Terms { limit: Some(1000), cap: Some(2), ..default() @@ -4731,13 +4628,14 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), spacers: 0, }, timestamp: id.block, - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), cap: Some(2), ..default() @@ -4757,7 +4655,7 @@ mod tests { amount: 1000, output: 0, }], - claim: Some(id), + mint: Some(id), ..default() } .encipher(), @@ -4771,6 +4669,7 @@ mod tests { [( id, RuneEntry { + block: id.block, spaced_rune: SpacedRune { rune: Rune(RUNE), spacers: 0, @@ -4778,7 +4677,7 @@ mod tests { timestamp: id.block, mints: 1, etching: txid0, - mint: Some(MintEntry { + terms: Some(Terms { cap: Some(2), limit: Some(1000), ..default() @@ -4804,7 +4703,7 @@ mod tests { amount: 1000, output: 0, }], - claim: Some(id), + mint: Some(id), ..default() } .encipher(), @@ -4818,13 +4717,14 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), spacers: 0, }, timestamp: id.block, - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), cap: Some(2), ..default() @@ -4860,7 +4760,7 @@ mod tests { amount: 1000, output: 0, }], - claim: Some(id), + mint: Some(id), ..default() } .encipher(), @@ -4874,13 +4774,14 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), spacers: 0, }, timestamp: id.block, - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), cap: Some(2), ..default() @@ -4916,7 +4817,7 @@ mod tests { Runestone { etching: Some(Etching { rune: Some(Rune(RUNE)), - mint: Some(Mint { + terms: Some(Terms { limit: Some(1000), cap: Some(100), ..default() @@ -4932,12 +4833,13 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), spacers: 0, }, - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), cap: Some(100), ..default() @@ -4959,7 +4861,7 @@ mod tests { amount: 0, output: 3, }], - claim: Some(id), + mint: Some(id), ..default() } .encipher(), @@ -4973,13 +4875,14 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), spacers: 0, }, timestamp: id.block, - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), cap: Some(100), ..default() @@ -5016,7 +4919,7 @@ mod tests { etching: Some(Etching { rune: Some(Rune(RUNE)), premine: Some(2000), - mint: Some(Mint { + terms: Some(Terms { limit: Some(1000), ..default() }), @@ -5036,12 +4939,13 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), spacers: 0, }, - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), ..default() }), @@ -5062,8 +4966,8 @@ mod tests { Runestone { etching: Some(Etching { rune: Some(Rune(RUNE)), - mint: Some(Mint { - term: Some(1), + terms: Some(Terms { + offset: (None, Some(1)), ..default() }), ..default() @@ -5077,14 +4981,15 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), spacers: 0, }, - mint: Some(MintEntry { + terms: Some(Terms { limit: None, - end: Some(id.block + 1), + offset: (None, Some(1)), ..default() }), timestamp: id.block, @@ -5099,12 +5004,12 @@ mod tests { fn premines_can_claim_over_the_max_limit() { let context = Context::builder().arg("--index-runes").build(); - let (txid0, rune_id) = context.etch( + let (txid, id) = context.etch( Runestone { etching: Some(Etching { rune: Some(Rune(RUNE)), premine: Some(2000), - mint: Some(Mint { + terms: Some(Terms { limit: Some(1000), cap: Some(1), ..default() @@ -5123,31 +5028,26 @@ mod tests { context.assert_runes( [( - rune_id, + id, RuneEntry { - etching: txid0, + block: id.block, + etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), spacers: 0, }, - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), cap: Some(1), ..default() }), - timestamp: rune_id.block, + timestamp: id.block, premine: 2000, mints: 0, ..default() }, )], - [( - OutPoint { - txid: txid0, - vout: 0, - }, - vec![(rune_id, 2000)], - )], + [(OutPoint { txid, vout: 0 }, vec![(id, 2000)])], ); } @@ -5159,7 +5059,7 @@ mod tests { Runestone { etching: Some(Etching { rune: Some(Rune(RUNE)), - mint: Some(Mint { + terms: Some(Terms { limit: Some(1000), cap: Some(100), ..default() @@ -5180,7 +5080,7 @@ mod tests { amount: 2000, output: 0, }], - claim: Some(id), + mint: Some(id), ..default() } .encipher(), @@ -5194,12 +5094,13 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), spacers: 0, }, - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), cap: Some(100), ..default() @@ -5227,7 +5128,7 @@ mod tests { Runestone { etching: Some(Etching { rune: Some(Rune(RUNE)), - mint: Some(Mint { + terms: Some(Terms { limit: Some(1000), cap: Some(100), ..default() @@ -5243,12 +5144,13 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), spacers: 0, }, - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), cap: Some(100), ..default() @@ -5281,7 +5183,7 @@ mod tests { output: 0, }, ], - claim: Some(id), + mint: Some(id), ..default() } .encipher(), @@ -5295,12 +5197,13 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid0, spaced_rune: SpacedRune { rune: Rune(RUNE), spacers: 0, }, - mint: Some(MintEntry { + terms: Some(Terms { limit: Some(1000), cap: Some(100), ..default() @@ -5341,7 +5244,7 @@ mod tests { let runestone = Runestone { etching: Some(Etching { rune: Some(Rune(RUNE)), - mint: Some(Mint { + terms: Some(Terms { limit: Some(1000), ..default() }), @@ -5401,7 +5304,7 @@ mod tests { let runestone = Runestone { etching: Some(Etching { rune: Some(Rune(RUNE)), - mint: Some(Mint { + terms: Some(Terms { limit: Some(1000), ..default() }), @@ -5461,7 +5364,7 @@ mod tests { let runestone = Runestone { etching: Some(Etching { rune: Some(Rune(RUNE)), - mint: Some(Mint { + terms: Some(Terms { limit: Some(1000), ..default() }), diff --git a/src/runes/etching.rs b/src/runes/etching.rs index 9329388b4e..79ee7908aa 100644 --- a/src/runes/etching.rs +++ b/src/runes/etching.rs @@ -3,9 +3,9 @@ use super::*; #[derive(Default, Serialize, Deserialize, Debug, PartialEq, Copy, Clone, Eq)] pub struct Etching { pub divisibility: Option, - pub mint: Option, pub premine: Option, pub rune: Option, pub spacers: Option, pub symbol: Option, + pub terms: Option, } diff --git a/src/runes/flag.rs b/src/runes/flag.rs index 13e05b7151..3bf3f328b5 100644 --- a/src/runes/flag.rs +++ b/src/runes/flag.rs @@ -1,6 +1,6 @@ pub(super) enum Flag { - Etch = 0, - Mint = 1, + Etching = 0, + Terms = 1, #[allow(unused)] Cenotaph = 127, } @@ -28,25 +28,25 @@ mod tests { #[test] fn mask() { - assert_eq!(Flag::Etch.mask(), 0b1); + assert_eq!(Flag::Etching.mask(), 0b1); assert_eq!(Flag::Cenotaph.mask(), 1 << 127); } #[test] fn take() { let mut flags = 1; - assert!(Flag::Etch.take(&mut flags)); + assert!(Flag::Etching.take(&mut flags)); assert_eq!(flags, 0); let mut flags = 0; - assert!(!Flag::Etch.take(&mut flags)); + assert!(!Flag::Etching.take(&mut flags)); assert_eq!(flags, 0); } #[test] fn set() { let mut flags = 0; - Flag::Etch.set(&mut flags); + Flag::Etching.set(&mut flags); assert_eq!(flags, 1); } } diff --git a/src/runes/mint.rs b/src/runes/mint.rs deleted file mode 100644 index 122714e4db..0000000000 --- a/src/runes/mint.rs +++ /dev/null @@ -1,9 +0,0 @@ -use super::*; - -#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Copy, Clone, Eq)] -pub struct Mint { - pub cap: Option, // mint cap - pub deadline: Option, // unix timestamp - pub limit: Option, // claim amount - pub term: Option, // relative block height -} diff --git a/src/runes/rune_id.rs b/src/runes/rune_id.rs index c8a600560c..d50546e5d0 100644 --- a/src/runes/rune_id.rs +++ b/src/runes/rune_id.rs @@ -14,12 +14,12 @@ use super::*; SerializeDisplay, )] pub struct RuneId { - pub block: u32, + pub block: u64, pub tx: u32, } impl RuneId { - pub(crate) fn new(block: u32, tx: u32) -> Option { + pub(crate) fn new(block: u64, tx: u32) -> Option { let id = RuneId { block, tx }; if id.block == 0 && id.tx > 0 { diff --git a/src/runes/runestone.rs b/src/runes/runestone.rs index 9741ff583e..efb03b5e59 100644 --- a/src/runes/runestone.rs +++ b/src/runes/runestone.rs @@ -5,10 +5,10 @@ const MAX_SPACERS: u32 = 0b00000111_11111111_11111111_11111111; #[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Runestone { pub cenotaph: bool, - pub claim: Option, - pub default_output: Option, pub edicts: Vec, pub etching: Option, + pub mint: Option, + pub pointer: Option, } struct Message { @@ -101,15 +101,13 @@ impl Runestone { mut fields, } = Message::from_integers(transaction, &integers); - let claim = Tag::Claim.take(&mut fields, |[block, tx]| { + let mint = Tag::Mint.take(&mut fields, |[block, tx]| { RuneId::new(block.try_into().ok()?, tx.try_into().ok()?) }); - let deadline = Tag::Deadline.take(&mut fields, |[deadline]| u32::try_from(deadline).ok()); - - let default_output = Tag::DefaultOutput.take(&mut fields, |[default_output]| { - let default_output = u32::try_from(default_output).ok()?; - (default_output.into_usize() < transaction.output.len()).then_some(default_output) + let pointer = Tag::Pointer.take(&mut fields, |[pointer]| { + let pointer = u32::try_from(pointer).ok()?; + (pointer.into_usize() < transaction.output.len()).then_some(pointer) }); let divisibility = Tag::Divisibility.take(&mut fields, |[divisibility]| { @@ -134,15 +132,29 @@ impl Runestone { char::from_u32(u32::try_from(symbol).ok()?) }); - let term = Tag::Term.take(&mut fields, |[term]| u32::try_from(term).ok()); + let offset = ( + Tag::OffsetStart.take(&mut fields, |[start_offset]| { + u64::try_from(start_offset).ok() + }), + Tag::OffsetEnd.take(&mut fields, |[end_offset]| u64::try_from(end_offset).ok()), + ); + + let height = ( + Tag::HeightStart.take(&mut fields, |[start_height]| { + u64::try_from(start_height).ok() + }), + Tag::HeightEnd.take(&mut fields, |[start_height]| { + u64::try_from(start_height).ok() + }), + ); let mut flags = Tag::Flags .take(&mut fields, |[flags]| Some(flags)) .unwrap_or_default(); - let etch = Flag::Etch.take(&mut flags); + let etching = Flag::Etching.take(&mut flags); - let mint = Flag::Mint.take(&mut flags); + let terms = Flag::Terms.take(&mut flags); let overflow = (|| { let premine = premine.unwrap_or_default(); @@ -152,30 +164,26 @@ impl Runestone { })() .is_none(); - let etching = if etch { - Some(Etching { - divisibility, - mint: mint.then_some(Mint { - cap, - deadline, - limit, - term, - }), - premine, - rune, - spacers, - symbol, - }) - } else { - None - }; + let etching = etching.then_some(Etching { + divisibility, + premine, + rune, + spacers, + symbol, + terms: terms.then_some(Terms { + cap, + height, + limit, + offset, + }), + }); Ok(Some(Self { cenotaph: cenotaph || overflow || flags != 0 || fields.keys().any(|tag| tag % 2 == 0), - claim, - default_output, edicts, etching, + mint, + pointer, })) } @@ -184,60 +192,35 @@ impl Runestone { if let Some(etching) = self.etching { let mut flags = 0; - Flag::Etch.set(&mut flags); + Flag::Etching.set(&mut flags); - if etching.mint.is_some() { - Flag::Mint.set(&mut flags); + if etching.terms.is_some() { + Flag::Terms.set(&mut flags); } Tag::Flags.encode([flags], &mut payload); - if let Some(rune) = etching.rune { - Tag::Rune.encode([rune.0], &mut payload); - } - - if let Some(divisibility) = etching.divisibility { - Tag::Divisibility.encode([divisibility.into()], &mut payload); - } - - if let Some(spacers) = etching.spacers { - Tag::Spacers.encode([spacers.into()], &mut payload); - } - - if let Some(symbol) = etching.symbol { - Tag::Symbol.encode([symbol.into()], &mut payload); - } - - if let Some(premine) = etching.premine { - Tag::Premine.encode([premine], &mut payload); - } - - if let Some(mint) = etching.mint { - if let Some(deadline) = mint.deadline { - Tag::Deadline.encode([deadline.into()], &mut payload); - } - - if let Some(limit) = mint.limit { - Tag::Limit.encode([limit], &mut payload); - } - - if let Some(term) = mint.term { - Tag::Term.encode([term.into()], &mut payload); - } - - if let Some(cap) = mint.cap { - Tag::Cap.encode([cap], &mut payload); - } + Tag::Rune.encode_option(etching.rune.map(|rune| rune.0), &mut payload); + Tag::Divisibility.encode_option(etching.divisibility, &mut payload); + Tag::Spacers.encode_option(etching.spacers, &mut payload); + Tag::Symbol.encode_option(etching.symbol, &mut payload); + Tag::Premine.encode_option(etching.premine, &mut payload); + + if let Some(terms) = etching.terms { + Tag::Limit.encode_option(terms.limit, &mut payload); + Tag::Cap.encode_option(terms.cap, &mut payload); + Tag::HeightStart.encode_option(terms.height.0, &mut payload); + Tag::HeightEnd.encode_option(terms.height.1, &mut payload); + Tag::OffsetStart.encode_option(terms.offset.0, &mut payload); + Tag::OffsetEnd.encode_option(terms.offset.1, &mut payload); } } - if let Some(RuneId { block, tx }) = self.claim { - Tag::Claim.encode([block.into(), tx.into()], &mut payload); + if let Some(RuneId { block, tx }) = self.mint { + Tag::Mint.encode([block.into(), tx.into()], &mut payload); } - if let Some(default_output) = self.default_output { - Tag::DefaultOutput.encode([default_output.into()], &mut payload); - } + Tag::Pointer.encode_option(self.pointer, &mut payload); if self.cenotaph { Tag::Cenotaph.encode([0], &mut payload); @@ -606,7 +589,7 @@ mod tests { assert_eq!( decipher(&[ Tag::Flags.into(), - Flag::Etch.mask(), + Flag::Etching.mask(), Tag::Body.into(), 1, 1, @@ -630,7 +613,7 @@ mod tests { pretty_assert_eq!( decipher(&[ Tag::Flags.into(), - Flag::Etch.mask(), + Flag::Etching.mask(), Tag::Rune.into(), 4, Tag::Body.into(), @@ -659,8 +642,8 @@ mod tests { pretty_assert_eq!( decipher(&[ Tag::Flags.into(), - Flag::Mint.mask(), - Tag::Term.into(), + Flag::Terms.mask(), + Tag::OffsetEnd.into(), 4, Tag::Body.into(), 1, @@ -684,8 +667,8 @@ mod tests { pretty_assert_eq!( decipher(&[ Tag::Flags.into(), - Flag::Etch.mask() | Flag::Mint.mask(), - Tag::Term.into(), + Flag::Etching.mask() | Flag::Terms.mask(), + Tag::OffsetEnd.into(), 4, Tag::Body.into(), 1, @@ -700,8 +683,8 @@ mod tests { output: 0, }], etching: Some(Etching { - mint: Some(Mint { - term: Some(4), + terms: Some(Terms { + offset: (None, Some(4)), ..default() }), ..default() @@ -716,7 +699,7 @@ mod tests { assert_eq!( decipher(&[ Tag::Flags.into(), - Flag::Etch.mask() | Flag::Mint.mask(), + Flag::Etching.mask() | Flag::Terms.mask(), Tag::Limit.into(), 4, Tag::Body.into(), @@ -732,7 +715,7 @@ mod tests { output: 0, }], etching: Some(Etching { - mint: Some(Mint { + terms: Some(Terms { limit: Some(4), ..default() }), @@ -773,7 +756,7 @@ mod tests { pretty_assert_eq!( decipher(&[ Tag::Flags.into(), - Flag::Etch.mask(), + Flag::Etching.mask(), Tag::Rune.into(), 4, Tag::Rune.into(), @@ -805,7 +788,7 @@ mod tests { pretty_assert_eq!( decipher(&[ Tag::Flags.into(), - Flag::Etch.mask(), + Flag::Etching.mask(), Tag::Divisibility.into(), 4, Tag::Divisibility.into(), @@ -950,7 +933,7 @@ mod tests { assert_eq!( decipher(&[ Tag::Flags.into(), - Flag::Etch.mask(), + Flag::Etching.mask(), Tag::Rune.into(), 4, Tag::Divisibility.into(), @@ -982,7 +965,7 @@ mod tests { assert_eq!( decipher(&[ Tag::Flags.into(), - Flag::Etch.mask(), + Flag::Etching.mask(), Tag::Rune.into(), 4, Tag::Divisibility.into(), @@ -1013,7 +996,7 @@ mod tests { assert_eq!( decipher(&[ Tag::Flags.into(), - Flag::Etch.mask(), + Flag::Etching.mask(), Tag::Symbol.into(), u128::from(u32::from(char::MAX) + 1), Tag::Body.into(), @@ -1039,7 +1022,7 @@ mod tests { pretty_assert_eq!( decipher(&[ Tag::Flags.into(), - Flag::Etch.mask(), + Flag::Etching.mask(), Tag::Rune.into(), 4, Tag::Symbol.into(), @@ -1071,18 +1054,16 @@ mod tests { pretty_assert_eq!( decipher(&[ Tag::Flags.into(), - Flag::Etch.mask() | Flag::Mint.mask(), + Flag::Etching.mask() | Flag::Terms.mask(), Tag::Rune.into(), 4, - Tag::Deadline.into(), - 7, Tag::Divisibility.into(), 1, Tag::Spacers.into(), 5, Tag::Symbol.into(), 'a'.into(), - Tag::Term.into(), + Tag::OffsetEnd.into(), 2, Tag::Limit.into(), 3, @@ -1090,11 +1071,11 @@ mod tests { 8, Tag::Cap.into(), 9, - Tag::DefaultOutput.into(), + Tag::Pointer.into(), 0, - Tag::Claim.into(), + Tag::Mint.into(), 1, - Tag::Claim.into(), + Tag::Mint.into(), 1, Tag::Body.into(), 1, @@ -1110,11 +1091,11 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(4)), - mint: Some(Mint { + terms: Some(Terms { cap: Some(9), - deadline: Some(7), - term: Some(2), + offset: (None, Some(2)), limit: Some(3), + height: (None, None), }), premine: Some(8), divisibility: Some(1), @@ -1122,8 +1103,8 @@ mod tests { spacers: Some(5), }), cenotaph: false, - default_output: Some(0), - claim: Some(RuneId::new(1, 1).unwrap()), + pointer: Some(0), + mint: Some(RuneId::new(1, 1).unwrap()), }, ); } @@ -1138,7 +1119,7 @@ mod tests { 1, Tag::Symbol.into(), 'a'.into(), - Tag::Term.into(), + Tag::OffsetEnd.into(), 2, Tag::Limit.into(), 3, @@ -1164,7 +1145,7 @@ mod tests { assert_eq!( decipher(&[ Tag::Flags.into(), - Flag::Etch.mask(), + Flag::Etching.mask(), Tag::Rune.into(), 4, Tag::Divisibility.into(), @@ -1199,7 +1180,7 @@ mod tests { pretty_assert_eq!( decipher(&[ Tag::Flags.into(), - Flag::Etch.mask(), + Flag::Etching.mask(), Tag::Divisibility.into(), Tag::Body.into(), Tag::Body.into(), @@ -1293,7 +1274,7 @@ mod tests { .unwrap() ) .push_slice::<&PushBytes>( - varint::encode(Flag::Etch.mask()) + varint::encode(Flag::Etching.mask()) .as_slice() .try_into() .unwrap() @@ -1454,18 +1435,18 @@ mod tests { Vec::new(), Some(Etching { divisibility: Some(MAX_DIVISIBILITY), - mint: Some(Mint { - cap: None, - deadline: Some(10000), - limit: Some(1), - term: Some(1), + terms: Some(Terms { + cap: Some(u32::MAX.into()), + limit: Some(u64::MAX.into()), + offset: (Some(u32::MAX.into()), Some(u32::MAX.into())), + height: (Some(u32::MAX.into()), Some(u32::MAX.into())), }), - premine: None, - rune: Some(Rune(0)), - symbol: Some('$'), - spacers: Some(1), + premine: Some(u64::MAX.into()), + rune: Some(Rune(u128::MAX)), + symbol: Some('\u{10FFFF}'), + spacers: Some(MAX_SPACERS), }), - 20, + 89, ); case( @@ -1655,8 +1636,8 @@ mod tests { assert_eq!( decipher(&[ Tag::Flags.into(), - Flag::Etch.mask(), - Tag::Term.into(), + Flag::Etching.mask(), + Tag::OffsetEnd.into(), u128::from(u64::MAX) + 1, ]), Runestone { @@ -1708,73 +1689,77 @@ mod tests { case( Runestone { - etching: Some(Etching { - premine: Some(1), - divisibility: Some(1), - mint: Some(Mint { - cap: Some(1), - deadline: Some(2), - limit: Some(3), - term: Some(5), - }), - symbol: Some('@'), - rune: Some(Rune(4)), - spacers: Some(6), - }), + cenotaph: true, edicts: vec![ Edict { - amount: 8, - id: rune_id(9), + id: RuneId::new(2, 3).unwrap(), + amount: 1, output: 0, }, Edict { - amount: 5, - id: rune_id(6), + id: RuneId::new(5, 6).unwrap(), + amount: 4, output: 1, }, ], - default_output: Some(0), - cenotaph: true, - claim: Some(rune_id(12)), + etching: Some(Etching { + divisibility: Some(7), + premine: Some(8), + rune: Some(Rune(9)), + spacers: Some(10), + symbol: Some('@'), + terms: Some(Terms { + cap: Some(11), + height: (Some(12), Some(13)), + limit: Some(14), + offset: (Some(15), Some(16)), + }), + }), + mint: Some(RuneId::new(17, 18).unwrap()), + pointer: Some(0), }, &[ Tag::Flags.into(), - Flag::Etch.mask() | Flag::Mint.mask(), + Flag::Etching.mask() | Flag::Terms.mask(), Tag::Rune.into(), - 4, + 9, Tag::Divisibility.into(), - 1, + 7, Tag::Spacers.into(), - 6, + 10, Tag::Symbol.into(), '@'.into(), Tag::Premine.into(), - 1, - Tag::Deadline.into(), - 2, + 8, Tag::Limit.into(), - 3, - Tag::Term.into(), - 5, + 14, Tag::Cap.into(), - 1, - Tag::Claim.into(), - 1, - Tag::Claim.into(), + 11, + Tag::HeightStart.into(), 12, - Tag::DefaultOutput.into(), + Tag::HeightEnd.into(), + 13, + Tag::OffsetStart.into(), + 15, + Tag::OffsetEnd.into(), + 16, + Tag::Mint.into(), + 17, + Tag::Mint.into(), + 18, + Tag::Pointer.into(), 0, Tag::Cenotaph.into(), 0, Tag::Body.into(), - 1, - 6, - 5, + 2, + 3, 1, 0, 3, - 8, - 0, + 6, + 4, + 1, ], ); @@ -1783,7 +1768,7 @@ mod tests { etching: Some(Etching { premine: None, divisibility: None, - mint: None, + terms: None, symbol: None, rune: Some(Rune(3)), spacers: None, @@ -1791,7 +1776,7 @@ mod tests { cenotaph: false, ..default() }, - &[Tag::Flags.into(), Flag::Etch.mask(), Tag::Rune.into(), 3], + &[Tag::Flags.into(), Flag::Etching.mask(), Tag::Rune.into(), 3], ); case( @@ -1799,7 +1784,7 @@ mod tests { etching: Some(Etching { premine: None, divisibility: None, - mint: None, + terms: None, symbol: None, rune: None, spacers: None, @@ -1807,7 +1792,7 @@ mod tests { cenotaph: false, ..default() }, - &[Tag::Flags.into(), Flag::Etch.mask()], + &[Tag::Flags.into(), Flag::Etching.mask()], ); case( @@ -1873,24 +1858,24 @@ mod tests { } #[test] - fn partial_claim_produces_cenotaph() { - assert!(decipher(&[Tag::Claim.into(), 1]).cenotaph); + fn partial_mint_produces_cenotaph() { + assert!(decipher(&[Tag::Mint.into(), 1]).cenotaph); } #[test] - fn invalid_claim_produces_cenotaph() { - assert!(decipher(&[Tag::Claim.into(), 0, Tag::Claim.into(), 1]).cenotaph); + fn invalid_mint_produces_cenotaph() { + assert!(decipher(&[Tag::Mint.into(), 0, Tag::Mint.into(), 1]).cenotaph); } #[test] fn invalid_deadline_produces_cenotaph() { - assert!(decipher(&[Tag::Deadline.into(), u128::MAX]).cenotaph); + assert!(decipher(&[Tag::OffsetEnd.into(), u128::MAX]).cenotaph); } #[test] fn invalid_default_output_produces_cenotaph() { - assert!(decipher(&[Tag::DefaultOutput.into(), 1]).cenotaph); - assert!(decipher(&[Tag::DefaultOutput.into(), u128::MAX]).cenotaph); + assert!(decipher(&[Tag::Pointer.into(), 1]).cenotaph); + assert!(decipher(&[Tag::Pointer.into(), u128::MAX]).cenotaph); } #[test] @@ -1916,7 +1901,7 @@ mod tests { #[test] fn invalid_term_produces_cenotaph() { - assert!(decipher(&[Tag::Term.into(), u128::MAX]).cenotaph); + assert!(decipher(&[Tag::OffsetEnd.into(), u128::MAX]).cenotaph); } #[test] @@ -1924,7 +1909,7 @@ mod tests { assert!( !decipher(&[ Tag::Flags.into(), - Flag::Etch.mask() | Flag::Mint.mask(), + Flag::Etching.mask() | Flag::Terms.mask(), Tag::Cap.into(), 1, Tag::Limit.into(), @@ -1936,7 +1921,7 @@ mod tests { assert!( decipher(&[ Tag::Flags.into(), - Flag::Etch.mask() | Flag::Mint.mask(), + Flag::Etching.mask() | Flag::Terms.mask(), Tag::Cap.into(), 2, Tag::Limit.into(), @@ -1948,7 +1933,7 @@ mod tests { assert!( decipher(&[ Tag::Flags.into(), - Flag::Etch.mask() | Flag::Mint.mask(), + Flag::Etching.mask() | Flag::Terms.mask(), Tag::Cap.into(), 2, Tag::Limit.into(), @@ -1960,7 +1945,7 @@ mod tests { assert!( decipher(&[ Tag::Flags.into(), - Flag::Etch.mask() | Flag::Mint.mask(), + Flag::Etching.mask() | Flag::Terms.mask(), Tag::Premine.into(), 1, Tag::Cap.into(), diff --git a/src/runes/tag.rs b/src/runes/tag.rs index 7dbfcca8f6..ee36a4d2fb 100644 --- a/src/runes/tag.rs +++ b/src/runes/tag.rs @@ -5,13 +5,15 @@ pub(super) enum Tag { Body = 0, Flags = 2, Rune = 4, - Limit = 6, - Term = 8, - Deadline = 10, - DefaultOutput = 12, - Claim = 14, - Cap = 16, - Premine = 18, + Premine = 6, + Cap = 8, + Limit = 10, + HeightStart = 12, + HeightEnd = 14, + OffsetStart = 16, + OffsetEnd = 18, + Mint = 20, + Pointer = 22, #[allow(unused)] Cenotaph = 126, @@ -53,6 +55,12 @@ impl Tag { varint::encode_to_vec(value, payload); } } + + pub(super) fn encode_option>(self, value: Option, payload: &mut Vec) { + if let Some(value) = value { + self.encode([value.into()], payload) + } + } } impl From for u128 { diff --git a/src/runes/terms.rs b/src/runes/terms.rs new file mode 100644 index 0000000000..768775b52a --- /dev/null +++ b/src/runes/terms.rs @@ -0,0 +1,9 @@ +use super::*; + +#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Copy, Clone, Eq)] +pub struct Terms { + pub cap: Option, + pub height: (Option, Option), + pub limit: Option, + pub offset: (Option, Option), +} diff --git a/src/subcommand/runes.rs b/src/subcommand/runes.rs index 2617755015..645cce331c 100644 --- a/src/subcommand/runes.rs +++ b/src/subcommand/runes.rs @@ -7,18 +7,18 @@ pub struct Output { #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct RuneInfo { - pub block: u32, + pub block: u64, pub burned: u128, pub divisibility: u8, pub etching: Txid, pub id: RuneId, - pub mint: Option, pub mints: u128, pub number: u64, pub premine: u128, pub rune: SpacedRune, pub supply: u128, pub symbol: Option, + pub terms: Option, pub timestamp: DateTime, pub tx: u32, } @@ -41,33 +41,34 @@ pub(crate) fn run(settings: Settings) -> SubcommandResult { |( id, entry @ RuneEntry { + block, burned, divisibility, etching, - mint, mints, number, premine, spaced_rune, symbol, + terms, timestamp, }, )| { ( spaced_rune.rune, RuneInfo { - block: id.block, + block, burned, divisibility, etching, id, - mint, mints, number, premine, rune: spaced_rune, supply: entry.supply(), symbol, + terms, timestamp: crate::timestamp(timestamp), tx: id.tx, }, diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 27fb5e0b99..1522cb70bf 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -690,15 +690,7 @@ impl Server { let block_height = index.block_height()?.unwrap_or(Height(0)); - let block_time: u32 = index - .block_time(block_height)? - .unix_timestamp() - .try_into() - .unwrap_or_default(); - - let mintable = entry - .mintable(Height(block_height.n() + 1), block_time) - .is_ok(); + let mintable = entry.mintable((block_height.n() + 1).into()).is_ok(); Ok(if accept_json { Json(api::Rune { @@ -983,7 +975,7 @@ impl Server { value: output.as_ref().map(|o| o.value), sat: entry.sat, satpoint, - timestamp: timestamp(entry.timestamp).timestamp(), + timestamp: timestamp(entry.timestamp.into()).timestamp(), }) .into_response(), ) @@ -1553,7 +1545,7 @@ impl Server { rune: info.rune, sat: info.entry.sat, satpoint: info.satpoint, - timestamp: timestamp(info.entry.timestamp).timestamp(), + timestamp: timestamp(info.entry.timestamp.into()).timestamp(), value: info.output.as_ref().map(|o| o.value), }) .into_response() @@ -1574,7 +1566,7 @@ impl Server { rune: info.rune, sat: info.entry.sat, satpoint: info.satpoint, - timestamp: timestamp(info.entry.timestamp), + timestamp: timestamp(info.entry.timestamp.into()), } .page(server_config) .into_response() @@ -2152,7 +2144,7 @@ mod tests { ( txid, RuneId { - block: self.index.block_count().unwrap() - 1, + block: (self.index.block_count().unwrap() - 1).into(), tx: 1, }, ) @@ -2701,6 +2693,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -2775,6 +2768,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune, spacers: 0 }, premine: u128::MAX, @@ -2888,6 +2882,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune, spacers: 1 }, premine: u128::MAX, @@ -2978,6 +2973,7 @@ mod tests { [( id, RuneEntry { + block: id.block, etching: txid, spaced_rune: SpacedRune { rune: Rune(RUNE), @@ -3042,6 +3038,7 @@ mod tests { [( id, RuneEntry { + block: id.block, divisibility: 1, etching: txid, spaced_rune: SpacedRune { rune, spacers: 0 }, diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 91e2b3063f..ef7ca90782 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -169,59 +169,7 @@ impl Inscribe { }; if let Some(etching) = etching { - let rune = etching.rune.rune; - - ensure!(!rune.is_reserved(), "rune `{rune}` is reserved"); - - ensure!( - etching.divisibility <= crate::runes::MAX_DIVISIBILITY, - " must be less than or equal 38" - ); - - ensure!( - wallet.has_rune_index(), - "etching runes requires index created with `--index-runes`", - ); - - ensure!( - wallet.get_rune(rune)?.is_none(), - "rune `{rune}` has already been etched", - ); - - let premine = etching.premine.to_amount(etching.divisibility)?; - - let supply = etching.supply.to_amount(etching.divisibility)?; - - let mintable = etching - .mint - .map(|mint| -> Result { - mint - .cap - .checked_mul(mint.limit.to_amount(etching.divisibility)?) - .ok_or_else(|| anyhow!("`mint.count` * `mint.limit` over maximum")) - }) - .transpose()? - .unwrap_or_default(); - - ensure!( - supply - == premine - .checked_add(mintable) - .ok_or_else(|| anyhow!("`premine` + `mint.count` * `mint.limit` over maximum"))?, - "`supply` not equal to `premine` + `mint.count` * `mint.limit`" - ); - - let bitcoin_client = wallet.bitcoin_client(); - - let count = bitcoin_client.get_block_count()?; - - let minimum = - Rune::minimum_at_height(wallet.chain(), Height(u32::try_from(count).unwrap() + 1)); - - ensure!( - rune >= minimum, - "rune is less than minimum for next block: {rune} < {minimum}", - ); + Self::check_etching(&wallet, &etching)?; } batch::Plan { @@ -266,6 +214,104 @@ impl Inscribe { Ok(None) } } + + fn check_etching(wallet: &Wallet, etching: &batch::Etching) -> Result { + let rune = etching.rune.rune; + + ensure!(!rune.is_reserved(), "rune `{rune}` is reserved"); + + ensure!( + etching.divisibility <= crate::runes::MAX_DIVISIBILITY, + " must be less than or equal 38" + ); + + ensure!( + wallet.has_rune_index(), + "etching runes requires index created with `--index-runes`", + ); + + ensure!( + wallet.get_rune(rune)?.is_none(), + "rune `{rune}` has already been etched", + ); + + let premine = etching.premine.to_amount(etching.divisibility)?; + + let supply = etching.supply.to_amount(etching.divisibility)?; + + let mintable = etching + .terms + .map(|terms| -> Result { + terms + .cap + .checked_mul(terms.limit.to_amount(etching.divisibility)?) + .ok_or_else(|| anyhow!("`terms.count` * `terms.limit` over maximum")) + }) + .transpose()? + .unwrap_or_default(); + + ensure!( + supply + == premine + .checked_add(mintable) + .ok_or_else(|| anyhow!("`premine` + `terms.count` * `terms.limit` over maximum"))?, + "`supply` not equal to `premine` + `terms.count` * `terms.limit`" + ); + + ensure!(supply > 0, "`supply` must be greater than zero"); + + let bitcoin_client = wallet.bitcoin_client(); + + let current_height = u32::try_from(bitcoin_client.get_block_count()?).unwrap(); + + let reveal_height = current_height + 1 + RUNE_COMMIT_INTERVAL; + + if let Some(terms) = etching.terms { + if let Some((start, end)) = terms.offset.and_then(|range| range.start.zip(range.end)) { + ensure!( + end > start, + "`terms.offset.end` must be greater than `terms.offset.start`" + ); + } + + if let Some((start, end)) = terms.height.and_then(|range| range.start.zip(range.end)) { + ensure!( + end > start, + "`terms.height.end` must be greater than `terms.height.start`" + ); + } + + if let Some(end) = terms.height.and_then(|range| range.end) { + ensure!( + end > reveal_height.into(), + "`terms.height.end` must be greater than the reveal transaction block height of {reveal_height}" + ); + } + + if let Some(start) = terms.height.and_then(|range| range.start) { + ensure!( + start > reveal_height.into(), + "`terms.height.start` must be greater than the reveal transaction block height of {reveal_height}" + ); + } + + ensure!(terms.cap > 0, "`terms.cap` must be greater than zero",); + + ensure!( + terms.limit.to_amount(etching.divisibility)? > 0, + "`terms.limit` must be greater than zero", + ); + } + + let minimum = Rune::minimum_at_height(wallet.chain(), Height(reveal_height)); + + ensure!( + rune >= minimum, + "rune is less than minimum for next block: {rune} < {minimum}", + ); + + Ok(()) + } } #[cfg(test)] diff --git a/src/subcommand/wallet/mint.rs b/src/subcommand/wallet/mint.rs index 7982457bf4..9b6a7d4649 100644 --- a/src/subcommand/wallet/mint.rs +++ b/src/subcommand/wallet/mint.rs @@ -26,25 +26,20 @@ impl Mint { let bitcoin_client = wallet.bitcoin_client(); - let block_height: u32 = bitcoin_client.get_block_count()?.try_into().unwrap(); - - let block_time = bitcoin_client - .get_block(&bitcoin_client.get_best_block_hash()?)? - .header - .time; + let block_height = bitcoin_client.get_block_count()?; let Some((id, rune_entry, _)) = wallet.get_rune(rune)? else { bail!("rune {rune} has not been etched"); }; let limit = rune_entry - .mintable(Height(block_height), block_time) + .mintable(block_height) .map_err(|err| anyhow!("rune {rune} {err}"))?; let destination = wallet.get_change_address()?; let runestone = Runestone { - claim: Some(id), + mint: Some(id), ..default() }; diff --git a/src/templates/rune.rs b/src/templates/rune.rs index e43c5c2b8b..d9c969a47d 100644 --- a/src/templates/rune.rs +++ b/src/templates/rune.rs @@ -23,15 +23,16 @@ mod tests { assert_regex_match!( RuneHtml { entry: RuneEntry { + block: 1, burned: 123456789123456789, divisibility: 9, etching: Txid::all_zeros(), mints: 100, - mint: Some(MintEntry { + terms: Some(Terms { cap: Some(101), - end: Some(11), + offset: (None, None), + height: (Some(10), Some(11)), limit: Some(1000000001), - deadline: Some(7), }), number: 25, premine: 123456789, @@ -65,8 +66,8 @@ mod tests {
mint
-
deadline
-
+
start
+
10
end
11
limit
@@ -105,8 +106,9 @@ mod tests { assert_regex_match!( RuneHtml { entry: RuneEntry { + block: 0, burned: 123456789123456789, - mint: None, + terms: None, divisibility: 9, etching: Txid::all_zeros(), mints: 0, @@ -137,11 +139,12 @@ mod tests { assert_regex_match!( RuneHtml { entry: RuneEntry { + block: 0, burned: 123456789123456789, - mint: Some(MintEntry { + terms: Some(Terms { cap: None, - deadline: None, - end: None, + offset: (None, None), + height: (None, None), limit: None, }), divisibility: 9, @@ -165,7 +168,7 @@ mod tests {
mint
-
deadline
+
start
none
end
none
diff --git a/src/wallet/batch.rs b/src/wallet/batch.rs index b5f4fe7051..56c8957623 100644 --- a/src/wallet/batch.rs +++ b/src/wallet/batch.rs @@ -16,14 +16,17 @@ use { pub(crate) use transactions::Transactions; -pub use {entry::Entry, etching::Etching, file::File, mint::Mint, mode::Mode, plan::Plan}; +pub use { + entry::Entry, etching::Etching, file::File, mode::Mode, plan::Plan, range::Range, terms::Terms, +}; pub mod entry; mod etching; pub mod file; -mod mint; pub mod mode; pub mod plan; +mod range; +mod terms; mod transactions; #[derive(Debug, Serialize, Deserialize)] diff --git a/src/wallet/batch/etching.rs b/src/wallet/batch/etching.rs index 9af5f4bcc2..700e90eaf9 100644 --- a/src/wallet/batch/etching.rs +++ b/src/wallet/batch/etching.rs @@ -4,9 +4,9 @@ use super::*; #[serde(deny_unknown_fields)] pub struct Etching { pub divisibility: u8, - pub mint: Option, pub premine: Decimal, pub rune: SpacedRune, pub supply: Decimal, pub symbol: char, + pub terms: Option, } diff --git a/src/wallet/batch/plan.rs b/src/wallet/batch/plan.rs index e3b6937ca1..4135dbbeef 100644 --- a/src/wallet/batch/plan.rs +++ b/src/wallet/batch/plan.rs @@ -458,19 +458,23 @@ impl Plan { let inner = Runestone { cenotaph: false, - claim: None, - default_output: None, edicts, etching: Some(runes::Etching { divisibility: (etching.divisibility > 0).then_some(etching.divisibility), - mint: etching - .mint - .map(|mint| -> Result { - Ok(runes::Mint { - cap: (mint.cap > 0).then_some(mint.cap), - deadline: mint.deadline, - limit: Some(mint.limit.to_amount(etching.divisibility)?), - term: mint.term, + terms: etching + .terms + .map(|terms| -> Result { + Ok(runes::Terms { + cap: (terms.cap > 0).then_some(terms.cap), + height: ( + terms.height.and_then(|range| (range.start)), + terms.height.and_then(|range| (range.end)), + ), + limit: Some(terms.limit.to_amount(etching.divisibility)?), + offset: ( + terms.offset.and_then(|range| (range.start)), + terms.offset.and_then(|range| (range.end)), + ), }) }) .transpose()?, @@ -479,6 +483,8 @@ impl Plan { spacers: (etching.rune.spacers > 0).then_some(etching.rune.spacers), symbol: Some(etching.symbol), }), + mint: None, + pointer: None, }; let script_pubkey = inner.encipher(); diff --git a/src/wallet/batch/range.rs b/src/wallet/batch/range.rs new file mode 100644 index 0000000000..69e83476a1 --- /dev/null +++ b/src/wallet/batch/range.rs @@ -0,0 +1,8 @@ +use super::*; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Copy, Clone, Default)] +#[serde(deny_unknown_fields)] +pub struct Range { + pub start: Option, + pub end: Option, +} diff --git a/src/wallet/batch/mint.rs b/src/wallet/batch/terms.rs similarity index 67% rename from src/wallet/batch/mint.rs rename to src/wallet/batch/terms.rs index cced5469b7..a0f7228e2a 100644 --- a/src/wallet/batch/mint.rs +++ b/src/wallet/batch/terms.rs @@ -2,9 +2,9 @@ use super::*; #[derive(Serialize, Deserialize, PartialEq, Debug, Copy, Clone, Default)] #[serde(deny_unknown_fields)] -pub struct Mint { - pub deadline: Option, - pub limit: Decimal, +pub struct Terms { pub cap: u128, - pub term: Option, + pub height: Option, + pub limit: Decimal, + pub offset: Option, } diff --git a/templates/block.html b/templates/block.html index b13fd560ea..dec589f8fb 100644 --- a/templates/block.html +++ b/templates/block.html @@ -2,7 +2,7 @@

Block {{ self.height }}

hash
{{self.hash}}
target
{{self.target}}
-
timestamp
+
timestamp
size
{{self.block.size()}}
weight
{{self.block.weight()}}
%% if self.height.0 > 0 { diff --git a/templates/rune.html b/templates/rune.html index c83430e8bd..282675be9e 100644 --- a/templates/rune.html +++ b/templates/rune.html @@ -16,23 +16,23 @@

{{ self.entry.spaced_rune }}

etching transaction
{{ self.id.tx }}
mint
-%% if let Some(mint) = self.entry.mint { +%% if let Some(terms) = self.entry.terms {
-
deadline
-%% if let Some(deadline) = mint.deadline { -
+
start
+%% if let Some(start) = self.entry.start() { +
{{ start }}
%% } else {
none
%% }
end
-%% if let Some(end) = mint.end { +%% if let Some(end) = self.entry.end() {
{{ end }}
%% } else {
none
%% }
limit
-%% if let Some(limit) = mint.limit { +%% if let Some(limit) = terms.limit {
{{ self.entry.pile(limit) }}
%% } else {
none
@@ -40,9 +40,9 @@

{{ self.entry.spaced_rune }}

mints
{{ self.entry.mints }}
cap
-
{{ mint.cap.unwrap_or_default() }}
+
{{ terms.cap.unwrap_or_default() }}
remaining
-
{{ mint.cap.unwrap_or_default() - self.entry.mints }}
+
{{ terms.cap.unwrap_or_default() - self.entry.mints }}
mintable
{{ self.mintable }}
diff --git a/tests/json_api.rs b/tests/json_api.rs index 3e5cd292be..abc8b635ed 100644 --- a/tests/json_api.rs +++ b/tests/json_api.rs @@ -546,8 +546,9 @@ fn get_runes() { rune_json, api::Rune { entry: RuneEntry { + block: a.id.block, burned: 0, - mint: None, + terms: None, divisibility: 0, etching: a.inscribe.reveal, mints: 0, @@ -582,8 +583,9 @@ fn get_runes() { ( RuneId { block: 11, tx: 1 }, RuneEntry { + block: a.id.block, burned: 0, - mint: None, + terms: None, divisibility: 0, etching: a.inscribe.reveal, mints: 0, @@ -600,8 +602,9 @@ fn get_runes() { ( RuneId { block: 19, tx: 1 }, RuneEntry { + block: b.id.block, burned: 0, - mint: None, + terms: None, divisibility: 0, etching: b.inscribe.reveal, mints: 0, @@ -618,8 +621,9 @@ fn get_runes() { ( RuneId { block: 27, tx: 1 }, RuneEntry { + block: c.id.block, burned: 0, - mint: None, + terms: None, divisibility: 0, etching: c.inscribe.reveal, mints: 0, diff --git a/tests/lib.rs b/tests/lib.rs index ab5c6d4903..1a84e390ce 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -190,7 +190,7 @@ fn etch( etching: Some(batch::Etching { supply: "1000".parse().unwrap(), divisibility: 0, - mint: None, + terms: None, premine: "1000".parse().unwrap(), rune: SpacedRune { rune, spacers: 0 }, symbol: '¢', @@ -238,11 +238,9 @@ fn batch( bitcoin_rpc_server.mine_blocks(1); let block_height = bitcoin_rpc_server.height(); - let block_hash = *bitcoin_rpc_server.state().hashes.last().unwrap(); - let block_time = bitcoin_rpc_server.state().blocks[&block_hash].header.time; let id = RuneId { - block: u32::try_from(block_height).unwrap(), + block: block_height, tx: 1, }; @@ -251,19 +249,19 @@ fn batch( let batch::Etching { divisibility, - mint, premine, rune, supply, symbol, + terms, } = batchfile.etching.unwrap(); { let supply = supply.to_amount(divisibility).unwrap(); let premine = premine.to_amount(divisibility).unwrap(); - let mintable = mint - .map(|mint| mint.cap * mint.limit.to_amount(divisibility).unwrap()) + let mintable = terms + .map(|terms| terms.cap * terms.limit.to_amount(divisibility).unwrap()) .unwrap_or_default(); assert_eq!(supply, premine + mintable); @@ -271,31 +269,54 @@ fn batch( let mut mint_definition = Vec::::new(); - if let Some(mint) = mint { + if let Some(terms) = terms { mint_definition.push("
".into()); mint_definition.push("
".into()); let mut mintable = true; - mint_definition.push("
deadline
".into()); - if let Some(deadline) = mint.deadline { - mintable &= block_time < deadline; - mint_definition.push(format!( - "
", - ord::timestamp(deadline) - )); - } else { - mint_definition.push("
none
".into()); + mint_definition.push("
start
".into()); + { + let relative = terms + .offset + .and_then(|range| range.start) + .map(|start| start + block_height); + let absolute = terms.height.and_then(|range| range.start); + + let start = relative + .zip(absolute) + .map(|(relative, absolute)| relative.max(absolute)) + .or(relative) + .or(absolute); + + if let Some(start) = start { + mintable &= block_height + 1 >= start; + mint_definition.push(format!("
{start}
")); + } else { + mint_definition.push("
none
".into()); + } } mint_definition.push("
end
".into()); - - if let Some(term) = mint.term { - let end = block_height + u64::from(term); - mintable &= block_height + 1 < end; - mint_definition.push(format!("
{end}
")); - } else { - mint_definition.push("
none
".into()); + { + let relative = terms + .offset + .and_then(|range| range.end) + .map(|end| end + block_height); + let absolute = terms.height.and_then(|range| range.end); + + let end = relative + .zip(absolute) + .map(|(relative, absolute)| relative.min(absolute)) + .or(relative) + .or(absolute); + + if let Some(end) = end { + mintable &= block_height + 1 < end; + mint_definition.push(format!("
{end}
")); + } else { + mint_definition.push("
none
".into()); + } } mint_definition.push("
limit
".into()); @@ -303,7 +324,7 @@ fn batch( mint_definition.push(format!( "
{}
", Pile { - amount: mint.limit.to_amount(divisibility).unwrap(), + amount: terms.limit.to_amount(divisibility).unwrap(), divisibility, symbol: Some(symbol), } @@ -312,9 +333,9 @@ fn batch( mint_definition.push("
mints
".into()); mint_definition.push("
0
".into()); mint_definition.push("
cap
".into()); - mint_definition.push(format!("
{}
", mint.cap)); + mint_definition.push(format!("
{}
", terms.cap)); mint_definition.push("
remaining
".into()); - mint_definition.push(format!("
{}
", mint.cap)); + mint_definition.push(format!("
{}
", terms.cap)); mint_definition.push("
mintable
".into()); mint_definition.push(format!("
{mintable}
")); diff --git a/tests/runes.rs b/tests/runes.rs index d0d074fbf0..ae561609b1 100644 --- a/tests/runes.rs +++ b/tests/runes.rs @@ -58,7 +58,7 @@ fn one_rune() { divisibility: 0, etching: etch.inscribe.reveal, id: RuneId { block: 8, tx: 1 }, - mint: None, + terms: None, mints: 0, number: 0, premine: 1000, @@ -106,7 +106,7 @@ fn two_runes() { divisibility: 0, etching: a.inscribe.reveal, id: RuneId { block: 8, tx: 1 }, - mint: None, + terms: None, mints: 0, number: 0, premine: 1000, @@ -128,7 +128,7 @@ fn two_runes() { divisibility: 0, etching: b.inscribe.reveal, id: RuneId { block: 16, tx: 1 }, - mint: None, + terms: None, mints: 0, number: 1, premine: 1000, diff --git a/tests/wallet/balance.rs b/tests/wallet/balance.rs index d47f6f178e..29ca5b8bf1 100644 --- a/tests/wallet/balance.rs +++ b/tests/wallet/balance.rs @@ -106,11 +106,11 @@ fn runic_utxos_are_deducted_from_cardinal() { batch::File { etching: Some(batch::Etching { divisibility: 0, - mint: None, premine: "1000".parse().unwrap(), - supply: "1000".parse().unwrap(), rune: SpacedRune { rune, spacers: 1 }, + supply: "1000".parse().unwrap(), symbol: '¢', + terms: None, }), inscriptions: vec![batch::Entry { file: "inscription.jpeg".into(), diff --git a/tests/wallet/inscribe.rs b/tests/wallet/inscribe.rs index 4bee89b88c..f117511f00 100644 --- a/tests/wallet/inscribe.rs +++ b/tests/wallet/inscribe.rs @@ -2643,7 +2643,159 @@ fn batch_inscribe_can_etch_rune() { supply: "1000".parse().unwrap(), premine: "1000".parse().unwrap(), symbol: '¢', - mint: None, + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }, + ); + + let parent = batch.inscribe.inscriptions[0].id; + + let request = ord_rpc_server.request(format!("/content/{parent}")); + + assert_eq!(request.status(), 200); + assert_eq!(request.headers().get("content-type").unwrap(), "image/jpeg"); + assert_eq!(request.text().unwrap(), "inscription"); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{parent}"), + r".*
rune
\s*
AAAAAAAAAAAAA
.*", + ); + + ord_rpc_server.assert_response_regex( + "/rune/AAAAAAAAAAAAA", + format!( + r".*
parent
\s*
{parent}
.*" + ), + ); + + assert!(bitcoin_rpc_server.state().is_wallet_address( + &batch + .inscribe + .rune + .unwrap() + .destination + .unwrap() + .require_network(Network::Regtest) + .unwrap() + )); +} + +#[test] +fn batch_inscribe_can_etch_rune_with_offset() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let batch = batch( + &bitcoin_rpc_server, + &ord_rpc_server, + batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "10000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 9, + limit: "1000".parse().unwrap(), + offset: Some(batch::Range { + start: Some(10), + end: Some(20), + }), + height: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.jpeg".into(), + ..default() + }], + ..default() + }, + ); + + let parent = batch.inscribe.inscriptions[0].id; + + let request = ord_rpc_server.request(format!("/content/{parent}")); + + assert_eq!(request.status(), 200); + assert_eq!(request.headers().get("content-type").unwrap(), "image/jpeg"); + assert_eq!(request.text().unwrap(), "inscription"); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{parent}"), + r".*
rune
\s*
AAAAAAAAAAAAA
.*", + ); + + ord_rpc_server.assert_response_regex( + "/rune/AAAAAAAAAAAAA", + format!( + r".*
parent
\s*
{parent}
.*" + ), + ); + + assert!(bitcoin_rpc_server.state().is_wallet_address( + &batch + .inscribe + .rune + .unwrap() + .destination + .unwrap() + .require_network(Network::Regtest) + .unwrap() + )); +} + +#[test] +fn batch_inscribe_can_etch_rune_with_height() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let batch = batch( + &bitcoin_rpc_server, + &ord_rpc_server, + batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "10000".parse().unwrap(), + premine: "1000".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 9, + limit: "1000".parse().unwrap(), + height: Some(batch::Range { + start: Some(10), + end: Some(20), + }), + offset: None, + }), }), inscriptions: vec![batch::Entry { file: "inscription.jpeg".into(), @@ -2712,7 +2864,7 @@ fn etch_existing_rune_error() { supply: "1000".parse().unwrap(), premine: "1000".parse().unwrap(), symbol: '¢', - mint: None, + terms: None, }), inscriptions: vec![batch::Entry { file: "inscription.txt".into(), @@ -2756,7 +2908,7 @@ fn etch_reserved_rune_error() { premine: "1000".parse().unwrap(), supply: "1000".parse().unwrap(), symbol: '¢', - mint: None, + terms: None, }), inscriptions: vec![batch::Entry { file: "inscription.txt".into(), @@ -2800,7 +2952,7 @@ fn etch_sub_minimum_rune_error() { supply: "1000".parse().unwrap(), premine: "1000".parse().unwrap(), symbol: '¢', - mint: None, + terms: None, }), inscriptions: vec![batch::Entry { file: "inscription.txt".into(), @@ -2812,7 +2964,7 @@ fn etch_sub_minimum_rune_error() { ) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: rune is less than minimum for next block: A < ZZWZRFAGQTKZ\n") + .expected_stderr("error: rune is less than minimum for next block: A < ZZQYZPATYGGX\n") .expected_exit_code(1) .run_and_extract_stdout(); } @@ -2843,7 +2995,7 @@ fn etch_requires_rune_index() { supply: "1000".parse().unwrap(), premine: "1000".parse().unwrap(), symbol: '¢', - mint: None, + terms: None, }), inscriptions: vec![batch::Entry { file: "inscription.txt".into(), @@ -2887,7 +3039,7 @@ fn etch_divisibility_over_maximum_error() { supply: "1000".parse().unwrap(), premine: "1000".parse().unwrap(), symbol: '¢', - mint: None, + terms: None, }), inscriptions: vec![batch::Entry { file: "inscription.txt".into(), @@ -2931,11 +3083,14 @@ fn etch_mintable_overflow_error() { supply: default(), premine: default(), symbol: '¢', - mint: Some(batch::Mint { + terms: Some(batch::Terms { cap: 2, - term: Some(2), + offset: Some(batch::Range { + end: Some(2), + start: None, + }), limit: "340282366920938463463374607431768211455".parse().unwrap(), - deadline: None, + height: None, }), }), inscriptions: vec![batch::Entry { @@ -2948,7 +3103,7 @@ fn etch_mintable_overflow_error() { ) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: `mint.count` * `mint.limit` over maximum\n") + .expected_stderr("error: `terms.count` * `terms.limit` over maximum\n") .expected_exit_code(1) .run_and_extract_stdout(); } @@ -2980,11 +3135,14 @@ fn etch_mintable_plus_premine_overflow_error() { supply: default(), premine: "1".parse().unwrap(), symbol: '¢', - mint: Some(batch::Mint { + terms: Some(batch::Terms { cap: 1, - term: Some(2), + offset: Some(batch::Range { + end: Some(2), + start: None, + }), limit: "340282366920938463463374607431768211455".parse().unwrap(), - deadline: None, + height: None, }), }), inscriptions: vec![batch::Entry { @@ -2997,7 +3155,7 @@ fn etch_mintable_plus_premine_overflow_error() { ) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: `premine` + `mint.count` * `mint.limit` over maximum\n") + .expected_stderr("error: `premine` + `terms.count` * `terms.limit` over maximum\n") .expected_exit_code(1) .run_and_extract_stdout(); } @@ -3029,11 +3187,423 @@ fn incorrect_supply_error() { supply: "1".parse().unwrap(), premine: "1".parse().unwrap(), symbol: '¢', - mint: Some(batch::Mint { + terms: Some(batch::Terms { + cap: 1, + offset: Some(batch::Range { + end: Some(2), + start: None, + }), + limit: "1".parse().unwrap(), + height: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: `supply` not equal to `premine` + `terms.count` * `terms.limit`\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn zero_offset_interval_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "2".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + offset: Some(batch::Range { + end: Some(2), + start: Some(2), + }), + limit: "1".parse().unwrap(), + height: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: `terms.offset.end` must be greater than `terms.offset.start`\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn zero_height_interval_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "2".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + height: Some(batch::Range { + end: Some(2), + start: Some(2), + }), + limit: "1".parse().unwrap(), + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: `terms.height.end` must be greater than `terms.height.start`\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn invalid_start_height_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "2".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + height: Some(batch::Range { + end: None, + start: Some(0), + }), + limit: "1".parse().unwrap(), + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr( + "error: `terms.height.start` must be greater than the reveal transaction block height of 8\n", + ) + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn invalid_end_height_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "2".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + height: Some(batch::Range { + start: None, + end: Some(0), + }), + limit: "1".parse().unwrap(), + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr( + "error: `terms.height.end` must be greater than the reveal transaction block height of 8\n", + ) + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn zero_supply_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "0".parse().unwrap(), + premine: "0".parse().unwrap(), + symbol: '¢', + terms: None, + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: `supply` must be greater than zero\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn zero_cap_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "1".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 0, + height: None, + limit: "1".parse().unwrap(), + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: `terms.cap` must be greater than zero\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn zero_limit_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + supply: "1".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + terms: Some(batch::Terms { + cap: 1, + height: None, + limit: "0".parse().unwrap(), + offset: None, + }), + }), + inscriptions: vec![batch::Entry { + file: "inscription.txt".into(), + ..default() + }], + ..default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: `terms.limit` must be greater than zero\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn oversize_runestone_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&batch::File { + etching: Some(batch::Etching { + divisibility: 0, + rune: SpacedRune { + rune: Rune(6402364363415443603228541259936211926 - 1), + spacers: 0b00000111_11111111_11111111_11111111, + }, + supply: u128::MAX.to_string().parse().unwrap(), + premine: (u128::MAX - 1).to_string().parse().unwrap(), + symbol: '\u{10FFFF}', + terms: Some(batch::Terms { cap: 1, - term: Some(2), + height: Some(batch::Range { + start: Some(u64::MAX - 1), + end: Some(u64::MAX), + }), + offset: Some(batch::Range { + start: Some(u64::MAX - 1), + end: Some(u64::MAX), + }), limit: "1".parse().unwrap(), - deadline: None, }), }), inscriptions: vec![batch::Entry { @@ -3046,7 +3616,7 @@ fn incorrect_supply_error() { ) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: `supply` not equal to `premine` + `mint.count` * `mint.limit`\n") + .expected_stderr("error: runestone greater than maximum OP_RETURN size: 125 > 82\n") .expected_exit_code(1) .run_and_extract_stdout(); } diff --git a/tests/wallet/mint.rs b/tests/wallet/mint.rs index 7160950ec9..172e94566a 100644 --- a/tests/wallet/mint.rs +++ b/tests/wallet/mint.rs @@ -29,11 +29,14 @@ fn minting_rune_and_fails_if_after_end() { premine: "0".parse().unwrap(), symbol: '¢', supply: "111.1".parse().unwrap(), - mint: Some(batch::Mint { + terms: Some(batch::Terms { cap: 1, - term: Some(2), + offset: Some(batch::Range { + end: Some(2), + start: None, + }), limit: "111.1".parse().unwrap(), - deadline: None, + height: None, }), }), inscriptions: vec![batch::Entry { @@ -125,7 +128,7 @@ fn minting_rune_fails_if_not_mintable() { supply: "1000".parse().unwrap(), premine: "1000".parse().unwrap(), symbol: '¢', - mint: None, + terms: None, }), inscriptions: vec![batch::Entry { file: "inscription.jpeg".into(), @@ -146,64 +149,6 @@ fn minting_rune_fails_if_not_mintable() { .run_and_extract_stdout(); } -#[test] -fn minting_rune_fails_if_after_deadline() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - let rune = Rune(RUNE); - let deadline = 9; - - batch( - &bitcoin_rpc_server, - &ord_rpc_server, - batch::File { - etching: Some(batch::Etching { - divisibility: 1, - rune: SpacedRune { rune, spacers: 0 }, - premine: "0".parse().unwrap(), - supply: "222.2".parse().unwrap(), - symbol: '¢', - mint: Some(batch::Mint { - cap: 2, - term: Some(2), - limit: "111.1".parse().unwrap(), - deadline: Some(deadline), - }), - }), - inscriptions: vec![batch::Entry { - file: "inscription.jpeg".into(), - ..default() - }], - ..default() - }, - ); - - CommandBuilder::new(format!( - "--chain regtest --index-runes wallet mint --fee-rate 1 --rune {rune}", - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new(format!( - "--chain regtest --index-runes wallet mint --fee-rate 1 --rune {rune}", - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .expected_stderr(format!("error: rune {rune} mint ended at {deadline}\n")) - .run_and_extract_stdout(); -} - #[test] fn minting_rune_with_no_rune_index_fails() { let bitcoin_rpc_server = test_bitcoincore_rpc::builder() @@ -253,11 +198,14 @@ fn minting_rune_and_then_sending_works() { premine: "111".parse().unwrap(), supply: "132".parse().unwrap(), symbol: '¢', - mint: Some(batch::Mint { + terms: Some(batch::Terms { cap: 1, - term: Some(10), + offset: Some(batch::Range { + end: Some(10), + start: None, + }), limit: "21".parse().unwrap(), - deadline: None, + height: None, }), }), inscriptions: vec![batch::Entry { diff --git a/tests/wallet/selection.rs b/tests/wallet/selection.rs index 26800030e9..46f37b0640 100644 --- a/tests/wallet/selection.rs +++ b/tests/wallet/selection.rs @@ -130,11 +130,11 @@ fn mint_does_not_select_inscription() { premine: "1000".parse().unwrap(), supply: "2000".parse().unwrap(), symbol: '¢', - mint: Some(batch::Mint { - deadline: None, + terms: Some(batch::Terms { cap: 1, limit: "1000".parse().unwrap(), - term: None, + offset: None, + height: None, }), }), inscriptions: vec![batch::Entry { diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index c02dfa4263..bbdb93bfb6 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -888,7 +888,7 @@ fn sending_rune_with_divisibility_works() { premine: "1000".parse().unwrap(), supply: "1000".parse().unwrap(), symbol: '¢', - mint: None, + terms: None, }), inscriptions: vec![batch::Entry { file: "inscription.jpeg".into(), @@ -1103,7 +1103,7 @@ fn sending_rune_creates_transaction_with_expected_runestone() { pretty_assert_eq!( Runestone::from_transaction(&tx).unwrap(), Runestone { - default_output: None, + pointer: None, etching: None, edicts: vec![Edict { id: etch.id, @@ -1111,7 +1111,7 @@ fn sending_rune_creates_transaction_with_expected_runestone() { output: 2 }], cenotaph: false, - claim: None, + mint: None, }, ); }