From c0f5bb23a5f8fed9ddfcffd6e28203ed458a79c9 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor <casey@rodarmor.com> Date: Mon, 25 Mar 2024 16:09:53 -0700 Subject: [PATCH] Allow supply-capped mints (#3365) --- src/index.rs | 2 +- src/index/entry.rs | 212 ++++++++++++-- src/index/testing.rs | 2 +- src/index/updater.rs | 19 +- src/index/updater/rune_updater.rs | 91 +++--- src/runes.rs | 448 +++++++++++++++++++++--------- src/runes/etching.rs | 5 +- src/runes/mint.rs | 1 + src/runes/runestone.rs | 186 +++++++++---- src/runes/tag.rs | 2 + src/subcommand/runes.rs | 7 +- src/subcommand/server.rs | 14 +- src/subcommand/wallet/inscribe.rs | 23 ++ src/subcommand/wallet/mint.rs | 2 +- src/templates/rune.rs | 67 +---- src/wallet/batch/etching.rs | 1 + src/wallet/batch/mint.rs | 1 + src/wallet/batch/plan.rs | 8 +- templates/rune.html | 12 +- tests/json_api.rs | 4 - tests/lib.rs | 19 +- tests/wallet/balance.rs | 1 + tests/wallet/inscribe.rs | 153 ++++++++++ tests/wallet/mint.rs | 7 + tests/wallet/selection.rs | 2 + tests/wallet/send.rs | 1 + 26 files changed, 951 insertions(+), 339 deletions(-) diff --git a/src/index.rs b/src/index.rs index c3219f70c6..b87ff832f8 100644 --- a/src/index.rs +++ b/src/index.rs @@ -47,7 +47,7 @@ mod updater; #[cfg(test)] pub(crate) mod testing; -const SCHEMA_VERSION: u64 = 22; +const SCHEMA_VERSION: u64 = 23; macro_rules! define_table { ($name:ident, $key:ty, $value:ty) => { diff --git a/src/index/entry.rs b/src/index/entry.rs index 47796eac54..e32d107f5b 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -34,11 +34,10 @@ pub struct RuneEntry { pub divisibility: u8, pub etching: Txid, pub mint: Option<MintEntry>, - pub mints: u64, + pub mints: u128, pub number: u64, pub premine: u128, pub spaced_rune: SpacedRune, - pub supply: u128, pub symbol: Option<char>, pub timestamp: u32, } @@ -46,22 +45,40 @@ pub struct RuneEntry { impl RuneEntry { pub fn mintable(&self, block_height: Height, block_time: u32) -> Result<u128, MintError> { let Some(mint) = self.mint else { - return Err(MintError::Unmintable(self.spaced_rune.rune)); + return Err(MintError::Unmintable); }; if let Some(end) = mint.end { if block_height.0 >= end { - return Err(MintError::End((self.spaced_rune.rune, end))); + return Err(MintError::End(end)); } } if let Some(deadline) = mint.deadline { if block_time >= deadline { - return Err(MintError::Deadline((self.spaced_rune.rune, deadline))); + return Err(MintError::Deadline(deadline)); } } - Ok(mint.limit.unwrap_or(runes::MAX_LIMIT)) + let cap = mint.cap.unwrap_or_default(); + + if self.mints >= cap { + return Err(MintError::Cap(cap)); + } + + Ok(mint.limit.unwrap_or_default()) + } + + pub fn supply(&self) -> u128 { + self.premine + self.mints * self.mint.and_then(|mint| mint.limit).unwrap_or_default() + } + + pub fn pile(&self, amount: u128) -> Pile { + Pile { + amount, + divisibility: self.divisibility, + symbol: self.symbol, + } } } @@ -70,23 +87,24 @@ pub(super) type RuneEntryValue = ( u8, // divisibility (u128, u128), // etching Option<MintEntryValue>, // mint parameters - u64, // mints + u128, // mints u64, // number u128, // premine (u128, u32), // spaced rune - u128, // supply Option<char>, // symbol u32, // timestamp ); #[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize, Default)] pub struct MintEntry { + pub cap: Option<u128>, // mint cap pub deadline: Option<u32>, // unix timestamp pub end: Option<u32>, // block height pub limit: Option<u128>, // claim amount } type MintEntryValue = ( + Option<u128>, // cap Option<u32>, // deadline Option<u32>, // end Option<u128>, // limit @@ -103,7 +121,6 @@ impl Default for RuneEntry { number: 0, premine: 0, spaced_rune: SpacedRune::default(), - supply: 0, symbol: None, timestamp: 0, } @@ -123,7 +140,6 @@ impl Entry for RuneEntry { number, premine, (rune, spacers), - supply, symbol, timestamp, ): RuneEntryValue, @@ -141,7 +157,8 @@ impl Entry for RuneEntry { high[14], high[15], ]) }, - mint: mint.map(|(deadline, end, limit)| MintEntry { + mint: mint.map(|(cap, deadline, end, limit)| MintEntry { + cap, deadline, end, limit, @@ -153,7 +170,6 @@ impl Entry for RuneEntry { rune: Rune(rune), spacers, }, - supply, symbol, timestamp, } @@ -178,16 +194,16 @@ impl Entry for RuneEntry { }, self.mint.map( |MintEntry { + cap, deadline, end, limit, - }| (deadline, end, limit), + }| (cap, deadline, end, limit), ), self.mints, self.number, self.premine, (self.spaced_rune.rune.0, self.spaced_rune.spacers), - self.supply, self.symbol, self.timestamp, ) @@ -492,6 +508,7 @@ mod tests { 0x1E, 0x1F, ]), mint: Some(MintEntry { + cap: Some(1), deadline: Some(2), end: Some(4), limit: Some(5), @@ -503,7 +520,6 @@ mod tests { rune: Rune(7), spacers: 8, }, - supply: 9, symbol: Some('a'), timestamp: 10, }; @@ -515,12 +531,11 @@ mod tests { 0x0F0E0D0C0B0A09080706050403020100, 0x1F1E1D1C1B1A19181716151413121110, ), - Some((Some(2), Some(4), Some(5))), + Some((Some(1), Some(2), Some(4), Some(5))), 11, 6, 12, (7, 8), - 9, Some('a'), 10, ); @@ -550,4 +565,167 @@ mod tests { assert_eq!(actual, expected); } + + #[test] + fn mintable() { + assert_eq!( + RuneEntry::default().mintable(Height(0), 0), + Err(MintError::Unmintable) + ); + + assert_eq!( + RuneEntry { + mint: Some(MintEntry { + end: Some(1), + limit: Some(1000), + cap: Some(1), + ..default() + }), + ..default() + } + .mintable(Height(0), 0), + Ok(1000), + ); + + assert_eq!( + RuneEntry { + mint: Some(MintEntry { + end: Some(1), + limit: Some(1000), + cap: Some(1), + ..default() + }), + ..default() + } + .mintable(Height(1), 0), + Err(MintError::End(1)), + ); + + assert_eq!( + RuneEntry { + mint: Some(MintEntry { + deadline: Some(1), + limit: Some(1000), + cap: Some(1), + ..default() + }), + ..default() + } + .mintable(Height(0), 0), + Ok(1000), + ); + + assert_eq!( + RuneEntry { + mint: Some(MintEntry { + deadline: Some(1), + limit: Some(1000), + cap: Some(1), + ..default() + }), + ..default() + } + .mintable(Height(0), 1), + Err(MintError::Deadline(1)), + ); + + assert_eq!( + RuneEntry { + mint: Some(MintEntry { + cap: Some(1), + limit: Some(1000), + ..default() + }), + mints: 0, + ..default() + } + .mintable(Height(0), 0), + Ok(1000), + ); + + assert_eq!( + RuneEntry { + mint: Some(MintEntry { + cap: Some(1), + limit: Some(1000), + ..default() + }), + mints: 1, + ..default() + } + .mintable(Height(0), 0), + Err(MintError::Cap(1)), + ); + + assert_eq!( + RuneEntry { + mint: Some(MintEntry { + cap: None, + limit: Some(1000), + ..default() + }), + mints: 0, + ..default() + } + .mintable(Height(0), 0), + Err(MintError::Cap(0)), + ); + } + + #[test] + fn supply() { + assert_eq!( + RuneEntry { + mint: Some(MintEntry { + limit: Some(1000), + ..default() + }), + mints: 0, + ..default() + } + .supply(), + 0 + ); + + assert_eq!( + RuneEntry { + mint: Some(MintEntry { + limit: Some(1000), + ..default() + }), + mints: 1, + ..default() + } + .supply(), + 1000 + ); + + assert_eq!( + RuneEntry { + mint: Some(MintEntry { + limit: Some(1000), + ..default() + }), + mints: 0, + premine: 1, + ..default() + } + .supply(), + 1 + ); + + assert_eq!( + RuneEntry { + mint: Some(MintEntry { + limit: Some(1000), + ..default() + }), + mints: 1, + premine: 1, + ..default() + } + .supply(), + 1001 + ); + } } diff --git a/src/index/testing.rs b/src/index/testing.rs index 0032a3e68f..dd0c5ed8ab 100644 --- a/src/index/testing.rs +++ b/src/index/testing.rs @@ -147,7 +147,7 @@ impl Context { for (id, entry) in runes { pretty_assert_eq!( outstanding.get(id).copied().unwrap_or_default(), - entry.supply - entry.burned + entry.supply() - entry.burned ); } } diff --git a/src/index/updater.rs b/src/index/updater.rs index b5229d171b..5e21331cc3 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -591,6 +591,8 @@ impl<'index> Updater<'index> { .unwrap_or(0); let mut rune_updater = RuneUpdater { + block_time: block.header.time, + burned: HashMap::new(), client: &self.index.client, height: self.height, id_to_entry: &mut rune_id_to_rune_entry, @@ -601,29 +603,14 @@ impl<'index> Updater<'index> { runes, sequence_number_to_rune_id: &mut sequence_number_to_rune_id, statistic_to_count: &mut statistic_to_count, - block_time: block.header.time, transaction_id_to_rune: &mut transaction_id_to_rune, - updates: HashMap::new(), }; for (i, (tx, txid)) in block.txdata.iter().enumerate() { rune_updater.index_runes(u32::try_from(i).unwrap(), tx, *txid)?; } - for (rune_id, update) in rune_updater.updates { - let mut entry = RuneEntry::load( - rune_id_to_rune_entry - .get(&rune_id.store())? - .unwrap() - .value(), - ); - - entry.burned += update.burned; - entry.mints += update.mints; - entry.supply += update.supply; - - rune_id_to_rune_entry.insert(&rune_id.store(), entry.store())?; - } + rune_updater.update()?; } height_to_block_header.insert(&self.height, &block.header.store())?; diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index 210039a134..5f2b6e8a57 100644 --- a/src/index/updater/rune_updater.rs +++ b/src/index/updater/rune_updater.rs @@ -9,22 +9,17 @@ struct Claim { } struct Etched { - balance: u128, divisibility: u8, id: RuneId, mint: Option<MintEntry>, + premine: u128, spaced_rune: SpacedRune, symbol: Option<char>, } -#[derive(Default)] -pub(crate) struct RuneUpdate { - pub(crate) burned: u128, - pub(crate) mints: u64, - pub(crate) supply: u128, -} - pub(super) struct RuneUpdater<'a, 'tx, 'client> { + pub(super) block_time: u32, + pub(super) burned: HashMap<RuneId, u128>, pub(super) client: &'client Client, pub(super) height: u32, pub(super) id_to_entry: &'a mut Table<'tx, RuneIdValue, RuneEntryValue>, @@ -35,9 +30,7 @@ pub(super) struct RuneUpdater<'a, 'tx, 'client> { pub(super) runes: u64, pub(super) sequence_number_to_rune_id: &'a mut Table<'tx, u32, RuneIdValue>, pub(super) statistic_to_count: &'a mut Table<'tx, u64, u64>, - pub(super) block_time: u32, pub(super) transaction_id_to_rune: &'a mut Table<'tx, &'static TxidValue, u128>, - pub(super) updates: HashMap<RuneId, RuneUpdate>, } impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { @@ -64,15 +57,14 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { .transpose()? { *unallocated.entry(claim.id).or_default() += claim.limit; + } - let update = self.updates.entry(claim.id).or_default(); + let etched = self.etched(tx_index, tx, &runestone)?; - update.mints += 1; - update.supply += claim.limit; + if let Some(Etched { id, premine, .. }) = etched { + *unallocated.entry(id).or_default() += premine; } - let mut etched = self.etched(tx_index, tx, &runestone)?; - if !cenotaph { for Edict { id, amount, output } in runestone.edicts { // edicts with output values greater than the number of outputs @@ -80,21 +72,18 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { let output = usize::try_from(output).unwrap(); assert!(output <= tx.output.len()); - let (balance, id) = if id == RuneId::default() { - // If this edict allocates new issuance runes, skip it - // if no issuance was present, or if the issuance was invalid. - // Additionally, replace ID 0 with the newly assigned ID, and - // get the unallocated balance of the issuance. - match etched.as_mut() { - Some(Etched { balance, id, .. }) => (balance, *id), - None => continue, - } + let id = if id == RuneId::default() { + let Some(Etched { id, .. }) = etched else { + continue; + }; + + id } else { - // Get the unallocated balance of the given ID - match unallocated.get_mut(&id) { - Some(balance) => (balance, id), - None => continue, - } + id + }; + + let Some(balance) = unallocated.get_mut(&id) else { + continue; }; let mut allocate = |balance: &mut u128, amount: u128, output: usize| { @@ -224,7 +213,17 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { // increment entries with burned runes for (id, amount) in burned { - self.updates.entry(id).or_default().burned += amount; + *self.burned.entry(id).or_default() += amount; + } + + Ok(()) + } + + pub(super) fn update(self) -> Result { + for (rune_id, burned) in self.burned { + let mut entry = RuneEntry::load(self.id_to_entry.get(&rune_id.store())?.unwrap().value()); + entry.burned += burned; + self.id_to_entry.insert(&rune_id.store(), entry.store())?; } Ok(()) @@ -232,10 +231,10 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { fn create_rune_entry(&mut self, txid: Txid, burn: bool, etched: Etched) -> Result { let Etched { - balance, divisibility, id, mint, + premine, spaced_rune, symbol, } = etched; @@ -248,8 +247,6 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { let number = self.runes; self.runes += 1; - let premine = u128::MAX - balance; - self .statistic_to_count .insert(&Statistic::Runes.into(), self.runes)?; @@ -265,7 +262,6 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { number, premine, spaced_rune, - supply: premine, symbol, timestamp: self.block_time, } @@ -320,36 +316,43 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> { }; Ok(Some(Etched { - balance: u128::MAX, - divisibility: etching.divisibility, + divisibility: etching.divisibility.unwrap_or_default(), id: RuneId { block: self.height, tx: tx_index, }, - spaced_rune: SpacedRune { - rune, - spacers: etching.spacers, - }, - symbol: etching.symbol, mint: etching.mint.map(|mint| MintEntry { + cap: mint.cap, deadline: mint.deadline, end: mint.term.map(|term| term + self.height), - limit: mint.limit.map(|limit| limit.min(runes::MAX_LIMIT)), + limit: mint.limit, }), + premine: etching.premine.unwrap_or_default(), + spaced_rune: SpacedRune { + rune, + spacers: etching.spacers.unwrap_or_default(), + }, + symbol: etching.symbol, })) } - fn claim(&self, id: RuneId) -> Result<Option<Claim>> { + fn claim(&mut self, id: RuneId) -> Result<Option<Claim>> { let Some(entry) = self.id_to_entry.get(&id.store())? else { return Ok(None); }; - let rune_entry = RuneEntry::load(entry.value()); + let mut rune_entry = RuneEntry::load(entry.value()); let Ok(limit) = rune_entry.mintable(Height(self.height), self.block_time) else { return Ok(None); }; + drop(entry); + + rune_entry.mints += 1; + + self.id_to_entry.insert(&id.store(), rune_entry.store())?; + Ok(Some(Claim { id, limit })) } diff --git a/src/runes.rs b/src/runes.rs index 1ed1d22886..6d03e69b5a 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -9,7 +9,6 @@ pub use { }; pub const MAX_DIVISIBILITY: u8 = 38; -pub const MAX_LIMIT: u128 = u64::MAX as u128; const MAGIC_NUMBER: opcodes::All = opcodes::all::OP_PUSHNUM_13; const RESERVED: u128 = 6402364363415443603228541259936211926; @@ -28,21 +27,21 @@ pub mod varint; type Result<T, E = Error> = std::result::Result<T, E>; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum MintError { - Deadline((Rune, u32)), - End((Rune, u32)), - Unmintable(Rune), + Cap(u128), + Deadline(u32), + End(u32), + Unmintable, } impl Display for MintError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - MintError::Deadline((rune, deadline)) => { - write!(f, "rune {rune} mint ended at {deadline}") - } - MintError::End((rune, end)) => write!(f, "rune {rune} mint ended on block {end}"), - MintError::Unmintable(rune) => write!(f, "rune {rune} not mintable"), + 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::Unmintable => write!(f, "not mintable"), } } } @@ -143,6 +142,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -160,7 +160,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -188,6 +187,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(minimum - 1)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -213,6 +213,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(minimum)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -230,7 +231,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -276,6 +276,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RESERVED - 1)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -293,7 +294,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -321,6 +321,7 @@ mod tests { }], etching: Some(Etching { rune: None, + premine: Some(u128::MAX), ..default() }), ..default() @@ -344,7 +345,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: 2, ..default() }, @@ -370,6 +370,7 @@ mod tests { output: 0, }], etching: Some(Etching { + premine: Some(u128::MAX), rune: None, ..default() }), @@ -395,7 +396,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: 2, ..default() }, @@ -409,7 +409,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: 4, number: 1, ..default() @@ -447,8 +446,9 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 1, + divisibility: Some(1), rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -469,7 +469,6 @@ mod tests { etching: txid, divisibility: 1, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -498,6 +497,7 @@ mod tests { ], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -515,7 +515,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -544,6 +543,7 @@ mod tests { ], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -561,7 +561,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, symbol: None, timestamp: id.block, ..default() @@ -586,6 +585,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(100), ..default() }), ..default() @@ -603,7 +603,6 @@ mod tests { spacers: 0, }, premine: 100, - supply: 100, timestamp: id.block, ..default() }, @@ -632,6 +631,7 @@ mod tests { ], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(200), ..default() }), ..default() @@ -650,7 +650,6 @@ mod tests { spacers: 0, }, premine: 200, - supply: 200, timestamp: id.block, ..default() }, @@ -696,7 +695,6 @@ mod tests { spacers: 0, }, premine: 0, - supply: 0, timestamp: id.block, ..default() }, @@ -718,6 +716,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -735,7 +734,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -777,7 +775,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -832,7 +829,7 @@ mod tests { } #[test] - fn etched_rune_open_etching_parameters_are_unset_for_cenotaph() { + fn etched_rune_open_mint_parameters_are_unset_for_cenotaph() { let context = Context::builder().arg("--index-runes").build(); let (txid0, id) = context.etch( @@ -843,15 +840,17 @@ mod tests { output: 0, }], etching: Some(Etching { + premine: Some(u128::MAX), rune: Some(Rune(RUNE)), mint: Some(Mint { + cap: Some(1), deadline: Some(1), limit: Some(1), term: Some(1), }), - divisibility: 1, + divisibility: Some(1), symbol: Some('$'), - spacers: 1, + spacers: Some(1), }), cenotaph: true, ..default() @@ -863,18 +862,17 @@ mod tests { [( id, RuneEntry { - burned: 0, + burned: u128::MAX, divisibility: 1, etching: txid0, mint: None, mints: 0, number: 0, - premine: 0, + premine: u128::MAX, spaced_rune: SpacedRune { rune: Rune(RUNE), spacers: 1, }, - supply: 0, symbol: Some('$'), timestamp: id.block, }, @@ -941,6 +939,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -958,7 +957,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -997,7 +995,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -1019,6 +1016,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -1036,7 +1034,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -1068,7 +1065,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -1096,6 +1092,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -1113,7 +1110,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -1146,7 +1142,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, burned: u128::MAX, ..default() @@ -1169,6 +1164,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -1186,7 +1182,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -1225,7 +1220,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -1253,6 +1247,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -1270,7 +1265,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -1309,7 +1303,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, burned: u128::MAX, timestamp: id.block, ..default() @@ -1333,6 +1326,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -1350,7 +1344,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -1382,7 +1375,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -1410,6 +1402,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -1427,7 +1420,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -1463,7 +1455,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -1485,6 +1476,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -1502,7 +1494,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id0.block, ..default() }, @@ -1525,6 +1516,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE + 1)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -1543,7 +1535,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id0.block, ..default() }, @@ -1557,7 +1548,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id1.block, number: 1, ..default() @@ -1603,7 +1593,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id0.block, ..default() }, @@ -1617,7 +1606,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id1.block, number: 1, ..default() @@ -1647,6 +1635,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -1664,7 +1653,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id0.block, ..default() }, @@ -1687,6 +1675,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE + 1)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -1705,7 +1694,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id0.block, ..default() }, @@ -1719,7 +1707,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id1.block, number: 1, ..default() @@ -1765,7 +1752,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id0.block, ..default() }, @@ -1779,7 +1765,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id1.block, number: 1, ..default() @@ -1832,7 +1817,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id0.block, ..default() }, @@ -1846,7 +1830,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id1.block, number: 1, ..default() @@ -1885,6 +1868,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -1902,7 +1886,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id0.block, ..default() }, @@ -1925,6 +1908,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE + 1)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -1943,7 +1927,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id0.block, ..default() }, @@ -1957,7 +1940,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id1.block, number: 1, ..default() @@ -2021,7 +2003,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id0.block, ..default() }, @@ -2035,7 +2016,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id1.block, number: 1, ..default() @@ -2066,6 +2046,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2083,7 +2064,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -2120,7 +2100,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -2130,7 +2109,7 @@ mod tests { } #[test] - fn rune_rarity_is_assigned_correctly() { + fn multiple_runes_may_be_etched_in_one_block() { let context = Context::builder().arg("--index-runes").build(); let (txid0, id0) = context.etch( @@ -2142,6 +2121,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2158,6 +2138,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE + 1)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2176,7 +2157,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id0.block, ..default() }, @@ -2190,7 +2170,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id1.block, number: 1, ..default() @@ -2231,6 +2210,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2248,7 +2228,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -2297,7 +2276,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -2325,6 +2303,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2342,7 +2321,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id0.block, ..default() }, @@ -2365,6 +2343,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE + 1)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2383,7 +2362,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id0.block, ..default() }, @@ -2397,7 +2375,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id1.block, number: 1, ..default() @@ -2458,7 +2435,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id0.block, ..default() }, @@ -2472,7 +2448,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id1.block, number: 1, ..default() @@ -2511,6 +2486,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX / 2), ..default() }), ..default() @@ -2528,7 +2504,6 @@ mod tests { spacers: 0, }, premine: u128::MAX / 2, - supply: u128::MAX / 2, timestamp: id.block, ..default() }, @@ -2570,7 +2545,6 @@ mod tests { spacers: 0, }, premine: u128::MAX / 2, - supply: u128::MAX / 2, timestamp: id.block, ..default() }, @@ -2598,6 +2572,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2616,7 +2591,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -2638,6 +2612,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2655,7 +2630,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -2684,6 +2658,7 @@ mod tests { ], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2701,7 +2676,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -2723,6 +2697,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2740,7 +2715,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -2774,6 +2748,7 @@ mod tests { ], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2791,7 +2766,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -2837,6 +2811,7 @@ mod tests { ], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2854,7 +2829,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -2881,6 +2855,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(4000), ..default() }), ..default() @@ -2898,7 +2873,6 @@ mod tests { spacers: 0, }, premine: 4000, - supply: 4000, timestamp: id.block, ..default() }, @@ -2932,6 +2906,7 @@ mod tests { ], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2949,7 +2924,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -2982,6 +2956,7 @@ mod tests { ], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2999,7 +2974,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -3029,6 +3003,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -3046,7 +3021,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -3089,7 +3063,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -3126,6 +3099,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -3143,7 +3117,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -3193,7 +3166,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -3230,6 +3202,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -3247,7 +3220,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -3297,7 +3269,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -3334,6 +3305,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -3351,7 +3323,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -3394,7 +3365,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -3431,6 +3401,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -3448,7 +3419,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -3498,7 +3468,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -3535,6 +3504,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -3552,7 +3522,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -3602,7 +3571,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -3654,6 +3622,7 @@ mod tests { etching: Some(Etching { rune: Some(Rune(RUNE)), symbol: Some('$'), + premine: Some(u128::MAX), ..default() }), ..default() @@ -3671,7 +3640,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, symbol: Some('$'), timestamp: id.block, ..default() @@ -3694,6 +3662,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -3711,7 +3680,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -3733,6 +3701,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -3750,7 +3719,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -3793,7 +3761,6 @@ mod tests { spacers: 0, }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() }, @@ -3808,13 +3775,6 @@ mod tests { ); } - #[test] - fn max_limit() { - MAX_LIMIT - .checked_mul(u128::from(u16::MAX) * u128::from(RUNE_COMMIT_INTERVAL) * 365 * 1_000_000_000) - .unwrap(); - } - #[test] fn rune_can_be_minted_without_edict() { let context = Context::builder().arg("--index-runes").build(); @@ -3825,6 +3785,7 @@ mod tests { rune: Some(Rune(RUNE)), mint: Some(Mint { limit: Some(1000), + cap: Some(100), ..default() }), ..default() @@ -3847,6 +3808,7 @@ mod tests { mints: 0, mint: Some(MintEntry { limit: Some(1000), + cap: Some(100), ..default() }), ..default() @@ -3876,6 +3838,7 @@ mod tests { etching: txid0, mint: Some(MintEntry { limit: Some(1000), + cap: Some(100), ..default() }), mints: 1, @@ -3884,7 +3847,6 @@ mod tests { spacers: 0, }, premine: 0, - supply: 1000, timestamp: id.block, ..default() }, @@ -3908,6 +3870,7 @@ mod tests { etching: Some(Etching { rune: Some(Rune(RUNE)), mint: Some(Mint { + cap: Some(100), limit: Some(1000), ..default() }), @@ -3929,10 +3892,10 @@ mod tests { }, timestamp: id.block, premine: 0, - supply: 0, mints: 0, mint: Some(MintEntry { limit: Some(1000), + cap: Some(100), ..default() }), ..default() @@ -3967,6 +3930,7 @@ mod tests { etching: txid0, mint: Some(MintEntry { limit: Some(1000), + cap: Some(100), ..default() }), mints: 1, @@ -3975,7 +3939,6 @@ mod tests { spacers: 0, }, premine: 0, - supply: 1000, timestamp: id.block, ..default() }, @@ -4016,6 +3979,7 @@ mod tests { etching: txid0, mint: Some(MintEntry { limit: Some(1000), + cap: Some(100), ..default() }), mints: 2, @@ -4024,7 +3988,6 @@ mod tests { spacers: 0, }, premine: 0, - supply: 2000, timestamp: id.block, ..default() }, @@ -4076,6 +4039,7 @@ mod tests { etching: txid0, mint: Some(MintEntry { limit: Some(1000), + cap: Some(100), ..default() }), mints: 3, @@ -4084,7 +4048,6 @@ mod tests { spacers: 0, }, premine: 0, - supply: 3000, timestamp: id.block, ..default() }, @@ -4109,7 +4072,7 @@ mod tests { } #[test] - fn open_etchings_can_be_limited_to_term() { + fn open_mints_can_be_limited_to_term() { let context = Context::builder().arg("--index-runes").build(); let (txid0, id) = context.etch( @@ -4118,6 +4081,7 @@ mod tests { rune: Some(Rune(RUNE)), mint: Some(Mint { limit: Some(1000), + cap: Some(100), term: Some(2), ..default() }), @@ -4140,6 +4104,7 @@ mod tests { mint: Some(MintEntry { limit: Some(1000), end: Some(id.block + 2), + cap: Some(100), ..default() }), timestamp: id.block, @@ -4180,10 +4145,10 @@ mod tests { mint: Some(MintEntry { limit: Some(1000), end: Some(id.block + 2), + cap: Some(100), ..default() }), premine: 0, - supply: 1000, timestamp: id.block, mints: 1, ..default() @@ -4227,11 +4192,11 @@ mod tests { spacers: 0, }, premine: 0, - supply: 1000, timestamp: id.block, mint: Some(MintEntry { limit: Some(1000), end: Some(id.block + 2), + cap: Some(100), ..default() }), mints: 1, @@ -4249,7 +4214,7 @@ mod tests { } #[test] - fn open_etchings_with_term_zero_can_be_premined() { + fn open_mints_with_term_zero_can_be_premined() { let context = Context::builder().arg("--index-runes").build(); let (txid, id) = context.etch( @@ -4261,6 +4226,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(1111), mint: Some(Mint { limit: Some(1000), term: Some(0), @@ -4289,7 +4255,6 @@ mod tests { }), timestamp: id.block, premine: 1111, - supply: 1111, ..default() }, )], @@ -4327,7 +4292,6 @@ mod tests { ..default() }), premine: 1111, - supply: 1111, ..default() }, )], @@ -4336,7 +4300,7 @@ mod tests { } #[test] - fn open_etchings_with_end_before_deadline() { + fn open_mints_with_end_before_deadline() { let context = Context::builder().arg("--index-runes").build(); context.mine_blocks(1); @@ -4346,6 +4310,7 @@ mod tests { etching: Some(Etching { rune: Some(Rune(RUNE)), mint: Some(Mint { + cap: Some(2), limit: Some(1000), deadline: Some(12), term: Some(2), @@ -4368,6 +4333,7 @@ mod tests { }, timestamp: 9, mint: Some(MintEntry { + cap: Some(2), deadline: Some(12), end: Some(11), limit: Some(1000), @@ -4401,11 +4367,11 @@ mod tests { spacers: 0, }, premine: 0, - supply: 1000, timestamp: 9, mints: 1, etching: txid0, mint: Some(MintEntry { + cap: Some(2), deadline: Some(12), end: Some(11), limit: Some(1000), @@ -4446,9 +4412,9 @@ mod tests { spacers: 0, }, premine: 0, - supply: 1000, timestamp: 9, mint: Some(MintEntry { + cap: Some(2), limit: Some(1000), deadline: Some(12), end: Some(11), @@ -4468,7 +4434,7 @@ mod tests { } #[test] - fn open_etchings_with_deadline_before_end() { + fn open_mints_with_deadline_before_end() { let context = Context::builder().arg("--index-runes").build(); context.mine_blocks(1); @@ -4478,6 +4444,7 @@ mod tests { etching: Some(Etching { rune: Some(Rune(RUNE)), mint: Some(Mint { + cap: Some(2), limit: Some(1000), deadline: Some(11), term: Some(3), @@ -4500,6 +4467,7 @@ mod tests { }, timestamp: id.block, mint: Some(MintEntry { + cap: Some(2), deadline: Some(11), end: Some(12), limit: Some(1000), @@ -4533,11 +4501,11 @@ mod tests { spacers: 0, }, premine: 0, - supply: 1000, timestamp: id.block, mints: 1, etching: txid0, mint: Some(MintEntry { + cap: Some(2), deadline: Some(11), end: Some(12), limit: Some(1000), @@ -4578,9 +4546,9 @@ mod tests { spacers: 0, }, premine: 0, - supply: 1000, timestamp: id.block, mint: Some(MintEntry { + cap: Some(2), limit: Some(1000), deadline: Some(11), end: Some(12), @@ -4600,7 +4568,7 @@ mod tests { } #[test] - fn open_etchings_can_be_limited_to_deadline() { + fn open_mints_can_be_limited_to_deadline() { let context = Context::builder().arg("--index-runes").build(); let (txid0, id) = context.etch( @@ -4609,6 +4577,7 @@ mod tests { rune: Some(Rune(RUNE)), mint: Some(Mint { limit: Some(1000), + cap: Some(100), deadline: Some(RUNE_COMMIT_INTERVAL + 4), ..default() }), @@ -4632,6 +4601,7 @@ mod tests { mint: Some(MintEntry { deadline: Some(id.block + 2), limit: Some(1000), + cap: Some(100), ..default() }), ..default() @@ -4667,13 +4637,13 @@ mod tests { rune: Rune(RUNE), spacers: 0, }, - supply: 1000, timestamp: id.block, mints: 1, etching: txid0, mint: Some(MintEntry { deadline: Some(id.block + 2), limit: Some(1000), + cap: Some(100), ..default() }), ..default() @@ -4716,14 +4686,103 @@ mod tests { rune: Rune(RUNE), spacers: 0, }, - supply: 1000, timestamp: id.block, mint: Some(MintEntry { limit: Some(1000), deadline: Some(id.block + 2), + cap: Some(100), + ..default() + }), + mints: 1, + ..default() + }, + )], + [( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id, 1000)], + )], + ); + } + + #[test] + fn open_mints_can_be_limited_to_cap() { + let context = Context::builder().arg("--index-runes").build(); + + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + limit: Some(1000), + cap: Some(2), + ..default() + }), + ..default() + }), + ..default() + }, + 1, + ); + + context.assert_runes( + [( + id, + RuneEntry { + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + timestamp: id.block, + mint: Some(MintEntry { + limit: Some(1000), + cap: Some(2), ..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 { + cap: Some(2), + limit: Some(1000), + ..default() + }), ..default() }, )], @@ -4735,10 +4794,122 @@ mod tests { vec![(id, 1000)], )], ); + + let txid2 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(3, 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 { + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + timestamp: id.block, + mint: Some(MintEntry { + limit: Some(1000), + cap: Some(2), + ..default() + }), + mints: 2, + ..default() + }, + )], + [ + ( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id, 1000)], + ), + ( + OutPoint { + txid: txid2, + vout: 0, + }, + vec![(id, 1000)], + ), + ], + ); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(4, 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 { + etching: txid0, + spaced_rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + timestamp: id.block, + mint: Some(MintEntry { + limit: Some(1000), + cap: Some(2), + ..default() + }), + mints: 2, + ..default() + }, + )], + [ + ( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id, 1000)], + ), + ( + OutPoint { + txid: txid2, + vout: 0, + }, + vec![(id, 1000)], + ), + ], + ); } #[test] - fn open_etching_claims_can_use_split() { + fn open_mint_claims_can_use_split() { let context = Context::builder().arg("--index-runes").build(); let (txid0, id) = context.etch( @@ -4747,6 +4918,7 @@ mod tests { rune: Some(Rune(RUNE)), mint: Some(Mint { limit: Some(1000), + cap: Some(100), ..default() }), ..default() @@ -4767,6 +4939,7 @@ mod tests { }, mint: Some(MintEntry { limit: Some(1000), + cap: Some(100), ..default() }), timestamp: id.block, @@ -4805,10 +4978,10 @@ mod tests { rune: Rune(RUNE), spacers: 0, }, - supply: 1000, timestamp: id.block, mint: Some(MintEntry { limit: Some(1000), + cap: Some(100), ..default() }), mints: 1, @@ -4842,6 +5015,7 @@ mod tests { Runestone { etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(2000), mint: Some(Mint { limit: Some(1000), ..default() @@ -4873,7 +5047,6 @@ mod tests { }), timestamp: id.block, premine: 2000, - supply: 2000, ..default() }, )], @@ -4930,8 +5103,10 @@ mod tests { Runestone { etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(2000), mint: Some(Mint { limit: Some(1000), + cap: Some(1), ..default() }), ..default() @@ -4957,11 +5132,11 @@ mod tests { }, mint: Some(MintEntry { limit: Some(1000), + cap: Some(1), ..default() }), timestamp: rune_id.block, premine: 2000, - supply: 2000, mints: 0, ..default() }, @@ -4986,6 +5161,7 @@ mod tests { rune: Some(Rune(RUNE)), mint: Some(Mint { limit: Some(1000), + cap: Some(100), ..default() }), ..default() @@ -5025,10 +5201,10 @@ mod tests { }, mint: Some(MintEntry { limit: Some(1000), + cap: Some(100), ..default() }), timestamp: id.block, - supply: 1000, mints: 1, ..default() }, @@ -5044,7 +5220,7 @@ mod tests { } #[test] - fn multiple_edicts_in_one_transaction_may_claim_open_etching() { + fn multiple_edicts_in_one_transaction_may_claim_open_mint() { let context = Context::builder().arg("--index-runes").build(); let (txid0, id) = context.etch( @@ -5053,6 +5229,7 @@ mod tests { rune: Some(Rune(RUNE)), mint: Some(Mint { limit: Some(1000), + cap: Some(100), ..default() }), ..default() @@ -5073,6 +5250,7 @@ mod tests { }, mint: Some(MintEntry { limit: Some(1000), + cap: Some(100), ..default() }), timestamp: id.block, @@ -5124,10 +5302,10 @@ mod tests { }, mint: Some(MintEntry { limit: Some(1000), + cap: Some(100), ..default() }), timestamp: id.block, - supply: 1000, mints: 1, ..default() }, diff --git a/src/runes/etching.rs b/src/runes/etching.rs index 0d5b9f49ec..9329388b4e 100644 --- a/src/runes/etching.rs +++ b/src/runes/etching.rs @@ -2,9 +2,10 @@ use super::*; #[derive(Default, Serialize, Deserialize, Debug, PartialEq, Copy, Clone, Eq)] pub struct Etching { - pub divisibility: u8, + pub divisibility: Option<u8>, pub mint: Option<Mint>, + pub premine: Option<u128>, pub rune: Option<Rune>, - pub spacers: u32, + pub spacers: Option<u32>, pub symbol: Option<char>, } diff --git a/src/runes/mint.rs b/src/runes/mint.rs index aaacfce349..122714e4db 100644 --- a/src/runes/mint.rs +++ b/src/runes/mint.rs @@ -2,6 +2,7 @@ use super::*; #[derive(Default, Serialize, Deserialize, Debug, PartialEq, Copy, Clone, Eq)] pub struct Mint { + pub cap: Option<u128>, // mint cap pub deadline: Option<u32>, // unix timestamp pub limit: Option<u128>, // claim amount pub term: Option<u32>, // relative block height diff --git a/src/runes/runestone.rs b/src/runes/runestone.rs index 896a87cc92..9741ff583e 100644 --- a/src/runes/runestone.rs +++ b/src/runes/runestone.rs @@ -112,23 +112,23 @@ impl Runestone { (default_output.into_usize() < transaction.output.len()).then_some(default_output) }); - let divisibility = Tag::Divisibility - .take(&mut fields, |[divisibility]| { - let divisibility = u8::try_from(divisibility).ok()?; - (divisibility <= MAX_DIVISIBILITY).then_some(divisibility) - }) - .unwrap_or_default(); + let divisibility = Tag::Divisibility.take(&mut fields, |[divisibility]| { + let divisibility = u8::try_from(divisibility).ok()?; + (divisibility <= MAX_DIVISIBILITY).then_some(divisibility) + }); - let limit = Tag::Limit.take(&mut fields, |[limit]| (limit <= MAX_LIMIT).then_some(limit)); + let limit = Tag::Limit.take(&mut fields, |[limit]| Some(limit)); let rune = Tag::Rune.take(&mut fields, |[rune]| Some(Rune(rune))); - let spacers = Tag::Spacers - .take(&mut fields, |[spacers]| { - let spacers = u32::try_from(spacers).ok()?; - (spacers <= MAX_SPACERS).then_some(spacers) - }) - .unwrap_or_default(); + let cap = Tag::Cap.take(&mut fields, |[cap]| Some(cap)); + + let premine = Tag::Premine.take(&mut fields, |[premine]| Some(premine)); + + let spacers = Tag::Spacers.take(&mut fields, |[spacers]| { + let spacers = u32::try_from(spacers).ok()?; + (spacers <= MAX_SPACERS).then_some(spacers) + }); let symbol = Tag::Symbol.take(&mut fields, |[symbol]| { char::from_u32(u32::try_from(symbol).ok()?) @@ -144,24 +144,34 @@ impl Runestone { let mint = Flag::Mint.take(&mut flags); + let overflow = (|| { + let premine = premine.unwrap_or_default(); + let cap = cap.unwrap_or_default(); + let limit = limit.unwrap_or_default(); + premine.checked_add(cap.checked_mul(limit)?) + })() + .is_none(); + let etching = if etch { Some(Etching { divisibility, - rune, - spacers, - symbol, mint: mint.then_some(Mint { + cap, deadline, limit, term, }), + premine, + rune, + spacers, + symbol, }) } else { None }; Ok(Some(Self { - cenotaph: cenotaph || flags != 0 || fields.keys().any(|tag| tag % 2 == 0), + cenotaph: cenotaph || overflow || flags != 0 || fields.keys().any(|tag| tag % 2 == 0), claim, default_output, edicts, @@ -186,18 +196,22 @@ impl Runestone { Tag::Rune.encode([rune.0], &mut payload); } - if etching.divisibility != 0 { - Tag::Divisibility.encode([etching.divisibility.into()], &mut payload); + if let Some(divisibility) = etching.divisibility { + Tag::Divisibility.encode([divisibility.into()], &mut payload); } - if etching.spacers != 0 { - Tag::Spacers.encode([etching.spacers.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); @@ -210,6 +224,10 @@ impl Runestone { 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); + } } } @@ -806,7 +824,7 @@ mod tests { }], etching: Some(Etching { rune: None, - divisibility: 4, + divisibility: Some(4), ..default() }), ..default() @@ -951,7 +969,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(4)), - divisibility: 5, + divisibility: Some(5), ..default() }), ..default() @@ -1050,7 +1068,7 @@ mod tests { #[test] fn decipher_etching_with_all_etching_tags() { - assert_eq!( + pretty_assert_eq!( decipher(&[ Tag::Flags.into(), Flag::Etch.mask() | Flag::Mint.mask(), @@ -1068,6 +1086,16 @@ mod tests { 2, Tag::Limit.into(), 3, + Tag::Premine.into(), + 8, + Tag::Cap.into(), + 9, + Tag::DefaultOutput.into(), + 0, + Tag::Claim.into(), + 1, + Tag::Claim.into(), + 1, Tag::Body.into(), 1, 1, @@ -1083,15 +1111,19 @@ mod tests { etching: Some(Etching { rune: Some(Rune(4)), mint: Some(Mint { + cap: Some(9), deadline: Some(7), term: Some(2), limit: Some(3), }), - divisibility: 1, + premine: Some(8), + divisibility: Some(1), symbol: Some('a'), - spacers: 5, + spacers: Some(5), }), - ..default() + cenotaph: false, + default_output: Some(0), + claim: Some(RuneId::new(1, 1).unwrap()), }, ); } @@ -1153,7 +1185,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(4)), - divisibility: 1, + divisibility: Some(1), symbol: Some('a'), ..default() }), @@ -1182,7 +1214,10 @@ mod tests { amount: 2, output: 0, }], - etching: Some(Etching::default()), + etching: Some(Etching { + divisibility: Some(0), + ..default() + }), ..default() }, ); @@ -1293,7 +1328,7 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 5, + divisibility: Some(5), ..default() }), ..default() @@ -1408,7 +1443,7 @@ mod tests { case( Vec::new(), Some(Etching { - divisibility: MAX_DIVISIBILITY, + divisibility: Some(MAX_DIVISIBILITY), rune: Some(Rune(0)), ..default() }), @@ -1418,15 +1453,17 @@ mod tests { case( Vec::new(), Some(Etching { - divisibility: MAX_DIVISIBILITY, + divisibility: Some(MAX_DIVISIBILITY), mint: Some(Mint { + cap: None, deadline: Some(10000), limit: Some(1), term: Some(1), }), + premine: None, rune: Some(Rune(0)), symbol: Some('$'), - spacers: 1, + spacers: Some(1), }), 20, ); @@ -1447,7 +1484,7 @@ mod tests { output: 0, }], Some(Etching { - divisibility: MAX_DIVISIBILITY, + divisibility: Some(MAX_DIVISIBILITY), rune: Some(Rune(u128::MAX)), ..default() }), @@ -1461,7 +1498,7 @@ mod tests { output: 0, }], Some(Etching { - divisibility: MAX_DIVISIBILITY, + divisibility: Some(MAX_DIVISIBILITY), rune: Some(Rune(u128::MAX)), ..default() }), @@ -1672,15 +1709,17 @@ mod tests { case( Runestone { etching: Some(Etching { - divisibility: 1, + 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: 6, + spacers: Some(6), }), edicts: vec![ Edict { @@ -1709,12 +1748,16 @@ mod tests { 6, Tag::Symbol.into(), '@'.into(), + Tag::Premine.into(), + 1, Tag::Deadline.into(), 2, Tag::Limit.into(), 3, Tag::Term.into(), 5, + Tag::Cap.into(), + 1, Tag::Claim.into(), 1, Tag::Claim.into(), @@ -1738,11 +1781,12 @@ mod tests { case( Runestone { etching: Some(Etching { - divisibility: 0, + premine: None, + divisibility: None, mint: None, symbol: None, rune: Some(Rune(3)), - spacers: 0, + spacers: None, }), cenotaph: false, ..default() @@ -1753,11 +1797,12 @@ mod tests { case( Runestone { etching: Some(Etching { - divisibility: 0, + premine: None, + divisibility: None, mint: None, symbol: None, rune: None, - spacers: 0, + spacers: None, }), cenotaph: false, ..default() @@ -1853,12 +1898,6 @@ mod tests { assert!(!decipher(&[Tag::Divisibility.into(), u128::MAX]).cenotaph); } - #[test] - fn invalid_limit_produces_cenotaph() { - assert!(decipher(&[Tag::Limit.into(), u128::MAX]).cenotaph); - assert!(decipher(&[Tag::Limit.into(), u128::from(u64::MAX) + 1]).cenotaph); - } - #[test] fn min_and_max_runes_are_not_cenotaphs() { assert!(!decipher(&[Tag::Rune.into(), 0]).cenotaph); @@ -1879,4 +1918,57 @@ mod tests { fn invalid_term_produces_cenotaph() { assert!(decipher(&[Tag::Term.into(), u128::MAX]).cenotaph); } + + #[test] + fn invalid_supply_produces_cenotaph() { + assert!( + !decipher(&[ + Tag::Flags.into(), + Flag::Etch.mask() | Flag::Mint.mask(), + Tag::Cap.into(), + 1, + Tag::Limit.into(), + u128::MAX + ]) + .cenotaph + ); + + assert!( + decipher(&[ + Tag::Flags.into(), + Flag::Etch.mask() | Flag::Mint.mask(), + Tag::Cap.into(), + 2, + Tag::Limit.into(), + u128::MAX + ]) + .cenotaph + ); + + assert!( + decipher(&[ + Tag::Flags.into(), + Flag::Etch.mask() | Flag::Mint.mask(), + Tag::Cap.into(), + 2, + Tag::Limit.into(), + u128::MAX / 2 + 1 + ]) + .cenotaph + ); + + assert!( + decipher(&[ + Tag::Flags.into(), + Flag::Etch.mask() | Flag::Mint.mask(), + Tag::Premine.into(), + 1, + Tag::Cap.into(), + 1, + Tag::Limit.into(), + u128::MAX + ]) + .cenotaph + ); + } } diff --git a/src/runes/tag.rs b/src/runes/tag.rs index cd22fea51e..7dbfcca8f6 100644 --- a/src/runes/tag.rs +++ b/src/runes/tag.rs @@ -10,6 +10,8 @@ pub(super) enum Tag { Deadline = 10, DefaultOutput = 12, Claim = 14, + Cap = 16, + Premine = 18, #[allow(unused)] Cenotaph = 126, diff --git a/src/subcommand/runes.rs b/src/subcommand/runes.rs index a162f471d7..2617755015 100644 --- a/src/subcommand/runes.rs +++ b/src/subcommand/runes.rs @@ -13,7 +13,7 @@ pub struct RuneInfo { pub etching: Txid, pub id: RuneId, pub mint: Option<MintEntry>, - pub mints: u64, + pub mints: u128, pub number: u64, pub premine: u128, pub rune: SpacedRune, @@ -40,7 +40,7 @@ pub(crate) fn run(settings: Settings) -> SubcommandResult { .map( |( id, - RuneEntry { + entry @ RuneEntry { burned, divisibility, etching, @@ -49,7 +49,6 @@ pub(crate) fn run(settings: Settings) -> SubcommandResult { number, premine, spaced_rune, - supply, symbol, timestamp, }, @@ -67,7 +66,7 @@ pub(crate) fn run(settings: Settings) -> SubcommandResult { number, premine, rune: spaced_rune, - supply, + supply: entry.supply(), symbol, timestamp: crate::timestamp(timestamp), tx: id.tx, diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 071654151c..27fb5e0b99 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -2687,6 +2687,7 @@ mod tests { etching: Some(Etching { rune: Some(Rune(RUNE)), symbol: Some('%'), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2706,7 +2707,6 @@ mod tests { spacers: 0 }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, symbol: Some('%'), ..default() @@ -2753,6 +2753,7 @@ mod tests { etching: Some(Etching { rune: Some(rune), symbol: Some('%'), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2777,7 +2778,6 @@ mod tests { etching: txid, spaced_rune: SpacedRune { rune, spacers: 0 }, premine: u128::MAX, - supply: u128::MAX, symbol: Some('%'), timestamp: id.block, ..default() @@ -2865,7 +2865,8 @@ mod tests { etching: Some(Etching { rune: Some(rune), symbol: Some('%'), - spacers: 1, + spacers: Some(1), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2890,7 +2891,6 @@ mod tests { etching: txid, spaced_rune: SpacedRune { rune, spacers: 1 }, premine: u128::MAX, - supply: u128::MAX, symbol: Some('%'), timestamp: id.block, ..default() @@ -2964,6 +2964,7 @@ mod tests { }], etching: Some(Etching { rune: Some(Rune(RUNE)), + premine: Some(u128::MAX), ..default() }), ..default() @@ -2983,7 +2984,6 @@ mod tests { spacers: 0 }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() } @@ -3026,8 +3026,9 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 1, + divisibility: Some(1), rune: Some(rune), + premine: Some(u128::MAX), ..default() }), ..default() @@ -3045,7 +3046,6 @@ mod tests { etching: txid, spaced_rune: SpacedRune { rune, spacers: 0 }, premine: u128::MAX, - supply: u128::MAX, timestamp: id.block, ..default() } diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index a7d69053fc..91e2b3063f 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -188,6 +188,29 @@ impl Inscribe { "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<u128> { + 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()?; diff --git a/src/subcommand/wallet/mint.rs b/src/subcommand/wallet/mint.rs index efefb0f2ad..7982457bf4 100644 --- a/src/subcommand/wallet/mint.rs +++ b/src/subcommand/wallet/mint.rs @@ -39,7 +39,7 @@ impl Mint { let limit = rune_entry .mintable(Height(block_height), block_time) - .map_err(|e| anyhow!(e))?; + .map_err(|err| anyhow!("rune {rune} {err}"))?; let destination = wallet.get_change_address()?; diff --git a/src/templates/rune.rs b/src/templates/rune.rs index 08b0fd05b7..e43c5c2b8b 100644 --- a/src/templates/rune.rs +++ b/src/templates/rune.rs @@ -28,6 +28,7 @@ mod tests { etching: Txid::all_zeros(), mints: 100, mint: Some(MintEntry { + cap: Some(101), end: Some(11), limit: Some(1000000001), deadline: Some(7), @@ -38,7 +39,6 @@ mod tests { rune: Rune(u128::MAX), spacers: 1 }, - supply: 123456789123456789, symbol: Some('%'), timestamp: 0, }, @@ -73,12 +73,16 @@ mod tests { <dd>1.000000001 %</dd> <dt>mints</dt> <dd>100</dd> + <dt>cap</dt> + <dd>101</dd> + <dt>remaining</dt> + <dd>1</dd> <dt>mintable</dt> <dd>true</dd> </dl> </dd> <dt>supply</dt> - <dd>123456789.123456789\u{A0}%</dd> + <dd>100.123456889\u{A0}%</dd> <dt>premine</dt> <dd>0.123456789\u{A0}%</dd> <dt>burned</dt> @@ -112,7 +116,6 @@ mod tests { rune: Rune(u128::MAX), spacers: 1 }, - supply: 123456789123456789, symbol: Some('%'), timestamp: 0, }, @@ -121,32 +124,10 @@ mod tests { parent: None, }, "<h1>B•CGDENLQRQWDSLRUGSNLBTMFIJAV</h1> -<dl> - <dt>number</dt> - <dd>25</dd> - <dt>timestamp</dt> - <dd><time>1970-01-01 00:00:00 UTC</time></dd> - <dt>id</dt> - <dd>10:9</dd> - <dt>etching block</dt> - <dd><a href=/block/10>10</a></dd> - <dt>etching transaction</dt> - <dd>9</dd> +<dl>.* <dt>mint</dt> <dd>no</dd> - <dt>supply</dt> - <dd>123456789.123456789\u{A0}%</dd> - <dt>premine</dt> - <dd>0\u{A0}%</dd> - <dt>burned</dt> - <dd>123456789.123456789\u{A0}%</dd> - <dt>divisibility</dt> - <dd>9</dd> - <dt>symbol</dt> - <dd>%</dd> - <dt>etching</dt> - <dd><a class=monospace href=/tx/0{64}>0{64}</a></dd> -</dl> +.*</dl> " ); } @@ -158,6 +139,7 @@ mod tests { entry: RuneEntry { burned: 123456789123456789, mint: Some(MintEntry { + cap: None, deadline: None, end: None, limit: None, @@ -171,7 +153,6 @@ mod tests { rune: Rune(u128::MAX), spacers: 1 }, - supply: 123456789123456789, symbol: Some('%'), timestamp: 0, }, @@ -180,17 +161,7 @@ mod tests { parent: None, }, "<h1>B•CGDENLQRQWDSLRUGSNLBTMFIJAV</h1> -<dl> - <dt>number</dt> - <dd>25</dd> - <dt>timestamp</dt> - <dd><time>1970-01-01 00:00:00 UTC</time></dd> - <dt>id</dt> - <dd>10:9</dd> - <dt>etching block</dt> - <dd><a href=/block/10>10</a></dd> - <dt>etching transaction</dt> - <dd>9</dd> +<dl>.* <dt>mint</dt> <dd> <dl> @@ -202,23 +173,15 @@ mod tests { <dd>none</dd> <dt>mints</dt> <dd>0</dd> + <dt>cap</dt> + <dd>0</dd> + <dt>remaining</dt> + <dd>0</dd> <dt>mintable</dt> <dd>false</dd> </dl> </dd> - <dt>supply</dt> - <dd>123456789.123456789\u{A0}%</dd> - <dt>premine</dt> - <dd>0\u{A0}%</dd> - <dt>burned</dt> - <dd>123456789.123456789\u{A0}%</dd> - <dt>divisibility</dt> - <dd>9</dd> - <dt>symbol</dt> - <dd>%</dd> - <dt>etching</dt> - <dd><a class=monospace href=/tx/0{64}>0{64}</a></dd> -</dl> +.*</dl> " ); } diff --git a/src/wallet/batch/etching.rs b/src/wallet/batch/etching.rs index f7d2683a9d..9af5f4bcc2 100644 --- a/src/wallet/batch/etching.rs +++ b/src/wallet/batch/etching.rs @@ -7,5 +7,6 @@ pub struct Etching { pub mint: Option<Mint>, pub premine: Decimal, pub rune: SpacedRune, + pub supply: Decimal, pub symbol: char, } diff --git a/src/wallet/batch/mint.rs b/src/wallet/batch/mint.rs index a55c95ad9c..cced5469b7 100644 --- a/src/wallet/batch/mint.rs +++ b/src/wallet/batch/mint.rs @@ -5,5 +5,6 @@ use super::*; pub struct Mint { pub deadline: Option<u32>, pub limit: Decimal, + pub cap: u128, pub term: Option<u32>, } diff --git a/src/wallet/batch/plan.rs b/src/wallet/batch/plan.rs index 88bf8f57fc..e3b6937ca1 100644 --- a/src/wallet/batch/plan.rs +++ b/src/wallet/batch/plan.rs @@ -462,19 +462,21 @@ impl Plan { default_output: None, edicts, etching: Some(runes::Etching { - divisibility: etching.divisibility, + divisibility: (etching.divisibility > 0).then_some(etching.divisibility), mint: etching .mint .map(|mint| -> Result<runes::Mint> { Ok(runes::Mint { + cap: (mint.cap > 0).then_some(mint.cap), deadline: mint.deadline, - term: mint.term, limit: Some(mint.limit.to_amount(etching.divisibility)?), + term: mint.term, }) }) .transpose()?, + premine: (premine > 0).then_some(premine), rune: Some(etching.rune.rune), - spacers: etching.rune.spacers, + spacers: (etching.rune.spacers > 0).then_some(etching.rune.spacers), symbol: Some(etching.symbol), }), }; diff --git a/templates/rune.html b/templates/rune.html index 5d61bc221a..c83430e8bd 100644 --- a/templates/rune.html +++ b/templates/rune.html @@ -33,12 +33,16 @@ <h1>{{ self.entry.spaced_rune }}</h1> %% } <dt>limit</dt> %% if let Some(limit) = mint.limit { - <dd>{{ Pile{ amount: limit, divisibility: self.entry.divisibility, symbol: self.entry.symbol } }}</dd> + <dd>{{ self.entry.pile(limit) }}</dd> %% } else { <dd>none</dd> %% } <dt>mints</dt> <dd>{{ self.entry.mints }}</dd> + <dt>cap</dt> + <dd>{{ mint.cap.unwrap_or_default() }}</dd> + <dt>remaining</dt> + <dd>{{ mint.cap.unwrap_or_default() - self.entry.mints }}</dd> <dt>mintable</dt> <dd>{{ self.mintable }}</dd> </dl> @@ -47,11 +51,11 @@ <h1>{{ self.entry.spaced_rune }}</h1> <dd>no</dd> %% } <dt>supply</dt> - <dd>{{ Pile{ amount: self.entry.supply, divisibility: self.entry.divisibility, symbol: self.entry.symbol } }}</dd> + <dd>{{ self.entry.pile(self.entry.supply()) }}</dd> <dt>premine</dt> - <dd>{{ Pile{ amount: self.entry.premine, divisibility: self.entry.divisibility, symbol: self.entry.symbol } }}</dd> + <dd>{{ self.entry.pile(self.entry.premine) }}</dd> <dt>burned</dt> - <dd>{{ Pile{ amount: self.entry.burned, divisibility: self.entry.divisibility, symbol: self.entry.symbol } }}</dd> + <dd>{{ self.entry.pile(self.entry.burned) }}</dd> <dt>divisibility</dt> <dd>{{ self.entry.divisibility }}</dd> %% if let Some(symbol) = self.entry.symbol { diff --git a/tests/json_api.rs b/tests/json_api.rs index 67719f45e7..3e5cd292be 100644 --- a/tests/json_api.rs +++ b/tests/json_api.rs @@ -557,7 +557,6 @@ fn get_runes() { rune: Rune(RUNE), spacers: 0 }, - supply: 1000, symbol: Some('¢'), timestamp: 11, }, @@ -594,7 +593,6 @@ fn get_runes() { rune: Rune(RUNE), spacers: 0 }, - supply: 1000, symbol: Some('¢'), timestamp: 11, } @@ -613,7 +611,6 @@ fn get_runes() { rune: Rune(RUNE + 1), spacers: 0 }, - supply: 1000, symbol: Some('¢'), timestamp: 19, } @@ -632,7 +629,6 @@ fn get_runes() { rune: Rune(RUNE + 2), spacers: 0 }, - supply: 1000, symbol: Some('¢'), timestamp: 27, } diff --git a/tests/lib.rs b/tests/lib.rs index 67b040260e..ab5c6d4903 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -188,6 +188,7 @@ fn etch( ord_rpc_server, batch::File { etching: Some(batch::Etching { + supply: "1000".parse().unwrap(), divisibility: 0, mint: None, premine: "1000".parse().unwrap(), @@ -250,12 +251,24 @@ fn batch( let batch::Etching { divisibility, + mint, premine, rune, + supply, symbol, - mint, } = 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()) + .unwrap_or_default(); + + assert_eq!(supply, premine + mintable); + } + let mut mint_definition = Vec::<String>::new(); if let Some(mint) = mint { @@ -298,6 +311,10 @@ fn batch( mint_definition.push("<dt>mints</dt>".into()); mint_definition.push("<dd>0</dd>".into()); + mint_definition.push("<dt>cap</dt>".into()); + mint_definition.push(format!("<dd>{}</dd>", mint.cap)); + mint_definition.push("<dt>remaining</dt>".into()); + mint_definition.push(format!("<dd>{}</dd>", mint.cap)); mint_definition.push("<dt>mintable</dt>".into()); mint_definition.push(format!("<dd>{mintable}</dd>")); diff --git a/tests/wallet/balance.rs b/tests/wallet/balance.rs index f3073c785e..d47f6f178e 100644 --- a/tests/wallet/balance.rs +++ b/tests/wallet/balance.rs @@ -108,6 +108,7 @@ fn runic_utxos_are_deducted_from_cardinal() { divisibility: 0, mint: None, premine: "1000".parse().unwrap(), + supply: "1000".parse().unwrap(), rune: SpacedRune { rune, spacers: 1 }, symbol: '¢', }), diff --git a/tests/wallet/inscribe.rs b/tests/wallet/inscribe.rs index 9c1549f353..4bee89b88c 100644 --- a/tests/wallet/inscribe.rs +++ b/tests/wallet/inscribe.rs @@ -2640,6 +2640,7 @@ fn batch_inscribe_can_etch_rune() { rune: Rune(RUNE), spacers: 0, }, + supply: "1000".parse().unwrap(), premine: "1000".parse().unwrap(), symbol: '¢', mint: None, @@ -2708,6 +2709,7 @@ fn etch_existing_rune_error() { rune: Rune(RUNE), spacers: 1, }, + supply: "1000".parse().unwrap(), premine: "1000".parse().unwrap(), symbol: '¢', mint: None, @@ -2752,6 +2754,7 @@ fn etch_reserved_rune_error() { spacers: 0, }, premine: "1000".parse().unwrap(), + supply: "1000".parse().unwrap(), symbol: '¢', mint: None, }), @@ -2794,6 +2797,7 @@ fn etch_sub_minimum_rune_error() { rune: Rune(0), spacers: 0, }, + supply: "1000".parse().unwrap(), premine: "1000".parse().unwrap(), symbol: '¢', mint: None, @@ -2836,6 +2840,7 @@ fn etch_requires_rune_index() { rune: Rune(RUNE), spacers: 0, }, + supply: "1000".parse().unwrap(), premine: "1000".parse().unwrap(), symbol: '¢', mint: None, @@ -2879,6 +2884,7 @@ fn etch_divisibility_over_maximum_error() { rune: Rune(RUNE), spacers: 0, }, + supply: "1000".parse().unwrap(), premine: "1000".parse().unwrap(), symbol: '¢', mint: None, @@ -2897,3 +2903,150 @@ fn etch_divisibility_over_maximum_error() { .expected_exit_code(1) .run_and_extract_stdout(); } + +#[test] +fn etch_mintable_overflow_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: default(), + premine: default(), + symbol: '¢', + mint: Some(batch::Mint { + cap: 2, + term: Some(2), + limit: "340282366920938463463374607431768211455".parse().unwrap(), + deadline: 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: `mint.count` * `mint.limit` over maximum\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn etch_mintable_plus_premine_overflow_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: default(), + premine: "1".parse().unwrap(), + symbol: '¢', + mint: Some(batch::Mint { + cap: 1, + term: Some(2), + limit: "340282366920938463463374607431768211455".parse().unwrap(), + deadline: 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: `premine` + `mint.count` * `mint.limit` over maximum\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn incorrect_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: "1".parse().unwrap(), + premine: "1".parse().unwrap(), + symbol: '¢', + mint: Some(batch::Mint { + cap: 1, + term: Some(2), + limit: "1".parse().unwrap(), + deadline: 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` + `mint.count` * `mint.limit`\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} diff --git a/tests/wallet/mint.rs b/tests/wallet/mint.rs index b6024638ef..7160950ec9 100644 --- a/tests/wallet/mint.rs +++ b/tests/wallet/mint.rs @@ -28,7 +28,9 @@ fn minting_rune_and_fails_if_after_end() { }, premine: "0".parse().unwrap(), symbol: '¢', + supply: "111.1".parse().unwrap(), mint: Some(batch::Mint { + cap: 1, term: Some(2), limit: "111.1".parse().unwrap(), deadline: None, @@ -120,6 +122,7 @@ fn minting_rune_fails_if_not_mintable() { rune: Rune(RUNE), spacers: 0, }, + supply: "1000".parse().unwrap(), premine: "1000".parse().unwrap(), symbol: '¢', mint: None, @@ -165,8 +168,10 @@ fn minting_rune_fails_if_after_deadline() { 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), @@ -246,8 +251,10 @@ fn minting_rune_and_then_sending_works() { spacers: 0, }, premine: "111".parse().unwrap(), + supply: "132".parse().unwrap(), symbol: '¢', mint: Some(batch::Mint { + cap: 1, term: Some(10), limit: "21".parse().unwrap(), deadline: None, diff --git a/tests/wallet/selection.rs b/tests/wallet/selection.rs index 25a4bf17d0..26800030e9 100644 --- a/tests/wallet/selection.rs +++ b/tests/wallet/selection.rs @@ -128,9 +128,11 @@ fn mint_does_not_select_inscription() { spacers: 0, }, premine: "1000".parse().unwrap(), + supply: "2000".parse().unwrap(), symbol: '¢', mint: Some(batch::Mint { deadline: None, + cap: 1, limit: "1000".parse().unwrap(), term: None, }), diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index e67e835059..c02dfa4263 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -886,6 +886,7 @@ fn sending_rune_with_divisibility_works() { divisibility: 1, rune: SpacedRune { rune, spacers: 0 }, premine: "1000".parse().unwrap(), + supply: "1000".parse().unwrap(), symbol: '¢', mint: None, }),